- createGuidanceSession + stepGuidance: the thresholds+grace reroute machine (idle/on_route/off_route/rerouting/resuming/no_reroute) with accuracy-aware threshold max(35, 2xacc) capped 100, 3-consecutive-fixes + grace gates, missed-turn shortened grace (3s), storm guard with off_route re-request after expiry, honest no_reroute degradation - projectAlongRoute: forward-only cos(lat)-scaled projection, cumulative remaining (D-07), haversineM deviation (one distance authority) - once-per-edge D-08 effect stream: approach 300 m / maneuver impact / arrive + arrival at 25 m (D-14); reroute-applied re-arms, measures against the NEW model only (Pitfall 6) - replay suite R1-R12 green: corridor/excursion/missed-turn/jitter drives, storm guard mechanics, self-heal, structural purity gate; 88/88 web tests
@araharah/web
The arah² Telegram Mini App — a MapLibre GL navigation shell built with Vite and TypeScript. It owns the three-source location acquisition chain, an in-memory-only session store, the Telegram WebApp integrations (theme slots, BackButton, safe-area insets), and the Rams chrome (status strip, boot/denied/signal-lost cards, recenter plate).
Part of the arah² monorepo (workspaces: theme, bot, web). Private package — not published to a registry.
Install
Installed through the monorepo root (npm workspaces), not standalone:
# from the repository root
npm install
@araharah/theme is a workspace dependency ("@araharah/theme": "*"), so the theme package must be built before this one:
npm run build -w theme # emits theme/dist/rams.css (only)
npm run basemap -w theme # separately emits theme/basemap/liberty-rams.json
Quick start
npm run web:dev # vite dev server on :5173 (strictPort)
With no Telegram client present, src/mock/telegram-mock.ts installs a fake window.Telegram.WebApp automatically in dev (import.meta.env.DEV), so the shell boots to LIVE in a plain browser with the KL default fix. Without it, the app renders its denied card — still no blank screen.
Production build (what the Worker serves):
npm run web:build # theme build + vite build -> web/dist
Scripts
Run from the package directory, or from the root with npm run <script> -w web.
| Command | Description |
|---|---|
npm run dev |
Vite dev server on port 5173 (strictPort: true, vite.config.ts). |
npm run build |
Production build to dist/ — the harness mock and fetch stubs are dead-code-eliminated. |
npm run build:harness |
vite build --mode harness to dist-harness/ — __ARAHARAH_HARNESS__ is true, so the mock installer and runner-page stubs are compiled in. Never deployed. |
npm test |
Zero-dependency runner (node test/runner.mjs) that auto-discovers test/*.test.mjs. |
npm run typecheck |
tsc --noEmit over src (strict, noUncheckedIndexedAccess, verbatimModuleSyntax). |
npm run capture |
Playwright harness (scripts/capture.mjs): serves dist-harness/, drives every boot state, writes screenshots/, and runs the runtime storage probe. |
dist/ and dist-harness/ must never share an outDir — the two builds would clobber each other and void the production-artifact gate.
Module map
src/
├── main.ts boot choreography — order is load-bearing
├── shell.css the Rams chrome; imports @araharah/theme/dist/rams.css
├── vite-env.d.ts ambient __ARAHARAH_HARNESS__ declaration
├── telegram/
│ ├── webapp.ts the one typed accessor for window.Telegram.WebApp (TelegramWebAppLike)
│ ├── theme.ts Telegram theme signal -> [data-tg-theme] slot switch
│ ├── safe-area.ts env(safe-area-inset-*) ∪ WebApp.safeAreaInset -> CSS custom properties
│ └── backbutton.ts BackButton total wire (root press -> WebApp.close())
├── session/
│ ├── client.ts POST /api/session with the verbatim initData string
│ └── store.ts memory-only token holder, 900s TTL — no storage API anywhere
├── location/
│ ├── chain.ts acquireFix(): the locked telegram -> webview -> chat-pin chain
│ ├── telegram.ts LocationManager wrapper + Android stale-fix nudge
│ ├── webview.ts navigator.geolocation (maximumAge: 0)
│ ├── chat-pin.ts bounded poll of /api/location + ASK VIA CHAT trigger
│ └── types.ts Fix, FixSource, LocationState, KL_CENTER
├── map/
│ ├── map.ts MapLibre v6 bootstrap; returns null when WebGL2 is absent
│ ├── follow.ts following/free camera controller
│ └── marker.ts position marker + accuracy circle + heading cone
├── ui/
│ ├── strip.ts status strip (validating/acquiring/live/lost/denied/awaiting)
│ ├── cards.ts boot / denied / signal-lost cards + gantry blink
│ └── recenter.ts follow/recenter plate
├── i18n/strings.ts every user-facing string, en + ms (the copy gate)
└── mock/telegram-mock.ts Mock-Telegram harness — excluded from dist by BA5
graph TD
MAIN["src/main.ts — boot choreography"]
WA["src/telegram/webapp.ts"]
TH["src/telegram/theme.ts"]
SA["src/telegram/safe-area.ts"]
BB["src/telegram/backbutton.ts"]
SESS["src/session/client.ts"]
STORE["src/session/store.ts"]
CHAIN["src/location/chain.ts"]
TG["src/location/telegram.ts"]
WV["src/location/webview.ts"]
PIN["src/location/chat-pin.ts"]
TYPES["src/location/types.ts"]
MAP["src/map/map.ts"]
FOLLOW["src/map/follow.ts"]
MARK["src/map/marker.ts"]
STRIP["src/ui/strip.ts"]
CARDS["src/ui/cards.ts"]
REC["src/ui/recenter.ts"]
STR["src/i18n/strings.ts"]
MOCK["src/mock/telegram-mock.ts"]
MAIN --> WA
MAIN --> TH
MAIN --> SA
MAIN --> BB
MAIN --> SESS
MAIN --> CHAIN
MAIN --> MAP
MAIN --> STRIP
MAIN --> CARDS
MAIN --> REC
MAIN --> MOCK
SESS --> STORE
CHAIN --> TG
CHAIN --> WV
CHAIN --> PIN
CHAIN --> TYPES
TG --> TYPES
WV --> TYPES
PIN --> TYPES
MAP --> FOLLOW
MAP --> MARK
FOLLOW --> TYPES
MARK --> TYPES
STRIP --> TYPES
STRIP --> STR
CARDS --> STR
REC --> STR
Usage examples
Acquire a fix through the chain (this is exactly what main.ts does):
import { acquireFix, isFixStale, fixAgeSeconds } from "./location/chain.ts";
import { createSession, } from "./session/client.ts";
import { getWebApp } from "./telegram/webapp.ts";
import { getSession } from "./session/store.ts";
const wa = getWebApp(); // null off-Telegram — the chain handles null
const session = await createSession(wa?.initData ?? "");
if (!session.ok) throw new Error(session.reason); // "malformed" | "tampered" | "stale" | "network"
const { fix, state } = await acquireFix({ wa, getToken: getSession }, null);
if (fix !== null && !isFixStale(fix, Date.now())) {
console.log(fix.source, fixAgeSeconds(fix, Date.now()), "s");
} else {
console.log(state.kind); // "denied" | "awaiting-pin" | ...
}
Boot the map on its own (returns null rather than throwing on a WebGL2-less device):
import { bootMap } from "./map/map.ts";
import { createPositionMarker } from "./map/marker.ts";
const map = bootMap(document.getElementById("map")!);
if (map !== null) {
const marker = createPositionMarker();
marker.marker.addTo(map);
marker.update(map, fix);
}
Testing
npm run web:test # or: npm test (inside web/)
npm run web:typecheck
test/runner.mjs auto-discovers every test/*.test.mjs suite and drains the shared registry from test/harness.mjs. Suites cover the location chain (location-chain.test.mjs), the memory-only session store (session.test.mjs), theme slots (theme.test.mjs), the en/ms copy gates (copy-gate.test.mjs), the no-storage invariant (no-storage.test.mjs), and the production build artifact (build-artifact.test.mjs — CDN references, self-hosted fonts, hashed MapLibre worker, inlined Rams style, and BA5/BA6 asserting no mock installer or fetch stub ships in dist/).
The Playwright capture harness is a separate command (npm run web:capture): it serves dist-harness/, waits on the RAMS-BASEMAP-READY and ARAHARAH-LIVE console signals, and writes the state gallery into screenshots/.
Related docs
- Root README.md — monorepo overview and system diagram
- docs/ARCHITECTURE.md — component responsibilities across
theme/bot/web - docs/DEVELOPMENT.md — local setup, build, and PR workflow
- docs/TESTING.md — the cross-package test strategy
- DESIGN.md and
theme/— the Telemetry Rams design system this shell consumes