import { useEffect, useMemo, useState } from 'react' import { useQuery } from '@tanstack/react-query' import { useNavigate } from '@tanstack/react-router' import { fetchLocalMeetings, fetchRaceHub, fetchSeasons, fetchWeekend, } from '../api' import { DatasetStrip } from '../components/DatasetStrip' import { RaceStoryCanvas } from '../components/RaceStoryCanvas' import { TabBar, type Tab } from '../components/TabBar' import { DatasetStatusView } from '../components/DatasetStatusView' import { StrategyView } from '../components/StrategyView' import { LapsView } from '../components/LapsView' import { CompareView } from '../components/CompareView' import { RaceControlView } from '../components/RaceControlView' import { WeatherView } from '../components/WeatherView' import { OverviewView } from '../components/OverviewView' import { PreSessionView } from '../components/PreSessionView' import { WeekendSwitcher } from '../components/WeekendSwitcher' import { SourceBadge } from '../components/SourceBadge' import { countryAccent, countryDecal, formatGpDateRange } from '../lib/gpIdentity' import { sessionTypeAbbrev } from '../lib/coverage' import { formatSessionScheduleTime, pickAnalysisFocusMeeting, sessionStartTime, sortSessionsByStart, } from '../lib/schedule' import { isPreSession, sessionState, sessionStateDotClass, sessionStateLabel, } from '../lib/sessionState' import type { Weekend, WeekendSession } from '../types' interface Props { sessionKey: number } /** * Resolve the session a bare `/race-hub` should open. Prefers the canonical * Weekend Context `default_analysis_session` (which never points at a future * session), then any completed session with the richest coverage. Returns * `undefined` when every session is still upcoming so the caller can fall back * to the switcher instead of opening empty analysis. */ function pickAnalysisSession(weekend: Weekend | undefined, now: Date): number | undefined { if (!weekend) return undefined if (weekend.default_analysis_session && weekend.default_analysis_session > 0) { return weekend.default_analysis_session } const started = weekend.sessions.filter((s) => { const start = sessionStartTime(s.session) return !start || start <= now }) if (started.length === 0) return undefined const local = started.filter((s) => s.source === 'local') const partial = started.filter((s) => s.source === 'partial') const pool = local.length > 0 ? local : partial.length > 0 ? partial : started const race = pool.find((s) => s.session.session_type?.toLowerCase().includes('race')) if (race) return race.session.session_key const qual = pool.find((s) => s.session.session_type?.toLowerCase().includes('qualifying')) if (qual) return qual.session.session_key return pool[pool.length - 1]?.session.session_key } export function RaceHubPage({ sessionKey }: Props) { const navigate = useNavigate() const [activeTab, setActiveTab] = useState('overview') const [switcherOpen, setSwitcherOpen] = useState(false) const now = useMemo(() => new Date(), []) // ─── Auto-redirect when no session_key is supplied ─── const seasonsQuery = useQuery({ queryKey: ['seasons'], queryFn: fetchSeasons, enabled: sessionKey === 0, }) const latestSeason = seasonsQuery.data?.[0] ?? null const meetingsQuery = useQuery({ queryKey: ['meetings', latestSeason], queryFn: () => fetchLocalMeetings(latestSeason!), enabled: sessionKey === 0 && latestSeason != null, }) const focusMeeting = useMemo(() => { if (sessionKey !== 0 || !meetingsQuery.data) return null return pickAnalysisFocusMeeting(meetingsQuery.data, now) }, [sessionKey, meetingsQuery.data, now]) const fallbackWeekendQuery = useQuery({ queryKey: ['weekend', focusMeeting?.meeting_key], queryFn: () => fetchWeekend(focusMeeting!.meeting_key), enabled: sessionKey === 0 && focusMeeting != null, }) useEffect(() => { if (sessionKey !== 0) return const weekend = fallbackWeekendQuery.data if (!weekend) return const target = pickAnalysisSession(weekend, now) if (target) { navigate({ to: '/race-hub', search: { session_key: target }, replace: true }) } }, [sessionKey, fallbackWeekendQuery.data, navigate, now]) // ─── Active session payload ─── const raceHubQuery = useQuery({ queryKey: ['race-hub', sessionKey], queryFn: () => fetchRaceHub(sessionKey), enabled: sessionKey > 0, staleTime: 30_000, }) const meetingKey = raceHubQuery.data?.meeting?.meeting_key const weekendQuery = useQuery({ queryKey: ['weekend', meetingKey], queryFn: () => fetchWeekend(meetingKey!), enabled: meetingKey != null && meetingKey > 0, staleTime: 60_000, }) const data = raceHubQuery.data const weekend = weekendQuery.data const accent = countryAccent(data?.meeting ?? null) const accentStyle = { '--gp-accent': accent } as React.CSSProperties const [showDiagnostics, setShowDiagnostics] = useState(false) // ─── No session_key: show resolving state, fall back to switcher if no local data ─── if (sessionKey === 0) { if (seasonsQuery.isLoading || meetingsQuery.isLoading || fallbackWeekendQuery.isLoading) { return (
resolving latest local weekend…
) } const seasons = seasonsQuery.data ?? [] if (seasons.length === 0) { return (
box-box · race hub

No local sessions yet

The Race Hub reads from local ingest only. Once a weekend is ingested it will open here automatically.

Open Admin · Data Health Back to Command Center
) } // Weekend resolved but every session is upcoming — offer the switcher instead // of silently opening empty analysis. if (fallbackWeekendQuery.data && !pickAnalysisSession(fallbackWeekendQuery.data, now)) { return (
box-box · race hub

No completed session to analyse yet

The next weekend hasn’t run. Pick a past session to review, or check back once it’s complete.

Back to Command Center
{switcherOpen && ( setSwitcherOpen(false)} /> )}
) } return (
resolving latest local weekend…
) } // ─── Loading / error for the requested session_key (retry + back to weekend) ─── if (raceHubQuery.isLoading) { return (
loading session {sessionKey}…
) } if (raceHubQuery.isError || !data) { return (
{raceHubQuery.error instanceof Error ? raceHubQuery.error.message : `Failed to load session ${sessionKey}.`}
Back to Weekend
) } const decal = countryDecal(data.meeting ?? null) const sessions = weekend ? sortSessionsByStart(weekend.sessions.map((w) => w.session)) : [] const sessionMeta = weekend ? Object.fromEntries(weekend.sessions.map((w) => [w.session.session_key, w])) : {} const activeSessionMeta: WeekendSession | undefined = sessionMeta[sessionKey] const activeState = activeSessionMeta ? sessionState(activeSessionMeta, now) : undefined const preSession = activeState != null && isPreSession(activeState) return (
{/* Topbar */}
box-box · race hub {data.meeting?.year ? ` · ${data.meeting.year}` : ''}
{switcherOpen && ( setSwitcherOpen(false)} /> )} {/* GP Identity band */} {data.meeting && (
)} {/* Session rail */} {sessions.length > 0 && ( )} {/* Active session sub-bar */} {data.session && (
{data.session.session_name} {formatSessionScheduleTime(data.session.date_start)} {activeState && ( )} key {sessionKey}
)} {preSession && data.session ? ( ) : ( <> {activeTab === 'overview' && } {activeTab === 'race_story' && (
)} {activeTab === 'strategy' && (
Race Strategy
)} {activeTab === 'compare' && (
Driver Compare
)} {activeTab === 'lap_data' && (
Lap Data {data.laps.length > 0 && ( {data.laps.length} samples )}
)} {activeTab === 'conditions' && (
Conditions {data.weather.length > 0 && ( {data.weather.length} samples )}
)} {activeTab === 'race_control' && (
Race Control {data.race_control.length > 0 && ( {data.race_control.length} messages )}
)} {activeTab === 'data_status' && (
Diagnostics
{showDiagnostics && (
)}
)} )}
) }