feat(#76): bound primary-route fetches and local-first driver summary

Add shared apiFetch timeouts/abort/typed errors with in-flight dedupe, a
RouteState UI for loading/empty/timeout/error+retry, and wire it through
Weekend (Data Health), Championship, Driver Profile, Briefing, Live, and
Race Hub. Driver summary is local-first with bounded optional OpenF1
enrichment so a hung remote call cannot block the profile.

Spike note: parseSourceMode defaults to openf1; driver summary now treats
omitted ?source= as auto so local season data is preferred.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-12 18:00:37 -04:00
parent e8a1f15f8b
commit b3d1730324
29 changed files with 1636 additions and 241 deletions

View File

@@ -24,6 +24,7 @@ import {
} from '../lib/digest'
import { stripHtml, timeAgo } from '../utils'
import type { ArticleContent, NewsItem } from '../types'
import { RouteState } from '../components/RouteState'
import '../styles/digest.css'
type Category = 'all' | 'official' | 'news' | 'video'
@@ -406,27 +407,27 @@ export function BriefingPage() {
const queryClient = useQueryClient()
const now = useMemo(() => new Date(), [])
const { data: allNews = [], isLoading, isError } = useQuery({
const { data: allNews = [], isLoading, isError, error, refetch, isFetching } = useQuery({
queryKey: ['news'],
queryFn: () => fetchNews(100),
queryFn: ({ signal }) => fetchNews(100, undefined, signal),
staleTime: 60_000,
})
const seasonsQuery = useQuery({
queryKey: ['seasons'],
queryFn: fetchSeasons,
queryFn: ({ signal }) => fetchSeasons(signal),
})
const latestSeason = seasonsQuery.data?.[0] ?? null
const meetingsQuery = useQuery({
queryKey: ['season-meetings', latestSeason],
queryFn: () => fetchSeasonMeetings(latestSeason!),
queryFn: ({ signal }) => fetchSeasonMeetings(latestSeason!, signal),
enabled: latestSeason != null,
})
const hubQuery = useQuery({
queryKey: ['championship-hub', latestSeason],
queryFn: () => fetchChampionshipHub(latestSeason!),
queryFn: ({ signal }) => fetchChampionshipHub(latestSeason!, signal),
enabled: latestSeason != null,
})
@@ -524,8 +525,21 @@ export function BriefingPage() {
</span>
</div>
{isLoading && <div className="loading-state">loading briefing</div>}
{isError && <div className="error-box">Failed to load paddock briefing.</div>}
{isLoading && (
<RouteState kind="loading" title="loading briefing…" testId="briefing-loading" />
)}
{isError && (
<RouteState
kind="error"
title="Briefing unavailable"
error={error}
onRetry={() => {
if (!isFetching) void refetch()
}}
retrying={isFetching}
testId="briefing-error"
/>
)}
{!isLoading && !isError && (
<>

View File

@@ -7,6 +7,7 @@ import type { ChampHubDriver, ChampionshipHub } from '../types'
import { ChampionshipSimulator } from '../components/ChampionshipSimulator'
import { RivalryCompare } from '../components/RivalryCompare'
import { Meaning } from '../components/Meaning'
import { RouteState } from '../components/RouteState'
import { TeammateH2H } from '../components/TeammateH2H'
import { teammatePairs } from '../lib/h2h'
import { pointsGapMeaning } from '../lib/meaning'
@@ -81,23 +82,55 @@ function teamSplit(teamName: string, drivers: ChampHubDriver[]): TeamSplit {
export function ChampionshipPage() {
const [view, setView] = useState<View>('drivers')
const seasonsQuery = useQuery({ queryKey: ['seasons'], queryFn: fetchSeasons })
const seasonsQuery = useQuery({
queryKey: ['seasons'],
queryFn: ({ signal }) => fetchSeasons(signal),
})
const latestSeason = seasonsQuery.data?.[0] ?? null
const hubQuery = useQuery({
queryKey: ['championship-hub', latestSeason],
queryFn: () => fetchChampionshipHub(latestSeason ?? undefined),
queryFn: ({ signal }) => fetchChampionshipHub(latestSeason ?? undefined, signal),
enabled: latestSeason != null,
staleTime: 5 * 60_000,
})
if (seasonsQuery.isLoading || hubQuery.isLoading) {
return <div className="page loading-state">loading championship</div>
return (
<div className="page">
<RouteState kind="loading" title="loading championship…" testId="championship-loading" />
</div>
)
}
if (seasonsQuery.isError) {
return (
<div className="page">
<RouteState
kind="error"
title="Championship unavailable"
error={seasonsQuery.error}
onRetry={() => {
if (!seasonsQuery.isFetching) void seasonsQuery.refetch()
}}
retrying={seasonsQuery.isFetching}
testId="championship-error"
/>
</div>
)
}
if (hubQuery.isError) {
return (
<div className="page error-box">
{hubQuery.error instanceof Error ? hubQuery.error.message : 'Failed to load championship'}
<div className="page">
<RouteState
kind="error"
title="Championship unavailable"
error={hubQuery.error}
onRetry={() => {
if (!hubQuery.isFetching) void hubQuery.refetch()
}}
retrying={hubQuery.isFetching}
testId="championship-error"
/>
</div>
)
}

View File

@@ -11,6 +11,7 @@ import {
import { SourceBadge, weekendStatusLabel } from '../components/SourceBadge'
import { CliCommands, ingestYearCommands } from '../components/CliCommands'
import { MeetingDetailPanel } from '../components/MeetingDetailPanel'
import { RouteState } from '../components/RouteState'
import type { Meeting, Weekend } from '../types'
function formatMeetingDate(meeting: Meeting): string {
@@ -27,12 +28,12 @@ export function DataLibraryPage() {
const seasonsQuery = useQuery({
queryKey: ['seasons'],
queryFn: fetchSeasons,
queryFn: ({ signal }) => fetchSeasons(signal),
})
const meetingsQuery = useQuery({
queryKey: ['meetings', selectedYear],
queryFn: () => fetchLocalMeetings(selectedYear!),
queryFn: ({ signal }) => fetchLocalMeetings(selectedYear!, signal),
enabled: selectedYear != null,
})
@@ -41,7 +42,7 @@ export function DataLibraryPage() {
const weekendQueries = useQueries({
queries: meetings.map((meeting) => ({
queryKey: ['weekend', meeting.meeting_key],
queryFn: () => fetchWeekend(meeting.meeting_key),
queryFn: ({ signal }: { signal: AbortSignal }) => fetchWeekend(meeting.meeting_key, signal),
enabled: meetings.length > 0,
staleTime: 60_000,
})),
@@ -78,13 +79,26 @@ export function DataLibraryPage() {
const weekendsLoading = weekendQueries.some((q) => q.isLoading)
if (seasonsQuery.isLoading) {
return <div className="page loading-state">loading local data library</div>
return (
<div className="page">
<RouteState kind="loading" title="loading local data library…" testId="weekend-loading" />
</div>
)
}
if (seasonsQuery.isError) {
return (
<div className="page error-box">
{seasonsQuery.error instanceof Error ? seasonsQuery.error.message : 'Failed to load seasons'}
<div className="page">
<RouteState
kind="error"
title="Weekend data unavailable"
error={seasonsQuery.error}
onRetry={() => {
if (!seasonsQuery.isFetching) void seasonsQuery.refetch()
}}
retrying={seasonsQuery.isFetching}
testId="weekend-error"
/>
</div>
)
}
@@ -222,15 +236,20 @@ export function DataLibraryPage() {
</div>
{meetingsQuery.isLoading && (
<div className="loading-state">loading meetings</div>
<RouteState kind="loading" title="loading meetings…" testId="weekend-meetings-loading" />
)}
{meetingsQuery.isError && (
<div className="error-box">
{meetingsQuery.error instanceof Error
? meetingsQuery.error.message
: 'Failed to load meetings'}
</div>
<RouteState
kind="error"
title="Weekend meetings unavailable"
error={meetingsQuery.error}
onRetry={() => {
if (!meetingsQuery.isFetching) void meetingsQuery.refetch()
}}
retrying={meetingsQuery.isFetching}
testId="weekend-error"
/>
)}
{!meetingsQuery.isLoading && !meetingsQuery.isError && meetings.length === 0 && (
@@ -316,11 +335,16 @@ export function DataLibraryPage() {
<div className="dl-detail-wrap">
{weekendsLoading && selectedWeekend == null && (
<div className="loading-state">loading weekend details</div>
<RouteState kind="loading" title="loading weekend details…" testId="weekend-detail-loading" />
)}
{selectedWeekend && <MeetingDetailPanel weekend={selectedWeekend} />}
{selectedMeetingKey != null && !weekendsLoading && selectedWeekend == null && (
<div className="missing-notice">Could not load weekend details.</div>
<RouteState
kind="empty"
title="Weekend details missing"
message="Could not load weekend details for this meeting."
testId="weekend-detail-empty"
/>
)}
</div>
</div>

View File

@@ -1,6 +1,7 @@
import { useQuery } from '@tanstack/react-query'
import { Link } from '@tanstack/react-router'
import { fetchDriverSummary, fetchSeasons } from '../api'
import { DataNotice, RouteState } from '../components/RouteState'
import { teamColor } from '../utils'
import { countryFlag } from '../lib/gpIdentity'
import {
@@ -26,7 +27,7 @@ export function DriverProfilePage({ driverNumber, year }: Props) {
// seasons list is empty/unavailable the backend falls back to the current year.
const seasonsQuery = useQuery({
queryKey: ['seasons'],
queryFn: fetchSeasons,
queryFn: ({ signal }) => fetchSeasons(signal),
enabled: year == null,
})
const resolvedYear = year ?? seasonsQuery.data?.[0]
@@ -34,39 +35,86 @@ export function DriverProfilePage({ driverNumber, year }: Props) {
const summaryQuery = useQuery({
queryKey: ['driver-summary', driverNumber, resolvedYear ?? 'latest'],
queryFn: () => fetchDriverSummary(driverNumber, resolvedYear),
queryFn: ({ signal }) => fetchDriverSummary(driverNumber, resolvedYear, signal),
enabled: seasonsSettled && driverNumber > 0,
staleTime: 5 * 60_000,
})
if (driverNumber <= 0) {
return <div className="page error-box">Invalid driver number</div>
return (
<div className="page">
<RouteState kind="empty" title="Invalid driver number" message="Pick a driver from the championship standings." />
</div>
)
}
if (!seasonsSettled || summaryQuery.isLoading) {
return <div className="page loading-state">loading driver profile</div>
return (
<div className="page">
<RouteState kind="loading" title="loading driver profile…" testId="driver-profile-loading" />
</div>
)
}
if (summaryQuery.isError) {
return (
<div className="page error-box">
{summaryQuery.error instanceof Error
? summaryQuery.error.message
: 'Failed to load driver profile'}
<div className="page">
<RouteState
kind="error"
title="Driver profile unavailable"
error={summaryQuery.error}
onRetry={() => {
if (!summaryQuery.isFetching) void summaryQuery.refetch()
}}
retrying={summaryQuery.isFetching}
testId="driver-profile-error"
/>
</div>
)
}
const summary = summaryQuery.data
if (!summary) {
return <div className="page error-box">No driver data</div>
return (
<div className="page">
<RouteState
kind="empty"
title="No driver data"
message="This driver is not in the local season standings yet."
/>
</div>
)
}
return <DriverProfileBody summary={summary} />
return (
<DriverProfileBody
summary={summary}
onRetry={() => {
if (!summaryQuery.isFetching) void summaryQuery.refetch()
}}
/>
)
}
function DriverProfileBody({ summary }: { summary: DriverSummary }) {
function DriverProfileBody({
summary,
onRetry,
}: {
summary: DriverSummary
onRetry?: () => void
}) {
const color = teamColor(summary.team_colour)
const deltas = gridFinishDeltas(summary.rounds)
return (
<div className="dp-page" data-testid="driver-profile">
{summary.enrichment === 'limited' && (
<DataNotice
availability="limited"
message="Optional remote details are unavailable. Showing local season identity and results."
onRetry={onRetry}
testId="driver-profile-limited"
/>
)}
{summary.source === 'local' && summary.enrichment !== 'limited' && (
<DataNotice availability="local" message="Loaded from local season data." testId="driver-profile-local" />
)}
<header className="dp-header" data-testid="dp-header" style={{ borderLeftColor: color }}>
<div className="dp-identity">
<div className="dp-title-row">

View File

@@ -36,6 +36,7 @@ import { EventRail } from '../components/live/EventRail'
import { TeamRadioTicker } from '../components/live/TeamRadioTicker'
import { TyreDegPanel } from '../components/live/TyreDegPanel'
import { LiveHandoff } from '../components/live/LiveHandoff'
import { RouteState } from '../components/RouteState'
import '../styles/live-state.css'
/** How often to re-check weekend-context while analysis is still ingesting. */
@@ -67,12 +68,15 @@ export function LiveTimingPage() {
const {
data,
isLoading,
isError,
error,
refetch,
isFetching,
isFetched: liveStateFetched,
} = useQuery({
queryKey: ['live-state'],
queryFn: fetchLiveState,
queryFn: ({ signal }) => fetchLiveState(signal),
staleTime: 5_000,
})
@@ -109,7 +113,7 @@ export function LiveTimingPage() {
// analysis-ready without a manual refresh.
const contextQuery = useQuery({
queryKey: ['weekend-context'],
queryFn: fetchWeekendContext,
queryFn: ({ signal }) => fetchWeekendContext(signal),
enabled: !isLive,
staleTime: WEEKEND_CONTEXT_STALE_MS,
refetchInterval: (query) => {
@@ -296,9 +300,16 @@ export function LiveTimingPage() {
return (
<div className="page live-page" data-testid="live-page" data-phase={phase}>
{isError && (
<div className="error-box">
{error instanceof Error ? error.message : 'Failed to load live timing state'}
</div>
<RouteState
kind="error"
title="Live timing unavailable"
error={error}
onRetry={() => {
if (!isFetching) void refetch()
}}
retrying={isFetching}
testId="live-initial-error"
/>
)}
{phase === 'disconnected' && (
@@ -316,8 +327,12 @@ export function LiveTimingPage() {
</div>
)}
{phase === 'connecting' && (
<div className="loading-state">connecting to live timing</div>
{phase === 'connecting' && (isLoading || !liveStateFetched) && (
<RouteState
kind="loading"
title="connecting to live timing…"
testId="live-initial-loading"
/>
)}
{(phase === 'settling' || phase === 'inactive') && (

View File

@@ -20,6 +20,7 @@ import {
} from '../components/PreSessionView'
import { WeekendSwitcher } from '../components/WeekendSwitcher'
import { SourceBadge } from '../components/SourceBadge'
import { RouteState } from '../components/RouteState'
import { countryAccent, countryDecal, formatGpDateRange } from '../lib/gpIdentity'
import { sessionTypeAbbrev } from '../lib/coverage'
import { formatSessionScheduleTime, sortSessionsByStart } from '../lib/schedule'
@@ -55,7 +56,7 @@ export function RaceHubPage({ sessionKey }: Props) {
const contextQuery = useQuery({
queryKey: ['weekend-context'],
queryFn: fetchWeekendContext,
queryFn: ({ signal }) => fetchWeekendContext(signal),
staleTime: 15_000,
refetchInterval: 30_000,
})
@@ -73,7 +74,7 @@ export function RaceHubPage({ sessionKey }: Props) {
const raceHubQuery = useQuery({
queryKey: ['race-hub', sessionKey],
queryFn: () => fetchRaceHub(sessionKey),
queryFn: ({ signal }) => fetchRaceHub(sessionKey, signal),
enabled: sessionKey > 0,
staleTime: 30_000,
})
@@ -81,7 +82,7 @@ export function RaceHubPage({ sessionKey }: Props) {
const meetingKey = raceHubQuery.data?.meeting?.meeting_key
const weekendQuery = useQuery({
queryKey: ['weekend', meetingKey],
queryFn: () => fetchWeekend(meetingKey!),
queryFn: ({ signal }) => fetchWeekend(meetingKey!, signal),
enabled: meetingKey != null && meetingKey > 0,
staleTime: 60_000,
})
@@ -102,33 +103,32 @@ export function RaceHubPage({ sessionKey }: Props) {
if (contextQuery.isLoading) {
return (
<div className="rh-page" style={accentStyle}>
<div className="loading-state">resolving weekend context</div>
<RouteState
kind="loading"
title="resolving weekend context…"
testId="race-hub-loading"
/>
</div>
)
}
if (contextQuery.isError) {
return (
<div className="rh-page" data-testid="race-hub-error" style={accentStyle}>
<div className="rh-recover">
<div className="error-box">
{contextQuery.error instanceof Error
? contextQuery.error.message
: 'Failed to load weekend context.'}
</div>
<RouteState
kind="error"
title="Weekend unavailable"
error={contextQuery.error}
onRetry={() => {
if (!contextQuery.isFetching) void contextQuery.refetch()
}}
retrying={contextQuery.isFetching}
>
<div className="rh-recover-actions">
<button
type="button"
className="rh-recover-btn primary"
onClick={() => contextQuery.refetch()}
data-testid="rh-retry"
>
Retry
</button>
<Link to="/" search={{}} className="rh-recover-btn" data-testid="rh-back-weekend">
Back to Weekend
</Link>
</div>
</div>
</RouteState>
</div>
)
}
@@ -170,7 +170,11 @@ export function RaceHubPage({ sessionKey }: Props) {
return (
<div className="rh-page" style={accentStyle}>
<div className="loading-state">resolving weekend context</div>
<RouteState
kind="loading"
title="resolving weekend context…"
testId="race-hub-loading"
/>
</div>
)
}
@@ -179,7 +183,11 @@ export function RaceHubPage({ sessionKey }: Props) {
if (raceHubQuery.isLoading) {
return (
<div className="rh-page" style={accentStyle}>
<div className="loading-state">loading session {sessionKey}</div>
<RouteState
kind="loading"
title={`loading session ${sessionKey}`}
testId="race-hub-loading"
/>
</div>
)
}
@@ -191,21 +199,21 @@ export function RaceHubPage({ sessionKey }: Props) {
return (
<div className="rh-page" data-testid="race-hub-error" style={accentStyle}>
<div className="rh-recover">
<div className="error-box">
{raceHubQuery.error instanceof Error
? raceHubQuery.error.message
: `Failed to load session ${sessionKey}.`}
</div>
<RouteState
kind="error"
title="Session unavailable"
error={raceHubQuery.error}
message={
raceHubQuery.error
? undefined
: `Session ${sessionKey} could not be loaded from local data.`
}
onRetry={() => {
if (!raceHubQuery.isFetching) void raceHubQuery.refetch()
}}
retrying={raceHubQuery.isFetching}
>
<div className="rh-recover-actions">
<button
type="button"
className="rh-recover-btn primary"
onClick={() => raceHubQuery.refetch()}
data-testid="rh-retry"
>
Retry
</button>
<Link
to="/"
search={backSearch}
@@ -217,7 +225,7 @@ export function RaceHubPage({ sessionKey }: Props) {
Back to Weekend
</Link>
</div>
</div>
</RouteState>
</div>
)
}

View File

@@ -271,7 +271,7 @@ export function RacePreviewPage() {
const seasonsQuery = useQuery({
queryKey: ['seasons'],
queryFn: fetchSeasons,
queryFn: ({ signal }) => fetchSeasons(signal),
})
const latestSeason = seasonsQuery.data?.[0] ?? null

View File

@@ -71,7 +71,8 @@ export function WeekendPage({
/** Restored from `/?session_key=` when returning from Race Hub analysis. */
focusSessionKey?: number
}) {
const { context, loadState, error, championship, briefing, now } = useWeekendContext()
const { context, loadState, error, championship, briefing, now, refetch, isFetching } =
useWeekendContext()
const hasFocus =
(focusMeetingKey != null && focusMeetingKey > 0) ||
(focusSessionKey != null && focusSessionKey > 0)
@@ -87,7 +88,7 @@ export function WeekendPage({
if (loadState === 'error' || context == null) {
return (
<main className="wk-page" data-testid="weekend-page" data-state="error">
<WeekendError message={error?.message} />
<WeekendError error={error} onRetry={refetch} retrying={isFetching} />
</main>
)
}