Commit graph

33 commits

Author SHA1 Message Date
Doxa Maps
ba54af0113 A destroyed map's container stops holding it
containerRef.__doxaMap was set when the map was created and never cleared, so the dead map (GL
context, sources, tiles) stayed reachable from the DOM after destroy(). It is now cleared, only if
it is still this instance's. The logo-owner claim is released by the commit before this one.
2026-09-29 00:06:52 +00:00
Doxa Maps
1eae8ed80d Every unmounted map stayed in memory, held by one line on window
rega's heap snapshots (leak-722ac7789): Simple Research and All People Groups
mounted twice went 54.2 MB to 65.1 MB, with live Vue apps 6 to 8. Every unmounted
application was still there, one copy per mount, and the retainers were all the
same shape -- a window key set during setup and never cleared. The closure keeps
its component, the component keeps its shadow root, and a whole app survives its
own unmount. An element property dies with the element; a window key does not.

The named one was window.__doxaLogoOwner -> Map -> div.rm-map-canvas -> ShadowRoot
-> app, released only when the map's 'remove' event fires. That listener cannot be
the guarantee the global is cleared, because it only runs if remove() is reached;
it is a consequence of the guarantee, not the guarantee itself. useMapInstance now
drops the claim inside destroy(), before removing, so it goes even if remove()
throws part-way.

As for why remove() was never reached: research-map's unmount hook ran ten
teardown steps and called destroy() last, and two of those steps were unguarded
calls into other composables. A single throw anywhere above stranded the map, and
with it the app. I could not reproduce which step threw without a browser, so the
fix does not depend on knowing: each step is isolated and destroy() runs from a
finally, where nothing above it can skip it. Ordering teardown so the most
important step is last, behind nine chances to fail, was the real defect.

The globals themselves are released by the components that set them -- four verify
hooks in research-map, three in the dashboard map panel, the vocabulary hook in
the grid, which had no unmount hook at all. Released as SETS: __rdashPinAt was the
one the snapshot named, but __rdashMap holds the map and retains strictly more, so
clearing only the reported one would have looked fixed and leaked nearly as much.

The deletes are written one per line instead of looped over an array of names, so
that the check can read them. A release a checker cannot see is worth no more than
no release, the day someone removes three of the four.

The check flags only values that actually retain. A warn-once boolean latch and a
page-wide cache keyed off itself are deliberate and must not be reported, or it
becomes noise and gets switched off. It is NOT in gates:pre: 19 sites remain in
other owners' plugins, and this is a fleet pattern rather than one plugin's bug.
2026-09-29 00:06:52 +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
02faaab75e A missing Mapbox token is one console.warn per page, not an error per map
With provider=mapbox explicit and no token, useMapInstance logged 'No Mapbox access token provided' as
console.error once per map (7x on pplr), while the UI already says "Map unavailable". It is now a single
console.warn per page, so production consoles stay at 0 errors (dx-shepherd, TKWARN).
2026-09-27 08:57:42 +00:00
Doxa Maps
36ad9ae73c The engine logo's label follows the reader's language, on both engines
rega's row 34 (map controls in ar) found the engine logo's label English: "Mapbox homepage" /
"MapLibre logo" — one engine key with two meanings. engineUiLocale(lang, provider) now takes the engine
and sets LogoControl.Title from mapui.mapboxHomepage or mapui.maplibreLogo (11 locales). gospel-resources
and cities-internet build their maps RAW; engineUiLocale is documented as exported for them. (PinHint's
close button was already translated on main as aria.dismissHint.)
2026-09-26 13:04:16 +00:00
Doxa Maps
7fbad5499c Map controls speak the reader's language on both engines ('Zoom in', 'Toggle attribution', gesture hints)
MapLibre's and Mapbox's own control labels stayed English under ar/es (rega's row 30). Both engines take
a `locale` map of their UI strings at construction; useMapInstance now passes one built from the new
mapui catalogue (17 strings x 11 locales, parity PASS) for the reader's language. The keys are the
engines' own (MapLibre 5.24's list, plus Mapbox's gesture-hint names); an engine ignores the rest.
Built doxa-simple-map carries the translations; check:i18n-treeshake PASS.
2026-09-26 08:39:53 +00:00
Doxa Maps
4a12b009aa Every runtime CDN script and stylesheet is an exact version with an integrity hash
maplibre-gl@5 and maplibre-gl-geocoder@1 were floating ranges, and no runtime loader set `integrity`,
so a CDN could serve any bytes to every partner's visitors unnoticed (CRITIC-5).

- library/constants/cdn.js is the one registry: mapbox-gl 3.24.0, maplibre-gl 5.24.0 (the release the
  @5 range already resolved to), both geocoders (1.9.4 / 5.0.0) and d3 7.9.0, each with its sha384.
  Every host answers Access-Control-Allow-Origin: *, so integrity + crossorigin work cross-origin.
- ensureEngineScript / ensureEngineCSS, the geocoder loaders (library and PPLR) and the generator's
  static MapLibre tags set integrity + crossorigin from it; the embed snippet's engine preload matches.
- check:cdn-pins (gates:post): no floating CDN url in shipped code, and every one is in the registry;
  --online re-fetches each and compares the hash (all 9 match today).

MapLibre stays on 5.x knowingly. GHSA-jrc7-96c5-q579 (DOM.sanitize, reached through a source's
attribution string) affects <= 6.4.0, and 6.x is ESM-only (no UMD global), so the upgrade is a loader
migration of its own. The attribution strings rendered today come from our configured basemap styles
and our own sources; the note in cdn.js says who is exposed until then.
2026-09-26 05:50:24 +00:00
Doxa Maps
0c7f4af47b Engine and token props: a token-service URL is honoured, Clear clears, and a pending token is awaited
(cherry picked from commit 6e4ddceced)
2026-09-26 03:34:15 +00:00
Doxa Maps
ab17c9b6aa The default engine is auto: Mapbox when a token is available, MapLibre when none is
(cherry picked from commit 3cb89eae52)
2026-09-26 03:34:12 +00:00
doxa-maps-88655c70
b0427efcd4 Cities with Internet: the map's stylesheet reaches its shadow root
connectivity-map builds its map raw (new gl.Map), not through useMapInstance, so
nothing injected the engine stylesheet into its shadow root. A document-level
<link> never reaches it. Mapbox's own CSS canary reported "missing CSS
declarations", and controls and markers had no rules to position them.

useMapInstance's ensureEngineCSS is now exported, and connectivity-map calls it
with engineStylesheetUrl(provider) before new gl.Map, re-checking afterwards that
the pane is still mounted. Maps that already use useMapInstance are unchanged.
gospel-resources/resources-map has the same raw-map shape and can make the same
call; that is its owner's change.

Measured with verify-map-paint on a served built tree: main failed both leaves
(canary not salmon, "missing CSS declarations"). This build passes
connectivity-map (live GL, engine CSS applied, 0 unclassified errors).
resources-map still fails, as expected until it adds the call.
2026-09-25 14:28:42 +00:00
doxa-maps-88655c70
319064a11b Map place names in the reader's language, on both engines
Every map translated its own chrome but left the basemap's country, city
and street names in the style default (English on Mapbox, Latin + native
on OpenFreeMap) in all eleven locales. A reader in Arabic saw an Arabic
legend over an English map.

library/maps/localize-labels.js uses each engine's supported mechanism:
- Mapbox: map.setLanguage(lang). It also localizes the Standard style's
  IMPORTED basemap, which a layer rewrite cannot reach.
- MapLibre: basemap symbol layers point at
  ['coalesce', name:<lang>, name:latin, name]. English restores each
  layer's original expression, re-captured on every style load so a theme
  switch never restores the previous style's labels.
Only layers whose text-field reads an OSM name field are touched. A
plugin's own label layers (zone titles, group names) are never rewritten.

useMapInstance applies it on every style.load and when the reader's
language changes (ProfileLoader's 'lang', else profile-config locale, else
?lang / <html lang>). The Prayer Walk Planner drops its private copy for
the shared one.

Measured on the Prayer Progress map, ar/zh/ru/hi:
- Mapbox getLanguage() == locale in all four.
- MapLibre: 13/14 symbol layers localized, and the plugin's own label
  layer untouched.
Read in the Arabic screenshots: القاهرة, الإسكندرية, السويس, الأردن on
both engines. check:gallery-embed (+ :maplibre) and
check:prayer-walk-planner pass. The shared change reaches all 11 bundles
that compile initializeMap, and all are rebuilt here. check:shared-reached
marker basemap-labels-localized: 10/10 consumers.
2026-09-25 09:46:21 +00:00
doxa-maps-88655c70
4fb8176b00 Shape Arabic, Hebrew and Persian map labels on MapLibre maps too
library/maps/rtl-text.js installed the RTL text plugin for MAPBOX only
(window.mapboxgl, from api.mapbox.com). So every MapLibre map, i.e. every
keyless embed, drew RTL place names as disconnected, reversed glyphs.
Measured on the Prayer Walk Planner's printed Arabic sheet.

Under it was a second gap. Bundle entries call the installer at LOAD
time, before useMapInstance has lazily injected a MapLibre engine, so even
an engine-aware installer found nothing to install into.

- rtl-text.js is engine-aware. Mapbox is unchanged (same URL, same lazy
  call). MapLibre gets the same plugin package from jsDelivr, keyless and
  not api.mapbox.com, so keyless pages still make zero Mapbox requests.
  New ensureRtlTextPlugin(gl) waits until it is LOADED, for callers that
  capture a single frame (print).
- useMapInstance installs it the moment an engine global exists, the one
  point every map passes through.
- The planner drops its private copy for the shared helper.

Measured: a print map of downtown Cairo (MapLibre, keyless) shows shaped
Arabic street and place names (شارع التحرير, مسجد الطباخ …); plugin
status "loaded", fetched from jsDelivr. check:gallery-embed passes js,
iframe, neighbour and linked (14 tabs); :maplibre passes;
check:prayer-walk-planner passes.

The shared change reaches all 11 bundles that compile initializeMap, and
all are rebuilt here. check:shared-reached gains an rtl-text-maplibre
marker (10/10 consumers carry it; cities-internet, gospel-resources and
my-upg-100-list boot maps another way and are not affected).

Also: the new Experimental bundle pplr-data-maps loads the Mapbox geocoder
under maplibre (pplrData.js:38). It is an owned, printed MAPBOX_KNOWN entry
routed to its owner; delete it in their fix commit.
2026-09-25 08:20:12 +00:00
doxa-maps-88655c70
0c1a91cd02 useMapInstance: a map unmounted mid-initialise returns quietly instead of throwing
Some checks are pending
build-gate / build (push) Waiting to run
initializeMap is async. If its component unmounts while it waits (a
studio tab switch, a composite swapping views), Vue nulls containerRef
and the next dereference threw "Cannot read properties of null (reading
'getRootNode')" from ensureEngineCSS. check:studio-all hit it about 1
run in 4 (gospel-dashboard); gospel-plugin found a second site one await
later (containerRef.value.clientWidth after the animation frame).

initializeMap now checks the ref after each of its three awaits and
returns silently when it is gone, and ensureEngineCSS returns on a null
element. An unmounted pane is normal, not an error, so nothing is
logged. The ref is the only liveness signal: requiring isConnected too
would silently stop maps that mount into a subtree before it is
attached.

Proof, deterministic in both directions:
- check:map-unmount-mid-init holds mapbox-gl.js, opens a map plugin in
  the studio, switches away (destroying the pane), then releases the
  script. Before: FAIL 2/2 runs (research-map, doxa-simple-map threw
  the production error). After: PASS 2/2 across research-map,
  gospel-dashboard and doxa-simple-map.
- check:map-unmount-race (gospel-plugin's browser-free test, adopted):
  the guarded ensureEngineCSS resolves; with the guard stripped it
  throws the exact message.
- No regression: check:studio-all passes 31/31, and 19 plugins still
  create a live map canvas, as before.

check:shared-reached now tracks this change with a regex marker (a
control-flow fix has no literal that survives minification). It
reports 12 consumer bundles still to rebuild. Verified: rebuilding
doxa-simple-map moves it to "carries it".
2026-09-25 05:01:13 +00:00
doxa-maps-88655c70
4d9b24a82d Merge commit '840830ebc47250b642d6d58ba1e584a30e74d47d' into bundler-1/work
# Conflicts:
#	public/doxa-maps/index.html
2026-09-24 22:45:25 +00:00
doxa-maps-88655c70
770d29fd25 Frame the region map the way their constructor does, not the way fitBounds does
The Africa crops showed theirs tighter than ours, and 107 identical CSS
properties could never have said why. Everything I could think to compare
matched: mapbox-gl 3.24.0 on both sides (their bun.lock, our CDN pin), the
style, the 1200x780 container, the padding of 40, the maxZoom of 6, the box
itself (their viewFor() pin extent is byte-identical to our rollup), and
NEITHER page sets a projection.

The asymmetry was when the fit runs. They frame the box in the constructor
(CountryMap.vue:179); Mapbox v3 resolves a constructor `bounds` while
getProjection() still reports 'mercator', giving zoom 3.210. We had recreated
that as a post-style-load fitBounds, resolved with the globe already active,
giving 2.777 -- a sphere shows more planet at the same zoom, so it pulls back.
0.433 zoom is 1.35x linear scale, which is exactly the tightness in the crops.
Copy don't recreate: I recreated their fit instead of copying their construction.

useMapInstance gains a bounds/fitBoundsOptions pass-through, inert unless a
caller passes it, for the deep-link case -- the only case Mapbox can honour,
since the dashboard mounts its one map before the fetch resolves. The normal
case goes through fitTheirWay(), which asks the engine the same question their
constructor asks: setProjection('mercator'), cameraForBounds, restore the globe
in a `finally` so a throw cannot leave the map flat, then easeTo. Asking the
engine beats reimplementing a Web Mercator fit that would drift from Mapbox's
own padding and worldSize semantics at the next upgrade.

Proof, three independent oracles: the camera moved 2.777 -> 3.210 with the
projection still 'globe' afterwards; the like-for-like tile oracle now reads
ours z3 / 8 tiles / [[-45,-66.51],[90,40.98]] against theirs z3 / 8 tiles /
[[-45,-66.51],[90,40.98]], the same tiles at 1.00x; and the regenerated crops,
looked at, put the same labels and pins on the same pixels.

Two oracles of mine were wrong and are fixed here too. The tile-extent probe
compared a z2-quantized extent against a z3 one and reported a "1.33x framing
gap" twice -- 180/135 is four tiles at z2 against three at z3 of the same box,
and our camera's own visible box was 135.44 deg against their 135 deg of tiles.
It now refuses to compare extents across levels and reports the level instead,
which is the real signal. The framing probe asserted our zoom equalled the
GLOBE's cameraForBounds answer, so it would have failed the correct clone; it
now compares against the mercator-resolved camera and checks that asking the
mercator question left the map on the globe.
2026-09-24 22:31:20 +00:00
doxa-maps-88655c70
d5fff78ccd upg-100: ask the registry for the stylesheet URL, and let the gate hear mapbox
Some checks are pending
build-gate / build (push) Waiting to run
A map inside a SHADOW ROOT has to inject its own copy of mapbox-gl.css, because
a document stylesheet does not cross a shadow boundary. This profile did that
by writing the URL out a second time, and the second copy drifted: it pinned
v3.3.0 while every production loader -- generate-tree's baked <script>,
useMapInstance, generate-gallery -- had moved to v3.24.0. So the map ran new JS
against old CSS.

MEASURED on the leaf: mapboxgl.version 3.24.0, the only mapbox stylesheet
reaching the shadow root v3.3.0, and mapbox-gl emitting its own "missing CSS
declarations" warning. The file's own header already says the engine loader is
imported rather than re-declared "because a second copy of one fact drifts".
The stylesheet URL was that second copy. It now comes from the same registry
via engineStylesheetUrl().

★ THE GATE COULD NOT SEE ANY OF IT, which is the more useful half of this
commit. verify-map-paint asserts a live GL context, zero console ERRORS, and
that mapbox emitted attribution -- all true of a map whose stylesheet never
applied, since the basemap paints fine while the chrome around it does not.
Mapbox itself says so, but as a WARNING, and the listener filtered to
type()==='error'. It now listens for _detectMissingCSS's verdict and fails on
it.

MUTATION-TESTED BOTH WAYS on the same leaf: restore v3.3.0 -> FAIL, exit 1;
restore the registry lookup -> PASS, exit 0.

Two things I had wrong on the way, recorded so the next reader does not repeat
them. I first asserted position:absolute on .mapboxgl-canvas-container -- but
v3.24.0 declares only cursor and touch-action there, so static is its CORRECT
value and that assertion would have failed every healthy map. I also read
"0 .mapboxgl-marker elements" as missing pins; mapbox draws pins as GL geometry
in the canvas, so zero is expected. A screenshot showed the polygons, the
region colours and hundreds of pins all rendering. Both were properties I
assumed rather than verified, and the canary's background-color is the one
mapbox actually defines -- it is the test _detectMissingCSS uses.

The canary alone would still have been a false green here: that rule is
identical in v3.3.0 and v3.24.0, so it is salmon under both. It catches an
ABSENT stylesheet; the warning catches a MISMATCHED one. The gate asserts both.
2026-09-24 22:27:52 +00:00
doxa-maps-88655c70
4350113041 my-upg-100-list joins the provider switch, and drops its search bar
The last plugin on the rollout list, and the only one the switch could not
reach: it builds its map RAW (new gl.Map on window.mapboxgl) instead of
through useMapInstance, so resolving the provider inside the factory --
which fixed every other bundle for free -- did nothing here. MEASURED at
?provider=maplibre before this: still Mapbox, 19 requests to api.mapbox.com,
while five sibling leaves swapped cleanly.

Three things blocked the keyless engine, not one:

  - the engine was hardcoded to window.mapboxgl. It now loads whatever the
    provider selects, through ensureEngineScript/engineGlobal EXPORTED from
    useMapInstance. Imported, not copied: a second ENGINES table living in a
    profile is how two copies of one fact drift apart.
  - a `if (!token) return` early exit. A token is a MAPBOX requirement, not
    a map requirement -- left in place it would have refused to draw the
    engine that needs no token, turning the keyless option into a blank
    cell. Now mapbox-only.
  - a hardcoded mapbox://styles/mapbox/light-v11. Now paired through
    resolveNamedStyle('paper'), the registry entry that already pairs
    light-v11 with OpenFreeMap positron, so both engines show one intent.

And the search bar follows the standing decision -- deferred to Mapbox,
because GeocoderComponent's maplibre path still geocodes through Mapbox, so
a keyless map with a search bar is not keyless. MapToolbar collapses its
reserved gap and the legend rises into the vacated space.

MEASURED on the built tree, both engines:
  mapbox    search@12   legend@58   19 mapbox requests   (all unchanged)
  maplibre  search@NONE legend@8     5 mapbox requests   maplibregl loaded
The remaining 5 are the leaf's own static preloads -- already filed, and
not something this file can reach.

Screenshot read, not just counted: positron basemap, region polygons, all
2107 pins, legend flush at the top, no gap and no collision.

22/22 bundles, 7/7 pre, 18/18 post, browser sweep of five StackLayout
consumers and all seven studio dashboards.
2026-09-24 03:04:11 +00:00
doxa-maps-88655c70
908181dea6 Fiord: the MapLibre dark that actually looks like the map it replaces
Applied after RENDERING the candidates side by side rather than reading a
gallery. OpenFreeMap ships a blue-toned dark style this registry never
used. At the band this product shows (30E 25N, z2.4), dark reads flat and
muddy -- near-black land against near-black water -- while fiord separates
land from sea and keeps labels legible in BOTH Latin and native script.
Endpoint verified 200 keyless first, the same bar every URL here must meet.
Note OpenFreeMap's documented '3d' style is NOT live at /styles/3d (404).

It took the three dark entries whose MAPBOX counterpart is also blue-ish:
midnight-glow (Standard@night), dusk (the entry literally says "blue
hour"), wayfinder-night (navigation-night is a blue palette). ink pairs
with near-black dark-v11 and its whole job is maximum pin contrast, so a
lighter, bluer ground would eat exactly what it exists to provide -- it
keeps dark, with a note saying so.

AND THE FALLBACK HAD TO LEARN THE THEME, which the swap alone would not
have fixed. A Standard-based profile names no registry style at all:
bootStyle() hands back the raw mapbox:// URL and the DARKNESS lives in the
config ({ basemap: { lightPreset: 'night' } }), a Mapbox-only mechanism.
maplibreStyle() collapsed every mapbox:// style to one LIGHT constant, so
flipping provider turned the dark research map into a light one -- found by
screenshotting it, not by any error: both engines mount, both paint, both
report success. Interoperability had "worked" while the product looked like
a different product. It now reads the preset the Mapbox side would have
applied, from the named entry or the boot config, so both engines tell the
same story.

VERIFIED: research map under ?provider=maplibre renders dark with the ember
dots intact and correct attribution; research dashboard under maplibre
renders the globe with all 2107 dots, greyscale choropleth, charts and
table. CAP-4 interop harness 4/4 combinations PASS. check:facet-changes-map
PASSES on mapbox (2107 -> 783 -> 2107 -> 1324, colour data-driven).
22/22 bundles, 7/7 pre, 18/18 post, browser sweep of five StackLayout
consumers and all seven studio dashboards; mapbox/maplibre search-bar and
legend geometry unchanged (56/104, NONE/58, 56/104).

NOT shipped, and filed instead: check:facet-changes-map cannot measure the
MapLibre engine at all -- it reads a private field that no MapLibre source
keeps live. Two fix attempts made it worse (one failed BOTH engines, one
called a node-side helper from page context) and were reverted; the gate is
back at HEAD and passing. The real fix re-expresses it on the public
querySourceFeatures(), which changes its reported counts and is a redesign,
not a patch.
2026-09-24 02:26:14 +00:00
doxa-maps-88655c70
b93b4cb984 One prop swaps the engine for every map, not just three
Some checks are pending
build-gate / build (push) Waiting to run
The switch existed and did not reach. resolveMapProvider has long read
profile-config.provider -> ?provider= -> window.MAP_PROVIDER, but
useMapInstance defaulted `provider` to the LITERAL 'mapbox', so the page
switch only reached a map whose bundle happened to pass provider through.
MEASURED: 3 of the 11 bundles that mount a map. The other 8 pinned Mapbox
whatever the page asked — it LOOKED global and was not.

The factory now resolves it when a caller leaves it unset, which moves the
decision to the one place every map already goes through. An explicit
caller still wins (prayer-walk-planner pins maplibre deliberately), and
with no switch set resolveMapProvider returns 'mapbox', so the default is
unchanged.

DashboardMapPanel collapsed anything that was not the literal 'maplibre' to
'mapbox', which outranked the page switch. But dashboard.config.js pins
Mapbox DELIBERATELY — the Driver's ruling for Mapbox Standard's native
starfield globe — so that default had to survive. Both hold by putting the
page switch BETWEEN the per-element config and the bundle default:
  1. profile-config.provider   this embed
  2. ?provider= / MAP_PROVIDER the page — "swap every map on this site"
  3. dashboard.config.js       this bundle's shipped default (Mapbox)

MEASURED across seven map leaves, default vs ?provider=maplibre, counting
requests to api.mapbox.com:
  doxa-simple-map          21 -> 4    engine swaps
  country-posters          24 -> 4    engine swaps
  research-dashboard       25 -> 4    engine swaps (was pinned)
  doxa-research-map        16 -> 4    engine swaps
  research-map-plus-grid   16 -> 0    fully keyless
  my-upg-100-list          19 -> 19   NOT swapped — see backlog
  regions-dashboard         4 ->  4   no map on that leaf

Simple Research reaches ZERO Mapbox requests, which is the case that
matters most: it is the one carrying the search bar, and the geocoder is
the quota risk (GeocoderComponent's maplibre path still geocodes through
Mapbox, so a keyless map with a search bar is not keyless).

Two gaps left, both filed rather than hidden: my-upg-100-list bypasses
useMapInstance entirely and cannot see the switch, and generated leaves
still preload mapbox-gl.js/.css + the geocoder from api.mapbox.com even
under maplibre (library downloads, no token, but still a CDN dependency —
the composite leaf proves 0 is reachable). The second belongs to the tree
verb, which is not mine to change.

22/22 bundles, 7/7 pre, 18/18 post, browser sweep of five StackLayout
consumers and all seven studio dashboards.
2026-09-24 01:34:03 +00:00
doxa-maps-88655c70
e315487e2b Reclaim the fold: dashboard 849 -> 623, and the people are visible
Some checks are pending
build-gate / build (push) Waiting to run
Driver: "it is a priority to reclaim height... so I can see half of the
first row of the people groups grid." Measured after: shell bottom 623,
first card top 640, half the card inside a 900px viewport -- the faces show.

THE HEIGHT: map 360 -> 260 and shell follows the same row arithmetic
(260 + 32 + 7x26 + 28 = 502); headers slim again (topbar 44 -> 38, stats
26 -> 22). Seven data rows unchanged.

THE ZOOM: a shorter map now shows MORE world, not less. Default zoom
1.45 -> 1.1 opens on the whole sphere, minZoom 1 -> 0.5, and the mercator
"no duplicate world" floor is skipped on the globe -- a sphere has no
second world, so the floor only stole zoom-out range.

TWO MORE SECOND-OWNER BUGS FOUND ON THE WAY, both now dead:
- research-dashboard.vue passed its own defaults {mapH:360, shellH:720} to
  useDashboardLayout, silently overriding the composable's DEFAULTS -- the
  bundle carried BOTH and the stale copy won, so two reclaim passes changed
  nothing. The override is deleted; one owner.
- one grid-template-rows fallback spelled 40svh WITHOUT a space and
  survived the fallback sweep -- at 900px that IS 360, re-pinning the row.

ALSO: the chart count column is now a real GRID column (label 1fr, count
auto, rows joined via subgrid), because per-row flex put each count at its
own row's right edge the moment the legend scrolled -- "less like a column,
more like cards". Measured: one shared right edge before and after scroll.
2026-09-23 21:31:05 +00:00
doxa-maps-88655c70
d9fbff14bb Galaxy style on MapLibre, and restore both dashboard resizes
Some checks are pending
build-gate / build (push) Waiting to run
GALAXY + MAPLIBRE. New 'galaxy' style in the shared registry -- deep-space
ground, blue oceans -- with both a mapbox and a keyless MapLibre URL, and a
paint override that is applied only if the loaded style actually has the
layer, so a miss is a silent no-op rather than a break. The dashboard now
runs provider 'maplibre' and mapStyle 'galaxy'. Reversible by two config
keys; the Mapbox path is untouched.

BOTH RESIZES RESTORED. My seven-row change had hard-capped the table's
keyhole at 7 x 26px, which read like a default and was not: nothing writes
--rd-table-rows, while the drag handles write --rd-shell-h and --rd-map-h.
So the cap was unreachable by any gesture and the Driver lost both resizes
at once, plus a grey gap where the freed pixels had no consumer.

Fixed by restoring the derivation and moving the default instead: the
keyhole is once again shell - map - bar - foot, and DEFAULTS.shellH drops
720 -> 602 (map 360 + bar 32 + 7 rows x 26 + foot 28) so the table OPENS at
seven rows while staying fully draggable. CSS fallback matched to 602.

Measured before the revert: shell handle 849 -> 731 with rows 7, table
divider map 360 -> 280 with rows 10, gap 1px after each.

Also carries the charts' frozen count column (sticky right:0 with an opaque
background, so a scrolled label cannot slide under the number) and the
ViewSelect library-index entry.
2026-09-23 20:35:38 +00:00
doxa-maps
7f08d09edc Remove the tile-403 on-screen banner; connect dashboard selections to the grid
Some checks are pending
build-gate / build (push) Waiting to run
Two Driver-reported defects.

1. No more "Basemap tiles blocked" popup on the map. The banner reported an
   operator problem (one Mapbox account setting on one deployment origin) to
   the end user, who can do nothing about it, on every map and every load. The
   console diagnosis stays, because it reaches the person who can act.

2. Selections in the dashboard now filter the grid beneath it. The two halves
   were never connected: research-dashboard had no defineEmits and its bus is a
   per-instance factory that is never provided, while dashboard-plus-grid passed
   the grid no filter props. The grid's receiving end already worked. Added the
   emit, plus a translation layer between the dashboard's snake_case facet keys
   and the grid's camelCase props, mirroring big-picture.

   Proven live: typing "Turk" takes the grid from 24 cards to 22 with wholly
   different rows. Only 4 of 12 facets map; the rest are dropped rather than
   guessed, because religion_family carries a family name while the grid's
   religion prop matches a code, and mapping them would filter to zero rows.
2026-09-22 01:43:56 +00:00
doxa-maps-88655c70
7ccdc37e8f show the tile-403 on screen, top-left, not only in the console
A console message only helps someone who already suspects a problem and opens
devtools. The reported experience was "I clicked the map and saw nothing", so
the page itself must say why.

The leaf's own #status node is not usable here: done() removes it the moment the
element mounts SUCCESSFULLY, which is exactly what happens in this failure -
mount succeeds, only the tiles 403 afterwards. So the composable creates its own
small banner instead.

A CORNER banner, never a full-cover overlay: the data layer, legend and labels
all render correctly and stay useful. Hiding a working map to report a missing
basemap would trade one blank screen for another.

TOP-LEFT was chosen by LOOKING at a screenshot, not by geometry. A first attempt
at bottom-left rendered underneath the legend panel and was unreadable, while
every numeric check still passed - 442x70px, correct palette, 3% coverage.
The other corners are taken: controls top-right, attribution bottom-right, hint
pill bottom-centre.

Material light only: #fff surface, #3b463d text, #d1d4cd border, #6d766e muted.
No red - this is a configuration notice, not a crash. pointer-events:none so it
can never block map interaction, and the container is only set position:relative
when it is still static, so a profile that positions its own container keeps it.
2026-09-22 00:02:50 +00:00
doxa-maps-88655c70
4c6d8571cf name the tile-403 failure instead of rendering a silent blank void
A URL-restricted Mapbox token returns 200 for the style document AND the fonts,
and 403 only for tilesets. So the element mounts at full height, the legend and
labels paint, the data layer paints - and the basemap silently never arrives.
It reads as a rendering bug in our code. It is not: it is one account setting.

Nothing listened for it. GL-JS fires an "error" event carrying e.status on every
failed tile request, so the information was always there - no one asked. That gap
cost real debugging hours this flight and is the first thing an inheritor
deploying to a NEW domain will hit.

The listener attaches after both engine branches converge, so it covers Mapbox
and MapLibre with one hook, and is wrapped in try/catch so an engine without an
.on() method can never break map creation.

ONE MESSAGE PER MAP, not per tile: a blank basemap 403s every tile in the
viewport, dozens per second while panning, and an unguarded console.error would
bury the page and train the reader to ignore it.

Verified both directions: a restricted map produced 9 tile 403s and exactly 1
diagnostic; the token-free maplibre profile mounted 800px and emitted 0.
Full build ALL GUARDS PASS, SRI consistent.
2026-09-21 23:44:11 +00:00
doxa-maps-88655c70
85014aad11 Session work: strobe fix, grid layout, stacked tree legend, selection camera, chip bounding, asset inheritance, gates 2026-09-20 04:49:25 +00:00
swift-river542
db6aabe99f Phones: Mapbox logo and attribution both bottom-right so the detail sheet's image stays clear 2026-09-16 21:26:48 -05:00
swift-river542
1b22fa419b Basemap follows the tab: prayer stays night, every other map is day in monochrome white
- useMapTheme: one look rule (any prayer strategy → night/default; everything else →
  day/monochrome) applied at boot and on every tab switch in the simple map and the
  research map; an unknown strategy keeps the current look
- Legends and the research search pill are 380px again; desktop numeric columns 70px at
  13px so every row stays on one line with no overlap
- Map containers carry their map instance for gates (no global)
2026-09-16 20:50:51 -05:00
swift-river542
8aa3848bdc Prayer legend: bar only in the header (no numbers); research map search pill matches the legend width; Mapbox attribution on every map 2026-09-16 20:16:04 -05:00
swift-river542
2e8b5f5932 Basemap never flashes light: the night preset travels in the Map constructor and is re-pinned after every style change
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
- useMapTheme: basemapConfig() is the single source of truth; bootConfig() hands Mapbox the
  Standard-style config at construction so the very first frame is already night; keepPinned
  re-asserts on style.load and styledata, writing only on drift; MapLibre maps are skipped
- useMapInstance passes that config to new Map() and stores the map in a shallowRef so
  Mapbox internals never run through Vue's reactive proxy
- Simple map's loading cover is dark like the research map's — the last light frame at boot
- New gate: internal/theme-flash-probe.mjs samples canvas luminance through boot, legend rows
  and tabs and fails on any light frame (21 phases, 0 flashes)
2026-09-16 15:08:30 -05:00
doxa-maps
9c521f11d5 Public builds carry zero platform surface + copy-embed on staging
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
- Compile-time gate __DOXA_QA__ (default off): the QA corner and every string
  it carries are dead-code-eliminated from public bundles; platform builds
  opt in with DOXA_QA=1. Shell QA becomes ?qa=1 opt-in. Audit: zero platform
  strings across all bundles + shell.
- One Mapbox wordmark per page (first live map claims it; released on destroy).
- Coach-mark points at the need: 'Tap a red dot — someone still needs prayer.'
- Pray-click glow: locally-prayed groups render as Has Prayer for that visitor
  (localStorage, 7-day TTL) — lasting glow comes from the real signup via the API.
- Staging forge gains 'copy embed': snippet built at click time from
  location.href, so it carries the FULL URL of whatever host serves the tree.
2026-09-16 05:13:44 +00:00
doxa-maps
d5ad3aab0b utm_source inherits from the embed URL (the question-mark mechanism)
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
The page URL's ?utm_source= now outranks config/global/default in every outbound
people-group link — an iframe embedded as <page>?utm_source=agwm, a partner page
carrying its own ?utm_source, or a host page like /embed/map/?utm_source=agwm
all attribute their traffic automatically. Applied in the shared popup and both
dashboard drawer links.
2026-09-16 04:04:35 +00:00
doxa-maps
c7260faaa3 feat: map provider swap — mapbox|maplibre engine + hybrid geocoder + provider badge; staging title doxa-maps 2026-09-11 21:47:25 +00:00
doxa-maps
d5ba21fa76 doxa-maps — portable bundler for embeddable JAMstack maps
Standalone repo that builds embeddable JAMstack map bundles (web components),
composes them into a lazy-loaded SPA via layouts/, and optionally wraps a
Capacitor Android/iOS app — one build, choose your output/CDN. Diátaxis docs.

History reset to a single clean commit; no PII, IPs, or internal terms.
2026-09-10 01:53:07 +00:00