upg-100: no mapbox stylesheet under maplibre, and assert the preview exists
Some checks are pending
build-gate / build (push) Waiting to run

Two things, both found by other agents' reports rather than by my own gates.

rega found upg-100-list.vue:1464 injecting mapbox-gl.css v3.3.0 into the shadow
root unconditionally, so a keyless maplibre embed still pulled a stylesheet from
api.mapbox.com. Token-free and harmless to render, but wrong-engine leakage on a
page whose point is having no Mapbox dependency. Gated on isMaplibre, which
already existed at line 119.

This commit is ATOMIC by design: the fix, the rebuilt bundle, and the deletion
of rega's MAPBOX_KNOWN exemption land together. Either half alone leaves a
window — delete first and main red-gates on its own known bug; fix first and a
stale exemption sits there hiding the next one.

Proven before deleting, because the exemption is keyed on a URL STRING while the
defect is a network REQUEST: the built bundle still contains the v3.3.0 literal
(the constant remains; the guard stops the fetch), so a source-grep gate would
have been misled. rega's gate measures requests, and it reported
"provider=maplibre made 0 request(s) to api.mapbox.com" in BOTH js and iframe
modes with no KNOWN line printed at all — i.e. the exemption was already dead
code. Re-proven after deletion: still 0 requests, still PASS.

The leaf-head preloads are explicitly NOT covered and the reason is recorded in
the gate: mapbox-gl.css v3.24.0 and mapbox-gl-geocoder v5.0.0 are a different
version and a different owner (generate-tree, bundler-1's lane), already tracked
separately.

Second: the Driver's report had two halves — "it is supposed to show the
previews" and the print options overlapping. I fixed and gated the overlap and
left the first unasserted. The over-preview check was written as `if (img)`,
which means a card with NO preview satisfies it perfectly: nothing can overlap
an image that is not there.

The gate now counts previews. Mutation-tested by aborting every previews/**
request: the product degrades well — all 27 cards swap to a "poster not
generated yet" placeholder with no broken-image icons and no <img> at all, which
is precisely why an image-health check cannot see it. So the fallback itself is
asserted, and all-cards-placeholder now fails.

A first version of that assertion was wrong and the reason is recorded in the
code: it called 18 lazy-loaded previews broken. Every img carries loading=lazy,
the first unpainted one sat 3926px down an 844px viewport, and no HTTP response
failed. Deferred is not broken; it now judges only in-viewport images.
This commit is contained in:
doxa-maps-88655c70 2026-09-24 20:12:27 +00:00
parent 460ccb564b
commit 25e3edec8f
9 changed files with 138 additions and 81 deletions

View file

@ -55,6 +55,7 @@ async function measure(width) {
const cards = all.filter((e) => (e.className||'').toString().split(' ').includes('card')).filter(vis)
const boxes = cards.map((c) => ({ c, b: c.getBoundingClientRect() }))
let intrusions = 0, overflowing = 0, worst = 0, coveringPreview = 0, worstY = 0
let previewsPainted = 0, previewsBroken = 0, noPreview = 0
for (const { c, b } of boxes) {
const row = c.querySelector('.pfs')
if (!row) continue
@ -68,7 +69,24 @@ async function measure(width) {
// 35px -> 78px, and because it sits at bottom:-14px the extra height pushed its TOP from
// 216px up to 173px in a 238px card: over the preview image and the card's outline.
// Horizontal clean, card visibly broken, gate green.
// COUNT THE PREVIEW, do not merely guard on it. An if-guard protects this loop from a
// placeholder card — and also means a card with NO preview passes the over-preview assertion
// perfectly. The driver reported BOTH a missing-previews symptom AND the overlap;
// measuring only the overlap leaves the first unasserted.
// NOTE: no backticks in this block — it lives inside a JS template literal.
const img = c.querySelector('img')
// ONLY JUDGE WHAT IS IN VIEW. Every preview carries loading=lazy, so a card below the fold
// legitimately has naturalWidth 0 — measured at 390px: 8 of 27 painted, the first unpainted
// one 3926px down an 844px viewport, and ZERO failing HTTP responses. A first version of this
// assertion called that 18 broken images. Deferred is not broken; only an in-viewport image
// that never painted is.
if (!img) noPreview++
else {
const ib2 = img.getBoundingClientRect()
const inView = ib2.top < window.innerHeight && ib2.bottom > 0
if (img.complete && img.naturalWidth > 0) previewsPainted++
else if (inView) previewsBroken++
}
if (img) {
const ib = img.getBoundingClientRect()
if (ib.height > 2 && rb.top < ib.bottom - 1) {
@ -87,6 +105,7 @@ async function measure(width) {
const tiny = ctrls.filter((e) => { const b = e.getBoundingClientRect(); return b.width < 18 || b.height < 10 })
return { cards: cards.length, rows: cards.filter((c) => c.querySelector('.pfs')).length,
overflowing, worst, intrusions, coveringPreview, worstY,
previewsPainted, previewsBroken, noPreview,
controls: ctrls.length, tiny: tiny.length }
})()`)
await page.close()
@ -107,6 +126,8 @@ for (const r of results) {
+ ` · overflow ${r.overflowing}${r.worst ? ` (${r.worst}px)` : ''}`
+ ` · neighbour ${r.intrusions}`
+ ` · over-preview ${r.coveringPreview}${r.worstY ? ` (${r.worstY}px)` : ''}`
+ ` · previews ${r.previewsPainted} painted`
+ `${r.previewsBroken ? `/${r.previewsBroken} BROKEN` : ''}${r.noPreview ? `/${r.noPreview} none` : ''}`
+ ` · ${r.controls} control(s), ${r.tiny} too small`)
}
@ -125,6 +146,27 @@ for (const r of results) {
}
// The row belongs BELOW the preview. Driver-reported 2026-09-24: "the download print options are
// not showing up under the blue outline, they are overlapping where the blue outline would be".
// THE PREVIEW MUST ACTUALLY BE THERE. A card with no image passes every geometry assertion
// above trivially — nothing can overlap what does not render. Driver 2026-09-24: "it is supposed
// to show the previews".
if (r.previewsBroken) {
fails.push(`${at}: ${r.previewsBroken} preview image(s) failed to load (naturalWidth 0) — `
+ 'a broken preview is not something the geometry checks can see')
}
// EVERY card falling back to the placeholder means the previews are gone. MEASURED by aborting
// every previews/** request: the product degrades WELL — all 27 cards swap to a .ph placeholder
// reading "poster not generated yet", with no broken-image icons and no <img> left at all. That
// is correct behaviour and exactly why it is invisible to an image-health check: previewsBroken
// stays 0 because there is nothing left to be broken. Assert the FALLBACK instead.
if (r.cards && r.noPreview === r.cards) {
fails.push(`${at}: all ${r.cards} card(s) fell back to the "poster not generated yet" `
+ 'placeholder — no preview rendered anywhere, and the geometry assertions above are vacuous '
+ 'without one (they cannot overlap an image that is not there)')
}
if (r.previewsPainted === 0 && r.cards && r.noPreview !== r.cards) {
fails.push(`${at}: NOT ONE preview image painted across ${r.cards} card(s) — the geometry `
+ 'assertions above are vacuous without them')
}
if (r.coveringPreview) {
fails.push(`${at}: ${r.coveringPreview} print row(s) COVER the preview image `
+ `(top sits ${r.worstY}px above the image's bottom edge) — growing the row taller to fit it `

View file

@ -49,9 +49,18 @@ const PROVIDER = argOf('--provider', '')
// KNOWN, ROUTED exemptions from the zero-Mapbox rule — each names its owner. Printed on every run so
// it cannot become invisible; DELETE the entry when the owner lands the fix (the gate then enforces it).
const MAPBOX_KNOWN = {
// my-upg-100-list/upg-100-list.vue:1464 injects a pinned Mapbox stylesheet regardless of engine.
// Token-free static CSS (no tiles/styles/auth), routed to upg100-plugin via dx-shepherd 2026-09-24.
'https://api.mapbox.com/mapbox-gl-js/v3.3.0/mapbox-gl.css': 'upg100-plugin (upg-100-list.vue pins mapbox-gl.css)',
// EMPTY ON PURPOSE. The one entry here — my-upg-100-list/upg-100-list.vue:1464 pinning
// mapbox-gl.css v3.3.0 regardless of engine — was fixed and the exemption deleted in the SAME
// COMMIT as the fix, so main never had a window where the gate red-flagged its own known bug and
// never had a window where a stale exemption hid a live one. Proven before deletion: this gate
// reported "provider=maplibre made 0 request(s) to api.mapbox.com" in BOTH js and iframe modes,
// with no KNOWN line printed at all.
//
// ⚠ NOT COVERED BY THIS MAP, and deliberately so: the generated leaf HEAD still preloads
// mapbox-gl.css v3.24.0 and mapbox-gl-geocoder v5.0.0. Different versions, different owner —
// that is generate-tree (bundler-1's lane), tracked as
// BUG-LEAF-PAGES-PRELOAD-MAPBOX-EVEN-UNDER-MAPLIBRE. This gate embeds the GALLERY, which does not
// load those leaves, which is why it reads zero today.
}
// ★ FREE PORTS, ASKED OF THE OS — never fixed ones. A stray server left on a fixed port by an
// earlier crashed run answered in place of the tree under test, and this gate PASSED a known-broken

View file

@ -1458,8 +1458,14 @@ onMounted(async () => {
// custom MapToolbar buttons never needed mapbox ctrl CSS, so this profile
// skipped it until the shared GeocoderComponent (a real mapbox ctrl) arrived.
// Mirrors src/composables/useMapInstance.js ensureMapboxCSS (idempotent).
// ⚠ MAPBOX ONLY. This ran unconditionally, so a keyless maplibre embed still pulled a
// stylesheet from api.mapbox.com — token-free and harmless to render, but wrong-engine
// leakage on a page whose whole point is having no Mapbox dependency. It was the last
// mapbox-under-maplibre reference in this profile, carried as a named exemption in the
// maplibre-embed gate; guarding it lets that exemption be deleted and enforced instead.
// MapLibre ships its own ctrl CSS via ensureEngineScript, so nothing is lost.
try {
const shadow = mapEl.value && mapEl.value.getRootNode()
const shadow = !isMaplibre && mapEl.value && mapEl.value.getRootNode()
if (shadow instanceof ShadowRoot) {
const MAPBOX_CSS = 'https://api.mapbox.com/mapbox-gl-js/v3.3.0/mapbox-gl.css'
if (!shadow.querySelector(`link[href="${MAPBOX_CSS}"]`)) {

File diff suppressed because one or more lines are too long

View file

@ -681,7 +681,7 @@
"bundle": "my-upg-100-list",
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"output": "doxa-maps/my-upg-100-list/my-upg-100-list.js",
"integrity": "sha384-/KD0HoAzJJzY6I+NIzT7od3PI0JIlzs7v8DLjTaeRQFYjbePUfnO3xDeNy3Q9yAT",
"integrity": "sha384-eEbOnVD6pMXOvGXN1VAnX8J3UfTAVruDCirw1585wIGTS+LwN/gCvupPPecye4yf",
"elements": [
"my-upg-100-list-map"
],
@ -701,7 +701,7 @@
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"embed": {
"iframe": "<iframe src=\"./my-upg-100-list/poster-wall/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-/KD0HoAzJJzY6I+NIzT7od3PI0JIlzs7v8DLjTaeRQFYjbePUfnO3xDeNy3Q9yAT\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-eEbOnVD6pMXOvGXN1VAnX8J3UfTAVruDCirw1585wIGTS+LwN/gCvupPPecye4yf\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
}
},
{
@ -715,7 +715,7 @@
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"embed": {
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-/KD0HoAzJJzY6I+NIzT7od3PI0JIlzs7v8DLjTaeRQFYjbePUfnO3xDeNy3Q9yAT\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-eEbOnVD6pMXOvGXN1VAnX8J3UfTAVruDCirw1585wIGTS+LwN/gCvupPPecye4yf\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
}
},
{
@ -729,7 +729,7 @@
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"embed": {
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-/KD0HoAzJJzY6I+NIzT7od3PI0JIlzs7v8DLjTaeRQFYjbePUfnO3xDeNy3Q9yAT\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-eEbOnVD6pMXOvGXN1VAnX8J3UfTAVruDCirw1585wIGTS+LwN/gCvupPPecye4yf\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
}
}
]

File diff suppressed because one or more lines are too long

View file

@ -26,7 +26,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
<script src="../my-upg-100-list.js" integrity="sha384-/KD0HoAzJJzY6I+NIzT7od3PI0JIlzs7v8DLjTaeRQFYjbePUfnO3xDeNy3Q9yAT" crossorigin="anonymous"></script>
<script src="../my-upg-100-list.js" integrity="sha384-eEbOnVD6pMXOvGXN1VAnX8J3UfTAVruDCirw1585wIGTS+LwN/gCvupPPecye4yf" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
<script src="../my-upg-100-list.js" integrity="sha384-/KD0HoAzJJzY6I+NIzT7od3PI0JIlzs7v8DLjTaeRQFYjbePUfnO3xDeNy3Q9yAT" crossorigin="anonymous"></script>
<script src="../my-upg-100-list.js" integrity="sha384-eEbOnVD6pMXOvGXN1VAnX8J3UfTAVruDCirw1585wIGTS+LwN/gCvupPPecye4yf" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -40,7 +40,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
<script src="../my-upg-100-list.js" integrity="sha384-/KD0HoAzJJzY6I+NIzT7od3PI0JIlzs7v8DLjTaeRQFYjbePUfnO3xDeNy3Q9yAT" crossorigin="anonymous"></script>
<script src="../my-upg-100-list.js" integrity="sha384-eEbOnVD6pMXOvGXN1VAnX8J3UfTAVruDCirw1585wIGTS+LwN/gCvupPPecye4yf" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order: