diff --git a/documentations/refactor/17-phase-9-navigation-data-api.md b/documentations/refactor/17-phase-9-navigation-data-api.md
new file mode 100644
index 0000000..27c29aa
--- /dev/null
+++ b/documentations/refactor/17-phase-9-navigation-data-api.md
@@ -0,0 +1,52 @@
+# Phase 9 Navigation Data API
+
+## Purpose
+
+Race Hub now has useful local-first session views, but it still depends on a
+manual `session_key`. Phase 9 should make the backend expose enough local
+navigation data for the Web UI to become race-weekend-first: season calendar,
+meeting detail, sessions, and ingestion coverage.
+
+This is a backend/read-model slice for Cursor. Keep the React redesign for the
+following phase.
+
+## Scope
+
+Add local-first Web API endpoints/read models for:
+
+- seasons or available years in the domain database;
+- meetings for a year;
+- one meeting/weekend with its sessions;
+- per-session dataset coverage using the same dataset vocabulary as Race Hub;
+- a sensible "latest available" or "default session" helper if it can be done
+ without guessing from remote API data.
+
+## Backend Work
+
+Expected changes:
+
+- add query-layer read models in `internal/query` for calendar/weekend data;
+- add store reads if existing methods are insufficient;
+- add HTTP handlers in `internal/web`;
+- keep responses local-first and deterministic;
+- expose empty but well-shaped responses when the database has no ingested
+ meetings;
+- add offline tests using temporary SQLite databases.
+
+## Guardrails
+
+- Do not fetch OpenF1 from these read endpoints.
+- Do not make React depend on OpenF1 directly.
+- Do not start frontend navigation implementation in this phase.
+- Keep endpoint names stable and boring; this is app infrastructure, not a
+ product copywriting exercise.
+- Keep the existing Race Hub API working unchanged.
+
+## Acceptance Criteria
+
+- Web API can list ingested years and meetings.
+- Web API can return a meeting/weekend with sessions.
+- Each session includes dataset coverage needed to guide users into Race Hub.
+- Empty database behavior is explicit and tested.
+- Focused Go tests pass.
+- Existing frontend unit/build/e2e checks still pass.
diff --git a/documentations/refactor/README.md b/documentations/refactor/README.md
index 7a48099..c990198 100644
--- a/documentations/refactor/README.md
+++ b/documentations/refactor/README.md
@@ -66,8 +66,11 @@ not implementation tickets yet.
- [16 Phase 8 Analytics Visuals](16-phase-8-analytics-visuals.md): frontend
slice for turning the newly available analytics datasets into useful Race Hub
views.
-- [Claude Phase 8 Prompt](claude-phase-8-analytics-visuals-prompt.md): current
- handoff prompt for the next Claude frontend phase.
+- [17 Phase 9 Navigation Data API](17-phase-9-navigation-data-api.md): backend
+ slice for local-first season/weekend/session navigation so users do not need
+ raw session keys.
+- [Cursor Phase 9 Prompt](cursor-phase-9-navigation-data-api-prompt.md):
+ current handoff prompt for the next Cursor backend phase.
## External References
diff --git a/documentations/refactor/claude-phase-8-analytics-visuals-prompt.md b/documentations/refactor/claude-phase-8-analytics-visuals-prompt.md
deleted file mode 100644
index cac2222..0000000
--- a/documentations/refactor/claude-phase-8-analytics-visuals-prompt.md
+++ /dev/null
@@ -1,88 +0,0 @@
-# Prompt For Claude: Phase 8 Analytics Visuals
-
-You are the frontend/UI engineer for Phase 8 of `box-box`. Please keep context
-usage low: do not read the whole refactor docs folder. Start with the files
-listed below and only open more if you are blocked.
-
-## Goal
-
-Turn the Race Hub Strategy and Position tabs from placeholders into real views
-powered by the local-first `/api/v1/race-hub` response.
-
-## Read First
-
-Open only these first:
-
-- `frontend/src/pages/RaceHubPage.tsx`
-- `frontend/src/components/StrategyView.tsx`
-- `frontend/src/components/PositionEvolutionView.tsx`
-- `frontend/src/types.ts`
-- `tests/race-hub.spec.ts`
-- `scripts/seed-e2e-db/main.go`
-
-Optional, only if you need design guidance:
-
-- `documentations/refactor/16-phase-8-analytics-visuals.md`
-- `documentations/refactor/06-visual-design-direction.md`
-
-## Current Backend Contract
-
-`RaceHub` already includes these arrays:
-
-- `stints`
-- `pit_stops`
-- `positions`
-- `race_control`
-- `weather`
-- `laps`
-
-Dataset availability is still reported under `datasets`.
-
-Seeded e2e sessions:
-
-- `9472`: has core data plus analytics data.
-- `9000`: has core data only, so missing-data states must still render.
-
-## Work To Do
-
-1. Update `RaceHubPage.tsx` to pass analytics arrays into the Strategy and
- Position components.
-2. Replace `"Strategy chart: not yet implemented."` with a real strategy view:
- per-driver stint bars, compound labels/colors, lap ranges, and pit context.
-3. Replace `"Position evolution chart: not yet implemented."` with a real
- position view: per-driver progression from `positions`, plus grid/finish
- context when available.
-4. Preserve honest missing-data states for sessions without analytics.
-5. Update tests so they assert real analytics UI for session `9472`, not
- placeholder text.
-
-## Design Constraints
-
-- Keep it dense, technical, and F1-native.
-- Use SVG/CSS for this first slice unless a dependency is truly necessary.
-- Team color identifies drivers; compound color identifies tyre data.
-- Avoid generic dashboard card sludge, decorative gradients, and fake runtime
- mock data.
-- Keep mobile/iPad usable.
-
-## Verification
-
-Run:
-
-```bash
-cd frontend && npm test -- --run
-cd frontend && npm run build
-npm run test:e2e
-```
-
-The root e2e command starts a seeded local database and local web/API servers.
-It should not need OpenF1 network access.
-
-## Report Back
-
-Summarize:
-
-- files changed;
-- UI behavior added;
-- tests run and results;
-- follow-up risks or refinements.
diff --git a/documentations/refactor/cursor-phase-9-navigation-data-api-prompt.md b/documentations/refactor/cursor-phase-9-navigation-data-api-prompt.md
new file mode 100644
index 0000000..dec3425
--- /dev/null
+++ b/documentations/refactor/cursor-phase-9-navigation-data-api-prompt.md
@@ -0,0 +1,89 @@
+# Prompt For Cursor: Phase 9 Navigation Data API
+
+You are working in the `box-box` repository as the backend engineer for Phase
+9. Please keep this phase focused: add local-first navigation APIs so the
+frontend can later stop requiring users to know raw `session_key` values.
+
+## Read First
+
+Open these files first:
+
+- `documentations/refactor/17-phase-9-navigation-data-api.md`
+- `internal/query/racehub.go`
+- `internal/web/racehub.go`
+- `internal/web/server.go`
+- `internal/store/store.go`
+- `internal/store/models.go`
+- `internal/store/store_test.go`
+- `scripts/seed-e2e-db/main.go`
+
+Only open older planning docs if you need context.
+
+## Goal
+
+Implement local-first Web API read models for season/weekend/session
+navigation. These endpoints must read from the SQLite domain database only.
+They must not fetch OpenF1 on demand.
+
+## Suggested API Shape
+
+Use boring, stable names unless the codebase suggests a better convention:
+
+- `GET /api/v1/seasons`
+ - returns years available in the local domain DB.
+- `GET /api/v1/meetings?year=2025`
+ - returns locally ingested meetings for that year.
+- `GET /api/v1/weekend?meeting_key=1229`
+ - returns meeting metadata, sessions, and per-session dataset coverage.
+
+Dataset coverage should reuse the Race Hub dataset vocabulary where practical:
+
+- meeting
+- session
+- drivers
+- results
+- starting_grid
+- stints
+- pit_stops
+- positions
+- race_control
+- weather
+- laps
+
+## Implementation Notes
+
+- Add query-layer structs/methods in `internal/query`; keep HTTP handlers thin.
+- Add store read methods only where needed.
+- Empty DB should return valid empty arrays, not 500s.
+- Missing meeting should return a clear 404 from the web handler.
+- Add tests against temp SQLite databases.
+- If you touch the e2e seed, keep session `9472` as full data and `9000` as
+ core-only data.
+
+## Do Not Do
+
+- Do not build the React navigation UI yet.
+- Do not add remote OpenF1 calls to these endpoints.
+- Do not change the existing Race Hub response shape.
+- Do not add live timing persistence in this phase.
+
+## Verification
+
+Run:
+
+```bash
+go test ./internal/store/... ./internal/query/... ./internal/web/...
+go build -o /private/tmp/box-box ./cmd/main.go
+cd frontend && npm test -- --run
+cd frontend && npm run build
+npm run test:e2e
+```
+
+## Report Back
+
+Summarize:
+
+- files changed;
+- endpoint shapes added;
+- tests run and results;
+- follow-up risks or frontend handoff notes.
diff --git a/frontend/src/components/PositionEvolutionView.tsx b/frontend/src/components/PositionEvolutionView.tsx
index f04c180..bcd7b10 100644
--- a/frontend/src/components/PositionEvolutionView.tsx
+++ b/frontend/src/components/PositionEvolutionView.tsx
@@ -1,20 +1,22 @@
-import type { EnrichedResult, EnrichedGrid } from '../types'
+import type { EnrichedResult, EnrichedGrid, PositionSample, Lap } from '../types'
import { gridDelta, gridDeltaClass } from '../utils'
interface Props {
results: EnrichedResult[]
grid: EnrichedGrid[]
+ positions: PositionSample[]
+ laps: Lap[]
hasPositions: boolean
}
-export function PositionEvolutionView({ results, grid, hasPositions }: Props) {
+export function PositionEvolutionView({ results, grid, positions, laps: _laps, hasPositions }: Props) {
if (!hasPositions) {
return (
- Lap-by-lap positions not available. The backend does not
- yet expose position samples in /api/v1/race-hub. Evolution
- charts require per-driver position per lap.
+ Lap-by-lap positions not available. This session does not
+ have ingested position samples in /api/v1/race-hub. Evolution
+ charts require per-driver position samples over time.
{results.length > 0 && grid.length > 0 && (
@@ -73,8 +75,165 @@ export function PositionEvolutionView({ results, grid, hasPositions }: Props) {
)
}
- // Placeholder for when position samples are available
+ // Build time-indexed position series per driver
+ const allTimes = [...new Set(positions.map((p) => p.date))].sort()
+
+ if (allTimes.length === 0) {
+ return
No position samples in this dataset.
+ }
+
+ const tMin = new Date(allTimes[0]).getTime()
+ const tMax = new Date(allTimes[allTimes.length - 1]).getTime()
+ const tRange = Math.max(tMax - tMin, 1)
+
+ const byDriver = new Map>()
+ for (const p of positions) {
+ if (!byDriver.has(p.driver_number)) byDriver.set(p.driver_number, [])
+ byDriver.get(p.driver_number)!.push({
+ t: (new Date(p.date).getTime() - tMin) / tRange,
+ pos: p.position,
+ })
+ }
+ for (const samples of byDriver.values()) {
+ samples.sort((a, b) => a.t - b.t)
+ }
+
+ const maxPos = Math.max(...positions.map((p) => p.position), results.length, 2)
+ const colorByDriver = new Map(results.map((r) => [r.driver_number, r.team_colour]))
+ const acronymByDriver = new Map(results.map((r) => [r.driver_number, r.name_acronym]))
+
+ const W = 640
+ const H = 180
+ const PL = 40
+ const PR = 48 // right margin for driver labels
+ const PT = 8
+ const PB = 8
+ const plotW = W - PL - PR
+ const plotH = H - PT - PB
+
+ const toX = (t: number) => PL + t * plotW
+ const toY = (pos: number) => PT + ((pos - 1) / Math.max(maxPos - 1, 1)) * plotH
+
return (
-
- Stints not available. The backend does not yet expose
+ Stints not available. This session does not have ingested
tyre compound and stint ranges in /api/v1/race-hub. Strategy
charts require per-driver stints: compound, lap_start, lap_end.