Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 41 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,22 @@

All notable changes to F1 Replay Timing will be documented in this file.

## 2.0.2

### Improvements

- **Qualifying lap notifications** — as each driver crosses the line in qualifying, a bubble appears at the top of the track map showing their lap time and sector markers. Toggleable in Settings>Leaderboard.

- **Knocked-out drivers in qualifying** — drivers eliminated in Q1/Q2 are now greyed out once the next session begins, instead of continuing to display as active. Drivers keep their position and the lap time they posted. Applies to both replay and live timing. Requires re-compute for historical qualifying sessions.

### Fixes

- **Qualifying sector timing** — live sector markers (yellow, green purple) were delayed, now fixed. Requires re-compute for historical qualifying sessions.

## 2.0.1

### Improvements

- **Broadcast delay up to 5 minutes** — Live Timing Offset slider now extends to -5m (previously -60s) to accommodate longer streaming-service lag, with M:SS formatting and ±30s quick-adjust buttons
- **Configurable auto-precompute** — new `AUTO_PRECOMPUTE` env var controls which session types the background task fetches during race weekends. Accepts `off`, `race`, `race+qual` (default), or `all`. Self-hosters who don't watch practice can avoid downloading FP1/FP2/FP3 data; sessions outside the configured set are still available on-demand

Expand All @@ -17,6 +30,7 @@ All notable changes to F1 Replay Timing will be documented in this file.
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
Expand All @@ -25,12 +39,14 @@ To migrate:
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
Expand All @@ -41,21 +57,23 @@ You no longer need `NEXT_PUBLIC_API_URL`, `FRONTEND_URL`, or any CORS settings.
## 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

- **Lap analysis lap number** — fixed showing incomplete current lap data; now only displays completed laps

---

## 1.3.2

### Improvements

- **Practice sector indicators** — live sector colours and track map sector overlay now available in practice sessions. Requires recompute
- **Last lap time for all sessions** — now available in practice and qualifying. Requires recompute
- **Last lap colour coding** — purple for fastest lap, green for personal best
Expand All @@ -66,6 +84,7 @@ You no longer need `NEXT_PUBLIC_API_URL`, `FRONTEND_URL`, or any CORS settings.
- **High contrast text** — toggle in Settings > Other for white text on muted elements

### Fixes

- **Practice session precompute** — fixed timestamp alignment for practice sessions causing leaderboard to show future data. Requires recompute
- **Practice session time** — countdown aligned to 60-minute session duration, not overall replay length
- **Mobile qualifying time** — now shows remaining time counting down, matching desktop
Expand All @@ -77,6 +96,7 @@ You no longer need `NEXT_PUBLIC_API_URL`, `FRONTEND_URL`, or any CORS settings.
## 1.3.1

### Fixes

- **Live session banner width** — live session card on the home page now matches the width of the round cards
- **Medium screen layout** — track map, lap analysis panel, and leaderboard no longer overflow on tablet-sized screens when lap analysis is open

Expand All @@ -85,15 +105,18 @@ You no longer need `NEXT_PUBLIC_API_URL`, `FRONTEND_URL`, or any CORS settings.
## 1.3.0

### New Features

- **Lap Analysis panel** (Beta) — compare lap times for up to two drivers with a line chart and full lap-by-lap history. Accessible via the Laps button on the track map on desktop, or as a collapsible section on mobile. Race replay only
- **Last lap time column** — shows each driver's most recently completed lap time on the race leaderboard, toggleable in settings. Race replay only
- **Leaderboard tooltips** — hover over any column value to see what it is (e.g. "Interval to car ahead", "Tyre age", "Last lap time")

### Improvements

- **Info button on mobile** — the features/info link is now visible in the mobile header
- **Features page opens in new tab** — no longer interrupts an active replay session

### Fixes

- **Mobile qualifying sectors** — sector overlay toggle and driver selection buttons now available on mobile track map, previously desktop-only
- **Mobile leaderboard spacing** — improved UI layout for mobile Leaderboard

Expand All @@ -102,6 +125,7 @@ You no longer need `NEXT_PUBLIC_API_URL`, `FRONTEND_URL`, or any CORS settings.
## 1.2.3 — Track detail, telemetry expansion, and race finish improvements

### New Features

- **Marshal sector flags** — localised yellow/double yellow flags on the track map at marshal sector positions. Requires recompute
- **Corner numbers** — turn numbers shown on the track map from FastF1 circuit data. Toggleable in settings. Requires recompute
- **Expanded telemetry** — unlimited driver selection. 3+ drivers open a moveable side panel with pinnable race control
Expand All @@ -112,12 +136,14 @@ You no longer need `NEXT_PUBLIC_API_URL`, `FRONTEND_URL`, or any CORS settings.
- **Pit stop timer** — live count-up shown in gap column while driver is in pit lane. Requires recompute

### Improvements

- **Pit prediction accuracy** — recalculated using precise pit lane timestamps. Unified 73% SC/VSC factor
- **Qualifying sector selection** — sector overlay buttons for all selected drivers
- **Settings modal** — redesigned as tabbed sidebar (Leaderboard, Weather, Track Map, Race Control, Other)
- **Features page** — consolidated with info panel content

### Fixes

- **Race finish** — drivers stay on leaderboard at classified position with chequered flag icon. Requires recompute
- **Track map bounds** — drivers no longer fly off screen with invalid position data
- **iPad viewport** — layout accounts for browser address bar
Expand All @@ -128,14 +154,15 @@ You no longer need `NEXT_PUBLIC_API_URL`, `FRONTEND_URL`, or any CORS settings.
## 1.2.2

### Improvements

- **Mobile race control** added collapsible race control messages section to mobile view for both live and replay
- **<1sec interval highlight** intervals under 1 second are highlighted in green during race sessions (toggleable in settings)
- **Live session styling** improved pulse animation on live indicators and cleaner live session button layout (contributed by [@Clav3rbot](https://github.com/Clav3rbot))
- **Broadcast delay** added manual input field for exact delay value
- **Minor UI/UX improvements** main page layout changed to expandable list, fixed minor UI bugs on navigation


### Fixes

- **Memory management** replay session data is now evicted from memory 5 minutes after the last client disconnects
- **Live race control messages** fixed race control messages not updating during live sessions when broadcast delay is set
- **Phantom tyre compounds** fixed incorrect tyre history in live sessions caused by interim/placeholder compound updates from the F1 feed
Expand All @@ -147,44 +174,52 @@ You no longer need `NEXT_PUBLIC_API_URL`, `FRONTEND_URL`, or any CORS settings.
## 1.2.1 - 2026-03-14

### Fixes

- **Connection error screen** - when the frontend cannot reach the backend, a clear error message is now shown instead of the passphrase screen, with the attempted URL and troubleshooting tips
- **Runtime API URL for Docker** - `NEXT_PUBLIC_API_URL` can now be set as a runtime environment variable on the frontend container, so pre-built Docker images work with any backend URL without rebuilding. See the README for details
- Fixed loading overlay staying visible when navigating back to the session picker

### New Features

- **Red flag countdown and skip** - during red flag periods in replay mode, a countdown timer shows how long until the session resumes, with a button to skip ahead to the restart

---

## 1.2.0 - 2026-03-14

### New Features

- **Live Timing** - real-time timing data via F1 SignalR stream, with broadcast delay slider, post-session replay check, and PiP window support
- **Race Control Messages** - live feed of steward decisions, investigations, penalties, track limits, and flag changes accessible via the RC button on the track map (available in both live and replay modes)
- **Driver Indicators** - investigation (warning triangle) and penalty (circled exclamation) icons on the leaderboard, with automatic clearing when stewards resolve incidents

### Improvements

- **Broadcast delay slider now persists** - your delay setting is saved and restored across page loads, so you only need to set it once to match your streaming service or broadcast feed
- **Docker images published to GHCR** - pre-built images are now automatically published to GitHub Container Registry on each release, so users can deploy with just a `docker-compose.yml` without cloning the repo
- PiP window track map and driver positions now continue updating when switching windows (contributed by [@Clav3rbot](https://github.com/Clav3rbot))
- Session picker shows session start times in the user's local timezone (contributed by [@Clav3rbot](https://github.com/Clav3rbot))
- Improved Docker Compose configuration with clearer comments on port and URL customisation

### Known Limitations

- **Live timing: no track positions or telemetry** - Driver positions on the track map and telemetry data are not available during live sessions. Position data requires an authenticated F1 TV subscription. Full track positions and telemetry become available in replay mode once session data is processed (typically 1–2 hours after the session).

### Note

- Race control messages in replay mode require a re-run of precompute for each session to take effect.
- Best lap time and gap to leader columns for practice/qualifying require a re-run of precompute for existing sessions. Live sessions work immediately.

## 1.1.0 - 2026-03-10

### New Features

- Docker Compose support for self-hosting - run the full app with `docker compose up`
- Picture-in-Picture popup window with collapsible track map, telemetry, and leaderboard sections (contributed by [@Clav3rbot](https://github.com/Clav3rbot))
- Clipboard paste support for leaderboard sync - users can now paste a screenshot of the F1 TV broadcast leaderboard directly from clipboard (Ctrl+V) instead of uploading a file, with a visual Ctrl+V hint in the UI (contributed by [@Clav3rbot](https://github.com/Clav3rbot))

### Improvements

- Season schedule data is now fetched on demand from FastF1 when not already in storage, removing the need to run precompute before using the app
- Leaderboard interval/leader toggle replaced with a clickable pill on the P1 row
- Leaderboard no longer wastes horizontal space when scaled down to fit shorter viewports
Expand All @@ -196,17 +231,21 @@ You no longer need `NEXT_PUBLIC_API_URL`, `FRONTEND_URL`, or any CORS settings.
## 1.0.1 - 2026-03-07

### Improvements

- Improved mobile layout, including track map rendering and playback controls
- Starting grid positions now fall back to qualifying result data when grid position data is unavailable
- Retired drivers now remain on the leaderboard in their final position, marked as "Out"
- Overall improvements to interval timing, including handling of lapped drivers
- Minor UI consistency fixes

### Bug Fixes

- Drivers with unavailable position data are now temporarily hidden from the track map and restored automatically when data resumes

### Security

- Upgraded Next.js 14 to 15 and React 18 to 19

### Note

For the position data, starting grid position, retired driver, and interval timing fixes to take effect, you'll need to re-run precompute for any Race sessions.
54 changes: 49 additions & 5 deletions backend/services/f1_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,18 @@ def _format_lap_time(seconds: float) -> str:
session_time_offset = st.total_seconds()
break

# Offset for timing the sector indicators. It must match the offset
# laps.json uses for lap completion (`min_date - t0_date`) so sector markers
# land exactly when the lap completes. In qualifying, session_time_offset is
# later overwritten to the Q1 green-flag time (for the phase countdown), which
# would make sectors trail lap completion by a few seconds — so we capture the
# telemetry-aligned value here and use it only inside the sector block.
sector_time_offset = (
(min_date - session.t0_date).total_seconds()
if getattr(session, "t0_date", None) is not None
else session_time_offset
)

# Pre-compute track status (yellow/SC/VSC/red) lookup
# track_status Time is a session timedelta, same as gap data
track_status_times = np.array([], dtype=np.float64)
Expand Down Expand Up @@ -1009,6 +1021,24 @@ def _format_lap_time(seconds: float) -> str:
except Exception as e:
logger.error(f"Failed to parse qualifying phases: {e}")

# Per-driver highest qualifying segment reached (1/2/3), read straight from
# the results Q1/Q2/Q3 best-lap columns. Used to grey out drivers once a
# later segment begins (they keep their position and posted time).
driver_reached_phase: dict[str, int] = {}
if is_quali:
try:
for _, row in session.results.iterrows():
abbr = str(row.get("Abbreviation", ""))
if not abbr:
continue
reached = 0
for idx, col in enumerate(("Q1", "Q2", "Q3"), start=1):
if col in row and pd.notna(row.get(col)):
reached = idx
driver_reached_phase[abbr] = reached
except Exception as e:
logger.error(f"Failed to compute qualifying reached phases: {e}")

def _get_quali_phase(t_sec: float) -> dict | None:
"""Get qualifying phase info at time t_sec."""
if not quali_intervals:
Expand Down Expand Up @@ -1474,8 +1504,22 @@ def _safe_float(v) -> float:
d["gap"] = "No time"
d["no_timing"] = False

# Qualifying: flag drivers knocked out in an earlier segment so the
# frontend can dim them. They keep their position and posted time.
if is_quali:
phase = _get_quali_phase(t_sec)
phase_idx = {"Q1": 1, "Q2": 2, "Q3": 3}.get(phase["phase"], 1) if phase else 1
for d in frame_drivers:
reached = driver_reached_phase.get(d["abbr"], 0)
d["knocked_out"] = phase_idx >= 2 and reached < phase_idx

# Add live sector indicators for qualifying and practice
if session_type in ("Q", "SQ", "FP1", "FP2", "FP3"):
# Time sector markers with the lap-completion offset (not the
# quali countdown's session_time_offset) so they appear exactly
# when the lap completes, in sync with the last-lap time.
session_t_sectors = t_sec + sector_time_offset

# Track overall best and personal best sector times up to now
overall_best_sectors: dict[int, float] = {} # sector_num -> best time
personal_best_sectors: dict[str, dict[int, float]] = {} # driver -> sector_num -> best time
Expand All @@ -1484,7 +1528,7 @@ def _safe_float(v) -> float:
for drv_abbr in drivers_list:
pb: dict[int, float] = {}
for evt_t, sec_num, sec_time, lap_num, is_out_lap in driver_sector_events.get(drv_abbr, []):
if evt_t > session_t_now:
if evt_t > session_t_sectors:
break
if is_out_lap:
continue
Expand All @@ -1507,7 +1551,7 @@ def _collect_sectors_for_lap(target_lap: int) -> list[dict]:
"""Collect completed sector indicators for a specific lap."""
result = []
for evt_t2, sec_num2, sec_time2, lap_num2, is_out_lap2 in events:
if evt_t2 > session_t_now:
if evt_t2 > session_t_sectors:
break
if lap_num2 == target_lap and not is_out_lap2:
pb = personal_best_sectors.get(drv_abbr, {})
Expand All @@ -1527,7 +1571,7 @@ def _collect_sectors_for_lap(target_lap: int) -> list[dict]:
last_evt_time = None
last_evt_out = False
for evt_t, sec_num, sec_time, lap_num, is_out_lap in reversed(events):
if evt_t <= session_t_now:
if evt_t <= session_t_sectors:
last_evt_lap = lap_num
last_evt_sec = sec_num
last_evt_time = evt_t
Expand All @@ -1538,7 +1582,7 @@ def _collect_sectors_for_lap(target_lap: int) -> list[dict]:
current_lap_num = last_evt_lap
is_current_out_lap = last_evt_out
for comp_t, comp_lap in driver_lap_completions.get(drv_abbr, []):
if comp_t <= session_t_now:
if comp_t <= session_t_sectors:
if current_lap_num is None or comp_lap >= current_lap_num:
current_lap_num = comp_lap + 1
is_current_out_lap = current_lap_num in driver_out_laps.get(drv_abbr, set())
Expand All @@ -1557,7 +1601,7 @@ def _collect_sectors_for_lap(target_lap: int) -> list[dict]:

# We've moved to a new lap — check if we should linger the previous lap's S3
if last_evt_sec == 3 and not last_evt_out and last_evt_time is not None:
if session_t_now - last_evt_time <= SECTOR_LINGER:
if session_t_sectors - last_evt_time <= SECTOR_LINGER:
# Show the completed previous lap's sectors for a few more seconds
sectors = _collect_sectors_for_lap(last_evt_lap)
d["sectors"] = sectors if sectors else None
Expand Down
Loading
Loading