Commit graph

1123 commits

Author SHA1 Message Date
Doxa Maps
beb82f2e2e UPG Neighbor Campaigns: hovering a dot names its group
After a language was picked, its believer (green) and unreached (red) dots were
anonymous until clicked, and a green dot could not be clicked at all. Hovering either
now shows the group and its country, as Mobilize does.
2026-09-28 10:08:43 +00:00
Doxa Maps
77c7b16fef UPG-100: the HUD band follows the reading direction
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
.upg-hud-band used margin-left:auto to push itself to the trailing end of the
flex row .upg-hud-head. That end is the LEFT in Arabic, so the band sat on the
wrong side. margin-inline-start:auto, the same class as the legend and table
conversions. No consumer overrides .upg-hud-band, so nothing can defeat it.

Found by a localization audit of this bundle (upg-100-mfe + poster-wall +
upg-100-list). The rest of the audit came back clean, and both scans were
NEGATIVE-TESTED before their zeros were believed:
  - hardcoded English in templates: a planted sentence IS found, so the zeros in
    poster-wall.vue and upg-100-mfe.vue mean something. The only template hits in
    upg-100-list.vue were `{{ LEGEND_TITLE }}` (a translated computed, a false
    positive of the regex) and one real one, below.
  - script-side user-visible English: none.
  - key coverage: 23 keys used, 0 missing across all 11 locales. A planted
    nonexistent key reports missing in 11/11 and a real key resolves in 11/11, so
    the checker is neither blind nor vacuous.

ONE FINDING IS NOT MINE TO FIX: upg-100-list.vue:2786 has a literal
title="Batch Select" where its siblings at :2811 and :2833 use a translated
:title. The fix needs a new key, and the catalogues live in library/i18n/locales/
— the build owner's under rule 5B. Sent to dx-shepherd rather than landed here.
2026-09-28 10:00:37 +00:00
Doxa Maps
8e13477340 Remove the superseded headline-tiles computed
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
`tiles` built four stat objects that nothing rendered. The commit which cloned
DX-life's region dashboard replaced its template block with `stats` and the
shared StatCard, and removed the .rd-tile styles with it, but left the computed
behind.

It was not merely unused: it carried four bare English labels, so a localisation
scan reports it as hardcoded English every time. It cost this audit the time to
prove it harmless, and would have cost the next one the same. `stats` renders
the same four figures and puts every label through t().

Verified superseded rather than unfinished before deleting: the template usage
and the computed were added by one commit and the usage removed by the next, the
styles are already gone, and nothing in the plugin refers to it. Behaviour is
unchanged.
2026-09-28 09:55:30 +00:00
Doxa Maps
ae960e82b5 UPG-100: read pin features through the shared helper, not source._data
Clicking a pin to open the people-group detail panel did nothing under MapLibre,
with no error of any kind.

openPgDetail read `src._data.features`. The two engines store _data differently,
and both bundles say so in the same constructor (this._options = Object.assign
follows in each):

  mapbox-gl v3.24.0   this._data = i.data                           the raw value
  maplibre-gl         this._data = "string"==typeof i.data
                                   ? {url: i.data} : {geojson: i.data}

So `._data.features` resolves under Mapbox and is UNDEFINED under MapLibre.
Every guard in the chain then turned the wrong shape into a no-op: `|| []` made
it an empty list, find() returned undefined, `if (f)` was false. Silent, and
indistinguishable from a missed click -- the same defect class as sr's landed
MapLibre source-read bug.

Now calls readSourceFeatures() from library/maps/pin-selection.js, which falls
back to the public serialize() that both engines expose. One import, one call, no
new API.

The mapbox citation was taken from the exact CDN bundle the app loads
(library/constants/cdn.js pins v3.24.0), fetched and verified against its pinned
SRI hash before reading, then deleted. No browser involved -- downloading and
grepping a bundle never starts one.

Verified in the BUILT output rather than by function name, which minification
renames: the helper's distinctive public fallback `serialize` appears once in the
rebuilt bundle and zero times in main's.
2026-09-28 09:48:45 +00:00
Doxa Maps
0d85e0dfcc Localise the regions fallbacks that still rendered English
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
A regions audit across 60 keys and 10 locales found the catalogue itself sound
-- every key present everywhere, and the few strings identical to English are
cognates their sibling languages spell differently. The gaps were in the plugin,
in the places a value is absent and a fallback renders instead.

Eight `|| 'Other'` fallbacks rendered the English word in every locale, though
'Other' is translated in all eleven and the regions list already called t() for
exactly this. A country subtitle concatenated ' people groups ' as a literal.
Both now go through t().

The map popup aligned its stat values with text-align:right, which does not flip
for Arabic, so the values sat on the wrong side of a right-to-left popup. It
uses the logical end instead, as nine other declarations in these files already
do.

Two strings are left deliberately: the region and all-regions subtitles build
"N countries" and "N regions" from template literals, and no key exists for
either in any locale. Word order and plural agreement differ per language, so
inventing a construction across eleven of them would be worse than the English
it replaced. Those need catalogue keys and are reported rather than guessed.
2026-09-28 09:44:48 +00:00
Doxa Maps
33362f5592 Let the gospel map's own text follow the reader's direction
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Eight CSS declarations in the production profile still named a physical side, so in Arabic they
pinned translated text to the left: the legend key's wrapped label, the search-match rows, the
people-group buttons in the gap list and in the detail panel, the error line, two chip margins,
and the row-count badge beside each table tab. Every one of them now uses the logical property —
text-align:start, margin-inline-start — which is identical to today in every left-to-right locale
and correct in the two right-to-left ones.

Found by auditing the production profile's localization rather than by a rendering failure, which
is how this kind of defect stays in: the Arabic page looks translated, because it is, and only the
alignment quietly disagrees with it.

Nothing in the English-prose islands changed. Those carry dir="ltr" on purpose — the caveats stay
English by the standing ruling, and the isolation is what keeps them from disturbing the Arabic
text around them.

Rebuilt gospel-resources only, then fix:sri; check:sri passes at 64/64 leaves.
2026-09-28 09:22:02 +00:00
Doxa Maps
a5eb9f6fa6 Make the all-regions chooser's resting pins easier to see
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
The Driver reported the pins as hard to see. On the chooser this is the state
that answers it: what the page shows on load, before any hover.

Two things make a pin harder to read here than on a cloned country page, and
neither is a matter of taste. The chooser renders the globe, so every pin sits
at the lowest useful zoom instead of a framed close-up; and the 1px white stroke
is a fixed cost, so at radius 3 it consumes a third of the pin's width and the
coloured core carrying the region's identity is only about 2px. Raising the
resting radius from 3 to 4 at zoom 2 grows that core from 2px to 3px -- a ~50%
gain in the part a reader actually distinguishes, for ~33% more footprint.
Opacity goes to 1 for the same reason: 10% transparency over a basemap costs
contrast and buys nothing while nothing is being de-emphasised.

Only the chooser changes. The cloned country and region pages keep DX-life's
exact geometry, and the hover behaviour is untouched -- dimming the other
regions is the design saying "these pins are one group", which is a taste call
rather than a legibility defect.

Whether this is now easy enough to see needs eyes; the argument here is only
that the readable part grows faster than the footprint.
2026-09-28 09:03:26 +00:00
Doxa Maps
112f4cee77 check:studio-rail — a production profile must DECLARE its name
The rail showed tiles nobody had named while this gate stayed green. The obvious
fix is a no-op, and writing it first is how that came out.

THE NO-OP: testing the rail's label for null/empty can never fail, because
rowsFromManifest falls back to humanize(id) (plugin-studio.vue:421,429). A
profile that declares nothing still arrives as "Doxa Simple Map" -- not empty,
and not equal to the bare id either, so the existing bare-id rule misses it too.
humanize() defeats BOTH rules. That is why country-posters could ship a tile
nobody had named.

So the property is the DECLARATION, read from the manifest, not the rendered
label: does this profile's <profile>.meta.json carry a "label"? A humanized id is
a reasonable DEFAULT and a bad NAME -- "Doxa Simple Map" tells a reader nothing
the folder did not already say.

PRODUCTION-STAGE ONLY, deliberately: 15 profiles declare no label today and 14
are experimental -- work in progress nobody has named yet, which is not a defect.
A gate that is red for a condition the repo accepts gets ignored.

Also declares the two labels my own plugin was missing: upg-100-list becomes
"UPG-100 — Zoned Map" and poster-wall "UPG-100 — Poster Wall".

⚠ THIS GATE IS RED ON MAIN TODAY, 8 violations across 4 plugins and 4 owners
(cities-internet 2, doxa-simple-map 1, gospel-resources 3, my-upg-100-list 2).
It must NOT land before those labels do, or it blocks every other agent's
landing for a defect that is not theirs. The two meta files here fix my share;
they reach the manifest only through the full build, which is the build owner's.

internal/ is the build owner's under rule 5B -- source sha to studio, sequenced
after the other three owners declare their labels.
2026-09-28 08:31:18 +00:00
Doxa Maps
fbc517c94a A browser gate refuses to launch outside the browser slot; check:browser-gates-guarded keeps it that way
Only the browser seat runs browsers, and only inside internal/browser-slot.sh.
That rule was remembered, not enforced: an agent ran a gate it took for a
headless source check and it launched Chromium outside the slot.
internal/lib/playwright-guarded.mjs is now the one way a repo file reaches
playwright: its launch(), launchPersistentContext(), launchServer(), connect()
and connectOverCDP() exit 75 (NOT MEASURED) with a message naming
browser-slot.sh unless DOXA_BROWSER_SLOT is set, and browser-slot.sh exports it
for the command it runs, so a bash -c grandchild inherits it. All 72 files that
imported playwright switch to it (internal/lib/browser.mjs and 64 internal gates;
plugin-designs scripts: doxa-research-map 3, my-upg-100-list 2,
research-map-plus-grid 2, research-dashboard 1): a mechanical import swap.
check:browser-gates-guarded (gates:pre) fails a direct import;
bun run browser-gates lists the 97 scripts that launch a browser. isBrowserGate
moves to internal/lib/browser-gates.mjs so the runner and the check share it.
2026-09-28 08:20:21 +00:00
Doxa Maps
e335d56409 Prayer Progress: the control column follows the reading direction, on MapLibre too
The legend and search column were placed with left:10px and styled only under Mapbox's
.mapboxgl-ctrl-top-left, so on the keyless engine they kept MapLibre's defaults and in
Arabic stayed on the left. Both engines' classes now take inset-inline-start (the same
fix country-posters carries), and the help close button and the detail labels use
logical sides.
2026-09-28 08:08:19 +00:00
Doxa Maps
e893db3718 Name the three gospel profiles the Studio was calling by their ids
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
charts, data-tables and resources-list had no <profile>.meta.json at all, so the manifest carried
no label for them and the Studio rail humanized the folder name: "Charts", "Data Tables",
"Resources List". Those say what the file is called, not what the page answers.

Each now has a meta with a label and its stage:

  charts          Gospel Resources -- where the gap is, by language
  data-tables     Gospel Resources -- every resource and every gap, in tables
  resources-list  Gospel Resources -- what each people group has

All three declare "stage": "experimental" EXPLICITLY, which is what they already were. A meta-less
profile and a meta without a stage key both default to experimental in profile-meta.mjs, so the
value would otherwise rest on that default -- and these three sit in a bundle whose stage reads
production through gospel-dashboard, so a later change to the default would silently promote them.
Measured after the change: the three stay experimental, gospel-dashboard stays production, and
bundleStage still returns production.

Type is deliberately not declared. generate-tree resolves the composition tier from meta.json
`type`, not from whether the file exists, so omitting it leaves all three inferred `single`
exactly as before. Nothing in the manifest moves except the three labels and three stage values
that already held.
2026-09-28 07:54:11 +00:00
Doxa Maps
96ba45b966 Name the two unlabelled Cities with Internet profiles
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
connectivity-list and data-tables had no profile meta at all, so the Studio and
the gallery fell back to a humanised profile id: a reader picking a view saw
"connectivity list" and "data tables" rather than what the pane actually is.
The sibling connectivity-map already carried a label; these two are now in step,
so the three read as one family in the picker instead of sorting apart.

The wording is not invented. Each pane renders its own heading from i18n —
connectivity.list.title is "Cities with Internet" and connectivity.tables.title
is "Cities Data" — so the labels lead with the product name the reader already
sees, then name the view, matching the shape connectivity-map established.

Both metas declare stage experimental explicitly. That is the existing default
for a profile with no meta, so nothing is promoted and the manifest is byte-for
-byte unchanged; it is stated because bundleStage turns a whole bundle
production as soon as one profile claims it, and a stage left implicit is one
default change away from moving on its own.

Source only, no bundle rebuild: the label is read from the meta by the gallery's
discovery, and never travels into manifest.json.
2026-09-28 07:44:45 +00:00
Doxa Maps
d8baaefd2e Country posters and Prayer Progress: every profile has a name in the manifest
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
countries-map, country-poster-wall and doxa-simple-map carried label:null, so the Studio
rail and the gallery showed their ids. Each now has a label in its meta; types are
unchanged (doxa-simple-map stays parameterized).
2026-09-28 07:33:23 +00:00
Doxa Maps
97903f756a Country posters: pre-warm skips only paints that already read the feature
An array is not the same as data-driven: a zoom-only interpolate is an array and
still compiles a variant on the first gesture. The warm-up now leaves alone only values
that read a feature property, and wraps constants and zoom-only expressions.
2026-09-28 07:26:59 +00:00
Doxa Maps
4b941be4bf Country posters: the packet prints the selected groups, not the first fifty loaded
Choosing "Asia" in the legend printed the same first 50 groups as any other row,
Sudan included. The packet now takes the pins the selection shows, through the shared
library/maps/pin-selection.js (reading pins as drawn, not raw rows). Selected pins
that match no rows fall back to the full list with a warning; an empty selection
reaches the printer's own error; over 50, the cover says '50 of N'.
2026-09-28 07:26:58 +00:00
Doxa Maps
16b2e7aeaf Country posters: the first country highlight no longer freezes on a shader compile
highlightCountryByIso makes the regions fill and border paints data-driven on a click,
and the first such click paid a one-off shader-variant compile (rega measured 5.5 s /
11.1 s on the research map). The three paints are now data-driven from the moment the
layer is added, with identical branches so nothing changes on screen.
2026-09-28 07:26:58 +00:00
Doxa Maps
e020e68bca Skip a paint that is already data-driven, not one that merely looks like an expression
exp-shepherd's copy of this pre-warm is better than mine in one way and accidentally right in another,
and both are worth taking.

BETTER: it leaves an existing expression alone instead of wrapping it. Mine wrapped regions-border's
line-color — which arrives as a region-coloured match on a feature property — into a case around that
match. No visual change, but a gratuitous rewrite of an expression that was already data-driven and had
no variant left to warm.

ACCIDENTALLY RIGHT: their test is "is it an array". For this layer that coincides with the right answer,
because the only array here is that match. It is not the right test in general — a zoom expression like
['interpolate',['linear'],['zoom'],…] is also an array and is NOT data-driven, so skipping it would
leave that property to compile its variant on the first gesture. That is the partial pre-warm this whole
exercise exists to avoid, and it would measure as a failure with no visible cause.

The test is now whether the expression reads the FEATURE — get, has, or feature-state, at any depth.
Constants and zoom-only expressions get wrapped; anything already keyed on a feature is left exactly as
the layer wrote it. The regions layers have no zoom expression today, so this changes nothing that runs
right now; it means the rule is correct for the reason it is correct.

Verified: gates:pre 13/13. Chained on 790d8838.
2026-09-28 07:07:34 +00:00
Doxa Maps
f83b3a195d The pre-warm was repainting the borders it was only supposed to warm
Found by reading addRegionsLayer while checking exp-shepherd's copy of this fix, not by anything that
failed. My pre-warm called paintRegionScope(map.value, []) — the CLEARED look: flat grey borders at
width 0.6. The layer does not start there. useMapLayers adds regions-border with line-color set to the
REGION-COLOURED match expression and line-width 1, so the moment the Regions tab was first opened, every
border turned grey and thinner than the layer had defined.

Nothing was going to catch it. The shader compile moved as intended, the write count was unaffected, the
highlight still worked, and the checks in flight were a timing measurement and a write count — a change
of appearance is invisible to both. It would have been found by a person looking at the map, eventually,
and blamed on something else.

prewarmRegionPaint reads each property back and re-writes it wrapped in a case whose two branches are
the value it already had. The appearance is preserved exactly as the layer defined it, and the property
still becomes data-driven, which is all the shader variant depends on. A property the layer does not
define is skipped rather than invented, and one already wrapped is left alone so a second call cannot
nest cases.

paintRegionScope keeps its idle values for the CLEAR path, which is a different thing from the initial
state and always was — conflating the two is what caused this.

Verified: gates:pre 13/13. Chained on 196eca13. Needs eyes: rega — on the first visit to the Regions
tab, the country borders should be REGION-COLOURED at their original width, not grey.
2026-09-28 07:07:34 +00:00
Doxa Maps
35955a4db6 check:one-palette sees a bundle's private src/colors/ palettes; Rudder's offline hint states Go 1.22+ and the setup
Two findings from upg100's census. check-one-palette read strategy palettes from
library/colors/ only, so a bundle's own src/colors/ strategy (which overrides the
library one for that bundle) was never checked; it now reads both, the bundle's
over the library's, and --self-test plants a private palette. Rudder's
engine-offline notice gave a go build line without saying Go 1.22+ is needed or
that setup-runtime.sh downloads the model; it now says both and points at
bun run desktop:build and the desktop HOWTO.
2026-09-28 07:07:33 +00:00
Doxa Maps
d0939c0492 library: one embedScript for the app shells, replacing four private copies
DEDUP-AUDIT.md (measured today: 253 files, 31 cross-plugin duplicate clusters,
622 duplicated lines) ranks this tenth by raw cost and first by suitability. It
is the only cluster where the copies are identical in code, close over nothing,
and sit in two plugins with one owner between them.

loadScriptOnce (13 lines) and whenTagDefined (11) were byte-identical between
doxa-maps-spa and plugin-studio. deriveTreeBase (9) differed by ONE COMMENT LINE
and not one character of code -- drift in the documentation of a shared function,
which is its own argument for having one copy.

They are one concern, not three: load a bundle's script once, wait for its custom
element to register, and work out the tree base to load it from. That is what a
capability-5 app shell does to nest another plugin, and both plugins here ARE the
capability-5 shells. Any future shell needs the same three.

Verbatim move: no parameterisation, no behaviour change, callers import instead
of declaring. The comments travel with the code because each records a
measurement -- particularly the one explaining why onload is not a usable success
signal for a dev-server ES module.

ONE BEHAVIOUR CHANGE, AND IT IS AN IMPROVEMENT: the _loaded cache is now shared.
Two shells on one page previously kept separate caches and each loaded the same
bundle; the second now reuses the first's promise.

deriveTreeBase reads document.currentScript, so it is only valid AT MODULE
EVALUATION TIME. The call sites did not move -- both entries still call it at
module top level -- and the library file documents the constraint, because a
later call silently returns '' and every nested bundle URL would break.

Verified: both bundles build; each built file contains all three functions
exactly once and still derives currentScript; check:sri, check:series-isolation,
check:studio-rail and check:capability-derived all PASS.

library/utils/embedScript.js is the build owner's under rule 5B. The
plugin-designs and public/ changes are in the same commit because they import it
-- landing them apart breaks the build in between.
2026-09-28 07:07:33 +00:00
Doxa Maps
2bbabaa72b A region's countries come from the regions dataset, not from whichever ones happen to have pins
Canonicalising the ISO list made the two region painters COMPARABLE. Reading the two sources shows it
cannot make them EQUAL, so the write count would have landed at six rather than three — and the reason
is a real defect underneath, not a tuning problem.

The two sets are different by construction:
  · node.isoCodes comes from useWagfRegionsLegendData, which builds its regions by walking the
    PEOPLE-GROUP LIST, so it holds only countries that have pins in the current data.
  · mapLayers.regionIsoMap comes from regionsData.isoToRegion — every country assigned to the region.

So a region highlight painted from the legend has HOLES wherever a country in that region has no
unreached group in the list, and which of the two a reader saw depended on which writer ran last. The
feature is "highlight the countries belonging to the selected WAGF region", so the regions dataset is
the right answer and the legend's pin-derived list is not.

region: rows therefore stop painting here and defer to the canonical path they already trigger
(selectRegion -> the store watcher -> applyDimFilter). block: and country: rows keep painting from
node.isoCodes, because nothing else paints them — only region: reaches selectRegion — and for those two
the legend's own list IS the scope being asked for.

★ THIS IS A PREDICTION, and it is falsifiable: region clicks should now be 3 writes, not 6. If rega
counts 6, the two sources disagree for some other reason and my reading of them is wrong. If they count
3 but the highlight covers FEWER countries than before, then the pin-derived set was the intended one
and this commit is the thing to revert — the previous behaviour was last-writer-wins, so nobody has
decided which is correct until now.

Verified: gates:pre 13/13. Chained on 1be024ac.
2026-09-28 06:44:14 +00:00
Doxa Maps
c0c1a72851 The write compare was comparing two orderings of the same set
rega's L43 check: three of four pass — the row-click shader compile is 0 ms on both engines (was
3.8 s / 7.5 s), the tab visit is no worse, the highlight stays. The collapse did not: still nine
setPaintProperty calls per row click instead of three.

Their hypothesis is right, and I had the evidence before I wrote the code. The two callers build the
same region from different lists — node.isoCodes and mapLayers.regionIsoMap[slug] — and they do not
agree on ORDER. rega said so in the very first stack trace on this bug: "a case over ['IND',…]" from one
path, "a case over ['MYS',…], a different literal order" from the other. I then wrote a JSON-string
compare, which cannot match two orderings of one set. The pre-warm half worked because that expression
is a shared constant; this half compared text that was never going to be equal.

The list is now canonical before both the compare and the write — de-duplicated, upper-cased, sorted —
so the expression is a function of the SET, which is the thing the two callers actually agree on. Order
is irrelevant to `in` membership, and the case normalisation is a no-op on today's data (both lists come
from the same regions dataset, and the highlight matches now, so they are already upper-case); it is
there so a future source that ships lower-case cannot silently select nothing.

★ IT IS ALSO A TEST, and worth reading as one. If the count drops to 3, the two sources agree about
which countries are in a region. If it does not, they disagree about the CONTENT — a data defect this
compare would otherwise have hidden behind a last-writer-wins repaint, and the next thing to chase.

Verified: gates:pre 13/13. Chained on f7a2f001 (same file). Needs rega: re-count with the same probe.
2026-09-28 06:44:14 +00:00
Doxa Maps
16c98acfe9 A comment of mine said this dropped jspdf from the bundle; it never did
The claim was wrong when I wrote it and I only checked the built artifact afterwards. jspdf arrives
through useMapPoster.toPdf, which this map still uses for the poster, so the packet path was never the
thing holding it. The bundle in fact GREW about 131 KB (983,264 -> 1,117,687 on L42) when the shared
HTML renderer came in: each bundle is a single .js with no code splitting, so the dynamic imports in
this function are inlined at build time rather than lazy, which also makes the older "keep it off the
critical boot path" note beside them untrue.

Correcting it in the source rather than only in the ledger because the sentence was copied VERBATIM into
country-posters when that map moved to the same renderer — a wrong comment written confidently travels,
and the next person to plan a dependency removal would have started from it.

The change itself stands: the packet prints instead of downloading, and the duplicate implementation is
gone. It cost bytes rather than saving them, and now the file says so.

Chained on 2cf369be because it edits the same function. gates:pre 13/13.
2026-09-28 06:44:14 +00:00
Doxa Maps
6451a2b158 bun run desktop:build <plugin> packages a desktop plugin as an app; check:targets keeps non-web plugins out of the web build
A plugin's <profile>.meta.json targets (web, mobile, desktop, widget; absent = web)
now decide where it is built. internal/plugin-targets.mjs is the one reading.
desktop:build builds a desktop plugin into .desktop/<plugin>/ with a page that
names its engine, builds its backend as a Tauri sidecar for this machine, and runs
tauri build with a --config override; tauri.conf.json and public/ are untouched.
The shell starts the bundled rudder-engine (RUDDER_AUDIO=browser, RUDDER_HOME in the
app data folder) and stops it on exit. A plugin whose targets leave out web
(Rudder: desktop + widget) is no longer built for the web tree: run-parallel,
generate-tree and check-composite-freshness skip it, and its public/ folder is
removed; the dev server still gives the Studio its corner. check:targets
(gates:pre, with a self-test of five planted violations) validates the values,
widget corners and backend folders and fails if a non-web plugin reaches the built
tree. Measured: desktop:build rudder-voice on Linux produced Rudder_0.1.0_amd64.deb
(8.8 MB) with the app and the engine side by side; the app has not been launched.
2026-09-28 06:44:14 +00:00
Doxa Maps
2ea3c53e0f The packet prints what the reader selected, and the question "which pins is that" becomes shared
PACKETSEL, found by exp-shepherd: the prayer packet printed the first 50 of EVERY people group whatever
was selected, so a reader who had narrowed the map to one language family got a packet about somebody
else. I carried that behaviour through verbatim when I moved the packet to the shared printer — it was
already wrong and I preserved it, which is worth saying plainly.

The rows now come from the pins currently SELECTED: the layer's own filter decides which pins render,
`_pinMatchExpr` decides which of those are lit (the affinity tab dims rather than filters), and both
must match — a pin at opacity 0 is not what the reader is looking at.

★ ASKED OF THE MAP'S FEATURES, NOT OF THE ROWS, and this is the part that had to be got right rather
than guessed. Pin properties are DERIVED from rows: useMapLayers fills languageFamily in when the API
returns null for all 2,069 groups, and canonicalizes doxaRegion onto the palette's key forms precisely
so setFilter matches. So evaluating a pin filter against a raw data row is wrong in a way that NEVER
THROWS — it just selects a different, plausible-looking subset. The features the map holds are the only
place the two vocabularies agree.

Answering that question already existed, privately, inside useMapFly: zoomToLegendRow has to solve it to
fit the camera to a legend row, including the reason it cannot use querySourceFeatures (viewport-clipped,
so an off-screen region silently selects nothing). Those three functions move VERBATIM to
library/maps/pin-selection.js and useMapFly imports them, so there is one evaluator rather than two —
and country-posters, which prints the same kind of packet from the same kind of selection, has the same
helper to reach for instead of a second copy.

Two failure modes are distinguished rather than collapsed: pins selected but NO row matching them is an
id mismatch, so it prints the full list and warns; a genuinely empty selection still reaches the shared
printer's own "that list resolved to no records", which is honest. And when the 50-row safety cap bites,
the cover now says "— 50 of 214" instead of quietly being a different packet than the one asked for.

Verified: gates:pre 13/13, check:composite-interop PASS. Needs eyes: rega — select one language family,
print, and check the cards are that family and the cover carries the count.
2026-09-28 06:29:49 +00:00
Doxa Maps
4f4dc9f3e4 Country posters: the manifest names the tag the two-tab element really registers
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
country-posters-mfe's @element said country-posters-mfe, a tag nothing defines; the
bundle registers country-posters-map, which the embed snippet and leaf page already
use with profile country-posters-mfe. Anything building the tag from the manifest
(check:composite-embed-knobs) mounted an element that never upgraded, so its map
cells could not be measured.
2026-09-28 06:13:32 +00:00
Doxa Maps
d8ee9e9787 Country posters: the packet prints through the shared renderer instead of downloading a PDF
The map's packet built a jsPDF file and downloaded packet.pdf while its poster already
printed. It now goes through usePosterPrints.printPacket, the same path as Simple
Research (8457b5dc): the live map rides in as an image data URL, and an explicit
adapter maps the rows, reading religionName and countryName so a card never prints a
religion code ("MSN") under a people group's name.
2026-09-28 06:05:04 +00:00
Doxa Maps
f8f81b9bcb One writer for the region polygons, and its idle state compiles the shader at layer-add
Both halves are rega's measurements on main 373b7979b, now that the WAGF selection holds and it is worth
optimising rather than optimising a bug.

NINE WRITES PER CLICK -> THREE. A single "Asia (Region)" click produced nine regions-* paint writes:
fill-opacity, line-color and line-width, three times each inside ~110 ms. Two paths painted the same
highlight from DIFFERENT sources — onSemanticTreeSelect from node.isoCodes, applyDimFilter from
mapLayers.regionIsoMap — each triggering the other through mapStore. Two sources for one visual is not
just wasted work: it is how they drift, and it already cost a debugging cycle, because when the highlight
was being cleared we first had to establish WHICH writer had done it. All four paths (click, watcher,
clear, and the layer-add below) now go through paintRegionScope, which writes a property only when the
value actually differs — asked of the MAP rather than a remembered key, so a layer torn down and re-added
cannot leave a skip holding a paint that is no longer there.

5.5 s AND 11.1 s OF SHADER COMPILE, MOVED OFF THE GESTURE. rega measured the row click compiling a new
variant — 5.5 s MapLibre, 11.1 s Mapbox on SwiftShader — because that click was the first moment these
three properties stopped being constants. Identical to the pins defect and fixed the same way: the idle
value is now ['case', ['has', 'iso_3166_1_alpha_3'], x, x], both branches the same so not one pixel
changes, and it is installed the moment addRegionsLayer returns. The variant therefore compiles on the
tab visit that is already loading a 6.4 MB GeoJSON, not in the middle of a gesture.

That placement is the whole point, and it is what I got wrong on the pins the first time: an idle
expression that is never installed pre-warms nothing. Here it is painted at layer-add, where the layers
first exist.

LEFT ALONE DELIBERATELY: highlightCountryByIso (0.30/0.02) and applyDimFilter's dim path (0.20) are
different states of the same layers, not duplicates of this one, so collapsing them into this writer
would have merged three features into one function to save nothing.

Verified: gates:pre 12/12, check:composite-interop PASS. Needs eyes: rega — expect 3 regions-* writes per
click instead of 9, and no compile on the row click; the compile should appear on the TAB visit instead.
2026-09-28 05:46:36 +00:00
Doxa Maps
0a713df497 The packet card prints the religion's NAME, not its code
Caught by reading the normalizer instead of waiting for the render: my adapter in f6a9d9d2 read
`g.religion` first, and DataSourceManager normalizes that field to the CODE — so every people-group card
would have printed MSN or BLA under the name, with the readable form sitting unused in `religionName`
one line away (DataSourceManager.js:357-358).

The country line had the same shape of error for the same reason. The renderer prints
flat(g.country_code) as display text next to the population, but the FIELD NAME invites handing it an
ISO code; normalized rows keep the code in countryIso and the label in countryName. Both now read the
readable form first and fall back toward the code, rather than the reverse.

This is the failure the commit before it flagged as the thing that could not be verified headlessly — a
wrongly mapped field prints blank or cryptic rather than throwing. It was findable without a browser
after all: the answer was in the normalizer, not in the rendering.

Verified: gates:pre 12/12. rega's check still stands and is still worth doing — this fixes the two fields
I could prove wrong, not the ones a rendered page would reveal.
2026-09-28 05:30:58 +00:00
Doxa Maps
7568c12610 The packet prints too, through the shared renderer this map was quietly duplicating
Driver-lens audit card 2: the packet button downloaded packet.pdf while its poster sibling, two
functions above it, opened the print dialog. Same map, same menu, two different answers to "export".

The fix is not to wrap that PDF in a dialog. A PDF blob cannot go in the <img> the in-place print frame
uses, and every workaround — embed, object, the browser's own viewer — prints on some engines and
silently does nothing on others, which is the dead-button class this file has already paid for twice.

usePosterPrints.printPacket ALREADY does this: the same packet, authored as HTML with @page rules and
printed in place. This map was running a private second implementation of a shared product feature, with
jspdf behind it. Routing to the shared one deletes the duplicate, drops jspdf from this bundle, and the
packet gains the pages the shared renderer has that the local one never did — the life calendar, the
funnel and the prayer wheel, in the Driver's own page order.

★ AN EXPLICIT ADAPTER, because the mismatch would have been SILENT. The renderer reads slug / name /
population / religion / country_code, and usePosterPrints reads image_url; this map's normalized rows
carry country and imageUrl in camelCase. Handed over unmapped, the packet would have printed with blank
names and missing photos rather than failing — so the mapping sits in one visible place instead of
either side renaming its vocabulary.

Page 1 keeps the LIVE map rather than a fetched poster: this half is the one that knows what is on
screen. The shared renderer interpolates mapSvg as raw markup, so the PNG this map already knows how to
produce rides in as an <img>, as a data URL because the print frame is a separate document and an object
URL created here would be revoked out from under it. If that render fails the packet still prints — a
cover without the map beats no packet, which is the call the shared composable already documents.

Dead code removed with it: the download helper that served packet.pdf, and the lazy-import cache for the
composable this no longer uses. Neither export hands over a file now.

Verified: gates:pre 12/12. NOT verified here, and it needs to be before anyone calls it done: I cannot
see a rendered packet. The specific risk is the adapter — a field I mapped wrongly prints blank rather
than failing. rega should print one and look at the people-group cards before this is released.
2026-09-28 05:30:58 +00:00
Doxa Maps
daffaa6ef5 Dev server: a pull no longer reloads the page in a burst, and Rudder's recording pill keeps a fixed width
The Driver's first laptop run with DX_SYNC_REMOTE: when main moved the page went
white. A landing rewrites dozens of built leaf index.html files under public/, and
Vite answers each changed .html with a full page reload (measured: one edited leaf
page, one full-reload broadcast), so a pull fired a burst of reloads while git was
still writing. The dev server no longer watches public/, which it never serves
through its module graph (measured after: no reload). Rudder's corner pill also
grew with every streamed word; while recording it is now 300px wide and shows the
newest words, cut from the front.
2026-09-28 05:30:58 +00:00
Doxa Maps
f3e29d026b UPG-100: test handles are also keyed by instance
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
AUDIT34 card 9 (LOW): window.__UPG_MAP__, __UPG_SELECT__ and __UPG_SEARCH__ are
last-writer-wins. Embed two copies of this map on one page and the second mount
overwrites the first, so a gate driving 'the' map silently drives whichever
happened to mount last.

Adds __UPG_MAPS__, __UPG_SELECTS__ and __UPG_SEARCHES__, each keyed by the
embedder's instanceId (the same field poster-wall.vue:55 keys on), falling back
to the profile name.

THE BARE HANDLES ARE KEPT AND STILL POINT AT THE LAST MOUNT, deliberately. Every
existing consumer reads them directly -- internal/check-map-data-painted.mjs plus
~10 call sites across data/visual-gate.mjs and data/palette-preview.mjs -- and
those are browser gates. Removing the bare handles would break them to fix a case
they never hit, since a gate page mounts exactly one copy. This change is purely
additive: nothing reads the registries yet, so no consumer behaviour moves.

The honest limit, stated in the code rather than papered over: two copies with NO
instanceId and the SAME profile still collide, because nothing in the embed
contract distinguishes them. A mount counter would give them unique keys but make
the key unpredictable between mounts, so a gate could not name the instance it
wants -- worse than a key that repeats.
2026-09-28 05:27:38 +00:00
Doxa Maps
6c46b7317e UPG-100: people-group data follows the reader's language, not English
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
AUDIT34 card 3 (gospel's Driver-lens audit): the on-screen prayer-card fetch
hardcoded &lang=en, so the wall showed English people-group and country names
whatever language the reader picked -- while every word of the interface around
them was translated.

Fixed at three more sites than the card named, all reader-visible:
  poster-wall.vue:161    the prayer-card deck (the card's site)
  upg-100-list.vue:1983  countryNameMap, which feeds resolveTerritory() -- so
                         these names are the territory labels and the legend
  upg-100-list.vue:2294  the MAIN records fetch: the pins, their popups, the list
The first uses getRuntimeLocale() || detectLocale(), the pattern the sibling
plugins already use (people-groups-grid.vue:276,
research-map-plus-grid.vue:147). The other two use this file's existing
packetLang(), which its two packet fetches already used -- so all four fetches in
that file now read one source.

The printed poster is deliberately NOT changed: generate.mjs fetches lang=en of
its own accord, and whether a print artifact is English-only is an open question
with the Driver (raised by regions-plugin). This is the on-screen surface only.
2026-09-28 05:11:14 +00:00
Doxa Maps
5673c209d1 The map was undoing its own selection: an empty external value cleared what the same payload had just lit
The Driver's "the research map isn't connecting with the legend", on the WAGF Regions tab. It connected
and then wiped itself 0.4 s later, which is why it read as not connecting at all.

rega's stack trace ended three rounds of my guessing. The highlight paints at +51 ms; at +438 ms
clearAllHighlights runs, reached from applyDimFilter({kind:null}) through the mapStore.selectedRegion
watcher — so selectedRegion had gone FALSY while externalRegion still read "asia". Not the echo guard I
suspected first, not the JS filter scan I suspected second, not the shared legend's nodes reset I fixed
third (that was a real and separate bug). The round trip did it.

A row click emits map-filtered; the composite pushes the state back down as externalFilters; that payload
carries every key it knows — region:"asia" AND language:"". This handler set the region and, one line
later, "cleared" a language that had never been set. mapStore.selectLanguage() nulls selectedRegion by
design (exclusive selection, Bug 13) and does so even when its argument is null, so selectLanguage(null)
is a clear-everything in disguise. The 0.4 s is the debounce plus the trip through the composite and the
grid, which is why it looked like a mysterious timer.

An empty incoming value is now a NO-OP unless that dimension is actually lit. A genuine host clear still
clears — the dimension is lit and the new value is empty — and a genuine host selection still swaps
exclusively, which is what Bug 13 asked for. Guarded on BOTH axes, not just the one that bit: region and
language are symmetric here and fixing one would have left its mirror image.

★ THE SHARP EDGE IS IN THE SHARED STORE, and I am not moving it from here. selectLanguage(null),
selectRegion(null) and their siblings each null the other dimensions unconditionally, so every caller that
means "clear this one axis" is silently clearing all of them. That is load-bearing for other maps and
changing it belongs to whoever owns the store, so this fix is profile-local and the edge is recorded
rather than patched from a plugin lane.

Verified: gates:pre 12/12. Needs eyes: rega — the WAGF "Asia (Region)" click should now still be lit at
5 s, with the legend row keeping its state class.
2026-09-28 04:41:00 +00:00
Doxa Maps
fe88f90f49 Cities with Internet: people-group dots are dark slate, not red
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
The Driver: red dots on a mostly green map look wrong. A dark slate dot (#1e293b) with the existing
white ring reads on every fill the map uses, the green/amber/red/grey country bands and India's
pale-to-deep blue states, and it carries no good/bad meaning (red is this map's 'under 25%').
Candidates compared side by side on the world, India and West Africa views before choosing.
Rebuilt cities-internet and regenerated the tree (integrity hashes).
2026-09-28 04:38:32 +00:00
Doxa Maps
a57dec789d Gospel x Internet: the Studio rail names it 'Gospel × Internet' 2026-09-28 04:31:15 +00:00
Doxa Maps
fa494dbac0 PPLR maps: the legend tabs are named in the reader's language
The ROP Tree and Language Families legend tabs (and the 'Go up to …' crumb built
from them) were typed in English; they now read the existing tabs.* catalogue
entries. ROP25 stays as the registry's name.
2026-09-28 04:06:20 +00:00
Doxa Maps
5d67a3969d PPLR maps: the legend and the instance store are the library's, not drifted copies
pplr carried private copies of SemanticTreeLegend and usePplrInstance that had drifted.
The instance copy lacked activeTabTouched, so the legend tab snapped back to its
default after a pick; the legend copy spoke English accessibility labels in every
locale. Both are deleted and the views import the library versions.
2026-09-28 03:58:06 +00:00
Doxa Maps
534bbdd5af Rudder stays out of the Research Gallery and makes no network call on a static host
The gallery added every experimental bundle as a tab, so Rudder's engine probes
(/api/rudder/status, /devices, /events) logged three 404s on every embed of the
gallery. The manifest now carries each profile's meta targets, and the gallery
skips a bundle whose profiles are all corner widgets or declare targets without
"web". Rudder itself contacts its engine only where one can exist: the dev
server, the desktop app, or an explicit apiBase.
2026-09-28 03:12:46 +00:00
Doxa Maps
91fac156ee Wall of People Groups: no debug logging in the shipped bundle
40 console.log calls (fetch progress, grid sizes, sample rows, API stats) ran on every
load; public-hygiene flagged them. Warnings and errors are kept.
2026-09-28 02:53:46 +00:00
Doxa Maps
9a146d486d The pre-warm has to be installed at boot, or the first click is still the first compile
70a9ce16 gave the cleared state a data-driven shape and stopped there, which was half a fix. The pins
layer is ADDED with a constant "circle-opacity": 1, and applyPinOpacity is otherwise first reached from
a legend click — so the new expression was only ever the CLEARED state, and the first selection was
still the moment MapLibre needed a new shader variant. The 3.2-3.5 s freeze rega profiled would have
survived the fix meant to remove it.

One line: applyPinOpacity(map.value) in the map-ready path, right after the layer is added. matchExpr is
left UNDEFINED on purpose — "re-apply what is current" — so a selection already restored from a
share-link hash is preserved rather than cleared at boot.

★ HOW IT WAS FOUND, because the method is the point: reading the BUILT bundle, not the source. The
addLayer call with its constant opacity is right there in the shipped JS, next to the pre-warm
expression I had just added — two facts that cannot both be true of a working fix. I had been about to
hand rega a tree to measure and would have read a still-slow first click as "the engine folds the
expression away", which is the wrong lesson and a day of chasing it.

Verified: gates:pre 12/12. Unchanged: this is still zero visual difference — both branches of the
boot-time expression are the same alpha.
2026-09-28 02:38:50 +00:00
Doxa Maps
6eae46c002 The legend reaches the map immediately: the shader variant compiles at boot, not on the first click
The Driver: on Simple Research the research map is not connecting with the legend. It was connecting —
two seconds later, which a reader correctly reads as "nothing happened".

I was wrong about the cause twice, and both wrong answers were cheap to discard because they were
measured. First guess: a late echo through applyDimFilter's 80ms dedupe, whose toggle branch would read
the echo as a second click and clear the highlight. rega instrumented the pin layer every frame for 2 s:
ZERO 'legend:highlight' events on every click and no apply-then-revert anywhere. Second guess: the JS
filter scan in zoomToLegendRow, which walks 2,106 features evaluating ['in',['get','uniqueId'],
['literal', ~700 ids]] linearly. Benchmarked at the real shapes: 13.9 ms per full scan, 4.4 ms with a
memoised Set. Two orders of magnitude short, so nothing there was "optimised".

rega's CPU profile named it. ONE task of 3.2-3.5 s, self time getShaderParameter 3,583 ms +
getProgramParameter 551 ms: the GPU compiling a NEW SHADER VARIANT. The click was the first moment
circle-opacity stopped being a constant and became a per-feature expression, and a data-driven paint
property needs a different program. The second click on the same map costs 205 ms — the program is
cached — which is the signature of a one-time compile and not of anything in our logic. The legend's DOM
was never involved: 34 rows before the click, 34 after.

So the fix is not a smaller expression. It is that the first SELECTION must not be the moment the
variant is first needed. With nothing selected the value is now ['case',['has','uniqueId'], a, a] —
both branches identical, so not one pixel changes — and circle-opacity is data-driven from the first
paint. The program compiles during boot beside every other one, and a selection afterwards only
re-uploads attribute values.

The trade, stated rather than hidden: boot compiles one more program. On a real GPU that is tens of
milliseconds; rega measured seconds only because that box rasterises on the CPU through SwiftShader.
Paying it at load beats freezing the map mid-gesture, and the variant was going to be compiled on the
first interaction anyway.

Second finding from the same profile, fixed here too: circle-opacity was being rewritten 4-8 times for a
single click (+1.52, +1.57, +1.94, +5.17 s) because the selection handler, the glow sync and the slider
watcher all end up in this one function, and every write re-uploads the per-feature attribute. It now
writes only when the value actually differs — asked of the MAP rather than of a remembered key, so a
layer that gets torn down and re-added cannot leave the skip holding a dim that is no longer painted.

Verified: gates:pre 12/12 (check:setup-order included, since this touches module-scope state next to a
function that reads it). Needs eyes: rega, the same affinity click — expect the dim at the first frame
and one long task at BOOT instead.
2026-09-28 02:38:50 +00:00
Doxa Maps
d690b1af41 The country's code comes from the row, not from its name
rega, ?lang=ar on landing 38: "People's Republic of China" and "Lao People's Democratic Republic" were
still English in the top-countries chart. The API labels some countries with their FORMAL name, and
library/data/countries.json does not carry those — it says "China" and "Laos" — so reversing the English
name missed precisely the countries whose names are longest. Measured: both miss the name table, and
both resolve through the row (CHN -> CN -> الصين, LAO -> LA -> لاوس).

Every row already carries both halves: the label the chart aggregates on, and the alpha-3 in
country_code.value — the same accessor the choropleth joins on. Reading the pair out of the data is
exact and needs no alias table. The name table stays as the fallback, so a label whose rows are
currently filtered out still resolves, and a synthetic key ("Other") resolves to neither and renders as
itself.

Deliberately NOT a looser text match. "Democratic Republic of the Congo" and "Republic of the Congo" are
two countries, so stripping formal prefixes is how one would silently become the other — the failure
countryName.js's own header warns about, and the reason that file refuses to guess.

Still English, and not fixable from CLDR: the regions "Middle East" and "North America & Non-Spanish
Caribbean". M49 145 is "Western Asia" and 021 is "Northern America"; neither means the region we draw,
so naming them from CLDR would redraw the region rather than translate it. They need a translation
source, which is the Driver's call — the same class as the affinity blocs and language families.

Verified: check:sfc-parse PASS. Needs eyes: rega, ?lang=ar top-countries.
2026-09-28 02:38:50 +00:00
Doxa Maps
ae10e4b978 Maps start their engine download at setup, and the dashboard's country boundaries download beside the basemap
rega's load waterfall: the engine was requested only after the component tree
had rendered, and the dashboard's 384 KB country boundaries only after the whole
basemap style had loaded, so each waited on the other. The engine download now
starts when the map composable is set up, and the boundaries when the dashboard's
map panel mounts; the style handler awaits that same download.
2026-09-28 02:38:49 +00:00
Doxa Maps
cdb9efc9d4 Plugin Studio names each row from the plugin's own meta label, never a bare profile id
The rail showed Gospel x Internet as "board": a row's name fell back to its
profile id unless a hand-kept DISPLAY_NAME table had an entry. The manifest now
carries each profile's .meta.json label (or title / displayName), and the rail
shows it without a trailing aside in parentheses; with no label it shows a name
made from the ids ("Gospel Resources · Charts"). DISPLAY_NAME is an override
only and is empty. check:studio-rail fails if any row would show a bare id.
2026-09-28 02:38:49 +00:00
Doxa Maps
2dc43364f3 Gospel x Internet: the row checkbox hit area is a full 24 x 24 px 2026-09-28 02:33:15 +00:00
Doxa Maps
3725930af9 Merge main into go-int/work
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2026-09-28 02:20:11 +00:00
Doxa Maps
dfb3dabe55 Merge remote-tracking branch 'origin/main' into bundler-1/work
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2026-09-28 02:16:49 +00:00
Doxa Maps
879bb3a8fe Merge main into go-int/work
# Conflicts:
#	public/dx-maps/doxa-maps-spa/manifest.json
#	public/dx-maps/gospel-internet/board/board/index.html
#	public/dx-maps/gospel-internet/board/index.html
#	public/dx-maps/gospel-internet/board/india-china/index.html
#	public/dx-maps/gospel-internet/board/language-tree/index.html
#	public/dx-maps/gospel-internet/board/platforms/index.html
#	public/dx-maps/gospel-internet/board/project-700/index.html
#	public/dx-maps/gospel-internet/board/zero/index.html
#	public/dx-maps/gospel-internet/gospel-internet.js
#	public/dx-maps/manifest.json
2026-09-28 02:15:00 +00:00
Doxa Maps
fb8a898d2e Gospel x Internet: 24 px hit area on the row checkboxes; the presets keep the coach-mark off 2026-09-28 02:03:11 +00:00