Commit graph

229 commits

Author SHA1 Message Date
doxa-maps-88655c70
a856a5e738 Regions: restore their singular "Country" header, found by comparing words
Applying last tick's lesson — a channel captured and never asserted —
to the vs-live diffs themselves. All four compared getComputedStyle and
NOTHING ELSE, so an element could render the wrong words with
pixel-perfect styling and still report 79/79. Two of them even captured
the text and never compared it.

region-vs-live now compares the STRUCTURAL text as well: stat labels,
goal-card headings, table headers and the back-link — the chrome whose
words are fixed. Values are deliberately excluded, since figures are
already compared against live by _probe-oceania-and-list.mjs and
duplicating that would break the moment the API's numbers move. The
lists are compared whole, so a missing or reordered label is caught too,
not just the first one.

It immediately found a defect I introduced last tick. Their live
/regions/africa table header reads "Country", singular. I had changed
ours to t('Countries') for plural symmetry with the chooser's dropdown,
having checked that the key was translated but NOT that the word matched
their page. Restored to t('Country'), which is in their catalogue and
translated in every locale — so parity and localisation both hold, and
ar still reads "الدولة". The chooser's own column keeps the plural,
since that column exists only in our addition and t('Region') has no key
in their catalogue.

list-vs-live and country-head-vs-live now assert their fixed-word text
too — the h1, the strapline and the back-link. Negative-tested by
perturbing the expected string: exit 1.
2026-09-24 10:29:07 +00:00
doxa-maps-88655c70
ebac982fcb Merge main into bundler-1/work (regenerate the tree, re-hash)
Some checks are pending
build-gate / build (push) Waiting to run
2026-09-24 10:13:28 +00:00
doxa-maps-88655c70
cf689adeeb Regions: stop overriding their localised labels with hardcoded English
I have reported "the SELECT VIEW strip's three strings need
translating" for several ticks without ever quantifying it. Measured at
ar and zh by classifying every visible string by script: TWENTY were
still Latin, not three. Most were not awaiting a decision at all — they
were bugs, and the largest was my own code discarding their data.

- The chooser's region legend read WAGF_REGIONS[value] || r.label, so
  the hardcoded English display names in library/colors/wagf-region.js
  always WON over r.label — the localised name the API returns for
  ?lang=, and the very name the region CARDS render. At ar the cards
  read "آسيا" while the legend beside them read "Asia". Inverted: the
  API's label now wins and WAGF_REGIONS is the fallback for a region
  returned with no label.
- The no-selection title was the literal 'All Regions' while this same
  file already calls t('All Regions') for the back-link — one string,
  translated in one place and hardcoded in another.
- syncTitle compared the subject title against the English literal, so
  once the title was localised the comparison could never match; both
  halves now go through t(), whose keys are theirs.
- The chooser's two dropdowns carried static label and placeholder
  attributes. That is the element's only keyboard and screen-reader
  path — pin clicking is mouse-only by construction — and it was
  labelled in English inside an otherwise localised page. Now
  t('Regions'), t('Countries') and t('Type to search...'), all their
  keys, all translated in every locale we ship. The table's first
  column too: t('Region') has no key in their catalogue, since they
  never needed an all-regions state.

Twenty Latin strings down to fourteen, of which nine are our own
additions, four are Mapbox's chrome and one is an API code. 93.9%
localised at both ar and zh.

Two lessons in the measuring itself. My scan read only TEXT NODES, so
the two hardcoded placeholders were invisible to a check that had just
reported 95% localised — it now reads placeholder, aria-label, title
and alt as well, which immediately surfaced four more English
aria-labels that exist purely for assistive tech. And the remainder is
now classified in the probe rather than left to fail forever: a
permanently red check only teaches you to ignore it, so it passes on
the known set and fails on anything new.
2026-09-24 10:12:16 +00:00
doxa-maps-88655c70
87bd98d610 Restore the clickable poster preview — the last capability the migration dropped
Some checks are pending
build-gate / build (push) Waiting to run
Migrating my-upg-100-list onto the shared wall left one behind, and it was not
decoration: clicking the poster preview opened it for printing, a Driver-named
behaviour ("show the poster in the browser to help print it"). I removed it, so
this is a regression I introduced and am closing.

previewAction: 'none' | 'print'. Default 'none' keeps the image inert, which is
exactly what country-posters and key-cities render today.

★ IT IS A KEYBOARD CONTROL, NOT JUST A CLICK TARGET. The original carried
role="button", tabindex="0" and Enter/Space handlers alongside @click. Re-adding
the click alone would have been a half-restoration that LOOKS complete — an <img>
with only @click is unreachable without a mouse, and the focus-visible outline is
how a keyboard user sees where they are on a control that is otherwise a picture.
The alt and title change with the mode too: "prayer poster" and "click to preview
and print" are different promises to a screen reader.

buttons.previewPrintPoster added to all eleven locales in the same change;
check:i18n-parity still reports 0 keys of debt.

VERIFIED AT THE RENDERED LAYER, and exercised BY KEYBOARD rather than by mouse:
  100-list (preview-action="print") — 27 previews, is-clickable, role=button,
  tabindex=0, cursor zoom-in, translated title, descriptive alt. Focused the image
  and dispatched ENTER: 1 print frame opened, 0 downloads, 0 errors, with every
  .webp ABORTED so it ran through the rasterise fallback.
  country-posters (default) — 174 previews, is-clickable false, role null,
  tabindex null, original alt. Inert exactly as before.

That completes the consolidation: my-upg-100-list 890 -> 179 lines, and all four
walls now render from one 633-line component with nothing left unported.
2026-09-24 10:02:22 +00:00
doxa-maps-88655c70
649b0872eb my-upg-100-list renders from the shared PosterWall — 890 lines to 174
Some checks are pending
build-gate / build (push) Waiting to run
The original wall is gone: 716 lines of duplicated markup, CSS, export modal and
print wiring deleted. This profile now supplies only what cannot be shared —
REGION_OF/regionFor (its taxonomy, keyed by list id because a selector can be a
wagf_block, a country_codes array or pg_slugs and none of those implies a
continent) and fetchListRecords (the people-groups API, the selector evaluator and
the pg_slugs special case). It joins country-posters (94 lines) and key-cities (75)
on one component, which is what the Driver asked for: "don't recreate the wheel but
imitate upg 100 list".

It took three capability additions across three ticks, because migrating without
them would have REGRESSED this wall rather than consolidated it: cardStocks, then
posterAction="print" and hidePrayerListWhenSoon — the last two a Driver ruling, not
a preference.

★ TWO REAL BUGS, BOTH INVISIBLE AT THE SOURCE LAYER, BOTH BUILDING CLEANLY.

1. groupBy SIGNATURE MISMATCH. regionFor takes an ID; the shared wall's groupBy
takes the whole ITEM. `:group-by="regionFor"` compiled, the page loaded, and then
threw `c.startsWith is not a function` at first render — the whole wall blank, zero
cards, nothing wrong in the source. Adapted in the template rather than changing
regionFor, since the id is what the taxonomy is keyed by.

2. A THROWN FETCH IS A MISS, NOT AN ERROR — and this one is a robustness gap in the
SHARED component, affecting every consumer. fetch REJECTS rather than returning
!ok when a request is blocked or offline, and exportPoster let that reject escape to
the outer catch, skipping the rasterise fallback and showing "Failed to fetch" on a
poster the browser could have generated from posters/<id>.html. The wall this
component was EXTRACTED FROM already guarded it; the extraction did not carry the
guard across. check:poster-print-fallback went PASS -> FAIL with 0 .html requests,
and is green again.

VERIFIED AT THE RENDERED LAYER, all three print actions on the migrated wall:
27 cards, 27 stock pickers, 4 buttons per card — the original's three prints plus
show-on-map. CARD DECK 116 cards / 116 QR codes / @page 2.5in x 3.5in / 0 errors,
identical to pre-migration. POSTER PRINT with every .webp aborted: 1 .html fetched,
"generating the poster", clean finish. PRAYER LIST: 1 people-groups fetch, 0 errors
— positive evidence the provider ran, since that print cannot work without it.
REGRESSION CHECK: country-posters still downloads (0 print frames, 1 download), so
the default is unchanged for the walls already on this component.
2026-09-24 09:54:40 +00:00
doxa-maps-88655c70
f824329466 One worldmap.svg, at the path that carries its provenance
Some checks are pending
build-gate / build (push) Waiting to run
c43aa39 landed the same 311KB asset at public/assets/images/worldmap.svg --
DX-life's own path -- together with a PROVENANCE record: a full-file scan
for copyright, licence, attribution, author and the usual stock-library
names returns nothing, the ids are design-tool export names, and DX-life
keeps no licence file beside that folder either, so this is internal reuse
of first-party artwork rather than redistribution of a third-party asset.
The contrast it draws is the right one: the vendored typefaces ARE
third-party and do carry their OFL licence in the folder that serves them.

I had ported the same file to assets/brand/ earlier in this flight. MEASURED
identical -- md5 b7af01b31096c265e221a1d4e3f88c8d on both -- so the tree was
shipping 311KB twice, and, worse, keeping the provenance record in one
folder and a copy of the asset in another, where the two can drift apart
without anything noticing.

So the grid now points at the documented copy and the brand/ one is deleted.
Nothing about the rendering changes; only which of two identical files it
asks for.

VERIFIED on the built tree: /assets/brand/worldmap.svg now 404,
/assets/images/worldmap.svg 200, the #filters backdrop still computes
background-size 60% and position 50% 0% with 0 failed requests, check:sri 0,
gates:pre 0, gates:post 0.
2026-09-24 09:37:45 +00:00
doxa-maps-88655c70
20c1c0d325 Delete the 100-list wall's dead hidden-map export, and stop it lying to a gate
Some checks are pending
build-gate / build (push) Waiting to run
renderFor() had ZERO call sites and was the only caller of makeHiddenMap() and
loadPoster(); EXPORT_SPEC was never read. The whole chain dated from when this wall
rendered a poster by spinning up an off-screen map and compositing tiles. Export and
print now take the shipped .webp, or rasterise posters/<id>.html in the browser.
52 lines out, 17 of explanation in: 925 -> 888.

★ IT WAS NOT MERELY UNUSED — IT WAS CORRUPTING A GATE. isMapLeaf() in
generate-tree.mjs is a REGEX over FILE TEXT, so the engine handle inside the
unreachable function flagged this profile isMap=true. That flag made
verify-map-paint.mjs EXCLUDE the profile, with a written justification —
"constructs a real gl.Map ON DEMAND during poster export, off-screen at
left:-9999px" — where every clause was true of the SOURCE and false of the RUNNING
PRODUCT. A profile was excluded from a paint gate on the strength of code that never
ran, and the exclusion read as a measured product fact.

Deleting the chain flips the manifest to isMap=false, which is correct: the poster
wall is a wall of cards, not a map. The gate's comment now says it is absent because
it IS NOT A MAP, rather than because it is a map the gate cannot see.

⚠ The explanation had to be written carefully. Naming the engine handle whole in a
comment RE-TRIPS the same regex and reinstates the bug, so it is spelled in two
pieces on purpose with a note saying why — the same class as asserting Mark 5:1-20
via the English word "Gadarenes": describing a thing in the terms the detector scans
for makes the description itself the defect.

ALSO FIXED, found while verifying: check:poster-print-fallback probed
'../assets/posters/<id>.html', one level up. A leaf lives at
<tree>/<bundle>/<profile>/index.html and resolves assetsBase to '../../assets', two
levels up — so on a tree that DID carry the sources the probe 404'd and the gate
SKIPped with "posterSources:true was not set" when it had been. A precondition check
that is wrong about the precondition silently turns the whole gate off.

VERIFIED AT THE RENDERED LAYER after the deletion, not before: the print path still
rasterises with every .webp aborted ("generating the poster", clean terminal state),
and the card deck still prints 116 cards at 2.5in x 3.5in with 116 QR codes and 0
page errors.
2026-09-24 09:12:17 +00:00
doxa-maps-88655c70
32936d731b Regions: delete four dead imports, after checking the slug port is consumed
Continuing last tick's question — is the data I ported actually used? —
applied to the slug half of countries-meta. It is: both tabs resolve
selections through r.slug and c.slug, which summarizeCountries computes
with countrySlug, so the canonical-English rule really reaches the URL.
Verified against the values the live site adjudicated: "United States"
from the API still yields the-united-states, and "DR Congo" yields
the-democratic-republic-of-the-congo.

What the check did turn up is dead weight. slugify was imported by both
tabs and called by neither, and regions-list also carried emptyStats
and toNumber — leftovers from the rewire, since that tab has no chooser
and never needed the zero accumulator or the coordinate parser. All
four removed; a re-audit of every import in all five components now
reports zero dead names. The two exports beyond their surface stay
justified: emptyStats has five uses in region-dashboards and toNumber
two, both in the chooser that needs them.

Also verified rather than assumed: the region legend really does render
in REGION_ORDER. That matters because validate_palette measured
ADJACENT PAIRS for colour-vision separation, so the 8.6 deutan and 30.6
normal-vision figures only hold in that order — sort the legend by
count and pairs the validator never checked become neighbours. Nothing
asserted it, so the chooser suite now pins the dot colours to the
palette's own key order, with the expected sequence read from
library/colors/wagf-region.js rather than retyped. Negative-tested by
reversing the sequence: exit 1.
2026-09-24 09:08:57 +00:00
doxa-maps-88655c70
820a9015ca Merge main into bundler-1/work (regenerate the tree, re-hash)
Some checks are pending
build-gate / build (push) Waiting to run
2026-09-24 08:51:21 +00:00
doxa-maps-88655c70
bfe659be3e Regions: the map was pointed at the wrong part of the planet
A half-finished port of my own, found by asking whether the data I had
ported was actually consumed. countries-meta computes centre, zoom and
bounds per country and per region via viewFor/fitView/lngExtent, and I
reported that port as closing the "maps derive their own frame" item.
It did not: this component still computed its own mapBounds from a
naive min/max over the focus pins, its own mapCenter from their
arithmetic mean, and a hardcoded zoom of 4 or 2 — discarding the ported
values entirely.

The consequence was severe and visible on Oceania, whose pins straddle
the antimeridian:

  ours    [[-175.2,-21.3],[178,-6.3]] — 353.2 degrees, the long way
          round the globe — centre [28.02,-16.06], which is in AFRICA,
          zoom 2 hardcoded
  theirs  [[143.9,-21.3],[187.9,-6.3]] — 44.0 degrees along the short
          Pacific arc — centre [165.9,-13.8], zoom 3.07

A naive min/max has no antimeridian handling. lngExtent exists for
exactly this: it walks the sorted longitudes for the largest gap and
spans the rest, letting east exceed 180 so the range stays continuous.

Why nothing caught it: region-vs-live diffs computed CSS properties and
reports 79/79. A map pointed at the wrong hemisphere is not a CSS
property. The chooser probe covers the no-selection world view, and
nothing had ever read the camera on a selected region page.

The frame now comes from the subject's ported values, with the
chooser's world view preserved as the documented exception — null
bounds, [10,28], zoom 1.25 — because fitBounds across every country
pulls the globe into a crescent, which was itself a measured decision.
Fallbacks use countries-meta's own WORLD_VIEW centre rather than an
invented one.

Proven on the live element: centre longitude 153.6 (Pacific, was 28.0),
span 145.7 degrees (short arc, was 353.2), zoom 2.56 (a real fit), with
west 80.8 and east 226.5 — the east-beyond-180 continuity the port
makes possible. The screenshot shows in-region pins framing the Pacific
with out-of-region pins correctly greyed at the edge.

Also checked and not a defect: the region page renders on the globe
projection at this zoom, which matches theirs — their CountryMap sets
no projection either, and this component already documents the cloned
pages as deliberately left alone, forcing mercator only for the chooser.
2026-09-24 08:47:16 +00:00
doxa-maps-88655c70
e5fe33b230 prayer-walk-planner can be shared — the last published map with no sharing path
check:share-policy's known-debt line is now GONE: it named two published maps, then
one, now zero. Every published map has exactly one decided sharing path.

★ THE OPEN QUESTION WAS ANSWERED BY THE CODE, NOT BY A RULING. This was held back
asking "is it MEANT to be shareable? a link carrying no route arrives as an empty
planner, and a button that reliably produces a blank map is worse than none". That
framing assumed a design choice existed. It did not: the profile keeps polygons in
localStorage and has NO BACKEND — its own header says so — so a URL cannot carry a
drawn route under any design. The shareable artefact is the PLANNER, exactly as
every other map shares its own page. The failure only exists if the link CLAIMS to
carry the route, so the status strip says once, on click: "This link shares the
planner. Your drawn zones stay on this device." — using the status mechanism the
profile already had rather than adding a surface.

★ ADDING <ShareButton> WAS NOT ENOUGH, AND check:share-policy COULD NOT SEE IT.
The button rendered NOWHERE. This bundle mounts its SFC directly as the custom
element — deliberate, it is what surfaces the defineExpose()d client-side API on the
DOM element ("the only API we can build has to be CLIENT SIDE") — and so installed
no i18n. ShareButton calls useI18n(), which throws in setup(), and index.js's own
header records the cost: "a thrown setup() removes the whole shadow-DOM subtree
silently." Measured: a runtime SyntaxError, five toolbar buttons rendered, the sixth
absent — while the policy gate, which counts <ShareButton> in SOURCE, said 1 button
and PASSED. That blind spot is now written into the gate, pointing at the smoke that
does catch it.

Fixed WITHOUT changing the mounting shape: defineCustomElement takes configureApp
whichever component is the root, so i18n installs and the API stays on the element.
The direct-mount decision and i18n were orthogonal; I had assumed they were coupled.

VERIFIED AT THE RENDERED LAYER: share-roundtrip-smoke resolves the url 200 text/html
and REOPENS it as <prayer-walk-planner> with a rendered canvas. The client-side API
was re-verified after the change — all 7 exposed methods present, addPolygon ->
listPolygons round-trips, 0 page errors. i18n verified in 6 locales: Share /
Compartir / مشاركة / साझा करें / 分享 / Partager. Added to smoke:share-all, now
12/12 pass 0 fail.
2026-09-24 08:23:30 +00:00
doxa-maps-88655c70
3a468c2bc3 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
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.
2026-09-24 08:07:22 +00:00
doxa-maps-88655c70
2392fe0319 Merge main into bundler-1/work (regenerate the tree, re-hash)
Some checks are pending
build-gate / build (push) Waiting to run
2026-09-24 07:30:43 +00:00
doxa-maps-88655c70
e5c2c77561 all-people-groups can be shared: its own button, verified by a round-trip
Some checks are pending
build-gate / build (push) Waiting to run
It was published, embeddable and reachable with NO sharing path by any mechanism —
no ShareButton, no shareUrl config, no composite parent overriding one on its
behalf — while every other published map had exactly one. A reader who landed on
it could not send it to anyone, which is the point of an embeddable jamstack map.
check:share-policy's known debt falls from 2 published maps to 1.

The button is in a MapToolbar (pure layout shell: position, z-index, mobile
offsets) rather than hand-positioned, so it sits where every other map's controls
sit. has-geocoder=false because this profile mounts no search pill. Only the share
control is added — zoom/location/fullscreen each need a live mapRef this bundle
does not expose, and the card asks for a sharing path, not a toolbar.

NOT gated on appReady, unlike this bundle's legend. Sharing a map does not require
its data to have loaded, and gating it would remove the affordance exactly when a
slow or failed load makes a reader most likely to want the link.

★ WHY A RENDERED CHECK WAS NEEDED. check:share-policy counts <ShareButton> in
SOURCE. Mutation-tested by gating the toolbar off: the policy gate still read
"1 button / own-button" and stayed GREEN while the button was absent from the DOM.
A source count cannot tell a rendered button from an unrendered one.

So this is verified by the EXISTING share-roundtrip-smoke, not a new gate. I wrote
one, then deleted it on finding that smoke does strictly more — it fetches the URL
and REOPENS it, asserting the same custom element renders, which catches SPA
fallbacks and hash-stripping that a link-text check cannot. A second share checker
would be the fourth-print-path mistake this repo has already paid for once.
Result: share URL resolves 200 text/html and reopens as <all-people-groups> with a
rendered canvas.

i18n verified at the rendered layer: under ?lang= the button title reads Share /
Compartir / مشاركة / साझा करें / 分享, so the new affordance is translated in every
locale, RTL included. buttons.share is one of the keys restored to parity earlier.

ALSO FIXED, found by running the suite: smoke:share-all was testing the LEAF urls
of the two 'header'-policy bundles, whose button lives in the MFE shell by design,
so it reported 10/12 with two failures that were the policy working as intended.
Swapped country-posters/countries-map for country-posters-mfe and dropped the stale
my-upg-100-list leaf (upg-100-mfe was already listed). Now 11/11 pass, 0 fail.
check-share-policy records the coupling so the list cannot drift from the policy
again.

prayer-walk-planner stays debt-no-share, deliberately. It builds a personal walking
route from polygons the reader draws; a link carrying none of that arrives as an
empty planner, and a button that reliably produces a blank map is worse than none
because it looks like sharing works. That is a semantics decision, not an
implementation one.
2026-09-24 07:25:19 +00:00
doxa-maps-88655c70
ca13f04bd2 Regions: delete a dead prop, after auditing what the probes can actually prove
The source change is small: countryName was declared on UupgsList and
passed by region-dashboards but never read — residue of the invented
count line removed earlier. Rule 3 prefers deleting a workaround to
keeping one, so both the prop and its call site are gone. Audits for
the two shapes that revealed real gaps earlier — a prop declared and
never used, and state read but never written — now come back clean
across all four components, and every reachable prop their CountryDetail
passes is implemented.

The substantial finding is about the probes themselves, and it lands in
_qa-cycle rather than here. Scanning all 42 for "no ok() calls AND
never exits non-zero" returned 23, including both numbers reported every
tick: region-vs-live's 79/79 and country-vs-live's 32/32 printed
"N IDENTICAL · M differ" and returned success either way, so the
headline parity was read from text rather than enforced.

_probe-standalone-app.mjs had already drifted for exactly that reason:
it printed its table, counted gaps, and exited 0 regardless. Read
properly it was 9 of 10, not the 10/10 reported for several ticks. The
gap was "one h1 on the page: h1 count 2", and measuring resolved it
rather than silencing it — the composite is a scroll column, both panes
are genuinely on the page (both h1s compute hiddenBy null at 1200x75),
so they are two cloned pages in one document. "One h1 per document"
collides with clone parity, since region-vs-live and list-vs-live both
diff an h1 selector; the clone requirement wins and the property now
states "one h1 per pane (scroll column)" with that measurement
attached.

All three now exit non-zero on a real failure, each proven by a
negative test. Two of those tests were wrong first: one swapped a
selector on BOTH sides so they stayed identical, and one read $? after
a pipe, which reports tail's exit rather than node's.

Also made the chooser's pin assertion wait for the condition instead of
a stopwatch. It had read queryRenderedFeatures at a fixed 4200ms and
measured 0 once while consecutive runs gave 1991 and 2107 — a flaky
check, which is worse than a missing one because it teaches you to
ignore a red. Three runs now return 2107 every time.

Regression: 18 suites green, 9 gates green.
2026-09-24 07:22:38 +00:00
doxa-maps-88655c70
a32097751b Regions: their second filters panel was missing, and it carries two of their bugs
The expanded filters panel was the largest interactive surface never
compared to theirs, and comparing it found a whole panel absent.
Measured on their live country page: TWO .filters__panel blocks and
THREE .filter-toggle buttons — Adopted, Engaged, Exact Search — against
our one panel and zero toggles. The dropdowns already matched exactly,
four of them, with their country dropdown correctly hidden by
lockCountryCode.

A tell I had missed: the search already read active.exact for the Fuse
threshold, so Exact Search's BEHAVIOUR was ported while the control
that sets it never was.

Ported their panel, their three toggles, their toggle semantics (a
second click removes the filter), their activeFilters shape so each
toggle also renders as a chip, and .filter-toggle styled from the
MEASURED computed values of their live button rather than by chasing
tokens. The labels needed no new keys — Adopted and Engaged were
already translated in all eight locales.

Two of their bugs are cloned deliberately, both measured on their live
/regions/india at 855 rows:

- ENGAGED RETURNS ZERO. Their branch filters on u.people_praying, and
  people_praying is not in the field list they request — they fetch
  people_committed. The field is always undefined, so the filter
  matches nothing: their page goes 855 to 0. Reading people_committed
  instead would make us differ, and the ruling is that what exists on
  DX-life must match. The upstream fix is one word in their file.
- THE ACTIVE STATE IS WHITE ON WHITE. Their rule sets background-color
  var(--color-surface-brand-lighter, rgba(0,80,160,0.05)) with color
  var(--color-white), and --color-surface-brand-lighter is not defined
  in their token files. Their active button computes background
  rgb(255,255,255) and color rgb(255,255,255) — an invisible label,
  now visible in our screenshot too. Defining that one token upstream
  fixes both sides.

Both are flagged for the Driver rather than quietly corrected: a bar
chart was already removed under this ruling for the opposite reason,
being in ours and not theirs.

Proof: _probe-filter-toggles.mjs, all assertions pass — two panels,
three toggles, nine measured style properties, Adopted narrowing 24 to
2 and appearing as a chip, a second click restoring, Engaged emptying
to 0 exactly as theirs does, Exact Search changing nothing alone.

Regression: 22 suites green.
2026-09-24 06:53:22 +00:00
doxa-maps-88655c70
a5628171ea Merge main into bundler-1/work (regenerate the tree, re-hash) 2026-09-24 06:30:29 +00:00
doxa-maps-88655c70
e8e2be1498 Merge main into bundler-2/work
Some checks are pending
build-gate / build (push) Waiting to run
# Conflicts:
#	package.json
#	public/doxa-maps/all-people-groups/all-people-groups.js
#	public/doxa-maps/all-people-groups/all-people-groups/index.html
#	public/doxa-maps/index.html
#	public/doxa-maps/manifest.json
2026-09-24 06:25:29 +00:00
doxa-maps-88655c70
2ddb09b42c Regions: RTL kept three rules on the wrong side, and the tab strip never localised
Every vs-live diff so far ran at LTR, so RTL was the next coverage gap —
and it is where layout actually breaks, because a physical `left`
silently survives a direction flip while a logical
`inset-inline-start` does not. Their CSS uses the logical form; ours had
physical `left` in three direction-sensitive places.

The new probe compares each element's offset from the INLINE-START edge
— the left edge in LTR, the right edge in RTL — which is the one number
that goes wrong when a physical property survives. Measured against
their live /ar/regions/cameroon:

- .search-icon sat 566px from the inline-start edge against their
  19.5px: it had stayed on the physical left, which is the inline END in
  Arabic. Replaced with their rule verbatim (blocks/_form.scss:119 —
  inset-inline-start:1em, top:0/bottom:0, margin:auto 0, 1em square),
  which also fixed a second difference, since our fixed 18px square
  could not track the input's type where theirs is 1em. Ours now
  measures their exact 19.5px.
- The country bullet kept left:-16px, so in RTL it detached on the
  wrong side — computed right 256.938px instead of a -16px overhang.
- The chooser's region legend was pinned to the physical left. That one
  is a sanctioned addition rather than a clone, but a legend stuck on
  the physical left in an RTL layout reads as broken whoever designed
  it.

Reading the Arabic screenshot then showed every heading, strapline and
map note in Arabic and RTL while the SELECT VIEW strip still read
"Regions List" — the one piece of chrome that never localised, because
regions-mfe hardcoded its labels and imported no i18n at all. Now
routed through t(), with the legend going through ViewSelect's existing
prop so the shared component and its three consumers stay untouched.

No catalogue keys were added, deliberately. Their common.json has
"Regions" and "View" but none of the composites, so there is nothing to
copy; translating our own additions is not this agent's call; and
check:i18n-parity tracks a per-locale debt ceiling — it/ro/zh sit at
exactly 94 missing because DX-life translates 8 of our 11 locales — so
English-only keys would push them to 97 and fail it. The shim falls
back to the key, so today's rendering is byte-identical. Proven rather
than assumed: a sentinel injected into ar/regions.json rendered on tab
1 at locale=ar while tab 2 stayed English, then was reverted.

Two of my own assertions were wrong first. The probe set
profile-config.locale on every custom element, which on the composite
means three regions-dashboard elements — host and both panes — and
writing a fresh {locale:'ar'} onto a pane that had inherited its config
by provide() clobbered it, leaving every root LTR; I nearly "fixed" RTL,
which works on all three leaves. And checking inset-inline-start in an
LTR context is meaningless, since it merely mirrors left.

Worth flagging: check:i18n-parity's DOXA_REPO defaults to wt-packet,
another worktree, so it must be pointed at this repo to mean anything.

Regression: 22 suites green.
2026-09-24 06:24:45 +00:00
doxa-maps-88655c70
9056b9329c All 12,379 pins were missing on MapLibre, and now a gate says so
Having found that MapLibre drops a whole layer for one unrecognised paint
property, I swept all 17 isMap leaves under ?provider=maplibre listening to
the map's own error event -- the only place this class surfaces. One more
real instance:

    layers.apg-pins.paint.circle-sort-key: unknown property

circle-sort-key is a LAYOUT property, written into paint. Mapbox tolerated
the misplacement; MapLibre rejected the layer, so every pin on
all-people-groups was absent under the keyless engine while the page looked
perfectly healthy. Moved to `layout`; the sort order itself is unchanged.
Screenshots read on both engines: 12,379 pins, identical.

NEW GATE check:maplibre-drops loads every isMap leaf under maplibre and
fails on the layer/source-dropping error family. It skips cleanly when the
server is unreachable, because an UNMEASURED check is not a FAILED one --
the flaw already filed against check:filter-width.

★ THE NEGATIVE CONTROL WAS VACUOUS TWICE BEFORE IT WAS REAL, and that is
worth more than the fix. The first poison renamed the layout block, which
does not reproduce the bug -- the property simply vanishes, no error. The
second poison was faithful, circle-sort-key back in paint, and the gate
STILL printed PASS. Cause: I rebuilt with `BUNDLE=... vite build` and did
not run `bun run tree`, so the leaf carried stale SRI, the browser REFUSED
the script, no map mounted, and the gate saw zero errors. A check that ran
correctly and measured nothing, reading as green -- the first entry in
AGENT-BRIEF's troubleshooting table, walked into anyway.

After `bun run tree` (check:sri 0) the gate FAILED on the real bug, naming
the exact property, and PASSED again once restored. Both directions seen on
this codebase, which is the only thing that makes its green mean anything.

gates:pre 0, gates:post 17/18 (pre-existing verify/tables-complete),
check:doc-scripts 0, check:no-orphan-gates 0.
2026-09-24 06:24:40 +00:00
doxa-maps-88655c70
10f83a943b Regions: links we render as buttons had lost their anchor styling
Found by a COVERAGE AUDIT, not a screenshot. region-vs-live diffs
/regions/africa and country-vs-live diffs only UupgsList's nine
elements, so the country page's head and the region card had no
property diff at all. Adding both produced four defects sharing one
root cause: DX-life renders these as <a>, we render them as <button>
because this element has no router, and a button inherits neither the
UA underline nor their `a` rule (_globals.scss:132 — color
var(--color-brand-light), font-weight medium,
text-decoration-thickness 2px, text-underline-offset 4px).

- The dashboard back-link was #4e594f with no underline against their
  #cbc5b9 underlined. Their .light-link recolours to
  --color-brand-secondary = $color-beige-300 = #cbc5b9, exactly the
  measured rgb(203, 197, 185). Wrong on BOTH our region and country
  pages.
- The region-card link was font-weight 400 against their 500.
- Its icon spacing came from gap:10px where theirs comes from
  .with-icon .icon.right { margin-inline-start: 1rem } — the same 10px
  on screen, but their rule would never have applied to our markup, so
  the mechanism is now theirs.
- The country entries were rgb(42, 51, 42), our body ink, against
  their rgb(78, 89, 79) = $color-green-400 = --color-brand-light.
  Their country links carry no class at all — they simply take the `a`
  rule, which is why '.country-link' matched nothing on their side and
  the element read as unmeasured until the selector became
  '.countries-grid a'.

Coverage added so none of this can come back: a new
_probe-country-head-vs-live.mjs at 29/29 (back-link, h1, strapline, map
note, search-box container); the back-link added to region-vs-live,
which went 73 to 79 IDENTICAL; the search-box CONTAINER added to
country-vs-live, 28 to 32, so the 50ch cap cannot regress; the region
card added to list-vs-live at 30/30.

Three of my own assertions were wrong before the code was: I omitted
fontWeight and the underline geometry from the back-link spec while
their `a` sets all three; I compared `gap` on an element where theirs
never sets it; and my sub-pixel tolerance used Number('246.938px'),
which is NaN, so NaN <= 0.5 was false and it silently never fired.

Regression: 22 suites green, 9 gates green.
2026-09-24 05:58:43 +00:00
doxa-maps-88655c70
ba6b9229f3 Merge main into bundler-1/work (regenerate the tree, re-hash)
Some checks are pending
build-gate / build (push) Waiting to run
Six conflicts, all in generated artifacts under public/, including this
bundle's own — another builder's tree regeneration had rebuilt it.
Regenerated rather than hand-merged: the three PosterWall consumers
rebuilt, then the tree and the hashes.

PosterWall.vue itself auto-merged with both sides intact — this
branch's embedded/sectionHeads props and main's usePosterPrints
refactor — and the four suites that exercise it through the regions
poster dropdown were re-run green after the merge, since their change
lands inside the component this feature renders.
2026-09-24 05:33:32 +00:00
doxa-maps-88655c70
a0910bcdef refactor(packet): the legend's packet export uses the shared action
Some checks are pending
build-gate / build (push) Waiting to run
Continues the consolidation. The card deck moved to usePosterPrints last tick;
the PACKET export in upg-100-list.vue was still a second hand-maintained copy of
the same shape — its own renderer import, its own strings fetch, its own
language resolution. Two copies of one fact drift.

printPacket now lives in the composable, so the browser export and the
build-time generator share one renderer AND one calling convention. What stays
in the bundle is what genuinely cannot be shared:

  packetRecords  who is in this list — the people-groups API, the selector
                 evaluator, and the pg_slugs special case that 12 of the 27
                 lists need (the evaluator intentionally THROWS on that type)
  packetMapSvg   where this bundle's posters are served from. Absent is fine:
                 the packet prints a cover without the map rather than failing

⚠ AND THE MIGRATION WOULD HAVE MADE THE PACKET PRINT SILENTLY. The composable
reports through its own exportState; this profile has no HUD for that and shows
`status` instead — exportState appears ZERO times in the file. Without a bridge
there would be no "building…", and a failure would vanish with nothing said: the
mechanism working while the feedback does not, which is the exact trap the wall
consolidation hit from the other direction. A watcher now maps the composable's
phases onto `status`.

Verified: check:poster-prints drives the shared action and asserts the packet's
SHAPE — 4 sheets for 2 groups (map + people page + points + wheel), 12 wheel
segments, Mark 5:1-20 present — not merely that bytes were produced. Confirmed
to fail before acceptance by removing the wheel page (3 sheets, 0 segments). The
build-time generator still passes check:packet-render at 8/8, and
check:deck-click still prints 116 cards through the real button.
2026-09-24 05:22:29 +00:00
doxa-maps-88655c70
63fad20739 Regions: their worldmap watermark, and two defects it uncovered
Measured first, as the Oswald lesson requires, and this one was real:
their UupgsList root carries worldmap.svg inline (UupgsList.vue:451),
and it genuinely renders on their live country page — HTTP 200, 62,740
bytes over the wire, computed background-size 60%, position 50% 0%,
repeat no-repeat, min-height 400px. Confirmed by screenshotting the
ELEMENT, not the viewport: the watermark sits below the fold, so a page
screenshot shows nothing at all.

public/assets/images/worldmap.svg is their file byte-for-byte — 311KB on
disk, small against this tree's existing assets, which run 644KB to
42MB. The inline background-image takes its base from a new imagesBase
prop, since a dropped bundle cannot assume their absolute /assets path.

generate-tree's staged allowlist needed 'images' added — the same trap
that silently broke the vendored fonts. A dir absent from the allowlist
404s, and an unfetched background-image is quieter than the font
failure was: nothing paints and nothing reports. So the probe asserts
the REQUEST returned 200 rather than that the CSS names the file.

Then, reading the screenshot after every number was already green, two
more defects:

- A CLASS WITHOUT ITS RULE. The search label uses their class name,
  sr-only — the right instinct — but their rule
  (utilities/_visibility.scss:2) was never ported, so the class matched
  nothing and the label rendered as a visible line of text above the
  input, duplicating its own placeholder. Borrowing a class name only
  works if its declarations travel with it, and a class matching no rule
  is invisible to a property diff. Now 1x1px and clipped, still in the
  DOM and still announced.

- A PIXEL CAP WHERE THEIRS IS RELATIVE. .search-box was capped at a
  hardcoded max-width:480px; theirs composes .max-width-md, which is
  max-width:50ch (utilities/_sizes.scss:11). Wrong value and wrong unit —
  ch tracks the input's font, px does not — and ours truncated the
  placeholder mid-word. 50ch now resolves to exactly 600px, their
  measured width.

Regression: 18 suites green, including a new _probe-watermark.mjs at
15/15.
2026-09-24 05:15:06 +00:00
doxa-maps-88655c70
6c32623856 refactor(poster): the 100-list wall stops carrying its own copies
Finishes the half this card left open last tick. The shared print composable
existed but the 903-line 100-list wall did not use it, so the card deck lived in
TWO places — I wrote both this flight, and they could already drift.

Two duplicates removed:

  printCardDeck   now delegates to usePosterPrints, sharing THIS wall's
                  exportState so the HUD keeps rendering one surface. The bundle
                  still owns what cannot be shared — fetchListRecords knows the
                  people-groups API, the selector evaluator and the pg_slugs
                  special case — and hands it over as the `records` provider. The
                  preview-only "no API configured" hint is preserved, because the
                  shared action cannot know about it.

  openPrintWindow was a local reimplementation of printInPlace.js — same
                  off-screen iframe, same doc.write, same autoprint script,
                  differing only in an unused `title` argument. A FOURTH copy of
                  one mechanism is how a fix to the blank-first-page behaviour
                  reaches one caller and not the others. Now a one-line adapter.

★ AND check:share-policy CAUGHT A REGRESSION I HAD CAUSED. Adding the
country-posters two-tab MFE last tick gave that bundle a second ShareButton —
the BUG-UPG100-DOUBLE-SHARE state the 100-list already fixed once. Fixed the
same way the 100-list fixed it: the MAP's button is REMOVED and the MFE header
owns sharing. Removed, not flag-gated — a `v-if="false"` button is dead code
that still reads as an affordance to every reader and every counter, which is
exactly why the gate counts tags rather than renders.

Verified by clicking the REAL button in a browser, not by driving the renderer:
116 cards at 2.5in 3.5in with 116 QR codes, and 3.5in 2.5in when the stock
picker is switched to landscape — so records provider, picker, language, shared
composable and print frame are connected end to end. check:mfe-two-tab still
reports exactly one share affordance on the country-posters MFE.
2026-09-24 05:10:44 +00:00
doxa-maps-88655c70
916ef35ee8 Merge main into bundler-2/work
Some checks are pending
build-gate / build (push) Waiting to run
# Conflicts:
#	public/doxa-maps/country-posters/countries-map/index.html
#	public/doxa-maps/country-posters/country-poster-wall/index.html
#	public/doxa-maps/country-posters/country-posters-mfe/index.html
#	public/doxa-maps/country-posters/country-posters.js
#	public/doxa-maps/index.html
#	public/doxa-maps/manifest.json
2026-09-24 05:09:42 +00:00
doxa-maps-88655c70
fc69adbbd5 Country boundaries load on MapLibre in the shared library, not per plugin
main added country-posters-mfe (isMap=true). It inherits the engine swap
correctly -- 14 -> 4 requests to api.mapbox.com, maplibregl loaded -- but
threw

    Fetch API cannot load mapbox://mapbox.country-boundaries-v1.
    URL scheme "mapbox" is not supported

A new map arriving is exactly what the rollout has to cover, so this was
worth chasing rather than noting.

TWO shared library sites, not one, which only measuring found. Fixing
useCountryOutline.js (consumers: doxa-simple-map, countries-map,
research-map) cleaned two of the four leaves and left countries-map -- and
the new element nesting it -- still throwing, from a SECOND site:
useMapLayers.js addRegionsLayer. Both now branch per engine, using the
keyless assets/boundaries/world-adm0.geojson the tree already ships, with
iso3 normalised once to iso_3166_1_alpha_3 so every downstream expression
keeps reading the name it always read, and 'source-layer' set for the
vector-tile path alone.

The engine is read off the LIVE map -- its container carries
maplibregl-map -- rather than from config, because both composables are
called with nothing but a map getter.

MEASURED: 0 mapbox:// errors across country-posters-mfe, countries-map,
doxa-simple-map, research-map and demo-map; the adm0 fetch confirmed firing;
provider sweep 15/17. The Mapbox path is unchanged -- its control screenshot
still shows the coloured WAGF region fills.

Also fixes check:descriptions, which the new profile red-gated by shipping
an empty description. Written from the component's own header, not a
placeholder: 34/34 profiles described.

NOT FIXED, and recorded in the ledger rather than glossed: under MapLibre
countries-map still paints NO coloured region fills. The error is gone and
the geojson loads, but the fill does not appear. That is a separate defect
from the source failing, and claiming this commit finishes it would be
false.
2026-09-24 05:09:12 +00:00
doxa-maps-88655c70
7f8b75d661 Experimental shelf: Kingdom-impact order + curated exclusions
Some checks are pending
build-gate / build (push) Waiting to run
The Experimental folder now presents most-impactful first per the
finalized roundup ranking (EXPERIMENTAL-MAPS-ROUNDUP.md): Prayer Walk
Planner, the rescued legacy set (entry: send-map), All People Groups,
Country Posters, Vision Card -- rank is editorial curation and lives
beside TAB_DEF; unranked bundles still discover and sort A-Z after.
Verified in the built shell rail.

Exclusions tightened: demo-map + golden-path-check (scaffolding /
authoring tooling -- the studio keeps them), doxa-research-map (the
part already composed into Simple Research), stack-demo (broken and
deferred; a broken tab on the giveaway shelf reads as damage).
Wrapper description corrected: the legacy IIFE holds FIVE maps.
2026-09-24 05:02:21 +00:00
doxa-maps-88655c70
c1ab013f9e Merge main into bundler-1/work (regenerate the tree, re-hash)
Some checks are pending
build-gate / build (push) Waiting to run
The one conflict was in public/doxa-maps/index.html, which generate-tree
writes. Built files are regenerated, not hand-merged: country-posters
was rebuilt because main changed its source (a new two-tab MFE),
regions-dashboard because this branch did, then the tree and the hashes.
2026-09-24 04:54:42 +00:00
doxa-maps-88655c70
c268d13a42 Regions: the list tab was half-localised, and Oswald must not be vendored
Two findings, both from measuring at locale=ru instead of trusting the
plan.

DO NOT VENDOR OSWALD. The work list called for it for Russian headings.
Their rule is [lang="ru-RU"] on h1/h2/h3 (_globals.scss:40) — but the
live document's lang is "ru", so the selector never matches. It is dead
code on their own site. Oswald never loads there either:
document.fonts reports it "loading"/"unloaded" while Bebas, Poppins and
Brandon all report "loaded", and the glyph oracle agrees — Oswald
renders "HAMBURGEFONS" at 855 against sans-serif's 856.36 (0.2%, noise),
where Bebas moves it to 486 (43.2%). Bebas itself has no Cyrillic (501
vs 501.42), so their Russian headings fall back to the system face at
letter-spacing normal. Vendoring Oswald would have made us DIFFER.
_probe-ru-parity.mjs now asserts that parity instead of assuming it:
10/10, both sides falling back identically at 444.88.

The list tab was half-localised, and three of the four defects were
invisible in English:

- .rl-title forced text-transform:uppercase. Their h1 is none on both
  locales. Invisible in English because Bebas is an all-caps face, and
  wrong in Russian, where theirs reads "Регионы" and ours read
  "РЕГИОНЫ". This file's own comment already recorded the correct
  finding for .region-card__title; this rule was missed.
- summaryLine() was a hardcoded English template where theirs is
  t('%count% people groups in %countries% countries'). The Russian
  string was already in our catalogue — copied, never wired.
- dashboardLink() likewise: their ru value is "Открыть панель: %region%",
  a different word order no English template could be patched into.
- Nothing refetched on a locale change. The region and country labels
  come from the API's ?lang=, and their site serves Азия/Африка/
  Азербайджан on /ru/regions. region-dashboards.vue already had
  watch(languageCode, loadRegions); this tab did not, so a locale switch
  translated the chrome and left every label English.

Why none of it was caught: _probe-region-vs-live.mjs does compare
textTransform and reports 73/73 — but it diffs the DASHBOARD tab's
.rd-title. Nothing ever property-diffed the LIST tab's head against
/regions. The gap was coverage, not the property list.
_probe-list-vs-live.mjs closes it at 8/8.

The loading state now uses their own 'Loading results...' key rather
than new English, since that string is already translated in all eight
locales.

Regression: 18 suites green.
2026-09-24 04:53:36 +00:00
doxa-maps-88655c70
2d8361a4a0 feat(country-posters): two-tab element, reusing the shared view switcher
Driver: "Country Posters is supposed to have two tabs, structured like the
UPG-100 list."

★ THE CARD'S REAL POINT WAS THE CONTROL, and it was already settled: this is the
THIRD consumer of the SELECT VIEW switcher, and library/components/ViewSelect.vue
already exists precisely because a third hand-rolled copy was imminent — its own
header says a fourth divergent copy is not what the library is for. So this
element imports the shared component rather than adding one, and the whole
feature is one new profile following an established shape.

country-posters-mfe.vue mirrors upg-100-mfe deliberately (index.js already
records that this bundle is "the same shape as my-upg-100-list"): the
pane-wrapper column, each pane under its own ProfileLoader/Pinia, one
ShareButton in the header sharing the COMPOSITE leaf — not a child, which is the
BUG-SHARE-FROM-CAP4-COMPOSITE bug this pattern already paid for.

LIST FIRST, MAP SECOND, per the Driver's principle that maps are a secondary
view: the poster wall is the concrete artefact a newcomer can act on, the map is
where it gets placed. Pane order lives in at[0] and in the array, because
buildPaneColumn sorts by at[0] while ViewSelect renders the array verbatim —
desynchronising them is a recorded trap.

It also CLOSES A DEAD SEAM. country-poster-wall has dispatched
'doxa-country-posters:show-on-map' since it was written, but only the app shell
listened — so in a dropped embed or a nested composite the action fired into a
document with no handler and did nothing. This shell owns the event and swaps to
the map tab, which is what makes the two tabs one element rather than two pages
that share a bundle.

Verified in a browser by the new check:mfe-two-tab: host 900px, tabs
"Country Posters | Map", both panes mounted at 1280x843, exactly one share
affordance, zero page errors. The same gate passes unchanged on upg-100-mfe and
regions-mfe, so the three composites are now held to one contract.
2026-09-24 04:32:05 +00:00
doxa-maps-88655c70
61dcd9b28e Merge main into bundler-1/work (regenerate the built tree, re-hash)
Some checks are pending
build-gate / build (push) Waiting to run
The nine conflicts were all in generated artifacts under public/. Built
files are not hand-merged: every bundle whose source either side touched
was rebuilt, the tree regenerated against main's new generate-tree, and
the hashes re-baked. Source merged clean — main never touched
PosterWall.vue or en/regions.json, they only appeared in a two-dot diff
because this branch changed them.
2026-09-24 04:28:25 +00:00
doxa-maps-88655c70
ddde518a85 Regions: expand a region to its country posters, reusing the shared wall
Driver request: "a toggle or carrot added to the bottom of the region
section that says view country posters, which will let you view country
posters for each of the countries in the regions" — with View-on-map and
Print, "the same reusable code from upg 100 list". This is a third
sanctioned addition alongside the tab strip and the no-selection chooser.

Almost nothing was built. library/components/poster/PosterWall.vue was
already the shared, fully parameterised grid, and it already owned the
card layout, the "Show on map" header button, the PRINT OPTIONS footer
and the client-side raster. regions-list.vue supplies only data and a
toggle — the same division country-poster-wall.vue states in its own
header.

- A "View country posters" toggle at the bottom of every region section,
  carrying aria-expanded and aria-controls, using the same caret SVG the
  header and footer links already use, rotated when open.
- Opening a region renders that region's countries. Measured: 41 cards
  for Africa and all 41 previews LOAD (naturalWidth>0 — a 404 still
  yields an <img>, so the load is the real check). No assets were
  copied: public/assets/previews already holds 174 country-*.webp and
  generate-tree already stages them.
- "Show Chad on the map" reaches the Region Dashboards tab with title
  "Chad" and hash "#chad", by mapping PosterWall's CustomEvent listId
  back through the country-selected the tabs already exchange.
- India prints its own reason with the exact number from the data; China
  renders a real poster. Only India is unsupported — 855 groups and no
  sheet, against China's 154 on a 3x4.

Two defects the screenshot caught and no assertion did:

- PosterWall's root is position:absolute;inset:0, because in its own
  bundles it IS a pane. Embedded in a region card it painted 1440x1100
  over the region's country list. Fixed by parameterising the shared
  component rather than overriding its CSS locally: new `embedded` and
  `sectionHeads` props, both defaulting to today's behaviour, so
  country-posters and key-cities are unchanged by construction. Both
  re-checked. Re-measured: root static, 1200px wide, panel below the
  grid, no overlap.
- With one unnamed group the wall printed a section head reading the
  literal "Other" plus a count duplicating the region card's own
  summary. Suppressed by sectionHeads=false.

The dataset moved to library/data/country-posters.json now that it has a
second consumer, one import line changed in country-posters, and
emit-countries.mjs was retargeted so the next generator run cannot
recreate the duplicate.

The three new strings do not exist on DX-life, so there was nothing to
copy and nothing was invented in another language: en only, with the
other seven locales falling back to the key.

Regression: 15 suites green, including region-vs-live 73/73,
country-vs-live 28/28, mobile 44/44 and 26/26, and a new
_probe-region-posters.mjs. check:library-index drifted for real this
time (3 new library files) and is regenerated here.
2026-09-24 04:26:29 +00:00
doxa-maps-88655c70
716e7c84eb Merge main into bundler-2/work
Some checks are pending
build-gate / build (push) Waiting to run
# Conflicts:
#	public/doxa-maps/index.html
2026-09-24 04:22:01 +00:00
doxa-maps-88655c70
86a3c3bb99 Experimental roundup: FIVE legacy maps resurrected, keyless, in Experimental
Some checks are pending
build-gate / build (push) Waiting to run
The pre-bundler upg-neighbor IIFE holds five lost experimental maps --
send-map, upg-neighbors (cockpit), lang-cluster, bloom-cockpit,
flower-mesh. One wrapper bundle now rescues all five as preset
instances, each mounted in a sandboxed iframe whose host page:
  - loads our self-hosted MapLibre and aliases it as the mapboxgl
    global with a patched Map constructor that rewrites the one
    mapbox:// style to keyless OpenFreeMap Positron -- the whole set
    runs WITHOUT a Mapbox token;
  - shims relative /api fetches to the live API and sets
    MAP_APP_API_URL (the cockpit fails closed without it);
  - passes the legacy profile-config contract (the elements share the
    modern loader lineage).

Wrapper reads profile-config as a PROP (direct-defined element --
inject('profileConfig') is null outside ProfileLoader; the send-map
default masked the miss because send-map is instance #1). The
vocabulary patch rides in the shipped copy ('Lower pressure').

Verified by screenshots read: send-map renders the full 4-D SEND JOIN
app; flower-mesh blooms (India, 2,040 UPGs); the cockpit renders map,
charts and bloom pane. Auto-flows into the SPA's Experimental section
via manifest discovery.
2026-09-24 04:20:24 +00:00
doxa-maps-88655c70
a68b7c2cc2 Merge main into bundler-2/work
# Conflicts:
#	public/doxa-maps/index.html
#	public/doxa-maps/manifest.json
#	public/doxa-maps/my-upg-100-list/my-upg-100-list.js
#	public/doxa-maps/my-upg-100-list/poster-wall/index.html
#	public/doxa-maps/my-upg-100-list/upg-100-list/index.html
#	public/doxa-maps/my-upg-100-list/upg-100-mfe/index.html
#	public/doxa-maps/regions-dashboard/region-dashboards/index.html
#	public/doxa-maps/regions-dashboard/regions-dashboard.js
#	public/doxa-maps/regions-dashboard/regions-list/index.html
#	public/doxa-maps/regions-dashboard/regions-mfe/index.html
2026-09-24 04:05:36 +00:00
doxa-maps-88655c70
b5f831701e Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-1/work 2026-09-24 04:02:02 +00:00
doxa-maps-88655c70
9b9f2765b4 Regions: one rollup, ported from theirs, instead of two copies
summarizeRegions() was duplicated in regions-list.vue and
region-dashboards.vue — two copies of the same rollup, so two chances
for the region page and the dashboard to disagree about one number.
Both now import a port of DX-life's config/countries-meta.ts, placed in
library/ because it has two named consumers.

Ported verbatim: slugify's replace chain, FULL_PRAYER_COVERAGE_COUNT,
addToStats's conditions, fitView's constants, lngExtent's largest-gap
walk, WORLD_VIEW, both sort orders. Changed only: TypeScript to JS
(interfaces to JSDoc), and their `import { COUNTRIES } from
'../app/utils/countries'` becomes library/data/countries.json — their
same 242-entry {value,label} array, in their order. toNumber and
emptyStats are exported beyond their surface, and only those two: their
pages need no cross-region total because DX-life has no "all regions"
view, and ours does for the no-selection chooser.

Two things the copies had wrong, both fixed by porting rather than
patching:

- They slugged a country from the API's LOCALIZED label. Theirs slugs
  from the canonical English name, deliberately. The live site settled
  it: /regions/the-united-states is 200 and /regions/united-states is
  404; /regions/the-democratic-republic-of-the-congo is 200 and
  /regions/dr-congo is 404. Since the dashboard writes the slug into
  the URL hash, our deep links pointed at pages that do not exist
  there.

- They dropped centre/zoom/bounds entirely, so our maps derived their
  own frame. Theirs computes a fit view per country and per region,
  including the antimeridian: Oceania's bounds come out east 187.9,
  where a naive max() says 178 and spans the globe backwards.

Also fixed regions-list.vue's formatNumber, still hardcoded en-US after
the same fix landed in region-dashboards.vue, and its region sort, which
used (a.label || 'Other') where theirs compares a.label directly.

Proof: region-vs-live 73/73 and country-vs-live 28/28 UNCHANGED — the
rollup is equivalent. New _probe-countries-meta.mjs is 20/20 and
includes a negative test showing the antimeridian assertion really
discriminates. oceania-and-list, regions-swap, standalone-app,
dashboard, total and search all green. 159 lines of duplication gone.
2026-09-24 04:01:34 +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
316939796b Merge main into bundler-2/work
# Conflicts:
#	public/doxa-maps/index.html
#	public/doxa-maps/manifest.json
#	public/doxa-maps/regions-dashboard/region-dashboards/index.html
#	public/doxa-maps/regions-dashboard/regions-dashboard.js
#	public/doxa-maps/regions-dashboard/regions-list/index.html
#	public/doxa-maps/regions-dashboard/regions-mfe/index.html
2026-09-24 03:52:54 +00:00
doxa-maps-88655c70
9f0df8b579 Exact sits in the search row, where DX-life puts it
Driver: "my filter underneath the simple research map does not look like the
same CSS styling of the one on dx-life."

Read the reference: UupgsList.vue puts the input and the Exact button in ONE
`.repel` row inside `.search-box`, so the modifier reads as part of the
field it changes. Ours had Exact in a SECOND filters__panel below the four
dropdowns, where it looked like a fifth filter -- and, because that panel
only exists while filters are expanded, a user searching with the panel
collapsed could not reach it at all.

★ COPYING THE REPEL WITHOUT THE WIDTH CAP MADE IT WORSE, and the screenshot
was the only thing that said so. dx-life's markup is
`.search-box | center | max-width-md` wrapping the repel: the repel only
reads as a PAIR because the box is narrow. Dropped into our full-width row
it pushed Exact against the far edge -- input ending at x=843, button at
x=1231, 390px apart -- which reads as an unrelated control. The row is now
capped at 600px and centred, and repels inside that. Wraps on a phone so the
button drops under the input rather than squeezing it.

NOT DONE, and deliberately left for the Driver: he listed Country, Adopted
and Engaged among dx-life's controls. This element ships FOUR dropdowns by
an explicit earlier decision of his, recorded twice in this file -- "COUNTRY
IS PROP-ONLY, NOT A FIFTH DROPDOWN ... the Driver's exclusion of Country was
about this element's OWN CONTROLS". Describing dx-life's controls is not the
same as asking for Country back, and adding it would silently reverse that.
Written up in the rollout ledger as a question instead.

gates:pre 0. gates:post 17/18, the single failure being the pre-existing
verify/tables-complete break from the visa-leak fix.
2026-09-24 03:52:44 +00:00
doxa-maps-88655c70
72be2bee84 The map keeps the wheel; the edge strips carry the page
Driver: "two finger scrolling on the map should allow me to interact with
the map, but instead it just scrolls down past the map and does not
interact with the map -- that's what the scroll side bars are there for, to
let me interact with the map AND have a way to scroll down past the map."

REPRODUCED before touching anything: wheel over the centre of the map in
research-map-plus-grid moved scrollY 0 -> 480 and the camera never moved.
The map was inert exactly where it is the main control.

CAUSE: _syncScrollZoomToHost disabled scroll-zoom whenever the document or
an ancestor could scroll -- which is ALWAYS true in a composite that stacks
a grid under a map. It was not careless: it fixed
BUG-CANNOT-SCROLL-DOWN-TO-THE-GRID, where the map ate every wheel and the
grid below could not be reached. But that bug already had a second fix --
the .map-scroll-edge gutters, a dedicated page-scroll lane down both sides,
which the Map constructor's own note calls "the single-finger page-scroll
escape". Two fixes for one bug, and the second silently undid the feature
while leaving the first redundant.

So scroll-zoom now stays enabled and the strips carry the page. The
docScrolls/ancestorScrolls probe stays, because the edge affordance keys its
gray indicator off the same question.

MEASURED on the built tree, 1280x900:
  wheel x4 over map centre    scrollY 0 -> 0     map takes the gesture
  wheel x4 over edge strip    scrollY 0 -> 480   escape still works
  (two strips, 46px wide each)
And the repo's own fitness gate, which exists for this property:
  check:fits-screen 17/17 PASS -- including "the grid is reachable" and
  "the wheel is targeted", so the bug this replaced has not returned.

DashboardMapPanel carries the same helper and was deliberately NOT changed:
different layout contract (map inside a fixed shell with facet/table/rail
columns), and fits-screen passes it today as "wheel escapes every column".
Flagged in the ledger for the Driver rather than altered on the strength of
a report about a different surface.

gates:pre 0. gates:post 17/18, the single failure being the pre-existing
verify/tables-complete break from the visa-leak fix.
2026-09-24 03:47:28 +00:00
doxa-maps-88655c70
3e54c7f6a3 Regions: their search, and three places I was not a clone
Port dx-life's fuzzy search into UupgsList onto the repo's existing
@map/components/search/fuzzy.js shim: their 7 SEARCH_KEYS, threshold 0
when exact else 0.4, minMatchCharLength 3, diacritic stripping, their
highlight() construction and match routing, and .search-highlight in
#92b195 from their blocks/_cards.scss:258. A typo now finds the row and
the matched run is wrapped, neither of which the old substring filter
did. _probe-search.mjs: 6/6.

Then, reading the screenshot taken to prove that search worked, three
clone-fidelity defects that every numeric check had certified green:

- The count line above the list read "N People Groups · Cameroon". It
  was invented; it is not on their page. Theirs
  (UupgsList.vue:577-583) is "Total: N" in .font-size-sm inside a
  .repel, with the see-all link suppressed by hide-see-all-link.
  _probe-total.mjs: 7/7.

- formatNumber was hardcoded en-US. Theirs (RegionDetail.vue:16) is
  n.toLocaleString(`${locale}-u-nu-latn`) — locale separators, Latin
  digits. We were wrong in de, fr and es.

- The country strapline formatted its count. Theirs
  (CountryDetail.vue:40) is the raw {{ country.count }} — that
  component never defines formatNumber. Their asymmetry with
  RegionDetail; a clone matches it instead of improving on it.

Why nothing caught the invented line: every check compares properties
of elements present on BOTH sides, so an element present only on ours
is invisible to all of them. _probe-total.mjs now carries a surplus
check — a phrase assertion scoped to .ul-root, since the first draft
swept the whole element and failed on their own legitimate "People
Groups" in the stat cards and the country table header.

Regression: region-vs-live 73/73, country-vs-live 28/28, mobile 44/44
and 26/26, i18n, chooser, dashboard, standalone-app, search, switcher,
goalcards, h1-highlight, total — all green.
2026-09-24 03:40:47 +00:00
doxa-maps-88655c70
9202469197 The grid's filter block gets DX-life's world-map backdrop
Driver: "there is an SVG of a map that shows up under the search bar, where
the toggles are... my filter underneath the simple research map does not
look like the same CSS styling of the one on dx-life."

Read the reference rather than guessing at it:
dx-life/app/components/public/UupgsList.vue puts
  background-image: url(<images>/worldmap.svg); background-size: 60%;
  background-position: top
on the container wrapping the search box AND the toggles. So it is a
BACKGROUND behind the whole block, not an icon beside the input -- which is
the detail that makes it read as a map tool instead of a form. Ported
faithfully, per the house rule to imitate DX-life's decisions rather than
substitute our own tokens.

The 311KB file is served as a SHARED ASSET, not inlined: a data: URI would
put it in every bundle that imports this grid, and it is decorative.
resolveAssetsBase() is the same helper the boundary geojson already uses, so
it resolves wherever the tree is dropped -- CDN, sub-path, or the studio --
instead of assuming a root-relative path. It lives in public/assets/, the
shared pool, which `clean` does not wipe, so no change to the tree verb.

It is drawn ONLY when the block exists. With show-search and show-filters
both off there is nothing to sit behind, and a backdrop with no controls on
it is noise.

MEASURED on the built tree:
  /assets/brand/worldmap.svg              HTTP 200, 0 failed requests
  #filters computed backgroundSize        60%
  #filters computed backgroundPosition    50% 0%   (top centre)
  grid leaf (defaults on)                 backdrop ON
  research-map-plus-grid (Simple Research) backdrop ON, behind search +
                                          Hide Filters + 4 dropdowns + Exact
  dashboard-plus-grid (both props false)  no #filters element at all
Screenshot read, not just counted.

gates:pre 0, check:public-hygiene 0. gates:post 17/18 -- the single failure
is the pre-existing verify/tables-complete break from the visa-leak fix,
filed and not ours.
2026-09-24 03:40:35 +00:00
doxa-maps-88655c70
cb75ba38b5 Regions map: country boundaries load on MapLibre too
Caught by spot-checking after main moved, not by the sweep. RegionCountryMap
added its country polygons from mapbox://mapbox.country-boundaries-v1, a
Mapbox-hosted VECTOR TILESET that MapLibre cannot fetch:

    Fetch API cannot load mapbox://mapbox.country-boundaries-v1.
    URL scheme "mapbox" is not supported

The basemap then drew perfectly with the highlighted countries simply
absent. My own sweep called this leaf "SWAPPED" because it counts canvases,
engines and request totals -- all of which were right. A coarse metric can
be green while the primary data layer is missing; only the console said so.

Fixed by the pattern DashboardMapPanel already proved, copied rather than
reinvented: under maplibre the source is geojson, fed from the keyless
assets/boundaries/world-adm0.geojson the tree already ships and three other
plugins already consume through resolveAssetsBase. Added EMPTY first and
setData after the fetch, so the addLayer calls stay synchronous and the
fills remain beneath the pins. The tileset names the ISO field
iso_3166_1_alpha_3 and the geojson names it iso3, so the property is
normalised ONCE at load and countryFilter() keeps reading the single name it
always read -- the Mapbox path is byte-for-byte unchanged. 'source-layer' is
set for Mapbox alone; MapLibre rejects a layer that declares one on geojson.

MEASURED via the PUBLIC querySourceFeatures (not a private field -- that
lesson cost two reverts earlier in this flight):
  mapbox    400 boundary features, 0 console errors
  maplibre  467 boundary features, 0 console errors   (was 1 scheme error)
Both engines expose the same three layers.

gates:pre 0. gates:post 17/18, the single failure being the pre-existing
verify/tables-complete break from the visa-leak fix (filed, not mine).
2026-09-24 03:34:19 +00:00
doxa-maps-88655c70
c279899990 Merge main into bundler-2/work
# Conflicts:
#	public/doxa-maps/index.html
#	public/doxa-maps/manifest.json
#	public/doxa-maps/regions-dashboard/region-dashboards/index.html
#	public/doxa-maps/regions-dashboard/regions-dashboard.js
#	public/doxa-maps/regions-dashboard/regions-list/index.html
#	public/doxa-maps/regions-dashboard/regions-mfe/index.html
2026-09-24 03:27:22 +00:00
doxa-maps-88655c70
efe2e506a8 Their heading highlight, and the page head tab 1 never had
A 68-of-68 computed-style diff was reporting the region page identical while two
visible things were wrong.

1. THEIR TITLES HIGHLIGHT THEIR LAST WORD. Every heading on their regions
surface is <h1 class="highlight" data-highlight-last>, and their
useTextHighlight wraps the final word in .color-primary. Measured live:
/regions/africa is ENTIRELY rgb(146,177,149) because "Africa" is the last word,
and /regions/middle-east renders 'Middle <span class="color-primary">East</span>'.
Ours was entirely dark.

★ HOW A 68/68 DIFF MISSED AN ENTIRELY GREEN HEADING: the h1 spec listed
fontSize, lineHeight, fontFamily, textAlign and textTransform — and not COLOUR.
The lesson is not "add colour to the list". It is that a property absent from
the spec is NOT MEASURED, and unmeasured is not identical. Same shape as the
font miss, where both sides named Poppins and only a glyph-width oracle showed
we were not rendering it. Colour now covers the h1, its highlight span, the stat
values, the headline figures and the goal lines: 68 -> 73 properties, still 0
differ. I found this reading their source for a different card, not from a check.

Ported declaratively rather than as their DOM walk. Theirs mutates innerHTML
once onMounted and stamps a processed flag — right for a Nuxt page whose title
never changes. Ours changes on every selection, so an imperative one-shot would
either fight Vue's re-render or go stale after the first subject. Same logic,
expressed where Vue can keep it true.

2. TAB 1 HAD NO PAGE HEAD AT ALL. pages/regions/index.vue:47-58 opens with that
h1 plus a .color-brand-lighter strapline; we rendered only the region cards.
Invisible inside the composite, where the tab strip names the view, and plainly
missing on the regions-list leaf, which is a page in its own right.

★ AND THE IMAGE CAUGHT MY OWN DEFECT. A whitespace-only <span> </span> between
two inline elements COLLAPSES: "Middle East" rendered "MIDDLEEAST". Every
assertion was green — the colour was right, the span was there, the text
content was right once whitespace-normalised. Only the screenshot showed it.
The space now lives inside the text interpolation, and an innerHTML assertion
guards it that was first shown to FAIL on the collapsed form.

Verified: region 73/73 vs live, country 28/28, mobile 44/0 and 26/0 at 390+768,
i18n, chooser and dashboard suites green, no gate newly red.
2026-09-24 03:22:15 +00:00
doxa-maps-88655c70
12e4c5e8f7 resources-map honours the provider switch
The last leaf the sweep found still pinned to Mapbox. Like
connectivity-map and my-upg-100-list it builds its map RAW (new gl.Map on
window.mapboxgl), so the resolution useMapInstance performs for every other
bundle never reached it: MEASURED at ?provider=maplibre it made 18 requests
to api.mapbox.com, identical to its mapbox run.

Same fix, third application, deliberately identical: the engine comes from
the shared ensureEngineScript/engineGlobal, and the two hardcoded mapbox://
style URLs are paired through the registry — 'ink' (dark-v11 / OpenFreeMap
dark) and 'paper' (light-v11 / positron). The engine loader is imported,
never re-declared.

No geocoder is mounted here, so the deferred-search-bar rule does not apply
and nothing about search behaviour changed.

MEASURED on the built tree:
  mapbox    canvas=1  18 mapbox requests  (unchanged)
  maplibre  canvas=1   4 mapbox requests  maplibregl loaded
Screenshot read: positron basemap, the gospel-resource gap choropleth, its
legend and controls all intact.

gates:pre 0. gates:post is 17/18 and the single failure is verify, which is
the PRE-EXISTING tables-complete break: commit 4475581 deliberately deleted
field-access-visas.json to close a visa-data leak while index.json still
declares it. 4475581 is an ancestor of main, so main was already red before
this flight. Filed as BUG-TABLES-COMPLETE-RED-AFTER-VISA-LEAK-FIX; not
touched here, because guessing risks re-adding leaked data.
2026-09-24 03:20:44 +00:00
doxa-maps-88655c70
874186660e Merge main into bundler-2/work
# Conflicts:
#	public/doxa-maps/index.html
#	public/doxa-maps/manifest.json
#	public/doxa-maps/my-upg-100-list/my-upg-100-list.js
#	public/doxa-maps/my-upg-100-list/poster-wall/index.html
#	public/doxa-maps/my-upg-100-list/upg-100-list/index.html
#	public/doxa-maps/my-upg-100-list/upg-100-mfe/index.html
#	public/doxa-maps/regions-dashboard/region-dashboards/index.html
#	public/doxa-maps/regions-dashboard/regions-dashboard.js
#	public/doxa-maps/regions-dashboard/regions-list/index.html
#	public/doxa-maps/regions-dashboard/regions-mfe/index.html
2026-09-24 03:17:16 +00:00
doxa-maps-88655c70
34cb921b96 connectivity-map honours the provider switch
Found by verifying the GOAL rather than the checklist. The rollout table
only ever listed plugins with a search bar, so a sweep of all 16 isMap
leaves was run under both providers: 12 of 16 swapped, and this leaf made
18 requests to api.mapbox.com under ?provider=maplibre — identical to its
mapbox run, because it builds its map RAW (new gl.Map on window.mapboxgl)
and never reaches the resolution useMapInstance does for everyone else.

Same shape and same fix as my-upg-100-list, minus the token gate this file
never had: the engine comes from the shared ensureEngineScript/engineGlobal
rather than a hardcoded global, and the two hardcoded mapbox:// style URLs
are paired through the registry — 'ink' (dark-v11 / OpenFreeMap dark) and
'paper' (light-v11 / positron) — so both engines show one intent. The
engine loader is imported, never re-declared; a second ENGINES table in a
profile is how two copies of one fact drift apart.

No geocoder is mounted here, so the deferred-search-bar rule does not apply
and nothing about the search behaviour changed.

MEASURED on the built tree:
  mapbox    canvas=1  18 mapbox requests  (unchanged)
  maplibre  canvas=1   4 mapbox requests  maplibregl loaded
Screenshot read: positron basemap, connectivity choropleth, legend and
controls all intact.

Also recorded from the sweep: poster-wall and regions-list draw NO canvas on
either engine — they are the already-filed isMap misclassification, not
gaps, and must not be "fixed".

7/7 pre, 18/18 post.
2026-09-24 03:14:35 +00:00