diff --git a/CHANGELOG.md b/CHANGELOG.md
index d4fc790ca..6d318fdaf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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.
diff --git a/scripts/check-santa-teresa-actions.mjs b/scripts/check-santa-teresa-actions.mjs
index 06ce34f6a..1f3262579 100644
--- a/scripts/check-santa-teresa-actions.mjs
+++ b/scripts/check-santa-teresa-actions.mjs
@@ -85,7 +85,7 @@ 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.
@@ -93,16 +93,16 @@ 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();
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]]) {
@@ -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';
diff --git a/scripts/smoke-routes.mjs b/scripts/smoke-routes.mjs
index c68afc827..ea21577ae 100644
--- a/scripts/smoke-routes.mjs
+++ b/scripts/smoke-routes.mjs
@@ -318,7 +318,7 @@ 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 });
@@ -326,7 +326,7 @@ async function runProfile(browser, profile, manifest, expectedCrossingCount) {
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(' | '));
diff --git a/src/components/dashboard/AnalyticsView.jsx b/src/components/dashboard/AnalyticsView.jsx
index ea76b6c79..462d5e699 100644
--- a/src/components/dashboard/AnalyticsView.jsx
+++ b/src/components/dashboard/AnalyticsView.jsx
@@ -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 {
@@ -355,12 +356,17 @@ export default function AnalyticsView({ crossings, language, direction = 'northb
+ {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.`}
+
+ {language === 'en' ? 'For 30-day hourly patterns from the shared record, open ' : 'Para patrones por hora de 30 días del registro compartido, abre '}
+
+ {language === 'en' ? 'Best time to cross' : 'Mejor hora para cruzar'}
+ .
+
{(() => { @@ -234,14 +234,16 @@ export default function WalkOrDrive() { 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.`