Compare commits

..
10 Commits
10 changed files with 218 additions and 72 deletions
@@ -132,3 +132,11 @@ Success Criterion 5 remains open until the user reports results.
**Resume signal:** "device matrix passed", a per-leg failure report, or a **Resume signal:** "device matrix passed", a per-leg failure report, or a
no-device-available report (→ Success Criterion 5 = UNVERIFIED; recommend Phase-7 no-device-available report (→ Success Criterion 5 = UNVERIFIED; recommend Phase-7
deferral with GEO-01/GEO-02 noted unverified on hardware). deferral with GEO-01/GEO-02 noted unverified on hardware).
## Checkpoint resolution (orchestrator, 2026-10-23)
Task 3 (device-matrix) resolved **PASSED** by the user after debug rounds 1–6
(see 03-DEVICE-MATRIX.md). Notable in-gate fixes: telegram-web-app.js
integration (round 3), HMAC signature-field erratum (round 6), web_app menu
button + settings CTA (rounds 1–4). iOS leg recorded UNVERIFIED — no device.
Verification: 03-VERIFICATION.md — phase 3 PASSED.
@@ -183,3 +183,88 @@ denied card (the one shown at denial) never got the CTA.
**Fix (deploy `253d7a3e`):** both denied-shaped instances now build with the **Fix (deploy `253d7a3e`):** both denied-shaped instances now build with the
Telegram-only settings CTA. Deployed `index.html` verified to carry Telegram-only settings CTA. Deployed `index.html` verified to carry
telegram-web-app.js (round 3 fix verified live via curl). 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.
@@ -0,0 +1,57 @@
# Phase 3: Mini App Shell + Geolocation — Verification
**Verified:** 2026-10-23
**Method:** goal-backward against ROADMAP §Phase 3 success criteria, after in-execution debug rounds 1–6 (see 03-DEVICE-MATRIX.md) and the user's final device-matrix verdict ("device matrix passed").
## Success Criteria
### SC1 (GEO-01) — Android: grant, live position on Rams basemap, permission persists — ✅ PASS
- Real Android phone, Telegram webview: boot reaches LIVE, `s ok` (probe round 7), position marker on the Rams basemap.
- Probe round 6 root-caused the last blocker: the server's HMAC validator excluded the Ed25519 `signature` field from the data-check-string (spec: exclude ONLY `hash` for the bot's own check). Fixed + regression-tested (T2b vectors, 50 bot tests) + live-verified (signed-with-signature initData → 200).
- User-confirmed reopen behavior: no second permission prompt.
### SC2 (GEO-02) — fallback chain recovers origin — ✅ PASS (with a recorded upgrade)
- Telegram Desktop delivered a **native** geolocation fix (`s ok · loc live`) — stronger than the designed denial-class fallback (research's issue-#77 assumption retired on that client).
- The chat-keyboard fallback route is fully implemented and gated: `POST /api/location-fallback` (claim-scoped), `GET /api/location` relay, 60s bounded client poll, `?pin=1` harness capture — 48 bot + 33 web tests green. Not exercised live on-device because no tested client landed in the denied class during the matrix; the plain-browser leg proves the deliberate denied surface.
### SC3 (PX-02) — deliberate denied state, themes, Rams-constant map — ✅ PASS
- Plain-browser leg evidence: `leg5-plain-browser-evidence.png` (real-browser capture at the deployed origin) — denied card over the Rams shell, red LOCATION OFF lamp, no blank, no off-Telegram settings CTA (version gate `19a51e5`).
- 7/7 capture suite includes light + dark live shots; theme-slot machine tested (PX02 gates); copy en+ms verbatim from the UI-SPEC (COPY1–6 gates).
### SC4 (GEO-03) — no coordinate persists — ✅ PASS
- Automated runtime probe: localStorage/sessionStorage/cookies/IndexedDB all empty after a real boot-to-live cycle (03-04 capture harness).
- Session token memory-only (D-03/D-07); KV namespaces TTL-only (PG1/PG2 gates from 03-01).
### SC5 — device-matrix exit pass — ✅ PASS (user verdict, 2026-10-23)
- Android ✅ · Telegram Desktop ✅ (native fix) · plain browser ✅ (denied by design) · theme flip ✅ (per user verdict) · iOS — not available: **UNVERIFIED — no device** (recorded honestly per the matrix protocol; does not block v1 KL-metro scope, flagged for Phase 7 hardening).
## Debug rounds during execution (summary)
| Round | Symptom | Root cause | Fix |
|---|---|---|---|
| 1 | No webview entry; dead CTAs on awaiting card | Menu button was `commands` (phase 2); wiring only on first denied instance | `set-menu-button.mjs` (web_app); wire both instances |
| 2 | No prompt / no enable path | `openSettings()` never invoked | LOCATION SETTINGS CTA + `locationManagerUpdated` re-run |
| 3 | App behaves as off-Telegram everywhere | `telegram-web-app.js` never loaded (research erratum) | Script tag in `<head>` |
| 4 | Settings CTA missing on main denied card | Patch attached to wrong instance | Both instances |
| 5 | Buttons "dead" | Diagnostic probe added (removed at exit) | On-screen category-only probe |
| 6 | `s tampered` on real clients | HMAC dcs excluded `signature` (spec: hash only) | Validator fix + T2b regressions + live 200 proof |
| 7 | Probe readings decoded; CTA visible off-Telegram | SDK stub defines LocationManager at v6.0 | CTA eligibility = chain's 8.0 version gate |
## Requirements coverage
- **GEO-01** ✅ · **GEO-02** ✅ (native-desktop upgrade recorded) · **GEO-03** ✅ · **PX-02** ✅
## Verdict
**PHASE 3: PASSED** — 4/4 plans executed; 5/5 success criteria met (iOS leg honestly UNVERIFIED, no device). Phase 4 (destination-search-+-kl-gazetteer) is next.
## Exit addendum (2026-10-23, post-verdict)
- Diagnostic probe removed (commit `fc99745`, deploy `dc0307ea`) — the probe's
exit obligation; final capture suite 7/7 green without it.
- GEO-03 runtime probe updated: the official `telegram-web-app.js` (debug
round 3) writes exactly one sessionStorage key (`__telegram__initParams`,
SDK launch bookkeeping, no coordinates). The probe now allowlists that
single platform key and still fails on ANY other write (`8a5c8b4`).
Our app's own surfaces remain byte-empty — the D-07 law holds.
- Final gates at close: web 33/33 · bot 50/50 · captures 7/7 · theme audit clean.
+11 -3
View File
@@ -10,8 +10,14 @@
* RED suite's node:crypto signing path, RFC-4231-pinned): * RED suite's node:crypto signing path, RFC-4231-pinned):
* 1. parse `raw` as a Telegram-style querystring (values URL-decoded ONCE; * 1. parse `raw` as a Telegram-style querystring (values URL-decoded ONCE;
* `+` stays literal — do NOT use URLSearchParams) * `+` stays literal — do NOT use URLSearchParams)
* 2. dataCheckString = fields minus `hash`/`signature`, sorted by FIELD * 2. dataCheckString = fields minus `hash` ONLY, sorted by FIELD
* NAME (byte order, JS default sort), `key=value`, joined with "\n" * NAME (byte order, JS default sort), `key=value`, joined with "\n"
* (ERRATUM 2026-10-23, device-matrix round 6: the HMAC path must NOT
* exclude `signature` — per core.telegram.org/bots/webapps the bot's
* own check excludes only `hash`; the signature exclusion belongs to
* THIRD-PARTY Ed25519 validation. Real webviews append `signature`,
* and excluding it produced "tampered" rejections on live clients
* while every synthetic suite vector — signature-less — passed.)
* 3. secretKey bytes = HMAC-SHA256(message = utf8(botToken), * 3. secretKey bytes = HMAC-SHA256(message = utf8(botToken),
* key = utf8(the salt constant, line 179)) * key = utf8(the salt constant, line 179))
* checkHex = hex(HMAC-SHA256(message = utf8(dataCheckString), * checkHex = hex(HMAC-SHA256(message = utf8(dataCheckString),
@@ -168,9 +174,11 @@ export async function validateInitData(
return { ok: false, reason: "malformed" as const }; return { ok: false, reason: "malformed" as const };
} }
// — data-check-string: hash/signature excluded, sorted by field name — // — data-check-string: hash excluded ONLY (signature participates — the
// HMAC spec excludes just `hash`; excluding it broke real clients,
// device-matrix round 6) — sorted by field name —
const dataCheckString = [...fields.entries()] const dataCheckString = [...fields.entries()]
.filter(([key]) => key !== "hash" && key !== "signature") .filter(([key]) => key !== "hash")
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)) .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
.map(([key, value]) => `${key}=${value}`) .map(([key, value]) => `${key}=${value}`)
.join("\n"); .join("\n");
+41 -2
View File
@@ -52,10 +52,12 @@ function loadValidator() {
/* ————— node:crypto signing helpers (test-side, independent path) ————— */ /* ————— node:crypto signing helpers (test-side, independent path) ————— */
/** Data-check-string: fields sorted by key, key=value, LF-joined (Telegram spec). */ /** Data-check-string: fields sorted by key, key=value, LF-joined (Telegram
* spec — ALL fields except `hash`; the signature field participates in the
* bot's own HMAC check, device-matrix round 6 erratum). */
function dataCheckString(fields) { function dataCheckString(fields) {
return fields return fields
.filter(([key]) => key !== "hash" && key !== "signature") .filter(([key]) => key !== "hash")
.slice() .slice()
.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)) .sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
.map(([key, value]) => `${key}=${value}`) .map(([key, value]) => `${key}=${value}`)
@@ -104,6 +106,43 @@ test("T1 known-good signed initData → ok:true, user.id 42, languageCode ms", a
/* ————— T2 — flipped hash character → tampered ————— */ /* ————— T2 — flipped hash character → tampered ————— */
/* T2b — ERRATUM regression (device-matrix round 6): real webviews append the
* Ed25519 `signature` field; per the official HMAC spec the data-check-string
* excludes ONLY `hash`, so a signed body carrying `signature` must validate.
* The old implementation excluded `signature` from the HMAC string and
* rejected every live client as "tampered" while all signature-less suite
* vectors passed — the classic blind spot this test pins. */
test("T2b real-shaped initData WITH a signature field → ok:true", async () => {
const validateInitData = await loadValidator();
const now = Math.floor(Date.now() / 1000);
const raw = buildInitData([
["query_id", "AAH3xSample"],
["signature", "c2FtcGxlX3NpZ25hdHVyZV9icnVmX2Jhc2U2NA"],
["user", JSON.stringify({ id: 42, first_name: "Zul", language_code: "ms" })],
["auth_date", String(now)],
]);
const res = await validateInitData(raw, TEST_TOKEN);
assert.equal(res.ok, true, `expected ok:true with signature field, got ${JSON.stringify(res)}`);
});
test("T2b flipped hash with signature present → still tampered", async () => {
const validateInitData = await loadValidator();
const now = Math.floor(Date.now() / 1000);
const raw = buildInitData([
["query_id", "AAH3xSample"],
["signature", "c2FtcGxlX3NpZ25hdHVyZV9icnVmX2Jhc2U2NA"],
["user", JSON.stringify({ id: 42, first_name: "Zul", language_code: "ms" })],
["auth_date", String(now)],
]);
const match = /hash=[0-9a-f]{8}/.exec(raw);
assert.ok(match !== null);
const tampered = raw.slice(0, match.index) + `hash=${(createHmac("sha256", "seed").update("x").digest("hex"))}${raw.slice(match.index + "hash=".length + 64)}`;
const res = await validateInitData(tampered, TEST_TOKEN);
assert.equal(res.ok, false);
assert.equal(res.reason, "tampered");
});
test("T2 flipped hash character → reason tampered", async () => { test("T2 flipped hash character → reason tampered", async () => {
const validateInitData = await loadValidator(); const validateInitData = await loadValidator();
const now = Math.floor(Date.now() / 1000); const now = Math.floor(Date.now() / 1000);
+1 -1
View File
@@ -38,7 +38,7 @@ const TEST_TOKEN = "12345:TESTTOKEN"; // same signer token as initdata.test.mjs
function dataCheckString(fields) { function dataCheckString(fields) {
return fields return fields
.filter(([key]) => key !== "hash" && key !== "signature") .filter(([key]) => key !== "hash")
.slice() .slice()
.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)) .sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
.map(([key, value]) => `${key}=${value}`) .map(([key, value]) => `${key}=${value}`)
+11 -4
View File
@@ -290,17 +290,24 @@ try {
]); ]);
if (signal === 'timeout') throw new Error('GEO-03 probe leg: ARAHARAH-LIVE never arrived within 30000ms'); if (signal === 'timeout') throw new Error('GEO-03 probe leg: ARAHARAH-LIVE never arrived within 30000ms');
await page.waitForTimeout(1500); await page.waitForTimeout(1500);
/* Platform allowlist (device-matrix exit addendum): the OFFICIAL
* telegram-web-app.js (added in debug round 3) writes exactly one
* sessionStorage key — `__telegram__initParams`, the SDK's own launch
* bookkeeping ({}, or Telegram-supplied launch params). It is not app
* data, carries no coordinates, and the D-07 law binds OUR surfaces —
* which remain byte-empty. Any OTHER key still fails the probe. */
const probe = await page.evaluate(async () => ({ const probe = await page.evaluate(async () => ({
local: localStorage.length, local: localStorage.length,
session: sessionStorage.length, foreignSessionKeys: Object.keys(sessionStorage).filter((k) => k !== '__telegram__initParams'),
ssForeignOnly: Object.keys(sessionStorage).every((k) => k === '__telegram__initParams'),
cookie: document.cookie, cookie: document.cookie,
dbs: (await indexedDB.databases()).length, dbs: (await indexedDB.databases()).length,
lsKeys: Object.keys(localStorage), lsKeys: Object.keys(localStorage),
})); }));
if (probe.local !== 0 || probe.session !== 0 || probe.cookie !== "" || probe.dbs !== 0) { if (probe.local !== 0 || !probe.ssForeignOnly || probe.cookie !== "" || probe.dbs !== 0) {
failures.push(`GEO-03 runtime probe FAILED — localStorage=${probe.local} sessionStorage=${probe.session} cookie="${probe.cookie}" indexedDB=${probe.dbs} keys=${JSON.stringify(probe.lsKeys)}`); failures.push(`GEO-03 runtime probe FAILED — localStorage=${probe.local} foreignSessionKeys=${JSON.stringify(probe.foreignSessionKeys)} cookie="${probe.cookie}" indexedDB=${probe.dbs} keys=${JSON.stringify(probe.lsKeys)}`);
} else { } else {
log.push('GEO-03 runtime probe PASSED — localStorage/sessionStorage/cookies/IndexedDB all empty after boot-to-live'); log.push('GEO-03 runtime probe PASSED — app storage surfaces empty after boot-to-live (platform allowlist: SDK-owned __telegram__initParams only)');
} }
} catch (err) { } catch (err) {
failures.push(`GEO-03 runtime probe: ${err.message}`); failures.push(`GEO-03 runtime probe: ${err.message}`);
+3 -10
View File
@@ -28,7 +28,7 @@ import { createPositionMarker } from "./map/marker.ts";
import { createFollowController } from "./map/follow.ts"; import { createFollowController } from "./map/follow.ts";
import { createStatusStrip } from "./ui/strip.ts"; import { createStatusStrip } from "./ui/strip.ts";
import { buildBootCard, buildDeniedCard, buildSignalLostCard, startGantry, stopGantry } from "./ui/cards.ts"; import { buildBootCard, buildDeniedCard, buildSignalLostCard, startGantry, stopGantry } from "./ui/cards.ts";
import { createProbe } from "./ui/probe.ts"; import { telegramLocationSupported } from "./location/telegram.ts";
import { createRecenterControl } from "./ui/recenter.ts"; import { createRecenterControl } from "./ui/recenter.ts";
import { getWebApp } from "./telegram/webapp.ts"; import { getWebApp } from "./telegram/webapp.ts";
import { watchTheme } from "./telegram/theme.ts"; import { watchTheme } from "./telegram/theme.ts";
@@ -98,18 +98,13 @@ async function boot(): Promise<void> {
if (lastFix !== null && map !== null) follow?.follow(lastFix); if (lastFix !== null && map !== null) follow?.follow(lastFix);
}); });
// DEBUG-ROUND probe: category-only on-screen telemetry for the device
// matrix (webview has no dev-tools); removed at the exit.
const probe = createProbe(mainEl);
probe.set({ wa: wa ? `${wa.version ?? "?"}/${wa.platform ?? "?"}` : "none", initDataChars: wa ? wa.initData.length : null });
// Cards are built once at boot (boot-time locale) and reused across re-runs. // Cards are built once at boot (boot-time locale) and reused across re-runs.
const bootCard1 = buildBootCard(locale, 1); const bootCard1 = buildBootCard(locale, 1);
const bootCard2 = buildBootCard(locale, 2); const bootCard2 = buildBootCard(locale, 2);
const deniedCard = buildDeniedCard( const deniedCard = buildDeniedCard(
locale, locale,
// Debug round 2/3: Telegram-only deep-link via LocationManager.openSettings. // Debug round 2/3: Telegram-only deep-link via LocationManager.openSettings.
{ awaitingPin: false, withLocationSettings: wa?.LocationManager !== undefined }, { awaitingPin: false, withLocationSettings: telegramLocationSupported(wa) },
); );
const awaitingCard = buildDeniedCard( const awaitingCard = buildDeniedCard(
locale, locale,
@@ -117,7 +112,7 @@ async function boot(): Promise<void> {
// is the whole eligibility test — off-Telegram (Desktop/browser) there // is the whole eligibility test — off-Telegram (Desktop/browser) there
// is nothing for openSettings() to open, and navigator.geolocation // is nothing for openSettings() to open, and navigator.geolocation
// denial is handled by the OS settings hint copy instead. // denial is handled by the OS settings hint copy instead.
{ awaitingPin: true, withLocationSettings: wa?.LocationManager !== undefined }, { awaitingPin: true, withLocationSettings: telegramLocationSupported(wa) },
); );
const lostCard = buildSignalLostCard(locale); const lostCard = buildSignalLostCard(locale);
const gantryOf = (card: HTMLElement): HTMLElement => { const gantryOf = (card: HTMLElement): HTMLElement => {
@@ -188,7 +183,6 @@ async function boot(): Promise<void> {
} }
function renderState(state: LocationState): void { function renderState(state: LocationState): void {
probe.set({ location: state.kind });
const ageSec = lastFix !== null ? fixAgeSeconds(lastFix, Date.now()) : null; const ageSec = lastFix !== null ? fixAgeSeconds(lastFix, Date.now()) : null;
const accuracyM = lastFix !== null ? lastFix.accuracyM : null; const accuracyM = lastFix !== null ? lastFix.accuracyM : null;
strip.render(state, ageSec, accuracyM); strip.render(state, ageSec, accuracyM);
@@ -240,7 +234,6 @@ async function boot(): Promise<void> {
async function runFromSession(): Promise<void> { async function runFromSession(): Promise<void> {
renderState({ kind: "validating" }); renderState({ kind: "validating" });
const session = await createSession(wa?.initData ?? ""); const session = await createSession(wa?.initData ?? "");
probe.set({ session: session.ok ? "ok" : session.reason, initDataChars: wa ? wa.initData.length : null });
if (!session.ok) { if (!session.ok) {
staleHandled = false; // session-first failure is boot-level, not a stale-fix recovery staleHandled = false; // session-first failure is boot-level, not a stale-fix recovery
renderDenied(); renderDenied();
+1 -5
View File
@@ -91,8 +91,4 @@ main { position: fixed; inset: 0; }
.pos-group[data-stale="true"] { opacity: 0.4; } .pos-group[data-stale="true"] { opacity: 0.4; }
/* No CSS transition or animation anywhere — state changes are instant hard swaps. */ /* No CSS transition or animation anywhere — state changes are instant hard swaps. */
/* DEBUG-ROUND probe styles removed at the device-matrix exit (2026-10-23). */
/* DEBUG-ROUND probe — on-screen category-only telemetry for the device-matrix
loop (no dev-tools in a phone webview). Removed at the exit. Layout-only
positioning; the label typography is the existing label-sm token. */
.diag-probe { position: absolute; left: var(--spacing-margin); bottom: calc(var(--safe-bottom, 0px) + var(--spacing-space-sm)); font: var(--type-label-sm); letter-spacing: var(--type-label-sm-tracking); color: var(--color-soft-beige); z-index: 10; pointer-events: none; }
-47
View File
@@ -1,47 +0,0 @@
/*
* probe.ts — DEBUG-ROUND diagnostic line (phase 3 device-matrix troubleshooting).
* ----------------------------------------------------------------------------
* One category-only instrument line, fixed bottom-left: which WebApp the app
* sees (version/platform), the character-count of the raw initData (a COUNT,
* never its content), the last session-gate category, and the location-chain
* category. No coordinates, no payload, no ids — same law as the Worker's
* category-only logs (D-14), only surfaced on-screen because a phone
* webview has no dev-tools access during the device-matrix pass.
*
* Rendering uses only machine tokens (letters, digits, separators) — no
* human copy, so no strings-module entry and no COPY5 concern. The probe is
* expected to be REMOVED or reduced at the device-matrix exit.
*/
export interface ProbeParts {
wa: string;
initDataChars: number | null;
session: string;
location: string;
}
export interface Probe {
el: HTMLElement;
set(parts: Partial<ProbeParts>): void;
}
export function createProbe(parent: HTMLElement): Probe {
const el = document.createElement("div");
el.className = "diag-probe";
el.setAttribute("aria-hidden", "true");
const parts: ProbeParts = { wa: "-", initDataChars: null, session: "-", location: "-" };
const state = { parts };
function render(): void {
const p = state.parts;
const q = p.initDataChars === null ? "-" : `${p.initDataChars}ch`;
el.textContent = `WA ${p.wa} · q ${q} · s ${p.session} · loc ${p.location}`;
}
parent.appendChild(el);
render();
return {
el,
set(delta: Partial<ProbeParts>): void {
Object.assign(state.parts, delta);
render();
},
};
}