A personal F1 dashboard. Editorial newsprint × paddock telemetry.
Live web: privatepitwall.vercel.app · Mobile: scan with Expo Go
|
exp://u.expo.dev/67389a52-0a95-4023-9002-b8a6a234b97e/group/06d79723-6445-4552-b79c-7fd93838c30f
|
A single-page F1 2026 season dashboard with per-race and per-driver detail pages, shipping to web (vanilla HTML/CSS/JS) and mobile (Expo React Native). Real data from the Jolpica F1 API flows through a FastAPI normalization layer with SWR caching on Upstash Redis, deployed to Vercel as a single project — static frontend on the same domain as the Python serverless backend.
The aesthetic is intentional: warm paper (#f1ebdd) + carbon (#0a0a09),
exactly one accent (#d93423 racing red), reserved gold for P1. Two
display fonts (Instrument Serif, JetBrains Mono) carry the entire system.
No Tailwind, no design libraries, no glassmorphism — hand-authored CSS
with tokens, motion limited to CSS keyframes (web) and Reanimated (mobile),
both honoring prefers-reduced-motion.
Single design token source. Colors, type, spacing, motion durations
all live in shared/design/tokens.json. Web reads them into CSS variables;
mobile imports the JSON directly. Adding a new team color is one edit; no
hex codes scattered across components.
Data is always behind a provider. Clients never reach into the JSON
file or the FastAPI URL directly — they go through getProvider(). A
single env flag flips between StaticProvider (bundled JSON) and
LiveProvider (FastAPI). Both implement the same DataProvider interface.
SWR cache with a Protocol abstraction. The server's Cache Protocol
defines get_or_refresh(key, fresh_ttl_s, stale_until_s, refresh) and has
two implementations: InMemoryCache for dev/tests and UpstashCache for
serverless prod. Swapping backends required zero callsite changes — the
abstraction was designed for the swap from day one.
Three-tier fallback chain. Live → retry live → bundled static snapshot → inline emergency payload. Failed network calls never produce a blank page or red error modal; stale data shows a calm ink-3 chip and the UI keeps rendering. PRD §13 enumerates every failure mode and its designed response.
Editorial overlay vs. live data. Curated copy (driver bios, race
briefings, watchlist items, ticker) lives in
server/app/data/editorial-2026.json as a separate layer. The mapper
turns Jolpica's results into a typed SeasonSnapshot; the editorial
overlay merges curation onto it server-side. Bio prose substitutes the
real team name dynamically — Leclerc reads "Ferrari team orders," not a
hardcoded "Mercedes" string from the original mockup.
Drift-guard discipline. Where two implementations of the same logic must coexist (TS provider + JS data-loader during the static-bundle era), both files carry comments explicitly flagging them as twins. Phase B1 collapsed both into a single composed bundle emitted by the server, removing the drift-guard burden entirely.
Same-origin deploy. One Vercel project serves both web/ (static)
and server/ (Python serverless) at the same domain via vercel.json
rewrites. The frontend hits /v1/snapshot as a same-origin request — no
CORS preflight, no separate API host, one URL to remember.
| Layer | Tech |
|---|---|
| Web | Vanilla HTML/CSS/JS — no build step, no framework |
| Mobile | Expo SDK 52 + expo-router + TypeScript + Reanimated |
| Server | FastAPI + httpx + pydantic v2 (Python 3.11) |
| Cache | Upstash Redis (REST API, HTTP-based, serverless-friendly) |
| Data | Jolpica F1 API (Ergast-compatible) |
| Deploy | Vercel — static frontend + Python serverless, single project |
| Tests | Playwright (web) + Jest/RNTL (mobile) + pytest + httpx mock |
| Tooling | uv (Python deps), no monorepo orchestrator (per-subproject npm) |
┌──────────────────────────┐
Browser / mobile ──> │ data-loader.js / │ ──> static fallback
│ StaticProvider/Live │ (bundled JSON)
└────────────┬─────────────┘
│ live mode
v
┌──────────────────────────┐
│ /v1/snapshot │
│ /v1/race/{r} │ ── FastAPI
│ /v1/driver/{c} │
└────────────┬─────────────┘
│
v
┌──────────────────────────┐
│ SWR cache (Upstash) │
│ fresh 10m / stale 24h │
└────────────┬─────────────┘
│ miss
v
┌──────────────────────────┐
│ Jolpica F1 API + │
│ editorial overlay │
└──────────────────────────┘
web/ vanilla HTML/CSS/JS dashboard (Playwright tests)
mobile/ Expo React Native app (Jest + RNTL)
server/ FastAPI backend (pytest + httpx mocks)
shared/ TS types, DataProvider, design tokens, static JSON
api/ Vercel Python serverless entry (imports server/app)
scripts/ Test runner + bundle regen + upstream fixture capture
docs/superpowers/ Design specs and implementation plans (the trail)
PRD.md Authoritative product spec
PROGRESS.md Append-only ledger of completed work
CLAUDE.md Codebase conventions for future sessions
# Backend (FastAPI on :8000, real Jolpica data)
cd server && uv sync && uv run uvicorn app.main:app --reload
# Web (Playwright runs against this)
cd web && npx http-server -p 5173 ../ # from repo root
# Mobile
cd mobile && npm install && npx expo startTo flip the web frontend to live mode locally, add this before
data-loader.js in web/index.html:
<script>
window.__PITWALL_CONFIG__ = {
source: "live",
serverBaseUrl: "http://localhost:8000",
};
</script>For mobile, edit mobile/app.json → expo.extra.pitwall.source from
"static" to "live".
A single entrypoint runs all three suites:
bash scripts/test-all.sh| Suite | Count | Coverage |
|---|---|---|
| Playwright | 10 | Dashboard sections, detail pages, live tick, reduced-motion |
| Jest + RNTL | 7 | useCountdown, drivers table, error states |
| pytest | 35 | Schema, mappers, editorial overlay, cache SWR, routes |
Upstream fixtures are captured from real Jolpica responses via
scripts/capture-upstream.py and checked into server/tests/fixtures/.
Tests never hit the network or generate fixtures at runtime.
The repo is deployed to Vercel as a single project. The vercel.json
rewrites send /v1/* to the FastAPI serverless function in api/index.py
and serve everything else from web/ as static files. Required environment
variables in production:
UPSTASH_REDIS_REST_URL # cache survives serverless cold starts
UPSTASH_REDIS_REST_TOKEN
ALLOWED_ORIGINS # CORS allowlist (defaults to "*" for local dev)
The cache backend auto-selects: Upstash if both env vars are present, in-memory otherwise. No code changes between environments.
- Custom domain in front of the Vercel URL
- Mobile production build via EAS + TestFlight
- Promote
shared/types.tsvalidation from a hand-written schema validator to Zod once the data contract grows - Visual regression tests on the dashboard hero and detail headers
- Wire reduced-motion preference through to the mobile app's
Reanimatedconfigurations (currently honored only at the component level)
Race data: Jolpica F1 API (Ergast-compatible).
Initial design system absorbed from a Claude Design handoff bundle and
extended in shared/design/tokens.json. PRD lives at the repo root for
all authoritative product decisions.



