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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

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

## 2.1.1

### Improvements

- **Reprocess a session from the picker** Right-click (or long-press on touch) a session on the home page for a menu to open it in a new tab or window, or reprocess its data. Reprocess rebuilds the stored session, which is required for some new features (eg track elevation). Alternative to using CLI.

## 2.1

### Improvements
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,14 @@ docker compose exec f1timing python precompute.py 2025 --skip-existing
docker compose exec f1timing python precompute.py 2024 2025 --skip-existing
```

**Re-computing a session:** Some updates (noted in the changelog as "requires re-compute") add new data to existing sessions. Re-run the same command **without** `--skip-existing` and it overwrites the stored session — that is the re-compute:

```bash
docker compose exec f1timing python precompute.py 2026 --round 1 --session R
```

This reprocesses from FastF1's local cache where available, so it usually does not re-download. To force a fresh pull from FastF1 (e.g. to pick up data F1 has since corrected), clear that session from `FASTF1_CACHE_DIR` first.

**Timing estimates:**
- A single session (e.g. one race) takes **1-3 minutes**
- A full race weekend (FP1, FP2, FP3, Qualifying, Race) takes **3-5 minutes**
Expand Down
18 changes: 17 additions & 1 deletion backend/routers/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from fastapi import APIRouter, Query, HTTPException
from services.storage import get_json, put_json, list_sizes
from services.process import ensure_session_data
from services.process import ensure_session_data, start_reprocess, get_reprocess_status

logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api", tags=["sessions"])
Expand Down Expand Up @@ -176,3 +176,19 @@ async def get_session(
status_code=404,
detail=f"Session data not available for {year} Round {round_num} ({type}).",
)


@router.post("/sessions/{year}/{round_num}/reprocess")
async def reprocess_session(year: int, round_num: int, type: str = Query("R")):
"""Re-run processing for a session, overwriting the stored data.

Auth is enforced by the global /api middleware. Runs in the background;
poll the status endpoint for completion.
"""
state = await start_reprocess(year, round_num, type)
return {"state": state}


@router.get("/sessions/{year}/{round_num}/reprocess/status")
async def reprocess_session_status(year: int, round_num: int, type: str = Query("R")):
return get_reprocess_status(year, round_num, type)
46 changes: 46 additions & 0 deletions backend/services/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,52 @@
# Locks to prevent duplicate processing of the same session
_locks: dict[str, asyncio.Lock] = {}

# State of user-triggered reprocess jobs: key -> {"state": ..., "message": ...}
# state is "running" | "done" | "error"
_reprocess_status: dict[str, dict] = {}


def get_reprocess_status(year: int, round_num: int, session_type: str) -> dict:
"""Return the current reprocess state + latest progress message for a session."""
return _reprocess_status.get(
f"{year}_{round_num}_{session_type}", {"state": "idle", "message": ""}
)


async def start_reprocess(year: int, round_num: int, session_type: str) -> str:
"""Kick off a background reprocess (overwrite) for a session.

Returns the resulting state immediately ("running", or "busy" if one is
already in flight for this session). Poll get_reprocess_status for progress.
"""
key = f"{year}_{round_num}_{session_type}"
if _reprocess_status.get(key, {}).get("state") == "running":
return "busy"
_reprocess_status[key] = {"state": "running", "message": "Starting…"}

def on_status(msg: str):
cur = _reprocess_status.get(key)
if cur is not None:
cur["message"] = msg

async def _run():
try:
# skip_existing defaults False, so this overwrites the stored session.
ok = await asyncio.to_thread(
process_session_sync, year, round_num, session_type, False, on_status
)
_reprocess_status[key] = {
"state": "done" if ok else "error",
"message": "Reprocess complete" if ok else "Reprocess failed",
}
except Exception as e:
logger.error(f"Reprocess failed for {key}: {e}")
traceback.print_exc()
_reprocess_status[key] = {"state": "error", "message": str(e)[:200] or "Reprocess failed"}

asyncio.create_task(_run())
return "running"


def process_session_sync(
year: int,
Expand Down
37 changes: 4 additions & 33 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@
"autoprefixer": "^10.4.20",
"eslint": "^8.57.1",
"eslint-config-next": "^15.5.12",
"postcss": "^8.4.49",
"postcss": "^8.5.10",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.3"
},
"overrides": {
"postcss": "^8.5.10"
}
}
Loading
Loading