Research Gallery: a host that asks for maplibre gets maplibre in every tab

The shell forwarded tk, tokenUrl, locale and treeBase to each nested map,
but not the engine. So <doxa-maps-spa provider="maplibre"> still loaded
mapbox-gl and Mapbox styles. On a keyless host, that is the embed failing.

The shell now forwards `provider` in each child's profile-config, validated
to the two known engines. That fixed the simple map tabs, but not the
composites: they mount their inner maps without passing the provider on.
Measured with one tab per clean page: Big Picture made 19 api.mapbox.com
requests, Regions 17, UPG-100 15, Simple Research 14. So when a host asks
for an engine, the shell also sets window.MAP_PROVIDER (only if the page
hasn't). That is rung 3 of every map's own ladder, so it reaches maps
nested at any depth. After it, all four composites make 0 Mapbox requests.

check:gallery-embed:maplibre runs the cross-origin gate with a keyless
provider and fails on any api.mapbox.com request, attributed to the tab
that was open. Before this change, js mode failed with 20 URLs. It has one
named, printed exemption: upg-100-list.vue pins a token-free mapbox-gl.css.
That belongs to the upg100 plugin and is routed there.
This commit is contained in:
doxa-maps-88655c70 2026-09-24 19:57:23 +00:00
parent 2693c6ffa3
commit 3a4f065ec5
9 changed files with 66 additions and 12 deletions

View file

@ -28,7 +28,7 @@
* party tile/glyph hiccups are reported, not failed — they are not this repo's to fix).
*
* PRECONDITION: a built tree in public/doxa-maps (this starts its own servers; no dev server).
* Usage: node internal/check-research-gallery-embed.mjs [--mode js|iframe] [--only id,id]
* Usage: node internal/check-research-gallery-embed.mjs [--mode js|iframe] [--only id,id] [--provider maplibre]
* Exit: 0 pass · 1 fail · 2 SKIP (no built tree / no browser)
*/
import { createServer } from 'node:http'
@ -42,6 +42,17 @@ const argv = process.argv.slice(2)
const argOf = (k, d) => { const i = argv.indexOf(k); return i >= 0 && argv[i + 1] ? argv[i + 1] : d }
const MODES = argOf('--mode', '') ? [argOf('--mode', '')] : ['js', 'iframe']
const ONLY = argOf('--only', '') ? argOf('--only', '').split(',') : null
// --provider maplibre: the KEYLESS embed. The host asks for maplibre (attribute in js mode, ?provider=
// in iframe mode) and every tab must still paint — with ZERO requests to api.mapbox.com. That is the
// promise a keyless partner embed depends on, and the shell once broke it by not forwarding the engine.
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)',
}
// ★ 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
// tree (caught by its own negative control, 2026-09-24): spawn failed with EADDRINUSE silently and
@ -63,8 +74,8 @@ try { ({ chromium } = await import('playwright')) } catch {
let mode = 'js'
const hostPage = () => {
const embed = mode === 'iframe'
? `<iframe src="${TREE}/doxa-maps-spa/doxa-maps-spa/index.html" style="width:100%;height:760px;border:0"></iframe>`
: `<script src="${TREE}/doxa-maps-spa/doxa-maps-spa.js"></script><doxa-maps-spa></doxa-maps-spa>`
? `<iframe src="${TREE}/doxa-maps-spa/doxa-maps-spa/index.html${PROVIDER ? '?provider=' + PROVIDER : ''}" style="width:100%;height:760px;border:0"></iframe>`
: `<script src="${TREE}/doxa-maps-spa/doxa-maps-spa.js"></script><doxa-maps-spa${PROVIDER ? ` provider="${PROVIDER}"` : ''}></doxa-maps-spa>`
return `<!doctype html><html><head><meta charset=utf-8><title>host</title>
<style>body{margin:0} header,footer{height:60px;background:#eee}</style></head>
<body><header id=hdr>HOST HEADER</header><main style="max-width:1200px;margin:0 auto">${embed}</main><footer>HOST FOOTER</footer></body></html>`
@ -80,7 +91,14 @@ const checked = {} // mode -> tabs actually driven; printed, and 0 is a FAIL (
try {
for (mode of MODES) {
const page = await browser.newPage({ viewport: { width: 1400, height: 900 } })
const own = new Set(), foreign = new Set()
const own = new Set(), foreign = new Set(), mapbox = new Set()
let current = '(page load)'
const mapboxBy = {} // tab id (the one open when the request fired) -> distinct mapbox URLs
page.on('request', q => {
if (!/^https:\/\/api\.mapbox\.com\//.test(q.url())) return
const u = q.url().split('?')[0]
mapbox.add(u); (mapboxBy[current] = mapboxBy[current] || new Set()).add(u)
})
const note = (url, why) => (url.startsWith(`http://127.0.0.1:${TREE_PORT}/`) ? own : foreign).add(why + ' ' + url.split('?')[0])
page.on('response', r => { if (r.status() >= 400) note(r.url(), String(r.status())) })
page.on('requestfailed', q => { const e = (q.failure() && q.failure().errorText) || ''; if (!/ABORTED/.test(e)) note(q.url(), e) })
@ -102,6 +120,7 @@ try {
const tabs = await frame.evaluate(() => document.querySelector('doxa-maps-spa')._instance.proxy.tabs.map(t => ({ id: t.id, discovered: !!t.discovered })))
for (const { id, discovered } of tabs) {
if (ONLY && !ONLY.includes(id)) continue
current = id
await frame.evaluate(id => document.querySelector('doxa-maps-spa')._instance.proxy.onSelect(id), id)
let v
for (let k = 0; k < 50; k++) {
@ -125,6 +144,17 @@ try {
if (!ok) fails.push(`[${mode}] ${id}: ${why}`)
}
for (const f of own) { console.log(`FAIL [${mode}] tree request failed: ${f}`); fails.push(`[${mode}] ${f}`) }
if (PROVIDER === 'maplibre') {
for (const u of [...mapbox]) if (MAPBOX_KNOWN[u]) { console.log(`KNOWN [${mode}] ${u} — owner: ${MAPBOX_KNOWN[u]}`); mapbox.delete(u); for (const k in mapboxBy) { mapboxBy[k].delete(u); if (!mapboxBy[k].size) delete mapboxBy[k] } }
const ok = mapbox.size === 0
console.log(`${ok ? 'PASS' : 'FAIL'} [${mode}] provider=maplibre made ${mapbox.size} request(s) to api.mapbox.com`)
if (!ok) {
for (const [tab, urls] of Object.entries(mapboxBy)) {
console.log(` while ${tab} was open: ${urls.size} — ${[...urls].slice(0, 3).join(' , ')}`)
fails.push(`[${mode}] provider=maplibre: ${tab} hit api.mapbox.com (${urls.size} distinct URL(s))`)
}
}
}
for (const f of foreign) console.log(`note [${mode}] third-party request failed (not gated): ${f}`)
await page.close()
}

View file

@ -150,6 +150,7 @@
"check:gallery-embed": "node internal/check-research-gallery-embed.mjs # the cap-5 Research Gallery must embed on a FOREIGN ORIGIN (a stand-in host page on localhost, the built tree on 127.0.0.1) in BOTH modes, bare <script>+<doxa-maps-spa> and <iframe>, with the layout inside the host box and every curated tab painting a map. The summary line prints the tab count PER MODE, and a mode that checked zero tabs is a FAIL. One mode only: check:gallery-embed:js or check:gallery-embed:iframe. Starts its own servers on OS-assigned ports; needs only a built tree. Exit 2 = SKIP (no built tree / no playwright).",
"check:gallery-embed:js": "node internal/check-research-gallery-embed.mjs --mode js",
"check:gallery-embed:iframe": "node internal/check-research-gallery-embed.mjs --mode iframe",
"check:gallery-embed:maplibre": "node internal/check-research-gallery-embed.mjs --provider maplibre",
"check:spa-tab-order": "node internal/check-spa-tab-order.mjs # the cap-5 SPA must open on Big Picture and keep the Driver's order (Big Picture, UPG-100, Prayer Progress, Gospel Resources) — asserted as PRESENCE + tab-1 + RELATIVE order on the RENDERED rail, because the rail gained three sections and now renders 12 rows with others between them. ⚠ NOT WAVE-WIRED: it FAILS rather than skips when the SPA is unreachable, deliberately (\"a gate that cannot reach its subject has not passed, it has not run\") — correct standalone, but gates:post runs on checkouts with no server. Needs `bun run serve` on 4173.",
"check:backlog-status": "node internal/check-backlog-status-enum.mjs",
"check:backlog-refs": "node internal/check-backlog-refs.mjs # a card may not depend on a card that does not exist. A dangling depends_on can never be done, so the card can never become ready and nothing says so — it reads as ordinary blocked work forever. FAILS only for depends_on on an OPEN card; a dangling parent or a stale link on a done card is REPORTED, because failing on filing debt teaches people to delete the reference rather than restore the card. Reads all three phase backlogs — references legitimately cross phases. SKIPs when _qa-cycle is absent.",

View file

@ -39,6 +39,7 @@ Add `?tab=<id>` to the `src` to open a specific map.
| `tab` | device default (Big Picture on desktop, Simple Research on phones) | Tab id to open first. Changing it later switches tabs. `?tab=` on the page URL works too. |
| `language` | parent page / saved choice | UI language pushed to every tab (`en`, `es`, `fr`, …). |
| `api-base-url` | manifest `runtime.apiBaseUrl` | Data API. Only applied when the page has not already set `window.MAP_APP_API_URL`. |
| `provider` | each map's own default (Mapbox) | Map engine for every nested map: `maplibre` needs no token and makes no Mapbox requests; `mapbox`. Forwarded to each map and, when the page has not set `window.MAP_PROVIDER`, set there too so maps nested inside composites follow. In the iframe embed use `?provider=maplibre`. |
| `tk` | manifest `runtime.token` | Mapbox public token. Only fills `window.MAP_TOKEN` when the page has not set one. |
| `token-url` | none | Token endpoint forwarded to each map. |
| `tree-base` | derived from the script URL | Pin the tree root explicitly (needed only when the script is re-hosted apart from its tree). |
@ -74,5 +75,6 @@ Both headers are **required** on a host other than the one serving the tree:
## Prove it
```sh
bun run check:gallery-embed # serves the built tree + a foreign host page, drives every tab in both modes
bun run check:gallery-embed # serves the built tree + a foreign host page, drives every tab in both modes
bun run check:gallery-embed:maplibre # same, keyless: provider=maplibre must make zero api.mapbox.com requests
```

View file

@ -737,6 +737,9 @@ export default defineComponent({
// the tree's own manifest.json (its `runtime` block), so a bare two-line embed needs no config.
// See EMBED.md beside this file.
apiBaseUrl: { type: String, default: '' },
// The map engine for every nested map: 'maplibre' (keyless) or 'mapbox'. Also accepted as
// profile-config.provider. Unset = each map's own ladder (?provider= → window.MAP_PROVIDER → mapbox).
provider: { type: String, default: '' },
// Which tab opens first, by tab id (e.g. "upg-100", "gospel-resources"). Lets a host page deep-
// link into one map of the gallery. Falls back to ?tab= on the page URL, then the device default.
tab: { type: String, default: '' }
@ -905,6 +908,11 @@ export default defineComponent({
const fromProp = (this.language || this.cfg.language || this.cfg.locale || this.cfg.lang || '').trim()
return (fromProp || this.locale || '').split('-')[0].toLowerCase()
},
// The engine a HOST asked for ('' = none; each map keeps its own ladder).
effectiveProvider() {
const p = String(this.provider || this.cfg.provider || '').trim().toLowerCase()
return (p === 'maplibre' || p === 'mapbox') ? p : ''
},
activeKey() {
const t = this.tabs[this.activeIndex]
return t ? t.id : ''
@ -1239,6 +1247,11 @@ export default defineComponent({
const tk = this.tk || this.cfg.tk || this.runtime.token
if (tk) pc.tk = tk
if (this.tokenUrl || this.cfg.tokenUrl) pc.tokenUrl = this.tokenUrl || this.cfg.tokenUrl
// ★ AND THE ENGINE. resolveMapProvider's ladder starts at profile-config.provider, and a child
// reads only the config written HERE — so a gallery asked for maplibre mounted children that
// resolved mapbox (bundler-2 measured 19 api.mapbox.com requests for a maplibre request on the
// studio, the same hole). Validated to the two known engines so a typo cannot pick a third.
if (this.effectiveProvider) pc.provider = this.effectiveProvider
// PASS THE TREE BASE DOWN — a nested cap-5 shell (or any element that fetches the manifest)
// cannot guess it, and guessing is what produced the original 404 class.
if (this.base) pc.treeBase = this.base
@ -1259,6 +1272,14 @@ export default defineComponent({
if (api && !window.MAP_APP_API_URL) window.MAP_APP_API_URL = api.replace(/\/+$/, '')
const tk = this.tk || this.cfg.tk || this.runtime.token
if (tk && !window.MAP_TOKEN) window.MAP_TOKEN = tk
// window.MAP_PROVIDER — the engine, when a host explicitly asked this gallery for one.
// childConfig forwards it as profile-config.provider, which the simple map elements honour,
// but a COMPOSITE mounts its inner maps without passing its own config's provider on.
// Measured 2026-09-24 with provider="maplibre", one tab per clean page: Prayer Progress and
// All People Groups made 0 api.mapbox.com requests, while Big Picture (19), Regions (17),
// UPG-100 (15) and Simple Research (14) loaded mapbox-gl and Mapbox styles. The page global
// is rung 3 of every map's resolveMapProvider ladder, so it reaches maps nested at any depth.
if (this.effectiveProvider && !window.MAP_PROVIDER) window.MAP_PROVIDER = this.effectiveProvider
},
// Mount a tab's REAL custom element: inject the bundle's script once, wait for the tag to be
// registered, then create it in the pane's empty slot. This is the ONE path for every tab.

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 <doxa-maps-spa> custom element. -->
<script src="../../doxa-maps-spa.js" integrity="sha384-fmp37RdhrkxuguGQ5MmwnakDu0So2YCJSYj8Bca867HoKCT1497o9z3K/wlHDlxP" crossorigin="anonymous"></script>
<script src="../../doxa-maps-spa.js" integrity="sha384-Cmdw0USq0X4/+OshXBayXKy4U5ZBD0bR8YR/yaR/CZ76CYEMpb4bymgfug+5AGyU" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -26,7 +26,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <doxa-maps-spa> custom element. -->
<script src="../doxa-maps-spa.js" integrity="sha384-fmp37RdhrkxuguGQ5MmwnakDu0So2YCJSYj8Bca867HoKCT1497o9z3K/wlHDlxP" crossorigin="anonymous"></script>
<script src="../doxa-maps-spa.js" integrity="sha384-Cmdw0USq0X4/+OshXBayXKy4U5ZBD0bR8YR/yaR/CZ76CYEMpb4bymgfug+5AGyU" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -302,7 +302,7 @@
"bundle": "doxa-maps-spa",
"scriptUrl": "./doxa-maps-spa/doxa-maps-spa.js",
"output": "doxa-maps/doxa-maps-spa/doxa-maps-spa.js",
"integrity": "sha384-fmp37RdhrkxuguGQ5MmwnakDu0So2YCJSYj8Bca867HoKCT1497o9z3K/wlHDlxP",
"integrity": "sha384-Cmdw0USq0X4/+OshXBayXKy4U5ZBD0bR8YR/yaR/CZ76CYEMpb4bymgfug+5AGyU",
"elements": [
"doxa-maps-spa"
],
@ -327,7 +327,7 @@
"scriptUrl": "./doxa-maps-spa/doxa-maps-spa.js",
"embed": {
"iframe": "<iframe src=\"./doxa-maps-spa/doxa-maps-spa/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=\"./doxa-maps-spa/doxa-maps-spa.js\" integrity=\"sha384-fmp37RdhrkxuguGQ5MmwnakDu0So2YCJSYj8Bca867HoKCT1497o9z3K/wlHDlxP\" crossorigin=\"anonymous\"></script>\n<doxa-maps-spa profile-config='{\"profile\":\"doxa-maps-spa\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-maps-spa>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-maps-spa/doxa-maps-spa.js\" integrity=\"sha384-Cmdw0USq0X4/+OshXBayXKy4U5ZBD0bR8YR/yaR/CZ76CYEMpb4bymgfug+5AGyU\" crossorigin=\"anonymous\"></script>\n<doxa-maps-spa profile-config='{\"profile\":\"doxa-maps-spa\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-maps-spa>"
},
"instances": [
{
@ -804,7 +804,7 @@
"plugin-studio"
],
"composes": [
"bp-big-picture",
"dashboard-plus-grid",
"people-groups-grid",
"doxa-research-map",
"doxa-simple-map",