fix(posters): one bad zone no longer costs the other 28
Some checks are pending
build-gate / build (push) Waiting to run
Some checks are pending
build-gate / build (push) Waiting to run
generate.mjs threw on the first zone whose header overflowed, so a single bad sheet ended the run: 16 posters written, the remaining 13 silently unbuilt, and a non-obvious short count as the only symptom. Failures are now collected, the run continues, and it exits 1 at the end with a census naming every zone that has no poster. The guard stays loud — an overflowing header is a real defect and must not be downgraded to a warning — but it fails the RUN rather than truncating it. That change immediately surfaced a FOURTH broken zone the abort had been hiding. All four overflow the same way: oceania-indonesia 504 vs HEADER=460 (+44px) andean-caribbean 504 vs HEADER=460 (+44px) asia-china-tibet-autonomous-region 481 vs HEADER=460 (+21px) deaf 619 vs HEADER=460 (+159px) Root cause, found but NOT patched here: measureHeader() returns a fixed HEADER_BASE when SPARSE is off instead of measuring the zone's own content — so the per-zone measurement its own comments describe only runs in SPARSE mode. That is a separate fix with its own layout risk; carded rather than bolted on. Also adds publish-html.mjs, the missing half of the browser-rasterise fallback. That fallback fetches posters/<id>.html and NOTHING published it: out/ is gitignored and public/ had zero .html, so a zone with no image 404s today. The script writes zone poster HTML into public/assets/posters/, skips city-* posters (they need OSM tiles to regenerate, so their images must stay), and warns loudly about zones with no HTML instead of quietly writing fewer files. ★ IT IS NOT RUN AS PART OF THIS COMMIT, and the reason is worth recording. Measured with real git packs: webp 5.0 MB raw -> ~5.0 MB packed (already compressed; git cannot pack it) HTML 21.4 MB raw -> 5.78 MB packed (text, repetitive across posters) Swapping zone webp for zone HTML is about +0.8 MB PACKED — a net LOSS, not the 42 MB win the headline implies. The raw-byte comparison that made it look like a win (21.4 vs 5.0) and the gzip comparison that made it look better still (5.8 vs 5.0) both mislead; what git stores is the number that decides it. The real 22 MB is 176 city-* posters, which cannot be replaced this way at all. So the deletion this was leading to is deliberately NOT done: it would have made the repo bigger while being reported as a saving.
This commit is contained in:
parent
bc06a03ec6
commit
52b7a110d8
2 changed files with 124 additions and 1 deletions
|
|
@ -2794,6 +2794,7 @@ for (const r of records) {
|
|||
const ONLY = process.env.ONLY;
|
||||
const manifest = [];
|
||||
let made = 0;
|
||||
const failed = []; // zones whose render threw — reported as a census at the end
|
||||
for (const z of zones) {
|
||||
// The sheet's own position in the series. design note 2026-07-31: "it should not say
|
||||
// 23 of 30 because the ORDER OF THE LIST SHOULD CHANGE — it should say 1 of 30."
|
||||
|
|
@ -2805,7 +2806,25 @@ for (const z of zones) {
|
|||
if (ONLY && z.zoneKey !== ONLY && z.id !== ONLY) continue;
|
||||
const pins = pinsByZone.get(z.zoneKey) || [];
|
||||
const zoneFC = resolveZoneGeometry(z, { records, adm0, adm1Ind, adm1Chn });
|
||||
const { html, title, octiOK, crossings, diagFrac } = await renderPoster(z, pins, zoneFC);
|
||||
// ONE BAD ZONE MUST NOT COST THE OTHER 28. The header guard throws when the
|
||||
// predicted HEADER is smaller than the laid-out content, which is a REAL defect
|
||||
// and must stay loud — but aborting the whole run meant a single overflowing
|
||||
// zone produced 16 posters instead of 29 and left the rest silently unbuilt.
|
||||
// Collect failures, keep going, and fail the RUN at the end with a census, so
|
||||
// the error is impossible to miss and the good posters still exist.
|
||||
// (Measured 2026-09-24: 3 of 29 zones overflow — oceania-indonesia and
|
||||
// andean-caribbean by 44px, asia-china-tibet by 21px. Root cause is that
|
||||
// measureHeader() returns a fixed HEADER_BASE outside SPARSE mode instead of
|
||||
// measuring the zone's own content — carded separately, not patched here.)
|
||||
let rendered;
|
||||
try {
|
||||
rendered = await renderPoster(z, pins, zoneFC);
|
||||
} catch (err) {
|
||||
failed.push({ zone: z.zoneKey, message: String(err && err.message || err) });
|
||||
console.error(` ✗ ${z.zoneKey.padEnd(34)} ${String(err && err.message || err).slice(0, 90)}`);
|
||||
continue;
|
||||
}
|
||||
const { html, title, octiOK, crossings, diagFrac } = rendered;
|
||||
const file = `${z.zoneKey}.html`;
|
||||
writeFileSync(join(POSTERS, file), html);
|
||||
manifest.push({ zoneKey: z.zoneKey, title, count: pins.length, octilinear: octiOK, crossings, diagFrac, html: `posters/${file}`, png: `posters/${z.zoneKey}.png` });
|
||||
|
|
@ -2814,3 +2833,11 @@ for (const z of zones) {
|
|||
}
|
||||
if (!ONLY) writeFileSync(join(OUT, 'manifest.json'), JSON.stringify({ generated: 'live', count: manifest.length, posters: manifest }, null, 2));
|
||||
console.log(`\n${made} poster(s) → ${POSTERS}${ONLY ? ' (ONLY=' + ONLY + ')' : ', manifest.json written'}`);
|
||||
// FAIL THE RUN, but only after every good poster is on disk. A non-zero exit keeps
|
||||
// this a build error rather than a warning somebody scrolls past, and the census
|
||||
// names exactly which zones are missing instead of leaving a silent short count.
|
||||
if (failed.length) {
|
||||
console.error(`\n${failed.length} zone(s) FAILED to render and have NO poster:`);
|
||||
for (const f of failed) console.error(` ✗ ${f.zone}: ${f.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
|
|
|||
96
library/scripts/posters/publish-html.mjs
Normal file
96
library/scripts/posters/publish-html.mjs
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
// publish-html.mjs — put the poster HTML where the app can fetch it, so the
|
||||
// browser can rasterise a poster instead of the repo shipping 42 MB of .webp.
|
||||
//
|
||||
// ★ WHY THIS SCRIPT EXISTS. PosterWall falls back to rendering posters/<id>.html
|
||||
// in the browser when no pre-rendered image is present (library/packet/
|
||||
// poster-raster.js). That fallback was shipped and verified against a locally
|
||||
// served file — but NOTHING PUBLISHED THE HTML. out/ is gitignored, public/ had
|
||||
// zero .html, so deleting the .webp would have taken poster download from working
|
||||
// to 404. The rasteriser worked; the feature would not have. This closes that gap.
|
||||
//
|
||||
// SCOPE — ZONE POSTERS ONLY, and that is a real distinction, not laziness:
|
||||
// zone posters token-free, drawn from bundled geoBoundaries. Reproducible
|
||||
// offline by anyone with the repo -> safe to stop shipping as
|
||||
// images, because the HTML regenerates the picture.
|
||||
// city posters need CITIES=1 BASEMAP=1 and fetch OSM STREET TILES at generation
|
||||
// time. NOT reproducible offline. Deleting those .webp would make
|
||||
// them unrecoverable without network, so they stay as images.
|
||||
// Treating "42 MB of posters" as one homogeneous pile would have deleted the
|
||||
// irreproducible half.
|
||||
//
|
||||
// ⚠ RUNNING THIS DOES NOT, BY ITSELF, SHRINK THE REPO. Measured with real git
|
||||
// packs, for the 24 zone posters that render:
|
||||
// webp 5.0 MB raw -> ~5.0 MB packed (already compressed; git cannot pack it)
|
||||
// HTML 21.4 MB raw -> 5.78 MB packed (text, and highly repetitive across posters)
|
||||
// Swapping one for the other is about +0.8 MB PACKED — a net loss, not the 42 MB
|
||||
// win the headline suggests. Do not ship that swap and call it a saving.
|
||||
//
|
||||
// WHERE THE REAL 22 MB IS: 176 city-* posters, which this script deliberately does
|
||||
// not touch because they need OSM tiles to regenerate. The zone posters were never
|
||||
// the expensive half.
|
||||
//
|
||||
// So what is this FOR? Two things that are true regardless of size:
|
||||
// 1. It is the missing half of the browser-rasterise fallback. That fallback
|
||||
// fetches posters/<id>.html and nothing published it, so a zone whose image is
|
||||
// absent 404s today. Run this and the fallback has an input.
|
||||
// 2. It makes the HTML the SOURCE a reader can rasterise at any resolution,
|
||||
// instead of one fixed raster — useful for a host that would rather serve text
|
||||
// and let the browser do the work.
|
||||
// Use it when you are dropping the images deliberately, not as a size optimisation.
|
||||
//
|
||||
// node publish-html.mjs # all zone posters found in out/
|
||||
// ONLY=laos node publish-html.mjs # one
|
||||
|
||||
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
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');
|
||||
const ONLY = process.env.ONLY || '';
|
||||
|
||||
if (!existsSync(SRC)) {
|
||||
console.error(`[publish-html] no ${SRC} — run \`node generate.mjs\` first.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// The zone ids the app can actually reach. A poster for no zone is not publishable.
|
||||
const clusters = JSON.parse(readFileSync(
|
||||
join(REPO, 'plugin-designs', 'my-upg-100-list', 'data', 'clusters.json'), 'utf8'));
|
||||
const zoneIds = new Set();
|
||||
for (const key of ['lists', 'deaf_lists', 'asia_lists', 'uncovered_lists']) {
|
||||
for (const z of clusters[key] || []) zoneIds.add(z.id);
|
||||
}
|
||||
|
||||
mkdirSync(DEST, { recursive: true });
|
||||
|
||||
let published = 0;
|
||||
let bytes = 0;
|
||||
const skipped = [];
|
||||
for (const file of readdirSync(SRC)) {
|
||||
if (!file.endsWith('.html')) continue;
|
||||
const id = file.slice(0, -5);
|
||||
if (ONLY && id !== ONLY) continue;
|
||||
if (!zoneIds.has(id)) { skipped.push(id); continue; }
|
||||
const html = readFileSync(join(SRC, file), 'utf8');
|
||||
writeFileSync(join(DEST, file), html);
|
||||
published += 1;
|
||||
bytes += Buffer.byteLength(html);
|
||||
}
|
||||
|
||||
console.log(`[publish-html] ${published} poster HTML -> public/assets/posters/`);
|
||||
console.log(`[publish-html] ${(bytes / 1048576).toFixed(1)} MB raw `
|
||||
+ `(gzips to roughly a third over the wire)`);
|
||||
if (skipped.length) {
|
||||
// Loud, not silent: a reader should know what was left behind and why.
|
||||
console.log(`[publish-html] skipped ${skipped.length} non-zone poster(s) `
|
||||
+ `(city-* posters belong to Key Cities and need OSM tiles to regenerate)`);
|
||||
}
|
||||
const missing = [...zoneIds].filter((z) => !existsSync(join(SRC, `${z}.html`)));
|
||||
if (missing.length && !ONLY) {
|
||||
console.warn(`[publish-html] ⚠ ${missing.length} zone(s) have NO poster HTML: `
|
||||
+ `${missing.slice(0, 6).join(', ')}${missing.length > 6 ? '…' : ''}`);
|
||||
console.warn('[publish-html] their download will 404 until generate.mjs covers them.');
|
||||
}
|
||||
Loading…
Reference in a new issue