Compare commits
1 commit
doxa-maps-
...
laptop/emb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48b7d0cb90 |
6 changed files with 475 additions and 0 deletions
194
embed-harness/HANDOFF-dx-maps-12h.md
Normal file
194
embed-harness/HANDOFF-dx-maps-12h.md
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
# Brief: finish dx-maps in one 12-hour run
|
||||
|
||||
**You are a long-running agent. Your job: get dx-maps ready to hand over to the DX team.** The Driver has about two days before handoff. dx-maps ships first; DX Base comes after it and is **not** your job.
|
||||
|
||||
"Done" means:
|
||||
- every production map works;
|
||||
- embedding works on the **two** DX Life branches;
|
||||
- the known bugs below are fixed;
|
||||
- a vibe coder can clone the repo, run `bun run dev`, and use it.
|
||||
|
||||
**Where this kit lives:** branch `laptop/embed-harness` of the PRIVATE repo `doxa-life/doxa-maps-cdn`, folder `embed-harness/`. It is never merged. `doxa-life/doxa-maps` is PUBLIC, so agent instructions don't go there.
|
||||
|
||||
Written 2026-09-29 by the laptop agent after testing on the Driver's laptop. Anything marked **VERIFIED** was measured there. Anything marked **DRIVER ASK** is a requirement the Driver stated but no one has checked in code yet.
|
||||
|
||||
---
|
||||
|
||||
## 0. Rules (read first)
|
||||
|
||||
- **Land on `development`. Never push to `main`.** `main` is production, and only the Driver merges into it. Work lands through the team's one lock, on `development`.
|
||||
- **Repos:** `https://git.nuxtbase.app/doxa-life/doxa-maps` (dx-maps, **public**; this is your work, landed on `development` or your own branch, never `main`) and `doxa-life/doxa-maps-cdn` (DX Life; its branches are `research-gallery-drop-in` and `research-gallery-subdomain`). `doxa-life/doxa-life` is a read-only mirror, so don't use it. `doxa-open/doxa-maps` is the public mirror and only has `main`.
|
||||
- **Credentials:** never put a token in a URL, a log, or a commit. Use git's credential helper.
|
||||
- **Browsers:** use Playwright with **Firefox** or bundled Chromium. **Never use Google Chrome.** Headless Firefox is already installed (`playwright firefox v1532`).
|
||||
- **Fix causes, not symptoms.** Don't add patches that create new bugs. Measure before and after every fix. The Driver wants the app fast and snappy, with small bundles and memory use kept down.
|
||||
- **Report failures exactly:** give the command and its output. Never work around a problem silently.
|
||||
- **One DX Life dev server uses 1–2.5 GB of RAM.** Don't start more than two at once.
|
||||
- **Keep the repo's own gates green.** Examples: `check:one-palette`, `check:synced-css`, `check:poster-print-fallback`, `check:no-eval`, `check:i18n-treeshake`. Run the full `bun run build` gate set before you land anything.
|
||||
|
||||
---
|
||||
|
||||
## 1. What is already VERIFIED (don't redo it)
|
||||
|
||||
Tested on dx-maps `development` at `17231bff`, DX Life `research-gallery-drop-in` at `5112851`, and `research-gallery-subdomain` at `ee1ad40`.
|
||||
|
||||
| What | Result |
|
||||
|---|---|
|
||||
| **CDN branch** (`research-gallery-cdn`) | **Dropped by the Driver.** Its embed URLs are fixed as absolute addresses at build time, so they differ between laptop and cloud. Don't spend time on it. |
|
||||
| `research-gallery-drop-in` | Checker **PASS 7/7**. Home, `/research/`, `/adopt` and `/pray` draw maps with 0 page errors. **The Driver confirmed it by eye.** Maps were dropped in with the repo's own `internal/sync-dropin.mjs`. |
|
||||
| *(heads moved since)* | Both branches have newer commits since that test: drop-in `a35daff`, subdomain `2269925` (country-map fix, laptop notes). **Re-run the checker on those heads first.** |
|
||||
| `research-gallery-subdomain` | Checker **PASS 7/7**, served from `http://research.localhost:4610`, a real subdomain-shaped origin. `/regions` redirects (301) to the gallery's Regions tab. **The Driver confirmed "all the maps embedded properly, no issues."** |
|
||||
| Local dev server following a remote branch | Works. `DX_SYNC_REMOTE=origin` and `DX_SYNC_BRANCH=development` in `.env.local` fast-forward the checkout every 5 s, and the browser reloads. The Driver has used this with Rudder on the laptop. |
|
||||
|
||||
The 7 production profiles are:
|
||||
- `cities-internet/connectivity-map`
|
||||
- `dashboard-plus-grid/dashboard-plus-grid`
|
||||
- `doxa-simple-map/doxa-simple-map`
|
||||
- `gospel-resources/gospel-dashboard`
|
||||
- `my-upg-100-list/upg-100-mfe`
|
||||
- `regions-dashboard/regions-mfe`
|
||||
- `research-map-plus-grid/research-map-plus-grid`
|
||||
|
||||
---
|
||||
|
||||
## 1b. NEW, the Driver's third branch: `research-gallery-plugin-studio` (doesn't exist yet)
|
||||
|
||||
Final branch set: **drop-in**, **subdomain**, and **plugin-studio**. CDN is dropped.
|
||||
|
||||
- **drop-in:** simplest, one server; the maps come from the build-config drop-in step (D2). No Plugin Studio, no PocketBase.
|
||||
- **subdomain:** the research gallery runs on `research.` as its own site.
|
||||
- **plugin-studio (the Driver's favourite)** runs the **Plugin Studio publicly on the `research.` subdomain**:
|
||||
- **Watches two branches:**
|
||||
- `development`: a live dev server that hot-reloads whatever cloud agents push. This is the one-table version of the "roundtable" answer.
|
||||
- `main`: when main changes, the maps are rebuilt and served from the same subdomain. That is the single embed URL for DX Life and partner sites, which update without a DX Life rebuild.
|
||||
- **PocketBase (experimental):** it comes with it for plugin users, APIs and data. Until DX Base provides OIDC, access is username + password, set by whoever deploys it.
|
||||
- **Deploys like Railway:** it subscribes to the repo and rebuilds on push, with a railpack file so it runs on Railway, DigitalOcean, Coolify or a plain VPS.
|
||||
- **Proof:** DX Life embeds the production maps from that subdomain, and `research.` shows the studio. Optionally, add one tiny demo plugin that uses PocketBase users.
|
||||
- Create it on `doxa-maps-cdn` (DX Life side) plus whatever dx-maps needs on its own branch.
|
||||
- **Your job:** scope it and report what's needed. Build it only after D1 and D2 are done, and don't let it block them.
|
||||
|
||||
## 2. Defects found (VERIFIED), in priority order
|
||||
|
||||
### D1. Poster export returns 404 for 350 of 378 posters
|
||||
|
||||
- **Cause:** `data/posters/` has 378 `.webp` images but only 28 `.html` sources (the zone posters). The build config ships only the sources (`"posterSources": true` → `.html` only) and deliberately leaves out the 41 MB of `.webp` (`bundlePosters: false`, `internal/generate-tree.mjs:107-130, 1815-1860`).
|
||||
- **What breaks:** export tries `<assetsBase>/posters/<id>.webp`, which is missing, then `<assetsBase>/posters/<id>.html`, which doesn't exist for these, and throws `poster not found (404)` (`library/composables/usePosterPrints.js:116`, `library/components/poster/PosterWall.vue:387`).
|
||||
- **Which posters:** 176 `city-*` and 174 `country-*`.
|
||||
- **On a Nuxt dev host** (drop-in), the missing `.webp` returns a **200 HTML page** rather than a 404. So the fetch "succeeds" and produces a broken image. Guard against that by checking `content-type` starts with `image/` before accepting the image.
|
||||
- **Proven fix direction:** copying the `.webp` into the served tree made export work on both branches.
|
||||
- **Recommended fix:** generate `.html` poster sources for the 350 city and country posters, matching how the 28 zone posters already work. That keeps the tree small and works offline.
|
||||
- **Alternative:** `"bundlePosters": true` (+41 MB, tree about 70 MB). A push was once rejected at that size.
|
||||
- **Also fix:** the content-type guard above, and a gate asserting that every poster id a map can request has a shipped `.webp` **or** `.html`.
|
||||
|
||||
### D2. The drop-in destination is stale
|
||||
|
||||
- `vite.doxa-maps-build-config.json` → `"dropin": { "dest": "../doxa-life/public/embed/doxa-maps-build" }`. The site now only redirects that path. Change it to `"../doxa-life/public"`, because the tree lands at `public/dx-maps/`, which is what `research-gallery-drop-in` loads.
|
||||
- The drop-in step (`node internal/sync-dropin.mjs --from-config --dest <doxa-life>/public`) has **no `bun run` script and no README entry.** Add `"dropin": "node internal/sync-dropin.mjs --from-config"` and document it. **This is the Driver's "simplest vibe-coder path":** change one path in the build config, build, and the maps appear in DX Life.
|
||||
- VERIFIED: it copied 853 files, and `research-gallery-drop-in` passed 7/7.
|
||||
|
||||
### D3. Absolute gallery URL on the subdomain branch (onion readiness, lower priority)
|
||||
|
||||
- `research-gallery-subdomain` gets the gallery from an **absolute** `NUXT_PUBLIC_RESEARCH_GALLERY_URL`.
|
||||
- The Driver's future plan is to reach the same VPS through an onion or diode ingress. A reverse proxy would route `/research/*` to the research service, and that only works if the app emits **relative, path-aware URLs**.
|
||||
- Don't build the onion ingress now. Just avoid adding any new hard-coded origin anywhere: bundle, app links, or outbound calls.
|
||||
|
||||
---
|
||||
|
||||
## 3. DRIVER ASKS from the ideation (unverified; confirm each, then fix)
|
||||
|
||||
Do these after D1–D2, highest first. For each one, **reproduce it first**, because other agents may already have fixed it.
|
||||
|
||||
1. **UPG 100 list prayer packet (highest).** The prayer packet "is not following the design pattern that I asked for" and the prayer list "is still not" either. Find the design spec in the repo (`_qa-cycle/`, `documentation/`, `plugin-designs/my-upg-100-list/`) and make the packet match it. Printing must work, because printing prayer packets is a core use.
|
||||
2. **Simple research map interoperability.** Selecting items isn't connecting to the legend.
|
||||
3. **"Export poster" buttons must open the browser's print dialog, never download a file.** On the simple research map it's a stray button. Either wire it properly or remove it until it's production-ready.
|
||||
4. **Gospel internet dashboard:**
|
||||
- It's missing from the Plugin Studio's list of plugins you can open directly.
|
||||
- Its default view should be the **language tree**, not the map.
|
||||
- **Legends should start collapsed.**
|
||||
5. **Rudder voice corner widget, on mobile especially:**
|
||||
- **Collapsed:** a fixed-width bubble (it must **not resize** while text streams in). It shows a red record dot while recording, a mic icon while you're speaking, and the last 3–5 words streaming on one line. When idle it says "Rudder voice-to-text".
|
||||
- **Expanded:** a small chat-window-style panel with a scrollable, editable text area. Less header space, usable on a phone.
|
||||
- **Recording keeps going** when collapsed and while switching Plugin Studio tabs.
|
||||
- It slowed down after about 1,400 words while a Tauri build ran. Check for unbounded growth in the transcript buffer or rendering.
|
||||
- On the shared cloud dev server, Rudder should tell the user to run the local dev server. It runs on the user's own CPU, and in the cloud it was too slow.
|
||||
6. **Wall of people groups (a big one).** Performance is poor. Its **default mode is "Bless"** (a printable poster); a separate "social" mode is the interactive wall with pray, comment and share. Poster spec:
|
||||
- **Header:** "Bless the nations in Jesus' name", then a small line explaining "Doxa" (= glory) and pointing to the prayer movement.
|
||||
- **Prayer-theme table:** three themes, **Treasure**, **Mountain**, **Sea**, each with a simple SVG icon (treasure chest, mountain, sea). Two scripture rows, Old Testament and New Testament:
|
||||
- Treasure: Haggai 2:7 / Matthew 13:44
|
||||
- Mountain: Isaiah 2:2 / Matthew 21:18-22
|
||||
- Sea: Habakkuk 2:14 / Matthew 8:23-28
|
||||
- The row labels may run vertically (rotated 90°), but they must localise and support right-to-left languages.
|
||||
- **Body:** every people group (about 2,000), each with a photo, a name, and a short blessing scripture underneath. Alternate the scriptures (mostly Psalms).
|
||||
- **Footer:** the priestly blessing, **Numbers 6:22-27**, then the "priesthood of every believer" story told in order: Genesis 12:1-4 → Genesis 15:5-6 → Galatians 3:5-7 → Galatians 3:13-14 → Galatians 3:28-29 → 1 Peter 2:4-10 (Christ the cornerstone, living stones) → Luke 19:9-10. Close with a line in the spirit of: *Jesus Christ is the cornerstone and we are living stones; these people groups are the reward of His suffering; take up our priestly mandate to bless them.*
|
||||
- **Translation:** use one that can legally be printed on a poster and exists in many languages (for example the World English Bible). Check the licence of every translation you use.
|
||||
- **Sizes:**
|
||||
- default **3 ft × 4 ft**;
|
||||
- a small option, **2 ft × 3 ft**;
|
||||
- a large option, **5 ft × 8 ft** as panels (6-inch glue overlap borders on three sides; the bottom panel has borders on all four).
|
||||
- Compute rows and columns and font sizes from the size so the scripture stays readable.
|
||||
- **Output:** print opens the browser's print dialog with one page per panel.
|
||||
- The poster generator must itself be an embeddable plugin.
|
||||
7. **Embed everywhere through relative paths.** Every production plugin must embed on both branches with no absolute origin baked in.
|
||||
8. **README:**
|
||||
- Add **vibe coders** as a named persona near the top.
|
||||
- Add a **"Cloud development environment"** section: set `DX_SYNC_REMOTE` / `DX_SYNC_BRANCH` in `.env.local` so a local `bun run dev` follows a remote branch that cloud agents push to. **The default stays fully local, needing no configuration.** Warn about merge conflicts if local agents also edit the checkout.
|
||||
- Add the two deployment paths (drop-in, subdomain) and D2's `bun run dropin`.
|
||||
- Add a line on bundle signing (SRI integrity) and how a host uses it.
|
||||
9. **Map load time.** The Driver asked about bundling the map library/tiles setup so maps don't make repeated outbound requests (3–5 s loads). Measure first, and report the trade-off against bundle size before you change anything.
|
||||
10. **Licence note:** the Driver wants terms that forbid military or war use. **Don't change the licence yourself.** Draft a proposal and flag it for the Driver.
|
||||
|
||||
---
|
||||
|
||||
## 4. How to prove each item (the test kit)
|
||||
|
||||
The kit is on branch `laptop/embed-harness` of doxa-life/doxa-maps-cdn (private), in `embed-harness/`. Set it up once, anywhere:
|
||||
|
||||
```bash
|
||||
git clone -b laptop/embed-harness https://git.nuxtbase.app/doxa-life/doxa-maps-cdn.git kit
|
||||
bash kit/embed-harness/setup.sh ~/dx-embed-work # clones dx-maps development + both DX Life branch worktrees, installs, copies the kit
|
||||
cd ~/dx-embed-work
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
```bash
|
||||
./run-all.sh # tree server on research.localhost:4610, drop-in :4611, subdomain :4613, then the checker
|
||||
./run-all.sh check # checker only (read-only): script 200, SRI hash, element defined, iframe page, CORS, snippet
|
||||
./run-all.sh stop # stop all servers, restore every public/ folder
|
||||
node browser-probe.mjs <manifestUrl> <pageUrl>... # headless Firefox: per map element, mounted vs drew a canvas, page errors
|
||||
node poster-probe2.mjs <pageUrl> # clicks regions poster buttons, logs every posters/ request + status
|
||||
```
|
||||
|
||||
- **Refresh first.** Before testing, run `git -C doxa-maps fetch origin development && git -C doxa-maps checkout --detach origin/development`, and the same `fetch` + `checkout` for each branch worktree.
|
||||
- **Probe caveats:**
|
||||
- `browser-probe` exits 1 whenever Nuxt's dev-tools elements appear, because they have no canvas. Read the per-element lines, not the exit code.
|
||||
- It can't see inside iframes. The country page map (`/research/<slug>`, try `abai-sungai`) needs a separate check.
|
||||
- "2 of 3 `regions-dashboard` drew" on the Regions tab has been seen on every branch, and one may be a hidden tab. Confirm it, don't assume.
|
||||
- **Per item, "done" means all of the following:**
|
||||
- the reproduction now passes;
|
||||
- the checker is still 7/7 on both branches;
|
||||
- the repo gates are green;
|
||||
- the probe shows 0 page errors.
|
||||
|
||||
---
|
||||
|
||||
## 5. Out of scope for this run (don't start)
|
||||
|
||||
- DX Base: the app-shell `platform-engineering` branch, multi-tenant PocketBase, OpenFGA, workerd edge functions, tenant graduation, the dashboard redesign.
|
||||
- The onion or diode ingress.
|
||||
- The CDN branch.
|
||||
- Changing the licence (propose only).
|
||||
|
||||
---
|
||||
|
||||
## 6. End-of-run report
|
||||
|
||||
Write this back to the Driver:
|
||||
|
||||
```
|
||||
DX-MAPS 12H RESULT (<date>, development <sha>)
|
||||
Fixed: <item → commit sha → how verified>
|
||||
Not fixed: <item → why → exact failing command/output>
|
||||
Checker: drop-in <n/7> subdomain <n/7>
|
||||
Gates: <list, PASS/RED>
|
||||
Needs the Driver's eyes: <URLs + what to look at>
|
||||
```
|
||||
49
embed-harness/browser-probe.mjs
Normal file
49
embed-harness/browser-probe.mjs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
#!/usr/bin/env node
|
||||
// browser-probe — headless FIREFOX (never Chrome) check of what the checker can't see:
|
||||
// does each production map element mount and draw a canvas, with no page errors?
|
||||
// Text output only (no screenshots). Usage: node browser-probe.mjs <manifestUrl> <pageUrl> ...
|
||||
import { createRequire } from 'node:module'
|
||||
const require = createRequire(new URL('./doxa-maps/package.json', import.meta.url))
|
||||
const { firefox } = require('playwright')
|
||||
|
||||
const [manifestUrl, ...pages] = process.argv.slice(2)
|
||||
const manifest = await (await fetch(manifestUrl)).json()
|
||||
const elements = [...new Set(manifest.bundles.flatMap(b => (b.profiles ?? []).filter(p => p.stage === 'production').map(p => p.element)))]
|
||||
|
||||
const browser = await firefox.launch({ headless: true })
|
||||
let bad = 0
|
||||
for (const url of pages) {
|
||||
const page = await browser.newPage({ viewport: { width: 1400, height: 1000 } })
|
||||
const errors = []
|
||||
page.on('pageerror', e => errors.push(String(e.message).split('\n')[0].slice(0, 160)))
|
||||
let status = 0
|
||||
try { status = (await page.goto(url, { waitUntil: 'load', timeout: 120000 }))?.status() ?? 0 } catch (e) { errors.push('goto: ' + e.message.split('\n')[0]) }
|
||||
// let maps init; scroll through so lazy/intersection-mounted cards render
|
||||
for (let y = 0; y < 12; y++) { await page.mouse.wheel(0, 900); await page.waitForTimeout(1500) }
|
||||
await page.waitForTimeout(8000)
|
||||
const r = await page.evaluate((els) => {
|
||||
const deep = (root, sel, out = []) => { root.querySelectorAll(sel).forEach(n => out.push(n)); root.querySelectorAll('*').forEach(n => n.shadowRoot && deep(n.shadowRoot, sel, out)); return out }
|
||||
const per = els.map(tag => {
|
||||
const nodes = deep(document, tag)
|
||||
const drawn = nodes.filter(n => deep(n.shadowRoot || n, 'canvas').some(c => c.width > 50 && c.height > 50)).length
|
||||
return { tag, mounted: nodes.length, defined: !!customElements.get(tag), drawn }
|
||||
}).filter(x => x.mounted)
|
||||
// every OTHER defined custom element on the page (catches wrapper tags the manifest doesn't name)
|
||||
const seen = new Set(els)
|
||||
const others = {}
|
||||
deep(document, '*').forEach(n => { const t = n.localName; if (t.includes('-') && !seen.has(t) && customElements.get(t)) (others[t] ||= []).push(n) })
|
||||
for (const [tag, nodes] of Object.entries(others)) {
|
||||
const drawn = nodes.filter(n => deep(n.shadowRoot || n, 'canvas').some(c => c.width > 50 && c.height > 50)).length
|
||||
per.push({ tag: tag + ' (other)', mounted: nodes.length, defined: true, drawn })
|
||||
}
|
||||
return { per, iframes: document.querySelectorAll('iframe').length, canvases: deep(document, 'canvas').length, dir: document.documentElement.dir || 'ltr' }
|
||||
}, elements)
|
||||
const notDrawn = r.per.filter(x => x.drawn < x.mounted)
|
||||
if (status !== 200 || errors.length || notDrawn.length) bad++
|
||||
console.log(`\n${url} HTTP ${status} dir=${r.dir} iframes=${r.iframes} canvases=${r.canvases} page-errors=${errors.length}`)
|
||||
for (const x of r.per) console.log(` ${x.drawn === x.mounted ? 'OK ' : 'GREY'} ${x.tag} mounted=${x.mounted} drawn=${x.drawn} defined=${x.defined}`)
|
||||
for (const e of [...new Set(errors)].slice(0, 8)) console.log(` ERR ${e}`)
|
||||
await page.close()
|
||||
}
|
||||
await browser.close()
|
||||
process.exitCode = bad ? 1 : 0
|
||||
110
embed-harness/check-embed-6way.mjs
Normal file
110
embed-harness/check-embed-6way.mjs
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
#!/usr/bin/env node
|
||||
// check-embed-6way — headless embeddability check, read-only. For every PRODUCTION
|
||||
// profile in a site's manifest it asserts, from THAT site:
|
||||
// script the bundle script (scriptUrl resolved against the MANIFEST url) returns 200 JS
|
||||
// sri sha384(bytes) equals the bundle's published integrity
|
||||
// element the profile's custom element name is defined in the bundle
|
||||
// iframe the profile page (iframeUrl) returns 200 HTML
|
||||
// cors a script on another origin sends Access-Control-Allow-Origin (needed for SRI)
|
||||
// snippet the copy-paste <script src> in embed.script loads when pasted on the site,
|
||||
// and carries the same integrity
|
||||
// Usage: node check-embed-6way.mjs [--json] <label>=<siteOrigin>@<manifestUrl> ...
|
||||
// Exit 0 all pass, 1 any fail, 75 NOT MEASURED (a manifest could not be read, or a site
|
||||
// listed no production profile).
|
||||
import { createHash } from 'node:crypto'
|
||||
|
||||
const DEFAULT = [
|
||||
'cdn=https://dx-life.nuxtbase.app@https://maps-dev.nuxtbase.app/cdn/dx-maps/manifest.json',
|
||||
'drop-in=https://dxlife-dropin.nuxtbase.app@https://dxlife-dropin.nuxtbase.app/dx-maps/manifest.json',
|
||||
'subdomain=https://research.nuxtbase.app@https://research.nuxtbase.app/dx-maps/manifest.json'
|
||||
]
|
||||
const args = process.argv.slice(2)
|
||||
const asJson = args.includes('--json')
|
||||
const sites = (args.filter(a => a !== '--json').length ? args.filter(a => a !== '--json') : DEFAULT).map(s => {
|
||||
const [label, rest] = s.split('=')
|
||||
const at = rest.indexOf('@')
|
||||
return { label, origin: rest.slice(0, at), manifestUrl: rest.slice(at + 1) }
|
||||
})
|
||||
|
||||
async function get(url, headers = {}) {
|
||||
const ctl = new AbortController()
|
||||
const t = setTimeout(() => ctl.abort(), 20000)
|
||||
try {
|
||||
const r = await fetch(url, { headers, signal: ctl.signal, redirect: 'follow' })
|
||||
const buf = Buffer.from(await r.arrayBuffer())
|
||||
return { status: r.status, type: r.headers.get('content-type') ?? '', acao: r.headers.get('access-control-allow-origin'), buf }
|
||||
} catch (e) {
|
||||
return { status: 0, type: '', acao: null, buf: Buffer.alloc(0), error: String(e?.cause?.code ?? e?.name ?? e) }
|
||||
} finally { clearTimeout(t) }
|
||||
}
|
||||
const sha384 = b => 'sha384-' + createHash('sha384').update(b).digest('base64')
|
||||
const snippetSrc = html => /<script[^>]*\bsrc="([^"]+\.js[^"]*)"[^>]*>/i.exec(html ?? '')?.[1] ?? null
|
||||
const snippetIntegrity = html => /<script[^>]*\bsrc="[^"]+\.js[^"]*"[^>]*\bintegrity="([^"]+)"/i.exec(html ?? '')?.[1] ?? null
|
||||
|
||||
const results = []
|
||||
let notMeasured = false
|
||||
for (const site of sites) {
|
||||
const m = await get(site.manifestUrl)
|
||||
let manifest = null
|
||||
try { manifest = JSON.parse(m.buf.toString('utf8')) } catch {}
|
||||
if (m.status !== 200 || !Array.isArray(manifest?.bundles)) {
|
||||
results.push({ site: site.label, notMeasured: `manifest ${site.manifestUrl} → ${m.status || m.error}` })
|
||||
notMeasured = true
|
||||
continue
|
||||
}
|
||||
const profiles = manifest.bundles.flatMap(b => (b.profiles ?? []).filter(p => p.stage === 'production').map(p => ({ b, p })))
|
||||
if (!profiles.length) { results.push({ site: site.label, notMeasured: 'no production profiles' }); notMeasured = true; continue }
|
||||
const scriptCache = new Map()
|
||||
for (const { b, p } of profiles) {
|
||||
const fails = []
|
||||
const scriptUrl = new URL(p.scriptUrl ?? b.scriptUrl, site.manifestUrl).href
|
||||
if (!scriptCache.has(scriptUrl)) scriptCache.set(scriptUrl, await get(scriptUrl, { Origin: site.origin }))
|
||||
const s = scriptCache.get(scriptUrl)
|
||||
if (s.status !== 200 || !/javascript/.test(s.type)) fails.push(`script ${s.status || s.error} ${s.type}`)
|
||||
else {
|
||||
if (!b.integrity) fails.push('sri: no published integrity')
|
||||
else if (sha384(s.buf) !== b.integrity) fails.push('sri: bytes do not match integrity')
|
||||
const el = p.element
|
||||
if (!el || !(s.buf.includes(`"${el}"`) || s.buf.includes(`'${el}'`) || s.buf.includes(`\`${el}\``))) fails.push(`element ${el} not defined`)
|
||||
}
|
||||
if (new URL(scriptUrl).origin !== site.origin && !(s.acao === '*' || s.acao === site.origin)) fails.push(`cors: no Access-Control-Allow-Origin for ${site.origin}`)
|
||||
if (p.iframeUrl) {
|
||||
const f = await get(new URL(p.iframeUrl, site.manifestUrl).href)
|
||||
if (f.status !== 200 || !/html/.test(f.type)) fails.push(`iframe ${f.status || f.error}`)
|
||||
}
|
||||
const src = snippetSrc(p.embed?.script)
|
||||
if (!src) fails.push('snippet: no script src')
|
||||
else {
|
||||
const pasted = new URL(src, site.origin + '/research/').href
|
||||
if (pasted !== scriptUrl) {
|
||||
const x = await get(pasted)
|
||||
if (x.status !== 200 || !/javascript/.test(x.type)) fails.push(`snippet: src "${src}" pasted on ${site.origin}/research/ → ${x.status || x.error} ${x.type.split(";")[0]} (not the script)`)
|
||||
}
|
||||
const si = snippetIntegrity(p.embed?.script)
|
||||
if (b.integrity && si && si !== b.integrity) fails.push('snippet: integrity differs from the bundle')
|
||||
if (b.integrity && !si) fails.push('snippet: no integrity attribute')
|
||||
}
|
||||
results.push({ site: site.label, profile: `${b.bundle}/${p.profile}`, fails })
|
||||
}
|
||||
}
|
||||
|
||||
if (asJson) { console.log(JSON.stringify(results, null, 2)) } else {
|
||||
const profiles = [...new Set(results.filter(r => r.profile).map(r => r.profile))]
|
||||
console.log(['site', ...profiles.map((_, i) => `P${i + 1}`)].join('\t'))
|
||||
for (const site of sites) {
|
||||
const nm = results.find(r => r.site === site.label && r.notMeasured)
|
||||
if (nm) { console.log(`${site.label}\tNOT MEASURED: ${nm.notMeasured}`); continue }
|
||||
console.log([site.label, ...profiles.map(pr => {
|
||||
const r = results.find(x => x.site === site.label && x.profile === pr)
|
||||
return !r ? '—' : r.fails.length ? `FAIL(${r.fails.length})` : 'PASS'
|
||||
})].join('\t'))
|
||||
}
|
||||
console.log('\n' + profiles.map((p, i) => `P${i + 1} = ${p}`).join('\n'))
|
||||
const failed = results.filter(r => r.fails?.length)
|
||||
if (failed.length) {
|
||||
console.log('\nFailures:')
|
||||
for (const r of failed) for (const f of r.fails) console.log(` ${r.site} ${r.profile}: ${f}`)
|
||||
}
|
||||
}
|
||||
const anyFail = results.some(r => r.fails?.length)
|
||||
process.exitCode = anyFail ? 1 : notMeasured ? 75 : 0
|
||||
19
embed-harness/poster-probe2.mjs
Normal file
19
embed-harness/poster-probe2.mjs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { createRequire } from 'node:module'
|
||||
const require = createRequire(new URL('./doxa-maps/package.json', import.meta.url))
|
||||
const { firefox } = require('playwright')
|
||||
const url = process.argv[2]
|
||||
const b = await firefox.launch({ headless: true }); const p = await b.newPage({ viewport: { width: 1400, height: 1000 } })
|
||||
const reqs = []; p.on('response', r => { if (/posters?\//.test(r.url())) reqs.push(r.status() + ' ' + r.url().replace(/^https?:\/\/[^/]+/, '')) })
|
||||
p.on('dialog', d => d.dismiss())
|
||||
await p.goto(url, { waitUntil: 'load', timeout: 120000 }); await p.waitForTimeout(15000)
|
||||
// every poster-ish button inside a regions-* element (shadow DOM aware)
|
||||
const btns = p.locator('regions-dashboard button, regions-mfe button, region-dashboards button, regions-list button').filter({ hasText: /poster|print|export/i })
|
||||
const n = await btns.count(); console.log('regions poster/print buttons:', n)
|
||||
for (let i = 0; i < Math.min(n, 4); i++) {
|
||||
const t = (await btns.nth(i).textContent()).trim().replace(/\s+/g, ' ').slice(0, 40)
|
||||
try { await btns.nth(i).scrollIntoViewIfNeeded({ timeout: 5000 }); await btns.nth(i).click({ timeout: 8000 }); await p.waitForTimeout(10000); console.log(`clicked "${t}"`) } catch (e) { console.log(`"${t}" not clickable: ${e.message.split('\n')[0].slice(0, 80)}`) }
|
||||
}
|
||||
const status = await p.evaluate(() => { const out = []; const walk = r => r.querySelectorAll('*').forEach(n => { if (n.shadowRoot) walk(n.shadowRoot); const t = (n.childElementCount ? '' : n.textContent || '').trim(); if (/not found|404|failed|generating|fetching/i.test(t) && t.length < 120) out.push(t) }); walk(document); return [...new Set(out)] })
|
||||
console.log('status text:', status.join(' | ') || 'none')
|
||||
console.log('poster requests:\n ' + ([...new Set(reqs)].join('\n ') || 'none'))
|
||||
await b.close()
|
||||
75
embed-harness/run-all.sh
Executable file
75
embed-harness/run-all.sh
Executable file
|
|
@ -0,0 +1,75 @@
|
|||
#!/usr/bin/env bash
|
||||
# run-all.sh — run BOTH DX Life embed branches AT ONCE, each from its own git worktree
|
||||
# (a folder named after the branch). The CDN branch was dropped 2026-09-29: its embed URLs are
|
||||
# fixed as absolute addresses at build time, so they differ laptop vs cloud. The two that remain
|
||||
# use the site's own origin (drop-in) or a research. subdomain (subdomain).
|
||||
#
|
||||
# research-gallery-drop-in http://localhost:4611 maps copied into its own public/dx-maps/ (one server)
|
||||
# research-gallery-subdomain http://localhost:4613 maps + gallery on the research. subdomain below
|
||||
# dx-maps tree server http://research.localhost:4610 (doxa-maps, development)
|
||||
#
|
||||
# *.localhost resolves to this machine in Firefox, curl and node, so research.localhost is a real
|
||||
# subdomain-shaped origin without touching /etc/hosts.
|
||||
#
|
||||
# start: ./run-all.sh stop: ./run-all.sh stop check: ./run-all.sh check
|
||||
# Logs: ./logs/<name>.log. Read-only against both remotes; never pushes.
|
||||
set -uo pipefail
|
||||
export PATH="$HOME/.bun/bin:$HOME/.local/bin:$PATH"
|
||||
B="$(cd "$(dirname "$0")" && pwd)"
|
||||
MAPS="$B/doxa-maps"
|
||||
TREE=4610
|
||||
SUB=http://research.localhost:$TREE
|
||||
declare -A PORT=([research-gallery-drop-in]=4611 [research-gallery-subdomain]=4613)
|
||||
BRANCHES=(research-gallery-drop-in research-gallery-subdomain)
|
||||
mkdir -p "$B/logs"
|
||||
|
||||
stopport() {
|
||||
for pid in $(ss -ltnp | sed -nE "s/.*:$1 .*pid=([0-9]+).*/\1/p" | sort -u); do
|
||||
kill -- -"$(ps -o pgid= -p "$pid" | tr -d ' ')" 2>/dev/null || kill "$pid"
|
||||
done
|
||||
for _ in $(seq 1 15); do ss -ltn | grep -q ":$1 " || return 0; sleep 1; done
|
||||
echo "port $1 still busy"
|
||||
}
|
||||
waitport() { # first Nuxt compile takes ~1 min and answers 503 until done
|
||||
for _ in $(seq 1 90); do [ "$(curl -s -o /dev/null -w '%{http_code}' -m 5 "http://localhost:$1$2")" = 200 ] && return 0; sleep 2; done
|
||||
return 1
|
||||
}
|
||||
check() {
|
||||
node "$B/check-embed-6way.mjs" \
|
||||
"research-gallery-drop-in=http://localhost:4611@http://localhost:4611/dx-maps/manifest.json" \
|
||||
"research-gallery-subdomain=$SUB@$SUB/dx-maps/manifest.json"
|
||||
}
|
||||
|
||||
case "${1:-start}" in
|
||||
stop)
|
||||
for p in 4611 4612 4613 $TREE; do stopport $p; done # 4612 = the retired cdn branch
|
||||
git -C "$B/research-gallery-drop-in" restore public/dx-maps 2>/dev/null
|
||||
git -C "$B/research-gallery-drop-in" clean -fdq public/dx-maps 2>/dev/null
|
||||
git -C "$MAPS" restore public
|
||||
echo "stopped; public/ folders restored"; exit 0 ;;
|
||||
check) check; exit $? ;;
|
||||
esac
|
||||
|
||||
# 1. research-gallery-drop-in gets its OWN copy of the maps, snippets pointing at itself (4611),
|
||||
# via the repo's own drop-in step (vite.doxa-maps-build-config.json "dropin" + internal/sync-dropin.mjs)
|
||||
( cd "$MAPS" && EMBED_BASE_URL=http://localhost:4611 bun run tree >/dev/null )
|
||||
rm -rf "$B/research-gallery-drop-in/public/dx-maps"
|
||||
( cd "$MAPS" && node internal/sync-dropin.mjs --from-config --dest "$B/research-gallery-drop-in/public" | head -1 )
|
||||
# 2. then regenerate for the research. subdomain that research-gallery-subdomain loads from
|
||||
( cd "$MAPS" && EMBED_BASE_URL=$SUB bun run tree >/dev/null )
|
||||
|
||||
# 3. start everything (skips anything already listening)
|
||||
up() { ss -ltn | grep -q ":$1 "; }
|
||||
up $TREE || ( cd "$MAPS" && PORT=$TREE setsid nohup bun run serve >"$B/logs/tree-server.log" 2>&1 </dev/null & )
|
||||
up 4611 || ( cd "$B/research-gallery-drop-in" && PORT=4611 setsid nohup bun run dev >"$B/logs/research-gallery-drop-in.log" 2>&1 </dev/null & )
|
||||
up 4613 || ( cd "$B/research-gallery-subdomain" && NUXT_PUBLIC_DX_MAPS_BASE=$SUB/dx-maps \
|
||||
NUXT_PUBLIC_RESEARCH_GALLERY_URL=$SUB/dx-maps/doxa-maps-spa/doxa-maps-spa/index.html \
|
||||
PORT=4613 setsid nohup bun run dev >"$B/logs/research-gallery-subdomain.log" 2>&1 </dev/null & )
|
||||
|
||||
waitport $TREE /dx-maps/manifest.json && echo "tree server (research.) $SUB/dx-maps/ UP"
|
||||
for br in "${BRANCHES[@]}"; do
|
||||
p=${PORT[$br]}
|
||||
if waitport "$p" /; then printf '%-27s http://localhost:%s/ UP (%s)\n' "$br" "$p" "$(git -C "$B/$br" rev-parse --short HEAD)"
|
||||
else printf '%-27s http://localhost:%s/ NOT UP - see logs/%s.log\n' "$br" "$p" "$br"; fi
|
||||
done
|
||||
echo; check
|
||||
28
embed-harness/setup.sh
Executable file
28
embed-harness/setup.sh
Executable file
|
|
@ -0,0 +1,28 @@
|
|||
#!/usr/bin/env bash
|
||||
# setup.sh — build the embed test workspace anywhere: dx-maps (development) + one git worktree per
|
||||
# DX Life branch, side by side, with this kit copied in. Then `cd <workdir> && ./run-all.sh`.
|
||||
#
|
||||
# usage: bash setup.sh [workdir] (default: ./dx-embed-work)
|
||||
#
|
||||
# Needs git access to git.nuxtbase.app through a credential helper (never a token in the URL),
|
||||
# bun 1.3+, node 20+, about 5 GB disk. Read-only against both remotes.
|
||||
set -euo pipefail
|
||||
export PATH="$HOME/.bun/bin:$HOME/.local/bin:$PATH"
|
||||
KIT="$(cd "$(dirname "$0")" && pwd)"
|
||||
W="$(mkdir -p "${1:-./dx-embed-work}" && cd "${1:-./dx-embed-work}" && pwd)"
|
||||
HOST=https://git.nuxtbase.app/doxa-life
|
||||
cd "$W"
|
||||
|
||||
[ -d doxa-maps ] || git clone -q -b development "$HOST/doxa-maps.git" doxa-maps
|
||||
[ -d doxa-life ] || git clone -q --filter=blob:none "$HOST/doxa-maps-cdn.git" doxa-life
|
||||
for br in research-gallery-drop-in research-gallery-subdomain; do
|
||||
[ -d "$br" ] || git -C doxa-life worktree add -q "../$br" "$br"
|
||||
done
|
||||
|
||||
( cd doxa-maps && bun install >/dev/null && node node_modules/playwright/cli.js install firefox >/dev/null ) # Firefox only, never Chrome
|
||||
for br in research-gallery-drop-in research-gallery-subdomain; do ( cd "$br" && bun install >/dev/null ); done
|
||||
|
||||
cp "$KIT"/run-all.sh "$KIT"/check-embed-6way.mjs "$KIT"/browser-probe.mjs "$KIT"/poster-probe2.mjs "$KIT"/HANDOFF-dx-maps-12h.md "$W"/
|
||||
chmod +x "$W/run-all.sh"
|
||||
echo "ready: cd $W && ./run-all.sh"
|
||||
echo " dx-maps $(git -C doxa-maps rev-parse --short HEAD) · drop-in $(git -C research-gallery-drop-in rev-parse --short HEAD) · subdomain $(git -C research-gallery-subdomain rev-parse --short HEAD)"
|
||||
Loading…
Reference in a new issue