Offline / trackside mode
Race day is the worst time for "the WiFi went down." The Race Platform keeps working when the API is unreachable, then quietly catches the server up when connectivity returns.
What's saved offline
Every mutating request the app makes (creating a lap, dragging a board card, editing a run sheet, dropping a corner marker on a track) goes through an outbox queue before it touches the network:
- The mutation is appended to a local queue stored in
SharedPreferences(browser localStorage on the web build, the keychain on iOS, etc.). - The HTTP request is sent. If it succeeds, the mutation is removed from the queue.
- If it fails (no internet, server down, 5xx), the mutation stays in the queue and is retried automatically.
Reads (lists, detail views) are not cached today — if you're offline and try to load data you haven't already opened in the session, you get an error. The first wave focuses on capture (don't lose race-day data) over read-while-offline (which needs a local mirror DB and is a bigger lift).
The status pill
In the top-right of every page, a pill shows the current sync state:
| Pill | Meaning |
|---|---|
| ● Synced | Online, queue empty, all caught up. |
| ● N pending | Online, replay in progress. |
| ● Offline · N saved | Disconnected, queue has N mutations. |
Click the pill to open a sheet that lists every queued mutation — which endpoint it targets, when it was queued, and how many retries it's seen.
Conflict resolution
When the device comes back online, the sync worker replays each
queued mutation in order against the live API. The current policy is
last-write-wins: whatever the queued mutation was, it overwrites
whatever the server has now. The server's updatedAt timestamp is
bumped on every replay so observers can tell something changed.
This is a sensible default for race-day data that's almost always single-author per row (one engineer is editing car #57's run sheet at a time, one strategist is closing a board card). For collaboratively-edited data — multiple engineers editing the same field of the same run sheet at the same time — the long-term plan is to use Automerge CRDTs, which merge deterministically. That work isn't shipped yet.
What happens to rejected mutations
If the server replays your mutation and rejects it (4xx other than 401 — e.g., the row was deleted while you were offline, or your input fails validation), the mutation is removed from the queue and the error message is logged. The pill sheet shows you what didn't make it so nothing disappears silently.
401 specifically (auth expired) keeps the mutation queued and waits for a new login.
What about AI
The AI panel works as long as the API can reach an LLM. For trackside deployments where the server is also offline, point the API at a local Ollama instance:
# On the pit-box laptop
ollama serve
ollama pull llama3.2
# In /opt/docker/.env on the server (or container env):
RACE_AI_PROVIDER=ollama
OLLAMA_URL=http://host.docker.internal:11434/api
OLLAMA_MODEL=llama3.2
Restart the API and the AI panel responds entirely from the local model, no internet required.
Roadmap
- Read-through cache (Drift) — every successful GET also writes to a local SQLite mirror so the UI can render lists offline.
- Automerge for run sheets / board / run plans — CRDT merge instead of last-write-wins for collaboratively edited rows.
- Bandwidth-aware sync — pause heavy uploads (telemetry blobs) on metered connections.