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:
@@ -657,7 +657,11 @@ async function captureLeg({ name, query, colorScheme, grantGeo, waitMode, expect
|
||||
} catch (err) {
|
||||
failures.push(`${name}: ${err.message}`);
|
||||
} finally {
|
||||
try {
|
||||
await context.close();
|
||||
} catch {
|
||||
// teardown race (context already disposed) — the capture itself was judged above
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user