From ac018269a5a32ffcfe11372caaf356ced32e3df3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Nov 2026 20:29:04 -0800 Subject: [PATCH] Keep the selected lane across Best time links Opening "Plan your crossing" or the card menu's Best time dropped the ?lane= key, and "See current wait time" then returned to Standard (25 to 185 minutes in the audit's SENTRI test). Pass the validated lane through withLane() on the share link, both card links and the Best time return link. Show a short EN/ES note on Best time when a non-Standard lane is selected, since that history is standard-passenger. The share test had asserted a lane-free link for every lane; it now expects the lane key. Add a withLane contract test. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01JB1Z5HSRuMsFpDeoEX6spM --- CHANGELOG.md | 8 ++++++++ DECISIONS.md | 4 ++++ scripts/test-crossing-share.mjs | 12 ++++++------ scripts/test-lane-context.mjs | 7 +++++++ src/components/dashboard/BorderCrossingCard.jsx | 4 ++-- src/components/dashboard/CrossingShare.jsx | 2 +- src/pages/BestTime.jsx | 14 ++++++++++++-- 7 files changed, 40 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dbae2350f..d4fc790ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # CHANGELOG +## 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. +- The crossing-share link, both card best-time links and the best-time page's return link now carry the validated `?lane=` key through `withLane()`. Standard stays query-free. +- Best time shows a short EN/ES note when a non-Standard lane is selected: the pattern is standard-passenger history and does not describe that lane. The history itself is unchanged. +- Test: the share test previously asserted a lane-free best-time link for every lane, which encoded the bug. It now expects the lane key; added a `withLane` contract test. +- Not changed: the "View trends" menu item, Walk or Drive wording, Analytics empty state, scroll position and the paused collector backup repair. + ## 2026-11-03 — Prepare the reviewed fixes as an ordered branch stack - Preserve current main data while reconciling the Spanish/snapshot, duplicate-lane and Lane Flip Watch branches in that order. Keep both endpoint regression groups, metadata imports and reviewed source-clock safeguards. diff --git a/DECISIONS.md b/DECISIONS.md index 366eccc3c..e5fdac8ed 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1,5 +1,9 @@ # DECISIONS +## 2026-11-07 · Every link between lane-aware pages carries the lane + +Extends the 2026-10-27 rule that a chosen lane is navigation context. Links from a crossing, card or share section to Best time, and Best time's return link, pass the validated lane key through `withLane()`. The Best time series stays Standard and is labeled as such when another lane is selected; it is not filtered or relabeled by lane. + ## 2026-11-02 · Lane comparisons describe retained fetches Do not infer a usual lane advantage from repeated fetches or call values current just because retrieval is fresh. Display both source clocks and actual historical coverage; mark an archive stale after 24 hours without a retained fetch. Earlier snapshots were normalized before duplicate-row repair and raw responses are unavailable: retain an explicit incompleteness caveat rather than claim they were reconstructed. Counts and percentages describe those stored fetches only. diff --git a/scripts/test-crossing-share.mjs b/scripts/test-crossing-share.mjs index 30567382e..d57b881fc 100644 --- a/scripts/test-crossing-share.mjs +++ b/scripts/test-crossing-share.mjs @@ -287,10 +287,10 @@ test('EN/ES rendering, WhatsApp payload, and best-time link formatting across al const root = createRoot(container); const testCases = [ - { lane: 'passenger_standard', expectedUrl: 'https://borderpulse.com/crossing/san-ysidro/', labelEn: 'Standard passenger', labelEs: 'Auto estándar' }, - { lane: 'passenger_ready', expectedUrl: 'https://borderpulse.com/crossing/san-ysidro/?lane=passenger_ready', labelEn: 'Ready Lane', labelEs: 'Ready Lane' }, - { lane: 'passenger_sentri', expectedUrl: 'https://borderpulse.com/crossing/san-ysidro/?lane=passenger_sentri', labelEn: 'SENTRI', labelEs: 'SENTRI' }, - { lane: 'pedestrian_standard', expectedUrl: 'https://borderpulse.com/crossing/san-ysidro/?lane=pedestrian_standard', labelEn: 'Pedestrian', labelEs: 'Peatones' }, + { lane: 'passenger_standard', expectedBestTime: '/best-time/san-ysidro/', expectedUrl: 'https://borderpulse.com/crossing/san-ysidro/', labelEn: 'Standard passenger', labelEs: 'Auto estándar' }, + { lane: 'passenger_ready', expectedBestTime: '/best-time/san-ysidro/?lane=passenger_ready', expectedUrl: 'https://borderpulse.com/crossing/san-ysidro/?lane=passenger_ready', labelEn: 'Ready Lane', labelEs: 'Ready Lane' }, + { lane: 'passenger_sentri', expectedBestTime: '/best-time/san-ysidro/?lane=passenger_sentri', expectedUrl: 'https://borderpulse.com/crossing/san-ysidro/?lane=passenger_sentri', labelEn: 'SENTRI', labelEs: 'SENTRI' }, + { lane: 'pedestrian_standard', expectedBestTime: '/best-time/san-ysidro/?lane=pedestrian_standard', expectedUrl: 'https://borderpulse.com/crossing/san-ysidro/?lane=pedestrian_standard', labelEn: 'Pedestrian', labelEs: 'Peatones' }, ]; for (const tc of testCases) { @@ -304,7 +304,7 @@ test('EN/ES rendering, WhatsApp payload, and best-time link formatting across al }); const linksEn = getLinks(container); - assert.equal(linksEn.bestTimeLink.href, '/best-time/san-ysidro/'); + assert.equal(linksEn.bestTimeLink.href, tc.expectedBestTime, 'best-time link keeps the selected lane'); assert.equal( linksEn.whatsappLink.href, 'https://wa.me/?text=' + encodeURIComponent(`Check San Ysidro (${tc.labelEn}) wait times and crossing hours on BorderPulse.\n${tc.expectedUrl}`) @@ -320,7 +320,7 @@ test('EN/ES rendering, WhatsApp payload, and best-time link formatting across al }); const linksEs = getLinks(container); - assert.equal(linksEs.bestTimeLink.href, '/best-time/san-ysidro/'); + assert.equal(linksEs.bestTimeLink.href, tc.expectedBestTime, 'best-time link keeps the selected lane'); assert.equal( linksEs.whatsappLink.href, 'https://wa.me/?text=' + encodeURIComponent(`Consulta los tiempos de espera y horarios de San Ysidro (${tc.labelEs}) en BorderPulse.\n${tc.expectedUrl}`) diff --git a/scripts/test-lane-context.mjs b/scripts/test-lane-context.mjs index 898f1717d..d80124076 100644 --- a/scripts/test-lane-context.mjs +++ b/scripts/test-lane-context.mjs @@ -45,3 +45,10 @@ test('selected lane never falls back to a Standard or summary wait; zero remains } assert.equal(availableLaneWait({ ...crossing, port_status: 'Closed' }, 'passenger_sentri'), null); }); + +test('best-time and crossing links carry a selected lane and drop the default', () => { + assert.equal(withLane('/best-time/san-ysidro/', 'passenger_sentri'), '/best-time/san-ysidro/?lane=passenger_sentri'); + assert.equal(withLane('/crossing/san-ysidro/', 'passenger_sentri'), '/crossing/san-ysidro/?lane=passenger_sentri'); + assert.equal(withLane('/best-time/san-ysidro/', DEFAULT_LANE), '/best-time/san-ysidro/'); + assert.equal(withLane('/best-time/san-ysidro/?lane=passenger_sentri', 'not_a_lane'), '/best-time/san-ysidro/'); +}); diff --git a/src/components/dashboard/BorderCrossingCard.jsx b/src/components/dashboard/BorderCrossingCard.jsx index 05dfb85a1..d0e3c1d42 100644 --- a/src/components/dashboard/BorderCrossingCard.jsx +++ b/src/components/dashboard/BorderCrossingCard.jsx @@ -312,7 +312,7 @@ export default function BorderCrossingCard({ )} {cardSlug && ( - track('best-time-open', { slug: cardSlug, source: 'card' })}> + track('best-time-open', { slug: cardSlug, source: 'card' })}> {language === 'en' ? 'Best time to cross' : 'Mejor hora para cruzar'} @@ -414,7 +414,7 @@ export default function BorderCrossingCard({ {showBestTimePill && cardSlug && (