diff --git a/internal/set-app-start-page.mjs b/internal/set-app-start-page.mjs new file mode 100644 index 000000000..9b1820ecf --- /dev/null +++ b/internal/set-app-start-page.mjs @@ -0,0 +1,115 @@ +#!/usr/bin/env node +/** + * set-app-start-page — the MOBILE APP opens the UPG-100 list, not the plugin studio. + * + * THE DEFECT, reported by repo: the Capacitor app launches public/index.html, which mounts + * . A recipient opening the phone app meets the DEV STUDIO rather than the map the + * README promises. RULED (Kingdom-agent, card DECISION-MOBILE-EXAMPLE-IS-UPG100-NOT-SIMPLE- + * RESEARCH): the ONE mobile-app showcase is the UPG-100 list. + * + * ⚠ THE CARD'S OWN BLUF SAYS "Simple Research" AND IS STALE. It predates that ruling, which is + * recorded as DONE in the same backlog and says Simple Research "was never mobile-friendly". + * Building to the card's wording would ship the app the ruling rejected. + * + * ★ WHY THIS RUNS AFTER `cap sync` AND WRITES ONLY INTO THE ANDROID ASSETS. + * public/index.html has THREE consumers (capacitor.config.json documents them): Railway/CDN, + * Capacitor, and local staging. The studio is CORRECT for the CDN root and for `bun run serve` — + * only the phone build wants a different landing page. Rewriting the shared root would fix the app + * by breaking the other two. `cap sync` copies webDir into + * deployment/capacitor/android/app/src/main/assets/public/, so overwriting the COPY changes what + * the app opens and leaves the served tree untouched. + * + * ★ WHY IT COPIES THE BUILT LEAF RATHER THAN HAND-WRITING A HOST PAGE. + * The leaf carries an SRI integrity hash for its bundle. A hand-written page needs its own correct + * hash, and a stale one makes the browser REFUSE the script with no console error at all: the + * element never upgrades and the app shows a blank screen. Copying the artifact the build already + * verified means there is no second copy of that hash to drift. Only the relative depth changes. + * + * Usage: node internal/set-app-start-page.mjs [--check] + * --check assert the app root is already the UPG-100 page; do not write. Exit 1 if not. + */ +import { readFileSync, writeFileSync, existsSync, copyFileSync } from 'node:fs' +import { resolve, dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const CHECK = process.argv.includes('--check') + +// The assets tree `cap sync` writes. NOT public/ — see the header. +const APP_PUBLIC = join(ROOT, 'deployment/capacitor/android/app/src/main/assets/public') +const APP_ROOT = join(APP_PUBLIC, 'index.html') +// The showcase leaf, inside the served tree. +const TREE = 'doxa-maps' +const LEAF_REL = `${TREE}/my-upg-100-list/upg-100-mfe/index.html` +const LEAF = join(APP_PUBLIC, LEAF_REL) + +// A marker so --check can tell the app page apart from the studio page without parsing HTML. +const MARKER = '' + +if (!existsSync(APP_PUBLIC)) { + console.log('APP-START-PAGE: SKIP — no synced assets tree.') + console.log(' Run `bun run cap:sync` first; this rewrites what that step copied.') + process.exit(0) +} +if (!existsSync(LEAF)) { + console.error(`APP-START-PAGE: FAIL — the showcase leaf is missing from the synced tree:`) + console.error(` ${LEAF_REL}`) + console.error(' The app would launch a 404. Build the tree and re-run cap:sync.') + process.exit(1) +} + +if (CHECK) { + const cur = existsSync(APP_ROOT) ? readFileSync(APP_ROOT, 'utf8') : '' + if (cur.includes(MARKER)) { + console.log('APP-START-PAGE: PASS — the app opens the UPG-100 list.') + process.exit(0) + } + console.error('APP-START-PAGE: FAIL — the app root is NOT the UPG-100 page.') + console.error(cur.includes(': a recipient opening the app meets the dev studio.' + : ' No UPG-100 marker found.') + console.error(' Fix: node internal/set-app-start-page.mjs (after cap:sync).') + process.exit(1) +} + +// ── REWRITE THE LEAF FOR ONE LEVEL UP ──────────────────────────────────────── +// The leaf lives at /my-upg-100-list/upg-100-mfe/index.html and reaches its bundle with +// `../my-upg-100-list.js`. At the app root that same relative path resolves ABOVE the web root. +// Every leaf-relative reference gains the leaf's own directory prefix; absolute and protocol URLs +// (the Mapbox CDN tags) are left alone. +const LEAF_DIR = `${TREE}/my-upg-100-list/upg-100-mfe/` +const html = readFileSync(LEAF, 'utf8') +const rebased = html.replace( + /\b(src|href)=(["'])(?!https?:|data:|#|\/)([^"']+)\2/g, + (_m, attr, q, url) => { + // Resolve the leaf-relative url against the leaf's directory, from the web root. + const abs = new URL(url, `http://x/${LEAF_DIR}`).pathname.replace(/^\//, '') + return `${attr}=${q}${abs}${q}` + }, +) +if (rebased === html) { + console.error('APP-START-PAGE: FAIL — rewrote nothing, so the paths were not what this expects.') + console.error(' Refusing to install a page whose bundle path would 404 at the app root.') + process.exit(1) +} + +// Prove the rewrite points at files that EXIST, rather than trusting the regex. A start page +// whose bundle 404s is a blank white screen with no error, which is the failure mode this whole +// script exists to prevent. +const referenced = [...rebased.matchAll(/\b(?:src|href)=["']([^"':]+)["']/g)].map((m) => m[1]) +const missing = referenced.filter((r) => !r.startsWith('data:') && !existsSync(join(APP_PUBLIC, r))) +if (missing.length) { + console.error('APP-START-PAGE: FAIL — the rewritten page references files that are not there:') + for (const m of missing) console.error(` ${m}`) + process.exit(1) +} + +// Keep the studio page reachable rather than deleting it: a developer with the apk still wants it. +const STUDIO_KEEP = join(APP_PUBLIC, 'studio.html') +if (existsSync(APP_ROOT) && !existsSync(STUDIO_KEEP)) copyFileSync(APP_ROOT, STUDIO_KEEP) + +writeFileSync(APP_ROOT, rebased.replace(//i, `\n ${MARKER}`)) +console.log('APP-START-PAGE: the app now opens the UPG-100 list.') +console.log(` from ${LEAF_REL}`) +console.log(` wrote assets/public/index.html (${referenced.length} reference(s) rebased, all present)`) +console.log(' the studio page is kept at assets/public/studio.html') diff --git a/package.json b/package.json index 332b04733..f42906584 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "cap:verify": "test -f deployment/capacitor/android/app/src/main/assets/public/index.html && echo \"OK webDir copied\" || (echo \"FAIL: empty assets/public -> blank white screen. Check webDir.\" && exit 1)", "check:tauri": "node internal/check-tauri-config.mjs", "tauri:build": "./node_modules/.bin/tauri build", - "apk:debug": "bun run build && bun run cap:sync && bun run cap:verify && cd deployment/capacitor/android && ./gradlew assembleDebug", + "apk:debug": "bun run build && bun run cap:sync && bun run app:start-page && bun run cap:verify && cd deployment/capacitor/android && ./gradlew assembleDebug", "apk:install": "adb install -r deployment/capacitor/android/app/build/outputs/apk/debug/app-debug.apk", "phone": "bun run apk:debug && bun run apk:install", "shell": "node internal/install-shell.mjs --no-token", @@ -193,7 +193,9 @@ "check:tree-weight": "node internal/check-published-tree-weight.mjs", "check:i18n-never-translated": "node internal/check-i18n-never-translated.mjs", "check:embed-base": "node internal/check-embed-base-resolution.mjs", - "check:composed-filters": "node internal/check-gospel-composed-filters.mjs" + "check:composed-filters": "node internal/check-gospel-composed-filters.mjs", + "app:start-page": "node internal/set-app-start-page.mjs", + "check:app-start-page": "node internal/set-app-start-page.mjs --check" }, "//scripts": { "_": "What each script does. These notes used to sit inside the command strings as trailing \"# …\" comments, which made the shell discard any arguments passed after \"--\" (so a gate given --url silently measured its hardcoded default port — possibly another agent's server). Keep notes here, never in a command.",