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) {
|
} catch (err) {
|
||||||
failures.push(`${name}: ${err.message}`);
|
failures.push(`${name}: ${err.message}`);
|
||||||
} finally {
|
} finally {
|
||||||
|
try {
|
||||||
await context.close();
|
await context.close();
|
||||||
|
} catch {
|
||||||
|
// teardown race (context already disposed) — the capture itself was judged above
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -101,6 +101,14 @@ export interface ShellStrings {
|
|||||||
ariaBannerToggle: string;
|
ariaBannerToggle: string;
|
||||||
/** The guidance card's progress bar label. */
|
/** The guidance card's progress bar label. */
|
||||||
ariaTripProgress: string;
|
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";
|
export type Locale = "en" | "ms";
|
||||||
@@ -169,6 +177,9 @@ export const strings: Record<Locale, ShellStrings> = {
|
|||||||
ariaManeuverList: "Full maneuver list",
|
ariaManeuverList: "Full maneuver list",
|
||||||
ariaBannerToggle: "Next maneuver",
|
ariaBannerToggle: "Next maneuver",
|
||||||
ariaTripProgress: "Trip progress",
|
ariaTripProgress: "Trip progress",
|
||||||
|
ctaRoute: "ROUTE",
|
||||||
|
ariaRouteButton: "Route to this destination",
|
||||||
|
titleRoute: "arah² → Routing to {name}",
|
||||||
},
|
},
|
||||||
ms: {
|
ms: {
|
||||||
bootValidatingRegister: "BOOT-01 // SESSION",
|
bootValidatingRegister: "BOOT-01 // SESSION",
|
||||||
@@ -233,6 +244,9 @@ export const strings: Record<Locale, ShellStrings> = {
|
|||||||
ariaManeuverList: "Senarai penuh manuver",
|
ariaManeuverList: "Senarai penuh manuver",
|
||||||
ariaBannerToggle: "Manuver seterusnya",
|
ariaBannerToggle: "Manuver seterusnya",
|
||||||
ariaTripProgress: "Kemajuan perjalanan",
|
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 { acquireFix, isFixStale, fixAgeSeconds } from "./location/chain.ts";
|
||||||
import { requestChatFallback } from "./location/chat-pin.ts";
|
import { requestChatFallback } from "./location/chat-pin.ts";
|
||||||
import type { Fix, LocationState } from "./location/types.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 { installMockWebApp } from "./mock/telegram-mock.ts";
|
||||||
import { readDestinationId, resolveDestination, setDestination } from "./search/resolve-destination.ts";
|
import { readDestinationId, resolveDestination, setDestination } from "./search/resolve-destination.ts";
|
||||||
import { buildDestinationCard, buildDestinationUnavailableCard } from "./search/destination-card.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).
|
// (D-12's startapp-less boot landing in the search-ready map is UNCHANGED).
|
||||||
const destinationCard =
|
const destinationCard =
|
||||||
destination !== null
|
destination !== null
|
||||||
? buildDestinationCard(locale, destination)
|
? buildDestinationCard(locale, destination, { withRouteAction: true })
|
||||||
: destPayload.id !== null
|
: destPayload.id !== null
|
||||||
? buildDestinationUnavailableCard(locale)
|
? buildDestinationUnavailableCard(locale)
|
||||||
: null;
|
: null;
|
||||||
@@ -219,6 +219,18 @@ async function boot(): Promise<void> {
|
|||||||
// check on the rendered card's text.
|
// check on the rendered card's text.
|
||||||
console.log("ARAHARAH-DEST state=" + (destination ? "resolved" : "absent"));
|
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, () => {
|
const recenter = createRecenterControl(mainEl, locale, () => {
|
||||||
if (lastFix !== null && map !== null) {
|
if (lastFix !== null && map !== null) {
|
||||||
if (routeController !== null && routeController.guidancePhase === "driving") {
|
if (routeController !== null && routeController.guidancePhase === "driving") {
|
||||||
@@ -490,6 +502,22 @@ async function boot(): Promise<void> {
|
|||||||
void tryResume(resumeId, routeController);
|
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
|
/** GET the ?g= envelope and resume: rebuild the model from the frozen
|
||||||
* snapshot (decode + cumulative + destination fields), hand the RAW
|
* snapshot (decode + cumulative + destination fields), hand the RAW
|
||||||
* anchors to the controller — it owns the projection. A failed,
|
* 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";
|
import { strings, type Locale } from "../i18n/strings.ts";
|
||||||
|
|
||||||
/** The destination confirmation card: register line, name headline, the
|
/** The destination confirmation card: register line, name headline, the
|
||||||
* curated address verbatim. */
|
* curated address verbatim — and (device-gate fix, 2026-10-24) a forward
|
||||||
export function buildDestinationCard(locale: Locale, entry: GazetteerEntry): HTMLElement {
|
* 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");
|
const card = document.createElement("div");
|
||||||
card.className = "card";
|
card.className = "card";
|
||||||
|
|
||||||
@@ -44,6 +49,19 @@ export function buildDestinationCard(locale: Locale, entry: GazetteerEntry): HTM
|
|||||||
card.appendChild(header);
|
card.appendChild(header);
|
||||||
card.appendChild(headline);
|
card.appendChild(headline);
|
||||||
card.appendChild(address);
|
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;
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user