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
+5 -1
View File
@@ -657,7 +657,11 @@ async function captureLeg({ name, query, colorScheme, grantGeo, waitMode, expect
} catch (err) {
failures.push(`${name}: ${err.message}`);
} finally {
await context.close();
try {
await context.close();
} catch {
// teardown race (context already disposed) — the capture itself was judged above
}
}
}
+14
View File
@@ -101,6 +101,14 @@ export interface ShellStrings {
ariaBannerToggle: string;
/** The guidance card's progress bar label. */
ariaTripProgress: string;
/** The destination card's forward action: re-trigger routing (device-gate fix:
* SEARCH AGAIN's destination state was terminal-with-no-forward). */
ctaRoute: string;
/** The route button's aria label. */
ariaRouteButton: string;
/** The document title while a destination is resolved — Telegram's header
* mirrors it (device-gate feedback: "arah² → Routing to <name>"). */
titleRoute: string;
}
export type Locale = "en" | "ms";
@@ -169,6 +177,9 @@ export const strings: Record<Locale, ShellStrings> = {
ariaManeuverList: "Full maneuver list",
ariaBannerToggle: "Next maneuver",
ariaTripProgress: "Trip progress",
ctaRoute: "ROUTE",
ariaRouteButton: "Route to this destination",
titleRoute: "arah² → Routing to {name}",
},
ms: {
bootValidatingRegister: "BOOT-01 // SESSION",
@@ -233,6 +244,9 @@ export const strings: Record<Locale, ShellStrings> = {
ariaManeuverList: "Senarai penuh manuver",
ariaBannerToggle: "Manuver seterusnya",
ariaTripProgress: "Kemajuan perjalanan",
ctaRoute: "LALUI",
ariaRouteButton: "Lalui ke destinasi ini",
titleRoute: "arah² → Perjalanan ke {name}",
},
};
+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,
+20 -2
View File
@@ -21,8 +21,13 @@ import type { GazetteerEntry } from "@araharah/search/src/types.ts";
import { strings, type Locale } from "../i18n/strings.ts";
/** The destination confirmation card: register line, name headline, the
* curated address verbatim. */
export function buildDestinationCard(locale: Locale, entry: GazetteerEntry): HTMLElement {
* curated address verbatim — and (device-gate fix, 2026-10-24) a forward
* ROUTE action: SEARCH AGAIN correctly lands here, but the auto-consult
* fires exactly once per boot, so the destination state was terminal with
* no way back to the route. The button re-triggers consult with the
* latest fix; wiring lives in main.ts (the card knows nothing of the
* controller). */
export function buildDestinationCard(locale: Locale, entry: GazetteerEntry, opts: { withRouteAction?: boolean } = {}): HTMLElement {
const card = document.createElement("div");
card.className = "card";
@@ -44,6 +49,19 @@ export function buildDestinationCard(locale: Locale, entry: GazetteerEntry): HTM
card.appendChild(header);
card.appendChild(headline);
card.appendChild(address);
if (opts.withRouteAction) {
const actions = document.createElement("div");
actions.className = "card-actions";
const routeButton = document.createElement("button");
routeButton.type = "button";
routeButton.className = "btn-primary";
routeButton.dataset.role = "route-to-destination";
routeButton.textContent = strings[locale].ctaRoute;
routeButton.setAttribute("aria-label", strings[locale].ariaRouteButton);
actions.appendChild(routeButton);
card.appendChild(actions);
}
return card;
}