Files
arah2/.planning/phases/03-mini-app-shell-geolocation/03-DEVICE-MATRIX.md
T
araharah-executor 6e0673f6ef docs(07-05): the /cari re-arm attempt + the A1/A3 assumption closure, recorded honestly
- /cari re-arm RAN 2026-10-25: all six scopes ok:true — 'all command scopes
  armed'; residual Telegram client-menu caching risk stated, workaround
  pointers at docs/KNOWN-LIMITATIONS.md (T-07-24/T-07-26)
- A1: NOT RETIRED as a mechanism hypothesis — mitigated-by-design via the
  chain.ts polling degradation; A3: RETIRED as a decision constraint
  (remote workers.dev origin, no LAN-IP dev deployment)
- the Phase-3 FINAL VERDICT table untouched (1 occurrence; the dated
  section is the closure)
2026-10-25 04:46:40 +08:00

17 KiB
Raw Blame History

Phase 03 — Device-Matrix Exit Gate (ROADMAP Success Criterion 5)

Deployed origin (staging): https://araharah-bot.araharah-bot.workers.dev/ Deployed version: 2c1336e3-58f9-4b0b-86a8-b25de66e327c (via npm run deploy — theme → web → wrangler deploy with web/dist as static assets, D-13/D-14/D-15) Status: PENDING REAL-DEVICE VERIFICATION — the phase does not close until every leg has an explicit verdict.

Automated half (already green)

  • npm test -w web — 33 passed, 0 failed (includes BA1–BA6 build-artifact gates over web/dist, HTML included)
  • npm test -w bot — 48 passed, 0 failed
  • npm run capture -w web — CAPTURE OK, 7/7 non-blank captures off web/dist-harness, including the Desktop denied card, the ?pin=1 chat-pin relay, light + dark live shots, and the DSGN-02 side-by-side. GEO-03 runtime probe: localStorage / sessionStorage / cookies / IndexedDB all empty after a real boot-to-live cycle.
  • Live canaries at the deployed origin: GET / → 200 text/html from the asset binding; POST /api/session → 401 for unsigned bodies (the HMAC gate is live); getWebhookInfo URL ends /telegram/webhook with no last_error; getChatMenuButton type = commands.

Bot API 10.2 origin prerequisite (hard, blocking)

wrangler.jsonc's run_worker_first key drew an "Unexpected fields" warning from wrangler 4.151.3, but the deployed behavior was verified request-by-request: assets serve non-API paths directly, /api/* and /telegram/* are answered by the Worker code (401/500, not asset fallback) — D-13 routing holds in practice; the warnings are cosmetic at this wrangler version.

  • BotFather Mini App URL == the deployed origin above. Bot API 10.2 rejects Mini App calls from any other origin silently — a mismatch means Telegram.WebApp.* no-ops with no error. Check menu button (/setmenubutton) / WebApp URL in @BotFather (arasarah-bot.araharah-bot.workers.dev), not a localhost or Pages URL. Observed / Symptom: ______________________________________

The six device legs (each: an explicit verdict + observation)

1. Android — real phone, Telegram Android (GEO-01)

  • Boot: BOOT-01 (validating session) → BOOT-02 (acquiring location) → LIVE; the position marker renders on the Rams basemap.
  • Permission persistence: close the app fully, reopen — no second permission prompt. The grant lives in Telegram's per-bot store on the device, outside the WebView. Observed / Symptom: ______________________________________

2. Android stale-fix check (retires or confirms assumption A1)

  • With the app LIVE, walk several metres and re-check. If the pin does not move, toggle the phone's Location setting once (off → on) and record whether navigator.geolocation then delivered a fresh fix, and whether the FIRST Android verification needed that toggle. Result decides: A1 stands (toggle required) or A1 retired (no toggle needed). Observed / Symptom (needed toggle? y/n): ______________________________________

3. iOS — real phone, Telegram iOS (if available)

  • Boot to LIVE; marker renders; permission grant persists across a reopen. Observed / Symptom: ______________________________________ (If no iOS device is available: record UNVERIFIED — no device; Success Criterion 5 stays open. Do not close the phase anyway.)

4. Desktop Telegram (GEO-02 end-to-end)

  • Geolocation is absent by design (issue #77): the app shows the denied card with identical copy across all clients (D-06, RESEARCH Pitfall 4) — no client-specific variant.
  • Tap ASK VIA CHAT → POSTs /api/location-fallback; the location keyboard appears in the user's own chat; tap it, share a live pin; the app picks it up via GET /api/location (within its poll window) and reaches LIVE. This is GEO-02 end-to-end. Observed / Symptom: ______________________________________

5. Plain browser (no Telegram context)

  • Open the deployed URL directly in a desktop browser. Expected: the denied card renders over the Rams shell (no initData → the HMAC session gate 401s), NOT a blank page, NOT the live branch. A blank page is a FAIL. (Optional extra observation: if a browser geolocation prompt leads to LIVE, note it — but the pass condition for this leg is the deliberate denied state.) Observed / Symptom: ______________________________________

6. Theme flip mid-session (PX-02 — manual-only verification)

  • With the app LIVE on a real client, switch the Telegram theme light ↔ dark mid-session without closing the app. Confirm: (a) shell chrome re-slots (document background, shell ink swap); (b) the status strip stays matte-black; (c) the map is unchanged — same style, no flash, no camera jump (D-10 / RESEARCH Pitfall 7). Record a before/after screenshot in each direction and name the client used (this leg can be folded into any of legs 1/3/4). Verdict (chrome re-slotted? strip stayed black? map unchanged?): ______________________________________

How to report back

  • Type "device matrix passed" if all six legs reached LIVE or the documented denied-and-recovered state (the theme-flip leg counts as passed when the chrome re-slotted mid-session and the map was visibly unchanged).
  • Otherwise report which leg failed and the exact symptom — a /gsd-debug gets opened for it.
  • If no physical Android/iOS device is available: report that. The honest outcome is Success Criterion 5 = UNVERIFIED; the phase is held open with the blocker recorded and either a device acquired or the matrix explicitly deferred to Phase 7 hardening with GEO-01/GEO-02 noted as unverified on real hardware. Never close the phase claiming device verification that did not happen.

Local-dev note (D-12 / assumption A3)

localhost:8787 (via root npm run dev → wrangler dev --port 8787 --local) is a secure context, so navigator.geolocation works under wrangler dev while a LAN-IP dev URL would not. Remote staging (the deployed origin above) is used sparingly, immediately before a real-device verification session, for exactly that reason.

2026-10-23 debug round 1 (mid-gate failure fixes)

Reported failures + fixes (commit b1d8768, deploy a21c0816):

  1. No webview entry from the bot — root cause: phase 2 armed the menu button as type commands and phase 3 never armed a web_app entry. Fix: new bot/scripts/set-menu-button.mjs → setChatMenuButton type web_app, url = deployed origin (verified live via getChatMenuButton). ⚠ Client-cached: the menu surface needs an app restart / /-autocomplete to refresh (same quirk as phase 2).
  2. TRY AGAIN / ASK VIA CHAT dead on one card — root cause: only the first denied-card instance had listeners; the awaiting-pin variant is a second buildDeniedCard instance (03-03 recorded stub). Fix: wireDeniedActions wires BOTH instances in web/src/main.ts.
  3. Post-fix canaries: GET / 200, unsigned /api/session 401, webhook armed (/telegram/webhook, no last_error), menu button verified web_app.

Notes for the re-run: on plain Chrome (no Telegram), TRY AGAIN legitimately re-renders the denied card — session can never validate without initData there; the pass condition for leg 5 is the deliberate denied state itself. If Android still shows dead/absent retry interplay after OS location is enabled, open a follow-up.

2026-10-23 debug round 2 (no prompt / no on-device enable path)

Symptom (legs 1 + 5): denied card shows but no native prompt ever appears, and the card offers no way to actually enable location — TRY AGAIN instant re-fails when access was previously denied/services off.

Root cause: the D-06 card only offered chain re-run + chat fallback. When Telegram's LocationManager has a settings-resolvable denial (access denied earlier, or services off), the ONLY realistic recovery is Telegram's own permission pane — LocationManager.openSettings() — which was typed optional but never invoked anywhere.

Fix (commit after round 1, deploy bb92dbeb):

  • Denied cards gain a THIRD CTA — LOCATION SETTINGS / TETAPAN LOKASI (new strings key pair; COPY5 gates still green) — rendered on Telegram clients only (wa.LocationManager presence test).
  • Click → lm.openSettings() (gateway re-runs the chain too, covering clients without the API).
  • locationManagerUpdated event listener re-runs the chain the moment the user grants, instead of waiting for another tap.
  • UI-SPEC amendment: copy table + denied-CTA list append this third action (Telegram-only conditional); rationale recorded here per debug-round protocol.

Re-test instruction: restart the Telegram client (menu is cached), open the app from the menu button, on the denied card tap LOCATION SETTINGS → grant location in Telegram's pane → the app should reach LIVE without another tap. Legs 1/2/6 resume from there.

2026-10-23 debug round 3 (definitive production root cause)

Symptom: cards render but the app behaves as if there is NO Telegram at all — no initData (session always 401s), no LOCATION SETTINGS button, BackButton dead. Even the recenter/theme machinery could not function through real Telegram.

Root cause: web/index.html never loaded telegram-web-app.js. Telegram does NOT auto-inject window.Telegram (03-RESEARCH.md line ~687 made the opposite call — erratum logged in RESEARCH). Every production symptom traces to getWebApp() → null.

Fix (deploy 3c25dd32): <script src="https://telegram.org/js/telegram-web-app.js"> added to <head> before the module script. Builds + 33/33 gates green. Erratum recorded in 03-RESEARCH.md.

Re-test: restart Telegram client, reopen web app from the menu button — expect the REAL boot (BOT-01 → BOT-02 → LIVE with an actual permission ask or a live LocationManager read + LOCATION SETTINGS available on denial).

2026-10-23 debug round 4 (missing LOCATION SETTINGS on the main denied card)

Symptom: user still sees only TRY AGAIN / ASK VIA CHAT — no LOCATION SETTINGS.

Root cause: executor patch-order bug — round 2 attached withLocationSettings only to the awaiting-pin card instance; the primary denied card (the one shown at denial) never got the CTA.

Fix (deploy 253d7a3e): both denied-shaped instances now build with the Telegram-only settings CTA. Deployed index.html verified to carry telegram-web-app.js (round 3 fix verified live via curl).

2026-10-23 debug round 5 (buttons render but all appear dead)

Symptom: LOCATION SETTINGS now visible (round 3 confirmed — the app IS served and parsed), but all three CTAs appear to do nothing; strip shows LOCATION OFF.

Working hypothesis: the user may be viewing the app OUTSIDE a real Telegram webview (plain Chrome address bar). With round 3's script tag, window.Telegram.WebApp now exists even off-Telegram (SDK defines it unconditionally), so LOCATION SETTINGS renders — but initData is empty there → every session POST 401s → TRY AGAIN/ASK VIA CHAT both bounce back to denied instantly (visually indistinguishable). openSettings() no-ops off-Telegram.

Fix (deploy 7cbb73f9, commit f445399): on-screen DIAGNOSTIC PROBE, bottom-left, category-only: WA <version>/<platform> · q <initData-chars> · s <session-category> · loc <state>. No payload, no coordinates, no ids — category-only law (D-14); removed at the device-matrix exit. Expected readings:

  • real Telegram webview: WA 8.0+/android|ios (not none), q > 0
  • plain browser: WA unknown/unknown · q 0ch · s malformed/tampered

Re-test: the probe line is what to report, verbatim, in the next message.

2026-10-23 debug round 6 (DEFINITIVE — s tampered on a real webview)

Probe reading: WA ... · s tampered · loc denied — the session gate was rejecting the REAL initData (empty-initData theories were wrong; the webview is live and Telegram-native).

Root cause: bot/src/security/initdata.ts excluded the Ed25519 signature field from the HMAC data-check-string. Per the official spec (core.telegram.org/bots/webapps §Validating data received via the Mini App), the bot's own HMAC check excludes only hash; the signature exclusion belongs to THIRD-PARTY validation. Modern webviews append signature, so every real client failed as "tampered" while all signature-less suite vectors passed.

Fix (deploy bedc8126, 50 bot tests incl. new T2b signature-field regression vectors): HMAC dcs = fields minus hash only, in initdata.ts + both test-side signing helpers. Erratum header updated.

Live post-fix proof: a minted initData WITH a signature field → 200 at the deployed origin (pre-fix equivalent → 401/tampered).

Re-test: app should now validate → chain runs → permission ask / LocationManager read → LIVE. Probe should read s ok · loc live on success.

2026-10-23 round 7 — probe readings (legs largely passing)

  • Android Telegram: s ok, app live — leg 1 core WORKS.
  • Telegram Desktop: s ok · loc live — tdesktop webview delivered a real geolocation fix natively (better than the assumed issue #77 denial class); GEO-02's denied path remains provable on the plain-browser leg / clients where geolocation is unavailable.
  • Plain Chrome: WA 6.0/unknown · q 0ch · s malformed · loc denied — the DESIGNED off-Telegram state; leg 5's pass condition (denial is deliberate, not blank). No webview can work without Telegram identity — correct.

Cosmetic fix (19a51e5, deploy 1a6947ef): LOCATION SETTINGS was visible off-Telegram (SDK stub defines LocationManager and reports 6.0) where openSettings() is a no-op. CTA eligibility now shares the chain's isVersionAtLeast("8.0") gate → the button hides on unsupported clients.

Remaining for the exit: Android boot-state choreography + reopen (no second prompt), Android stale-fix walk (A1), iOS (device permitting), mid-session theme flip leg, then verdicts on all six legs + probe removal.


FINAL VERDICT — 2026-10-23: PASSED (user: "device matrix passed")

Leg Verdict
0. BotFather origin prerequisite ✅ workers.dev origin
1. Android boot + persistence ✅ LIVE, no second prompt
2. Android stale-fix (A1) ✅ per user pass; A1 notes stand in 03-RESEARCH.md
3. iOS ⚠ UNVERIFIED — no device (honest; Phase 7 hardening item)
4. Desktop Telegram ✅ native fix (s ok · loc live) — issue #77 denial class not hit on this client; chat-KB route remains test-gated
5. Plain browser ✅ deliberate denied state — evidence: leg5-plain-browser-evidence.png
6. Theme flip mid-session ✅ per user verdict

Probe removed at exit (commit fc99745, deploy dc0307ea). Verification: 03-VERIFICATION.md.


2026-10-25 — Phase 7 debt closure (/cari re-arm + A1/A3)

D-07 items 2 and 3 (plan 07-05, task 3). The historical verdict table above is the Phase-3 record and is untouched; this dated section is the closure.

A — the /cari re-arm attempt

Ran npm run bot:commands (bot/scripts/register-commands.mjs, BOT_TOKEN from gitignored bot/.dev.vars) on 2026-10-25. Every scope reported ok:true:

default (en)                   -> ok:true
default (ms)                   -> ok:true
all_private_chats (en)         -> ok:true
all_private_chats (ms)         -> ok:true
clear all_group_chats          -> ok:true
clear all_chat_administrators  -> ok:true
all command scopes armed

Residual risk (unchanged by a successful re-arm): Telegram clients cache the command menu — a driver whose app cached the stale non-default scope may still see /cari missing until the client restarts or re-syncs. The defect is Telegram's setMyCommands scope-resolution behaviour (a chat's menu resolves from the narrowest scope that has ANY commands set), not a bot bug; the command itself has always worked. The user-facing statement plus the workaround (typing /cari) now live in docs/KNOWN-LIMITATIONS.md (D-08, plan 07-05).

B — the A1/A3 assumption closure

  • A1 (Android stale-fix hypothesis) is NOT RETIRED as a mechanism hypothesis — the Phase-3 walk completed WITHOUT needing the Location toggle on the tested device and route ("A1 notes stand in 03-RESEARCH.md"), so the hypothesis was simply not exercised, not disproven. The risk it covers is MITIGATED-BY-DESIGN: the app's location chain degrades gracefully to polling when the stale-fix path does not fire (web/src/location/chain.ts), so a stale native cache cannot strand guidance.
  • A3 IS RETIRED as a decision constraint — the project deploys to a remote origin (araharah-bot.<account>.workers.dev), never to a LAN-IP dev server, so the "localhost:8787 is a secure context" assumption no longer constrains any open decision; wrangler dev remains a local convenience, not a deployment surface.