diff --git a/CHANGELOG.md b/CHANGELOG.md index 67bf2d6..a6861b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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 @@ -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 @@ -41,6 +57,7 @@ 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)) --- @@ -48,14 +65,15 @@ You no longer need `NEXT_PUBLIC_API_URL`, `FRONTEND_URL`, or any CORS settings. ## 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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -147,11 +174,13 @@ 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 --- @@ -159,11 +188,13 @@ You no longer need `NEXT_PUBLIC_API_URL`, `FRONTEND_URL`, or any CORS settings. ## 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)) @@ -171,20 +202,24 @@ You no longer need `NEXT_PUBLIC_API_URL`, `FRONTEND_URL`, or any CORS settings. - 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 @@ -196,6 +231,7 @@ 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" @@ -203,10 +239,13 @@ You no longer need `NEXT_PUBLIC_API_URL`, `FRONTEND_URL`, or any CORS settings. - 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. diff --git a/backend/services/f1_data.py b/backend/services/f1_data.py index 23c680c..ae9ba4f 100644 --- a/backend/services/f1_data.py +++ b/backend/services/f1_data.py @@ -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) @@ -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: @@ -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 @@ -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 @@ -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, {}) @@ -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 @@ -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()) @@ -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 diff --git a/backend/services/live_state.py b/backend/services/live_state.py index 0c8a72d..3d95fb9 100644 --- a/backend/services/live_state.py +++ b/backend/services/live_state.py @@ -87,6 +87,7 @@ class _DriverState: "has_fastest_lap", "flag", "retired", + "knocked_out", "no_timing", "grid_position", "sectors", @@ -124,6 +125,7 @@ def __init__(self, racing_number: str) -> None: self.has_fastest_lap: bool = False self.flag: str | None = None # "investigation" | "penalty" | None self.retired: bool = False + self.knocked_out: bool = False self.no_timing: bool = False self.grid_position: int | None = None self.sectors: list[dict[str, Any]] | None = None @@ -159,6 +161,7 @@ def to_dict(self) -> dict[str, Any]: "has_fastest_lap": self.has_fastest_lap, "flag": self.flag, "retired": self.retired, + "knocked_out": self.knocked_out, "no_timing": self.no_timing, "grid_position": self.grid_position, "sectors": self.sectors, @@ -368,8 +371,9 @@ def _handle_timing_data(self, data: dict, _ts: float) -> None: drv.retired = True if "KnockedOut" in updates: - if updates["KnockedOut"]: - drv.retired = True + # Keep knocked-out drivers visible with their posted time; the + # frontend dims them rather than dropping them out of timing. + drv.knocked_out = bool(updates["KnockedOut"]) # Sectors (qualifying sector indicators) if "Sectors" in updates: diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 43aa869..aff37ff 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -702,9 +702,9 @@ } }, "node_modules/@next/env": { - "version": "15.5.14", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.14.tgz", - "integrity": "sha512-aXeirLYuASxEgi4X4WhfXsShCFxWDfNn/8ZeC5YXAS2BB4A8FJi1kwwGL6nvMVboE7fZCzmJPNdMvVHc8JpaiA==", + "version": "15.5.19", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.19.tgz", + "integrity": "sha512-sWWluFvcv5v3Fxznmf2ZfjyoVQt/64oCnYqS90inQWGzMPK1VjvekPiz3OPHKmFT30EnHrjlbyaHLt3M0vWabw==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -718,9 +718,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.5.14", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.14.tgz", - "integrity": "sha512-Y9K6SPzobnZvrRDPO2s0grgzC+Egf0CqfbdvYmQVaztV890zicw8Z8+4Vqw8oPck8r1TjUHxVh8299Cg4TrxXg==", + "version": "15.5.19", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.19.tgz", + "integrity": "sha512-jx9wWlTKueHKPvVOndyr7WuaevWCkuYqsQ8gC0TMPKAVWG3MhcdMrjfo9tvIZNXd0QOUYXXvAcZ325y8Uq7uzg==", "cpu": [ "arm64" ], @@ -734,9 +734,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.5.14", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.14.tgz", - "integrity": "sha512-aNnkSMjSFRTOmkd7qoNI2/rETQm/vKD6c/Ac9BZGa9CtoOzy3c2njgz7LvebQJ8iPxdeTuGnAjagyis8a9ifBw==", + "version": "15.5.19", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.19.tgz", + "integrity": "sha512-291KFcsIQ3OenRdiUDFOR6W3wezzH4auENXm1gbm1Bjd4ANMMRgxPrWTUztQN43BnVoVuMnHCrLeECIMwgFKbA==", "cpu": [ "x64" ], @@ -750,9 +750,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.5.14", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.14.tgz", - "integrity": "sha512-tjlpia+yStPRS//6sdmlVwuO1Rioern4u2onafa5n+h2hCS9MAvMXqpVbSrjgiEOoCs0nJy7oPOmWgtRRNSM5Q==", + "version": "15.5.19", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.19.tgz", + "integrity": "sha512-WeH+nelQyyMeE2f8FxBRZNrGipya5zHZV2vjzfCOAYyiI6am+NbnWAAldOBFQBB2w0DjJcsvrKqoFT2b7+5YoA==", "cpu": [ "arm64" ], @@ -766,9 +766,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.5.14", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.14.tgz", - "integrity": "sha512-8B8cngBaLadl5lbDRdxGCP1Lef8ipD6KlxS3v0ElDAGil6lafrAM3B258p1KJOglInCVFUjk751IXMr2ixeQOQ==", + "version": "15.5.19", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.19.tgz", + "integrity": "sha512-5xTOE0lDlDCSSfp+BAif7j17VRRCjWp//ZPZy6NI0QpdrhxtQnsZguSx0xAAZ0c9XZLrLLwCe/XVe5YPrRilKw==", "cpu": [ "arm64" ], @@ -782,9 +782,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.5.14", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.14.tgz", - "integrity": "sha512-bAS6tIAg8u4Gn3Nz7fCPpSoKAexEt2d5vn1mzokcqdqyov6ZJ6gu6GdF9l8ORFrBuRHgv3go/RfzYz5BkZ6YSQ==", + "version": "15.5.19", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.19.tgz", + "integrity": "sha512-LTxRmMgqqMv05Had879W00Fm53quiJd3Zuz8h1JSNJ3nGSlbZ/7Tjs1tKyScgN3Au3t3MyPsjPlq60fMmSHLsg==", "cpu": [ "x64" ], @@ -798,9 +798,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.5.14", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.14.tgz", - "integrity": "sha512-mMxv/FcrT7Gfaq4tsR22l17oKWXZmH/lVqcvjX0kfp5I0lKodHYLICKPoX1KRnnE+ci6oIUdriUhuA3rBCDiSw==", + "version": "15.5.19", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.19.tgz", + "integrity": "sha512-eoNQSpA5PQfB9wBO4RA47MTDXWz1fizy9Y3Z6e4DetYIF3dvjuu8sj7aIGn/bFCU6lnFzTK34NtCaffP4NsQ7Q==", "cpu": [ "x64" ], @@ -814,9 +814,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.5.14", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.14.tgz", - "integrity": "sha512-OTmiBlYThppnvnsqx0rBqjDRemlmIeZ8/o4zI7veaXoeO1PVHoyj2lfTfXTiiGjCyRDhA10y4h6ZvZvBiynr2g==", + "version": "15.5.19", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.19.tgz", + "integrity": "sha512-6UNt2dFuCHOe446sm/Kp69nUe8/wIhnh9bm6Xcqw4qEWCOppLMOvhTBVgvM7invVUNr4SPpP6NOQsACtn2IN9Q==", "cpu": [ "arm64" ], @@ -830,9 +830,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.5.14", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.14.tgz", - "integrity": "sha512-+W7eFf3RS7m4G6tppVTOSyP9Y6FsJXfOuKzav1qKniiFm3KFByQfPEcouHdjlZmysl4zJGuGLQ/M9XyVeyeNEg==", + "version": "15.5.19", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.19.tgz", + "integrity": "sha512-PhmojAHyqMne56HBLGu9dhDnHPuFmEjrXSQMM/nW0J6j849lk3ESrVtqNJcCk8CKOV7brpTTbaYAjwKPzKM69w==", "cpu": [ "x64" ], @@ -1228,9 +1228,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -4310,10 +4310,20 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -4453,9 +4463,9 @@ } }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, "node_modules/lodash.merge": { @@ -4563,9 +4573,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", "funding": [ { "type": "github", @@ -4604,12 +4614,12 @@ "license": "MIT" }, "node_modules/next": { - "version": "15.5.14", - "resolved": "https://registry.npmjs.org/next/-/next-15.5.14.tgz", - "integrity": "sha512-M6S+4JyRjmKic2Ssm7jHUPkE6YUJ6lv4507jprsSZLulubz0ihO2E+S4zmQK3JZ2ov81JrugukKU4Tz0ivgqqQ==", + "version": "15.5.19", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.19.tgz", + "integrity": "sha512-xNOW6tYshGX1/Oi3F8uuk4gpDeWsSUE/1Z0G5uUMekIxaQ0xc03UXd9II0VQHYMWviMeA0OHpJFAKsHf8bTYVg==", "license": "MIT", "dependencies": { - "@next/env": "15.5.14", + "@next/env": "15.5.19", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -4622,14 +4632,14 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.5.14", - "@next/swc-darwin-x64": "15.5.14", - "@next/swc-linux-arm64-gnu": "15.5.14", - "@next/swc-linux-arm64-musl": "15.5.14", - "@next/swc-linux-x64-gnu": "15.5.14", - "@next/swc-linux-x64-musl": "15.5.14", - "@next/swc-win32-arm64-msvc": "15.5.14", - "@next/swc-win32-x64-msvc": "15.5.14", + "@next/swc-darwin-arm64": "15.5.19", + "@next/swc-darwin-x64": "15.5.19", + "@next/swc-linux-arm64-gnu": "15.5.19", + "@next/swc-linux-arm64-musl": "15.5.19", + "@next/swc-linux-x64-gnu": "15.5.19", + "@next/swc-linux-x64-musl": "15.5.19", + "@next/swc-win32-arm64-msvc": "15.5.19", + "@next/swc-win32-x64-msvc": "15.5.19", "sharp": "^0.34.3" }, "peerDependencies": { @@ -5039,9 +5049,9 @@ } }, "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "dev": true, "funding": [ { @@ -5060,7 +5070,7 @@ "license": "MIT", "peer": true, "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 0a182e3..b61452f 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -39,3 +39,12 @@ body { ::-webkit-scrollbar-thumb:hover { background: #4a4a5a; } + +@keyframes lapBubbleIn { + from { opacity: 0; transform: translateY(-8px); } + to { opacity: 1; transform: translateY(0); } +} + +.lap-bubble { + animation: lapBubbleIn 0.25s ease-out; +} diff --git a/frontend/src/app/replay/page.tsx b/frontend/src/app/replay/page.tsx index cd4d066..b2b80a5 100644 --- a/frontend/src/app/replay/page.tsx +++ b/frontend/src/app/replay/page.tsx @@ -13,6 +13,7 @@ import TelemetryChart from "@/components/TelemetryChart"; import SyncPhoto from "@/components/SyncPhoto"; import PiPWindow from "@/components/PiPWindow"; import LapAnalysisPanel from "@/components/LapAnalysisPanel"; +import LapNotifications from "@/components/LapNotifications"; import type { SectorOverlay } from "@/lib/trackRenderer"; import { Maximize, Minimize, ArrowUpRight } from "lucide-react"; @@ -495,6 +496,17 @@ export default function ReplayPage() { )} + {/* Qualifying lap-completion bubbles */} + + {/* Race Control toggle - desktop only, mobile has its own section */}