Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# CHANGELOG

## 2026-11-07 - Stop overstating Walk or Drive, Analytics and the embed (Phase 1)

- Walk or Drive: the result no longer says "Walk." / "You save". It states the comparison it actually made (standard car lane vs a pedestrian lane plus an assumed 20 min for parking and the bridge), notes it applies only if you can use that lane and your trip works on foot, and reminds that Ready Lane needs an RFID document. Heading changed to "Comparison from reported waits, not a personal recommendation." EN/ES. Logic unchanged.
- Analytics: the empty state no longer says "check back after a few visits". It explains the view uses waits saved in this browser only, shows how many of the 10 it needs, and links to Best time for shared 30-day patterns.
- Embed: northbound source label now names the lane ("CBP · Standard passenger").
- Tests: smoke and Santa Teresa browser checks updated to the new text.

## 2026-11-07 - Keep the selected lane when opening Best time and returning (draft PR)

- Fix: choosing SENTRI, Ready Lane or Pedestrian and then opening "Plan your crossing" (or the card menu's "Best time to cross") dropped the lane. "See current wait time" then returned to Standard. In the audit's sampled test the displayed wait changed from 25 to 185 minutes.
Expand Down
10 changes: 5 additions & 5 deletions scripts/check-santa-teresa-actions.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -85,24 +85,24 @@ export async function checkSantaTeresaActions(context, baseUrl) {
await action.focus();
await page.keyboard.press('Enter');
await page.waitForURL(url => url.pathname === comparisonPath);
await page.getByText(es ? 'Según las esperas reportadas' : 'Based on reported waits', { exact: true }).waitFor();
await page.getByText(es ? 'Comparación con esperas reportadas, no una recomendación personal' : 'Comparison from reported waits, not a personal recommendation', { exact: true }).waitFor();
await page.goBack();
await page.waitForURL(url => url.pathname === detailPath);
// Enter from the promoted action just before the unchanged 90-minute limit.
snapshot.fetched_at = new Date(now - 90 * 60_000).toISOString();
await page.reload();
await checkEntry();
await action.click();
await page.getByText(es ? 'Según las esperas reportadas' : 'Based on reported waits', { exact: true }).waitFor();
await page.getByText(es ? 'Comparación con esperas reportadas, no una recomendación personal' : 'Comparison from reported waits, not a personal recommendation', { exact: true }).waitFor();
fail = true;
await page.clock.runFor(61_000);
await page.getByTestId('walk-drive-unavailable').waitFor();
assert.ok(!/Based on reported waits|Según las esperas reportadas|Recommended|Recomendado|Walk\.|Camina\.|Drive\.|Maneja\.|It's a wash\.|Está parejo\./.test(await page.locator('#root').innerText()));
assert.ok(!/Comparison from reported waits|Comparación con esperas reportadas|On these reports|Con estos reportes|Recommended|Recomendado|Walk\.|Camina\.|Drive\.|Maneja\.|It's a wash\.|Está parejo\./.test(await page.locator('#root').innerText()));
// A failed response did not create fresh advice; a new real timestamp does.
fail = false;
snapshot.fetched_at = new Date(now + 61_000).toISOString();
await page.evaluate(() => window.dispatchEvent(new Event('online')));
await page.getByText(es ? 'Según las esperas reportadas' : 'Based on reported waits', { exact: true }).waitFor();
await page.getByText(es ? 'Comparación con esperas reportadas, no una recomendación personal' : 'Comparison from reported waits, not a personal recommendation', { exact: true }).waitFor();
await page.goBack();
await page.waitForURL(url => url.pathname === detailPath);
for (const [drive, walk] of [[60, null], [null, null]]) {
Expand All @@ -120,7 +120,7 @@ export async function checkSantaTeresaActions(context, baseUrl) {
await page.reload();
await checkEntry();
await action.click();
await page.getByText(es ? 'Según las esperas reportadas' : 'Based on reported waits', { exact: true }).waitFor();
await page.getByText(es ? 'Comparación con esperas reportadas, no una recomendación personal' : 'Comparison from reported waits, not a personal recommendation', { exact: true }).waitFor();
assert.match(await page.locator('#root').innerText(), /\b0 min\b/, 'genuine zero is preserved');
await page.goBack();
crossing.port_status = 'Closed';
Expand Down
4 changes: 2 additions & 2 deletions scripts/smoke-routes.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -318,15 +318,15 @@ async function runProfile(browser, profile, manifest, expectedCrossingCount) {
await page.getByRole('button', { name: 'Analytics', exact: true }).click();
await page.waitForFunction(() => {
const body = document.body?.innerText || '';
return body.includes('Analytics populate as you use the app.')
return body.includes('This view uses waits saved in this browser only.')
|| body.includes('Average wait by hour')
|| body.includes('Something went wrong');
}, null, { timeout: 5_000 });
const rendered = await inspectRenderedPage(page);
if (rendered.body.includes('Something went wrong') || rendered.body.includes('Border Pulse encountered an error')) {
throw new Error('ErrorBoundary rendered');
}
if (!rendered.body.includes('Analytics populate as you use the app.') && !rendered.body.includes('Average wait by hour')) {
if (!rendered.body.includes('This view uses waits saved in this browser only.') && !rendered.body.includes('Average wait by hour')) {
throw new Error('Analytics view did not render');
}
if (activeErrors.length) throw new Error(activeErrors.join(' | '));
Expand Down
18 changes: 12 additions & 6 deletions src/components/dashboard/AnalyticsView.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useMemo } from 'react';
import { Link } from 'react-router-dom';
import { Activity, AlertTriangle, Database, TrendingDown, TrendingUp } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import {
Expand Down Expand Up @@ -355,12 +356,17 @@ export default function AnalyticsView({ crossings, language, direction = 'northb
<Card className="border-dashed border-slate-300 bg-white/80 dark:border-slate-700 dark:bg-slate-950/80">
<CardContent className="p-8 text-center text-sm text-slate-500 dark:text-slate-400">
<Database className="mx-auto mb-3 h-7 w-7 text-slate-300" />
{language === 'en'
? 'Analytics populate as you use the app. Check back after a few visits.'
: 'Los análisis se llenan conforme uses la app. Vuelve después de varias visitas.'}
<div className="mt-2 text-[11px] text-slate-400">
{language === 'en' ? 'Samples collected' : 'Muestras recolectadas'}: {summary.totalSamples}
</div>
<p data-testid="analytics-local-empty">
{language === 'en'
? `This view uses waits saved in this browser only. It has ${summary.totalSamples} of the 10 it needs.`
: `Esta vista usa solo las esperas guardadas en este navegador. Tiene ${summary.totalSamples} de las 10 que necesita.`}
</p>
<p className="mt-2">
{language === 'en' ? 'For 30-day hourly patterns from the shared record, open ' : 'Para patrones por hora de 30 días del registro compartido, abre '}
<Link to="/best-time/" className="font-medium text-emerald-700 underline underline-offset-4 dark:text-emerald-400">
{language === 'en' ? 'Best time to cross' : 'Mejor hora para cruzar'}
</Link>.
</p>
</CardContent>
</Card>
);
Expand Down
3 changes: 2 additions & 1 deletion src/pages/Embed.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { getWaitMinutes } from '@/components/utils/crossingDirection';
import { nowInTz } from '@/components/utils/crossingMeta';
import BorderPulseLogo from '@/components/BorderPulseLogo';
import { isSparseCell } from '@/lib/aggregates';
import { DEFAULT_LANE, laneLabel } from '@/lib/laneContext';
import { freshnessOf, fetchLabel, reportTimeLabel } from '@/lib/trustState';

const FLAG = { CA: '🇺🇸', AZ: '🇺🇸', NM: '🇺🇸', TX: '🇺🇸' };
Expand Down Expand Up @@ -115,7 +116,7 @@ export default function Embed() {

const sourceLabel = isSouthbound
? (lang === 'es' ? 'Estimación' : 'Estimate')
: 'CBP';
: `CBP · ${laneLabel(DEFAULT_LANE, lang)}`;

const deltaText = (() => {
if (!typicalDelta) return null;
Expand Down
12 changes: 7 additions & 5 deletions src/pages/WalkOrDrive.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -226,22 +226,24 @@ export default function WalkOrDrive() {
: recommendation.verdict === 'drive' ? 'text-sky-800 dark:text-sky-300'
: 'text-slate-600 dark:text-slate-400'
}`}>
{language === 'en' ? 'Based on reported waits' : 'Según las esperas reportadas'}
{language === 'en' ? 'Comparison from reported waits, not a personal recommendation' : 'Comparación con esperas reportadas, no una recomendación personal'}
</div>
<p className="text-sm text-slate-900 dark:text-white">
{(() => {
const r = recommendation;
if (r.verdict === 'walk') {
const laneEn = r.via === 'ready' ? 'Ready Lane pedestrian' : 'standard pedestrian';
const laneEs = r.via === 'ready' ? 'peatonal Ready Lane' : 'peatonal estándar';
const readyEn = r.via === 'ready' ? ' Ready Lane requires an RFID-enabled document.' : '';
const readyEs = r.via === 'ready' ? ' Ready Lane requiere un documento con RFID.' : '';
return language === 'en'
? `Walk. The ${laneEn} wait plus ${FOOT_OVERHEAD_MIN} min for parking and the bridge totals ${r.footTotal} min, vs ${veh} min driving. You save about ${r.savings} min.`
: `Camina. La espera ${laneEs} más ${FOOT_OVERHEAD_MIN} min de estacionar y cruzar el puente suma ${r.footTotal} min, contra ${veh} min manejando. Ahorras unos ${r.savings} min.`;
? `On these reports, walking is shorter. The ${laneEn} wait plus ${FOOT_OVERHEAD_MIN} min assumed for parking and the bridge totals ${r.footTotal} min, vs ${veh} min in the standard car lane. That is about ${r.savings} min less, if you can use that lane and your trip works on foot.${readyEn}`
: `Con estos reportes, a pie es más corto. La espera ${laneEs} más ${FOOT_OVERHEAD_MIN} min supuestos para estacionar y cruzar el puente suma ${r.footTotal} min, contra ${veh} min en el carril estándar de auto. Son unos ${r.savings} min menos, si puedes usar ese carril y tu viaje funciona a pie.${readyEs}`;
}
if (r.verdict === 'drive') {
return language === 'en'
? `Drive. Vehicle wait is ${veh} min vs ${r.footTotal} min on foot (including ${FOOT_OVERHEAD_MIN} min parking + bridge). Driving saves about ${r.savings} min.`
: `Maneja. La espera en auto es de ${veh} min vs ${r.footTotal} min a pie (incluyendo ${FOOT_OVERHEAD_MIN} min de estacionar + puente). Manejar ahorra unos ${r.savings} min.`;
? `On these reports, driving is shorter. ${veh} min in the standard car lane vs ${r.footTotal} min on foot, including ${FOOT_OVERHEAD_MIN} min assumed for parking and the bridge. That is about ${r.savings} min less.`
: `Con estos reportes, en auto es más corto. ${veh} min en el carril estándar de auto contra ${r.footTotal} min a pie, incluyendo ${FOOT_OVERHEAD_MIN} min supuestos para estacionar y cruzar el puente. Son unos ${r.savings} min menos.`;
}
return language === 'en'
? `It's a wash. Vehicle wait is ${veh} min, foot total ${r.footTotal} min (within ±${RECOMMEND_GAP_MIN}). Pick whichever is more convenient for your destination on the U.S. side.`
Expand Down
Loading