Analysis Hall¶
Status: Shipped
Owner: PWA (src/pwa/)
Route: /garage/analysis
The Analysis Hall is the off-track telemetry workbench. It replaces the prior Analysis Hub tile-grid with a tabbed surface that queries the session parquet bundles directly in the browser via DuckDB-Wasm. The old tile grid still ships as the Modules tab so the per-domain sub-pages (Lap Times Hall, Corner Mastery, Track Atlas, …) stay one click away.
Overview¶
- Why tabs: the prior tile-grid forced a screen change for every view; comparing lap-time and IMU traces meant losing the active lap
- outlier filter context. The tabbed shell keeps
sessionId,selectedLapIndex, andfilterOutliersconstant while the workbench pane swaps. - What it consumes: session parquet bundles already exported by the
bridge (wide canonical telemetry + tall
telemetry_signals+ signal registry), plus the bridge's server-side lap detector atGET /session/<sid>/laps. - What it does not do: it never live-streams. The realtime cockpit is owned by the On-Track HUD. Analysis Hall is a paddock surface.
Architecture¶
parquet bundle ─▶ DuckDB-Wasm (web worker) ─▶ tab queries
▲ │
│ ▼
/session/<sid>/laps Chart.js / Leaflet
│ (lazy-loaded)
▼
analysisStore (Pinia)
sessionId · laps · activeTab
selectedLapIndex · filterOutliers
- DuckDB-Wasm worker is bundled same-origin
(
src/pwa/src/shared/lib/duckdb/) to avoid the jsdelivr CORS failure that bit the earlier prototype. - Tabs are
defineAsyncComponent-loaded. The Leaflet bundle ships only when the Overview tab opens; Chart.js + the zoom plugin only when Telemetry / Systems / IMU / Signals open. - Lap filter is one fragment.
analysisStore.lapFilterSql(col)returns{ sql, params }(e.g.' AND timestamp BETWEEN ? AND ?') that every tab concatenates into its own DuckDB query. Empty lap selection = empty fragment, so the same query string works for "full session" or any windowed lap.
Tabs¶
Six tabs in src/pwa/src/pages/analysis-hub/tabs/. Each is independent
and re-runs its queries when sessionId, selectedLapIndex, or
filterOutliers change.
Overview (OverviewTab.vue)¶
An 11-cell summary grid on the left (duration, wide-row count,
tall-row count, distinct signals, distance, peak speed mph, peak rpm,
peak G, wide rate Hz, signal rate, total registry size) and a Leaflet
map of Sonoma on the right. The map draws three layers: the centerline
from GET /track/<id>/elevation, the raw GPS trace from the parquet,
and a virtual path mapped from distance_m along the centerline.
DuckDB-Wasm queries against telemetry, telemetry_signals, and
signal_registry.
Telemetry (TelemetryTab.vue)¶
Three time-series panels: Speed + RPM, driver inputs (throttle / brake
/ steering), and G-forces (g_lat, g_long, combo_g, plus g_vert
merged in from the tall store). Decimates to ~4000 points to keep
Chart.js + the zoom plugin responsive. Pure DuckDB-Wasm — no bridge
call.
Systems (SystemsTab.vue)¶
TPMS 2x2 corner grid (latest sample per corner: pressure / temperature
/ voltage / alarm) plus stock-comparison overlays for engine
temperatures, engine pressures, TPMS pressures + temperatures, wheel
speeds, and GPS-vs-wheel speed. Each comparison panel renders only
when at least one of its signals has samples this session. All values
come from the tall telemetry_signals table.
IMU (ImuTab.vue)¶
Two stacked charts pulled from the 50 Hz tall store: accelerometers
(inline_accel_g, lateral_accel_g, vertical_accel_g) and gyros
(roll_rate_degs, pitch_rate_degs, yaw_rate_degs). DuckDB-Wasm
only.
Signals (SignalsTab.vue)¶
Full port of the standalone telemetry-viewer's signal picker. Left
pane: search box, preset buttons (speed+rpm, driver inputs, G's, IMU,
oil, clear), grouped signal list with checkboxes (auto-grouped by
units from signal_registry). Right pane: twin-axis overlay chart
with zoom + pan. Series are capped at two units per axis so the chart
never carries more than two y-axes.
Modules (ModulesTab.vue)¶
The old tile-grid hub, preserved so the per-domain sub-pages stay discoverable. Tiles route to Lap Times Hall, Corner Mastery, Straights & Speed, Track Atlas, Driver Evolution, Pedal Profile, Ghosts, Replay, and SQL Console. Tile counts come from the session store + save-slot, never hardcoded.
Toolbar¶
The toolbar sits above the tab bar and applies to every tab.
| Control | Source | Notes |
|---|---|---|
| Session picker | GET /sessions?limit=50 |
Defaults to the most recent session by started_at |
| Lap picker | GET /session/<sid>/laps |
Index 0 is the synthetic "Full session" pseudo-lap; >0 indexes detected laps |
| Filter outliers checkbox | src/pwa/src/shared/lib/telemetry/outliers.ts |
Sanity-clips signals against per-signal ranges; OOB values become nulls (chart gaps) |
| Loading / error chips | analysisStore.loading / error / lapsError |
Surfaces DuckDB and laps-endpoint failures inline |
Keyboard¶
1-6 switch tab (Overview / Telemetry / Systems / IMU / Signals / Modules)
◀ / ▶ cycle lap selection
O toggle outlier filter
B / ⌫ back to /garage
ESC router.back() (owned by App.vue, not the page)
Touch targets on the toolbar are ≥44×44 to keep Pixel 10 landscape taps clean.
Lap detection¶
Laps come from the bridge — they're computed server-side off the same data the parquets export, so the workbench shares the canonical lap boundaries with Stage Clear, Lap Times Hall, and the HUD's derived lap counter.
Endpoint: GET /session/<sid>/laps →
{
"laps": [
{ "name": "L01", "t_start": 0, "t_end": 102.3,
"duration_s": 102.3, "distance_m": 4034.1, "method": "gps" }
],
"track_length_m": 4034.1
}
method is one of:
gps— start/finish line crossing detected on lat/londistance— cumulativedistance_mmodulo the loaded track lengthstint— heuristic fallback (continuous driving windows when GPS is absent)all— the synthetic full-session pseudo-lap injected at index 0 client-side
When the bridge can't serve laps the store surfaces the error in the toolbar and degrades to "Full session" only — it does not fabricate laps.
File layout¶
src/pwa/src/pages/analysis-hub/
├── AnalysisHub.vue ← tabbed shell + toolbar + keyboard
├── tabs/
│ ├── OverviewTab.vue ← summary grid + Leaflet map
│ ├── TelemetryTab.vue ← speed/rpm, inputs, G's
│ ├── SystemsTab.vue ← TPMS + engine compare overlays
│ ├── ImuTab.vue ← accel + gyro
│ ├── SignalsTab.vue ← signal picker + overlay
│ └── ModulesTab.vue ← old tile grid (sub-pages)
├── GhostManager.vue ← /analysis/ghosts (sub-page)
└── TelemetryReplay.vue ← /analysis/replay (sub-page)
src/pwa/src/entities/analysis/model/analysisStore.ts
sessionId · laps · trackLengthM · selectedLapIndex
filterOutliers · activeTab · loading · error · lapsError
getters: selectedLap, lapFilterSql(col)
actions: loadSession, setSelectedLap, cycleLap,
setFilterOutliers, setActiveTab
src/pwa/src/shared/lib/telemetry/outliers.ts
SANITY_RANGES (per-signal min/max from AiM MXP v3.0 + DBC hints)
clipOutliers(name, data, enabled)
src/pwa/src/shared/lib/telemetry/queries.ts
asNum, decimate, tallT0, fetchSignalSeries, latestSignalValue
The shell pulls CyberTabs, CyberPanel, CyberCheckbox from
src/pwa/src/shared/ui/core/; charts use
src/pwa/src/shared/ui/charts/CyberLineChart.vue for theming.
Known limits¶
- Parquet exports are required. Sessions recorded on backends that
only ship the SQLite DDL (e.g. the phone deployment running without
pyarrow) won't have parquet bundles, and the workbench fails open
with an honest
DATA UNAVAILABLEchip. The bridge needs either DuckDB or pyarrow to materialise the bundles. - DuckDB-Wasm is a worker; the first session load incurs a one-time
Wasm download that's then cached by the service worker (Workbox
runtime cache configured in
vite.config.ts). - Leaflet ships only when the Overview tab opens — if a session has no GPS the centerline still draws but the raw GPS and virtual-path layers will be empty.
- Outlier ranges are static (
SANITY_RANGESliteral). Signals not in the table pass through unfiltered regardless of the toggle.