Dove's DataViewer / HackTheTrack — Open-source, offline-first motorsport telemetry viewer.
- Live: hackthetrack.net | Published: dovesdataviewer.lovable.app
- Companion hardware: DovesDataLogger (ESP32 GPS logger with BLE)
- PWA with full offline support via service worker + IndexedDB
- Offline-first: 99% of features must work without network. Only weather, satellite tiles, and admin are exceptions.
- Modular & reusable: Prefer small composable modules over monoliths. Rewrites for reusability are always welcome.
- Update README.md when adding parsers, changing env vars, or modifying build params.
- Update credits (in README) when adding new FOSS dependencies.
- Never do on the server what you can do on the client.
| Layer | Technology |
|---|---|
| Framework | React 18 + TypeScript |
| Build | Vite + vite-plugin-pwa |
| Styling | Tailwind CSS + shadcn/ui (HSL design tokens in index.css) |
| Mapping | Leaflet (CartoDB + Esri tiles, cached 30 days by SW) |
| Charts | Custom Canvas 2D (not a library — see TelemetryChart.tsx, SingleSeriesChart.tsx) |
| Video Export | WebCodecs + mp4-muxer (H.264 video + AAC audio video + AAC audio MP4 output) |
| State | React hooks + React Query (for admin only) |
| Local Storage | IndexedDB (dbUtils.ts) for files/metadata/karts/notes/setups/video-sync/graph-prefs; localStorage for tracks & settings |
| Backend | None for core features. Optional admin via Supabase (Lovable Cloud) |
| BLE | Web Bluetooth API for DovesDataLogger device communication |
src/
├── pages/
│ ├── Index.tsx # Main SPA — file import, tab views, all state orchestration
│ ├── Admin.tsx # Admin panel (behind VITE_ENABLE_ADMIN)
│ ├── Login.tsx / Register.tsx / Privacy.tsx
│ └── NotFound.tsx
├── components/
│ ├── ui/ # shadcn/ui primitives (button, dialog, tabs, etc.)
│ ├── admin/ # Admin tabs: TracksTab, CoursesTab, SubmissionsTab, BannedIpsTab, ToolsTab, MessagesTab
│ ├── tabs/ # Main view tabs: GraphViewTab, RaceLineTab, LapTimesTab, LabsTab
│ ├── graphview/ # Pro mode: GraphPanel, GraphViewPanel, MiniMap, SingleSeriesChart, InfoBox
│ ├── drawer/ # File manager drawer tabs: FilesTab, KartsTab, NotesTab, SetupsTab, DeviceSettingsTab, DeviceTracksTab
│ ├── track-editor/ # Track editor sub-components
│ ├── RaceLineView.tsx # Leaflet map with race line, speed heatmap, braking zones
│ ├── TelemetryChart.tsx # Canvas-based speed/telemetry chart (simple mode)
│ ├── VideoPlayer.tsx # Synced video playback with modular overlay system
│ ├── video-overlays/ # Overlay system for video export
│ │ ├── types.ts # OverlayInstance, OverlaySettings, DataSourceDef, ThemeDef
│ │ ├── registry.ts # Overlay type definitions + factory
│ │ ├── themes.ts # Classic + Neon theme definitions
│ │ ├── dataSourceResolver.ts # Maps data source IDs → values/ranges/units
│ │ ├── DigitalOverlay.tsx # Numeric value + unit display
│ │ ├── AnalogOverlay.tsx # Canvas needle gauge (~252° arc)
│ │ ├── GraphOverlay.tsx # Rolling canvas line chart
│ │ ├── BarOverlay.tsx # Horizontal 0-100% progress bar
│ │ ├── BubbleOverlay.tsx # XY joystick-style circular widget
│ │ ├── sectorUtils.ts # Shared sector status logic (colors, segment computation)
│ │ ├── MapOverlay.tsx # Mini canvas race line with position dot + optional sector coloring
│ │ ├── PaceOverlay.tsx # Horizontal pace delta indicator
│ │ ├── SectorOverlay.tsx # 3 sector bubbles with delta + sparkle animation
│ │ ├── LapTimeOverlay.tsx # Lap timer with optional pace mode (delta + best lap)
│ │ ├── OverlaySettingsPanel.tsx # Add/configure/remove overlay instances
│ │ └── VideoExportDialog.tsx # Export dialog with quality options
│ ├── FileImport.tsx # Drag-and-drop file import
│ ├── DataloggerDownload.tsx # BLE device download UI
│ ├── ContactDialog.tsx # Public contact form dialog (categories shared const)
│ └── ...
├── hooks/
│ ├── useSessionData.ts # Parses imported file → ParsedData
│ ├── useLapManagement.ts # Lap calculation, selection, visible range
│ ├── usePlayback.ts # Playback cursor (shared across chart + map)
│ ├── useReferenceLap.ts # Reference lap overlay logic
│ ├── useVideoSync.ts # Video ↔ telemetry synchronization
│ ├── useFileManager.ts # IndexedDB file CRUD
│ ├── useKartManager.ts # Backward compat re-export → useVehicleManager
│ ├── useVehicleManager.ts # Vehicle profiles CRUD
│ ├── useTemplateManager.ts # Vehicle types & setup templates CRUD
│ ├── useNoteManager.ts # Session notes CRUD
│ ├── useSetupManager.ts # Generic setup sheets CRUD (template-driven)
│ ├── useSettings.ts # User preferences (units, smoothing, dark mode, etc.)
│ ├── useSessionMetadata.ts # Per-file metadata (selected track/course)
│ └── useOnlineStatus.ts # Navigator.onLine wrapper
├── lib/
│ ├── datalogParser.ts # ★ Format auto-detection router (entry point for all parsing)
│ ├── nmeaParser.ts # NMEA 0183 text parser (fallback format)
│ ├── ubxParser.ts # u-blox UBX binary parser
│ ├── vboParser.ts # Racelogic VBO parser
│ ├── doveParser.ts # DovesDataLogger CSV parser
│ ├── dovexParser.ts # DovesDataLogger extended format (.dovex) with 8192-byte metadata header
│ ├── alfanoParser.ts # Alfano CSV parser
│ ├── aimParser.ts # AiM MyChron CSV parser
│ ├── motecParser.ts # MoTeC LD binary + CSV parser
│ ├── parserUtils.ts # Shared parser helpers (haversine, speed calc, etc.)
│ ├── fieldResolver.ts # Canonical field name mapping across parsers
│ ├── courseDetection.ts # ★ Auto course detection, direction detection, waypoint mode
│ ├── lapCalculation.ts # Start/finish line crossing detection → Lap[]
│ ├── brakingZones.ts # Braking zone detection from G-force data
│ ├── speedEvents.ts # Min/max speed event detection
│ ├── speedBounds.ts # Speed range utilities
│ ├── gforceCalculation.ts # G-force derivation from GPS data
│ ├── chartUtils.ts # Canvas chart rendering helpers
│ ├── chartColors.ts # Color palette for multi-series charts
│ ├── trackUtils.ts # Track geometry utilities (findNearestTrack: 5mi radius)
│ ├── trackStorage.ts # localStorage: tracks + courses (merged with public/tracks.json) + course drawings loader
│ ├── referenceUtils.ts # Reference lap comparison utilities
│ ├── dbUtils.ts # ★ Shared IndexedDB: DB_NAME, DB_VERSION, openDB(), transaction helpers
│ ├── fileStorage.ts # IndexedDB: raw file blobs
│ ├── kartStorage.ts # Old kart storage (kept for compat)
│ ├── vehicleStorage.ts # ★ Vehicle profiles CRUD (replaces kartStorage)
│ ├── templateStorage.ts # ★ Vehicle types + setup templates, default kart schema
│ ├── noteStorage.ts # IndexedDB: session notes
│ ├── setupStorage.ts # IndexedDB: kart setups
│ ├── videoStorage.ts # IndexedDB: video sync points + overlay settings
│ ├── videoFileStorage.ts # ★ IndexedDB: video file blobs + metadata (exportType, lapNumber, hasOverlays)
│ ├── videoExport.ts # VideoWebCodecs H.264+AAC, fallback MediaRecorder fix-webm-duration)
│ ├── overlayCanvasRenderer.ts # Canvas-based overlay drawing for export
│ ├── graphPrefsStorage.ts # IndexedDB: per-session graph selections
│ ├── bleDatalogger.ts # Web Bluetooth: DovesLapTimer BLE protocol (files + settings + tracks)
│ ├── deviceTrackSync.ts # Track sync logic: merge/compare app↔device tracks, coordinate diff
│ ├── deviceSettingsSchema.ts # Device settings key definitions + validation
│ ├── weatherService.ts # OpenWeatherMap API (online-only)
│ ├── db/ # Admin database layer (modular, swappable)
│ │ ├── types.ts # ITrackDatabase interface
│ │ ├── supabaseAdapter.ts # Supabase implementation
│ │ └── index.ts # Factory: getDatabase()
│ └── utils.ts # Tailwind cn() helper
├── types/
│ └── racing.ts # ★ Core types: GpsSample, ParsedData, Lap, Course, Track, etc.
├── contexts/
│ ├── SettingsContext.tsx # Settings provider (useKph, gForce, brakingZones, darkMode, labs)
│ ├── DeviceContext.tsx # Global BLE connection state provider
│ └── AuthContext.tsx # Admin auth context
│ └── AuthContext.tsx # Admin auth context
└── integrations/supabase/ # Auto-generated — DO NOT EDIT
├── client.ts
└── types.ts
File Import (drag-drop / BLE download / file manager)
→ fileStorage.ts (save raw blob to IndexedDB)
→ useSessionData.ts (read blob, call parseDatalogFile)
→ datalogParser.ts (auto-detect format, route to specific parser)
→ returns ParsedData { samples: GpsSample[], fieldMappings, bounds, duration, startDate, dovexMetadata?, parserStats? }
→ courseDetection.ts (auto-detect track, course, direction; waypoint fallback)
→ returns CourseDetectionResult { track, course, direction, laps, isWaypointMode }
→ useLapManagement.ts (detect laps via lapCalculation.ts using selected course's start/finish line)
→ returns Lap[] with timing, speed stats, sector times
→ Visualization:
Simple mode: RaceLineView (Leaflet map) + TelemetryChart (Canvas)
Pro mode: GraphViewPanel (multi-series Canvas charts) + MiniMap (Leaflet)
Each parser exports two functions:
isXxxFormat(input: string | ArrayBuffer): boolean— format detectionparseXxxFile(input: string | ArrayBuffer): ParsedData— full parse
To add a new parser:
- Create
src/lib/xxxParser.tswithisXxxFormat()+parseXxxFile() - Register in
src/lib/datalogParser.ts— add import + detection check in bothparseDatalogFile()andparseDatalogContent() - Update
README.mdsupported formats table - Update this file's architecture map
Detection order matters: binary formats first (MoTeC LD → UBX), then text formats from most-specific to least (VBO → MoTeC CSV → Dovex → Dove → Alfano → AiM → NMEA fallback).
| Type | Key Fields |
|---|---|
GpsSample |
t (ms), lat, lon, speedMps/Mph/Kph, heading?, extraFields: Record<string,number> |
ParsedData |
samples[], fieldMappings[], bounds, duration, startDate?, dovexMetadata?, parserStats? |
ParserStats |
totalRows, acceptedRows, rejected: { nanFields, zeroCoords, outOfRange, speedCap, teleportation, incompleteRow } |
DovexMetadata |
datetime?, driver?, course?, shortName?, bestLapMs?, optimalMs?, lapTimesMs?[] |
Lap |
lapNumber, startTime/endTime, lapTimeMs, speed stats, startIndex/endIndex, sectors? |
Course |
name, lengthFt?, startFinishA/B (lat/lon), optional sector2/sector3 lines |
Track |
name, shortName? (max 8 chars), courses[] |
CourseDetectionResult |
track, course, direction?, laps[], isWaypointMode, waypointNotice? |
CourseDirection |
'forward' | 'reverse' |
FieldMapping |
index, name, unit?, enabled — maps extraFields to UI toggles |
FileMetadata |
fileName, trackName, courseName, weatherStation*?, sessionKartId?, sessionSetupId?, fastestLapMs?, fastestLapNumber? |
When a file is loaded and no track/course is saved in metadata, the system auto-detects:
- Track discovery: Find first valid GPS sample within 5 miles (~8047m) of any known track
- Course matching: Try each course's S/F line → calculate laps → compare average lap distance (ft) to course
lengthFt→ pick closest match within 25% tolerance - Direction detection: After S/F crossing, check which sector is crossed first — Sector 2 = forward, Sector 3 = reverse. Only works on courses with known sector lines.
- Waypoint mode fallback: If no track matches or no course produces valid laps:
- Drop a waypoint at the first sample where speed ≥ 30 MPH
- Track returns to waypoint (within 30m after traveling 100m+) for rough lap timing
- Divide lap distance by 3 for approximate sector boundaries
- Show notice: "Waypoint timing — lower accuracy. Create a track for precise timing."
Extended Dove format with an 8192-byte (8 KB) metadata header:
Line 1: datetime,driver,course,short_name,best_lap_ms,optimal_ms
Line 2: 2024-03-15 14:30:00,Mike,Full CW,OKC,62345,61200
Line 3: lap_times_ms
Line 4: 65432,64321,62345,63456 (lap times in ms, comma-separated)
\n padding to byte 8192
Byte 8192+: standard .dove CSV (timestamp,sats,hdop,lat,lng,...)
GPS data is always parseable even if metadata is corrupted. Metadata is attached as ParsedData.dovexMetadata.
Single shared database: "dove-file-manager", version 9.
| Store | Key | Module |
|---|---|---|
files |
name |
fileStorage.ts |
metadata |
fileName |
fileStorage.ts |
karts |
id |
kartStorage.ts |
notes |
id (indexed by fileName) |
noteStorage.ts |
setups |
id (indexed by kartId) |
setupStorage.ts |
video-sync |
sessionFileName |
videoStorage.ts |
graph-prefs |
sessionFileName |
graphPrefsStorage.ts |
vehicle-types |
id |
templateStorage.ts |
setup-templates |
id |
templateStorage.ts |
session-videos |
sessionFileName |
videoFileStorage.ts |
To add a new store: increment DB_VERSION, add store name to STORE_NAMES, add creation logic in openDB(), create a corresponding storage module.
The course_layouts table stores polyline drawings of track layouts (1:1 with courses, unique on course_id, cascade delete).
| Column | Type | Description |
|---|---|---|
id |
uuid PK | Auto-generated |
course_id |
uuid FK → courses.id (unique) | One layout per course |
layout_data |
jsonb | Array of {lat, lon} coordinate points |
created_at / updated_at |
timestamptz | Timestamps |
Access: Admin-only RLS (same pattern as courses table). Layout lengths (in feet) ARE exported to track JSON files as lengthFt.
Draw tool: In the VisualEditor, a "Draw" button allows clicking on the satellite map to build a polyline outline. This manual drawing tool is admin-only (isAdminEditor={true} in CoursesTab).
Generate Drawing: A "Generate" button (visible when laps are available and showDrawTool is true) lets users select a lap and auto-populate the drawing from that lap's GPS samples. Always available in user-side TrackEditor when session data is loaded. Laps and samples are threaded from Index.tsx → TrackEditor → VisualEditor.
"Generate Course Mapping" button: Placeholder in admin CoursesTab — will eventually produce fingerprint data for automatic track detection on the DovesDataLogger hardware.
Submissions: The submissions table has has_layout (bool) and layout_data (jsonb) columns to carry drawing data through the submission workflow.
Public drawings: Admin exports drawings to public/drawings.json (keyed by shortName/courseName → [{lat, lon}, ...]). Loaded by trackStorage.ts:loadCourseDrawings() (cached). Rendered on the race line map as a dashed polyline outline when a course is selected. Helper: getDrawingForCourse(shortName, courseName).
Connects to DovesLapTimer ESP32 device via Web Bluetooth.
| UUID | Characteristic | Purpose |
|---|---|---|
0x1820 |
Service | Internet Protocol Support (container) |
0x2A3D |
File List | Read: newline-separated filename,size pairs |
0x2A3E |
File Request | Write: GET:filename, LIST, SLIST, SGET:key, SSET:key=value, SRESET, TLIST, TGET:name, TPUT:name, TDEL:name, BATT |
0x2A3F |
File Data | Notify: chunked file data (reassembled client-side) |
0x2A40 |
File Status | Notify: SIZE:n, DONE, ERROR:msg, settings (SVAL, SEND, SOK, SERR), tracks (TFILE, TEND, TREADY, TOK, TERR), battery (BATT:<pct>,<volt>) |
LIST → select file → GET:filename → receive SIZE → stream data chunks → DONE.
SLIST→ device sendsSVAL:key=valuefor each setting on fileStatus, ends withSENDSGET:key→ device respondsSVAL:key=valueorSERR:NOT_FOUNDon fileStatusSSET:key=value→ device respondsSOK:keyorSERR:WRITE_FAILon fileStatusSRESET→ device respondsSOK:RESETon fileStatus, then reboots. App should disconnect immediately after receiving confirmation.
TLIST→ device sendsTFILE:name.jsonper file on fileStatus, ends withTENDTGET:name.json→ reuses existing SIZE → data chunks (fileData) → DONE (fileStatus) transfer patternTPUT:name.json→ device respondsTREADYon fileStatus → app sends data chunks on fileRequest (64-byte max) →TDONE→ device respondsTOKorTERR:reasonTDEL:name.json→ device respondsTOKon fileStatus (success) orTERR:reason(failure). 10s timeout.
BATT→ device respondsBATT:<percent>,<voltage>on fileStatus (e.g.,BATT:85,3.98). 5s timeout.
Settings schema is defined in src/lib/deviceSettingsSchema.ts — maps keys to labels, types, and validation rules. Unknown keys from the device are displayed as raw string fields (forward-compatible).
Pure comparison/conversion logic for merging app tracks with device track files:
buildMergedTrackList()— matches tracks by shortName, courses by name, classifies as synced/mismatch/device_only/app_onlycoursesMatch()— coordinate comparison with epsilon (0.0000005°)buildTrackJsonForUpload()— serializes app Track to device JSON format (flat course array, includeslengthFt)deviceCourseToAppCourse()/appCourseToDeviceJson()— format converters (both includelengthFt)DeviceCourseJsonincludeslengthFt?: numberfor hardware course detection by lap distance
The slide-out drawer (FileManagerDrawer.tsx) has two top-level tabs:
- Garage — Files, Karts, Setups, Notes (original functionality)
- Device — BLE device management, gated behind a "Connect to Logger" prompt
Device sub-tabs:
- Settings — Read/write device settings via SLIST/SGET/SSET protocol
- Tracks — Full track sync manager: downloads all device track JSONs, merges with app tracks, shows sync status per track/course, supports upload/download/diff with side-by-side comparison modal
Global BLE connection state is managed by DeviceContext.tsx, wrapping the app tree in Index.tsx.
useSettings hook (persists to localStorage) → SettingsContext for tree-wide access.
Key settings: useKph, gForceSmoothing, gForceSmoothingStrength, brakingZoneSettings (thresholds, duration, smoothing, color, width), enableLabs (hidden when no labs features), darkMode.
fieldResolver.ts maps parser-specific field names (e.g., "Lat G", "Lateral G", "LatG") to canonical IDs (lat_g) so settings apply uniformly.
| Variable | Client/Server | Description |
|---|---|---|
VITE_SUPABASE_URL |
Client | Backend URL (auto-set) |
VITE_SUPABASE_PUBLISHABLE_KEY |
Client | Backend anon key (auto-set) |
VITE_SUPABASE_PROJECT_ID |
Client | Backend project ID (auto-set) |
VITE_ENABLE_ADMIN |
Client | "true" to enable admin UI + /login route |
VITE_ENABLE_REGISTRATION |
Client | "true" to enable /register route |
VITE_TURNSTILE_SITE_KEY |
Client | Cloudflare Turnstile site key (optional CAPTCHA) |
TURNSTILE_SECRET_KEY |
Server (edge fn) | Turnstile secret — ??? |
npm run dev # Dev server on :8080
npm run build # Production build → dist/
npm run lint # ESLint
npm run preview # Preview production build- No server when client works — this is the #1 rule
- Hooks are composable — each hook does one thing,
Index.tsxorchestrates - Parsers: always export
isXxxFormat()+parseXxxFile(), register indatalogParser.ts - IndexedDB stores: all registered in
dbUtils.ts, individual modules usewithReadTransaction/withWriteTransaction - Tracks:
public/tracks.jsonis the source of truth at runtime; admin DB builds this file. Export format includeslongName,shortName,defaultCourse, and per-courselengthFt. Tracks table hasdefault_course_idFK. CourselengthFtvalues are imported aslength_ft_overridein the database. - Course Detection:
courseDetection.tshandles auto-detection of track/course/direction on file load, with waypoint mode fallback. Find nearest track within 5mi, match course by lap distance vslengthFt. - Course Drawings: Admin can export/import course layout drawings separately from tracks. Import clears
length_ft_overridefor imported courses (drawing becomes source of truth). - CSS: use Tailwind semantic tokens from
index.css, never hardcode colors in components - Admin code is fully optional and gated behind env vars — core app has zero admin dependencies
- Edge functions live in
supabase/functions/, auto-deployed, configured insupabase/config.toml - Stale-state gotcha: When calling a function immediately after
setState, the new value isn't available in the current closure. Pass values explicitly (e.g.,calculateAndSetLaps(course, samples, fileName)) instead of relying on state that was just set.
Update the readme when new parsers are added and when build parameters change. Make sure to ALWAYS note new environment variables and their values (use "???" When it is a secret value) in the readme.
Update the credits list when new Foss libraries are added.
Never do on a server what you can do on the client, the NUMBER ONE PRIORITY for this webapp is that 99% of the features are available offline. (Things like weather, satellite view etc, are obvious exceptions).
Keep code modular and reusable, fuck line count as long as you can reuse the shit out of things, rewrites to make things more reusable are always cool.
ALWAYS keep CLAUDE.md updated with new files and information to help it as well.