wiki: Home — HTML-entity the confusable literals (²→·×) so Gitea's ambiguous-unicode banner clears; rendered text identical

araharah-executor
2026-10-25 13:43:02 +08:00
parent 1cba153edb
commit 86739aaa94
+21 -21
@@ -1,10 +1,10 @@
<!-- generated-by: gsd-doc-writer --> <!-- generated-by: gsd-doc-writer -->
<p align="center"> <p align="center">
<img src="http://gitmirah:3000/c-fu/arah2/media/branch/main/assets/MASTER_FILE-arah2_logo.svg" alt="arah² logo" width="280"> <img src="http://gitmirah:3000/c-fu/arah2/media/branch/main/assets/MASTER_FILE-arah2_logo.svg" alt="arah&#178; logo" width="280">
</p> </p>
# arah² # arah&#178;
A privacy-respecting, Telegram-first GPS navigator — a waze-like route navigator delivered through a Telegram bot and Mini App instead of an app-store install, built on OpenStreetMap data, and designed to Dieter Rams' "less, but better." First users are in the Kuala Lumpur metro. A privacy-respecting, Telegram-first GPS navigator — a waze-like route navigator delivered through a Telegram bot and Mini App instead of an app-store install, built on OpenStreetMap data, and designed to Dieter Rams' "less, but better." First users are in the Kuala Lumpur metro.
@@ -14,7 +14,7 @@ The Telegram bot is **arah2bot**. The product is live — a private beta with re
## Overview ## Overview
arah² is an npm-workspaces monorepo with five packages: arah&#178; is an npm-workspaces monorepo with five packages:
| Package | Name | What it does | | Package | Name | What it does |
|---|---|---| |---|---|---|
@@ -46,7 +46,7 @@ graph TD
APP -->|"GET /api/location — Bearer token"| W APP -->|"GET /api/location — Bearer token"| W
APP -->|"POST /api/route — Bearer token"| R2 APP -->|"POST /api/route — Bearer token"| R2
W --> KV W --> KV
W -->|"cron */2 → turn cards"| TG W -->|"cron */2 &#8594; turn cards"| TG
APP --> MAP APP --> MAP
THEME -.->|"inlined at build time"| MAP THEME -.->|"inlined at build time"| MAP
THEME -.->|"dist/rams.css"| APP THEME -.->|"dist/rams.css"| APP
@@ -109,14 +109,14 @@ cp bot/.dev.vars.example bot/.dev.vars
--- ---
## Quickstart: clone → production (your own bot, your own Cloudflare) ## Quickstart: clone &#8594; production (your own bot, your own Cloudflare)
The concise path. Full detail lives in [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md); local development in [docs/GETTING-STARTED.md](docs/GETTING-STARTED.md). The concise path. Full detail lives in [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md); local development in [docs/GETTING-STARTED.md](docs/GETTING-STARTED.md).
**1. Create the bot** — in Telegram, open [@BotFather](https://t.me/BotFather): **1. Create the bot** — in Telegram, open [@BotFather](https://t.me/BotFather):
- `/newbot` → pick a name + username → copy the **bot token** - `/newbot` &#8594; pick a name + username &#8594; copy the **bot token**
- `/setuserpic` → upload `assets/logo_arah2.png` (the bot's avatar) - `/setuserpic` &#8594; upload `assets/logo_arah2.png` (the bot's avatar)
- Bot Settings → **Main Mini App** → set your future production URL (you'll fill the real one in step 4; BotFather's app *name* is what Telegram shows in the mini-app header) - Bot Settings &#8594; **Main Mini App** &#8594; set your future production URL (you'll fill the real one in step 4; BotFather's app *name* is what Telegram shows in the mini-app header)
**2. Clone + install** **2. Clone + install**
@@ -139,8 +139,8 @@ MINI_APP_URL=https://<your-worker>.workers.dev
```bash ```bash
npx wrangler login # browser auth, once npx wrangler login # browser auth, once
npm run build # theme tokens → web/dist npm run build # theme tokens &#8594; web/dist
npm run deploy # theme → web → worker + assets + webhook + commands npm run deploy # theme &#8594; web &#8594; worker + assets + webhook + commands
``` ```
**5. Provision the road graph** (engine A's routing data — R2, also free): **5. Provision the road graph** (engine A's routing data — R2, also free):
@@ -155,7 +155,7 @@ node routing/scripts/upload-road-graph.mjs # builds + uploads the KL graph, ro
```bash ```bash
node bot/scripts/set-webhook.mjs # arms the webhook (+ /telegram/webhook path) node bot/scripts/set-webhook.mjs # arms the webhook (+ /telegram/webhook path)
node bot/scripts/register-commands.mjs # /help /privacy /cari in every command scope node bot/scripts/register-commands.mjs # /help /privacy /cari in every command scope
node bot/scripts/set-menu-button.mjs # the menu button opens the app ("arah²") node bot/scripts/set-menu-button.mjs # the menu button opens the app ("arah&#178;")
``` ```
**7. Verify** — the canaries (read-backs, nothing trusted blindly): **7. Verify** — the canaries (read-backs, nothing trusted blindly):
@@ -163,9 +163,9 @@ node bot/scripts/set-menu-button.mjs # the menu button opens the app ("arah
```bash ```bash
npm run privacy-audit -- --live && npm run budget-audit -- --live npm run privacy-audit -- --live && npm run budget-audit -- --live
``` ```
Then in Telegram: restart the client → open the bot → `/cari KLCC` → venue card → **OPEN** → the map routes. Done: your own private navigator, $0/month. Then in Telegram: restart the client &#8594; open the bot &#8594; `/cari KLCC` &#8594; venue card &#8594; **OPEN** &#8594; the map routes. Done: your own private navigator, $0/month.
> **Permission notes:** the Cloudflare token (optional, only for the audit `--live` probes) needs Account → Workers KV Storage → **Read** only. The bot itself needs no special Telegram permissions; `/setprivacy` default (off) is correct — the bot never reads non-command messages. > **Permission notes:** the Cloudflare token (optional, only for the audit `--live` probes) needs Account &#8594; Workers KV Storage &#8594; **Read** only. The bot itself needs no special Telegram permissions; `/setprivacy` default (off) is correct — the bot never reads non-command messages.
--- ---
@@ -176,7 +176,7 @@ Then in Telegram: restart the client → open the bot → `/cari KLCC` → venue
- **WASM-router track** — fully client-side routing in the webview (origin+destination never touch a server; static-hosted; the fork-perfect endgame) - **WASM-router track** — fully client-side routing in the webview (origin+destination never touch a server; static-hosted; the fork-perfect endgame)
- **Battery measurement** — the <10%/hr guidance-drain record (deferred from launch) - **Battery measurement** — the <10%/hr guidance-drain record (deferred from launch)
- **Bitmap map snapshots** in minimized chat turn-cards (needs a rasterizer in the $0 stack) - **Bitmap map snapshots** in minimized chat turn-cards (needs a rasterizer in the $0 stack)
- **Voice guidance** · **live trip share** (`sendLocation(live_period)`) · **toll toggle** · **speed-limit display** — the REQUIREMENTS deferred table - **Voice guidance** &#183; **live trip share** (`sendLocation(live_period)`) &#183; **toll toggle** &#183; **speed-limit display** — the REQUIREMENTS deferred table
**v2 (post user-validation):** **v2 (post user-validation):**
- Incident reports + community layer, gamification, traffic-weighted ETAs — per the original vision doc ([IDEAS.md](IDEAS.md)) - Incident reports + community layer, gamification, traffic-weighted ETAs — per the original vision doc ([IDEAS.md](IDEAS.md))
@@ -195,7 +195,7 @@ The bot answers in English and Bahasa Melayu. Commands are registered into Teleg
| Command | Alias | Behaviour | | Command | Alias | Behaviour |
|---|---|---| |---|---|---|
| `/start` | `/mula` | One-sentence introduction to arah² and its privacy posture. | | `/start` | `/mula` | One-sentence introduction to arah&#178; and its privacy posture. |
| `/help` | — | Lists the commands that exist and points at the known-limits record. | | `/help` | — | Lists the commands that exist and points at the known-limits record. |
| `/privacy` | `/privasi` | Names exactly what is stored (user id, chat id, language, ~15 min) and exactly what is never stored (location, routes, trip history, profile). | | `/privacy` | `/privasi` | Names exactly what is stored (user id, chat id, language, ~15 min) and exactly what is never stored (location, routes, trip history, profile). |
| `/cari` | `/search` | Searches the curated KL gazetteer (`/cari KLCC`) and replies with **one** native venue card (`sendVenue`) carrying an OPEN IN ARAHARAH Mini App button plus 2–4 alternates as callback buttons. Out-of-area queries get an honest refusal that names the v1 area, never the query text. | | `/cari` | `/search` | Searches the curated KL gazetteer (`/cari KLCC`) and replies with **one** native venue card (`sendVenue`) carrying an OPEN IN ARAHARAH Mini App button plus 2–4 alternates as callback buttons. Out-of-area queries get an honest refusal that names the v1 area, never the query text. |
@@ -204,7 +204,7 @@ The bot answers in English and Bahasa Melayu. Commands are registered into Teleg
1. **Search.** `/cari <place>` normalizes and scores the query against the KL gazetteer (`@araharah/search`); the best hit ships as a venue card with deep link `MINI_APP_URL/?dest=<gazetteer-id>`. 1. **Search.** `/cari <place>` normalizes and scores the query against the KL gazetteer (`@araharah/search`); the best hit ships as a venue card with deep link `MINI_APP_URL/?dest=<gazetteer-id>`.
2. **Open the Mini App.** The webview reads `?dest=` from `location.search`, preloads the destination, and mints a session by posting the verbatim `Telegram.WebApp.initData` string to `/api/session`. 2. **Open the Mini App.** The webview reads `?dest=` from `location.search`, preloads the destination, and mints a session by posting the verbatim `Telegram.WebApp.initData` string to `/api/session`.
3. **Acquire a starting point.** The three-link location chain (`web/src/location/chain.ts`) tries **Telegram → WebView → chat pin**, in that locked order. 3. **Acquire a starting point.** The three-link location chain (`web/src/location/chain.ts`) tries **Telegram &#8594; WebView &#8594; chat pin**, in that locked order.
4. **Route.** The app posts the origin and destination to `/api/route`; the Worker's router (`bot/src/routing/worker-router.ts`) loads the R2-hosted CSR graph once per cold isolate, snaps, and runs A* (`@araharah/routing`). Every miss class answers `200 null` — router-pending, never a wrong line. 4. **Route.** The app posts the origin and destination to `/api/route`; the Worker's router (`bot/src/routing/worker-router.ts`) loads the R2-hosted CSR graph once per cold isolate, snaps, and runs A* (`@araharah/routing`). Every miss class answers `200 null` — router-pending, never a wrong line.
5. **Guidance.** Accepting a route mints a guidance envelope (`POST /api/guidance`, 7200 s TTL); a scheduled cron (`*/2 * * * *`) projects progress along the frozen route snapshot and pushes due turn cards — a maneuver glyph photo with a live caption — into the chat, with a button back into the Mini App at `?g=<envelope-id>`. 5. **Guidance.** Accepting a route mints a guidance envelope (`POST /api/guidance`, 7200 s TTL); a scheduled cron (`*/2 * * * *`) projects progress along the frozen route snapshot and pushes due turn cards — a maneuver glyph photo with a live caption — into the chat, with a button back into the Mini App at `?g=<envelope-id>`.
@@ -224,16 +224,16 @@ The Worker's `fetch` handler routes exactly six paths and returns `404` for anyt
```bash ```bash
# Mint a session from a Mini App initData string # Mint a session from a Mini App initData string
curl -X POST http://localhost:8787/api/session --data-binary @initdata.txt curl -X POST http://localhost:8787/api/session --data-binary @initdata.txt
# → {"token":"<opaque>"} # &#8594; {"token":"<opaque>"}
# Poll the location relay with the token in memory # Poll the location relay with the token in memory
curl -H "Authorization: Bearer <opaque>" http://localhost:8787/api/location curl -H "Authorization: Bearer <opaque>" http://localhost:8787/api/location
# → {"pin":null} (honest null, not a fabricated coordinate) # &#8594; {"pin":null} (honest null, not a fabricated coordinate)
``` ```
### Location acquisition in the Mini App ### Location acquisition in the Mini App
`web/src/location/chain.ts` is a three-link state machine with a locked order — **Telegram → WebView → chat pin** — and freshness is a separate axis from availability. A Telegram fix byte-identical to the last accepted one routes to the nudge, never to success; denial is a state, not an exception. The chain exposes a `Fix`-shaped abstraction with a pluggable cadence and hard-codes no interval, so continuous-mode polling is a later decision, not a rewrite. Past 45 seconds the strip flips to `SIGNAL LOST` and the last marker dims. `web/src/location/chain.ts` is a three-link state machine with a locked order — **Telegram &#8594; WebView &#8594; chat pin** — and freshness is a separate axis from availability. A Telegram fix byte-identical to the last accepted one routes to the nudge, never to success; denial is a state, not an exception. The chain exposes a `Fix`-shaped abstraction with a pluggable cadence and hard-codes no interval, so continuous-mode polling is a later decision, not a rewrite. Past 45 seconds the strip flips to `SIGNAL LOST` and the last marker dims.
### Consuming the design tokens ### Consuming the design tokens
@@ -262,9 +262,9 @@ import ramsStyle from "@araharah/theme/basemap/liberty-rams.json";
The Telemetry Rams design system lives in `theme/` as a Tailwind v4 CSS-first token package — the single source of truth for every color, spacing, typography, and radius value in the project. Consume the tokens by importing `theme/src/tokens.css` (plain CSS custom properties, usable by any stylesheet or the MapLibre basemap) or the compiled stylesheet produced by `npm run build`. The token vocabulary mirrors the mockup screens verbatim, so markup derived from them maps one to one. Rams philosophy lives on in `DESIGN.md`, which is superseded for specifics — values are never restated outside the token package. The Telemetry Rams design system lives in `theme/` as a Tailwind v4 CSS-first token package — the single source of truth for every color, spacing, typography, and radius value in the project. Consume the tokens by importing `theme/src/tokens.css` (plain CSS custom properties, usable by any stylesheet or the MapLibre basemap) or the compiled stylesheet produced by `npm run build`. The token vocabulary mirrors the mockup screens verbatim, so markup derived from them maps one to one. Rams philosophy lives on in `DESIGN.md`, which is superseded for specifics — values are never restated outside the token package.
```bash ```bash
npm run build -w theme # src/theme.css → dist/rams.css (minified) npm run build -w theme # src/theme.css &#8594; dist/rams.css (minified)
npm run sheet # build + print the sheet.html path npm run sheet # build + print the sheet.html path
npm run gallery # build + screenshot → serve gallery.html and basemap on :8123 npm run gallery # build + screenshot &#8594; serve gallery.html and basemap on :8123
npm run audit # off-token audit of the rendered CSS — exits non-zero on violations npm run audit # off-token audit of the rendered CSS — exits non-zero on violations
npm run capture # Playwright screenshots of the token sheet npm run capture # Playwright screenshots of the token sheet
``` ```