diff --git a/CHANGELOG.md b/CHANGELOG.md index 253408155..b81ae2aee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # CHANGELOG +## 2026-11-09 - Main screen, first pass (Phase 3, session 1, draft) + +- Phones (under 900px): the crossing results list now comes before the map, so the first thing after the controls is a wait. A starred crossing is already sorted first, so returning visitors see their own crossing's wait first. Desktop layout unchanged. +- One clock: the saved-data / refresh notice now shows its time in the visitor's zone, matching the header, instead of UTC. The duplicate "Snapshot retrieved" tile in the footer is removed; the header and the "Fetched ... ago" line already show it. +- Checks: full `npm test` (including route and border-map browser checks) passes after a prerendered build. Before/after phone screenshots captured in EN and ES. Entry bundle 62.00 KB gzip. +- Not in this pass: a "your crossing" hero, removing repeated info between map, list and crossing page, and lane cues in Standard-only sections (sessions 2-3). + ## 2026-11-09 - Fresher backup data and an honest "could not load" state - Fallback freshness: when the live CBP endpoint fails, the site now also reads the collector's committed feed through a same-origin rewrite (`/data/latest/crossings.json` -> `main/public/data/crossings.json` on GitHub). It keeps whichever valid backup is newer, the collector feed or the bundled copy. The bundled copy only changes when Vercel deploys (daily), so it could be most of a day old; the collector commits every 15 minutes. Backup readings are still labeled as fallback with their original clock. diff --git a/src/components/dashboard/AboutFooter.jsx b/src/components/dashboard/AboutFooter.jsx index 08deb69ec..b3c9125a7 100644 --- a/src/components/dashboard/AboutFooter.jsx +++ b/src/components/dashboard/AboutFooter.jsx @@ -5,8 +5,7 @@ import React from 'react'; * Goal: give LLMs and SEO crawlers explicit, citable facts about this * site's data, coverage, update frequency, and privacy model. */ -export default function AboutFooter({ language, fetchedAt, count, direction = 'northbound' }) { - const updatedStr = fetchedAt ? new Date(fetchedAt).toLocaleString() : '—'; +export default function AboutFooter({ language, count, direction = 'northbound' }) { const isSouthbound = direction === 'southbound'; return ( @@ -32,7 +31,7 @@ export default function AboutFooter({ language, fetchedAt, count, direction = 'n -
+
{language === 'en' ? (isSouthbound ? 'Method' : 'Source') : (isSouthbound ? 'Método' : 'Fuente')} @@ -57,12 +56,6 @@ export default function AboutFooter({ language, fetchedAt, count, direction = 'n {language === 'en' ? 'Regular schedule' : 'Horario regular'}
-
-
- {language === 'en' ? 'Snapshot retrieved' : 'Datos consultados'} -
-
{updatedStr}
-
diff --git a/src/components/dashboard/CrossingMapExplorer.css b/src/components/dashboard/CrossingMapExplorer.css index c037d6c9a..d4e16c232 100644 --- a/src/components/dashboard/CrossingMapExplorer.css +++ b/src/components/dashboard/CrossingMapExplorer.css @@ -56,7 +56,7 @@ .mapLegend { display:flex; flex-wrap:wrap; gap:6px 16px; font-size:11px; color:var(--map-muted); margin-top:10px; }.mapLegend>span { display:inline-flex; align-items:center; gap:5px; }.mapLegend i { width:7px; height:7px; border-radius:50%; display:inline-block; background:#64748b; }.mapLegend .good { background:#059669; }.mapLegend .moderate { background:#d97706; }.mapLegend .heavy { background:#e11d48; } .mapSources { margin-top:4px; font-size:11px; color:var(--map-muted); }.mapSources summary { min-height:44px; display:flex; align-items:center; cursor:pointer; text-decoration:underline; text-underline-offset:3px; }.mapSources p { margin:8px 0; max-width:90ch; }.mapSources a { color:var(--map-link); text-decoration:underline; } .emptyMap { font-size:13px; color:var(--map-muted); padding:16px 6px; } -@media(max-width:899px) { .explorerWorkspace { grid-template-columns:1fr; }.explorerMap { height:330px; }.explorerPane { border-left:0; border-top:1px solid var(--map-border); max-height:450px; }.explorerHeading h2 { font-size:16px; }.explorerMode>span { width:100%; }.explorerHeading>button { padding-right:0; }.explorerMap:focus-visible { outline:2px solid var(--map-link); } } +@media(max-width:899px) { .explorerWorkspace { grid-template-columns:1fr; }.explorerMap { height:330px; }.explorerPane { border-left:0; border-bottom:1px solid var(--map-border); max-height:450px; order:-1; }.explorerHeading h2 { font-size:16px; }.explorerMode>span { width:100%; }.explorerHeading>button { padding-right:0; }.explorerMap:focus-visible { outline:2px solid var(--map-link); } } @media(prefers-reduced-motion:reduce) { .crossingExplorer * { transition:none!important; animation:none!important; scroll-behavior:auto!important; } } .explorerHeading>div:last-child { display:flex; gap:4px; }.mapTools svg { width:16px; height:16px; } diff --git a/src/components/dashboard/CrossingMapExplorer.jsx b/src/components/dashboard/CrossingMapExplorer.jsx index 4a947235b..f599d88db 100644 --- a/src/components/dashboard/CrossingMapExplorer.jsx +++ b/src/components/dashboard/CrossingMapExplorer.jsx @@ -73,6 +73,8 @@ const clock = (value, language, zone = 'UTC') => value && Number.isFinite(Date.p timeZone: zone, timeZoneName: 'short' }).format(new Date(value)) : '—'; +// The page header shows fetch time in the visitor's zone; the notice matches it. +const localZone = () => { try { return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'; } catch { return 'UTC'; } }; const labels = { en: { title: 'Explore the border', @@ -434,7 +436,7 @@ export default function CrossingMapExplorer({ setParams(q, { replace: true }); - }} />{clock(selectedSnapshot?.fetched_at, language)}}{unavailableInstant &&

{es ? 'La observación solicitada no está en este archivo. Elige otra hora.' : 'The requested observation is absent from this archive. Choose another time.'}

}{lane !== 'passenger_standard' &&

{t.notRetained}.

}
: !trusted &&
{noData ? t.unavailable : offline ? es ? 'Sin conexión. Conservamos reportes anteriores.' : 'Offline. Retained reports are shown.' : fromFallback ? es ? 'Datos de respaldo guardados. Las condiciones actuales no están verificadas.' : 'Saved fallback data. Current conditions are not verified.' : success === false ? es ? 'No se pudo actualizar. Conservamos la hora y los valores del último reporte.' : 'Refresh unavailable. The last report values and original time are retained.' : es ? 'Consulta desactualizada o de hora desconocida.' : 'Stale or unknown retrieval time.'} {clock(fetchedAt, language)}
} + }} />{clock(selectedSnapshot?.fetched_at, language)}}{unavailableInstant &&

{es ? 'La observación solicitada no está en este archivo. Elige otra hora.' : 'The requested observation is absent from this archive. Choose another time.'}

}{lane !== 'passenger_standard' &&

{t.notRetained}.

} : !trusted &&
{noData ? t.unavailable : offline ? es ? 'Sin conexión. Conservamos reportes anteriores.' : 'Offline. Retained reports are shown.' : fromFallback ? es ? 'Datos de respaldo guardados. Las condiciones actuales no están verificadas.' : 'Saved fallback data. Current conditions are not verified.' : success === false ? es ? 'No se pudo actualizar. Conservamos la hora y los valores del último reporte.' : 'Refresh unavailable. The last report values and original time are retained.' : es ? 'Consulta desactualizada o de hora desconocida.' : 'Stale or unknown retrieval time.'} {clock(fetchedAt, language, localZone())}
}
{ if (e.target !== e.currentTarget) return;