feat: add replay track map scrubber

Spike: replay location fan-out is bounded with a semaphore at 4 concurrent GetLocation calls, covered by TestAssembleReplayFramesBoundsLocationFanOut.

Adds /api/v1/replay/frames with 5s interval floor, 3000-frame cap, nearest-sample snapping, and empty-driver omission. Wires RaceStoryCanvas to lazy-load replay frames plus cached track outlines when the map panel opens, sharing the existing scrubber/playback state across chart and map.

Tests: HOME=/private/tmp/box-box-home GOCACHE=/private/tmp/box-box-go-cache GOMODCACHE=/Users/aman/go/pkg/mod go test ./internal/web; go build ./... (same env, passed with read-only module stat-cache warning); npm run test; npx tsc --noEmit. Full go test ./... is blocked in this sandbox by existing network/listener-dependent internal/api and internal/news tests.
This commit is contained in:
2026-07-11 18:17:15 -04:00
parent 5970649a9c
commit c2617d39bb
12 changed files with 1209 additions and 15 deletions

View File

@@ -10,6 +10,7 @@ import type {
Meeting,
NewsItem,
RaceHub,
ReplayFramesResponse,
Session,
TrackOutline,
Weekend,
@@ -80,6 +81,21 @@ export async function fetchTrackOutline(circuitKey: number, year: number): Promi
return data as TrackOutline
}
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()
}
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) {