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>
This commit is contained in:
2026-07-12 18:38:08 -04:00
parent 06223d992f
commit 07e5857760
30 changed files with 1468 additions and 811 deletions

View File

@@ -449,67 +449,73 @@ export interface DriverSummary {
}
// ── Weekend Context ──
// Canonical contract for the adaptive Weekend home. Served by
// /api/v1/weekend-context (sibling backend story #72) and, until that endpoint
// ships, derived client-side from existing endpoints (see lib/weekendContext.ts).
export type WeekendState =
| 'loading'
| 'error'
| 'limited_data'
| 'between_races'
// Canonical local-first contract for the adaptive Weekend home, served verbatim
// by /api/v1/weekend-context (backend story #72, internal/query/context.go).
// The frontend consumes this shape as the single source of truth; view-model
// derivation lives in lib/weekendContext.ts.
// TemporalState mirrors query.TemporalState exactly (the JSON `temporal_state`).
export type TemporalState =
| 'no_season'
| 'between_weekends'
| 'pre_session'
| 'between_sessions'
| 'session_live'
| 'session_settling'
| 'between_sessions'
| 'post_weekend'
| 'season_complete'
export interface WeekendPodiumEntry {
position: number
driver_number: number
name_acronym: string
team_name: string
team_colour: string
gap: string
// ContextAvailability mirrors query.ContextAvailability. Every field is present
// in a canonical payload except the optional `observed_at`.
export interface ContextAvailability {
schedule: string
live_transport: string
live_session: string
archive: string
local_analysis: string
freshness: string
observed_at?: string
limitations: string[]
}
export type WeekendSessionStatus = 'done' | 'live' | 'next' | 'upcoming'
export interface WeekendTimelineSession {
session_key: number
session_name: string
session_type: string
date_start: string
date_end: string
status: WeekendSessionStatus
// ContextSession mirrors query.ContextSession: a session identity coupled with
// its meeting and structured availability contract.
export interface ContextSession {
session: Session
meeting?: Meeting
availability: ContextAvailability
}
export interface WeekendCompletedEvent {
meeting_key: number
meeting_name: string
country_code: string
country_flag: string
circuit_short_name: string
round?: number
analysis_session_key: number
analysis_session_name: string
label: string
podium: WeekendPodiumEntry[]
story?: string
// WeekendContext mirrors query.WeekendContext (the canonical endpoint body).
export interface WeekendContext {
season?: number
temporal_state: TemporalState
previous_meeting?: Meeting
focus_meeting?: Meeting
next_meeting?: Meeting
previous_completed_session?: ContextSession
active_session?: ContextSession
next_session?: ContextSession
default_analysis_session?: ContextSession
championship_round: number
total_championship_rounds: number
}
export interface WeekendUpcomingEvent {
meeting_key: number
meeting_name: string
country_code: string
country_flag: string
circuit_short_name: string
circuit_key?: number
round?: number
date_start?: string
next_session_name?: string
next_session_start?: string
}
// ── Weekend view model (client-only) ──
// The rendered Weekend home layers a small set of non-canonical UI states
// (loading/error) plus supplementary data (championship movers, briefing) on top
// of the canonical context. None of these are part of the #72 contract.
export type WeekendViewState =
| 'loading'
| 'error'
| 'no_season'
| 'between_weekends'
| 'pre_session'
| 'session_live'
| 'session_settling'
| 'between_sessions'
| 'post_weekend'
| 'season_complete'
export interface WeekendChampionshipMover {
position: number
@@ -534,32 +540,13 @@ export interface WeekendBriefingItem {
image_url?: string
}
export type WeekendRoundStatus = 'completed' | 'next' | 'upcoming'
export interface WeekendSeasonRound {
round: number
meeting_key: number
country_code: string
country_flag: string
status: WeekendRoundStatus
analysis_session_key?: number
}
export interface WeekendContext {
state: WeekendState
season: number
message?: string
live?: boolean
active_meeting_name?: string
active_circuit_short_name?: string
last_event?: WeekendCompletedEvent
next_event?: WeekendUpcomingEvent
last_session?: WeekendCompletedEvent
next_session?: WeekendTimelineSession
sessions?: WeekendTimelineSession[]
championship_impact?: WeekendChampionshipImpact
season_rounds?: WeekendSeasonRound[]
briefing?: WeekendBriefingItem[]
export interface WeekendPodiumEntry {
position: number
driver_number: number
name_acronym: string
team_name: string
team_colour: string
gap: string
}
export interface NewsItem {