This commit is contained in:
araharah-executor
2026-10-23 19:18:43 +08:00
parent 1890b5b1ca
commit af3edefbc6
9 changed files with 3027 additions and 2 deletions
+207
View File
@@ -0,0 +1,207 @@
<!-- generated-by: gsd-doc-writer -->
# @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²](../README.md) monorepo (workspaces: `theme`, `bot`, `web`). Private package — not published to a registry.
<!-- VERIFY: the deploy step mounts web/dist/ via the bot worker's ASSETS binding (see ../bot wrangler config) -->
---
## Install
Installed through the monorepo root (npm workspaces), not standalone:
```bash
# from the repository root
npm install
```
`@araharah/theme` is a workspace dependency (`"@araharah/theme": "*"`), so the theme package must be built before this one:
```bash
npm run build -w theme # emits theme/dist/rams.css + the basemap style
```
---
## Quick start
```bash
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):
```bash
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
```
```mermaid
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):
```ts
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):
```ts
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
```bash
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](../README.md) — monorepo overview and system diagram
- [docs/ARCHITECTURE.md](../docs/ARCHITECTURE.md) — component responsibilities across `theme` / `bot` / `web`
- [docs/DEVELOPMENT.md](../docs/DEVELOPMENT.md) — local setup, build, and PR workflow
- [docs/TESTING.md](../docs/TESTING.md) — the cross-package test strategy
- [DESIGN.md](../DESIGN.md) and `theme/` — the Telemetry Rams design system this shell consumes