Open source motorsport data acquisition and analytics
🌐 Live Demo: HackTheTrack.net
🔧 Hardware Project: DovesDataLogger on GitHub
- Multi-format file support (NMEA, UBX, iRacing IBT, VBO, MoTeC, AiM CSV + XRK/XRZ, Alfano, Dove, Dovex)
- Automatic track & course detection within 5 miles
- Automatic driving direction detection (forward/reverse)
- Waypoint mode — lap timing anywhere, no track needed
- Interactive race line map with speed heatmap
- Braking zone detection & visualization
- Automatic lap detection via start/finish line
- 3-sector split timing with optimal lap
- Pro graph view with multi-series telemetry charts
- Reference lap overlay & pace delta comparison
- Lap snapshots — save a "course fastest lap" per engine, frozen for cross-session comparison (local-first, optionally cloud-synced)
- Video sync with telemetry playback
- 9 overlay gauge types (digital, analog, graph, bar, bubble, map, pace, sector, lap time)
- MP4 video export with overlays & audio (H.264 + AAC)
- Vehicle profiles & setup sheet management
- Session notes per file
- BLE device integration (DovesDataLogger)
- Device track sync over Bluetooth
- Custom track & course editor with community submissions
- Local weather lookup
- Optional cloud sync of files & garage data across devices (requires backend + sign-in)
- Dark & light mode
- PWA — installable & fully offline
This project is 100% open source. The entire codebase—every feature, every parser, every visualization—is freely available for anyone to use, modify, and self-host.
- Local Processing: All data analysis happens in your browser. Your telemetry data never leaves your device.
- No Server Required: No uploads, no database, no accounts, no cloud sync.
- Team Transparency: Organizations can audit the code themselves for security compliance.
- Single file processing on HackTheTrack.net is always free—no download or account required
- Self-hosting is always an option—clone this repo and run it yourself
- The only potential future paid feature: optional cloud storage for users who want hosted data retention on my infrastructure
All formats are auto-detected on import:
| Format | Source | Extension |
|---|---|---|
| UBX Binary | u-blox GPS receivers | .ubx |
| iRacing IBT | iRacing sim native binary telemetry | .ibt |
| VBO | Racelogic VBOX, RaceBox | .vbo |
| Dove CSV | DovesDataLogger | .dove |
| Dovex | DovesDataLogger (extended with metadata) | .dovex |
| Alfano CSV | Alfano ADA app, Off Camber Data | .csv |
| AiM CSV | MyChron 5/6, Race Studio 3 | .csv |
| AiM XRK/XRZ | MyChron / SoloDL native binary | .xrk, .xrz |
| MoTeC CSV | MoTeC i2 Pro export | .csv |
| MoTeC LD | MoTeC native binary | .ld |
| NMEA | Standard GPS sentences | .nmea, .txt, .csv |
AiM XRK/XRZ is parsed by libxrk's pure-Rust core compiled to a small (~200 KB) WebAssembly module — no Pyodide, no Python. It runs entirely client-side in a Web Worker, is fully offline (the wasm is precached), and parses a typical session in tens to a couple hundred milliseconds. See AiM XRK / XRZ import for how the wasm is built and pinned.
iRacing IBT is the sim's only native on-disk telemetry export — the binary
.ibtfile iRacing writes (at the session tick rate, typically 60 Hz) once logging is armed (Alt-L, or the always-on telemetry setting). It is the same data the live shared-memory irsdk API serves; iRacing has no built-in CSV/MoTeC export (those are third-party conversions of this same file), so we parse the.ibtdirectly. GPSLat/Lon/Speed/Altmake it a first-class GPS source; driver inputs (throttle, brake, gear, steering), engine channels (RPM, water/oil temp) and native lateral/longitudinal g ride along.
| Layer | Technology |
|---|---|
| Framework | React 18 + TypeScript |
| Build | Vite |
| Styling | Tailwind CSS + shadcn/ui |
| Mapping | Leaflet (CARTO basemaps + Esri World Imagery / Wayback) |
| Charts | Custom Canvas 2D renderer |
| Video Export | WebCodecs + mp4-muxer (H.264 MP4) |
| State | React Query |
| Backend | None – zero server dependencies (optional admin backend via Lovable Cloud) |
| BLE | Web Bluetooth API for DovesDataLogger device communication & settings |
The app includes an optional admin system for managing a community track database. When enabled, users can submit new tracks/courses for review, and admins can manage everything through a web interface.
The app always reads tracks from public/tracks.json — zero database calls on normal page loads. The database exists solely for the admin workflow.
| Variable | Required | Description |
|---|---|---|
VITE_SUPABASE_URL |
Yes (if using Cloud) | Backend URL (auto-set by Lovable Cloud) |
VITE_SUPABASE_PUBLISHABLE_KEY |
Yes (if using Cloud) | Backend public/anon key (auto-set by Lovable Cloud) |
VITE_SUPABASE_PROJECT_ID |
Yes (if using Cloud) | Backend project ID (auto-set by Lovable Cloud) |
VITE_ENABLE_ADMIN |
No | Set to true to enable admin UI and /admin route. /login mounts when admin OR cloud is enabled. Default false — a fresh clone ships the public, offline-first app, not admin UI pointed at an upstream backend. |
VITE_ENABLE_CLOUD |
No | Set to true to enable public user accounts: Cloud Sync Labs panel, email sign-in/registration, /register, /forgot-password, /reset-password, /auth/callback. Default false — flag-off builds ship zero cloud auth code (offline-first invariant). |
VITE_ENABLE_GOOGLE_AUTH |
No | Set to true to show the "Continue with Google" buttons (login, register, Profile). Requires VITE_ENABLE_CLOUD. Default false: Google sign-in currently routes through Lovable's hosted OAuth broker, so it stays hidden until native Supabase Google OAuth is configured (Google Cloud OAuth client + Supabase provider). |
VITE_TURNSTILE_SITE_KEY |
No | Cloudflare Turnstile site key for track submission CAPTCHA |
VITE_FIRMWARE_MANIFEST_URL |
No | Override the DovesDataLogger firmware OTA manifest URL used by the in-app firmware updater. When unset: main builds use the production manifest (https://theangryraven.github.io/DovesDataLogger/manifest.json); non-main/preview builds use the beta channel (https://theangryraven.github.io/DovesDataLogger/beta/manifest.json). Set this to force a specific channel on any branch. |
TURNSTILE_SECRET_KEY |
No | Cloudflare Turnstile secret key (edge function secret — ???) |
STRIPE_SECRET_KEY |
No (required for paid tiers) | Stripe secret key used by the create-checkout-session, stripe-webhook, and create-portal-session edge functions (edge function secret — ???) |
STRIPE_WEBHOOK_SECRET |
No (required for paid tiers) | Signing secret for the stripe-webhook endpoint, from the Stripe dashboard webhook config (edge function secret — ???) |
DELETION_CRON_SECRET |
No (required for scheduled account deletion) | Shared secret the process-account-deletions edge function requires in the x-cron-secret header. Must match the Vault secret deletion_cron_secret that the daily pg_cron job sends (edge function secret — ???) |
DOVE_PLUGIN_PACKAGES |
No | Build-time: comma-separated external plugin npm packages to load. Overrides the default (@perchwerks/eye-in-the-sky, the public AI coach) when set |
Note:
TURNSTILE_SECRET_KEYis a server-side secret stored in Lovable Cloud, not aVITE_client variable. If not set, Turnstile verification is skipped.
Build version stamp:
VITE_APP_VERSION,VITE_GIT_HASH,VITE_BUILD_DATE,VITE_GIT_BRANCH, andVITE_GIT_COMMIT_DATEare not configured by hand —vite.config.tsbakes them in automatically (frompackage.jsonand git) for the home-page footer version/commit stamp. The stamp mirrors the_PREVIEWbackend switch: amainbuild showsv<version> · <hash>, while any other branch shows<branch> · <hash> · <commit time>. The commit hash prefers CI-provided SHAs (WORKERS_CI_COMMIT_SHA/CF_PAGES_COMMIT_SHA/GITHUB_SHA) and the branch prefers CI branch vars (WORKERS_CI_BRANCH/CF_PAGES_BRANCH/GITHUB_REF_NAME) so both are correct even on shallow checkouts, falling back to a localgitcall and then"unknown".
Stripe / paid tiers:
STRIPE_SECRET_KEYandSTRIPE_WEBHOOK_SECRETare edge-function secrets (notVITE_client vars). Prices are resolved live by lookup_key — there are no Price ids in code or env. Create the Plus/Premium/Pro Products in Stripe with one recurring Price per billing interval, each tagged with the matching lookup_key:plus_monthly,plus_annual,premium_monthly,premium_annual,pro_monthly,pro_annual. Then point a Stripe webhook (events:checkout.session.completed,customer.subscription.created/updated/deleted) at thestripe-webhookfunction URL. WhenSTRIPE_SECRET_KEYis absent the pricing UI falls back to showing only the two free cards (Guest + Free). Use Stripe test mode first. Tier entitlements are granted only by the webhook, never the client.On-hold / comped tiers: the Premium and Pro (AI) tiers are listed in
COMING_SOON_TIERS(src/lib/billing.ts, mirrored increate-checkout-session) so they're hidden from the pricing UI entirely and can't be bought via the app (only Free + Plus are shown at launch). To give one to a tester/friend, create the subscription directly in Stripe on thepremium_*/pro_*price and set the subscription'smetadata.user_idto their account id (or change an existing customer's price) — the webhook grants it. Remove the tier from bothCOMING_SOON_TIERSsets to open self-service purchase.Cancellation grace + log trimming: a cancelled subscription ends at the period boundary and drops to the free tier's limit immediately, but the user's cloud logs are kept for a 60-day grace window (
grace_until). After it expires, thetrim_expired_logs()function (scheduled daily viapg_cron) deletes their synced log files newest-first until their pooled total (docs + remaining logs + snapshots) fits the freetotal_bytesallowance; snapshots and garage docs are never auto-deleted. Ifpg_cronisn't enabled on the project, enable it (Dashboard → Database → Extensions) or invokeselect public.trim_expired_logs();from an external scheduler.
Note:
DOVE_PLUGIN_PACKAGESis build-time only (read byvite.config.ts), not a clientVITE_variable. It overrides which external plugin packages the build loads; by default the build pulls in the public AI coach (@perchwerks/eye-in-the-sky) from npm as an optional dependency — seesrc/plugins/README.md.
Build fallback:
vite.config.tsnow hardcodes the project's public backend URL, publishable key, and project ID as a fallback for production builds. Local.envvalues still take precedence, but published builds no longer white-screen if managed env injection is temporarily missing.
PWA cache recovery: the legacy
/sw.jspath now ships a one-release cleanup worker that deletes old app caches and unregisters itself without touching IndexedDB telemetry/session data. The active offline worker is now published at/service-worker.js, and HTML navigations useNetworkFirstto reduce the chance of users getting stuck on an old shell after future deploys.
The admin system uses Lovable Cloud (Supabase) for the database. The schema is created automatically via migrations. Tables:
- tracks — Track names with short names (max 8 chars) and enabled flag
- courses — Course definitions with start/finish and optional sector lines
- submissions — User-submitted tracks/courses pending admin review
- banned_ips — IP addresses blocked from submissions
- login_attempts — Rate limiting for login (5 attempts, 1 hour lockout)
- user_roles — Admin/user role assignments (uses
has_role()security definer) - sync_records — Per-user cloud-sync documents (files/garage data), RLS-scoped to the owner
- user-files (Storage bucket) — Private per-user session file blobs for cloud sync
- lap_snapshots — Per-user frozen "course fastest lap" captures (one per course+engine), RLS-scoped; its own table, but its size counts toward the unified storage pool (below)
- subscription_tiers — Data-driven plan catalogue (free/plus/premium/pro): label, price, and a single pooled cloud-storage budget (
total_bytes: 50 MB / 10 GB / 100 GB / 500 GB) shared by documents + logs + snapshots - user_subscriptions — Per-user tier + Stripe customer/subscription state, status, renewal date, cancellation grace (service-role-written only)
- profiles — Per-user unique, editable display name
- account_deletions — Pending self-service account-deletion requests (7-day, reversible grace window)
Data retention (GDPR): a daily
pg_cronjob runspurge_expired_personal_data(), which nulls the submitter IP onsubmissionsandmessages90 days after they were received, deletes the rows entirely after 1 year (allmessages;submissionsonly once reviewed — pending ones are kept for moderation), and deletes expiredbanned_ips/ stalelogin_attempts. Account deletion is scheduled 7 days out (cancellable); theprocess-account-deletionsworker then removes the user's Storage objects and auth row. To auto-schedule that worker, add a Supabase Vault secret nameddeletion_cron_secretand set the matchingDELETION_CRON_SECRETenv on the function, then re-run the GDPR migration — it wires the dailypg_cron+pg_netjob for you.
Cloud sync is independent of the admin system — it only needs a signed-in user account, not the admin role. It's an online-only, opt-in feature; the core app stays fully offline without it.
All database code lives behind src/lib/db/ with a clean interface (ITrackDatabase). The current implementation uses Supabase, but you can swap in PostgreSQL/MySQL by implementing the same interface:
src/lib/db/
types.ts — Interface definitions
supabaseAdapter.ts — Supabase implementation
index.ts — Factory: getDatabase()
- Submissions — Approve/deny user-submitted tracks and courses
- Tracks CRUD — Add, edit, enable/disable, delete tracks (with short names)
- Courses CRUD — Manage courses per track with coordinate editing
- Tools — Build
tracks.jsonfrom DB, download tracks ZIP, import JSON to rebuild DB, export/import course drawings - Banned IPs — View and manage banned IP addresses, with a selectable expiry (TTL; defaults to 90 days, expired bans auto-purged)
| Function | Purpose |
|---|---|
submit-track |
Public endpoint for track submissions (with IP ban check) |
admin-build-zip |
Admin-only: generates per-track JSON files |
check-login-rate |
Rate limiting for login attempts |
submit-message |
Public contact-form endpoint (with IP ban + rate limit) |
stripe-prices |
Public: reports whether Stripe is configured + live monthly/annual prices (resolved by lookup_key) for the pricing UI |
create-checkout-session |
Auth: starts Stripe Checkout for a tier + interval |
create-portal-session |
Auth: opens the Stripe Billing Portal (manage/cancel/renewal) |
stripe-webhook |
Stripe-signed: the only writer of subscription tier/status + grace window |
export-account-data |
Authenticated: returns all server-side data for the caller (GDPR access/portability) |
request-account-deletion |
Authenticated: schedules the caller's account for deletion 7 days out |
process-account-deletions |
Cron-only (x-cron-secret): erases Storage objects + auth rows for accounts past their grace window |
Every track has a short_name (max 8 characters) used for:
- ZIP export filenames (
OKC.json) - Compact UI display in the header
- Falls back to
abbreviateTrackName()for tracks without a short name
- Enable Lovable Cloud
- Run the database migration (automatic)
- Create an admin user via the auth system
- Add the admin role:
INSERT INTO user_roles (user_id, role) VALUES ('<your-user-id>', 'admin'); - Set
VITE_ENABLE_ADMIN=true
- Node.js 18+ (or Bun)
# Clone the repository
git clone https://github.com/your-username/doves-dataviewer.git
cd doves-dataviewer
# Install dependencies
npm install
# or: bun install
# Start development server
npm run dev
# or: bun devOpen http://localhost:8080 in your browser.
| Command | Description |
|---|---|
npm run dev |
Start dev server on port 8080 |
npm run build |
Production build to dist/ |
npm run preview |
Preview production build locally |
npm run lint |
Run ESLint |
npm run typecheck |
Type-check via tsc -b (build mode — follows project references) |
npm test |
Run Vitest in watch mode |
npm run test:run |
Run Vitest once (CI-style) |
Phones and installed PWAs have no dev-tools console, so a silent runtime error is
invisible. Append ?dbg=true to the URL to show a bottom overlay that mirrors
all console.* output plus uncaught errors and unhandled promise rejections, with
copy / clear / collapse controls. The flag is sticky (persisted to
localStorage); load ?dbg=false to turn it back off. The capture installs
before first render so early errors are caught, and the overlay renders nothing
unless enabled. Implemented in src/lib/debugConsole.ts (pure flag-parse + log
buffer + capture) and src/components/DebugConsole.tsx (overlay).
The live coverage badge is a shields.io endpoint
backed by a GitHub Gist (not a Git branch — that kept Cloudflare Workers
Builds trying to deploy a badge-only branch). The coverage.yml workflow runs
npm run coverage:badge (which computes the % + color from the Vitest summary)
and pushes those fields to the gist on every push to main. To wire it up on a
fork:
- Create a public gist with a single file named
coverage-badge.json(any placeholder contents) and copy its ID from the URL (gist.github.com/<user>/<THIS_IS_THE_ID>). - Create a fine-grained/classic PAT with the
gistscope and add it as the repo secretGIST_TOKEN(Settings → Secrets and variables → Actions). - Add the gist ID as the repo variable
COVERAGE_GIST_ID(same page → Variables). - Replace
COVERAGE_GIST_IDin the Coverage badge URL at the top of this README with your gist ID.
The app is a static single-page app — npm run build emits a self-contained
dist/ folder (HTML + hashed JS/CSS + assets) with no server runtime to host.
It runs on any static host. The optional admin backend (Supabase) is independent
and unaffected by where the frontend is served — the browser just calls it over
HTTPS.
The repo ships ready for Cloudflare Workers (static assets) via the GitHub integration / Workers Builds:
- In the Cloudflare dashboard, create a Worker and connect this repo (Workers Builds).
- Build settings:
- Build command:
npm run build - Deploy command:
npx wrangler deploy(Workers Builds default). - Node version: pinned to
20via.nvmrc(matches CI).
- Build command:
wrangler.jsonc(repo root) configures the deploy — there's no Worker script, just a static-assets binding:assets.directory: "./dist"— uploads the Vite build output.not_found_handling: "single-page-application"— returns the app shell (index.html) for unmatched client-side routes like/privacyand/admininstead of 404ing.
public/_headersis copied into./distand honored by Workers static assets: it forcesno-cacheon the service workers +index.html(so new deploys take over instead of clients running a stale shell) and long-livedimmutablecaching on hashed/assets/*.
The public viewer needs none — vite.config.ts hardcodes public backend
fallbacks, so a zero-config build serves the full offline app with admin off.
To run the admin/track-submission features on the Cloudflare deploy, set these build-time variables (they're baked in at build, so a redeploy is required after changing them):
| Variable | Value |
|---|---|
VITE_ENABLE_ADMIN |
true |
VITE_ENABLE_REGISTRATION |
true (only if you want the /register route) |
VITE_SUPABASE_URL |
your Supabase project URL |
VITE_SUPABASE_PUBLISHABLE_KEY |
your Supabase anon key |
VITE_SUPABASE_PROJECT_ID |
your Supabase project ID |
VITE_TURNSTILE_SITE_KEY |
optional — Turnstile site key for the contact/submission CAPTCHA |
TURNSTILE_SECRET_KEY stays a Supabase edge-function secret — it is never a
client variable and does not belong in Cloudflare. The Supabase edge functions
(supabase/functions/) continue to run on Supabase; the Worker only serves the
static frontend.
Pushes to a non-production branch produce a preview version/URL of the same
Worker. To point those preview builds at a Supabase preview-branch database
(so beta work doesn't touch production data), set parallel *_PREVIEW build
variables. Workers Builds exposes WORKERS_CI_BRANCH (Pages: CF_PAGES_BRANCH)
on every build; vite.config.ts prefers the _PREVIEW value of each key
whenever that branch isn't main, and ignores them on main and in local dev.
-
Enable Branching in Supabase, then copy the preview branch's URL, anon key, and project ref from the Branches panel.
-
In the Worker → Settings → Build → Variables and Secrets, add (alongside the production values):
Variable Value HTT_SUPABASE_URL_PREVIEWpreview branch URL HTT_SUPABASE_PUBLISHABLE_KEY_PREVIEWpreview branch anon key HTT_SUPABASE_PROJECT_ID_PREVIEWpreview branch project ref Any key works the same way (e.g.
HTT_ENABLE_CLOUD_PREVIEW).VITE_*_PREVIEWis also accepted. Add the Cloudflare preview URL to the preview branch's Auth → Redirect URLs so cloud sign-in works there.
src/
├── components/ # React components
│ ├── ui/ # shadcn/ui base components
│ ├── admin/ # Admin panel tabs
│ ├── RaceLineView.tsx
│ ├── TelemetryChart.tsx
│ └── ...
├── lib/ # Parsers and utilities
│ ├── db/ # Modular database layer
│ ├── nmeaParser.ts
│ ├── ubxParser.ts
│ ├── iracingParser.ts
│ ├── vboParser.ts
│ ├── doveParser.ts
│ ├── alfanoParser.ts
│ ├── aimParser.ts
│ ├── motecParser.ts
│ └── ...
├── hooks/ # React hooks
├── pages/ # Route pages
└── types/ # TypeScript definitions
Contributions are welcome — new parsers, bug fixes, overlays, and reusability rewrites especially. See CONTRIBUTING.md for dev setup, coding conventions, how to add a new parser, and the PR checklist.
By participating you agree to abide by our Code of Conduct (CODE_OF_CONDUCT.md).
Found a security issue? Please follow the disclosure process in SECURITY.md rather than opening a public issue.
Release history is tracked in CHANGELOG.md.
AiM's native binary logs (.xrk, and zlib-compressed .xrz) are parsed
entirely in the browser by libxrk's
pure-Rust core compiled to WebAssembly (no Pyodide, no Python). No server
round-trip; nothing is uploaded. XRK behaves like every other format — fast,
fully offline, and usable as the main session, a reference, or an overlay.
How it runs (src/lib/xrk/):
- The wasm module (
src/lib/xrk/wasm/, ~200 KB, precached) is instantiated in a Web Worker the first time an XRK/XRZ file is parsed, so building a large session's arrays never freezes the UI. xrkWorker.tsruns libxrk on the uploaded bytes, thenxrkResample.ts(pure, unit-tested) aligns every channel onto the GPS timebase (interpolate vs forward-fill per channel), and the worker ships the result back as transferableFloat64Arraybuffers.xrkMapping.ts(pure, unit-tested) turns those channels into the app'sParsedData— GPS Latitude/Longitude/Speed become the sample primaries; the rest map to the canonical channel registry (channels.ts).- Progress (load → parse → align) is surfaced in the import UI.
Page weight & timing (3.1 MB .xrk / 4.4 MB .xrz, measured in Node — the
browser is comparable):
| Cost | When | Size / time |
|---|---|---|
| App bundle delta | every load | ~negligible — a few KB of eager glue; the worker is a separate ~6 KB lazy chunk |
libxrk wasm (src/lib/xrk/wasm/) |
first XRK import (precached) | ~200 KB (~81 KB gzipped), instantiate in ~tens of ms |
| Parse | per file | ~0.1 s (3.1 MB / 4.7k samples) · ~0.3 s (4.4 MB / 42k samples) |
Building / updating the wasm. The artifacts are committed under
src/lib/xrk/wasm/, built from libxrk's pure-Rust core via the thin wrapper
crate in xrk-wasm/ (which pins the libxrk revision in xrk-wasm/Cargo.toml).
To rebuild (e.g. to bump libxrk), run:
scripts/build-xrk-wasm.sh # needs rustup + (auto-downloads) wasm-bindgenBump the libxrk rev in xrk-wasm/Cargo.toml + the wasm-bindgen version in
the build script together, then re-run it and commit the regenerated artifacts.
License notices for libxrk + TrackDataAnalysis ship at
src/lib/xrk/wasm/THIRD-PARTY-NOTICES.txt.
Built on the shoulders of these incredible open-source projects and free services:
- React · Vite · TypeScript
- Tailwind CSS · shadcn/ui · Radix UI · Lucide Icons
- Leaflet · CARTO basemaps · Esri World Imagery & Wayback (satellite + historical imagery dates)
- TanStack Query · Sonner · react-resizable-panels
- mp4-muxer · Savitzky-Golay (ml.js) · JSZip · fix-webm-duration
- IEM ASOS (Iowa State) · NWS API · Open-Meteo (global weather fallback, CC-BY 4.0)
- MoTeC i2 (file format reference)
- libxrk (MIT) + TrackDataAnalysis (MIT) — AiM XRK/XRZ parser (Rust → WebAssembly)
Optional admin backend powered by Supabase via Lovable Cloud.
Licensed under the GNU General Public License v3.0 (or later) — see LICENSE. You are free to use, modify, and self-host; derivative works that you distribute must also be released under the GPL.
