The poster print preview rasterises too, and a deployment can ship sources without rasters
Some checks are pending
build-gate / build (push) Waiting to run
Some checks are pending
build-gate / build (push) Waiting to run
posters/*.webp is not shipped by default (42MB; bundling pushed the tree to 80MB,
git-packed to 57MiB, and the catalog rejected the push). The DOWNLOAD path has
handled that since d154a76 — PosterWall fetches posters/<id>.html and rasterises
it in the browser. The PRINT path did not: it handed an <img> a url it never
checked, so on a default drop the SAME poster downloaded fine and PRINTED as
"Poster not available yet." That is the bad first impression
IDEA-POSTER-EXPORT-404-ON-DROPPED-FOLDER-TRADEOFF called out by name.
Three things were missing; all three are here:
1. printPoster now climbs the same ladder as the download path — probe the webp,
and only on a miss fetch the .html, rasterise, and verify PIXELS before opening
the dialog (an SVG in an <img> is an isolated document: a lost stylesheet prints
a black square with no error). Blob urls are held until unmount, not revoked at
print time — the preview <img> is still reading from them.
2. publish-html.mjs wrote into public/assets/posters, which is a GENERATED MIRROR
that every `bun run tree` rmSync's. Its output never survived, which is a large
part of why the fallback still had no input months after it shipped. Retargeted
at data/posters, the tracked source the mirror is made from.
3. bundlePosters is one boolean over a directory holding 40.9MB of rasters AND the
28 sources, so it could only ever ship both or neither — and "neither" is what
made both full-size paths 404. posterSources:true ships the sources without the
rasters, the combination that was previously inexpressible. DEFAULT UNCHANGED:
28 sources are 27.9MB raw (6.9MB packed, measured with pack-objects), real
weight in a published tree that needs a deployment decision, not a silent one.
VERIFIED AT THE RENDERED LAYER, not by reading source. check:poster-print-fallback
clicks the real control with every posters/*.webp ABORTED at the network layer —
the default-drop condition forced rather than hoped for — and asserts the HUD
enters "generating the poster" and settles clean. Mutation-tested: removing the
fallback produces 4 failures including "no posters/*.html was fetched". The other
branch is verified too — with a webp present it is used directly, zero html
fetches, no rasterise — so the extra probe is paid only on a miss.
★ THE GATE WAS WRONG THREE TIMES FIRST, each time blaming the product for a
harness fault, which is why it now asserts the HUD instead of an internal:
- it matched /print/i, but the control reads "UPG prayer poster" — the product
names the ARTEFACT, not the action;
- it patched Document.prototype in the PARENT realm while openPrintFrame writes
into the iframe's own;
- openPrintFrame calls doc.open() AFTER append, and open() RESETS the document,
discarding a write patched at append time.
Through all three the .webp/.html request counters looked corroborating, because
the wall loads both by itself. The HUD is what a user sees, survives a refactor of
the print internals, and cannot silently miss.
The 28 poster sources are committed because the fallback has no input without
them. city-* posters are deliberately NOT covered: they fetch OSM street tiles at
generation time, so they are not reproducible offline and their rasters must stay.
This commit is contained in:
parent
f47ee3dfbe
commit
3a468c2bc3
40 changed files with 10314 additions and 90 deletions
355
data/posters/andean-caribbean.html
Normal file
355
data/posters/andean-caribbean.html
Normal file
File diff suppressed because one or more lines are too long
355
data/posters/asia-china-tibet-autonomous-region.html
Normal file
355
data/posters/asia-china-tibet-autonomous-region.html
Normal file
File diff suppressed because one or more lines are too long
355
data/posters/asia-china-yunnan-province.html
Normal file
355
data/posters/asia-china-yunnan-province.html
Normal file
File diff suppressed because one or more lines are too long
355
data/posters/asia-india-andhra-pradesh.html
Normal file
355
data/posters/asia-india-andhra-pradesh.html
Normal file
File diff suppressed because one or more lines are too long
355
data/posters/asia-india-gujarat.html
Normal file
355
data/posters/asia-india-gujarat.html
Normal file
File diff suppressed because one or more lines are too long
355
data/posters/asia-india-karnataka.html
Normal file
355
data/posters/asia-india-karnataka.html
Normal file
File diff suppressed because one or more lines are too long
355
data/posters/asia-india-kerala.html
Normal file
355
data/posters/asia-india-kerala.html
Normal file
File diff suppressed because one or more lines are too long
355
data/posters/asia-india-madhya-pradesh.html
Normal file
355
data/posters/asia-india-madhya-pradesh.html
Normal file
File diff suppressed because one or more lines are too long
355
data/posters/asia-india-odisha.html
Normal file
355
data/posters/asia-india-odisha.html
Normal file
File diff suppressed because one or more lines are too long
355
data/posters/asia-india-rajasthan.html
Normal file
355
data/posters/asia-india-rajasthan.html
Normal file
File diff suppressed because one or more lines are too long
355
data/posters/asia-india-tamil-nadu.html
Normal file
355
data/posters/asia-india-tamil-nadu.html
Normal file
File diff suppressed because one or more lines are too long
355
data/posters/asia-india-uttarakhand.html
Normal file
355
data/posters/asia-india-uttarakhand.html
Normal file
File diff suppressed because one or more lines are too long
355
data/posters/asia-india-west-bengal.html
Normal file
355
data/posters/asia-india-west-bengal.html
Normal file
File diff suppressed because one or more lines are too long
355
data/posters/brazil.html
Normal file
355
data/posters/brazil.html
Normal file
File diff suppressed because one or more lines are too long
355
data/posters/central-africa.html
Normal file
355
data/posters/central-africa.html
Normal file
File diff suppressed because one or more lines are too long
355
data/posters/deaf.html
Normal file
355
data/posters/deaf.html
Normal file
File diff suppressed because one or more lines are too long
355
data/posters/east-africa.html
Normal file
355
data/posters/east-africa.html
Normal file
File diff suppressed because one or more lines are too long
355
data/posters/europe.html
Normal file
355
data/posters/europe.html
Normal file
File diff suppressed because one or more lines are too long
355
data/posters/laos.html
Normal file
355
data/posters/laos.html
Normal file
File diff suppressed because one or more lines are too long
355
data/posters/maghreb-egypt.html
Normal file
355
data/posters/maghreb-egypt.html
Normal file
File diff suppressed because one or more lines are too long
355
data/posters/mainland-se-asia-philippines.html
Normal file
355
data/posters/mainland-se-asia-philippines.html
Normal file
File diff suppressed because one or more lines are too long
355
data/posters/middle-east-core.html
Normal file
355
data/posters/middle-east-core.html
Normal file
File diff suppressed because one or more lines are too long
355
data/posters/oceania-indonesia.html
Normal file
355
data/posters/oceania-indonesia.html
Normal file
File diff suppressed because one or more lines are too long
355
data/posters/southern-africa-islands.html
Normal file
355
data/posters/southern-africa-islands.html
Normal file
File diff suppressed because one or more lines are too long
355
data/posters/sudan.html
Normal file
355
data/posters/sudan.html
Normal file
File diff suppressed because one or more lines are too long
355
data/posters/uncovered-nas-a.html
Normal file
355
data/posters/uncovered-nas-a.html
Normal file
File diff suppressed because one or more lines are too long
355
data/posters/uncovered-scatter.html
Normal file
355
data/posters/uncovered-scatter.html
Normal file
File diff suppressed because one or more lines are too long
355
data/posters/west-africa.html
Normal file
355
data/posters/west-africa.html
Normal file
File diff suppressed because one or more lines are too long
193
internal/check-poster-print-fallback.mjs
Normal file
193
internal/check-poster-print-fallback.mjs
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
#!/usr/bin/env node
|
||||
// check-poster-print-fallback — the PRINT preview must survive a missing .webp.
|
||||
//
|
||||
// ★ THE ASYMMETRY THIS GUARDS. posters/*.webp is not shipped by default (42MB;
|
||||
// bundling it pushed the tree to 80MB, git-packed to 57MiB, and the catalog
|
||||
// rejected the push outright). The DOWNLOAD path has always handled that —
|
||||
// PosterWall.vue fetches posters/<id>.html and rasterises it in the browser. The
|
||||
// PRINT path did not: it handed an <img> a url it never checked, so on a default
|
||||
// drop the SAME poster downloaded fine and printed as "Poster not available yet."
|
||||
//
|
||||
// Two paths, one missing file, different outcomes — and the difference was
|
||||
// invisible to every existing gate, because check:poster-raster drives the
|
||||
// RENDERER directly and never goes near the print button.
|
||||
//
|
||||
// ⚠ IT INTERCEPTS THE PRINTED HTML rather than reading the frame back.
|
||||
// openPrintFrame calls window.print(), which BLOCKS in headless Chromium, and the
|
||||
// frame self-removes on afterprint — reading it back measures a frame that has
|
||||
// already gone. Same technique as check-deck-click.
|
||||
//
|
||||
// ⚠ IT FORCES THE MISS. The dev tree may or may not have the .webp; a gate whose
|
||||
// result depends on that is measuring the fixture, not the product. So every
|
||||
// request for posters/<id>.webp is failed at the network layer, which is exactly
|
||||
// the default-drop condition, and the .html is left reachable.
|
||||
//
|
||||
// Usage: node internal/check-poster-print-fallback.mjs <wallUrl> [zoneId]
|
||||
import { chromium } from 'playwright'
|
||||
|
||||
// SKIP, never fail, when the preconditions are absent. This gate needs a SERVED tree
|
||||
// that carries posters/<id>.html (posterSources:true or bundlePosters:true). The
|
||||
// default tree ships neither, and a clean checkout has no server at all — failing the
|
||||
// build on that would only teach people to disable the gate. It says what is missing.
|
||||
const WALL = process.argv[2] || process.env.POSTER_WALL_URL
|
||||
if (!WALL) {
|
||||
console.log('POSTER-PRINT-FALLBACK: SKIP — no wall url given.')
|
||||
console.log(' Pass one, or set POSTER_WALL_URL. Needs a tree built with posterSources:true')
|
||||
console.log(' and a server: `node library/scripts/posters/publish-html.mjs && bun run tree`.')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
// The poster SOURCE must be reachable before any of this means anything. Without it
|
||||
// the fallback has nothing to rasterise and every assertion below would report a
|
||||
// product bug that is really a missing fixture.
|
||||
const probeId = process.argv[3] || 'laos'
|
||||
const srcUrl = new URL(`../assets/posters/${probeId}.html`, WALL).href
|
||||
try {
|
||||
const r = await fetch(srcUrl)
|
||||
if (!r.ok) {
|
||||
console.log(`POSTER-PRINT-FALLBACK: SKIP — ${srcUrl} is ${r.status}.`)
|
||||
console.log(' The tree was not built with posterSources:true, so there is no poster source to rasterise.')
|
||||
process.exit(0)
|
||||
}
|
||||
} catch {
|
||||
console.log(`POSTER-PRINT-FALLBACK: SKIP — cannot reach ${srcUrl} (no server?).`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const browser = await chromium.launch({ args: ['--use-gl=swiftshader', '--enable-unsafe-swiftshader'] })
|
||||
const page = await browser.newPage({ viewport: { width: 1400, height: 1000 } })
|
||||
const errs = []
|
||||
page.on('pageerror', (e) => errs.push(String(e).slice(0, 140)))
|
||||
|
||||
// THE DEFAULT-DROP CONDITION, forced: the print-resolution image is absent, its
|
||||
// HTML source is not. Anything else would leave the gate at the mercy of whether
|
||||
// this particular checkout happens to carry 42MB of posters.
|
||||
let webpBlocked = 0
|
||||
let htmlServed = 0
|
||||
await page.route('**/posters/*.webp', (route) => { webpBlocked += 1; route.abort() })
|
||||
await page.route('**/posters/*.html', (route) => { htmlServed += 1; route.continue() })
|
||||
|
||||
await page.goto(WALL, { waitUntil: 'load', timeout: 45000 })
|
||||
await page.waitForTimeout(8000)
|
||||
|
||||
// Capture what reaches the print frame instead of letting it print-and-vanish.
|
||||
// ⚠ ASSERT THE HUD, NOT doc.write. Three separate attempts to intercept the printed
|
||||
// document all captured nothing while the feature DEMONSTRABLY worked (the HUD ran
|
||||
// "generating the poster" and finished clean): openPrintFrame writes into the iframe's
|
||||
// OWN realm, calls doc.open() AFTER append — which resets the document and discards a
|
||||
// patched write — and the frame self-removes on afterprint. A capture that silently
|
||||
// misses reports a product bug that is not there.
|
||||
// The HUD is the better assertion anyway: it is what a USER sees, it survives a
|
||||
// refactor of the print internals, and it distinguishes the three outcomes that
|
||||
// matter — rasterised / shipped-image / failed — which is exactly what this gate is for.
|
||||
await page.evaluate(() => {
|
||||
window.__HUD = []
|
||||
window.print = () => {}
|
||||
const walk = (root, out = []) => {
|
||||
for (const el of root.querySelectorAll('*')) { out.push(el); if (el.shadowRoot) walk(el.shadowRoot, out) }
|
||||
return out
|
||||
}
|
||||
window.__readHud = () => walk(document)
|
||||
.filter((e) => e.childElementCount === 0 && e.tagName !== 'SCRIPT' && e.tagName !== 'STYLE')
|
||||
.map((e) => (e.textContent || '').trim())
|
||||
.filter((t) => t && /generating|preparing|saving|not available|failed|blank|error/i.test(t))
|
||||
setInterval(() => { for (const t of window.__readHud()) if (!window.__HUD.includes(t)) window.__HUD.push(t) }, 120)
|
||||
})
|
||||
|
||||
// Click the REAL print control, found by its accessible name through every shadow root.
|
||||
const clicked = await page.evaluate(() => {
|
||||
const walk = (root, out = []) => {
|
||||
for (const el of root.querySelectorAll('*')) {
|
||||
out.push(el)
|
||||
if (el.shadowRoot) walk(el.shadowRoot, out)
|
||||
}
|
||||
return out
|
||||
}
|
||||
const all = walk(document)
|
||||
const btn = all.find((el) => {
|
||||
if (!(el instanceof HTMLElement)) return false
|
||||
if (el.tagName !== 'BUTTON' && el.getAttribute('role') !== 'button') return false
|
||||
const label = `${el.getAttribute('aria-label') || ''} ${el.getAttribute('title') || ''} ${el.textContent || ''}`
|
||||
// ⚠ THE CONTROL IS NOT LABELLED "print". It reads "UPG prayer poster" — the
|
||||
// product names the ARTEFACT, not the action. A /print/i matcher finds nothing
|
||||
// and this gate then reports "nothing reached the print frame", which reads as a
|
||||
// product bug and is purely the selector. Measured 2026-09-24: the .webp and
|
||||
// .html request counters even LOOKED corroborating, because the wall loads both
|
||||
// on its own. Match the poster control, and exclude its siblings by name.
|
||||
return /poster/i.test(label) && !/list|deck|packet|show on map/i.test(label)
|
||||
})
|
||||
if (!btn) {
|
||||
return { ok: false, labels: all.filter((e) => e.tagName === 'BUTTON')
|
||||
.map((e) => (e.getAttribute('title') || e.textContent || '').trim().slice(0, 30)).filter(Boolean).slice(0, 14) }
|
||||
}
|
||||
btn.click()
|
||||
return { ok: true }
|
||||
})
|
||||
|
||||
let hud = []
|
||||
let settled = false
|
||||
if (clicked.ok) {
|
||||
// Rasterising a 3000px poster takes real time; poll for the terminal state rather
|
||||
// than guessing one timeout.
|
||||
for (let i = 0; i < 45 && !settled; i++) {
|
||||
await page.waitForTimeout(1000)
|
||||
hud = await page.evaluate(() => window.__HUD || [])
|
||||
// Terminal states: an explicit failure, or "generating" seen AND no longer showing.
|
||||
const live = await page.evaluate(() => window.__readHud())
|
||||
const failing = hud.some((t) => /not available|failed|blank|error/i.test(t))
|
||||
const generated = hud.some((t) => /generating/i.test(t))
|
||||
if (failing) { settled = true; break }
|
||||
if (generated && !live.some((t) => /generating|preparing|saving/i.test(t))) { settled = true }
|
||||
}
|
||||
}
|
||||
await browser.close()
|
||||
|
||||
const fails = []
|
||||
if (!clicked.ok) {
|
||||
fails.push(`no poster print control found in any shadow root (buttons seen: ${(clicked.labels || []).join(' | ')})`)
|
||||
}
|
||||
if (clicked.ok && !hud.length) {
|
||||
fails.push('the poster control was clicked but the export HUD never reported anything — '
|
||||
+ 'the click did not reach printPoster at all')
|
||||
}
|
||||
// THE LOAD-BEARING ASSERTIONS.
|
||||
if (hud.some((t) => /Poster not available yet/i.test(t))) {
|
||||
fails.push('the print preview opened on "Poster not available yet" — the old behaviour: '
|
||||
+ 'the .webp 404ed and the rasterise fallback did not run')
|
||||
}
|
||||
if (hud.some((t) => /not available|failed|blank|error/i.test(t))) {
|
||||
fails.push(`the print path ended in an error state: ${JSON.stringify(hud.filter((t) => /not available|failed|blank|error/i.test(t)))}`)
|
||||
}
|
||||
// With EVERY .webp aborted, a clean run is only possible via the rasteriser — so the
|
||||
// "generating the poster" phase MUST have been entered. Its absence means the preview
|
||||
// came from somewhere this gate did not intend, and proves nothing about the fallback.
|
||||
if (clicked.ok && !hud.some((t) => /generating/i.test(t))) {
|
||||
fails.push(`the fallback never entered its "generating the poster" phase (HUD: ${JSON.stringify(hud)}) — `
|
||||
+ 'with every .webp aborted there is no other way to a working preview, so this did not exercise the fallback')
|
||||
}
|
||||
if (clicked.ok && !settled) {
|
||||
fails.push('the print path never reached a terminal state within 45s')
|
||||
}
|
||||
if (webpBlocked === 0) {
|
||||
fails.push('no posters/*.webp request was made at all — the print path never probed the '
|
||||
+ 'image, so this gate did not exercise the fallback it exists to test')
|
||||
}
|
||||
if (htmlServed === 0) {
|
||||
fails.push('no posters/*.html was fetched — the rasterise fallback never ran')
|
||||
}
|
||||
if (errs.length) fails.push(`${errs.length} page error(s): ${errs[0]}`)
|
||||
|
||||
console.log('POSTER-PRINT-FALLBACK')
|
||||
console.log(` wall : ${WALL}`)
|
||||
console.log(` .webp requests : ${webpBlocked} (all aborted — the default-drop condition)`)
|
||||
console.log(` .html requests : ${htmlServed}`)
|
||||
console.log(` print button : ${clicked.ok ? 'found and clicked' : 'NOT FOUND'}`)
|
||||
console.log(` HUD : ${hud.length ? JSON.stringify(hud) : '(nothing)'}`)
|
||||
|
||||
if (fails.length) {
|
||||
console.error(`\nPOSTER-PRINT-FALLBACK: FAIL — ${fails.length} problem(s)`)
|
||||
for (const f of fails) console.error(` - ${f}`)
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('\nPOSTER-PRINT-FALLBACK: PASS — with every .webp missing, the print preview still '
|
||||
+ 'opens on a real rasterised poster instead of "Poster not available yet".')
|
||||
|
|
@ -96,9 +96,14 @@ function readBuildConfig() {
|
|||
// from previews/, so the tree stays visually complete without them; bundling both pushed the
|
||||
// tree to 74 MB and the catalog server refused the pack. Turn on only for a fully offline drop.
|
||||
const bundlePosters = cfg.bundlePosters === true
|
||||
return { name, paths, embedBaseUrl, tokenUrl, token, assetsBaseUrl, apiBaseUrl, gatedProfiles, qaWidget, bundlePosters }
|
||||
// Ship the poster SOURCES (<id>.html, what the browser rasterises) without the
|
||||
// 40.9 MB of rasters. bundlePosters is a single boolean over a directory holding
|
||||
// both, so it could not express this — and "neither" is what left export and print
|
||||
// preview 404ing on a default drop. Implied by bundlePosters:true.
|
||||
const posterSources = cfg.posterSources === true || bundlePosters
|
||||
return { name, paths, embedBaseUrl, tokenUrl, token, assetsBaseUrl, apiBaseUrl, gatedProfiles, qaWidget, bundlePosters, posterSources }
|
||||
} catch {
|
||||
return { name: 'doxa-maps', paths: [], embedBaseUrl: '', tokenUrl: '', token: '', assetsBaseUrl: '', apiBaseUrl: '', gatedProfiles: [], qaWidget: true, bundlePosters: false }
|
||||
return { name: 'doxa-maps', paths: [], embedBaseUrl: '', tokenUrl: '', token: '', assetsBaseUrl: '', apiBaseUrl: '', gatedProfiles: [], qaWidget: true, bundlePosters: false, posterSources: false }
|
||||
}
|
||||
}
|
||||
const DEST_CONFIG = readBuildConfig()
|
||||
|
|
@ -1481,17 +1486,47 @@ function main() {
|
|||
// so the URL is fetched from INSIDE the tree; with images/ absent from this allowlist it
|
||||
// 404s and the watermark is simply not there, with no error anywhere — the background just
|
||||
// does not paint, which is even quieter than the font failure was.
|
||||
const assetDirs = DEST_CONFIG.bundlePosters
|
||||
// ★ posters/ CARRIES TWO VERY DIFFERENT PAYLOADS and bundlePosters is one boolean,
|
||||
// so it could only ever ship both or neither:
|
||||
// <id>.webp 378 files, 40.9 MB — the print-resolution rasters. This is the weight
|
||||
// that pushed the tree to 80MB and got the push rejected.
|
||||
// <id>.html 28 files, the poster SOURCE the browser rasterises when no .webp is
|
||||
// present (library/packet/poster-raster.js). ~0.3 MB each.
|
||||
// "Neither" is what a default drop got, and it is why BOTH full-size paths 404'd:
|
||||
// export fell back to fetching <id>.html and that was not shipped either, so the
|
||||
// fallback had nothing to fetch. posterSources ships the sources WITHOUT the rasters,
|
||||
// which is the only combination the old boolean could not express.
|
||||
// Default stays FALSE — 28 sources are ~27.9 MB raw, real weight that needs a
|
||||
// deployment decision, not a silent default. This makes the choice available.
|
||||
const posterExts = DEST_CONFIG.bundlePosters ? null // everything
|
||||
: (DEST_CONFIG.posterSources ? ['.html'] : []) // sources only / nothing
|
||||
const assetDirs = (posterExts === null || posterExts.length)
|
||||
? ['previews', 'posters', 'fonts', 'images']
|
||||
: ['previews', 'fonts', 'images']
|
||||
for (const dirName of assetDirs) {
|
||||
const src = join(pubAssets, dirName)
|
||||
if (!existsSync(src)) { console.warn(`[generate-tree] WARN missing tree asset dir: assets/${dirName}`); continue }
|
||||
if (dirName === 'posters' && posterExts !== null) {
|
||||
// Selective copy: only the extensions this deployment asked for.
|
||||
mkdirSync(join(treeAssets, dirName), { recursive: true })
|
||||
let n = 0
|
||||
for (const f of readdirSync(src)) {
|
||||
if (!posterExts.some((e) => f.endsWith(e))) continue
|
||||
cpSync(join(src, f), join(treeAssets, dirName, f))
|
||||
n += 1
|
||||
}
|
||||
staged += n
|
||||
continue
|
||||
}
|
||||
cpSync(src, join(treeAssets, dirName), { recursive: true })
|
||||
staged += readdirSync(src).length
|
||||
}
|
||||
if (!DEST_CONFIG.bundlePosters) {
|
||||
console.log('[generate-tree] posters/ NOT bundled (bundlePosters:false) — the wall renders from previews; set an absolute assetsBaseUrl to serve full posters on demand')
|
||||
if (DEST_CONFIG.bundlePosters) {
|
||||
console.log('[generate-tree] posters/ bundled in full (bundlePosters:true) — rasters + sources')
|
||||
} else if (DEST_CONFIG.posterSources) {
|
||||
console.log('[generate-tree] posters/*.html bundled (posterSources:true) — no rasters; export and print RASTERISE in the browser')
|
||||
} else {
|
||||
console.log('[generate-tree] posters/ NOT bundled — the wall renders from previews; export and print need posterSources:true, bundlePosters:true, or an absolute assetsBaseUrl')
|
||||
}
|
||||
console.log(`[generate-tree] staged ${staged} runtime asset(s) → ${TREE_NAME}/assets (tree is self-contained)`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,13 @@ import { dirname, join, resolve } from 'node:path';
|
|||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const SRC = join(__dirname, 'out', 'posters');
|
||||
const REPO = resolve(__dirname, '..', '..', '..');
|
||||
const DEST = join(REPO, 'public', 'assets', 'posters');
|
||||
// ⚠ data/posters, NOT public/assets/posters. public/assets/<name> is a GENERATED
|
||||
// MIRROR: generate-tree.mjs rmSync's it and re-copies data/<name> over it on every
|
||||
// `bun run tree`, so anything written there is deleted by the next build. This
|
||||
// script wrote there and its output therefore never survived — which is a large
|
||||
// part of why the browser-rasterise fallback still had no input months after it
|
||||
// shipped. data/ is the tracked source the mirror is made from.
|
||||
const DEST = join(REPO, 'data', 'posters');
|
||||
const ONLY = process.env.ONLY || '';
|
||||
|
||||
if (!existsSync(SRC)) {
|
||||
|
|
@ -88,7 +94,7 @@ for (const file of readdirSync(SRC)) {
|
|||
bytes += Buffer.byteLength(html);
|
||||
}
|
||||
|
||||
console.log(`[publish-html] ${published} poster HTML -> public/assets/posters/`);
|
||||
console.log(`[publish-html] ${published} poster HTML -> data/posters/`);
|
||||
console.log(`[publish-html] ${(bytes / 1048576).toFixed(1)} MB raw `
|
||||
+ `(gzips to roughly a third over the wire)`);
|
||||
if (skipped.length) {
|
||||
|
|
|
|||
|
|
@ -145,7 +145,8 @@
|
|||
"build:bundles": "node internal/run-parallel.mjs --bundles",
|
||||
"check:maplibre-drops": "node internal/check-maplibre-layer-drops.mjs",
|
||||
"check:tabs-localized": "node internal/check-tabs-localized.mjs # a chosen locale must CHANGE the rendered tab text. check:i18n-parity compares catalogs to each OTHER, so it cannot tell a shipped translation from a dead one; tabs.json is reached only via a DYNAMIC key (SemanticTreeLegend.vue:40), invisible to any grep. Renders it and reads the result.",
|
||||
"check:packets-all": "node internal/check-packets-all.mjs # runs check:packet-render over EVERY generated packet, not one hand-named file. The single-file gate SKIPs with no argument, so it was in package.json, passing in all 11 locales, and in neither gates:pre nor gates:post — it only ran when someone typed a filename. SKIPs when no packets are generated."
|
||||
"check:packets-all": "node internal/check-packets-all.mjs # runs check:packet-render over EVERY generated packet, not one hand-named file. The single-file gate SKIPs with no argument, so it was in package.json, passing in all 11 locales, and in neither gates:pre nor gates:post — it only ran when someone typed a filename. SKIPs when no packets are generated.",
|
||||
"check:poster-print-fallback": "node internal/check-poster-print-fallback.mjs # clicks the REAL poster print control with every posters/*.webp aborted, and asserts the preview RASTERISES from posters/<id>.html instead of opening on \"Poster not available yet\". Needs a served tree built with posterSources:true; SKIPs otherwise."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/android": "^7.6.9",
|
||||
|
|
|
|||
|
|
@ -30,9 +30,15 @@
|
|||
* (~42MB). See IDEA-POSTER-EXPORT-404-ON-DROPPED-FOLDER-TRADEOFF:
|
||||
* bundling them pushed the tree to 80MB, git-packed to 57MiB, and the
|
||||
* catalog REJECTED the push — so the all-in version shipped nothing.
|
||||
* Export and print-preview therefore 404 on a default drop and degrade
|
||||
* honestly; set bundlePosters:true or point assetsBase at a host that
|
||||
* serves posters/ to restore them.
|
||||
* Export and print-preview no longer 404 on a default drop: BOTH now
|
||||
* fall back to fetching posters/<id>.html and rasterising it in the
|
||||
* browser (library/packet/poster-raster.js). Print was the odd one out
|
||||
* until 2026-09-24 — the same poster downloaded fine and PRINTED as
|
||||
* "Poster not available yet." bundlePosters:true or an assetsBase that
|
||||
* serves posters/ still short-circuits the fallback with the shipped webp.
|
||||
* posters/<id>.html — the poster SOURCE, what the fallback rasterises. Also not shipped by
|
||||
* default (28 zones, 27.9MB raw), so on a drop with neither file the
|
||||
* preview still refuses to open rather than printing a blank sheet.
|
||||
* The @error fallback to a labelled placeholder is still live and still the right behaviour —
|
||||
* it now covers the missing-poster case rather than the nothing-generated-yet case.
|
||||
*/
|
||||
|
|
@ -267,6 +273,16 @@ async function renderFor(list, spec) {
|
|||
const EXPORT_SPEC = { widthIn: 8.5, heightIn: 12.75, dpi: 150 } // single-canvas, phone-safe
|
||||
const exportState = ref(null) // { label, phase, message, pct, kind }
|
||||
|
||||
// Blob URLs minted for a rasterised print preview. They CANNOT be revoked when the
|
||||
// print window opens — the preview <img> is still loading from them, and revoking
|
||||
// early gives a blank sheet — so they are held until this element goes away.
|
||||
// One poster is ~1.5MB as a blob; a reader printing a dozen in one session would
|
||||
// otherwise pin ~18MB for the life of the page.
|
||||
const printObjectUrls = []
|
||||
onBeforeUnmount(() => {
|
||||
for (const u of printObjectUrls.splice(0)) { try { URL.revokeObjectURL(u) } catch { /* already gone */ } }
|
||||
})
|
||||
|
||||
// CARD STOCK for the prayer-card deck. The Driver asked to be able to "choose the
|
||||
// layout of the print dialog to choose the aspect ratio of these prayer cards", so
|
||||
// the stock is a user choice, not a constant. Poker (2.5x3.5in) is the default: it
|
||||
|
|
@ -369,7 +385,40 @@ async function printPoster(list) {
|
|||
exportState.value = { label: list.label, kind: 'Poster', phase: 'fetch',
|
||||
message: 'preparing print preview', pct: 30 }
|
||||
try {
|
||||
const url = posterUrl(list.id)
|
||||
let url = posterUrl(list.id)
|
||||
// ── THE SAME FALLBACK LADDER THE DOWNLOAD PATH ALREADY CLIMBS ───────────
|
||||
// ★ THE ASYMMETRY THIS FIXES. posters/*.webp is NOT shipped by default (42MB;
|
||||
// bundling it pushed the tree to 80MB, git-packed to 57MiB, and the catalog
|
||||
// rejected the push). The DOWNLOAD path handles that: PosterWall.vue fetches
|
||||
// posters/<id>.html and rasterises it in the browser. The PRINT path did not —
|
||||
// it handed the <img> a url it never checked and, on 404, replaced the whole
|
||||
// preview with "Poster not available yet."
|
||||
// So on a default drop the same poster DOWNLOADED fine and PRINTED as an error
|
||||
// message. The card that recorded this (IDEA-POSTER-EXPORT-404-ON-DROPPED-FOLDER-
|
||||
// TRADEOFF) calls it out precisely: "a print preview that opens on 'Poster not
|
||||
// available yet' is a worse first impression than a failed download."
|
||||
//
|
||||
// Probing costs one HEAD-like GET that is almost always a cache hit when the
|
||||
// image IS shipped, and only on a miss do we pay for the HTML.
|
||||
let res = null
|
||||
try { res = await fetch(url) } catch { /* offline/blocked — treated as a miss */ }
|
||||
if (!res || !res.ok) {
|
||||
exportState.value = { ...exportState.value, phase: 'render',
|
||||
message: 'generating the poster', pct: 55 }
|
||||
const htmlRes = await fetch(`${assetsBase.value}/posters/${list.id}.html`)
|
||||
// Still honest when there is genuinely nothing to render: the preview only
|
||||
// opens once we HAVE a poster, so the reader never sees a blank print dialog.
|
||||
if (!htmlRes.ok) throw new Error(`poster not available (${htmlRes.status})`)
|
||||
const { rasterPoster, looksRendered } = await import('@map/packet/poster-raster.js')
|
||||
const blob = await rasterPoster(await htmlRes.text(), { scale: 2 })
|
||||
// ⚠ An SVG loaded through <img> is an isolated document: a lost stylesheet makes
|
||||
// every fill fall back to BLACK, with no error and a normal file size. Printing
|
||||
// a black square is worse than failing, so verify pixels before opening the
|
||||
// dialog — the same guard the download path uses.
|
||||
if (!(await looksRendered(blob))) throw new Error('the generated poster rendered blank')
|
||||
url = URL.createObjectURL(blob)
|
||||
printObjectUrls.push(url)
|
||||
}
|
||||
// Preview + print in a dedicated window: a single centered, page-fit image and an
|
||||
// onload -> print() so the dialog opens on the poster the user is looking at. The
|
||||
// poster is a 3ft square print (see the template) → @page portrait, image scaled to
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
2
public/doxa-maps/index.html
generated
2
public/doxa-maps/index.html
generated
File diff suppressed because one or more lines are too long
8
public/doxa-maps/manifest.json
generated
8
public/doxa-maps/manifest.json
generated
|
|
@ -658,7 +658,7 @@
|
|||
"bundle": "my-upg-100-list",
|
||||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||
"output": "doxa-maps/my-upg-100-list/my-upg-100-list.js",
|
||||
"integrity": "sha384-DTQvsXU3C5sstv8K7pnmiFWMP23nSlwYzIHbwpVoqQzQc+7tGx9IQvPXbMqAPA5p",
|
||||
"integrity": "sha384-eIh/eQ3SOnMBPQh5z4wAfhFK1MScit5ksl9pVBqHe3wPEuT48xlPwZz1OGwPSICH",
|
||||
"elements": [
|
||||
"my-upg-100-list-map"
|
||||
],
|
||||
|
|
@ -678,7 +678,7 @@
|
|||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./my-upg-100-list/poster-wall/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-DTQvsXU3C5sstv8K7pnmiFWMP23nSlwYzIHbwpVoqQzQc+7tGx9IQvPXbMqAPA5p\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-eIh/eQ3SOnMBPQh5z4wAfhFK1MScit5ksl9pVBqHe3wPEuT48xlPwZz1OGwPSICH\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -692,7 +692,7 @@
|
|||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-DTQvsXU3C5sstv8K7pnmiFWMP23nSlwYzIHbwpVoqQzQc+7tGx9IQvPXbMqAPA5p\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-eIh/eQ3SOnMBPQh5z4wAfhFK1MScit5ksl9pVBqHe3wPEuT48xlPwZz1OGwPSICH\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -706,7 +706,7 @@
|
|||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-DTQvsXU3C5sstv8K7pnmiFWMP23nSlwYzIHbwpVoqQzQc+7tGx9IQvPXbMqAPA5p\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-eIh/eQ3SOnMBPQh5z4wAfhFK1MScit5ksl9pVBqHe3wPEuT48xlPwZz1OGwPSICH\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
138
public/doxa-maps/my-upg-100-list/my-upg-100-list.js
generated
138
public/doxa-maps/my-upg-100-list/my-upg-100-list.js
generated
File diff suppressed because one or more lines are too long
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-DTQvsXU3C5sstv8K7pnmiFWMP23nSlwYzIHbwpVoqQzQc+7tGx9IQvPXbMqAPA5p" crossorigin="anonymous"></script>
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-eIh/eQ3SOnMBPQh5z4wAfhFK1MScit5ksl9pVBqHe3wPEuT48xlPwZz1OGwPSICH" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-DTQvsXU3C5sstv8K7pnmiFWMP23nSlwYzIHbwpVoqQzQc+7tGx9IQvPXbMqAPA5p" crossorigin="anonymous"></script>
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-eIh/eQ3SOnMBPQh5z4wAfhFK1MScit5ksl9pVBqHe3wPEuT48xlPwZz1OGwPSICH" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-DTQvsXU3C5sstv8K7pnmiFWMP23nSlwYzIHbwpVoqQzQc+7tGx9IQvPXbMqAPA5p" crossorigin="anonymous"></script>
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-eIh/eQ3SOnMBPQh5z4wAfhFK1MScit5ksl9pVBqHe3wPEuT48xlPwZz1OGwPSICH" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
Loading…
Reference in a new issue