14 KiB
phase: 03-mini-app-shell-geolocation plan: 03 subsystem: web-shell-chrome epoch_start: 2026-10-22 tags: [d-06, d-08, d-09, d-10, px-02, maplibre-v6, copy-gate, rams-shell] requires:
- "03-01 web/ workspace (vite build, strict tsconfig, /api half routes)"
- "03-02 location chain (acquireFix/isFixStale/fixAgeSeconds), session store+client, telegram wrappers, D-11 mock"
- "theme/dist/rams.css tokens + component classes; theme/basemap/liberty-rams.json" provides:
- "web/src/i18n/strings.ts — 25-key ShellStrings copy module, UI-SPEC table verbatim (en+ms), pickLocale + localeFromInitData"
- "web/src/shell.css — token-only Rams chrome (dark-slot activation, strip/card/control/marker geometry, data-map=absent fallback)"
- "web/src/map/{map,marker,follow}.ts — MapLibre v6 bootstrap (setWorkerUrl, GPUInitializationError gate, RAMS-BASEMAP-READY), DOM position marker (dot + capped accuracy circle + null-hidden cone), 96px/400ms follow controller"
- "web/src/ui/{strip,cards,recenter}.ts — honest state strip, boot/denied/lost cards, machined recenter control"
- "web/src/main.ts — boot choreography: map mounts FIRST, then session, then chain; staged validating→acquiring→live"
- "vite.config.ts define ARAHARAH_HARNESS (mode-keyed) — the build constant 03-04's dual build keys on"
- "gates: COPY1-6 copy gate, SESS1-4 session gates (27-case suite total)" affects:
- "plan 03-04 (capture harness drives main.ts's mount ordering, mock-fallback guard, ARAHARAH-LIVE race signal)"
- "Phase 5 (route snapping consumes Fix + map bootstrap); Phase 6 (tick re-runs chain rather than continuous cadence)"
tech-stack:
added:
- "maplibre-gl v6 (ESM-only, WebGL2-only) via Vite ?worker&url + setWorkerUrl"
- "build-time JSON style inlining (@araharah/theme/basemap/liberty-rams.json) with resolveJsonModule" patterns:
- "Map-first boot order as the anti-blank-map invariant (order asserted by script)"
- "Null-guarded map consumers — WebGL2-absent shell renders deliberate absent-map state, never crashes"
- "Machine-blink duty cycle via setInterval; zero CSS transition/animation in chrome"
- "Named-CTA data slots in card markup for element-specific class-mapping gates" key-files: created:
- web/src/i18n/strings.ts
- web/src/shell.css
- web/src/map/map.ts
- web/src/map/marker.ts
- web/src/map/follow.ts
- web/src/ui/strip.ts
- web/src/ui/cards.ts
- web/src/ui/recenter.ts
- web/test/copy-gate.test.mjs
- web/test/session.test.mjs modified:
- web/src/main.ts (stub → composition root)
- web/src/map/map.ts (attribution bottom-left)
- web/src/map/marker.ts (comment reword only)
- web/src/shell.css (attrib chip scope, strip cluster, recenter z-index)
- web/vite.config.ts (define harness constant)
- .planning/phases/03-mini-app-shell-geolocation/03-UI-SPEC.md (Open-Question-1 amendment append) decisions:
- "localeFromInitData parses the RAW initData locally — POST /api/session returns only {token}, the claim's language_code is unreachable client-side; worst case of a forged locale is wrong-language copy (HMAC validation stays server-side)"
- "Mock install is a guarded fallback ((ARAHARAH_HARNESS || DEV) && !window.ARAHARAH_MOCK) — plan 03-04's profile-configured runner page must never be clobbered by the default android/light fake"
- "vite define keyed on --mode harness supplies the ambient constant as a real static value — a bare declared-but-undefined global would ReferenceError in plain vite dev (Rule 3)"
- "Attribution control moved to bottom-left via addControl (UI-SPEC geometry) — bottom-right was covering the recenter control; chip restyled with #map-scoped specificity because maplibre css lands after shell.css in the bundle"
- "COPY5 scans comment-stripped sources: prose may cite banned register labels, code may not"
- "CTA buttons ship named placeholder slots ({strings.ctaTryAgain}) overwritten with strings-module text before mount — keeps the Pitfall-4 class inversion element-greppable without inline copy"
- "Signal-lost card folds bottom-register via inline style on the shared card-stack (token-referenced bottom offset), not a second stylesheet layer" metrics: duration: ~55 min wall (15:49 → Oct 23 ~00:05 UTC, spans a date rollover; ~45 min focused) completed: 2026-10-23T00:05:00Z ...
Phase 3 Plan 03: Rams shell — copy, chrome, MapLibre bootstrap, boot choreography Summary
One-liner: the visible Rams Mini App shell — verbatim en+ms copy module with a 6-case copy gate, token-only stylesheet, MapLibre v6 basemap with WebGL2-absent fallback, dot/cone/accuracy marker, machined recenter control, six-state honest strip, denied card with the Desktop-first CTA inversion, and the map-first staged boot that never shows a blank screen.
Tasks
| task | Name | Commit | Files |
|---|---|---|---|
| 1 | i18n/strings.ts copy module (25 keys, en+ms) + COPY1-6 gate |
83985c4 |
web/src/i18n/strings.ts, web/test/copy-gate.test.mjs |
| 2 | shell.css + map bootstrap / marker / follow |
c437d31 |
web/src/shell.css, web/src/map/{map,marker,follow}.ts (+ UI-SPEC amendment) |
| 3 | strip, cards, recenter, boot choreography + SESS gate | d573acc |
web/src/ui/{strip,cards,recenter}.ts, web/src/main.ts, web/test/session.test.mjs, (task-3 wiring edits: map.ts, shell.css, marker.ts, vite.config.ts) |
Verification Results
npm test -w web→[runner] 27 passed, 0 failed(10 GEO + 3 GEO3 + 4 PX02 + 6 COPY + 4 SESS; ≥24 required)npm run typecheck -w web→ clean after every task;npm run build -w web→ clean, 3 assets (js + css + worker) + hashed woff2 fonts in dist/fonts- Boot-order script:
bootMap(precedesacquireFix(in main.ts — the D-09 anti-blank invariant is greppable - Null-map guard script: 2×
if (map !== null),follow = createFollowController(mapmapped,follow?.followguarded;ARAHARAH-LIVEemitted OUTSIDE the guard (comment-stripped grep = 1) - Marker wiring script:
.addTo(map)inside the first guard and before the firstmarker.update(map, fix); marker DOM script: explicit svg w/h/viewBox, cx/cy on both circles, cone/dot under the svg, no body-parenting - Copy gates: 25-key en/ms parity (deniedRegister + lostRegister included), COPY1-6 all PASS`; COPY5 element-level no-inline-copy incl. the GEO/SYS register labels
- CTA class script: chat-fallback button element carries
.btn-primary, retry carries.btn-secondary(Pitfall-4 inversion) — element-specific match, not file-level - Theme audit:
npm run audit -w theme→ AUDIT CLEAN (354 declarations, 0 off-token values; also clean with web/index.html added via --pages) - shell.css value discipline: 0 literal hex, 0
transition:/animation:,border-radius: 0present; ui/*.ts greps 0 for transition/animation words - Strip telemetry uses the U+00B1
±glyph;+-<accuracyM> Mabsent - Third notch state shipped:
data-fix="lost"in shell.css +recenter.setFixState("lost")in main.ts +setFixStatein recenter.ts - UI-SPEC amendments: Pitfall-4 callout pre-present (verified, not duplicated); Open-Question-1 sentence appended exactly once under Boot Choreography
- Real-browser smoke (headless Playwright, vite dev): map tiles render, strip lamp+word clustered left, D-06 denied card visible with inverted CTA classes, recenter visible and top-most at bottom-right, attribution chip bottom-left, webfonts loaded (dist/fonts hashed copies)
Threat model compliance
| Threat | Where implemented | Evidence |
|---|---|---|
| T-03-13 (style JSON swapped at runtime) | build-time ramsStyle import; no map.setStyle anywhere in web/src |
source grep; PX02 case 3 still green |
| T-03-14 (Telegram hexes as design values) | shell.css re-points --shell-* to Rams tokens only; zero hex in code lines |
hex grep = 0 (comment-stripped) |
| T-03-16 (stale fix rendered fresh) | tick checks isFixStale each second → marker data-stale, red lamp, SIGNAL LOST + LAST FIX |
main.ts tick + criteria greps |
| T-03-17 (worker chunk) | setWorkerUrl(workerUrl) from Vite-emitted hashed asset; error listener logs RAMS-BASEMAP-ERROR |
map.ts; worker emitted as dist asset |
| T-03-18 (WebGL2 absent → blank) | bootMap catches GPUInitializationError → null; data-map="absent"; every map consumer guarded; capture signal unguarded-handled | null-map guard script; marker wiring script |
| T-03-19 (Fix escapes closure) | Fix lives in boot()'s closure, passed by argument | D-07 lock in main.ts; GEO3 gate still green |
Deviations from Plan
Auto-fixed Issues
1. [Rule 3 - Blocking] vite.config.ts gained define: __ARAHARAH_HARNESS__ (mode-keyed)
- Found during: task 3 (typecheck/dev wiring)
- Issue: the plan mandates the literal condition
(__ARAHARAH_HARNESS__ || import.meta.env.DEV) && !window.__ARAHARAH_MOCK__; with only an ambientdeclare const, plainvite devwould ReferenceError on the unreplaced global. - Fix:
define: { __ARAHARAH_HARNESS__: JSON.stringify(mode === "harness") }— true exactly in 03-04's harness builds, statically false elsewhere. Consistent with 03-04 task 2's dual-build intent, not a fork of it. - Commit:
d573acc
2. [Rule 3 - Blocking] ramsStyle needs a typed cast + error listener listener-shape fix
- Found during: task 2 (typecheck)
- Issue: the plan asserted
resolveJsonModulealone suffices; in practice TS2732-adjacent narrowing failed: the JSON infersversion: numbervs the style spec'sversion: 8literal (TS2322), and the v6errorevent'sErrorEvent & Objectshape rejects{ error?: Error }(TS2769). - Fix:
style: ramsStyle as StyleSpecification(typed cast — resolveJsonModule import kept verbatim, noany, no shim, no synthetic defaults) and the inner error read as{ error?: { message?: string } }. Behavior unchanged; RAMS-BASEMAP-ERROR/READY prefixes verbatim. - Commit:
c437d31
3. [Rule 1 - Bug] Attribution sat bottom-right and covered the recenter control; chip rendered MapLibre-white
- Found during: task 3 (real-browser render inspection)
- Issue: default attribution position conflicts with the floating control; and because maplibre's css lands after shell.css in the bundle, the chip style (same specificity) lost the cascade — translucent white chip (banned).
- Fix:
attributionControl: false+addControl(new AttributionControl({ compact: true }), "bottom-left")(UI-SPEC says flush bottom-left) and#map-scoped chip overrides. - Commit:
d573acc
4. [Rule 2 - Critical] ARAHARAH-LIVE console signal was missing from the implementation
- Found during: task 3 (acceptance script)
- Issue: the comment referenced it but the emit did not exist — a WebGL2-absent device would hang the 03-04 capture race.
- Fix:
console.log("ARAHARAH-LIVE fix=… acc=…")in handleLive, outside the map guard. - Commit:
d573acc
5. [Rule 3 - Blocking] COPY5 resilient to execution order
- Found during: task 1 (first test run)
- Issue: COPY5 hard-requires
web/src/ui/to exist; task 1 precedes task 3, so the suite failed ENOENT mid-plan. - Fix: scan
[main.ts]always +ui/*.tswhen present; at plan completion the full tree (4 files) is scanned, restoring the full gate strength. - Commit:
83985c4
6. [Rule 1 - Bug] Comments tripped several greps (grep does not strip comments)
- Found during: tasks 2-3 (acceptance runs)
- Issue: marker.ts's prose cited
document.body.appendChild(group), cards.ts cited the banned register-label literals and CTA words, strip/recenter comments said "transition"/"animation". - Fix: reworded comments (e.g. "named-CTA data slots", "the chat-fallback CTA", "the retry CTA", "instant hard swaps"); behavioral gates unchanged (COPY5 still comment-strips correctly).
- Commits:
c437d31,d573acc
Plan-text variances (documented, non-fixes)
pickLocaleis NOT imported by main.ts (plan's import list had it): BOOT's rule "no second locale helper" holds — main.ts uses onlylocaleFromInitData, whose body calls pickLocale inside strings.ts. Unused import avoided.- The two card CTA
<button>elements carry named-string-module placeholder slots briefly overwritten before mount — this keeps the acceptance script's element-specific class assertions truthful without inlining copy (COPY5 does not ban slot identifiers). session-staterender: denied-then-waiting is a secondbuildDeniedCard(locale, { awaitingPin: true })instance — visually identical per UI-SPEC's "body swaps" semantics.
Known Stubs
- The card checkout slot
{strings.ctaTryAgain}/{strings.ctaAskViaChat}innerHTML placeholders are never rendered to users (textContent overwritten pre-mount) — not a data-flow stub. installMockWebApp()default profile (telegramFix null) — capture profiles are plan 03-04's runner's job; the fallback installs only when no profile mock is present.- Boot stages validating/acquiring cards render code-complete but the dev-server smoke could only photograph the denied terminus (no /api/session in vite dev); full staged-state capture is 03-04's harness purpose.
Threat Flags
None — no new network endpoints (tiles were the one pre-declared runtime fetch), no auth-path changes, no schema changes. The /api/location-fallback call is the pre-existing 03-01 surface consumed as specced.
Auth Gates
None — no live Telegram, no secrets; dev-signed mocks and real runs were both untouched.
Self-Check: PASSED
- 10 created + 5 modified files present on disk (sweep live at write time); 3 asset + 6 font artifacts in web/dist
- 3 task commits verified in
git log --oneline(83985c4,c437d31,d573acc); zero file deletions across all three - Suite: 27 passed / 0 failed; typecheck + build clean; theme audit clean
- STATE.md and ROADMAP.md untouched by this executor (ROADMAP.md pre-existing modification belongs to the orchestrator)