feat(04-04): ONBD-03 home-screen hooks + the boot wiring + the ?dest= harness profile

- homescreen.ts: wireHomeScreen behind the BOTH-halves Bot API 8.0
  guard; 'added' hides (no nag), 'unknown' (always iOS) shows the
  neutral prompt — indeterminate, never an error; homeScreenAdded
  hides; a refused addToHomeScreen is caught, the affordance stays;
  .btn-secondary in the existing strip — no new furniture; NO
  beforeinstallprompt/manifest/service worker (T-04-38)
- main.ts: locale derivation moved above the bindings block; stage 13
  reads BOTH destination carriers, seeds the memory-only holder, builds
  the destination/unavailable surface (null when no id — Phase 3 boot
  byte-identical, D-12); the live register shows the destination
  surface; the ARAHARAH-DEST state-token signal fires unconditionally
- index.html: ?dest=<id> harness profile threaded to the mock's
  startParam; the block stays inside __ARAHARAH_HARNESS__
- HS1-HS9 green (54/54), web:typecheck green
This commit is contained in:
araharah-executor
2026-10-24 01:36:30 +08:00
parent 6e421e6b59
commit c2d78b0c8f
3 changed files with 140 additions and 7 deletions
+53 -5
View File
@@ -41,6 +41,9 @@ import { requestChatFallback } from "./location/chat-pin.ts";
import type { Fix, LocationState } from "./location/types.ts";
import { localeFromInitData, type Locale } from "./i18n/strings.ts";
import { installMockWebApp } from "./mock/telegram-mock.ts";
import { readDestinationId, resolveDestination, setDestination } from "./search/resolve-destination.ts";
import { buildDestinationCard, buildDestinationUnavailableCard } from "./search/destination-card.ts";
import { wireHomeScreen } from "./telegram/homescreen.ts";
declare global {
interface Window {
@@ -60,8 +63,15 @@ async function boot(): Promise<void> {
// single-shell predicate for the no-dead-ends wire.
const overlayPresent = (): boolean => false; // Phase 3: single shell state (UI-SPEC BackButton contract)
// 2 — Telegram bindings: theme slot switch + BackButton total wire.
// 12 — the locale comes from the RAW initData string (the session POST
// returns only {token} — the claim's language_code is not reachable
// client-side). Derived BEFORE the Telegram-bindings block so the
// home-screen wire (ONBD-03) receives it; the strip consumes it below
// exactly where it always has.
const wa = getWebApp();
const locale: Locale = wa ? localeFromInitData(wa.initData) : "en";
// 2 — Telegram bindings: theme slot switch + BackButton total wire.
if (wa) {
watchTheme(wa);
wireBackButton(wa, overlayPresent);
@@ -89,11 +99,45 @@ async function boot(): Promise<void> {
stack.className = "card-stack";
mainEl.appendChild(stack);
// 12 — locale comes from the RAW initData string (the session POST returns
// only {token} — the claim's language_code is not reachable client-side).
const locale: Locale = wa ? localeFromInitData(wa.initData) : "en";
// 12 — the strip consumes the locale derived above the bindings block.
strip.setLocale(locale);
// 13 — the destination (ONBD-02). TWO carriers, ONE validation (mechanism
// corrected per RESEARCH Pitfall 1): the URL's own query string first
// (the inline-button path — a web_app button's WebAppInfo carries only
// a `url`), then initData's start_param (the documented direct-link /
// home-screen path). An unknown id yields the honest unavailable state
// and the boot continues into the normal staged boot — never a blank
// map. The id resolves against the webview's OWN build-time copy of
// the shared gazetteer, and the result is held in MEMORY ONLY
// (GEO-03/D-07) — seeded once here, no persistence anywhere.
const destPayload = readDestinationId(window.location.search, wa?.initData ?? null);
const destination = resolveDestination(destPayload.id);
setDestination(destination);
// The destination surface — the confirmation card when the id resolved, the
// honest unavailable card when an id ARRIVED and resolved to nothing, and
// null when no id arrived at all: the Phase 3 boot is then byte-identical
// (D-12's startapp-less boot landing in the search-ready map is UNCHANGED).
const destinationCard =
destination !== null
? buildDestinationCard(locale, destination)
: destPayload.id !== null
? buildDestinationUnavailableCard(locale)
: null;
// ONBD-03 — the home-screen affordance behind the Bot API 8.0 capability
// guard. Wired after the strip exists (it hosts the affordance) and after
// the locale is known. Telegram-native methods only — no browser install
// APIs exist in the webview (T-04-38).
if (wa) wireHomeScreen(wa, locale);
// The destination capture signal — emitted UNCONDITIONALLY (outside any map
// guard, the same shape as the ARAHARAH-LIVE token): a WebGL2-absent device
// still resolves the race. A STATE TOKEN, never the id (D-15 hygiene: a
// console line carries no place data); the capture leg pairs it with a DOM
// check on the rendered card's text.
console.log("ARAHARAH-DEST state=" + (destination ? "resolved" : "absent"));
const recenter = createRecenterControl(mainEl, locale, () => {
if (lastFix !== null && map !== null) follow?.follow(lastFix);
});
@@ -196,7 +240,11 @@ async function boot(): Promise<void> {
startGantry(gantryOf(bootCard2));
break;
case "live":
showCard(null);
// With a preloaded destination the destination surface IS the live
// state's card (the visible confirmation); with an id that arrived
// but resolved to nothing, the honest unavailable card holds the
// register. No id → null → the stack hides exactly as Phase 3 left it.
showCard(destinationCard);
break;
case "stale":
showCard(lostCard, "bottom");