Files
arah2/docs/GETTING-STARTED.md
T

428 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- generated-by: gsd-doc-writer -->
# Getting Started
This guide takes you from an empty machine to a locally running arah² — the Telegram
bot (**arah2bot**), its Cloudflare Worker, and the Mini App webview — in five steps.
For what each piece does, see [ARCHITECTURE.md](ARCHITECTURE.md). For the full list of
environment variables, config files, and per-package scripts, see
[CONFIGURATION.md](CONFIGURATION.md). For a package overview, see the root
[README.md](../README.md). For what arah² honestly does **not** do yet, see
[KNOWN-LIMITATIONS.md](KNOWN-LIMITATIONS.md).
---
## Prerequisites
| Requirement | Why | Notes |
| --- | --- | --- |
| **Node.js + npm** | Every build, dev, test, and deploy script in the repo is an npm script; all five workspaces are ESM (`"type": "module"`) | No `engines` field, `.nvmrc`, or `.node-version` exists anywhere in the repo. Use a current Node LTS — the repo's development and all four test runners run on Node v24, and the search/routing corpus tooling relies on Node 24's native TypeScript type-stripping. |
| A **Telegram account** | You chat with arah2bot and receive the Mini App webview | — |
| A bot token from **[@BotFather](https://t.me/BotFather)** | `BOT_TOKEN` is required for the Worker and every registration script | Also used as the HMAC key that validates Mini App `initData` |
| **Internet access** | `wrangler dev`, the Telegram Bot API, and OpenFreeMap tile fetches all need the network | — |
| **A Cloudflare account** *(deploy and `npm run bot:commands` only)* | `wrangler deploy`, the Workers KV namespace, and the R2 road-graph bucket live there | Wrangler prompts for browser-based auth on first use |
<!-- VERIFY: minimum Node.js version — no `engines` field, `.nvmrc`, or `.node-version` file exists in the repository, so no supported floor is recorded. The repo is developed on Node v24.18.0; confirm the intended minimum with the maintainers. -->
Only required for specific commands, not for a normal first run:
- **Python 3** — `npm run gallery` serves the design gallery via `python3 -m http.server 8123`
(`theme/package.json`, script `basemap:preview`).
- **A Playwright-managed Chromium** — `npm run capture` and `npm run web:capture` both call
bare `chromium.launch()` (`theme/scripts/screenshot.mjs:103`, `web/scripts/capture.mjs:201`), so
they need `npx playwright install chromium` first. No system Chrome is used.
- **`sharp`** (in `routing/`'s devDependencies) — only the `npm run glyphs -w routing` glyph-builder
needs it; nothing else in a first run imports it. Install is still one `npm install` from the root.
There is no database to install, no Docker step, and no `.env` file to author — local secrets
live only in `bot/.dev.vars` (created in step 3). See
[CONFIGURATION.md](CONFIGURATION.md#environment-variables) for what each variable does.
---
## Installation
1. **Clone the repository.**
```bash
git clone http://gitmirah:3000/c-fu/arah2.git
```
2. **Enter the project directory.** The clone is named `arah2` after the repo slug; the
workspace root is what `package.json` lives in.
```bash
cd arah2
```
3. **Install all five workspaces from the root.** npm workspaces hoist dependencies into a
single root `node_modules`, so one install covers `theme`, `bot`, `web`, `search`, and
`routing` — including `@araharah/theme`, which `web` consumes by relative path and never
needs to be published.
```bash
npm install
```
---
## First run
```mermaid
flowchart TD
CLONE["git clone"] --> INSTALL["npm install"]
INSTALL --> VARS["cp bot/.dev.vars.example bot/.dev.vars"]
VARS --> FILL["fill BOT_TOKEN, WEBHOOK_SECRET, WEBHOOK_URL"]
FILL --> BUILD["npm run build"]
BUILD --> DEV["npm run dev — wrangler dev on :8787"]
DEV --> HOOK["point the webhook at the tunnel"]
HOOK --> TALK["message arah2bot in Telegram"]
style CLONE fill:#D9D2C6,stroke:#1A1A1A,stroke-width:2px,color:#1A1A1A
style INSTALL fill:#D9D2C6,stroke:#1A1A1A,stroke-width:2px,color:#1A1A1A
style VARS fill:#D9D2C6,stroke:#1A1A1A,stroke-width:2px,color:#1A1A1A
style FILL fill:#EB8008,stroke:#1A1A1A,stroke-width:2px,color:#1A1A1A
style BUILD fill:#D9D2C6,stroke:#1A1A1A,stroke-width:2px,color:#1A1A1A
style DEV fill:#1A1A1A,stroke:#EB8008,stroke-width:2px,color:#F7F0E3
style HOOK fill:#1A1A1A,stroke:#EB8008,stroke-width:2px,color:#F7F0E3
style TALK fill:#736B1E,stroke:#1A1A1A,stroke-width:2px,color:#F7F0E3
```
### 1. Create your local secrets file
`bot/.dev.vars` is gitignored (`bot/.gitignore`) and must never be committed. Copy the template:
```bash
cp bot/.dev.vars.example bot/.dev.vars
```
Then fill in all three variables:
```bash
# Copy to .dev.vars and fill for local wrangler dev (D-15). NEVER commit .dev.vars.
BOT_TOKEN=
WEBHOOK_SECRET=
# Full origin + path; set-webhook.mjs auto-appends /telegram/webhook if only an origin is set.
WEBHOOK_URL=
```
- **`BOT_TOKEN`** — from [@BotFather](https://t.me/BotFather). Required.
- **`WEBHOOK_SECRET`** — any high-entropy random string you choose. Telegram echoes it in the
`X-Telegram-Bot-Api-Secret-Token` header, and the Worker constant-time-compares it before
touching any update.
- **`WEBHOOK_URL`** — the public origin Telegram will POST updates to. Locally this is a
tunnel (see step 5); `set-webhook.mjs` appends `/telegram/webhook` automatically if you give it
a bare origin.
Every script in `bot/scripts/` resolves each variable as `process.env.<NAME> ?? devVars.<NAME>`,
so your shell environment wins over the file and the file is the fallback.
### 2. Build
```bash
npm run build
```
This runs `build -w theme` then `build -w web`. **The order matters:** `theme/dist/rams.css` is an
input to the web build, so the theme must compile first. Neither `theme/dist` nor `web/dist` is
committed (root `.gitignore` excludes `dist/`), so this step is required on every fresh clone and
before every `dev`/`deploy`.
### 3. Run the Worker locally
```bash
npm run dev
```
Root `dev` is `npm run build && npm run dev -w bot`, which is `wrangler dev --port 8787 --local`.
Wrangler reads `bot/.dev.vars`, emulates the `chat_sessions` KV namespace under `bot/.wrangler/`,
and serves three things on **one origin, port `8787`**:
| Path | Served by |
| --- | --- |
| `/telegram/webhook` | Worker `fetch` handler (`run_worker_first`) |
| `/api/*` | Worker `fetch` handler (`run_worker_first`) |
| everything else | `web/dist` static assets via the `ASSETS` binding |
Because it is the single origin, you can smoke-test the API without Telegram at all:
```bash
# Unknown paths are 404 — the Worker's fetch handler routes exactly six paths.
curl -i http://localhost:8787/api/location
# → 401 (no bearer token)
# An unauthenticated webhook POST fails closed when WEBHOOK_SECRET is unset,
# otherwise it 401s on the secret-token mismatch.
curl -i -X POST http://localhost:8787/telegram/webhook
```
The turn-card cron can be fired by hand against the local server — wrangler's scheduled-event
endpoint, no tunnel needed:
```bash
curl "http://localhost:8787/cdn-cgi/local/scheduled"
```
### 4. Expose the webhook
`wrangler dev` binds to localhost only, so Telegram cannot reach it. Expose port `8787` with a
tunnel, then run the one-time registration script, which reads the same `bot/.dev.vars`:
```bash
# Terminal 1 — the dev server (from step 3)
npm run dev
# Terminal 2 — a tunnel that publishes port 8787 at a public HTTPS origin
cloudflared tunnel --url http://localhost:8787
```
Take the `https://…trycloudflare.com` origin it prints, put it in `WEBHOOK_URL` in
`bot/.dev.vars`, and register it:
```bash
npm run deploy --workspace bot # or, without the deploy half: see below
```
`bot`'s own `deploy` script is `wrangler deploy && node scripts/set-webhook.mjs`, which is the
wrong tool for local work. For a **local** webhook registration, run the script directly — it only
calls Telegram's `setWebhook`; it does not deploy anything:
```bash
node bot/scripts/set-webhook.mjs
```
Expected output on success:
```text
setWebhook ok:true — updates will be delivered to the configured URL with the secret-token header.
```
If `WEBHOOK_URL` has no path, the script prints a normalization line first:
```text
WEBHOOK_URL had no path — normalized to .../telegram/webhook
```
<!-- VERIFY: the recommended tunnel tooling is not pinned by the repository — no `cloudflared`, `ngrok`, or `localtunnel` dependency or config exists. Replace with whatever tunnel your team standardizes on, and record the choice here. -->
Register the persistent command menu (English + Bahasa Melayu) once, so `/help`, `/privacy`,
and `/cari` appear in the Telegram UI:
```bash
npm run bot:commands
```
`register-commands.mjs` registers exactly `["help", "privacy", "cari"]` in four private-chat
scopes — default (en), default+ms, all_private_chats (en), all_private_chats+ms — and clears the
group scopes, because Telegram resolves a chat's menu from the *narrowest* scope that has any
commands and a stale set shadows the default one. `/start` is a working handler that does **not**
appear in the menu (type it manually); `/search` is `/cari`'s registered twin handler. The
descriptions are derived from the `helpMenu` lines in `bot/src/i18n/strings.ts`, the same text
`/help` replies with, so the menu and the reply cannot drift.
### 5. Talk to arah2bot
Open Telegram, find **arah2bot**, and send `/start`. The Worker receives the update at
`/telegram/webhook`, verifies the secret-token header, and replies. Supported commands:
| Command | Alias | Behaviour |
| --- | --- | --- |
| `/start` | `/mula` | One-sentence introduction to arah² and its privacy posture |
| `/help` | — | Lists the commands available and points at the known-limits record |
| `/privacy` | `/privasi` | States exactly what is stored (~15 min) and what is never stored |
| `/cari` | `/search` | Searches the KL gazetteer (`/cari KLCC`) and replies with one venue card carrying the OPEN IN ARAHARAH Mini App button plus 2–4 alternates |
The Mini App opens from the venue card's OPEN button, or from the bot's menu button. The script
that repoints the menu button at the deployed origin (`type: web_app`) has **no npm alias** —
run it directly:
```bash
node bot/scripts/set-menu-button.mjs
```
### Working on the Mini App alone
For map and UI work that doesn't need the bot, run Vite instead of the Worker. It serves on port
**`5173`** with `strictPort: true`, so a port clash fails loudly rather than drifting:
```bash
npm run web:dev
```
There is no Worker behind it, so `web/src/main.ts` falls back to the mock Telegram WebApp via
`import.meta.env.DEV` and the `/api/*` calls stay stubbed. This is the fastest map/UI loop; run
`npm run dev` when you need the real session handshake.
---
## Verify the setup
Run the four test suites and the four typechecks — the runners are hand-rolled and
zero-dependency (`node <workspace>/test/runner.mjs` each), so no test framework needs installing:
```bash
npm run bot:test # 10 suites in bot/test/*.test.mjs
npm run web:test # 14 suites in web/test/*.test.mjs
npm run search:test # 8 suites in search/test/*.test.mjs
npm run routing:test # 9 suites in routing/test/*.test.mjs
npm run bot:typecheck # tsc --noEmit
npm run web:typecheck # tsc --noEmit
npm run search:typecheck # tsc --noEmit
npm run routing:typecheck # tsc --noEmit
```
Each runner prints `PASS <name>` / `FAIL <name>` per case and exits `0` only when every case
passed. If a suite fails to load, it is recorded as one failing case rather than crashing the run.
(The search and routing counts include their golden-corpus suites, which measure the *committed*
datasets offline — no network.)
The bot suites are `cari`, `commands`, `guidance-cron`, `guidance-envelope`, `initdata`,
`location-pin`, `privacy`, `route-privacy`, `sessions`, and `webhook`; the web suites include
`build-artifact`, `copy-gate`, `destination-preload`, `failure-state-matrix`, `fullscreen`,
`guidance-eta`, `guidance-replay`, `guidance-surface`, `homescreen`, `location-chain`,
`no-storage`, `route-surface`, `session`, and `theme`. Four are worth knowing about as you get
started:
- **`no-storage`** pins that nothing in `web/src` uses `localStorage`, `sessionStorage`, cookies,
IndexedDB, or the Cache API.
- **`privacy`** greps that every `chat_sessions` write carries an explicit `expirationTtl`.
- **`route-privacy`** pins the same category-only, claim-derived access control on `/api/route`.
- **`guidance-*`** (bot and web) pin the envelope lifecycle, the cron card projection, and the
Mini App's replay/ETA surfaces.
Optionally, prove the repo-wide privacy and budget invariants — these are the maintainer-time
audits a fork must re-run before its first driver:
```bash
npm run privacy-audit # storage, KV-TTL, category-only logs, host allowlist, bundle telemetry
npm run budget-audit # Worker CPU, bundle sizes, KV write classes, golden corpora
```
And prove the design tokens carry no off-token values:
```bash
npm run audit
```
`theme/scripts/audit.mjs` scans the rendered CSS and exits `1` on any colour, spacing,
typography, radius, shadow, or blur value not derived from `theme/src/tokens.css`; it prints
`AUDIT CLEAN — …` and exits `0` when the sheet is pure.
---
## Common setup issues
**`npm run dev` starts but every request returns the Mini App, or nothing.**
`web/dist` may be stale or absent. `run_worker_first` covers only `/api/*` and `/telegram/*`
(a glob **array**, not a boolean), so everything else falls through to the assets binding. Run
`npm run build` first, or just use `npm run dev` — it already builds.
**`[webhook] rejected: misconfigured` → `500 Server misconfigured`.**
`WEBHOOK_SECRET` is missing or empty in `bot/.dev.vars`. The fail-closed guard sits at the top of
the `/telegram/webhook` branch in `bot/src/webhook.ts` and it fails closed deliberately: grammY's
`compareSecretToken` silently accepts *every* request when the secret is `undefined`, so an
unarmed secret must never reach it. Generate a new random string and put it in `WEBHOOK_SECRET`.
**`setWebhook ok:true` but arah2bot never responds.**
Telegram accepted the URL, so it answered `ok` while delivering updates to a path that `404`s.
This happens when `WEBHOOK_URL` is a bare workers.dev origin. `set-webhook.mjs` now appends
`/telegram/webhook` for you — if you registered the webhook by hand or via a different tool,
re-run `node bot/scripts/set-webhook.mjs` and confirm the registered URL includes the path.
**`/cari` is in the code but missing from the Telegram command menu.**
The bot-side bug that caused this was found and fixed on 2026-10-25 (`register-commands.mjs` sent
the scope under the wrong parameter name); re-running `npm run bot:commands` now arms all
private-chat scopes correctly. What can *still* bite: the Telegram client **caches** the menu, so
a device that opened the bot before a fix may keep showing the stale menu until the app restarts.
The workaround is always available — type `/cari` in the chat; the command itself has always
worked.
**`Missing BOT_TOKEN, WEBHOOK_URL, WEBHOOK_SECRET` and exit code `1`.**
`set-webhook.mjs` checks all three before any network call and never echoes values — only names.
Confirm you ran it from a path where `bot/.dev.vars` resolves (`bot/.dev.vars` is read relative to
the script, so it works from the repo root too), and that you did not commit a truncated file.
**Nothing happens when I send `/start`.**
Almost always the tunnel. `wrangler dev --local` binds to localhost, so Telegram has no route in.
Re-run the tunnel, update `WEBHOOK_URL`, re-run `node bot/scripts/set-webhook.mjs`, and confirm the
`tunnel origin` in `.dev.vars` still matches the one currently printing in Terminal 2 — tunnels
rotate their hostname on every restart.
**`/api/route` answers bare `null` for long trips.**
That is the honest degradation, not a bug: route computation runs inside the Cloudflare Workers
free plan's small per-request CPU allowance. Short KL trips (2–8 km) measure 2–8 ms and resolve;
cross-metro trips (20–30 km) measured p95 38–93 ms against the same graph, beyond the free plan,
so the app shows the router-pending card instead of freezing or drawing a wrong line. The committed
upgrade lever (ALT landmark preprocessing) is a next-milestone item — see
[KNOWN-LIMITATIONS.md](KNOWN-LIMITATIONS.md). Workers Paid was deliberately rejected: it breaks
the project's $0 mandate.
**`chromium.launch()` fails — `npm run capture` or `npm run web:capture` errors on a missing browser.**
Both scripts use bare `chromium.launch()` with no `executablePath` and no `channel`, so they need
Playwright's own browser build:
```bash
npx playwright install chromium
```
`npm run web:capture` additionally requires the harness build to exist — it serves
`web/dist-harness/`, not `web/dist`, and waits for `RAMS-BASEMAP-READY` then `ARAHARAH-LIVE`
signals. Run `npm run web:build:harness` first. Its 90-second denial timeout is deliberate: the
chat-pin poll must elapse before the denied card appears.
**Mini App boots but shows no map.**
Check the network tab for tile requests to `https://tiles.openfreemap.org/…` — the generated
`theme/basemap/liberty-rams.json` points there, so a blocked or offline request leaves a blank
canvas. `npm run gallery` serves the basemap locally on `:8123` if you want to inspect the style
in isolation.
**`theme/dist/rams.css` is missing when `web` builds.**
Run `npm run build` from the root rather than `npm run build -w web`. The root `build` script
chains theme then web; the web build imports the compiled stylesheet, not the token source.
---
## Next steps
- **[ARCHITECTURE.md](ARCHITECTURE.md)** — how the Worker, KV namespace, R2 graph, Mini App, and
design system fit together; the Worker routing table; the three-link location acquisition chain.
- **[CONFIGURATION.md](CONFIGURATION.md)** — every environment variable, `bot/wrangler.jsonc`
and `web/vite.config.ts`, and the full command reference per workspace.
- **[TESTING.md](TESTING.md)** — each suite's contract and how to write a new one.
- **[KNOWN-LIMITATIONS.md](KNOWN-LIMITATIONS.md)** — stated plainly: no traffic awareness, no lane
guidance, the cross-metro CPU degradation, the stale Telegram menu cache, and what is on the
roadmap. Beta-quality caveats included (e.g. battery drain is a *planned* measurement before the
next driver onboarding, not an estimate).
- **[../README.md](../README.md)** — package overview, HTTP surface, design-system usage.
- **[../DESIGN.md](../DESIGN.md)** — the Dieter Rams philosophy behind the token vocabulary.
### Everyday commands
| Command | What it does |
| --- | --- |
| `npm run dev` | Build, then `wrangler dev --port 8787 --local` |
| `npm run build` | Build theme → Mini App (`theme/dist/rams.css` must exist first) |
| `npm run web:dev` | Vite dev server on `5173` with a mock WebApp and no Worker |
| `npm run bot:test` / `npm run web:test` / `npm run search:test` / `npm run routing:test` | Zero-dependency test runners |
| `npm run bot:typecheck` / `npm run web:typecheck` / `npm run search:typecheck` / `npm run routing:typecheck` | `tsc --noEmit` |
| `npm run privacy-audit` | Repo-wide privacy audit (add `--` and `--live` for the live KV/cache probes) |
| `npm run budget-audit` | Free-tier budget audit, measured and printed |
| `npm run audit` | Off-token audit of the rendered CSS; non-zero on violations |
| `npm run bot:commands` | Register the persistent Telegram command menu (EN + MS) |
| `npm run deploy` | Build, then `wrangler deploy` + `set-webhook.mjs` |
The two build outputs must **never** share an `outDir`: `web:build:harness` emits to
`web/dist-harness/` with the mock installer and `/api/*` fetch stubs that production must not
carry. `vite.config.ts` calls this out as the reason the split exists.
### Deploying
`npm run deploy` publishes the Worker to Cloudflare and registers the webhook. It reads no local
file for secrets — production values live as Cloudflare Workers secrets. See
[CONFIGURATION.md](CONFIGURATION.md#per-environment-overrides). A fork must also provision its own
R2 road-graph bucket (`npx wrangler r2 bucket create <name>` +
`routing/scripts/upload-road-graph.mjs`) — the fork contract is [ROAD-GRAPH.md](ROAD-GRAPH.md).
<!-- VERIFY: which environment variables are set as Cloudflare Workers secrets via `wrangler secret put`, and under which Cloudflare account, cannot be determined from the repository. -->