REST & WebSocket API¶
Everything the dashboard does goes through this API, so anything the UI can do, a
script can too. All REST routes live under /api; responses are JSON.
Status & health¶
| Method | Path | Purpose |
|---|---|---|
| GET | /api/health |
{"status": "ok"} |
| GET | /api/status |
source kind, connection state, packet/error counters, recording flag, current session id, track name. Also the lap-clock cross-check (diagnostic): lap_clock_drift_ms (current lap, signed), lap_clock_drift_worst_ms (session worst) and lap_clock_samples — how far the integrated time axis has drifted from GT7's own packet-C lap clock, 0/absent below packet C |
Sessions & laps¶
| Method | Path | Purpose |
|---|---|---|
| GET | /api/sessions |
all sessions, newest first, with lap count and best lap. ?category=Gr.3 filters by car class; blank returns everything, which is also the only way to reach recordings made before packet C. A session whose own category is empty (its first packet was a narrower format) inherits its laps' category |
| DELETE | /api/sessions/{id} |
delete a session and its laps |
| GET | /api/sessions/{id}/laps |
lap summaries for one session |
| GET | /api/laps |
all lap summaries, newest first |
| GET | /api/laps/best |
track, category → the fastest full lap ever recorded at that circuit in that class, or null. Partial pit out-laps are excluded, for the same reason they never own a session best |
| GET | /api/laps/{id}?samples=true |
full lap detail: metrics, events, gearing, and (optionally) the 60 Hz samples |
| DELETE | /api/laps/{id} |
delete a lap |
| GET | /api/laps/{id}/export |
JSON export envelope (see Lap file format) |
| GET | /api/laps/{id}/export.csv |
MoTeC-compatible CSV (one row per tick, up to 34 channels with units — the optional channels appear only when the recording carried them) |
| POST | /api/laps/import |
import an exported lap file |
Tracks¶
| Method | Path | Purpose |
|---|---|---|
| GET | /api/tracks |
stored track signatures |
| POST | /api/tracks |
{name, lap_id} — name a circuit from a lap's geometry |
| DELETE | /api/tracks/{id} |
remove a signature (session data untouched) |
| POST | /api/tracks/identify |
name every unlabelled session that was driven on a surveyed circuit → {checked, identified, tracks}. New sessions do this for themselves; this is for history recorded before the bundles existed. Sessions with no confident match are left alone. 409 when nothing has been surveyed |
Overlay / dashboard layouts¶
| Method | Path | Purpose |
|---|---|---|
| GET | /api/layouts |
all saved layouts with their grid configs |
| GET | /api/layouts/{ref} |
one layout by numeric id or name (id wins if both match) |
| POST | /api/layouts |
{name, kind: "overlay"\|"dash", config} — 409 on duplicate name |
| PUT | /api/layouts/{id} |
{name?, config?} — rename and/or replace the config |
| DELETE | /api/layouts/{id} |
delete the layout |
Configs are v2 grid layouts ({version: 2, grid, cells, …}) as produced by the Admin
builder; the server only checks the version and a 64 KB size cap.
Analysis¶
| Method | Path | Purpose |
|---|---|---|
| GET | /api/analysis/compare |
laps (CSV ids), ref (id), step (m, default 5, 0.5–50), channels (optional CSV) → per-lap distance-resampled series, speed peaks/valleys, events, and delta-vs-reference. The reference lap also carries corners: the circuit's authored corners when it has them (authored: true, and a name when given), otherwise detected from that lap's curvature. Top-level accel is the broadcast accelerometer's unit + sign calibration, fitted on the reference lap and applied to all of them; each lap with the channels also gets gg, its peak g in each direction taken from the raw ticks |
| GET | /api/analysis/deviation |
session_id, count (2–20, default 5) → median speed + standard deviation by distance across the best N laps |
| GET | /api/analysis/fuel |
lap_id → relative fuel-map table for settings −5…+5 |
Default compare channels: t, speed, throttle, brake, coast, gear, rpm, boost,
tire_slip, yaw_rate, pos_x, pos_z. Any other stored column can be requested via
channels=; t, pos_x, pos_z are always included (the delta and map need them).
Delta values are milliseconds, positive = slower than the reference.
Controls¶
| Method | Path | Purpose |
|---|---|---|
| POST | /api/control/recording |
{"recording": bool} — pause/resume lap recording |
| POST | /api/control/log-lap-now |
save the in-progress lap immediately (409 if none) |
Surface survey¶
Runs the surface survey capture in the 60 Hz packet path (packet format C required for surface data).
| Method | Path | Purpose |
|---|---|---|
| POST | /api/survey/start |
{track_width_m?, track?} — start a run; resets counters, opens a new JSONL log; 409 if a run is already active. track labels which circuit the samples describe (default: the session's identified track; picked up mid-run if identification happens later, and a user-typed label is never auto-overwritten) |
| POST | /api/survey/stop |
stop and close the log |
| POST | /api/survey/track |
{track} — name the circuit a running survey is describing; 409 if none is running, 422 if the name is blank. A run started before the track was known keeps everything it has gathered and merges it into that circuit's bundle (a survey with no label saves no bundle at all). The label is marked as the driver's own, so auto-identification will not override it. Re-assigning an already-labelled run is a circuit change, not a correction: its evidence is flushed to the previous circuit first — to fix a wrong label, rebuild from the JSONL with backend/scripts/jsonl_to_bundle.py |
| GET | /api/survey/status |
track/session the run is tied to, per-wheel char histogram, unknown chars, recent transitions, log path, undocumented flag-bit activity, the finish line located from lap rollovers (finish: mean crossing point/heading, crossing count, spread, confidence), and the track width in use — the entered assumption until enough out-and-back edge rides have measured the real axle width (width_estimate_m, width_samples) |
| GET | /api/survey/trail |
breadcrumb of the path driven (since/epoch for incremental fetch; the epoch bumps when the trail is decimated) |
| GET | /api/survey/edges |
every border-edge point of the run — the track taking shape (since/epoch incremental; append-only within a run). Kinds: auto (surface-flip contacts), straddle (sampled continuously while one side's wheels are held off the tarmac), edge/runoff/wall (manual marking) |
| POST | /api/survey/mark |
{side: "L"\|"R"\|null, kind: "edge"\|"runoff"\|"wall"} — arm manual boundary marking: while armed, the survey samples edge points from that side's wheel line every ~2 m, which is how boundaries invisible to the surface chars (walls, and track edges with paved run-off beyond) get mapped |
| GET | /api/survey/packet |
the latest raw telemetry packet, fully decoded — the Survey view's field inspector polls this |
| GET | /api/survey/logs |
every survey run's JSONL, summarised (track, marks, transitions, finish crossings, size) and flagged orphaned when it gathered evidence and never reached a circuit — a run that saved no bundle at all and exists only as this file |
| POST | /api/survey/logs/{name}/assign |
{track} — rebuild an orphaned run from its log and merge it into that circuit through the normal voting path, exactly as if it had been named while driving. The run is credited to the installation that recorded the log (its id is in the log's meta header), not the one replaying it. 409 while that run is still going, and 409 if it has already been assigned to that circuit — re-assigning to a different one is the mis-label correction. 404 for a name that is not a log in the data directory |
| GET | /api/survey/logs/{name}/download |
the raw JSONL file itself — how a run recorded on one machine reaches another (upload it there, then assign). 404 for a name that is not a log |
| POST | /api/survey/logs/upload |
(admin) accept a survey JSONL from another installation. The body is validated line by line against the log format (header shape, mark and transition records) before anything is kept — a bundle document or a truncated file is refused with the offending line number. Names are sanitised to the surface_survey_*.jsonl scheme and an existing file is never overwritten (a _2 suffix is claimed atomically instead). 413 over 64 MB |
| GET | /api/survey/export.jsonl |
full log of the current/last run (404 if none). First line is a {"meta": ...} header (track, session id, track-width assumption, wheel order); transition records carry the session id and lap, so they join back to the laps recorded during the same drive. Interleaved lines: {"mark": ...} for manually-marked boundary points and {"track": ...} when the circuit is identified mid-run |
Track bundles & management¶
The joined view of the three sources of track knowledge, and the export / import path for track bundles. See the Tracks view.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/track-catalog |
official GT7 track/layout metadata (bundled data/tracks.json: 41 tracks, 85 layouts, lengths, corner counts, reverse configs) |
| GET | /api/track-overview |
one row per circuit, merged across the DB's named tracks, the survey bundles and the official catalog: named (auto-identification will work), bundle stats (points, runs, sources, elevation %, finish crossings, corners), the compiled coverage score (per-side boundary % + closed + road_pct) and compiled_at when the bundle compiles, session count, the confirmed official match and — only when there is none — a suggestion with its confidence and reasoning. Also lists the survey logs and this installation's source id |
| GET | /api/track-outline |
lap_id (resolves the circuit from the lap's session) or track → the surveyed road, from the compiled geometry: road (a contiguous quad strip [x1,z1,…,x4,z4] between the ordered border curves), edges and walls (segments [x1,z1,x2,z2]), finish, gaps (unsurveyed spans the ordering had to bridge — drawn dashed, never as road) and coverage (per-side boundary coverage + road_pct). Answers with an empty outline rather than 404 when the circuit has no bundle — never having been surveyed is the common case, not an error. Compiled off the event loop and recompiled per bundle revision: a bundle is up to 50,000 records and the browser must not download it |
| GET | /api/track-bundles |
every circuit's bundle, with the same stats and coverage |
| GET | /api/track-bundles/{slug} |
one bundle document — the export unit, and what import consumes |
| POST | /api/track-bundles/import |
merge a bundle document from elsewhere. ?track= overrides the document's own label, which is how a near-miss name lands on the right circuit. Every field is validated and rebuilt before anything is merged; versions 1–4 are accepted and upgraded. Your own authored corners and confirmed layout match are never overwritten (corners_kept says when incoming ones were dropped). 400 on any malformed document; 413 over 64 MB, enforced while reading rather than after buffering |
| PATCH | /api/track-bundles/{slug} |
{track?} renames — merging when the new name is an existing bundle, which is the fix for one circuit living under two spellings. {official, set_official: true} records the confirmed official layout (official: null clears it). 409 while a survey is running on that circuit: it holds the old name in memory, so its next save would recreate the bundle just moved |
| DELETE | /api/track-bundles/{slug} |
remove a bundle (the survey JSONL logs are untouched). 409 while a survey is running on that circuit |
| GET | /api/track-bundles/{slug}/corners |
the circuit's authored corners and sections |
| PUT | /api/track-bundles/{slug}/corners |
{corners?, sections?} — replace them; omitted lists are left alone. Renumbered from list order. 404 when the circuit has no bundle: corners are anchored to positions on a surveyed map |
Admin¶
| Method | Path | Purpose |
|---|---|---|
| GET | /api/admin/settings |
current runtime settings |
| PUT | /api/admin/settings |
{ps_ip?, source?, log_level?, webhook_url?, webhook_events?, packet_format?, race_engineer?, race_engineer_verbosity?, race_engineer_categories?, race_engineer_units?} — applied live, persisted to the DB |
| POST | /api/admin/test-webhook |
send a test notification |
| GET | /api/admin/race-engineer |
Race Engineer diagnostics: counters, detector state, connected voice clients, last callout |
| POST | /api/admin/race-engineer/test |
{text?, event_type?} — inject a callout into every connected browser |
| GET | /api/admin/logs |
limit (≤2000), level — recent log records from the ring buffer |
| DELETE | /api/admin/logs |
clear the ring buffer |
| GET | /api/admin/stats |
uptime, DB stats, source stats, client count, LAN IP |
| POST | /api/admin/restart-source |
stop/start the telemetry source |
| POST | /api/admin/clear-data |
delete all sessions and laps (settings/tracks kept) |
| POST | /api/admin/vacuum |
SQLite VACUUM |
| POST | /api/admin/update-cars |
download the community car list and reload the lookup |
WebSocket — /ws/live¶
One endpoint. Traffic is overwhelmingly server-push; the only client → server
messages are the Race Engineer voice protocol (below), and anything else a client
sends is ignored. Every message is {"type": ..., "data": ...}.
Server → browser:
telemetry— the live frame, throttled toGT7_WS_RATE(default 30 Hz): speed, RPM + redline, gear + suggested gear, throttle/brake %, boost, fuel level and capacity, lap counters, best/last lap, session best and previous best, race position, tire temps (FL/FR/RL/RR), tire slip, water/oil temps, oil pressure, driver-aids bitmask (TCS=1, ASM=2, handbrake=4, rev limiter=8), packed per-wheel surface codes (4 bits per wheel, FL lowest; 0 = no data), car id/name, world position, in-game time of day, track name, on-track/paused flags.lap— sent when a lap is saved: id, session, number, time, per-lap metrics, and event counts. The UI uses this to refresh lists live.session— sent on new session, track identification, or track naming.survey— one per-wheel surface transition while a survey is running: from/to chars, changed wheels, position, velocity, heading, raw rotation floats, and derived wheel-contact points (sent at full 60 Hz resolution — these are single-tick events the throttled telemetry frames would miss).status— sent on connect and whenever the source or console IP changes.voice_callout— a Race Engineer callout:id,event_type,text,category,priority(0–100),created_at_ms,expires_at_ms,ttl_ms,interrupt,dedupe_key,message_key/message_args(localization) andmetadata. Sent to every client; only the active speaker should voice it.voice_output_status—{active_client_id}: which browser may speak.race_engineer_status— feature enabled/active, verbosity, emitted categories, and the connected voice-capable clients.
Browser → server (Race Engineer only; never token-gated, and unparseable messages are ignored so older pages keep working):
| Message | Data |
|---|---|
client_capabilities |
client_id, page, voice_supported, voice_enabled |
claim_voice_output |
client_id |
release_voice_output |
client_id |
voice_callout_ack |
callout_id, client_id, status, spoken_at_ms, reason? |
Ack statuses: spoken, expired, duplicate, disabled, category_disabled,
not_active_speaker, interrupted (stopped on purpose — a critical callout, a
disconnect, or the user pressing Test voice) and speech_error, which carries the
engine's own reason. Acks are diagnostics only and never gate the pipeline.
On connect the client immediately receives a status and a race_engineer_status
message — never past callouts, which are live events. The frontend auto-reconnects
every 2 s if the socket closes.
CORS
CORS is wide open (*) — the API is designed for a trusted home network, not
public exposure. Don't port-forward it to the internet.