fix(06-08): device-gate round 1 — destination card gains ROUTE forward action; document title mirrors Telegram header (arah² → Routing to <name>); capture teardown race guard

This commit is contained in:
araharah-executor
2026-10-25 00:58:27 +08:00
parent e87b9c7fa6
commit a69fe31d86
4 changed files with 69 additions and 5 deletions
+30 -2
View File
@@ -39,7 +39,7 @@ import { getSession, isSessionStale } from "./session/store.ts";
import { acquireFix, isFixStale, fixAgeSeconds } from "./location/chain.ts";
import { requestChatFallback } from "./location/chat-pin.ts";
import type { Fix, LocationState } from "./location/types.ts";
import { localeFromInitData, type Locale } from "./i18n/strings.ts";
import { localeFromInitData, strings, 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";
@@ -201,7 +201,7 @@ async function boot(): Promise<void> {
// (D-12's startapp-less boot landing in the search-ready map is UNCHANGED).
const destinationCard =
destination !== null
? buildDestinationCard(locale, destination)
? buildDestinationCard(locale, destination, { withRouteAction: true })
: destPayload.id !== null
? buildDestinationUnavailableCard(locale)
: null;
@@ -219,6 +219,18 @@ async function boot(): Promise<void> {
// check on the rendered card's text.
console.log("ARAHARAH-DEST state=" + (destination ? "resolved" : "absent"));
// The document title mirrors the Telegram header (device-gate feedback,
// 2026-10-25): a resolved destination re-titles the app "arah² → Routing to
// <name>" (en/ms via the strings module — the NAME is public data, no
// coordinates, D-12/D-15); the root state restores the bare brand. The
// title element always exists (index.html's <title> is the base).
const titleEl = document.querySelector("title");
if (titleEl !== null) {
titleEl.textContent = destination !== null
? strings[locale].titleRoute.replace("{name}", destination.name)
: "araharah";
}
const recenter = createRecenterControl(mainEl, locale, () => {
if (lastFix !== null && map !== null) {
if (routeController !== null && routeController.guidancePhase === "driving") {
@@ -490,6 +502,22 @@ async function boot(): Promise<void> {
void tryResume(resumeId, routeController);
}
// Device-gate fix (2026-10-24): the destination card's ROUTE button — the
// forward action SEARCH AGAIN's destination state was missing (auto-consult
// fires exactly once per boot; a user walking back was stranded). Re-consults
// with the latest fix + the resolved destination; no fix yet = the honest
// boot continues (the chain is already running or will render its own state).
const routeButton = destinationCard?.querySelector('[data-role="route-to-destination"]');
if (routeButton instanceof HTMLButtonElement && routeController !== null && destination !== null) {
const controller = routeController;
const dest = destination;
routeButton.addEventListener("click", () => {
const fix = lastFix;
if (fix === null) return; // no position yet — the chain's own surfaces own this state
void controller.consult(fix, dest);
});
}
/** GET the ?g= envelope and resume: rebuild the model from the frozen
* snapshot (decode + cumulative + destination fields), hand the RAW
* anchors to the controller — it owns the projection. A failed,