stint9 · system schematic
You built the front end. This is what sits behind it: a static site with no server of its own, a Supabase database it talks to directly, and — on race day — a live feed borrowed from stint9's own API.
The dashboard is a plain HTML page on GitHub Pages. It has no backend of its own — instead the browser talks straight to Supabase over REST, writing with a public key that row-level security keeps in check. The only external system is stint9's live-timing API, and only your logged-in browser can reach it.
Map, leaderboard, fuel, positions. Static, ~840 KB, no build step.
Self-contained sub-app, talks to the dashboard by postMessage.
Timing, crew data, messages. Read by anyone; writes gated by policy.
Per-dashboard update jobs; the live collector runs in-browser instead.
/api/worker/… JSON behind Clerk login. The race feed's origin.
Every view draws from a single in-memory DB object. Where that object comes from is the only difference between the two modes — the map, positions and charts don't care which.
A past NLS event, pre-processed offline into a const DB embedded in the page. The scrubber replays it. Nothing leaves the browser.
Polls Supabase for raw lap rows and rebuilds the exact same DB shape via build-db.js — verified to match the offline generator.
stint9 already ingests the timing feed, so instead of scraping WIGE we borrow stint9's own JSON. A snippet in your logged-in tab bridges it into Supabase; the dashboard reads from there. Two independent loops, joined by one table.
Your Clerk session — the only thing allowed to read the feed. Serves /api/worker/events/{id}/laps as JSON.
Polls the laps endpoint every 5 s, maps each lap to a row, and upserts it.
Raw per-lap rows: car · lap · s1–s5 · time-of-day · pit. One row per (car, lap); the collector upserts, RLS allows the anon write.
In LIVE mode the page fetches today's rows and hands them on. No stint9 credentials ever touch the dashboard.
Rebuilds the DB (positions, legs, sector times), re-indexes the selected class, and repaints the map at the latest lap. Identical code path to SIM.
All tables are public-read (the dashboard uses the anon publishable key) and write-gated by row-level-security policy — the same pattern across every table.
Raw laps the collector writes; the dashboard rebuilds the DB from these.
Race-control board — penalties, flags. Scraper-fed; test rows as fallback.
Fuel entries + per-car calculator settings (tank, consumption) in state.set.
Free-text pit-wall notes, one per lap.
The whole tyre board as one JSON blob — stock, serials/km/cycles, moves, highlights.
Periodic track temp / conditions logged in LIVE for lap-time correlation.
One row per completed lap with the temperature at the time.
Single ONLINE/OFFLINE flag toggled from admin.html.
Usage analytics and the feedback widget.
Because fuel, notes and tyres are all keyed by the selected car, two teams sharing the link — each on a different car — write to different rows and never overwrite each other. No login, no localStorage; the database is the single source of truth, so it also syncs across devices.
…_state (car=650)
…_state (car=667)
Still shared on purpose: either team can select the other's car and see its data. Real team isolation (each side sees only its own cars) is the one planned feature left before a wide rollout.
tyre.html runs in an iframe so its ~260 variables and styling can't collide with the dashboard's. They speak over a three-message channel.
On a car change it flushes the outgoing car, then loads & re-renders the incoming car's stint9_tyre_state row — or a fresh default board if that car has none.