forked from c-fu/arah2
- New: docs/ARCHITECTURE.md, docs/GETTING-STARTED.md, docs/DEVELOPMENT.md, docs/TESTING.md, docs/CONFIGURATION.md (colored mermaid diagrams, quoted labels) - Regenerated README.md (arah² / arah2bot naming, canonical clone URL) - New per-package READMEs: theme/, bot/, web/ - Verified 13 docs against the live codebase (2 fix iterations + 2 residual corrections); corrected 56 stale claims across AGENTS/DESIGN/IDEAS/GSD_FIX
208 lines
8.3 KiB
Markdown
208 lines
8.3 KiB
Markdown
<!-- 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 (only)
|
||
npm run basemap -w theme # separately emits theme/basemap/liberty-rams.json
|
||
```
|
||
|
||
---
|
||
|
||
## 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 |