--- 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(` precedes `acquireFix(` in main.ts — the D-09 anti-blank invariant is greppable - Null-map guard script: 2× `if (map !== null)`, `follow = createFollowController(map` mapped, `follow?.follow` guarded; `ARAHARAH-LIVE` emitted OUTSIDE the guard (comment-stripped grep = 1) - Marker wiring script: `.addTo(map)` inside the first guard and before the first `marker.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: 0` present; ui/*.ts greps 0 for transition/animation words - Strip telemetry uses the U+00B1 `±` glyph; `+- M` absent - Third notch state shipped: `data-fix="lost"` in shell.css + `recenter.setFixState("lost")` in main.ts + `setFixState` in 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 ambient `declare const`, plain `vite dev` would 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 `resolveJsonModule` alone suffices; in practice TS2732-adjacent narrowing failed: the JSON infers `version: number` vs the style spec's `version: 8` literal (TS2322), and the v6 `error` event's `ErrorEvent & Object` shape rejects `{ error?: Error }` (TS2769). - **Fix:** `style: ramsStyle as StyleSpecification` (typed cast — resolveJsonModule import kept verbatim, no `any`, 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/*.ts` when 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) - `pickLocale` is NOT imported by main.ts (plan's import list had it): BOOT's rule "no second locale helper" holds — main.ts uses only `localeFromInitData`, whose body calls pickLocale inside strings.ts. Unused import avoided. - The two card CTA `