Commit graph

6 commits

Author SHA1 Message Date
doxa-maps-88655c70
467b7f01a2 feat(packet): all 11 locales — Hindi unblocked with a vendored Devanagari face
Some checks are pending
build-gate / build (push) Waiting to run
The packet now ships in every locale the app ships: en es fr pt de it ro ru ar
hi zh. Each carries all 21 translatable keys, the twelve prayer points, the
eleven rhythms, both attributions and the privacy note. point4_passage stays
omitted everywhere and falls back to the licensed English WEB text, for the
reason already recorded — translating scripture here would put unlicensed,
unverified Bible text into a printed prayer packet.

HINDI WAS BLOCKED ON A FONT, not on a translation. Devanagari renders as tofu
boxes in the packet's Georgia/Palatino/serif stack because this machine has no
Devanagari font at all. Noto Serif Devanagari (SIL OFL) is now vendored —
devanagari subset only, 124 KB, licence and provenance beside it, matching how
poppins/ ships latin and latin-ext separately.

It is base64-INLINED into the packet, not linked. A packet is a standalone file
meant to print offline — the Node generator writes HTML with no server, and the
photos are already inlined for exactly this reason — so a URL would break
precisely where the packet is most used. Cost: ~165 KB on a 3.5 MB packet, and
only for that language. One rule prepends the face to the stack rather than
editing 22 hardcoded Georgia declarations, and the browser falls back PER
CHARACTER, so Latin scripture references in a Hindi packet still set in Georgia.
mr and ne map to the same face; a missing font warns loudly rather than silently
producing rectangles.

Chinese needed no font work — CJK already shapes in the system stack — only the
translation.

★ TWO MEASUREMENTS LIED ABOUT SCRIPT COVERAGE, recorded so nobody automates this
cheaply. Comparing shaped width against a missing font reported ar and zh BROKEN
and hi FINE — exactly backwards, because Devanagari tofu boxes happen to match
real glyph advance. An ink-column-profile heuristic then scored CORRECTLY
RENDERED Devanagari the same as tofu (3 vs 2 distinct heights), because its
baseline bar gives a uniform profile. Both were abandoned: a detector that blocks
working packets is worse than none.

Verified: all 11 languages render 8 sheets -> 8 letter portrait pages, map
painted, zero overflow. The Hindi prayer-points page was READ AS A PICTURE —
conjuncts and matras shaped correctly, no boxes, Latin references still Georgia.
2026-09-24 04:03:57 +00:00
doxa-maps-88655c70
507cf68e12 feat(cards): horizontal prayer cards — the layout changes, not just the page
Some checks are pending
build-gate / build (push) Waiting to run
Driver asked for "horizontal cards" alongside the vertical ones, with the print
dialog's aspect ratio chosen at the print point.

★ LANDSCAPE IS NOT TRANSPOSED DIMENSIONS. The portrait card stacks the photo
ABOVE the text, occupying the top 46%; at 3.5 x 2.5in that same band is a strip
too short to carry a face AND leave room for the rows under it. The landscape
stocks put the photo BESIDE the text instead — full-bleed down the left 42%,
name over a scrim at its foot, data and QR in the right column. So `side` is a
LAYOUT switch the CSS reads, not a cosmetic flag.

It is driven by the stock rather than by comparing width to height: a square
stock would be ambiguous, and the composition is a deliberate property of the
format, not an inference from its numbers.

Two landscape stocks added and exposed in the poster-wall picker:
  pokerLandscape      3.5 x 2.5 in  ->  252 x 180 pt
  postcardLandscape   6 x 4 in      ->  432 x 288 pt

Verified at the rendered layer by the new check:print-geometry, which measures
the MediaBox rather than reading the CSS — and read as a picture, not only
asserted on: photo left, "Aheu" over the scrim, country/people/language/religion
in the right column, QR and "PRAY FOR THEM" at the foot, zero overflow.
2026-09-24 03:55:02 +00:00
doxa-maps-88655c70
dd14b5fe76 feat(packet): five more languages, and right-to-left support for Arabic
Some checks are pending
build-gate / build (push) Waiting to run
The packet now ships in 9 of the app's 11 locales: en es fr pt de it ro ru ar.
Each carries all 21 translatable keys — page furniture, the twelve prayer points
with sub-headings, the eleven rhythms, both Metacamp attributions, the privacy
note. point4_passage (Mark 5:1-20 in full) stays omitted in every non-en block
and falls back to the licensed English WEB text with its credit, for the reason
already recorded: translating scripture here would put unlicensed, unverified
Bible text into a printed prayer packet.

RTL IS NOW REAL, not assumed. The renderer emits dir on <html> from an RTL
locale set. Without it an Arabic packet renders readable glyphs LEFT-aligned —
which looks like a styling quirk rather than a bug, and no assertion noticed.

★ TWO BIDI FINDINGS, both from reading the render rather than the code:

1. Scripture references are LTR runs inside RTL paragraphs, and the bidi
   algorithm REORDERS a run that starts with a digit: "1 Peter 5:6-11" rendered
   as "Peter 5:6-11 1". Each reference is now dir="ltr" + unicode-bidi:isolate.
2. With isolation, the reference LIST reads right-to-left on an RTL page (first
   item rightmost). That is correct for an RTL reader, not a defect — recorded
   in _meta so nobody "fixes" it. English source order is unchanged, asserted.

★ HINDI IS DELIBERATELY NOT ADDED. Rendering each script at 48px and READING THE
PIXELS: Latin, Cyrillic, Arabic and CJK all shape correctly in the packet's
Georgia/serif stack; DEVANAGARI RENDERS AS TOFU BOXES, because this box has zero
Devanagari fonts (fc-list). A hi block would ship a page of empty rectangles. It
is blocked on vendoring a Devanagari font into public/assets/fonts, which is its
own piece of work.

⚠ THE MEASUREMENT THAT LIED. My first test compared rendered text advance against
a missing-font baseline and reported ar and zh BROKEN and hi FINE — the exact
opposite of the truth, because Devanagari tofu boxes happen to match real glyph
advance. One screenshot settled in a glance what two rounds of clever
arithmetic got backwards. Recorded in _meta.

Verified at the rendered layer in all nine languages: 8 sheets -> 8 letter
portrait pages, 95 people cards, 12 wheel segments, map painted, zero
overflowing sheets, 19 strings differing from English per locale. The Arabic
prayer-points page was read as a picture, not just asserted on.
2026-09-24 03:50:35 +00:00
doxa-maps-88655c70
d154a76f64 feat(posters): generate the poster in the browser when none is shipped
Some checks are pending
build-gate / build (push) Waiting to run
Driver: "it would be a big win to not have to have 80mb of images in a public
folder and let the posters get generated by the website."

Downloading a poster used to end at `poster not found (404)` whenever the
pre-rendered .webp was absent. It now falls back to rasterising the poster's own
HTML in the browser — fetch posters/<id>.html, inline its stylesheet into the
<svg>, draw to a canvas, hand back a blob. Same move the prayer packet already
makes: generate on click, ship nothing.

This unblocks dropping the 41 MB of committed .webp posters without degrading a
working feature, which is the part that had to come first. It also covers the
KEY CITY poster wall for free — both walls share this component.

⚠ AN SVG LOADED THROUGH <img> IS AN ISOLATED DOCUMENT: it cannot see the host
page's CSS, so every var() falls back to its initial value and `fill`'s initial
value is BLACK. The packet shipped exactly that bug once (660 of 661 map
elements black, no console error). The stylesheet is therefore inlined INTO the
svg, and the result is checked by measuring pixels — a blank render throws
rather than handing over a black rectangle. Verified by check:poster-raster:
laos rasterises to 3000x4000 with luminance 229 and 54 distinct colours; strip
the <style> block and it drops to 13 and 2 colours and the gate fires.

WHAT I MEASURED AND REJECTED ALONG THE WAY, so nobody repeats it: shipping the
poster HTML *instead of* the webp looked like a clean win on the first sample
(laos: 115 KB gzipped vs 213 KB webp) and does NOT generalise — poster HTML
scales with geometry complexity while webp is a fixed-resolution raster, so
asia-india-rajasthan is 414 KB gzipped vs 239 KB webp. Generalising from one
sample is how that idea nearly shipped.

Also measured: 351 of the 378 committed posters (35.4 MB) are city-* posters
belonging to the Key Cities feature, not to any of the 29 UPG-100 lists. They
are a live feature, not dead weight — which is why this lands as a fallback in
the shared component rather than a deletion.

encodeURIComponent, not btoa, for the data URL: the posters carry non-Latin-1
people-group and place names and btoa throws on those.
2026-09-24 03:05:17 +00:00
doxa-maps-88655c70
7bba1c0771 feat(upg-100): the prayer card deck prints as CARDS, not as paper
Some checks are pending
build-gate / build (push) Waiting to run
Driver: "the print dialog that opens up needs to show all the cards not in a
paper format but a CARD format, the aspect ratio that is used in the card
generator."

The deck set @page{size:letter portrait} and gave each card height:100%, so 95
cards printed as 95 mostly-empty 8.5x11 sheets — and with NO photo, because
image_url was never in the fetch. Measured: 95 of 95 groups DO have a portrait,
so the omission cost every card the one thing that makes it a prayer card.

Now the PAGE IS THE CARD. @page takes the stock's own inches, so one card is
exactly one page: 95 cards -> 95 pages at 180x252pt (2.5x3.5in). Print on
cardstock and cut, or send the PDF to a card printer. The stock is a user
choice, per the Driver's ask to "choose the layout of the print dialog to choose
the aspect ratio": poker 2.5x3.5in (default), postcard 4x6in, A6.

⚠ @page SIZE MUST BE EXPLICIT INCHES. Chromium SILENTLY falls back to letter for
a named size it does not know: `size: A6 portrait` yields 612x792pt with no
error of any kind, while `size: 2.5in 3.5in` yields exactly 180x252pt. The gate
therefore asserts the MEASURED MediaBox — a check that grepped the stylesheet
would pass a deck that prints as paper.

Card content follows doxa-life/promotional-material-generation, the Python data
pipeline feeding the InDesign template: per language it emits Description,
Religion, Area, Country, a QR to that group's page, and the URL. Ported to
JavaScript here so the deck generates in the map app on click. Description and
Area are NOT available from this API — measured 0 of 2107 records carry either —
so the card is built from what genuinely exists: portrait, name, country,
population, language, religion. An empty block reserved for data that never
arrives is worse than a card designed around the data there is.

QR is hand-rolled (library/packet/qr.js, ~200 lines): byte mode, level M,
smallest fitting version, mask 0. A symbol is pure computation, so it costs one
small file instead of a package and runs identically under Node and in a tab —
the standing constraint is "without adding a lot of dependencies on the
internet". A QR that does not scan looks exactly like one that does, so it is
verified by DECODING: an independent reverse-path decoder re-derives the data
placement, unmasks, de-interleaves and parses the header. 5/5 round-trip,
including version selection (v2 for a short URL, v3 for real ones).

I18N: the QR encodes the LOCALISED profile URL, so a Spanish card scans through
to the Spanish page — verified, pray.doxa.life/es/aheu. Strings fall back per
key; checked with a partial Spanish block ("Ora por ellos" rendering while
Language/Religion stayed English).

★ ONE BUG THE NUMERIC GATE PASSED, worth recording. The first deck rendered
"LAO", "thm", "EAN" — raw codes on a card a person is meant to pray from —
because normalizeRecords() flattens each { value, label } dict to its VALUE and
parks the label under <field>_label. A gate counting "4 rows rendered" passed
it: the rows were there, just unreadable. The gate now asserts the values are
HUMAN-READABLE, not merely present.

Verified by check:card-deck across all three stocks: 95 cards / 95 pages, 0
broken portraits, 95 QR codes with real symbols, 0 raw codes, 0 overflow.
2026-09-24 02:56:20 +00:00
doxa-maps-88655c70
301cc38fba feat(upg-100): print a prayer packet from the map legend
Some checks are pending
build-gate / build (push) Waiting to run
Driver, as a first-time user: "I select one and it gives me print options at
the top right of the legend... that's the only way that map would be usable."

The affordance was already built and switched OFF. The shared legend emits
'export' with 'poster' | 'packet' for the selected node, the profile wired
@export, and SHOW_EXPORT_BUTTON was held false with the note "until the export
packet is finished: the button currently produces a broken packet". That
precondition is now met, so the switch is on.

ONE RENDERER, TWO CALLERS. The pages move to library/packet/packet-render.js,
imported by BOTH the build-time generator and the browser export, so the printed
packet and the generated one cannot drift. The card asked for exactly this:
"reuse the existing print actions rather than growing a fourth implementation."
The extraction is behaviour-preserving — the generated packet is byte-identical
to before apart from leading whitespace (the code is now inside a factory), and
check:packet-render still passes 8/8.

★ THE PACKET IS GENERATED ON CLICK, NEVER SHIPPED. One packet is ~3.5 MB (photos
dominate) and 28 zones would add ~100 MB to a repo that ALREADY had a publish
blocked at a 74 MB asset tree. Generating client-side costs zero repository
bytes; the photos come from URLs the records already carry. This is the Driver's
instruction: "instead of fetching the poster from the public folder I want the
client side code to generate the poster real time when you click on the button
so it does not take a lot of mb storage." Bundle cost: +21 KB (494 -> 516 KB),
which includes all 12 prayer points and Mark 5:1-20 in full.

Printing goes through the SHARED openPrintFrame — a hidden same-page iframe, not
window.open(). A popup is blockable and drops the reader out of the map.

Two traps the browser path had to handle, both already known to this codebase:
  - pg_slugs BYPASSES the shared selector evaluator (it intentionally throws on
    that type, and 12 of the 27 lists use it). Same special-case the map and the
    poster wall apply, so all three agree about who is in a list.
  - evaluate() returns a Set, not an array.

VERIFIED IN A BROWSER, not by reading source (new gate check:packet-browser):
live records -> shared renderer -> page.pdf(), giving 8 sheets -> 8 pages at
612x792pt, 95 people cards, 12 wheel segments, Mark 5 in full, 2 metacamp.org
attributions, 0 page errors. I18n verified with a partial Spanish block: the
translated headings rendered in Spanish while Mark 5 and the attributions fell
back to English, per key, never blank.

Card BUILD-UPG100-MAP-LEGEND-LEADS-WITH-THE-UPG-100-LIST part (d). Parts (a) and
(b) shipped earlier. Part (c) — removing the People Group tab and retabbing the
legend — stays OPEN: that card is explicitly driver_gated on "what is the
legend's tree once Region/Zone/People Group are removed", and the current tabs
are wired to real hierarchy data.
2026-09-24 02:43:49 +00:00