From b911a0fc52656597ebd56ee5a475dd97148fd640 Mon Sep 17 00:00:00 2001
From: Sebastian Becerra <207095575+sbc1-code@users.noreply.github.com>
Date: Wed, 14 Oct 2026 08:57:51 -0700
Subject: [PATCH] fix: gate walk-or-drive advice on retrieval freshness
---
CHANGELOG.md | 14 +++++
scripts/check-walk-drive-availability.mjs | 1 +
scripts/check-walk-drive-freshness.mjs | 75 +++++++++++++++++++++++
scripts/prerender.mjs | 4 +-
scripts/smoke-routes.mjs | 2 +
scripts/test-trust-state.mjs | 14 +++++
src/lib/walkDriveFreshness.js | 13 ++++
src/pages/WalkOrDrive.jsx | 54 +++++++++++-----
8 files changed, 161 insertions(+), 16 deletions(-)
create mode 100644 scripts/check-walk-drive-freshness.mjs
create mode 100644 src/lib/walkDriveFreshness.js
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8f69e7544..aa568e95a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,19 @@
# CHANGELOG
+## 2026-10-14 — Walk-or-drive retrieval freshness (prepared)
+
+- Preserve the actual snapshot retrieval time and suppress walk/drive/tie advice
+ and recommendation badges for stale, missing, invalid or future timestamps.
+ Re-evaluate age while the page stays open, using the existing 45/90-minute
+ thresholds; failed refreshes retain the original timestamp.
+- Show retrieval age separately from raw CBP lane report clocks, including
+ unavailable-clock labels. Replace unconditional live/current decision wording
+ in the comparison and its metadata with reported-wait wording.
+- Add EN/ES desktop/mobile regression coverage for zero waits, expiry without a
+ new feed, offline refresh, recovery, future timestamps and report clocks.
+ Retain closure, pending-report and Back-navigation checks. No feed, threshold,
+ route-inventory, camera or ad changes; publication awaits independent review.
+
## 2026-10-13 — Camera coverage beyond El Paso (prepared)
- Added a camera-area selector for all nine corridors. The camera area follows
diff --git a/scripts/check-walk-drive-availability.mjs b/scripts/check-walk-drive-availability.mjs
index 09f136813..0d6125dd2 100644
--- a/scripts/check-walk-drive-availability.mjs
+++ b/scripts/check-walk-drive-availability.mjs
@@ -4,6 +4,7 @@ import fs from 'node:fs';
// This fixture tests retained CBP lane values without hitting CBP or ad networks.
export async function checkWalkDriveAvailability(page, context, baseUrl) {
const snapshot = JSON.parse(fs.readFileSync(new URL('../public/data/crossings.json', import.meta.url)));
+ snapshot.fetched_at = new Date().toISOString();
const crossing = snapshot.crossings.find(row => row.port_number === '250201');
assert.ok(crossing, 'Andrade fixture must exist');
crossing.hours = '6 am-10 pm';
diff --git a/scripts/check-walk-drive-freshness.mjs b/scripts/check-walk-drive-freshness.mjs
new file mode 100644
index 000000000..723ad690d
--- /dev/null
+++ b/scripts/check-walk-drive-freshness.mjs
@@ -0,0 +1,75 @@
+import assert from 'node:assert/strict';
+import fs from 'node:fs';
+
+// Reuse the smoke context's external-request blocker; no CBP/ad traffic.
+export async function checkWalkDriveFreshness(context, baseUrl) {
+ const snapshot = JSON.parse(fs.readFileSync(new URL('../public/data/crossings.json', import.meta.url)));
+ const crossing = snapshot.crossings.find(row => row.port_number === '250201');
+ crossing.port_status = 'Open';
+ const lane = (delay_minutes, update_time) => ({ delay_minutes, status: 'delay', lanes_open: 1, update_time });
+ crossing.lanes = {
+ passenger_standard: lane(90, 'At Midnight PDT'),
+ passenger_ready: lane(15, 'At 1:00 am PST'),
+ pedestrian_standard: lane(0, 'At 11:00 pm MST'),
+ pedestrian_ready: lane(10, null),
+ };
+ const feedMatcher = url => ['/data/crossings.json', '/api/public/crossings'].includes(url.pathname);
+ let fail = false;
+ let requests = 0;
+ const feedHandler = route => {
+ requests++;
+ return fail ? route.fulfill({ status: 503, body: 'fixture unavailable' })
+ : route.fulfill({ contentType: 'application/json', body: JSON.stringify(snapshot) });
+ };
+ await context.route(feedMatcher, feedHandler);
+ const page = await context.newPage();
+ const now = Date.now();
+ await page.clock.install({ time: now });
+ try {
+ for (const lang of ['en', 'es']) {
+ await page.goto(baseUrl.href);
+ await page.evaluate(value => localStorage.setItem('borderPulse_language', value), lang);
+ await page.clock.setSystemTime(now);
+ fail = false;
+ snapshot.fetched_at = new Date(now - 90 * 60_000).toISOString();
+ await page.goto(new URL('/walk-or-drive/andrade/', baseUrl).href);
+ await page.getByText(lang === 'en' ? 'Recommended' : 'Recomendado', { exact: true }).waitFor();
+ let content = await page.locator('#root').innerText();
+ for (const text of ['0 min', 'At Midnight PDT', 'At 1:00 am PST', 'At 11:00 pm MST',
+ lang === 'en' ? 'CBP report time unavailable' : 'Hora del reporte CBP no disponible']) assert.ok(content.includes(text), text);
+ assert.ok(!/Right now|Ahora mismo|Live decision|Decisión en vivo/.test(content));
+ // No new feed is required to expire advice while the page remains open.
+ fail = true;
+ await page.clock.runFor(61_000);
+ await page.getByTestId('walk-drive-unavailable').waitFor();
+ content = await page.locator('#root').innerText();
+ assert.ok(!/Recommended|Recomendado|Walk\.|Camina\.|Drive\.|Maneja\.|It's a wash\.|Está parejo\./.test(content));
+ const previousLabel = await page.getByTestId('walk-drive-fetch-time').innerText();
+ const before = requests;
+ const refreshFailed = page.waitForEvent('console', {
+ predicate: message => message.text().includes('[dataService] fetch failed'),
+ timeout: 10_000,
+ });
+ await page.evaluate(() => window.dispatchEvent(new Event('online')));
+ await refreshFailed;
+ assert.ok(requests > before, 'online refresh attempted and failed');
+ assert.equal(await page.getByTestId('walk-drive-fetch-time').innerText(), previousLabel, 'failed refresh must not renew retrieval time');
+ assert.ok(await page.getByTestId('walk-drive-unavailable').isVisible());
+ // Recovery restores the recommendation using the new actual timestamp.
+ fail = false;
+ snapshot.fetched_at = new Date(now + 61_000).toISOString();
+ await page.evaluate(() => window.dispatchEvent(new Event('online')));
+ await page.getByText(lang === 'en' ? 'Recommended' : 'Recomendado', { exact: true }).waitFor();
+ // Upstream allows small clock skew, but the page must not recommend from it.
+ snapshot.fetched_at = new Date(now + 180_000).toISOString();
+ await page.reload();
+ await page.getByTestId('walk-drive-unavailable').waitFor();
+ assert.equal(await page.getByTestId('walk-drive-fetch-time').innerText(), lang === 'en' ? 'Fetch time unknown' : 'Hora de consulta desconocida');
+ assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1));
+ }
+ } finally {
+ await page.close();
+ await context.unroute(feedMatcher, feedHandler);
+ }
+ console.log('[walk-drive] PASS: EN/ES raw clocks, aging, expiry, failed refresh, recovery and future retrievals');
+}
diff --git a/scripts/prerender.mjs b/scripts/prerender.mjs
index e39ead539..2da22fb0e 100644
--- a/scripts/prerender.mjs
+++ b/scripts/prerender.mjs
@@ -781,8 +781,8 @@ async function main() {
for (const c of crossings.filter(hasPedestrianLane)) {
const slug = portToSlug[c.port_number];
if (!slug) continue;
- const title = `Walk or Drive Across ${c.name}? Live Wait Comparison (2026) | Border Pulse`;
- const desc = `Compare live pedestrian and vehicle wait times at ${c.name} from CBP. See whether walking saves enough minutes to be worth parking.`;
+ const title = `Walk or Drive Across ${c.name}? CBP Wait Comparison | Border Pulse`;
+ const desc = `Compare reported pedestrian and vehicle wait times at ${c.name} from CBP. See whether walking saves enough minutes to be worth parking.`;
const canonical = `${BASE}/walk-or-drive/${slug}/`;
const ogImage = `${BASE}/og/${slug}.png`;
const breadcrumb = {
diff --git a/scripts/smoke-routes.mjs b/scripts/smoke-routes.mjs
index 0374badf7..f2549be6c 100644
--- a/scripts/smoke-routes.mjs
+++ b/scripts/smoke-routes.mjs
@@ -6,6 +6,7 @@ import { chromium } from 'playwright-core';
import { checkGrowthInteractions } from './check-growth-interactions.mjs';
import { checkLocalCrossings } from './check-local-crossings.mjs';
import { checkNoJsReports } from './check-nojs-reports.mjs';
+import { checkWalkDriveFreshness } from './check-walk-drive-freshness.mjs';
import { checkWalkDriveAvailability } from './check-walk-drive-availability.mjs';
import { checkComparisonGuide } from './check-comparison-guide.mjs';
import { checkReportTimes } from './check-report-times.mjs';
@@ -322,6 +323,7 @@ async function runProfile(browser, profile, manifest, expectedCrossingCount) {
await checkGrowthInteractions(page, context, baseUrl);
await checkLocalCrossings(page, context, baseUrl);
await checkWalkDriveAvailability(page, context, baseUrl);
+ await checkWalkDriveFreshness(context, baseUrl);
await checkComparisonGuide(page, context, baseUrl);
} catch (error) {
failures.push(`${profile.name} growth interactions: ${error.stack || error.message}`);
diff --git a/scripts/test-trust-state.mjs b/scripts/test-trust-state.mjs
index 55d8cb5bb..b21fd7d9d 100644
--- a/scripts/test-trust-state.mjs
+++ b/scripts/test-trust-state.mjs
@@ -166,3 +166,17 @@ test('walk-or-drive requires a usable lane report rather than an old numeric val
assert.equal(wait(null), null);
assert.equal(wait({ status: ' No Delay ', delay_minutes: 0, lanes_open: 1 }), 0);
});
+
+// Walk/drive uses the same measured thresholds without treating clock skew as fresh.
+test('walk-drive retrieval gate handles fresh, aging, stale and inconsistent clocks', async () => {
+ const { walkDriveFreshness } = await import('../src/lib/walkDriveFreshness.js');
+ for (const minutes of [0, 45, 46, 90]) {
+ assert.equal(walkDriveFreshness(minutesAgo(minutes), NOW).usable, true);
+ }
+ for (const stamp of [minutesAgo(91), null, undefined, '', 'invalid', 0, {}, minutesAgo(-1)]) {
+ assert.equal(walkDriveFreshness(stamp, NOW).usable, false);
+ }
+ const stamp = minutesAgo(90);
+ assert.equal(walkDriveFreshness(stamp, NOW + 60_000).state, FRESHNESS.STALE);
+ assert.equal(walkDriveFreshness(stamp, NOW + 60_000).usable, false);
+});
diff --git a/src/lib/walkDriveFreshness.js b/src/lib/walkDriveFreshness.js
new file mode 100644
index 000000000..f22d47cab
--- /dev/null
+++ b/src/lib/walkDriveFreshness.js
@@ -0,0 +1,13 @@
+import { freshnessOf, FRESHNESS } from './trustState.js';
+
+// Retrieval freshness is separate from CBP's clock-only lane report times.
+// Missing, malformed and future retrievals cannot support a recommendation.
+export function walkDriveFreshness(fetchedAt, now = Date.now()) {
+ const freshness = typeof fetchedAt === 'string' && Date.parse(fetchedAt) <= now
+ ? freshnessOf(fetchedAt, now)
+ : { state: FRESHNESS.UNKNOWN, age: null };
+ return {
+ ...freshness,
+ usable: [FRESHNESS.FRESH, FRESHNESS.AGING].includes(freshness.state),
+ };
+}
diff --git a/src/pages/WalkOrDrive.jsx b/src/pages/WalkOrDrive.jsx
index 15120b308..22e50ddc6 100644
--- a/src/pages/WalkOrDrive.jsx
+++ b/src/pages/WalkOrDrive.jsx
@@ -7,6 +7,8 @@ import { buildSlugMap } from '@/lib/slugs';
import { updatePageMeta, resetPageMeta } from '@/lib/seo';
import { usePersistentLanguage } from '@/lib/useLanguage';
import { availableLaneWait } from '@/lib/crossingAvailability';
+import { fetchLabel, reportTimeLabel } from '@/lib/trustState';
+import { walkDriveFreshness } from '@/lib/walkDriveFreshness';
import { getPortStatus } from '@/components/utils/crossingMeta';
// Time it typically takes to park, walk the bridge, and reach a rideshare /
@@ -22,7 +24,7 @@ const FOOT_OVERHEAD_MIN = 20;
// dominates the wait-time savings.
const RECOMMEND_GAP_MIN = 15;
-function PanelCard({ icon: Icon, title, wait, sub, lang, recommendedLabel }) {
+function PanelCard({ icon: Icon, title, wait, sub, reportTime, lang, recommendedLabel }) {
return (
{sub} {reportTimeLabel(reportTime, lang)}
@@ -149,6 +163,7 @@ export default function WalkOrDrive() { ? `CBP has no current pedestrian wait report for ${crossing.name}, so a walk-vs-drive recommendation is unavailable. A missing report does not mean zero wait or prove that there is no pedestrian lane.` : `CBP no tiene un reporte actual de espera peatonal para ${crossing.name}, así que no hay recomendación entre caminar o manejar. La falta de un reporte no significa espera cero ni demuestra que no haya carril peatonal.`)}
+{retrievalLabel}
{closed && crossing.hours && ({language === 'en' ? 'Posted CBP hours: ' : 'Horario publicado por CBP: '}{crossing.hours} @@ -185,11 +200,19 @@ export default function WalkOrDrive() {
{retrievalLabel}
+{language === 'en' + ? 'Lane report times may differ. A recent fetch does not establish how recent each CBP report is.' + : 'Las horas de reporte pueden variar por carril. Una consulta reciente no confirma la antigüedad de cada reporte de CBP.'}
+ {!usableSnapshot &&{language === 'en' + ? 'Walk-or-drive recommendation unavailable: the snapshot is stale or its fetch time cannot be verified. These are retained reports; check CBP before leaving.' + : 'Recomendación entre caminar o manejar no disponible: la consulta está desactualizada o no se puede verificar su hora. Estos son reportes guardados; consulta CBP antes de salir.'}
} + {recommendation && ({(() => { @@ -233,8 +256,9 @@ export default function WalkOrDrive() { icon={Car} title={language === 'en' ? 'Drive (standard)' : 'En auto (estándar)'} wait={veh} + reportTime={crossing.lanes?.passenger_standard?.update_time} sub={vehReady != null - ? (language === 'en' ? `Ready Lane: ${vehReady} min · SENTRI: ${vehSentri ?? '—'} min` : `Ready Lane: ${vehReady} min · SENTRI: ${vehSentri ?? '—'} min`) + ? `Ready Lane: ${vehReady} min (${reportTimeLabel(crossing.lanes?.passenger_ready?.update_time, language)}) · SENTRI: ${vehSentri ?? '—'} min (${reportTimeLabel(crossing.lanes?.passenger_sentri?.update_time, language)})` : (language === 'en' ? 'CBP standard passenger lane' : 'Carril estándar de pasajero CBP')} lang={language} recommendedLabel={recommendation?.verdict === 'drive' ? (language === 'en' ? 'Recommended' : 'Recomendado') : null} @@ -243,6 +267,7 @@ export default function WalkOrDrive() { icon={User} title={language === 'en' ? 'Walk (standard)' : 'A pie (estándar)'} wait={ped} + reportTime={crossing.lanes?.pedestrian_standard?.update_time} sub={language === 'en' ? `Add ~${FOOT_OVERHEAD_MIN} min for parking + bridge` : `Agrega ~${FOOT_OVERHEAD_MIN} min por estacionar + puente`} @@ -253,6 +278,7 @@ export default function WalkOrDrive() { icon={User} title={language === 'en' ? 'Walk (Ready Lane)' : 'A pie (Ready Lane)'} wait={pedReady} + reportTime={crossing.lanes?.pedestrian_ready?.update_time} sub={language === 'en' ? `Requires RFID document. Add ~${FOOT_OVERHEAD_MIN} min for parking + bridge` : `Requiere documento con RFID. Agrega ~${FOOT_OVERHEAD_MIN} min por estacionar + puente`}