Only the browser seat runs browsers, and only inside internal/browser-slot.sh. That rule was remembered, not enforced: an agent ran a gate it took for a headless source check and it launched Chromium outside the slot. internal/lib/playwright-guarded.mjs is now the one way a repo file reaches playwright: its launch(), launchPersistentContext(), launchServer(), connect() and connectOverCDP() exit 75 (NOT MEASURED) with a message naming browser-slot.sh unless DOXA_BROWSER_SLOT is set, and browser-slot.sh exports it for the command it runs, so a bash -c grandchild inherits it. All 72 files that imported playwright switch to it (internal/lib/browser.mjs and 64 internal gates; plugin-designs scripts: doxa-research-map 3, my-upg-100-list 2, research-map-plus-grid 2, research-dashboard 1): a mechanical import swap. check:browser-gates-guarded (gates:pre) fails a direct import; bun run browser-gates lists the 97 scripts that launch a browser. isBrowserGate moves to internal/lib/browser-gates.mjs so the runner and the check share it.
70 lines
3.6 KiB
JavaScript
70 lines
3.6 KiB
JavaScript
#!/usr/bin/env node
|
|
// Targeted re-capture: cap1 (vision-card WITH token so it renders, not the token-error state)
|
|
// and cap5 (SPA deep-linked to a plugin so a map shows, not "choose a map").
|
|
// Retries navigation while concurrent builds churn the tree.
|
|
import { chromium } from './lib/playwright-guarded.mjs'
|
|
import { createServer } from 'node:http'
|
|
import { readFile, stat } from 'node:fs/promises'
|
|
import { join, extname, resolve, dirname } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { TREE_NAME } from './_tree-name.mjs'
|
|
|
|
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
const ROOT = resolve(HERE, '..', 'public')
|
|
const OUT = resolve(HERE, '..', 'documentation', 'readme-images')
|
|
const TYPES = { '.html':'text/html', '.js':'text/javascript', '.mjs':'text/javascript',
|
|
'.json':'application/json', '.css':'text/css', '.webp':'image/webp', '.png':'image/png',
|
|
'.svg':'image/svg+xml', '.geojson':'application/json', '.csv':'text/csv', '.woff2':'font/woff2' }
|
|
const server = createServer(async (req, res) => {
|
|
try { let p = join(ROOT, decodeURIComponent(req.url.split('?')[0]))
|
|
if ((await stat(p)).isDirectory()) p = join(p, 'index.html')
|
|
res.writeHead(200, { 'content-type': TYPES[extname(p)] || 'application/octet-stream' })
|
|
res.end(await readFile(p)) } catch { res.writeHead(404).end('not found') }
|
|
})
|
|
await new Promise(r => server.listen(0, '127.0.0.1', r))
|
|
const BASE = `http://127.0.0.1:${server.address().port}`
|
|
let TK = ''
|
|
try { const r = await fetch('https://doxa.life/api/maps/token', { signal: AbortSignal.timeout(8000) })
|
|
TK = r.ok ? ((await r.json()).token || '') : '' } catch {}
|
|
console.log(TK ? `token acquired (${TK.length})` : 'NO token')
|
|
const browser = await chromium.launch()
|
|
|
|
async function tryShot(name, path, { vp = { width: 1440, height: 900 }, wait = 5000, dsf = 2, before = null, waitFor = null, tries = 8 } = {}) {
|
|
for (let t = 1; t <= tries; t++) {
|
|
const ctx = await browser.newContext({ viewport: vp, deviceScaleFactor: dsf })
|
|
const page = await ctx.newPage()
|
|
let ok = false
|
|
// Insert tk into the QUERY, keeping any #hash fragment LAST (else the route breaks).
|
|
const hashAt = path.indexOf('#')
|
|
const basePath = hashAt >= 0 ? path.slice(0, hashAt) : path
|
|
const frag = hashAt >= 0 ? path.slice(hashAt) : ''
|
|
const url = BASE + basePath + (basePath.includes('?') ? '&' : '?') + 'tk=' + encodeURIComponent(TK) + frag
|
|
try {
|
|
const resp = await page.goto(url, { waitUntil: 'load', timeout: 30000 })
|
|
if (resp && resp.status() === 404) throw new Error('404')
|
|
if (waitFor) { try { await page.waitForSelector(waitFor, { timeout: 12000 }) } catch {} }
|
|
if (before) await before(page)
|
|
await page.waitForTimeout(wait)
|
|
await page.screenshot({ path: join(OUT, name + '.png') })
|
|
ok = true
|
|
console.log(' OK', name, `(try ${t})`)
|
|
} catch (e) { console.log(' retry', name, t, e.message.split('\n')[0]) }
|
|
finally { await ctx.close() }
|
|
if (ok) return true
|
|
await new Promise(r => setTimeout(r, 4000))
|
|
}
|
|
console.log(' GAVE UP', name)
|
|
return false
|
|
}
|
|
|
|
// cap1: vision-card DEFAULT instance page (TOKEN_FREE=true -> renders the card instantly, no token error).
|
|
// Long wait so the aurora backdrop + rotating stat cards fully paint (they animate in).
|
|
await tryShot('cap1-single-element', `/${TREE_NAME}/doxa-vision-card/doxa-vision-card/default/index.html`,
|
|
{ wait: 9000 })
|
|
|
|
// cap5: SPA deep-linked to the prayer map instance so a real painted map shows in the right pane.
|
|
await tryShot('cap5-doxa-maps-spa', '/index.html#/doxa-simple-map/doxa-simple-map/prayer',
|
|
{ wait: 11000, waitFor: '#app' })
|
|
|
|
await browser.close(); server.close()
|
|
console.log('fix done')
|