Three bugs rega found on a laptop run of the container (2026-09-29): - /pb/_/ never rendered. admin.mjs proxied with fetch(), which returns the body decompressed, but forwarded PocketBase's Content-Encoding: gzip and Content-Length, so the browser tried to gunzip plain HTML. The proxy now drops both headers (for /pb and /api-prod). - PocketBase's own redirects left the mount: /pb/_ went to /_/, and /pb/_/index.html to http://127.0.0.1:8090/_/ (the internal address). Root-relative and upstream-absolute locations are put back under the mount: /pb/_/. - The research host's /embed-test set no MAP_APP_API_URL, so the maps asked that static host's own /api (404s; dashboard-plus-grid drew 489 nodes, not 1,399). The page now sets it from the manifest's runtime.apiBaseUrl before any bundle loads, as every generated page does. Checked both ways against an upstream that answers like PocketBase (gzipped HTML, redirects to /_/). The old proxy sends a gzip label on a plain body and /_/ locations; the new one sends a decodable body and /pb/_/.
138 lines
7.6 KiB
HTML
138 lines
7.6 KiB
HTML
<!doctype html>
|
|
<!--
|
|
/embed-test on a subdomain app (for example research.<host>/embed-test): proves that EVERY dx-maps plugin built
|
|
for the web embeds as JavaScript (no iframe), straight from this host's published tree (/dx-maps/). admin.mjs
|
|
serves this file on the subdomain server. It is the same test as DX-life's /embed-test page:
|
|
- the list is read at runtime from /dx-maps/manifest.json, never typed here;
|
|
- section 1 is every profile whose stage is "production", section 2 every other one;
|
|
- a profile whose `targets` leaves out "web" is skipped.
|
|
A card mounts its element when it scrolls into view and removes it when it scrolls far away (a browser allows
|
|
only a handful of WebGL contexts at once).
|
|
?only=<bundle>/<profile> (comma-separated, or a bundle name) shows just those cards.
|
|
?lang=<locale> sets the language (Arabic, Persian, Urdu and Hebrew lay out right to left).
|
|
-->
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<meta name="robots" content="noindex">
|
|
<title>Embed test</title>
|
|
<style>
|
|
body { font-family: system-ui, sans-serif; margin: 0 auto; max-width: 1200px; padding: 16px; color: #1f2620; }
|
|
.embed-card { border: 1px solid #d1d4cd; border-radius: 10px; overflow: hidden; margin: 16px 0; }
|
|
.embed-card__head { display: flex; flex-wrap: wrap; gap: 8px 12px; align-items: baseline; padding: 10px 12px; background: #f3f3f1; }
|
|
.embed-card__state { margin-inline-start: auto; font-size: 12px; color: #6d766e; }
|
|
.embed-card__desc { margin: 0; padding: 6px 12px; font-size: 13px; color: #3b463d; }
|
|
.embed-card__error { margin: 0; padding: 6px 12px; color: #a0302a; }
|
|
.embed-card__host { position: relative; height: 560px; }
|
|
.embed-card__host > * { display: block; width: 100%; height: 100%; }
|
|
@media (max-width: 600px) { .embed-card__host { height: 480px; } }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Embed test</h1>
|
|
<p data-embed-status>Reading the manifest…</p>
|
|
<div id="cards"></div>
|
|
<script>
|
|
(async () => {
|
|
const q = new URLSearchParams(location.search)
|
|
const lang = (q.get('lang') || 'en').trim()
|
|
document.documentElement.lang = lang
|
|
if (/^(ar|fa|ur|he)\b/.test(lang)) document.documentElement.dir = 'rtl'
|
|
const base = `${location.origin}/dx-maps`
|
|
const status = document.querySelector('[data-embed-status]')
|
|
const resolveUrl = (ref) => /^(https?:)?\/\//.test(ref) ? ref : ref.startsWith('/') ? new URL(ref, base).href : `${base}/${ref.replace(/^\.\//, '')}`
|
|
const scripts = new Map()
|
|
const loadScript = (src, integrity) => {
|
|
if (!scripts.has(src)) scripts.set(src, new Promise((resolve, reject) => {
|
|
const s = document.createElement('script')
|
|
s.src = src
|
|
if (integrity) { s.integrity = integrity; s.crossOrigin = 'anonymous' }
|
|
s.onload = () => resolve()
|
|
s.onerror = () => reject(new Error(`the script did not load: ${src}`))
|
|
document.head.appendChild(s)
|
|
}))
|
|
return scripts.get(src)
|
|
}
|
|
let manifest
|
|
try {
|
|
const r = await fetch(`${base}/manifest.json`, { cache: 'no-cache' })
|
|
if (!r.ok) throw new Error(`HTTP ${r.status}`)
|
|
manifest = await r.json()
|
|
// The data API the maps read (people groups, statistics). Every generated page and copy-paste snippet sets it
|
|
// before its bundle loads; this page builds its cards itself, so it sets it from the manifest. Without it the
|
|
// bundles asked this host's own /api, which the research host doesn't serve (404s, fewer nodes drawn).
|
|
if (!window.MAP_APP_API_URL && manifest.runtime && manifest.runtime.apiBaseUrl) window.MAP_APP_API_URL = manifest.runtime.apiBaseUrl
|
|
} catch (e) {
|
|
status.textContent = `The manifest did not load from ${base}/manifest.json (${e.message}).`
|
|
return
|
|
}
|
|
const only = (q.get('only') || '').split(',').map((s) => s.trim()).filter(Boolean)
|
|
const out = []
|
|
for (const b of manifest.bundles || []) {
|
|
const defined = b.elements || []
|
|
for (const p of b.profiles || []) {
|
|
const targets = p.targets || b.targets
|
|
if (Array.isArray(targets) && !targets.includes('web')) continue
|
|
const key = `${b.bundle}/${p.profile}`
|
|
if (only.length && !only.includes(key) && !only.includes(b.bundle)) continue
|
|
// the tag the bundle actually defines (a profile's own element name can name one it does not)
|
|
const tag = p.element && (!defined.length || defined.includes(p.element)) ? p.element : defined[0] || p.element
|
|
out.push({ key, bundle: b.bundle, profile: p.profile, tag, script: resolveUrl(b.scriptUrl || `${b.bundle}/${b.bundle}.js`),
|
|
integrity: b.integrity || '', description: p.description || '', production: (p.stage || b.stage) === 'production', state: 'idle' })
|
|
}
|
|
}
|
|
const cards = [...out.filter((c) => c.production), ...out.filter((c) => !c.production)]
|
|
const prod = out.filter((c) => c.production).length
|
|
status.textContent = `${out.length} web plugins from ${base}/manifest.json (${prod} production, ${out.length - prod} other), each embedded as JavaScript.`
|
|
|
|
const root = document.getElementById('cards')
|
|
const el = (tag, attrs = {}, text) => { const n = document.createElement(tag); for (const [k, v] of Object.entries(attrs)) n.setAttribute(k, v); if (text) n.textContent = text; return n }
|
|
cards.forEach((c, i) => {
|
|
if (i === 0 && c.production) root.appendChild(el('h2', { 'data-embed-section': 'production' }, '1. Production'))
|
|
if (!c.production && (i === 0 || cards[i - 1].production)) root.appendChild(el('h2', { 'data-embed-section': 'other' }, '2. Every other web plugin'))
|
|
const sec = el('section', { class: 'embed-card', 'data-card': c.key, 'data-state': 'idle', 'data-production': String(c.production), 'data-locale': '' })
|
|
const head = el('header', { class: 'embed-card__head' })
|
|
head.append(el('strong', {}, `${c.bundle} / ${c.profile}`), el('code', {}, `<${c.tag}>`), el('span', { class: 'embed-card__state' }, 'idle'))
|
|
sec.appendChild(head)
|
|
if (c.description) sec.appendChild(el('p', { class: 'embed-card__desc' }, c.description))
|
|
c.host = el('div', { class: 'embed-card__host' })
|
|
sec.appendChild(c.host)
|
|
c.section = sec
|
|
root.appendChild(sec)
|
|
})
|
|
const setState = (c, s, err) => {
|
|
c.state = s; c.section.dataset.state = s; c.section.querySelector('.embed-card__state').textContent = s
|
|
if (err) { const p = el('p', { class: 'embed-card__error', role: 'alert' }, err); c.section.insertBefore(p, c.host) }
|
|
}
|
|
const mount = async (c) => {
|
|
if (c.state === 'mounted' || c.state === 'loading') return
|
|
setState(c, 'loading')
|
|
try {
|
|
await loadScript(c.script, c.integrity)
|
|
if (!customElements.get(c.tag)) throw new Error(`<${c.tag}> was not defined by its bundle`)
|
|
const node = document.createElement(c.tag)
|
|
// shells (the Research Gallery, Plugin Studio) find their child maps through the tree's manifest
|
|
node.setAttribute('tree-base', base)
|
|
node.setAttribute('profile-config', JSON.stringify({ profile: c.profile, assetsBase: `${base}/assets`, locale: lang, provider: 'maplibre' }))
|
|
node.setAttribute('language', lang)
|
|
c.section.dataset.locale = lang
|
|
c.host.replaceChildren(node)
|
|
setState(c, 'mounted')
|
|
} catch (e) {
|
|
setState(c, 'error', String(e && e.message || e))
|
|
}
|
|
}
|
|
const unmount = (c) => { if (c.state !== 'mounted') return; c.host.replaceChildren(); setState(c, 'idle') }
|
|
const observer = new IntersectionObserver((entries) => {
|
|
for (const en of entries) {
|
|
const c = cards.find((x) => x.key === en.target.dataset.card)
|
|
if (!c) continue
|
|
if (en.isIntersecting) mount(c); else unmount(c)
|
|
}
|
|
}, { rootMargin: '600px 0px' })
|
|
cards.forEach((c) => observer.observe(c.section))
|
|
})()
|
|
</script>
|
|
</body>
|
|
</html>
|