docs(04-04): complete webview deep-link hand-off plan — two-carrier resolver + ONBD-03 hooks + device gate escalated
This commit is contained in:
@@ -0,0 +1,244 @@
|
|||||||
|
---
|
||||||
|
phase: 04-destination-search-kl-gazetteer
|
||||||
|
plan: 04
|
||||||
|
subsystem: web
|
||||||
|
tags: [destination-preload, deep-link, start-param, gazetteer, homescreen, add-to-home-screen, device-gate, capture, tdd]
|
||||||
|
requires:
|
||||||
|
- phase: 04-01 (shared search module)
|
||||||
|
provides: "@araharah/search — getEntryById for the webview's own build-time id lookup"
|
||||||
|
- phase: 04-03 (chat search surface)
|
||||||
|
provides: "the corrected carrier — <MINI_APP_URL>/?dest=<slug> on a web_app button; MINI_APP_URL origin"
|
||||||
|
- phase: 03 (mini app shell)
|
||||||
|
provides: "the staged boot, card stack, strip, harness mock, capture runner, BA gates"
|
||||||
|
provides:
|
||||||
|
- "web/src/search/resolve-destination.ts — the two-carrier reader (URL ?dest= PRIMARY, signed initData start_param SECONDARY, one slug validation, dest_ prefix stripped) + the memory-only holder"
|
||||||
|
- "web/src/search/destination-card.ts — the destination confirmation + honest-unavailable surfaces (createElement/textContent only)"
|
||||||
|
- "web/src/telegram/homescreen.ts — wireHomeScreen behind the BOTH-halves Bot API 8.0 guard (added hides / unknown shows / missed shows / unsupported nothing)"
|
||||||
|
- "TelegramWebAppLike gains startParam, addToHomeScreen?, checkHomeScreenStatus? + HomeScreenStatus; the mock satisfies them in the same change"
|
||||||
|
- "BA7 (bot/web id-set parity: the shipped bundle carries EVERY gazetteer id) + BA8 (no deep-link crypto in the shipped JS)"
|
||||||
|
- "ARAHARAH-DEST state-token boot signal + the shell-dest-preload capture leg (8/8)"
|
||||||
|
- "the deployed Worker (31138f79) with the destination-preload webview live; 04-DEVICE-GATE.md with six UNVERIFIED physical legs"
|
||||||
|
affects:
|
||||||
|
- "phase 5 (origin/destination resolvers consume getDestination(); the destination is already seeded at boot)"
|
||||||
|
- "phase 7 (ONBD-02 device verification fallback venue if legs stay unverified)"
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns:
|
||||||
|
- "two-carriers-one-validation reader: URLSearchParams for location.search, hand-rolled &-split (never URLSearchParams) for the signed initData string"
|
||||||
|
- "RED-verified gates first (suite-load failure = RED), per-task test→feat commit pairs"
|
||||||
|
- "state-token capture signals (ARAHARAH-DEST state=resolved|absent, never the id) paired with a DOM text assertion"
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- web/src/search/resolve-destination.ts
|
||||||
|
- web/src/search/destination-card.ts
|
||||||
|
- web/src/telegram/homescreen.ts
|
||||||
|
- web/test/destination-preload.test.mjs
|
||||||
|
- web/test/homescreen.test.mjs
|
||||||
|
- .planning/phases/04-destination-search-kl-gazetteer/04-DEVICE-GATE.md
|
||||||
|
modified:
|
||||||
|
- web/src/telegram/webapp.ts
|
||||||
|
- web/src/mock/telegram-mock.ts
|
||||||
|
- web/src/i18n/strings.ts
|
||||||
|
- web/src/main.ts
|
||||||
|
- web/index.html
|
||||||
|
- web/package.json
|
||||||
|
- package-lock.json
|
||||||
|
- web/test/build-artifact.test.mjs
|
||||||
|
- web/test/no-storage.test.mjs
|
||||||
|
- web/scripts/capture.mjs
|
||||||
|
decisions:
|
||||||
|
- "The destination surface mounts into the EXISTING card stack and holds the center register at live (null when no id → Phase 3 boot byte-identical, D-12); the denied/awaiting location registers keep their cards — the destination returns at live"
|
||||||
|
- "waitMode 'dest' waits on the DOM (page.waitForFunction for 'Suria KLCC') after the state-token assertion — the signal fires at boot but the card mounts at live, so a one-shot text sample would race the chain"
|
||||||
|
- "homeScreenAlreadyAdded copy is defined in both locales but has NO render site: status 'added' HIDES the affordance (do not nag) — the string is reserved for a future surface that must say 'already added' (T-04-37 law)"
|
||||||
|
- "Deploy canary drift (menu button unarmed, command list empty on the live bot) fixed by re-running the repo's own one-time registration scripts — Telegram-side state, not source"
|
||||||
|
metrics:
|
||||||
|
duration: "~14 min executor time"
|
||||||
|
completed: 2026-10-23
|
||||||
|
tasks: 4 (3 automated + 1 blocking device-gate checkpoint)
|
||||||
|
filesCreated: 6
|
||||||
|
filesModified: 10
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 4 Plan 4: Webview Deep-Link Hand-off + Device Gate Summary
|
||||||
|
|
||||||
|
**One-liner:** the webview half of ONBD-02 — a two-carrier destination reader (`?dest=` URL
|
||||||
|
query first, signed `initData.start_param` second, one `[A-Za-z0-9_-]{1,64}` validation,
|
||||||
|
`dest_` prefix stripped) resolving against the webview's own build-time copy of the shared
|
||||||
|
gazetteer into a memory-only holder, rendering the destination card (never a blank map,
|
||||||
|
never a fabricated coordinate) — plus ONBD-03's minimal Telegram-native home-screen hooks
|
||||||
|
behind a Bot API 8.0 guard with the iOS "unknown" treated as indeterminate, the BA7/BA8
|
||||||
|
drift gates proving the shipped bundle carries all 30 ids and zero crypto, the 8/8 capture
|
||||||
|
including the destination-preload leg, and the deployed origin with its blocking
|
||||||
|
six-leg device gate recorded honest-UNVERIFIED.
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Duration:** ~14 min
|
||||||
|
- **Started:** 2026-10-23T17:31:23Z
|
||||||
|
- **Completed:** 2026-10-23T17:45Z (automated half; device gate pending user)
|
||||||
|
- **Tasks:** 3/3 automated + the blocking checkpoint escalated
|
||||||
|
- **Files:** 16 (6 created, 10 modified)
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- The deep-link hand-off is complete end-to-end in code and live on the deployed Worker:
|
||||||
|
`/cari` → venue card → OPEN (`?dest=<slug>`) → the webview resolves the id against its
|
||||||
|
own gazetteer copy → the DESTINATION card renders (verified by the capture leg's
|
||||||
|
`state=resolved` + "Suria KLCC" DOM assertion). No crypto anywhere on the path.
|
||||||
|
- ONBD-03's minimal hooks are live: `wireHomeScreen` guards on BOTH halves (method
|
||||||
|
presence + `isVersionAtLeast("8.0")`), hides on "added", shows the neutral prompt on
|
||||||
|
"unknown"/"missed", nothing on "unsupported", catches refused installs, and hides on
|
||||||
|
`homeScreenAdded` — with NO browser install APIs (HS9).
|
||||||
|
- The honesty laws are mechanical: DP6 rejects traversal/XSS/empty/200-char ids on BOTH
|
||||||
|
carriers; DP8/D-11 pin the honest null; BA7 proves the shipped bundle carries every
|
||||||
|
committed gazetteer id (the bot/web parity gate); BA8 proves no deep-link crypto ships.
|
||||||
|
- Deploy `31138f79` is live with all canaries green after re-arming Telegram-side
|
||||||
|
registrations (menu button web_app, help/privacy/cari command menus).
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
| task | commit | message |
|
||||||
|
| ---- | ------ | ------- |
|
||||||
|
| 1 RED | `3e3d191` | test(04-04): RED — the ONBD-02 destination-preload gate suite (DP1-DP12) |
|
||||||
|
| 1 GREEN | `4f95841` | feat(04-04): the destination resolver — two carriers, one validation, memory only |
|
||||||
|
| 2 RED | `6e421e6` | test(04-04): RED — the ONBD-03 home-screen gate suite (HS1-HS9) |
|
||||||
|
| 2 GREEN | `c2d78b0` | feat(04-04): ONBD-03 home-screen hooks + the boot wiring + the ?dest= harness profile |
|
||||||
|
| 3 | `747eeb7` | test(04-04): the BA7/BA8 drift gates + the search-tree GEO3 pin + the destination-preload capture leg |
|
||||||
|
| 4 | `7b02c7d` | docs(04-04): the blocking ONBD-02/ONBD-03 device gate — deployed + canaries green, six legs UNVERIFIED pending a real device |
|
||||||
|
|
||||||
|
## Verification Results
|
||||||
|
|
||||||
|
- `npm test -w web` — **57 passed, 0 failed** (DP1–DP12, HS1–HS9, BA1–BA8, GEO3 incl. the
|
||||||
|
new explicit `web/src/search` pin, COPY gates over the 5 new strings, all prior suites)
|
||||||
|
- `npm test -w bot` — **65 passed, 0 failed** (unaffected; the deep-link contract's bot half)
|
||||||
|
- `npm run search:corpus` — threshold PASS: top-3 68/68 (100%), negatives refused 7/7,
|
||||||
|
margin 0.643 < 0.72 (D-14 close holds)
|
||||||
|
- `npm run web:typecheck` / `search:typecheck` — green
|
||||||
|
- `npm run build -w theme && npm run build -w web` — green; BA7 RED against the stale
|
||||||
|
pre-gazetteer bundle, GREEN after (the RED evidence for task 3)
|
||||||
|
- `npm run capture -w web` — **CAPTURE OK — 8/8 non-blank captures**, incl.
|
||||||
|
`shell-dest-preload.png` (34,499 B) whose leg asserted `ARAHARAH-DEST state=resolved`
|
||||||
|
AND the rendered card text "Suria KLCC"; GEO-03 runtime probe PASSED (storage empty)
|
||||||
|
- Live-origin canaries: `getWebhookInfo.url` ends `/telegram/webhook` (last_error is the
|
||||||
|
STALE 2026-10-22 phase-3-era 500, pending 0); menu button `web_app` → origin ✓;
|
||||||
|
commands default+ms+all_private_chats list `help/privacy/cari` ✓; unsigned webhook
|
||||||
|
POST → 401 ✓; the live `main-*.js` chunk carries **30/30 gazetteer ids, zero crypto
|
||||||
|
tokens**, and the new copy ✓
|
||||||
|
- `MINI_APP_URL` == deployed origin (binding shown in the deploy log); secrets never echoed
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
### Auto-fixed Issues
|
||||||
|
|
||||||
|
**1. [Rule 1 - Bug] The dest capture leg's DOM check raced the boot chain**
|
||||||
|
- **Found during:** task 3 (design review of the wait mode, pre-commit)
|
||||||
|
- **Issue:** the plan's `waitMode: 'dest'` snippet sampled `page.textContent('body')` ONCE,
|
||||||
|
immediately after the ARAHARAH-DEST race. The signal fires at boot stage 13, but the
|
||||||
|
destination card mounts only when the chain reaches LIVE (the boot stages replace the
|
||||||
|
card stack transiently) — a one-shot sample could read the body before the card existed.
|
||||||
|
- **Fix:** kept the plan's `state=resolved` assertion, then waited for the card text with
|
||||||
|
`page.waitForFunction(...'Suria KLCC', { timeout: 15000, polling: 250 })` instead of a
|
||||||
|
single sample. Same assertion, race-free. Capture passed 8/8.
|
||||||
|
- **Files modified:** web/scripts/capture.mjs
|
||||||
|
|
||||||
|
**2. [Rule 3 - Blocking] Live-bot registrations were un-armed (menu button + command menu)**
|
||||||
|
- **Found during:** task 4 (deploy canaries — `getChatMenuButton` returned the default
|
||||||
|
`commands` type, `getMyCommands` empty)
|
||||||
|
- **Issue:** Telegram-side bot registrations from earlier phases did not survive on the
|
||||||
|
live bot; the plan requires the menu button web_app armed and the command list verified
|
||||||
|
post-deploy.
|
||||||
|
- **Fix:** re-ran the repo's own one-time registration scripts (`node
|
||||||
|
bot/scripts/set-menu-button.mjs`, `node bot/scripts/register-commands.mjs`); re-verified
|
||||||
|
via `getChatMenuButton`/`getMyCommands`. No source changes.
|
||||||
|
- **Files modified:** none (Telegram-side state)
|
||||||
|
|
||||||
|
**3. [Planner-note] Capture-leg comment numbering**
|
||||||
|
- The plan's leg snippet was commented "/* 8 —" while instructing placement after leg 6;
|
||||||
|
inserted as leg 7 with gallery-compare renumbered 8 so execution order reads correctly.
|
||||||
|
Cosmetic only; the `expected` array and the 8/8 line match the plan verbatim.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Total deviations:** 2 auto-fixed (1 × Rule 1, 1 × Rule 3) + 1 cosmetic note
|
||||||
|
**Impact on plan:** none on scope; both fixes were required for the plan's own gates/
|
||||||
|
canaries to hold.
|
||||||
|
|
||||||
|
## Auth Gates
|
||||||
|
|
||||||
|
None — no secrets handled beyond reading the local gitignored `bot/.dev.vars` for deploy
|
||||||
|
canary Bot API calls; the token was never echoed (only concatenated into API URLs), and
|
||||||
|
`MINI_APP_URL` is a public var by design.
|
||||||
|
|
||||||
|
## Known Stubs
|
||||||
|
|
||||||
|
- `strings.*.homeScreenAlreadyAdded` (web/src/i18n/strings.ts) — defined + copy-gated in
|
||||||
|
both locales but with NO render site: the plan's own behaviour law makes status "added"
|
||||||
|
HIDE the affordance (do not nag), so no surface can honestly display "Already on your
|
||||||
|
home screen" in this phase. Reserved for a future surface that must SAY "already added"
|
||||||
|
(e.g. a settings view); the string lives in the strings module so copy stays centralized.
|
||||||
|
|
||||||
|
No other stubs — every export is wired to real behavior; no TODO/FIXME/placeholder in the
|
||||||
|
new tree.
|
||||||
|
|
||||||
|
## Threat Flags
|
||||||
|
|
||||||
|
None — the built surface is exactly the plan's threat model (T-04-32…T-04-42), every
|
||||||
|
mitigation landed WITH a gate: charset validation before any lookup (DP6, T-04-32), the
|
||||||
|
signed-string-only initData parse with start_param as a pure lookup key (DP2/DP10,
|
||||||
|
T-04-33), the bounded prank consequence + honest null (DP8, T-04-34), memory-only
|
||||||
|
destination (DP9/DP12 + GEO3 search-tree pin + runtime probe, T-04-35), the both-halves
|
||||||
|
capability guard with caught refusals (HS1/HS2/HS7, T-04-36), the never-fail status
|
||||||
|
handling (HS4/HS5, T-04-37), the browser-install-API ban (HS9, T-04-38), the id-parity
|
||||||
|
drift gate (BA7, T-04-39), the no-crypto bundle gate (BA8 + DP10, T-04-40), the harness
|
||||||
|
DCE proof (BA5/BA6 + capture's inverse assertion, T-04-41), and the anti-fake capture
|
||||||
|
race (state=resolved + DOM text, T-04-42).
|
||||||
|
|
||||||
|
## TDD Gate Compliance
|
||||||
|
|
||||||
|
PASSED — tasks 1 and 2 each carry a `test(...)` RED commit verified failing (suite-load
|
||||||
|
failures: 33+1 and 45+1) followed by a `feat(...)` GREEN commit:
|
||||||
|
`3e3d191→4f95841`, `6e421e6→c2d78b0`. Task 3 is a gates-and-tooling task (no production
|
||||||
|
source): its RED (`BA7` failing against the stale pre-gazetteer `web/dist`, 56+1) and its
|
||||||
|
GREEN (BA1–BA8 green after the rebuild — the build output is gitignored, so no source
|
||||||
|
commit exists for the GREEN half) are recorded here and in the execution log; the git
|
||||||
|
sequence is therefore `test → feat → feat → test → test → docs` at plan level.
|
||||||
|
|
||||||
|
## Device Gate (task 4 — BLOCKING CHECKPOINT)
|
||||||
|
|
||||||
|
`.planning/phases/04-destination-search-kl-gazetteer/04-DEVICE-GATE.md` records the
|
||||||
|
deploy (version `31138f79`), the canary table, the origin prerequisite, and SIX physical
|
||||||
|
legs — ONBD-02 on Android / iOS / Desktop, the ONBD-03 home-screen add + launch, the
|
||||||
|
unknown-id honesty check, and the `/cari Kuching` honest refusal — **every leg
|
||||||
|
UNVERIFIED**: no physical device is attached to this environment and none was faked.
|
||||||
|
|
||||||
|
**ORCHESTRATOR ACTION REQUIRED:** per this wave's split of ownership, `.planning/STATE.md`
|
||||||
|
and `.planning/ROADMAP.md` were deliberately NOT touched. The device-gate blocker
|
||||||
|
("ONBD-02/ONBD-03 device legs UNVERIFIED — needs a real Android/iOS/Desktop; phase held
|
||||||
|
open") must be recorded under STATE.md "Blockers/Concerns" by the orchestrator, and
|
||||||
|
ONBD-02/ONBD-03 must NOT be marked complete in REQUIREMENTS.md until the legs pass (the
|
||||||
|
plan's resume signal: "device gate passed").
|
||||||
|
|
||||||
|
## User Setup Required
|
||||||
|
|
||||||
|
Walk the six legs in `04-DEVICE-GATE.md` on a real device (the bot is LIVE at
|
||||||
|
https://araharah-bot.araharah-bot.workers.dev — `/cari KLCC` → OPEN). Also visually
|
||||||
|
confirm @BotFather's Mini App URL equals that origin (the Bot API cannot read the
|
||||||
|
BotFather-side value; the bot-side menu button + `MINI_APP_URL` are confirmed equal).
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
- Phase 5 can consume `getDestination()` — the boot already seeds the memory-only holder
|
||||||
|
from either carrier; the origin/destination resolvers plug into the SearchSource seam.
|
||||||
|
- The carrier-swap lever stands ready if device legs 1–3 fail: `miniAppUrlFor(origin, id)`
|
||||||
|
in `bot/src/bot/commands/cari.ts` is the one place the deep link is built.
|
||||||
|
- `/pergi_ke` remains registered nowhere (D-01 reservation); the corpus gate and all
|
||||||
|
suite gates are green for the phase-exit verifier.
|
||||||
|
|
||||||
|
## Self-Check: PASSED
|
||||||
|
|
||||||
|
- All 6 created files exist on disk (verified below); all 10 modified files committed.
|
||||||
|
- All 6 task commits present in `git log` (`3e3d191`, `4f95841`, `6e421e6`, `c2d78b0`,
|
||||||
|
`747eeb7`, `7b02c7d`); zero file deletions in any (`git diff --diff-filter=D` = 0 after
|
||||||
|
each).
|
||||||
|
- Suites re-run at close: web 57/57, bot 65/65, corpus threshold PASS; capture 8/8;
|
||||||
|
deploy live with canaries green.
|
||||||
Reference in New Issue
Block a user