Files
box-box/CLAUDE.md
AmanTahiliani 3c6115e815 Make Playwright runs hermetic and fix stale race-hub specs
Six suite failures had rotted in: three race-hub specs targeted the
sub-tabbed Race Story UI deleted in the Phase 22 redesign, and the
command-center specs depended on the real OpenF1 calendar, whose focus
weekend drifted away from the seeded Monaco session as time passed.

The e2e/visual configs now point BOXBOX_OPENF1_BASE_URL (new override)
at an unreachable address so runs are hermetic and date-stable; the
race-hub specs were rewritten against the current RaceStoryCanvas UI,
and the one drifted mobile snapshot regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 00:48:07 -04:00

12 KiB

CLAUDE.md

Commands

go build -o box-box ./cmd/main.go    # Build binary
go run cmd/main.go                   # Run TUI
go run cmd/main.go --web --port 8080 # Run web server (default port 8080)
go run cmd/main.go --ingest-year 2025  # Ingest a season into the domain DB (also: --ingest-meeting, --ingest-session, --ingest-news, --coverage)
go test ./...                        # All Go tests
go test -v ./internal/api            # API integration tests (requires internet, rate-limit aware)
OPENF1_API_KEY=key go run cmd/main.go  # Run with paid tier (enables live session access)

# Frontend (run inside frontend/)
npm run dev                          # Vite dev server on :5173, proxies /api to :8080 (override with BOXBOX_API_PORT)
npm run build                        # tsc --noEmit && vite build -> frontend/dist
npm run test                         # Vitest unit tests (frontend/src/test/); npm run test:watch for watch mode

# E2E / visual (run at repo root; Playwright auto-starts a seeded Go server + Vite dev server)
npm run test:e2e                     # Playwright e2e (tests/*.spec.ts, playwright.config.ts)
npm run test:visual                  # Visual snapshots (tests/visual/, playwright.visual.config.ts)
npm run test:visual:update           # Regenerate visual snapshots
# :prod variants (test:e2e:prod, test:visual:prod) run against the *.prod.config.ts configs

Project Overview

box-box is an F1 dashboard in Go with two frontends sharing the same data layer: a Bubble Tea TUI and a web app (--web flag) — a Go HTTP server in internal/web/ serving a React SPA plus a REST/SSE API. It shows live timing, standings, race calendar, driver telemetry, track maps, and race replay — sourced from the OpenF1 API and a local domain SQLite DB filled by the ingestion CLI.

Status: Pre-beta, actively developed.

Tech Stack

  • Bubble Tea — TUI framework (Elm architecture: Model -> Update -> View)
  • Lipgloss — Terminal styling
  • Bubbles — TUI components (spinner, viewport, table)
  • OpenF1 API — F1 data at https://api.openf1.org
  • gorilla/websocket — Official F1 SignalR live feed
  • modernc.org/sqlite — HTTP response cache + domain DB
  • React 18 + Vite + TypeScript — Web frontend (frontend/), TanStack Router + TanStack Query
  • Vitest + Testing Library — Frontend unit tests; Playwright — e2e and visual tests (repo root)

File Map

cmd/main.go                  Entry point. Flags: --web/--port (web server), --ingest-* /
                             --backfill-season/--coverage (ingestion CLI), --db. Default: TUI.

internal/api/
  client.go                  OpenF1Client: HTTP client, 15s timeout, optional Bearer auth
  cache.go                   SQLite cache (~/.cache/box-box/cache.db), TTL-based, stale fallback
  openf1.go                  37 API endpoint methods
  openf1_test.go             Integration tests (real API, rate-limit skip)

internal/models/
  types.go                   All data structs (Meeting, Session, Driver, Lap, Stint, etc.)

internal/ui/
  app.go                     Root AppModel. 7 tabs, message routing, splash screen
  messages.go                All tea.Msg types for async data loading
  styles.go                  Lipgloss styles, F1 color palette, team colors
  keys.go                    key.Binding definitions for all keybindings
  util.go                    Helpers: formatSeconds, sparkline, matchKey, country flags
  dashboard.go               Tab 0: Next race countdown + session schedule
  standings.go               Tab 1: Driver/constructor championship tables
  calendar.go                Tab 2: Season meeting list, select -> race detail
  racedetail.go              Tab 3: Session results, grid, sectors, RC, weather, overtakes
  driver.go                  Tab 4: Driver list + per-driver telemetry (stints, laps, pits)
  official_live.go           Tab 5: Real-time timing via F1 SignalR WebSocket
  live.go                    Legacy/alternate live timing implementation
  trackmap.go                Tab 6: ASCII track outline with live car positions
  battles.go                 Sub-view: Auto-detected on-track battles with gap sparkline
  pitwindow.go               Sub-view: Pit stop rejoin position calculator
  replay.go                  Sub-view: Lap-by-lap race replay scrubber

internal/store/              Domain SQLite DB (~/.local/share/box-box/boxbox.db), season/session data
internal/ingest/             OpenF1 -> domain DB ingestion (driven by cmd/main.go --ingest-* flags)
internal/query/              Read models over the domain DB, used by web handlers
internal/news/               RSS/Atom paddock briefing feed ingestion (--ingest-news)
internal/live/               Shared F1 SignalR live feed client + LiveStreamData types

internal/web/
  server.go                  HTTP server: route table, CORS/logging middleware, SPA file server.
                             Serves frontend/dist if found (walks up from cwd), else embedded assets/
  api.go                     REST handlers under /api/v1/ (results, laps, telemetry, championship
                             hub aggregation, news + readability article extraction, ...)
  live.go                    SSEHub + SignalR bridge: relays official F1 feed to SSE subscribers
  racehub.go                 /api/v1/race-hub: per-session payload assembled from the domain DB
  navigation.go              Local-first navigation: /api/v1/seasons, meetings, sessions, weekend
  source.go                  ?source=openf1|local|auto data-source resolution
  assets/                    Embedded fallback SPA (legacy vanilla JS; used when no frontend/dist)

frontend/                    React + Vite + TypeScript SPA
  src/main.tsx               Entry: QueryClientProvider + RouterProvider
  src/router.tsx             TanStack Router: / (command center), /race-hub, /live, /championship,
                             /briefing, /data-library (also /admin alias)
  src/api.ts                 Typed fetch wrappers for /api/v1/ endpoints
  src/types.ts               TypeScript mirrors of API payloads
  src/pages/                 CommandCenterPage, RaceHubPage, LiveTimingPage, ChampionshipPage,
                             BriefingPage, DataLibraryPage
  src/components/            Shared components (Nav, TabBar, race hub views, live/ timing tower)
  src/lib/                   Client helpers: live SSE parsing, schedule, coverage, GP identity
  src/test/                  Vitest + Testing Library unit tests

tests/                       Playwright e2e specs; tests/visual/ visual snapshot specs
playwright*.config.ts        Dev/prod e2e + visual configs (webServer blocks seed a temp domain DB
                             and start Go API + Vite automatically)
scripts/seed-e2e-db/         Seeds the throwaway domain DB used by Playwright runs

Architecture

Bubble Tea Pattern

Each tab is a sub-model with Init(), Update(msg), View(). The root AppModel in app.go holds all sub-models and routes messages by type. All state changes are message-driven — no direct mutation.

Async work (API calls, WebSocket) returns tea.Cmd that emits typed messages back to Update. Use tea.Batch() for parallel fetches.

Web Layer

box-box --web starts an HTTP server (default :8080) with three surfaces:

  • REST API at /api/v1/... — Handlers in internal/web/api.go wrap OpenF1Client; navigation/race-hub endpoints read the domain DB via internal/query (empty responses if the DB is missing). ?source=openf1|local|auto picks the data source where supported. Register routes in routes() in server.go — Go ServeMux longest-prefix matching means more specific paths (e.g. /api/v1/laps/comparison) must be registered before their prefixes.
  • SSE live streaminternal/web/live.go runs a background SignalR connection to the official F1 feed (exponential-backoff reconnect; disabled with BOXBOX_DISABLE_LIVE=1). An SSEHub broadcasts snapshots to browsers on /api/v1/live/stream, with /api/v1/live/state for the initial snapshot and a 20s heartbeat. LiveTimingPage.tsx consumes it; parsing helpers live in frontend/src/lib/live.ts.
  • Embedded SPA — Static file server with SPA fallback to index.html. Prefers a frontend/dist directory found by walking up from cwd (so npm run build output is served without rebuilding Go); otherwise serves the legacy assets embedded via //go:embed assets.
  • Championship hub/api/v1/championship/hub aggregates official standings with derived stats (wins, podiums, poles, last-5 form, teammate head-to-head) and per-round cumulative points, computed from all season race results.

Frontend dev loop: run go run cmd/main.go --web and npm run dev in frontend/ — Vite proxies /api to the Go server.

Key Patterns

  • Two-phase standings load: GetLatestDriverChampionship() -> extract SessionKey -> GetDriversForSession(sessionKey) -> join by DriverNumber for names/colors
  • Driver tab lazy load: Drivers loaded on first Tab 4 focus via TriggerLoad()
  • Stale data fallback: When API errors, client returns expired cache data + sets atomic flag for UI disclaimer banner
  • Cache TTL tiers: 15min (live telemetry), 1hr (standings), 24hr (recent), forever (historical 2023/2024)
  • Track outline pre-fetch: Background fetch of circuit GPS data during app init
  • matchKey helper: Renamed from key to avoid collision with bubbles/key package import

Keybindings

Global: 1-7 tabs, tab/shift+tab cycle, j/k navigate, enter select, b/esc back, y cycle year, g/G top/bottom, ctrl+u/ctrl+d half-page, q quit

Standings: d driver view, c constructor view

Race Detail: [/] prev/next session, r replay mode, K/J scroll RC

Live: s sectors, r race control, b battles, p pit window, K/J scroll RC

Replay: h/l or arrows scrub laps

API Endpoint Groups

  • Season: GetMeetingsForYear, GetSessionsForMeeting
  • Championship: GetDriverChampionshipForYear, GetTeamChampionshipForYear, GetLatest*
  • Results: GetSessionResult, GetStartingGrid, GetStintsForSession
  • Telemetry: GetLapsForDriver, GetPitStopsForSession, GetPositions, GetIntervals
  • Live: GetCarData, GetLocation (GPS), GetTeamRadio
  • Events: GetRaceControl, GetOvertakes, GetWeather
  • Track: PrefetchTrackOutlines

How To Extend

  • New tab: Create model in internal/ui/, add to AppModel struct in app.go, add tab constant, implement Init/Update/View, handle message routing in app.go Update()
  • New API endpoint: Add method to openf1.go, add response struct to types.go, set cache TTL in the method
  • New message type: Define in messages.go, handle in relevant model's Update()
  • New keybinding: Define in keys.go, handle in relevant model's Update()
  • New styles: Add to styles.go, reference F1 palette constants
  • New web page/route: Create page in frontend/src/pages/, register route in frontend/src/router.tsx, add nav link in frontend/src/components/Nav.tsx, add fetchers to src/api.ts and payload types to src/types.ts, add a test in frontend/src/test/
  • New web API endpoint: Add handler in internal/web/api.go (or a new file in internal/web/), register it in routes() in server.go (mind prefix ordering), add a handler test alongside (see championship_hub_test.go)

Testing

  • Go: tests in openf1_test.go hit the real OpenF1 API and use skipOnRateLimit(t, err) to skip on HTTP 429 (require internet). internal/web handler tests run offline.
  • Frontend: Vitest + Testing Library in frontend/src/test/ (npm run test inside frontend/).
  • E2E/visual: Playwright at repo root (npm run test:e2e, npm run test:visual). Configs seed a temp domain DB and start the Go server with BOXBOX_DISABLE_LIVE=1 and BOXBOX_OPENF1_BASE_URL pointed at an unreachable address (hermetic: no live OpenF1 dependency, date-stable) plus a Vite dev server — no manual setup needed.

Environment

  • OPENF1_API_KEY — Optional Bearer token for paid tier (live session WebSocket access)
  • BOXBOX_DISABLE_LIVE=1 — Skip the background SignalR live feed in web mode (used by e2e)
  • BOXBOX_OPENF1_BASE_URL — Override the OpenF1 API root (default https://api.openf1.org; e2e sets an unreachable address for hermetic runs)
  • BOXBOX_API_PORT — Go API port that the Vite dev proxy targets (default 8080)
  • Logs: TUI writes box-box.log in project root; web/ingest modes log to stderr
  • HTTP cache at ~/.cache/box-box/cache.db; domain DB at ~/.local/share/box-box/boxbox.db (override with --db)