feat: session-replay backend + PWA install/nav/SSE polish + deploy scripts - #36
Merged
Conversation
added 6 commits
May 24, 2026 06:04
Adds a session-replay feature so the PWA can drive a "simulated car"
from a previously-recorded SQLite session. The replay thread reads the
target session's wide rows + tall signals, groups them by nearest wide
timestamp, and re-publishes canonical wide_snapshot frames to
telemetry_bus at the original cadence (or scaled via the `speed` param).
Contract:
POST /session/replay/start body {source_session_id, speed?, loop?}
-> 200 {replay_id, source_session_id, total_frames, est_duration_s,
speed, loop}
-> 404 if source has no telemetry rows
-> 409 if a replay is already running
-> 503 if no DB backend reachable
POST /session/replay/stop -> 200 {stopped, frames_emitted}
GET /session/replay/status -> 200 {running, source_session_id, speed,
loop, frame_idx, total_frames,
elapsed_s, est_remaining_s}
Behaviour:
- Sets state.active_session_id to source_session_id so /health and the
existing SSE stream pick the replay up automatically (no PWA changes
required).
- Single-publisher invariant: if a CAN reader is running at start,
stop it cleanly so frames don't interleave.
- Wide_snapshot uses PWA-facing keys (speed/rpm/brake_pressure/throttle/
steering/g_lat/g_long/distance/lat/lon) — NOT the v3.0 raw names
(speed_ms/brake_bar/etc).
- Sleep is capped at 1.0 s per chunk so /stop responds within ~1 s.
- On loop=true, restarts from frame 0 at end of source.
Smoke-tested on the phone against the 12,936-row Sonoma recording:
status -> idle, start (speed=10, loop=true) -> running, 254 frames
emitted in 3 s, SSE delivered canonical-shape frames, stop returned
{stopped:true, frames_emitted:260} in ~1 s.
Memory-leak hardening:
- HardwareDetail: null out liveTimer/histTimer refs after clearInterval
in onUnmounted (telemetry.close() + clears were already wired).
- PedalProfile: null out refetchTimer ref after clearTimeout in
onUnmounted.
v-for key stability (replace array index with stable identity):
- OnboardingFlow.vue: step dots -> `step-${i}` (i is 1..N from
totalSteps; stable per slot).
- AvatarSelect.vue: avatar buttons -> avatar.id.
- PedalProfile.vue: friction G-G samples -> composite of index +
(gLat,gLong); utilisation histogram bins -> `bin-${i*10}`.
- GlobalLeaderboard.vue: rows -> `rank-${entry.rank}-${entry.initials}`
(future-proof; store is currently empty).
- SqlConsole.vue: result rows -> `row-${i}-${JSON.stringify(row)}`;
examples list -> ex.title.
- DriverEvolution.vue: heatmap cells -> `${c.id}-lap-${i}` composite.
- AskCoachMode.vue: conversation turns -> t.recorded_at when present,
else composite of (i, role, text-prefix).
…persistence (PR-C)
Unify the keyboard contract across the PWA: ESC is now "go back" on every page (router.back) and P is the pause overlay (in-session pages only, opted in via route.meta.allowPause). Fixes a bag of nav-discoverability papercuts shipped on landscape phones. 1. App.vue — ESC routes back globally (or dismisses pause if visible); P only toggles pauseStore on routes with meta.allowPause === true. Removed the old ESC-as-pause-anywhere global handler. 2. router/index.ts + OnTrackHud.vue — /hud opts into meta.allowPause; HUD's local handler now uses P for its pause overlay and treats B as a resume alias. 3. GarageHub.vue + CoachFloat.vue — raised CoachFloat z-index from 20 to 100 so the welcome dialogue can never be covered by a focused spatial tile. Hint bar now lists B/C/H so previously-hidden shortcuts (CALIBRATE, HARDWARE) are discoverable. 4. PreBrief.vue — already had WALK · W in its actions array; verified. 5. OnTrackHud.vue — added a top-right ESC · BACK / P · PAUSE corner hint so the immersive HUD has a visible escape hatch; wrapped the 10px status-pip in a 44x44 hit target (WCAG 2.5.5). 6. TelemetryReplay.vue — decoupled from can.connected; SSE subscribes whenever bridgeStore.health.active_session_id is non-null, mirroring PitStall's watch shape, so replay-only sessions are not blocked by missing CAN. 7. HintBar.vue — dev-only sanity-check stub for missing key hints, tree-shaken from prod; TODO documents the useKeyboard registry it would consume. 8. SaveSelect.vue — dropped the redundant window.addEventListener keydown listener; HOLD-B-DELETE now goes through useKeyboard with a single narrow keyup listener for the release edge.
Wires up the InstallPrompt widget added in 7f42f1a (PR-C) so Chrome's beforeinstallprompt event surfaces a sticky banner. No-ops on iOS Safari, in standalone contexts, and for 7 days after dismissal.
…scripts, PWA icons
PWA:
- PreBrief: BriefSourcePicker + SessionStartPicker widgets to choose
the replay session that drives /coach/brief and the live HUD
- Remove templated/hardcoded fallbacks across pages so screens show
real bridge data or an honest 'unavailable' state (NotificationCenter,
CornerMastery, TrackWalk, TrainerCard, EndOfDay, LapTimesHall,
StraightsAndSpeed, CoachCodexMode)
- Real-data notificationStore + leaderboardStore (SSE-aware, no mocks)
- PWA installability: PNG 192/512/maskable + apple-touch-icon, manifest
fix in vite.config.ts and index.html
- Tests: sessionStartPicker widget test, hardened SSE reconnect tests
Backend:
- coaching/litert_coach: drop _templated_pre_brief synthesis; on LLM
failure return ('', [], 'neutral') and record llm_friction
- coaching/bp_coaching: /coach/brief response now exposes error field
from latest llm_friction row
Deploy:
- deploy/phone/00..99 ladder of shell scripts (rooted Pixel 10 +
Termux), README + _common.sh + status.sh, SIM env knobs for the
synthetic simulator path
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Originally a session-replay backend PR; expanded to bundle the connected PWA polish (install + nav + SSE), the no-fake-fallback sweep, and the
deploy/phone/ladder.Bridge — session replay
POST /session/replay/start{source_session_id, speed?, loop?}→ 200 withtotal_frames+est_duration_s. 404 if no rows, 409 if already running, 503 if no DB.POST /session/replay/stop→{stopped, frames_emitted}within ~1 s.GET /session/replay/status→frame_idx / elapsed_s / est_remaining_s.state.active_session_idflips tosource_session_idso/healthand the PWA telemetry store auto-bind to the replay.PWA — install + key contract + nav (PR-A, PR-B, PR-C, mount)
ESC → router.back(),P → pauseStore(only onmeta.allowPauseroutes like/hud). Pause overlay ESC dismisses overlay instead of route.beforeinstallprompt, sticky banner, 7-day dismiss cooldown, no-op on iOS/standalone. Mounted globally inApp.vue.fullscreenPreferred) with private-mode safety.bridgeStore.health?.active_session_idso SSE subscribes during replay.active_session_id.PWA — no fake fallbacks
PreBriefintegratesBriefSourcePicker+SessionStartPickerto pick the replay session that drives/coach/briefand the live HUD; "Settle in. Peak grip…" fake text removed; surfacesbriefErrorpanel.notificationStore+leaderboardStore(SSE-aware, no mocks).PWA — installability
192/512/maskable+apple-touch-icon-180.pngundersrc/pwa/public/icons/.vite.config.ts, viewport + icon link refs inindex.html.Backend — no fake fallbacks
coaching/litert_coach.py: drop_templated_pre_brief; on LLM failure return('', [], 'neutral')and recordllm_friction.coaching/bp_coaching.py:/coach/briefresponse exposeserrorfield from latestllm_frictionrow.Deploy
deploy/phone/00..99shell ladder (rooted Pixel 10 + Termux) — prerequisite check, Termux packages, repo stage, deps, recording stage, build PWA, port forwards, start bridge (withSIM=1/SIM_SPEED/SIM_LAP_SECONDSenv knobs), open PWA, stop._common.sh+status.sh+README.md.Smoke-tested on phone (12,936-row Sonoma recording)
statusidle →{running:false}✓start(speed=10, loop=true) → 254 frames emitted in 3 s ✓stop→{stopped:true, frames_emitted:260}in ~1 s ✓/health.active_session_idflipped totrack-sonoma-2026-05-23-1✓Test plan
POST /session/replay/start, verify SSE cadence.POST /session/replay/startreturns 409 when called twice./; P opens pause only on/hud.