check:composite-freshness found stale on main: key-cities template-bundle. Rebuilt, tree + SRI regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
258 lines
16 KiB
HTML
258 lines
16 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<!-- THIS PAGE IS OURS. The only thing that knows a document belongs to the plugin is the page
|
|
the generator writes, so it says so here. library/i18n reads it before reflecting an RTL
|
|
locale on <html dir>; nothing else sets it, so a PARTNER page defaults to "not ours" and
|
|
is never touched. Absence must mean restraint — an embedded plugin flipping a partner's
|
|
whole <html> because a reader followed a ?lang=ar link is the bug this prevents. -->
|
|
<script>window.MAP_OWNS_PAGE = true;</script>
|
|
<title>country-posters-mfe · country-posters</title>
|
|
<link rel="icon" href="data:,">
|
|
<!-- NO STATIC ENGINE TAGS (2026-09-25). This leaf used to hard-link mapbox-gl.css, the
|
|
geocoder css, mapbox-gl.js and the geocoder js from Mapbox's CDN, chosen at BUILD time —
|
|
so a partner embedding it with ?provider=maplibre still sent every visitor's IP to Mapbox
|
|
(4 requests per leaf, measured). The provider is chosen at RUNTIME (profile-config.provider
|
|
→ ?provider= → window.MAP_PROVIDER → auto), and the element's own runtime loaders load
|
|
exactly that engine: ensureEngineScript / ensureEngineCSS (useMapInstance) and the
|
|
geocoder's on-demand loader — the path composite leaves already run on. Gate:
|
|
check:leaf-engine-per-provider (0 Mapbox requests under maplibre; Mapbox unchanged). -->
|
|
<style>
|
|
/* SCROLL-PAGE leaf (cap-4 nested-elements-mfe composite: a StackLayout scroll-stack). The DOCUMENT
|
|
owns the scroll — the element takes its content's NATURAL height and grows the page, so a reader
|
|
scrolls PAST one stacked section down to the next (BUG-DASHBOARD-CANT-SCROLL-DOWN). This is the
|
|
OPPOSITE of the fixed-viewport map/single-element leaf below: NO position:fixed, NO inset:0,
|
|
height:auto so the page can grow. See StackLayout.vue's "THE LAYOUT OWNS THE SCROLL" contract. */
|
|
html { height: 100%; }
|
|
html, body { margin: 0; background: #0a0e15; }
|
|
body { min-height: 100%; overflow-y: auto; }
|
|
/* MIN-HEIGHT IN VIEWPORT UNITS, NOT PERCENT (BUG-UPG100-NESTED-ELEMENTS-ZERO-HEIGHT).
|
|
min-height:100% here resolved against BODY's computed height, which is auto (body only has
|
|
min-height:100%, never a definite height) — and a percentage of auto collapses to 0. Any
|
|
element whose own chrome is position:absolute / height:100% (upg-100-mfe -> .pane-col -> .pane)
|
|
then measured 0 and the leaf looked empty. 100dvh depends on NO ancestor, keeps the page
|
|
growable (it is a MIN), and is correct on mobile with dynamic toolbars. */
|
|
#slot, country-posters-map { position: static; inset: auto; width: 100%; min-height: 100dvh; display: block; }
|
|
#status { position: fixed; inset: 0; z-index: 2; display: grid; place-items: center;
|
|
padding: 24px; text-align: center; color: #9aa7b8;
|
|
font: 13px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
#status.err { color: #f87171; }
|
|
|
|
/* ── PRINT, BOTH LEAF SHAPES ────────────────────────────────────────────────────────────
|
|
Two things every generated leaf got wrong on paper, because no generated page has ever
|
|
carried an @media print rule (measured: 0 across the tree).
|
|
|
|
The background is #0a0e15 — a dark page, printed. Browsers drop background colours by
|
|
default, but a reader who ticks "Background graphics" to get the MAP also gets the page
|
|
flooded black, and that is the reader who most wants the printout.
|
|
|
|
#status is the LOADING OVERLAY: position:fixed, inset:0, over everything. On paper it is
|
|
a full sheet reading "loading…" above the content that did in fact load. It is hidden
|
|
only while it is NOT an error — an error still prints, because a blank sheet with the
|
|
explanation suppressed is worse than a sheet that says what went wrong. */
|
|
@media print {
|
|
html, body { height: auto; background: #fff; }
|
|
#status:not(.err) { display: none; }
|
|
/* ★ RELEASE THE FIXED HOST, MEASURED (rega, RQ-RGPRINT on a035408e2). The leaf pins both the
|
|
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
|
|
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
|
|
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its
|
|
top screenful and silently dropping the rest — the dashboard printed its stats and its map,
|
|
then blank to the bottom of the sheet.
|
|
min-height, not a released height alone: a full-bleed map leaf takes its SIZE from this rule,
|
|
so height:auto with nothing else can leave the element a zero-height box and print an empty
|
|
page — a worse failure than the one being fixed, and the discriminator is not isMap
|
|
(region-dashboards is isMap:true and is a scrolling dashboard). The floor keeps a definite
|
|
box for the map case while letting a taller document grow onto page two. */
|
|
#slot, country-posters-map { position: static; inset: auto; width: 100%; height: auto; min-height: 14cm; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="slot"></div>
|
|
<div id="status">loading token…</div>
|
|
|
|
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
|
<!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
|
|
<script src="../country-posters.js" integrity="sha384-P3EAFnzOcpWCYHXhA9BwrMuURM9KQTNF2/0lCy8OZ+b5BCJpdl/0JmI35/34HIu+" crossorigin="anonymous"></script>
|
|
|
|
<script>
|
|
// Mapbox token resolution — the A3 ladder, in precedence order:
|
|
// 1. prop — a token hardcoded into this page's profile-config (config.tk)
|
|
// 2. ?tk= URL param — token carried in the shared link
|
|
// 3. baked token — opt-in literal token inlined at build time (build-config.token)
|
|
// A token from rungs 1-3 is used DIRECTLY (no network). Otherwise a token
|
|
// *endpoint* is resolved and fetched:
|
|
// 4. window.MAP_TOKEN_URL override 5. configured TOKEN_SRC URL 6. same-origin /api/maps/token
|
|
// TOKEN_SRC is treated as DATA, never eval'd: the literal "window.MAP_TOKEN_URL"
|
|
// means "read that global"; anything else is used as a URL — so both a global
|
|
// name and a plain URL are safe (no SyntaxError footgun).
|
|
(function () {
|
|
var slot = document.getElementById('slot');
|
|
var status = document.getElementById('status');
|
|
var BASE_CONFIG = {"pinHint":false,"profile":"country-posters-mfe","dataSource":"pray-tools","instanceId":"country-posters-country-posters-mfe","assetsBase":"../../assets"};
|
|
// MapLibre map pages AND shell pages (app-shell / nested-mfe-spa) need NO token:
|
|
// the ladder below is skipped entirely and the element mounts immediately.
|
|
// (A leaf whose engine is MapLibre at RUNTIME joins them — see PROVIDER below.)
|
|
var TOKEN_FREE = false;
|
|
// A token-free COMPOSITE still nests plugins, and a nested Mapbox map reads window.MAP_TOKEN
|
|
// rather than this page's profile-config. With no baked token (the public copy), it must get
|
|
// one from ?tk= or a pasted token, or be asked for one — see the rungs below.
|
|
var NESTS_MAPS = true;
|
|
var TOKEN_SRC = "window.MAP_TOKEN_URL";
|
|
var BAKED_TOKEN = "";
|
|
// Expose the baked token as window.MAP_TOKEN so a BAKED composite (a compiled element that mounts
|
|
// a map component directly, with no ProfileLoader/profile-config wrapper) still finds a token —
|
|
// useMapInstance falls back to window.MAP_TOKEN. No effect when BAKED_TOKEN is empty.
|
|
// A token the HOST set before this page ran outranks the baked one (measured: it used to lose).
|
|
var HOST_TOKEN = (typeof window.MAP_TOKEN === 'string' && window.MAP_TOKEN) || '';
|
|
if (BAKED_TOKEN && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = BAKED_TOKEN; }
|
|
var DEFAULT_TOKEN_URL = '/api/maps/token';
|
|
|
|
function fail(msg) { if (status) { status.className = 'err'; status.textContent = '⚠ ' + msg; } }
|
|
// No token anywhere on the ladder → ask ONCE, store locally, remount. No dead ends.
|
|
function askToken() {
|
|
if (!status) return;
|
|
status.className = 'err';
|
|
status.innerHTML = 'map needs a Mapbox token — ' +
|
|
'<input id="tkIn" type="password" placeholder="paste pk.… (stays in this browser)" ' +
|
|
'style="font:12px ui-monospace,monospace;padding:6px 8px;border-radius:6px;border:1px solid #223047;background:#0e1623;color:#eef2f7;width:min(320px,70vw)"> ' +
|
|
'<button id="tkGo" style="font:700 11px ui-monospace,monospace;padding:7px 12px;border-radius:999px;border:0;cursor:pointer;background:linear-gradient(90deg,#46d4ff,#b9a6f5);color:#0a0e15">use it</button>';
|
|
var go = document.getElementById('tkGo');
|
|
if (go) go.addEventListener('click', function () {
|
|
var v = (document.getElementById('tkIn').value || '').trim();
|
|
if (!v) return;
|
|
try { localStorage.setItem('MAPS_STAGING_TK', v); } catch (e) {}
|
|
mount(v);
|
|
});
|
|
}
|
|
/* ★ RELEASE THE LOAD BACKDROP WHEN THE ELEMENT MOUNTS (BUG-GOSPEL-MAP-BLACK-RECTANGLE).
|
|
html/body carry background:#0a0e15 so the light #status text (#9aa7b8) is readable while the
|
|
bundle loads. That backdrop is LOAD-STATE CHROME, but it outlived its moment: any gap the
|
|
mounted element does not cover shows it through, because every layer above body is
|
|
transparent. On gospel-resources/resources-map that produced a 352x126 DARK RECTANGLE between
|
|
the legend card and the detail panel — MEASURED rgb(8,12,18), i.e. the page background, NOT an
|
|
unpainted canvas as first supposed. Clearing it here (the one place we already know the
|
|
element is up) is a one-shot style write — no resize, no repaint loop. Keep it a one-shot
|
|
write: do NOT turn this into a per-frame or per-event handler (a resize loop strobes the screen). */
|
|
function done() {
|
|
if (status && status.parentNode) { status.parentNode.removeChild(status); }
|
|
document.documentElement.style.background = '#f3f3f1';
|
|
document.body.style.background = '#f3f3f1';
|
|
}
|
|
|
|
function mount(token) {
|
|
if (!token && !TOKEN_FREE) { return askToken(); }
|
|
// Nested maps (a composite's children) read window.MAP_TOKEN, not this element's config.
|
|
if (token && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = token; }
|
|
try {
|
|
var cfg = token ? Object.assign({}, BASE_CONFIG, { tk: token }) : BASE_CONFIG;
|
|
var el = document.createElement("country-posters-map");
|
|
el.setAttribute('profile-config', JSON.stringify(cfg));
|
|
slot.innerHTML = '';
|
|
slot.appendChild(el);
|
|
done();
|
|
} catch (e) { fail(e && e.message ? e.message : String(e)); }
|
|
}
|
|
|
|
function qp(name) {
|
|
try { return new URLSearchParams(location.search).get(name) || ''; }
|
|
catch (e) { return ''; }
|
|
}
|
|
|
|
function extractToken(data) {
|
|
if (typeof data === 'string') return data.trim();
|
|
if (data && typeof data === 'object') return data.token || data.tk || data.accessToken || '';
|
|
return '';
|
|
}
|
|
|
|
// Rungs 1-3b: an actual token → mount directly, no fetch.
|
|
// 2b = localStorage: the paste-once store shared with the staging pages
|
|
// (MAPS_STAGING_TK) — paste a token ONCE anywhere in the tree, every page uses it.
|
|
var stored = '';
|
|
try { stored = localStorage.getItem('MAPS_STAGING_TK') || ''; } catch (e) {}
|
|
var urlTk = qp('tk');
|
|
if (urlTk) { try { localStorage.setItem('MAPS_STAGING_TK', urlTk); } catch (e) {} }
|
|
var directToken = (BASE_CONFIG && BASE_CONFIG.tk) || urlTk || stored || HOST_TOKEN || BAKED_TOKEN;
|
|
// PROVIDER — the same ladder as resolveMapProvider (library/composables/useMapProvider.js):
|
|
// profile-config.provider → ?provider= → window.MAP_PROVIDER → 'auto'. An explicit engine wins.
|
|
// 'auto' = Mapbox when a token is available (a rung above, a host-set window.MAP_TOKEN, or a token
|
|
// endpoint to ask), otherwise MapLibre, keyless: no token prompt, no request to Mapbox. The
|
|
// element decides the same way from the token this page hands it, so the two cannot disagree.
|
|
var ASKED = String((BASE_CONFIG && BASE_CONFIG.provider) || qp('provider') || window.MAP_PROVIDER || '').toLowerCase().trim();
|
|
var EXPLICIT = (ASKED === 'mapbox' || ASKED === 'maplibre') ? ASKED : '';
|
|
var HAS_TOKEN = !!(directToken || (typeof window.MAP_TOKEN === 'string' && window.MAP_TOKEN));
|
|
var HAS_ENDPOINT = !!(window.MAP_TOKEN_URL || (TOKEN_SRC && TOKEN_SRC !== 'window.MAP_TOKEN_URL'));
|
|
if (EXPLICIT === 'maplibre' || (!EXPLICIT && !HAS_TOKEN && !HAS_ENDPOINT)) { TOKEN_FREE = true; }
|
|
if (TOKEN_FREE) {
|
|
if (directToken && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = directToken; }
|
|
mount('');
|
|
// A token-free composite with no token anywhere: its content renders, but its nested maps
|
|
// cannot. Ask once; the token is stored for the whole tree and the page reloads with it.
|
|
// Only when Mapbox was ASKED for: under 'auto' the nested maps draw with MapLibre instead.
|
|
if (NESTS_MAPS && !directToken && EXPLICIT === 'mapbox') {
|
|
var bar = document.createElement('div');
|
|
bar.id = 'status';
|
|
bar.setAttribute('style', 'position:fixed;left:12px;right:12px;bottom:12px;z-index:2147483000;padding:10px 12px;border-radius:10px;background:#0e1623;color:#eef2f7;font:13px system-ui,sans-serif;box-shadow:0 6px 24px rgba(0,0,0,.25)');
|
|
document.body.appendChild(bar);
|
|
status = bar;
|
|
askToken();
|
|
var goBtn = document.getElementById('tkGo');
|
|
if (goBtn) goBtn.addEventListener('click', function () {
|
|
var v = (document.getElementById('tkIn').value || '').trim();
|
|
if (v) { location.reload(); }
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
if (directToken) { return mount(directToken); }
|
|
|
|
// Rungs 4-6: resolve the token ENDPOINT candidates and try them IN ORDER — the first
|
|
// one that answers with a token wins. Order: window.MAP_TOKEN_URL override → the
|
|
// configured TOKEN_SRC (an ABSOLUTE url, so a tree hosted on a CDN away from the API
|
|
// still finds it) → same-origin /api/maps/token (dev, or the site hosting both).
|
|
// A single hard-coded pick used to kill the page when its one url 404'd or was
|
|
// blocked by CORS; a shared link must work wherever the tree is hosted.
|
|
var candidates = [];
|
|
function addCandidate(u) { if (u && candidates.indexOf(u) === -1) candidates.push(u); }
|
|
addCandidate(window.MAP_TOKEN_URL);
|
|
addCandidate(TOKEN_SRC && TOKEN_SRC !== 'window.MAP_TOKEN_URL' ? TOKEN_SRC : '');
|
|
addCandidate(DEFAULT_TOKEN_URL);
|
|
|
|
function fetchToken(i, errors) {
|
|
if (i >= candidates.length) {
|
|
// Every endpoint failed. Under 'auto' that means no token: draw with MapLibre, keyless.
|
|
if (!EXPLICIT) { TOKEN_FREE = true; return mount(''); }
|
|
// Mapbox was asked for: say which endpoints failed, and still offer the paste-once box.
|
|
askToken();
|
|
if (status) {
|
|
var d = document.createElement('div');
|
|
d.textContent = '⚠ Token fetch failed from ' + candidates.join(', ') + ' — set window.MAP_TOKEN_URL or pass ?tk=. (' + errors.join(' | ') + ')';
|
|
status.insertBefore(d, status.firstChild);
|
|
}
|
|
return;
|
|
}
|
|
var url = candidates[i];
|
|
return fetch(url)
|
|
.then(function (r) {
|
|
if (!r.ok) throw new Error(url + ' → HTTP ' + r.status);
|
|
var ct = r.headers.get('content-type') || '';
|
|
return ct.indexOf('json') !== -1 ? r.json() : r.text();
|
|
})
|
|
.then(function (data) {
|
|
var tk = extractToken(data);
|
|
if (!tk) throw new Error(url + ' → no token in response');
|
|
mount(tk);
|
|
})
|
|
.catch(function (e) {
|
|
errors.push(e && e.message ? e.message : String(e));
|
|
return fetchToken(i + 1, errors);
|
|
});
|
|
}
|
|
fetchToken(0, []);
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|