Overview
One page of commands for every action an operator runs against the local Watchparty stack. Each entry is a pointer, not the full story: the repository’sdocs/OPERATOR_CHEAT_SHEET.md is the source copy of this page, and docs/LIVE_DAY_RUNBOOK.md and ops/local/README.md hold the detail. All commands run from the repository root in PowerShell.
Before Anything
Stacks and Ports
Load the Environment Into a Shell
Scripts underscripts/ read DATABASE_URL (some read DATABASE_DSN) from the shell. Do this once per PowerShell window:
--database-url <dsn>. The DEV DSN is postgresql://vpw:vpw@localhost:5433/vpw_dev.
Stack Up, Status, Down
The data volume is preserved on down.psql Into DEV
Migrate DEV by Hand
The launchers migrate for you. By hand, only when you need it without launching:Backfill a Weekend From FastF1
Downloads one weekend’s sessions from the FastF1 archive and writes them through the normal normaliser. Rows land under synthetic session keys of the form year, round, code index (FP1 00, FP2 01, FP3 02, SQ 03, S 04, Q 05, R 06) withsource = fastf1_backfill. A live OpenF1 copy of the same session keeps its own key and is never touched; the two converge in the picker through the canonical identity map.
--session-codesnames exactly which sessions to fetch. Omit it and the script derives the full weekend from the FastF1 schedule, which fails loudly for sessions that have not happened yet. The rows that exist still land; the exit code is non-zero.- Round numbers are FastF1 rounds. 2026: Zandvoort 12, Monza 13, Madrid 14.
- By default the script also precomputes each session’s insight outputs, and for a race or sprint its strategy outputs.
--skip-insightsand--skip-strategyturn those off and leave the session data-only, with a loud analytics-incomplete warning. Compute later from the Analytics section if you skipped. - Repeat
--year N --round Mpairs to do several weekends in one run. - Cache:
data/fastf1_cache, override with--cache-dir. A warm cache makes a re-download fast. - Re-running an already backfilled session is a no-op: the backfill manifest records finished endpoints and the script skips them. There is no force flag; see Refresh below.
- Typical time: a few minutes per practice session on a warm network, ten or more for a race with full telemetry.
Refresh a Weekend
Use when the source data changed or a partial run went wrong. Clears the weekend’s synthetic keys from every backfilled table plus its manifest rows, then backfill again. Dry run by default; live OpenF1 keys are out of scope and survive.--include-analytics also drops the weekend’s metric outputs and event parameters. Prod refuses without --allow-prod.
Verify
In psql:Launch the Demo
The replay viewer for archived weekends.1
One-time setup
On a cold machine, or after a data wipe. Brings up infra, installs Python dependencies into
.venv, migrates, seeds, and backfills the two reference weekends (2025 Abu Dhabi round 24, 2026 Miami round 4) data-only. Needs Docker Desktop running, a repo-root .venv, and npm install already done in apps/pitwall-web.2
Every demo
Ensures infra is up and the database has sessions, then opens three windows: replay, gateway and the web server.Open
http://localhost:5174. launch-demo.bat -Env dev is the same thing for a double-click launch or a snapshot recipient.3
Stop
Close the three windows.
Replay only publishes server-computed insight channels for a session that has been precomputed (see Analytics). Sessions that have not been precomputed still replay; the webapp falls back to client-side derivations.
Launch Live Ingest
Full detail, including what LIVE looks like in the app and recovery steps, lives in the repository’sdocs/LIVE_DAY_RUNBOOK.md.
The Week Before
None of this needs a session row yet.python scripts\build_strategy_priors.py --source-session <session_key> --year 2026 --round 14 --dry-run, then drop --dry-run to write.
Pre-Weekend Docker and WSL Guard
Do this before every live weekend. Full detail is in the repository’sdocs/LIVE_DAY_RUNBOOK.md. The preflight inside launch-live.ps1 also warns (it never blocks) on the two machine settings below, read-only. If the Store auto-update policy is absent it reads as a note rather than a warning; the winget upgrade check below is the interim mitigation.
Pre-Weekend Sleep and Power
The ingestion window and the insights and strategy supervisors each hold a Windows keep-awake lock while they run, which stops idle sleep and Modern Standby. The screen may still turn off. The preflight insidelaunch-live.ps1 warns (it never blocks) when sleep is not set to Never on mains or on battery, and when the machine is running on battery. Full detail is in the repository’s docs/LIVE_DAY_RUNBOOK.md, “Pre-weekend sleep and power”.
python.exe appears under SYSTEM while live, the lock is not held (look for a keep-awake warning in that window’s log) and the sleep settings are the only protection.
Start
Brings compose up, migrates, then opens five windows: gateway, web, insights supervisor, strategy supervisor, ingestion. OpenF1 credentials are read by key name from$HOME\Desktop\openf1.env (OPENF1_USERNAME=... and OPENF1_PASSWORD=..., order independent) and handed only to the ingestion window.
The launcher refuses to start while an ingestion is already running or the gateway or web port is busy, and names the PID. Close the earlier windows or run -Down first.
-Year 2026 restricts the session watcher. -VenvPath <dir> borrows another checkout’s venv when running from a worktree. The raw archive lands in C:\Projects\vpw-captures\live-raw\<env>\<stamp> and strategy solve packages in C:\Projects\vpw-captures\strategy-archive\<env>\<stamp>.
Logs and Heartbeat
Every one of the five windows records its own output to<raw archive>\logs\<service>.log (gateway, web, insights-supervisor, strategy-supervisor, ingestion), so closing a window no longer loses what it printed. Ingestion also writes <raw archive>\heartbeat.log every 60 s. Each log opens with [start] at <UTC> and ends with [exit] code=<n> at <UTC> when the process stops.
The web window alone restarts Vite if it exits: 3 s pause, logs [restart] attempt <k>, gives up after 5 restarts in 10 minutes, and Ctrl+C stops it for good.
When reporting a live-day problem, attach the run folder first (C:\Projects\vpw-captures\live-raw\<env>\<stamp>). It carries the logs and the heartbeat alongside the raw payloads, so it is a complete record of what that launch actually did.
MQTT Client Id
Defaults tovpw-live-ingest-<computername>, printed at launch. To use a specific id, export it before launching. It must stay stable across restarts and never be shared with another install.
T-20 Minutes
solve_vs_lap line of the strategy preflight. It compares the archived lap-0 solve wall time (timings.total_s in output_manifest.json) with the fastest driver’s base-pace lap in the uploaded race_data.json, and warns above 60% of a lap. A WARN means live solves will lag laps and be abandoned (Singapore 2026: 72 s against laps of about 90 s at ctl.number_repeats 100). Lower ctl.number_repeats in the file (50 fixed Singapore) and re-upload with load_race_data.py (see Race Data Template and the Lap-0 Solve). It never fails the preflight, and load_race_data.py prints the same line straight after the lap-0 solve. “skipped: no lap-0 solve found” means the archive holds none for this key (pass --archive-dir if it lives elsewhere); “cannot check: no base pace in race_data” means the file has no usable sector base paces.
If the identity preflight reports an unresolved meeting, add the printed entry to services/normalisation/src/vpw_normalise/reference/openf1_meeting_rounds.json, then run --restamp. Without it the strategy child resolves no round parameters. The session row appears in the picker about 60 seconds after OpenF1 publishes the session.
If a race_data.json was uploaded for this round, promote its lap-0 rows onto the live session key now, straight after the --restamp. See Race Data Template and the Lap-0 Solve below.
Stop and Restart
Close each service window (Ctrl+C), then stop compose:launch-live.ps1, or re-run the whole script; the compose and migrate steps are idempotent and ingestion resumes from its persisted checkpoints.
Restart the Live Strategy Child
Only for a wedged child. A late or newerstrategy: or race_data: row is adopted from the next lap by itself, so a restart is not needed to reload parameters. Never kill process trees by hand.
--wait-s the script follows strategy-supervisor.log (newest run, or --log <path>) and prints the restart, respawn, warm-up and “race_data template pinned” or “newer parameters adopted” lines. The flag folder is %TEMP%\vpw-strategy-restart, or the folder named by VPW_STRATEGY_RESTART_DIR if set (the supervisor reads the same variable).
Post-Session Verification
Backfill or Repair From OpenF1
There is no OpenF1 historical backfill. MQTT live is the only processed OpenF1 path; REST polling runs alongside as an archive-only witness. What exists is repair from the raw archive that live ingestion wrote. Every repair is a dry run until--apply, routes through the real normaliser mapping, and never deletes.
<raw dir> is the raw tree under the live-raw stamp directory for that day. --raw-dir and --session-key can repeat.
The gmt_offset repair is the one entry here that needs no raw archive: it looks each session’s circuit-local UTC offset up by that session’s own key, from OpenF1 for live rows or from the FastF1 event schedule for backfilled rows, and writes that single column. A session the lookup cannot resolve is reported and left NULL, never guessed. Every clock in the webapp reads track-local time from this column, falling back to browser-local time only when it is absent.
Raw-Only Capture
Records OpenF1 REST to disk for later analysis and writes nothing to the database:launch-live.ps1 -Capture is retired: the capture server is HTTP-only and ingestion now requires MQTT.
Parameters
Parameters are versioned rows, never constants. Loads are append-only: identical values are a no-op, changed values write a new active version.load_default_parameters.py changes the value for every track; load_circuit_parameters.py changes it for one, and wins over the global value at that circuit. Precedence runs defaults, then circuit, then round, then session. Find a circuit key with SELECT DISTINCT circuit_key, country_name FROM sessions WHERE year = <y>.
Pass every pin you want that circuit to keep in the same command: a key you do not pass keeps the circuit’s existing pin if it has one, otherwise it is filled from the global row. --deactivate drops the circuit’s pin so the global value applies again.
Neither command rewrites rows that are already stored. A parameter change takes effect at the next precompute or live run, so re-precompute the sessions you care about (see Analytics After Data Lands).
race_data:{year}:{round:02d} is a parameter family too, but it is loaded from the owner’s uploaded file rather than a CSV, and it carries a lap-0 solve with it. It has its own section below.
Race Data Template and the Lap-0 Solve
The owner’srace_data.json for one round. Uploading it does four things in a single command: validates the file as a template, writes race_data:{year}:{round:02d} to event_parameters (append-only, like every other parameter family), creates a provisional race session YYYYRR90 if the real one does not exist yet, and solves lap 0 into the strategy archive so a pre-race prediction is stored before a wheel turns.
Keep uploaded files at C:\Projects\vpw-captures\race-data-uploads\<year>\<round>-<circuit>\race_data.json, with a sha256.txt beside each. The parameter row carries the whole uploaded file plus race_data_source = {filename, sha256}, so a file on disk can always be matched to the version that ran.
ctl.num_strategies is optional (whole number, at least 1, default 5) and sets how many ranked strategy categories the engine carries per driver.
Validate First
No database connection and no writes.Upload and Solve Lap 0
Windows only, because the lap-0 solve runs the engine bundle.OK: or WARN: line comparing the solve time with the base-pace lap (60% limit), naming ctl.number_repeats as the lever when it warns. It never changes the exit code.
A running live strategy child picks up a late strategy: or race_data: load, or a newer version of one, from the next lap it processes (one INFO log line names the old and new versions); no restart is needed.
If a driver in the file will not start (late driver change), re-upload the file without them before the race: a template driver who never posts a lap holds the opening solves until lap 3.
--race-date <iso> sets the provisional session’s timestamp, defaulting to qualifying plus one day. --archive-dir, or the VPW_STRATEGY_ARCHIVE_DIR environment variable, moves the solve archive off the default .\strategy-archive. Re-running clears this service’s own strategy_* rows at trigger lap 0 first, so a corrected file can be re-uploaded safely, and the parameter row gains a new version rather than overwriting the old one.
Confirm the row landed with python scripts\strategy_preflight.py --year 2026 --round 14. The race_data_row check is a hard failure in round mode.
These lap-0 rows survive a later precompute_strategy.py run, so the upload and the precompute can be done in either order.
Associate the Lap-0 Rows With Every Race Key
The lap-0 solve runs once per round, never once per session key. At upload timeload_race_data.py copies its own lap-0 rows onto every other non-provisional race session key of the round, so opening either the OpenF1 live key or a FastF1 backfill twin in the picker shows lap 0. A backfill twin that only appears after the upload, which is the usual case because FastF1 keys land after the race weekend, has nothing to copy from at upload time; precompute_strategy.py picks this up for its own --session-key before it replays, so running the precompute against the twin associates it. If neither has run for a given twin yet, repair it directly.
Promote at T-20
Once OpenF1 has published the real race session andidentity_preflight.py --restamp has stamped it, move the lap-0 rows off the provisional key onto the live one and remove the provisional session. Both happen in one transaction.
--archive-dir the upload used. Running python scripts\strategy_preflight.py --session-key <sk> warns if a stale provisional session is still present for the round.
Analytics After Data Lands
--session-key covers every stale session, which on a fresh environment means every session anyone has ever excluded a lap on, because the reconcile bookkeeping table starts empty. Name the session when only one is meant.
--session-key narrows a pass, it never forces one. A named session that is not stale is reported and left alone, and a named session whose newest lap is under an hour old is deferred exactly as it would be in a full pass, because clearing its rows under a live engine would leave a permanent hole in the timeline. To force a recompute regardless of staleness, use precompute_insights.py --session-key <sk>.--keep-existing skips the clear step and is only safe when no version changed. Precompute only the sessions being looked at; no historical sweeps during active development.
To time the real precompute path or compare its output against what is already stored, with zero database writes (metric_outputs untouched, no rows cleared):
out.jsonl as one canonical JSON line each, in the metric_outputs column shape, and the run ends with a per-metric row-count table. --output-file and --keep-existing cannot be combined. A full race precompute takes about five minutes rather than 100.
What-If Studies on the Strategy Side
To try a different race-data file on a stored race, or just a timing and comparison run, with zero database writes (no clear, no lap-0 association, nometric_outputs write at all):
studies\ folder inside --archive-dir (default .\strategy-archive, or VPW_STRATEGY_ARCHIVE_DIR): the collected rows as JSON Lines at studies\<stem>.jsonl and the per-lap packages at studies\<stem>\<session_key>\lap_NNN\. The stem encodes the session key, the file’s own name, the first 12 hex characters of its sha256 and the run’s UTC start time, so two runs more than a second apart never collide. Both paths print at the end. Nothing prunes these study folders; delete them once a study is finished. --output-file PATH overrides only the JSONL path, and an existing non-empty file at that path is refused rather than overwritten.
--output-file alone (no --race-data) is also a no-write study: the engine’s per-lap input stays the database’s own resolved parameters, only the output moves to a file. Neither study flag can be combined with --keep-existing or --clear-all (exit 2): those two only mean something for a database write, which a study never makes. A study replays from lap 1 and never produces the lap-0 pre-race row, so its row count is two short of a stored session’s when compared row for row.
The lap-0 pre-race rows from an uploaded
race_data.json survive a strategy re-precompute: the clear spares trigger='pre_race'. They are removed anyway, with a warning, if they carry a different model version from the run, because a metric holding two versions is skipped by replay altogether; rebuild them with load_race_data.py when that happens. --clear-all deletes them deliberately.Check a Race Against the FIA Timing PDFs
The FIA publish two timing documents per race: the Race Lap Analysis and the Race History Chart. Between them they give every car’s lap time and the clock time at which it completed lap 1, so comparing them against stored rows is the quickest proof that a weekend ingested cleanly. Read-only against the database. Keep the PDFs with the round’s other uploads inC:\Projects\vpw-captures\race-data-uploads\<year>\<round>-<circuit>\, alongside race_data.json and sha256.txt. PDFs dropped at the repository root are gitignored so they cannot be committed by accident.
The race history chart is indexed by the leader’s lap, not by each car’s own lap. A lapped car simply has no row for the lap it lost, so its later rows sit one index higher than its own lap count. The scripts handle this; a hand comparison that ignores it produces a few hundred false differences.
New Circuit
Needs real car location rows from a session at the venue. Read-only against the database; it prints a paste-ready registry entry. Two cases: Circuit already raced in a backfilled season (the normal case): fit it before the weekend from the previous year’s Race session, which has 300k to 460k location rows. Race sessions do not need--laps-only. Paste the entry into apps/pitwall-web/src/circuits/registry.ts keyed by the verbatim backfill circuit name only. Then, once the first live OpenF1 row of the weekend has landed (FP1), add the one-line circuit key entry pointing at the same constant, quoting the observed session key. Never invent the key beforehand. In the same pass, run the identity preflight from the T-20 steps (identity_preflight.py, then --restamp): a new weekend’s OpenF1 meeting key is not in the round map until you add it, and until the live row is stamped the picker shows the live session and its FastF1 backfill as two separate rows instead of one slot with two badges (Baku 2026 FP1).
--laps-only or garage and pit-lane points wreck the fit.
apps/pitwall-web/public/circuits/circuits/.
Measure the Start Offset for a Track Definition
startOffset is the fraction [0, 1) of the vendored geojson outline’s own arc length at which the real start/finish line sits, so the Track Dominance widget can rotate the painted elements to line up with it (the outline’s first point is not necessarily the line). Measure it from the previous year’s Race session on DEV: take the race winner (the driver with the most completed laps), read laps 10 to 14, and take the median of the five per-lap fractions. Read-only against DEV. --calibration takes the circuit’s committed registry constants as scale,rotationRad,tx,ty[,mirrored] and --geojson its outline file. The printed “median fraction” is what goes into the definition’s startOffset.
Measure Sector Lines for a Track Definition
The Telemetry and Track Dominance widgets pin every lap’s own S1 and S2 positions (its integrated distance at the official cumulative sector times) to a circuit’s canonicalsectorLinesM in apps/pitwall-web/src/circuits/definitions/*.json. The lines are not published anywhere, so they are measured from stored laps: the median length-scaled S1 and S2 distance across every timed lap with all three sector times. Read-only against DEV.
--session-key values to pool laps across sessions of the same circuit; --track-length-m comes from the definition’s own trackLengthM, never assumed. The printed s1_m and s2_m are what goes into sectorLinesM; the printed p10 and p90 spread for each line is worth checking before committing a wide band. A band of tens of metres or more means degraded telemetry, not a genuinely wide sector line (2026 Budapest is the known example: re-run against a healthy season’s sessions rather than commit it). The session keys used for each committed circuit are recorded in that circuit’s sectorLinesM commit message.
Backups and Promotion
Every table matches the manifest (...) when every count matches.
Gates Before Committing or Merging
$env:VPW_GOLDEN_SESSION = 11353 while other fixtures are stale.
Default Webapp Layout
The webapp’s default workspace (tabs, open widgets, window geometry) is the owner’s own captured layout, held verbatim inapps/pitwall-web/src/store/defaultWorkspace.json. Re-capturing it is a paste, not a code change.
1
Copy the layout out of the browser
On the Watchparty tab you actually use, press F12, open the Console, and run the line below. Make the tab you want the workbook to open on the active one first, because the capture records it.
2
Paste it over the JSON file
Replace the whole contents of
defaultWorkspace.json with the clipboard. Pretty-printing is fine; the values must stay exactly as captured, so never tidy the file by hand.3
Run the webapp tests and commit
The tests prove the default equals the pasted blob as loaded, then the usual gate applies.
A browser that already holds a stored workspace keeps it. The new default only shows on a first-ever load and after Reset layout in the transport footer. Each origin (port) has its own storage, so capture from the build you actually use.
Next Steps
Data Journey
How live and backfilled data travel the same pipeline.
Insights and Versioning
Why a precompute must be re-run after a model version bump.
