2026-07-03 00:09:47 -04:00
|
|
|
import type {
|
|
|
|
|
ArticleContent,
|
2026-07-04 00:18:10 -04:00
|
|
|
CarDataSample,
|
2026-07-03 00:09:47 -04:00
|
|
|
ChampionshipHub,
|
2026-07-11 18:12:00 -04:00
|
|
|
DriverSummary,
|
2026-07-11 17:59:00 -04:00
|
|
|
EnrichedGrid,
|
|
|
|
|
EnrichedResult,
|
2026-07-04 00:18:10 -04:00
|
|
|
LapsComparisonResponse,
|
2026-07-03 00:09:47 -04:00
|
|
|
LiveStateResponse,
|
2026-07-03 19:34:09 -04:00
|
|
|
LiveSessionMeta,
|
2026-07-03 00:09:47 -04:00
|
|
|
Meeting,
|
|
|
|
|
NewsItem,
|
|
|
|
|
RaceHub,
|
2026-07-11 18:17:15 -04:00
|
|
|
ReplayFramesResponse,
|
2026-07-03 00:09:47 -04:00
|
|
|
Session,
|
2026-07-03 19:34:09 -04:00
|
|
|
TrackOutline,
|
2026-07-03 00:09:47 -04:00
|
|
|
Weekend,
|
2026-07-12 18:09:43 -04:00
|
|
|
WeekendContext,
|
2026-07-03 00:09:47 -04:00
|
|
|
} from './types'
|
2026-05-25 01:10:44 -04:00
|
|
|
|
|
|
|
|
export async function fetchRaceHub(sessionKey: number): Promise<RaceHub> {
|
|
|
|
|
const res = await fetch(`/api/v1/race-hub?session_key=${sessionKey}`)
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
throw new Error(`API ${res.status}: ${res.statusText}`)
|
|
|
|
|
}
|
|
|
|
|
return res.json()
|
|
|
|
|
}
|
2026-05-25 02:31:35 -04:00
|
|
|
|
|
|
|
|
export async function fetchSeasons(): Promise<number[]> {
|
|
|
|
|
const res = await fetch('/api/v1/seasons')
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
throw new Error(`API ${res.status}: ${res.statusText}`)
|
|
|
|
|
}
|
|
|
|
|
const years = await res.json()
|
|
|
|
|
return Array.isArray(years) ? years : []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchLocalMeetings(year: number): Promise<Meeting[]> {
|
|
|
|
|
const res = await fetch(`/api/v1/meetings?year=${year}&source=local`)
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
throw new Error(`API ${res.status}: ${res.statusText}`)
|
|
|
|
|
}
|
|
|
|
|
const meetings = await res.json()
|
|
|
|
|
return Array.isArray(meetings) ? meetings : []
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 18:14:55 -04:00
|
|
|
export async function fetchSeasonMeetings(year: number): Promise<Meeting[]> {
|
2026-07-11 17:59:00 -04:00
|
|
|
return fetchMeetings(year, 'openf1')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchMeetings(year: number, source = 'auto'): Promise<Meeting[]> {
|
|
|
|
|
const res = await fetch(`/api/v1/meetings?year=${year}&source=${source}`)
|
2026-05-25 18:14:55 -04:00
|
|
|
if (!res.ok) {
|
|
|
|
|
throw new Error(`API ${res.status}: ${res.statusText}`)
|
|
|
|
|
}
|
|
|
|
|
const meetings = await res.json()
|
|
|
|
|
return Array.isArray(meetings) ? meetings : []
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-11 17:59:00 -04:00
|
|
|
export async function fetchResults(sessionKey: number, source = 'auto'): Promise<EnrichedResult[]> {
|
|
|
|
|
const res = await fetch(`/api/v1/results?session_key=${sessionKey}&source=${source}`)
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
throw new Error(`API ${res.status}: ${res.statusText}`)
|
|
|
|
|
}
|
|
|
|
|
const results = await res.json()
|
|
|
|
|
return Array.isArray(results) ? results : []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchStartingGrid(sessionKey: number, source = 'auto'): Promise<EnrichedGrid[]> {
|
|
|
|
|
const res = await fetch(`/api/v1/grid?session_key=${sessionKey}&source=${source}`)
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
throw new Error(`API ${res.status}: ${res.statusText}`)
|
|
|
|
|
}
|
|
|
|
|
const grid = await res.json()
|
|
|
|
|
return Array.isArray(grid) ? grid : []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchTrackOutline(circuitKey: number, year: number): Promise<TrackOutline | null> {
|
|
|
|
|
const res = await fetch(`/api/v1/track-outline?circuit_key=${circuitKey}&year=${year}`)
|
|
|
|
|
if (!res.ok) return null
|
|
|
|
|
const data = await res.json()
|
|
|
|
|
if (data?.error || !Array.isArray(data?.points) || data.points.length < 2) return null
|
|
|
|
|
return data as TrackOutline
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-11 18:17:15 -04:00
|
|
|
export async function fetchReplayFrames(
|
|
|
|
|
sessionKey: number,
|
|
|
|
|
intervalMs = 5000,
|
|
|
|
|
): Promise<ReplayFramesResponse> {
|
|
|
|
|
const params = new URLSearchParams({
|
|
|
|
|
session_key: String(sessionKey),
|
|
|
|
|
interval_ms: String(intervalMs),
|
|
|
|
|
})
|
|
|
|
|
const res = await fetch(`/api/v1/replay/frames?${params}`)
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
throw new Error(`API ${res.status}: ${res.statusText}`)
|
|
|
|
|
}
|
|
|
|
|
return res.json()
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 19:53:42 -07:00
|
|
|
export async function fetchSessions(meetingKey: number, source = 'openf1'): Promise<Session[]> {
|
|
|
|
|
const res = await fetch(`/api/v1/sessions?meeting_key=${meetingKey}&source=${source}`)
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
throw new Error(`API ${res.status}: ${res.statusText}`)
|
|
|
|
|
}
|
|
|
|
|
const sessions = await res.json()
|
|
|
|
|
return Array.isArray(sessions) ? sessions : []
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 02:31:35 -04:00
|
|
|
export async function fetchWeekend(meetingKey: number): Promise<Weekend> {
|
|
|
|
|
const res = await fetch(`/api/v1/weekend?meeting_key=${meetingKey}`)
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
throw new Error(`API ${res.status}: ${res.statusText}`)
|
|
|
|
|
}
|
|
|
|
|
return res.json()
|
|
|
|
|
}
|
2026-05-25 02:56:06 -04:00
|
|
|
|
2026-07-12 18:09:43 -04:00
|
|
|
// fetchWeekendContext consumes the canonical /api/v1/weekend-context endpoint
|
fix(#73): consume canonical Weekend Context contract and repair navigation
Address the independent review blockers on PR #80 after rebasing onto the
authoritative #72 Weekend Context API.
- Replace the invented frontend WeekendContext with the exact backend contract
(temporal_state, previous/focus/next meetings, previous_completed/active/next/
default_analysis sessions with availability). Every valid canonical payload now
maps to a designed state via a total resolveViewState; a well-formed response
can never fall through to the limited-data placeholder.
- Make the canonical read the single source of truth: useWeekendContext no longer
fans out to season/meetings/per-weekend/OpenF1/live queries. Only supplementary
championship + news reads run, and only once the canonical context resolves.
- Fix the Prepare/analysis flow: /preview is a stable alias that renders the
preparation surface (PreSessionView) instead of redirecting back to the same
between-races screen.
- One primary navigation system per breakpoint: the mobile top-bar links are
hidden so the bottom bar is the sole primary nav, and Admin is moved out of
every Primary landmark into an operator-utilities toolbar.
- Add Vitest coverage for the contract mapping, every temporal state, loading/
error/limited surfaces, the no-fanout guarantee, the /preview CTA, and the nav
hierarchy; add hermetic Playwright journeys (seeded + injected canonical
payloads), 390/768/1440 overflow checks, and Weekend visual snapshots. Retire
the stale Command Center specs/snapshots.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-12 18:38:08 -04:00
|
|
|
// (backend story #72). The response is the authoritative WeekendContext shape and
|
|
|
|
|
// is used verbatim as the Weekend home's source of truth. Any HTTP error throws
|
|
|
|
|
// so the hook can surface an explicit error state; there is no client-side
|
2026-07-12 18:49:10 -04:00
|
|
|
// re-derivation of the contract. Race Hub bare-default landing also reads this
|
|
|
|
|
// for `default_analysis_session` (#75).
|
fix(#73): consume canonical Weekend Context contract and repair navigation
Address the independent review blockers on PR #80 after rebasing onto the
authoritative #72 Weekend Context API.
- Replace the invented frontend WeekendContext with the exact backend contract
(temporal_state, previous/focus/next meetings, previous_completed/active/next/
default_analysis sessions with availability). Every valid canonical payload now
maps to a designed state via a total resolveViewState; a well-formed response
can never fall through to the limited-data placeholder.
- Make the canonical read the single source of truth: useWeekendContext no longer
fans out to season/meetings/per-weekend/OpenF1/live queries. Only supplementary
championship + news reads run, and only once the canonical context resolves.
- Fix the Prepare/analysis flow: /preview is a stable alias that renders the
preparation surface (PreSessionView) instead of redirecting back to the same
between-races screen.
- One primary navigation system per breakpoint: the mobile top-bar links are
hidden so the bottom bar is the sole primary nav, and Admin is moved out of
every Primary landmark into an operator-utilities toolbar.
- Add Vitest coverage for the contract mapping, every temporal state, loading/
error/limited surfaces, the no-fanout guarantee, the /preview CTA, and the nav
hierarchy; add hermetic Playwright journeys (seeded + injected canonical
payloads), 390/768/1440 overflow checks, and Weekend visual snapshots. Retire
the stale Command Center specs/snapshots.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-12 18:38:08 -04:00
|
|
|
export async function fetchWeekendContext(): Promise<WeekendContext> {
|
|
|
|
|
const res = await fetch('/api/v1/weekend-context')
|
2026-07-12 18:09:43 -04:00
|
|
|
if (!res.ok) {
|
|
|
|
|
throw new Error(`API ${res.status}: ${res.statusText}`)
|
|
|
|
|
}
|
|
|
|
|
return res.json()
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-03 00:09:47 -04:00
|
|
|
export async function fetchChampionshipHub(year?: number): Promise<ChampionshipHub> {
|
2026-07-03 01:11:31 -04:00
|
|
|
const params = new URLSearchParams({ source: 'auto' })
|
|
|
|
|
if (year) params.set('year', year.toString())
|
|
|
|
|
const url = `/api/v1/championship/hub?${params.toString()}`
|
2026-07-03 00:09:47 -04:00
|
|
|
const res = await fetch(url)
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
throw new Error(`API ${res.status}: ${res.statusText}`)
|
|
|
|
|
}
|
|
|
|
|
return res.json()
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-11 18:12:00 -04:00
|
|
|
export async function fetchDriverSummary(
|
|
|
|
|
driverNumber: number,
|
|
|
|
|
year?: number,
|
|
|
|
|
): Promise<DriverSummary> {
|
|
|
|
|
const params = new URLSearchParams({ driver_number: String(driverNumber) })
|
|
|
|
|
if (year) params.set('year', String(year))
|
|
|
|
|
const res = await fetch(`/api/v1/driver/summary?${params.toString()}`)
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
throw new Error(`API ${res.status}: ${res.statusText}`)
|
|
|
|
|
}
|
|
|
|
|
return res.json()
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 02:56:06 -04:00
|
|
|
export async function fetchLiveState(): Promise<LiveStateResponse> {
|
|
|
|
|
const res = await fetch('/api/v1/live/state')
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
throw new Error(`API ${res.status}: ${res.statusText}`)
|
2026-07-03 19:34:09 -04:00
|
|
|
}
|
|
|
|
|
return res.json()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchLiveTrackOutline(
|
|
|
|
|
session: LiveSessionMeta,
|
|
|
|
|
year = new Date().getFullYear(),
|
|
|
|
|
): Promise<TrackOutline> {
|
|
|
|
|
const params = new URLSearchParams({ year: year.toString() })
|
|
|
|
|
if (session.MeetingName) params.set('meeting_name', session.MeetingName)
|
|
|
|
|
if (session.CircuitName) params.set('circuit_name', session.CircuitName)
|
|
|
|
|
const res = await fetch(`/api/v1/track-outline?${params.toString()}`)
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
throw new Error(`API ${res.status}: ${res.statusText}`)
|
2026-05-25 02:56:06 -04:00
|
|
|
}
|
|
|
|
|
return res.json()
|
|
|
|
|
}
|
2026-05-25 13:04:29 -04:00
|
|
|
|
|
|
|
|
export async function fetchNews(limit?: number, source?: string): Promise<NewsItem[]> {
|
|
|
|
|
const params = new URLSearchParams()
|
|
|
|
|
if (limit) params.set('limit', limit.toString())
|
|
|
|
|
if (source) params.set('source', source)
|
2026-05-25 15:16:00 -04:00
|
|
|
|
2026-05-25 13:04:29 -04:00
|
|
|
const query = params.toString()
|
|
|
|
|
const url = query ? `/api/v1/news?${query}` : '/api/v1/news'
|
2026-05-25 15:16:00 -04:00
|
|
|
|
2026-05-25 13:04:29 -04:00
|
|
|
const res = await fetch(url)
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
throw new Error(`API ${res.status}: ${res.statusText}`)
|
|
|
|
|
}
|
|
|
|
|
return res.json()
|
|
|
|
|
}
|
2026-05-25 15:16:00 -04:00
|
|
|
|
|
|
|
|
export async function fetchNewsArticle(articleUrl: string): Promise<ArticleContent> {
|
|
|
|
|
const res = await fetch(`/api/v1/news/article?url=${encodeURIComponent(articleUrl)}`)
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
throw new Error(`API ${res.status}: ${res.statusText}`)
|
|
|
|
|
}
|
|
|
|
|
return res.json()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function markNewsRead(articleUrl: string): Promise<void> {
|
|
|
|
|
await fetch('/api/v1/news/read', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ url: articleUrl }),
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-07-04 00:18:10 -04:00
|
|
|
|
|
|
|
|
export async function fetchTelemetry(
|
|
|
|
|
sessionKey: number,
|
|
|
|
|
driverNumber: number,
|
|
|
|
|
): Promise<CarDataSample[]> {
|
|
|
|
|
const res = await fetch(
|
|
|
|
|
`/api/v1/telemetry?session_key=${sessionKey}&driver_number=${driverNumber}`,
|
|
|
|
|
)
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
throw new Error(`API ${res.status}: ${res.statusText}`)
|
|
|
|
|
}
|
|
|
|
|
const data = await res.json()
|
|
|
|
|
return Array.isArray(data) ? data : []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchLapsComparison(
|
|
|
|
|
sessionKey: number,
|
|
|
|
|
drivers?: number[],
|
|
|
|
|
): Promise<LapsComparisonResponse> {
|
|
|
|
|
const params = new URLSearchParams({ session_key: String(sessionKey) })
|
|
|
|
|
if (drivers?.length) {
|
|
|
|
|
params.set('drivers', drivers.join(','))
|
|
|
|
|
}
|
|
|
|
|
const res = await fetch(`/api/v1/laps/comparison?${params}`)
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
throw new Error(`API ${res.status}: ${res.statusText}`)
|
|
|
|
|
}
|
|
|
|
|
return res.json()
|
|
|
|
|
}
|