diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..e891e3f
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,14 @@
+.env
+.git
+.github
+docs
+*.md
+backend/venv
+backend/__pycache__
+backend/*.pyc
+backend/data/sessions
+backend/data/fastf1-cache
+backend/data/pit_loss_raw.json
+frontend/node_modules
+frontend/.next
+frontend/out
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..aa750b6
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,25 @@
+# F1 Replay Timing — Environment Variables
+# Copy to .env and fill in your values
+
+PORT=8000
+DATA_DIR=/data
+
+# Storage: "local" (default) or "r2"
+# Local stores computed session data on disk. R2 reads pre-computed data from Cloudflare R2.
+STORAGE_MODE=local
+
+# Cloudflare R2 (only needed when STORAGE_MODE=r2)
+R2_ACCOUNT_ID=
+R2_ACCESS_KEY_ID=
+R2_SECRET_ACCESS_KEY=
+R2_BUCKET_NAME=f1timingdata
+
+# Optional — authentication
+# AUTH_ENABLED=true
+# AUTH_PASSPHRASE=your-passphrase
+
+# Optional — photo sync feature (requires OpenRouter API key)
+# OPENROUTER_API_KEY=your-key-here
+
+# Only needed for pre-compute script (not required at runtime)
+# FASTF1_CACHE_DIR=/data/fastf1-cache
diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml
index 9f01b6c..5a55b66 100644
--- a/.github/workflows/pr-checks.yml
+++ b/.github/workflows/pr-checks.yml
@@ -30,14 +30,3 @@ jobs:
- name: npm audit
run: npm audit --audit-level=high
- osv-scan:
- name: Google OSV Scan
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
-
- - uses: google/osv-scanner-action/osv-scanner-action@v2
- with:
- scan-args: |-
- --recursive
- .
diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml
index 50f213c..030ccfc 100644
--- a/.github/workflows/publish-docker.yml
+++ b/.github/workflows/publish-docker.yml
@@ -1,14 +1,12 @@
-name: Publish Docker Images
+name: Publish Docker Image
on:
- push:
- tags:
- - "v*"
- branches:
- - dev # TEMPORARY: remove before release
+ release:
+ types: [published]
env:
REGISTRY: ghcr.io
+ IMAGE_NAME: f1replaytiming
jobs:
build-and-push:
@@ -17,14 +15,6 @@ jobs:
contents: read
packages: write
- strategy:
- matrix:
- include:
- - image: f1replaytiming-backend
- context: ./backend
- - image: f1replaytiming-frontend
- context: ./frontend
-
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -40,7 +30,7 @@ jobs:
id: meta
uses: docker/metadata-action@v5
with:
- images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ matrix.image }}
+ images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
@@ -52,11 +42,10 @@ jobs:
- name: Build and push
uses: docker/build-push-action@v6
with:
- context: ${{ matrix.context }}
+ context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- build-args: ${{ matrix.build-args || '' }}
platforms: linux/amd64,linux/arm64
cache-from: type=gha
cache-to: type=gha,mode=max
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e70d6d0..4fc4ee4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,11 +2,46 @@
All notable changes to F1 Replay Timing will be documented in this file.
+## 2.0.0
+
+### Migrating from v1.x
+
+The Docker image has changed. The old `f1replaytiming-backend` and `f1replaytiming-frontend` images will no longer receive updates. The new image is `ghcr.io/adn8naiagent/f1replaytiming:latest` (single unified image).
+
+To migrate:
+1. Pull the new image: `docker pull ghcr.io/adn8naiagent/f1replaytiming:latest`
+2. Copy `.env.example` to `.env` and configure (most defaults work out of the box)
+3. Replace your `docker-compose.yml` with the one from the repo. It's now a single service on one port
+4. `docker compose up`
+
+You no longer need `NEXT_PUBLIC_API_URL`, `FRONTEND_URL`, or any CORS settings. Your session data volume carries over as-is, no reprocessing needed.
+
+### Breaking Changes
+- **Single container architecture** — frontend and backend are now merged into a single Docker container serving everything from one port. The separate frontend and backend containers have been removed
+- **Simplified configuration** — all config is now in a single `.env` file. `NEXT_PUBLIC_API_URL`, `FRONTEND_URL`, and CORS configuration are no longer needed
+- **Static frontend** — Next.js switched from `output: 'standalone'` to `output: 'export'`, producing static HTML/CSS/JS served by FastAPI. No Node.js runtime in the final image
+- **URL format change** — dynamic routes (`/replay/2026/5`) replaced with query parameters (`/replay?year=2026&round=5&type=R`). Old URLs redirect automatically
+
+### Improvements
+- **No CORS** — frontend and API are the same origin, eliminating all cross-origin issues
+- **Reverse proxy friendly** — single port means Traefik, nginx, and Cloudflare tunnels just work with no special configuration
+- **WebSocket reliability** — same-origin WebSocket connections no longer break behind TLS termination or mixed protocol proxies
+- **Screen wake lock** — prevents screen dimming and device sleep during replay playback and live sessions
+
+---
+
+## 1.3.2.2
+
+### Fixes
+- **Replay timing drift** — replaced fixed-duration sleeps with wall-clock-anchored playback to prevent timing drift during sessions (contributed by [@stephenwilley](https://github.com/stephenwilley))
+
+---
+
## 1.3.2.1
### Fixes
- **Lap analysis lap number** — fixed showing incomplete current lap data; now only displays completed laps
-- **Mobile lap analysis scroll** — section is now scrollable on mobile
+
---
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..068d89e
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,35 @@
+# ── Stage 1: Build frontend ──
+FROM node:20-alpine AS frontend
+WORKDIR /app
+COPY frontend/package.json frontend/package-lock.json* ./
+RUN npm ci
+COPY frontend/ ./
+RUN npm run build
+# Produces /app/out/ with static HTML/CSS/JS
+
+# ── Stage 2: Production ──
+FROM python:3.11-slim
+
+WORKDIR /app
+
+# System deps (numpy/pandas, HEIC support)
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ gcc g++ libheif-dev && \
+ rm -rf /var/lib/apt/lists/*
+
+COPY backend/requirements.txt .
+RUN pip install --no-cache-dir -r requirements.txt
+
+COPY backend/ .
+
+# Copy frontend build output
+COPY --from=frontend /app/out /app/static
+
+# Create data directory
+RUN mkdir -p /data/fastf1-cache
+
+EXPOSE 8000
+
+ENV PORT=8000
+ENV STATIC_DIR=/app/static
+CMD sh -c "cp -n /app/data/pit_loss.json /data/pit_loss.json 2>/dev/null; uvicorn main:app --host 0.0.0.0 --port $PORT"
diff --git a/README.md b/README.md
index d16eaa7..86a4920 100644
--- a/README.md
+++ b/README.md
@@ -7,15 +7,13 @@ https://github.com/user-attachments/assets/952b8634-2470-46d9-96e2-67a820459a49
-> **Disclaimer:** This project is intended for **personal, non-commercial use only**. This website is unofficial and is not associated in any way with the Formula 1 companies. F1, FORMULA ONE, FORMULA 1, FIA FORMULA ONE WORLD CHAMPIONSHIP, GRAND PRIX and related marks are trade marks of Formula One Licensing B.V.
-
A web app for watching Formula 1 sessions with real timing data, car positions on track, driver telemetry, and more - both live during race weekends and as replays of past sessions. Built with Next.js and FastAPI.
## Features
- **Live timing** (Beta) - connect to live F1 sessions during race weekends with real-time data from the F1 SignalR stream, including a broadcast delay slider and automatic detection of post-session replays
- **Track map** with real-time car positions from GPS telemetry, updating every 0.5 seconds with smooth interpolation, marshal sector flags, and toggleable corner numbers
-- **Driver leaderboard** showing position, gap to leader, interval, last lap time, tyre compound and age, tyre history, pit stop count and live pit timer, grid position changes, fastest lap indicator, investigation/penalty status, and sub-1-second interval highlighting
+- **Driver leaderboard** showing position, gap to leader, interval, last lap time, sector indicators (qualifying/practice), tyre compound and age, tyre history, pit stop count and live pit timer, grid position changes, fastest lap indicator, investigation/penalty status, and sub-1-second interval highlighting
- **Race control messages** - steward decisions, investigations, penalties, track limits, and flag changes displayed in a draggable overlay on the track map with optional sound notifications
- **Pit position prediction** estimates where a driver would rejoin if they pitted now, with predicted gap ahead and behind, using precomputed pit loss times per circuit with Safety Car and Virtual Safety Car adjustments
- **Telemetry** for unlimited drivers showing speed, throttle, brake, gear, and DRS (2025 and earlier) plotted against track distance, with a moveable side panel for 3+ driver comparisons
@@ -32,222 +30,222 @@ A web app for watching Formula 1 sessions with real timing data, car positions o
## Architecture
-- **Frontend**: Next.js (React) with Tailwind CSS
-- **Backend**: FastAPI (Python) - serves pre-computed data from local storage or Cloudflare R2
+- **Frontend**: Next.js (React) with Tailwind CSS — built as static files, served by the backend
+- **Backend**: FastAPI (Python) — serves the API, WebSocket endpoints, and the frontend from a single port
- **Data Source**: [FastF1](https://github.com/theOehrly/Fast-F1) (used during data processing only)
-Session data is processed once and stored locally (or in R2 for remote access). You can either pre-compute data in bulk ahead of time, or let the app process sessions on demand when you select them.
+Everything runs as a **single container on one port**. The frontend and backend are the same service — no cross-origin configuration, no CORS, no separate URLs to manage.
+
+Session data is processed once and stored locally (or in Cloudflare R2 for persistence). You can either pre-compute data in bulk ahead of time, or let the app process sessions on demand when you select them.
## Self-Hosting Guide
-### Option A: Docker with pre-built images (easiest)
+### Quick start (Docker)
Requires [Docker](https://docs.docker.com/get-docker/) and Docker Compose.
-Create a `docker-compose.yml` file:
+**1. Clone the repository:**
-```yaml
-services:
- backend:
- image: ghcr.io/adn8naiagent/f1replaytiming-backend:latest
- ports:
- - "8000:8000"
- environment:
- - FRONTEND_URL=http://localhost:3000
- - DATA_DIR=/data
- volumes:
- - f1data:/data
- - f1cache:/data/fastf1-cache
+```bash
+git clone Race Results
-
- Watch Replay
-
-
-
-
-
-
-
-
- {results.map((r) => {
- const gained =
- r.grid_position && r.position
- ? r.grid_position - r.position
- : null;
- return (
- Pos
- Driver
- Team
- Grid
- +/-
- Status
- Points
-
-
- );
- })}
-
-
- {r.position ?? "-"}
-
-
-
-
- {r.team}
-
-
- {r.grid_position ?? "-"}
-
-
- {gained === null ? (
- "-"
- ) : gained > 0 ? (
- +{gained}
- ) : gained < 0 ? (
- {gained}
- ) : (
- 0
- )}
-
-
- {r.status}
-
-
- {r.points}
-
-
- {API_URL}
+ {API_URL || (typeof window !== "undefined" ? window.location.origin : "(same origin)")}
Common causes:
diff --git a/frontend/src/components/SessionPicker.tsx b/frontend/src/components/SessionPicker.tsx index adb18de..0de25c5 100644 --- a/frontend/src/components/SessionPicker.tsx +++ b/frontend/src/components/SessionPicker.tsx @@ -240,7 +240,7 @@ export default function SessionPicker() { )} { e.stopPropagation(); setNavigating(true); @@ -265,7 +265,7 @@ export default function SessionPicker() { )} { e.stopPropagation(); setNavigating(true); @@ -392,7 +392,7 @@ export default function SessionPicker() { {liveSession && liveSession.year === year && (