**Goal**: The system automatically fetches the full F1 race calendar and serves it as structured data — users can see all sessions for the current season with correct times.
**Depends on**: Phase 1
**Requirements**: SCHED-01
**Success Criteria** (what must be TRUE):
1. The `/schedule` API endpoint returns all races for the current season with session types (FP1-3, Qualifying, Sprint, Race) and UTC-correct timestamps
2. Schedule data persists to NFS and is served correctly after a pod restart without re-fetching the API
3. APScheduler triggers a background refresh of schedule data at least once daily without manual intervention
4. A race that has already occurred shows a "past" status and an upcoming race shows "upcoming" status
**Plans**: TBD
### Phase 3: Extractor Framework and First Site
**Goal**: The extractor plugin system is in place and at least one site extractor returns a valid, live HLS URL — proving the end-to-end extraction architecture.
**Depends on**: Phase 2
**Requirements**: EXTR-01, EXTR-02, EXTR-03
**Success Criteria** (what must be TRUE):
1. The extractor registry lists all registered site extractors and dispatches to the correct one by site key
2. The first site extractor returns a working m3u8 URL that plays when pasted into VLC, including passing any CSRF or token requirements
3. Background polling runs automatically on the APScheduler, re-extracts streams at a configured interval, and caches results in Redis with a TTL
4. Adding a second extractor requires only creating a new class file and registering it — no changes to the dispatcher or other extractors
5. Extractor failures are logged with enough detail to identify exactly which step failed (request, token parse, URL extraction)
**Plans**: TBD
### Phase 4: Stream Health and Fallback
**Goal**: Only verified-live streams reach users, broken streams are flagged, and when multiple sources exist the system automatically tries the next one on failure.
1. The `/streams` API endpoint only returns streams that have passed a HEAD/partial-GET liveness check within the last health-check interval
2. A stream that returns a non-200 or empty playlist is marked as dead and excluded from the API response without manual intervention
3. The `/streams` response includes bitrate and liveness metadata per stream so the frontend can display stream quality
4. When configured with multiple sources for the same session, the API returns them in reliability-ranked order (most recently verified first)
**Plans**: TBD
### Phase 5: HLS Proxy Core
**Goal**: The proxy layer converts raw CDN HLS URLs into browser-playable same-origin URLs with full CORS support — a stream URL from the extractor can be played in any browser via the proxy.
**Depends on**: Phase 4
**Requirements**: PRXY-01, PRXY-02, PRXY-03
**Success Criteria** (what must be TRUE):
1. Fetching `/proxy?url=<master-m3u8>` returns an m3u8 where every URI at every level (master, variant, segment) points back through the `/relay` endpoint — zero requests escape to the original CDN domain
2. A browser playing a proxied stream completes all preflight CORS checks without errors, including the `Range` header
3. Segment relay streams bytes to the browser as chunked transfer with no full-segment buffering — peak memory per active stream stays under 5 MB
4. The proxy correctly handles both master playlists (multi-variant) and media playlists (single-variant) without special-casing at the caller
**Plans**: TBD
### Phase 6: CDN Token Lifecycle and Quality
**Goal**: Streams stay alive for full 2+ hour F1 sessions without user intervention, and users can select video quality when multiple variants are available.
**Depends on**: Phase 5
**Requirements**: EXTR-04, PRXY-04, PRXY-05
**Success Criteria** (what must be TRUE):
1. A stream that has been playing for 90 minutes continues without interruption — the background token refresh loop re-extracts and updates the cached URL before the CDN token expires
2. The `/streams` response exposes available quality variants (resolution labels) for streams that provide multi-variant playlists
3. Selecting a different quality variant via the API returns a proxied URL for that specific variant stream
4. Token refresh failures are logged and surface in stream health status without crashing the relay or affecting other active streams
**Plans**: TBD
### Phase 7: Frontend Core — Schedule, Picker, and Player
**Goal**: Users can open the service in a browser, see the F1 session schedule, pick a live stream from the available sources, and watch it in an embedded player on the same page.
**Depends on**: Phase 6
**Requirements**: FRNT-01, FRNT-02
**Success Criteria** (what must be TRUE):
1. The schedule page lists all upcoming and past sessions grouped by Grand Prix, with correct local-timezone display and live/upcoming/past badges
2. Clicking a live session shows a stream picker with available sources labeled by site name and liveness status
3. Selecting a stream loads and begins playing it in the embedded hls.js player without leaving the page
4. The player recovers from transient network errors automatically and displays a clear error message only on unrecoverable failure
5. The app is usable on a desktop browser without requiring any browser extension or plugin
**Plans**: TBD
### Phase 8: Multi-Stream Layout
**Goal**: Users can watch multiple streams side by side simultaneously — for example, the main race feed alongside a specific driver onboard camera.
**Depends on**: Phase 7
**Requirements**: FRNT-03
**Success Criteria** (what must be TRUE):
1. The user can add a second stream to the view and both play simultaneously in a split-screen layout without audio or video interference between streams
2. The layout adapts gracefully when two streams are loaded — each player gets equal visible area and independent controls
3. Removing one stream from the multi-stream view does not interrupt the other stream