2026-03-03 00:48:05 -05:00
# CLAUDE.md
2026-03-27 19:27:38 -04:00
## Commands
```bash
go build -o box-box ./cmd/main.go # Build binary
go run cmd/main.go # Run directly
go test ./... # All 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)
```
2026-03-03 00:48:05 -05:00
## Project Overview
2026-03-27 19:27:38 -04:00
**box-box** is an F1 Terminal UI (TUI) dashboard built in Go with Bubble Tea. It shows live timing, standings, race calendar, driver telemetry, track maps, and race replay — all sourced from the OpenF1 API.
2026-03-03 00:48:05 -05:00
2026-03-27 19:27:38 -04:00
**Status**: Pre-beta, actively developed. All layers (API, models, UI) are fully implemented.
2026-03-03 00:48:05 -05:00
2026-03-27 19:27:38 -04:00
## Tech Stack
2026-03-03 00:48:05 -05:00
2026-03-27 19:27:38 -04:00
- **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 caching with TTL
2026-03-03 00:48:05 -05:00
2026-03-27 19:27:38 -04:00
## File Map
2026-03-03 00:48:05 -05:00
```
2026-03-27 19:27:38 -04:00
cmd/main.go Entry point (package main). Inits client, launches 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
```
2026-03-03 00:48:05 -05:00
## Architecture
2026-03-27 19:27:38 -04:00
### Bubble Tea Pattern
2026-03-03 00:48:05 -05:00
2026-03-27 19:27:38 -04:00
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.
2026-03-03 00:48:05 -05:00
2026-03-27 19:27:38 -04:00
Async work (API calls, WebSocket) returns `tea.Cmd` that emits typed messages back to Update. Use `tea.Batch()` for parallel fetches.
2026-03-03 00:48:05 -05:00
2026-03-27 19:27:38 -04:00
### Key Patterns
2026-03-03 00:48:05 -05:00
2026-03-27 19:27:38 -04:00
- **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
2026-03-03 00:48:05 -05:00
2026-03-27 19:27:38 -04:00
### Keybindings
2026-03-03 00:48:05 -05:00
2026-03-27 19:27:38 -04:00
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
2026-03-03 00:48:05 -05:00
2026-03-27 19:27:38 -04:00
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
2026-03-03 00:48:05 -05:00
2026-03-27 19:27:38 -04:00
- **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
2026-03-03 00:48:05 -05:00
2026-03-27 19:27:38 -04:00
## Testing
2026-03-03 00:48:05 -05:00
2026-03-27 19:27:38 -04:00
Tests in `openf1_test.go` hit the real OpenF1 API. They use `skipOnRateLimit(t, err)` to gracefully skip on HTTP 429. Require internet.
2026-03-03 00:48:05 -05:00
2026-03-27 19:27:38 -04:00
## Environment
2026-03-03 00:48:05 -05:00
2026-03-27 19:27:38 -04:00
- `OPENF1_API_KEY` — Optional Bearer token for paid tier (live session WebSocket access)
- Logs go to `box-box.log` in project root (prevents TUI pollution)
- Cache at `~/.cache/box-box/cache.db` (SQLite WAL mode, auto-created)