You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Ride Recording is currently a property of an active Board Session. This prevents the rider from recording a route when:
riding without a VESC connection,
riding a borrowed or otherwise unlinked board,
the board disconnects during a ride,
only phone GPS is available,
GPS-derived speed alerts are desired without board telemetry.
The user intent is broader than the current implementation: pressing REC should start a durable riding activity. Board telemetry should enrich that activity when available, not define whether it exists.
Current architecture
Native owns durable ride truth. JS renders native state and sends intents.
Today the persistence chain is:
A Board Session reaches a recordable phase.
RecordingCoordinator enables TelemetryRepository for that Board Session.
Each VESC telemetry packet becomes a TelemetryCapture.
The latest acceptable precise GPS fix is attached to that telemetry capture.
Native writes delta-encoded telemetry_frames, minute buckets, and lifecycle markers.
RideHistoryRepository derives Ride History sessions by grouping minute buckets.
History discards groups without board speed samples and renders routes only from GPS embedded in telemetry-backed samples.
There is no durable Activity/Ride Recording identity independent of a board. There is also no production write path for standalone GPS history.
Concrete blockers found
Recording lifecycle
FloatingBar.canToggleRecording() enables REC only for connected.
Android BoardSessionController.setTelemetryRecordingEnabled(true) rejects missing/idle/connecting/error sessions with Recording requires a connected board.
iOS RecordingCoordinator.setTelemetryRecordingEnabled(true) returns false when activeConfig is absent.
Android and iOS RecordingCoordinator hold active recording state inside Board Session lifetime.
Board stop, fatal error, service stop, and explicit disconnect flush and disable telemetry recording.
Board switching intentionally stops recording before disconnecting/selecting another board.
LiveState.recording.activeBoardId models one active board, not an independent ride activity with an optional board attachment.
Auto-record is triggered by board-ready. Its relationship to manual boardless recording is undefined.
Idle Pause is driven by board telemetry. A GPS-only ride has no equivalent movement/idle contract.
Persistence and history projection
Production recording accepts TelemetryCapture; no standalone GPS/activity-sample write contract exists.
telemetry_frames is board-telemetry-shaped. GPS is an optional field on a frame, not an independent sample stream.
Minute buckets require a deviceId and derive key stats from board metrics.
RideHistoryRepository filters out groups whose avgSpeedSampleCount is zero, so GPS-only buckets would still be invisible.
Ride identity is derived from deviceId + start/end timestamps, not stored as durable activity identity.
Session boundaries are inferred from gaps/device changes/markers. They cannot safely express one manual ride spanning no board → board → disconnect → reconnect or board A → no board → board B.
Route reads project GPS from telemetry frames. Reintroducing a naive standalone GPS table risks the already documented double-trail/marker problems.
History summaries prefer odometer distance, then GPS distance. GPS-only and mixed-source rides need an explicit, non-double-counting distance policy.
Moving time, average speed, top speed, sample count, battery energy, duty, temperatures, faults, chart availability, and sanitizers assume board samples exist.
Favorites, range trimming, deletion, paging cursors, profile stats, media-history time matching, route thumbnails, sync/backup schemas, fixture databases, replay tools, and privacy-zone suppression consume the current board-derived model.
Both Room and iOS SQLite schemas/migrations must remain behaviorally aligned.
GPS ownership and background execution
App-level GPS already exists independently for live map/navigation, but it is not durable Ride History truth.
Android foreground-service types/notification state currently follow the board service lifecycle. A GPS-only recording must keep the legitimate location foreground service alive after BLE ends, without being killed by board teardown.
iOS background location is the legitimate long-running activity. GPS-only recording must survive JS suspension and must obey authorization transitions, app termination, force-quit limitations, and background flush semantics.
Start-before-permission, denied/restricted permission, approximate-only location, revoked permission mid-ride, stale fixes, airplane mode, provider disabled, clock jumps, process death, crash recovery, and low-power throttling need explicit behavior.
Replay sessions must not mix recorded replay GPS with phone GPS or accidentally create new history.
Group Ride and Navigation also consume app-level location. Recording must not duplicate listeners, fight ownership, or alter egress/privacy rules.
Alerts
Alert Rules are Board-owned and only the connected Board's rules are loaded.
Native speed alerts evaluate abs(RefloatTelemetry.speed), not LocationEvent.speedMps.
Legal Mode is also Board-owned and injected into a Board Session alert engine.
A GPS-only speed-alert policy needs an owner: app/rider profile, selected board fallback, or a dedicated riding profile.
Mixed rides must select one authoritative speed source at a time. Evaluating both can double-fire alerts.
Board-specific non-speed rules must remain inactive without matching board telemetry.
Product and UX semantics
REC must be available without a connection, while connection controls remain separate.
UI needs distinct states for recording with GPS only, recording with board telemetry, reconnecting while continuing GPS, paused, GPS unavailable/degraded, and stopping/flushing.
User must understand which stats are unavailable rather than seeing misleading zeros.
Borrowed-board behavior is undefined: attach telemetry to an anonymous/temporary source, selected Board, or GPS-only ride while showing live telemetry.
A board connecting during a GPS-only ride must not silently split or replace the user's manually started ride.
An explicit board disconnect should stop BLE, not necessarily the user-owned ride recording.
Auto-record and manual record need precedence rules to avoid stopping a manual ride when the auto-connected board disappears.
Recommended domain model
Introduce a native-owned Ride Activity as durable recording truth.
A Ride Activity has a stable id, explicit start/stop lifecycle, origin (manual or board-auto), and state independent of Board Session.
GPS samples belong directly to the Ride Activity.
Zero or more Board Telemetry Segments may attach/detach over its lifetime.
Manual STOP ends the activity. Board disconnect ends only its telemetry segment and leaves GPS recording active.
Board auto-record may create an activity only when no activity exists. It may auto-stop only an activity it created; it must never stop a manual activity.
History is projected from the persisted activity identity, not reconstructed solely from bucket adjacency.
Missing board metrics are null/unavailable, never fabricated as zero.
Suggested source policy for a mixed ride:
Route and GPS distance: accepted precise GPS samples.
Displayed speed/top speed/average speed: source-qualified. Prefer trustworthy board speed while a matching telemetry segment is live; use filtered GPS speed outside it. Never integrate both over the same interval.
Speed alerts: one selected source per instant; board speed when valid and connected, GPS speed otherwise, with freshness/accuracy gates and source-switch hysteresis.
This is a recommended direction, not permission to preserve the current telemetry-frame schema at any cost. Architecture should favor explicit activity identity and separate sample streams over synthetic empty telemetry frames.
Required decisions before implementation slices
Borrowed board identity: store a temporary telemetry source, attach to a known selected Board only after explicit confirmation, or keep the activity GPS-only.
Auto-stop policy: whether a board-auto activity continues as GPS-only after disconnect, and for how long without movement.
GPS acceptance: accuracy, freshness, cadence, spike rejection, and approximate-location behavior for history and alerts.
Idle Pause: GPS movement thresholds and timers; whether manual GPS-only recording ever auto-pauses.
Speed authority: exact source-selection and fallback rules for stats and alerts.
Alert ownership: app-wide GPS speed rule vs reusable Riding Profile vs selected-Board rule fallback.
History UX: naming/iconography and unavailable-stat rendering for GPS-only/mixed/borrowed rides.
Recovery: how active activity identity and start provenance survive process/service restoration on Android and iOS.
Privacy Zones: drop GPS samples only, pause the entire activity timeline, or preserve a redacted gap while board telemetry continues.
Sync compatibility: representation of activities, GPS samples, and board segments in backup/server contracts.
Acceptance criteria for the feature
Pressing REC with no selected or connected Board starts a native-owned Ride Activity when required location permission is available.
The activity records accepted GPS fixes, route, GPS distance, duration, moving time, GPS speed summaries, and explicit gaps without fabricating board telemetry.
Recording continues natively while JS is backgrounded/suspended, within Android and iOS platform guarantees.
A Board connecting during an active manual ride attaches a telemetry segment without creating a second history ride or resetting GPS history.
A Board disconnecting/reconnecting closes/reopens telemetry segments while the manual Ride Activity and GPS route continue.
Explicit board disconnect and board switching do not stop a manually started Ride Activity.
Manual STOP durably closes and flushes the activity exactly once.
Auto-record never replaces or stops a manual activity; provenance determines auto-stop behavior.
Process/service restoration does not duplicate, orphan, or merge unrelated activities.
GPS-only and mixed activities appear in Ride History, paging, details, map routes, deletion, favorites, profile stats, and media matching.
History shows unavailable board-derived metrics as unavailable, not zero.
Distance, moving time, average speed, and top speed do not double-count intervals where both board and GPS data exist.
Privacy Zones redact the route according to one documented rule while preserving correct activity boundaries.
GPS speed alerts work without a Board Session, use native background evaluation, and reject stale/inaccurate/spiking fixes.
Mixed rides evaluate speed alerts from exactly one authoritative source at a time and do not double-fire during source changes.
Non-speed Board Alert Rules remain inactive without corresponding board telemetry.
Android foreground notification and iOS background-location behavior accurately describe a GPS-only/mixed active ride.
Location permission denial/revocation, provider loss, approximate-only fixes, force stop/force quit, and low-power throttling degrade explicitly without corrupting history.
Debug replay never mixes live phone GPS into replay data and never records a replay as a new user ride.
Android/iOS native contracts, payloads, schema migrations, recovery behavior, and tests remain in parity and carry required @parity links.
docs/history.md, docs/connectionState.md, docs/alerts.md, docs/native-api.md, and any warranted ADR/domain terms are updated to describe the new ownership model.
Suggested delivery slices
Define Ride Activity contract and ADR — identity, provenance, state machine, segment model, speed/distance policies, recovery.
Persist activities and independent GPS samples — Android+iOS schema/migrations, repositories, privacy/gap handling.
Project GPS-only and mixed history — paging, ranges, summaries, nullable stats, deletion/favorites/profile/media paths.
Each implementation slice should be its own issue with one vertical outcome. Do not land a schema-only half-state that existing history readers can misinterpret.
Test matrix
Manual REC/STOP with no Board, foreground and background.
Start with no permission; grant/deny/revoke during recording.
GPS unavailable at start, acquired later; GPS lost and recovered mid-ride.
Approximate-only and poor-accuracy fixes; implausible GPS speed spike.
Speed alert crossing/re-arm/repeat using GPS only and during board↔GPS source transitions.
History paging, charts, map, favorite, delete, profile stats, media matching, sync/restore.
Existing pre-migration board rides render identically after migration.
Likely files
docs/history.md — current persistence, grouping, route, and standalone-GPS contract.
docs/connectionState.md — native live-state and Board Session recording ownership.
modules/vescape-core/src/index.ts — cross-platform LiveState, history, GPS, and recording API contracts.
modules/vescape-core/android/src/main/java/expo/modules/vescapecore/recording/RecordingCoordinator.kt — Android recording lifecycle currently scoped to Board Session.
modules/vescape-core/ios/recording/RecordingCoordinator.swift — iOS parity owner for recording lifecycle and background flush.
modules/vescape-core/android/src/main/java/expo/modules/vescapecore/telemetry/TelemetryRepository.kt — Android persistence currently accepts telemetry-backed captures.
modules/vescape-core/{android,ios}/telemetry/RideHistoryRepository.* — history grouping/filtering that assumes board samples.
modules/vescape-core/{android,ios}/alerts/AlertEngine.* — speed evaluation currently consumes board telemetry only.
Out of scope
Recording arbitrary non-ride workouts.
Cloud route sharing or public social features.
Deriving missing battery, duty, temperature, current, fault, or odometer values from GPS.
Treating approximate/coarse location as a precise route without a deliberate product decision.
Changing Board Alert semantics for non-speed metrics.
Resolved decisions from domain grill
These decisions are authoritative and override any exploratory alternatives earlier in this PRD.
Ride Recording is the aggregate. Do not introduce a parallel Ride Activity term. A Ride Recording starts manually or through Board Auto, owns a durable identity, records GPS independently, and may contain Board Telemetry Spans.
At most one Board per Ride Recording. A manual GPS-only recording may adopt the first Board that connects. Reconnecting that same Board stays in the same recording. Connecting a different Board creates a separate history ride.
Manual and Board Auto lifecycles differ. Manual REC continues until manual STOP, including across Board loss and intentional Board disconnect. Board Auto recording remains owned by the Board that created it.
Unexpected Board loss uses a two-minute Recording Reconnect Grace for Board Auto. GPS is buffered during the grace. Reconnection of the same Board commits the buffer into the same recording. No reconnection discards the buffer and ends the recording at the last Board telemetry.
Intentional Board disconnect ends Board Auto immediately. It does not end a manually started Ride Recording.
GPS-only supports Idle Pause. After 30 seconds without trustworthy movement, sample persistence pauses while GPS observation remains active. The next trustworthy moving fix resumes recording. Idle Pause never ends a manual recording.
Never synthesize empty VESC telemetry. Persist GPS fixes and GPS speed independently. Missing Board metrics remain unavailable/null and appear as gaps, never zero or interpolated values.
Preserve both speed streams. Board speed and GPS speed are stored independently whenever available. Existing free-spin detection may continue comparing them.
Primary Speed Source is automatic. Board speed is primary whenever trustworthy Board telemetry exists; GPS speed is primary otherwise. No manual source selector.
Source is visible. Board speed uses the existing blue/sky telemetry treatment. GPS speed uses the existing green GPS treatment plus a textual/icon cue; color alone is insufficient.
History comparison belongs in diagrams. Full-screen diagrams include a separate GPS Speed series. Expanded Speed overlays Board Speed (blue) and GPS Speed (green), with honest gaps and no interpolation.
Compact speed stats stay single-source. With Board data they use Board speed. GPS-only rides use GPS speed and identify GPS as the source. Do not merge Board and GPS into one average or top speed.
GPS quality follows platform evidence. Keep the existing recording-grade horizontal accuracy limit of 20 m. Persist platform speed accuracy metadata. A GPS speed is usable only when the OS reports speed and valid speed accuracy. GPS speed alerts require a fresh fix (target 3 s) and bounded speed uncertainty (initial target 1.5 m/s); rejected values become gaps, not zeros. Final numeric thresholds must be parity-tested and may be tuned from device evidence.
GPS speed alerts need app-wide ownership. Add a separate app-wide GPS Speed Alert. It operates for GPS-only recording and as fallback while Board speed is unavailable. Board speed alerts take precedence when Board telemetry is trustworthy. Do not borrow rules from the selected/last Board. Legal Mode remains per-Board.
Updated implementation implications
Introduce durable rideRecordingId and Ride Recording Origin (manual or board-auto).
Persist independent GPS samples keyed by rideRecordingId, including speedMps, horizontal accuracy, speed accuracy, timestamp, and acceptance metadata/reason.
Associate telemetry frames/spans with the same rideRecordingId without fabricating Board fields.
Project history from explicit Ride Recording identity instead of only deviceId, time gaps, and lifecycle markers.
Model live recording state independently from activeBoardId, including origin, assigned Board, Idle Pause, and reconnect-grace state.
Preserve Android/iOS parity for lifecycle, schema, quality gates, background behavior, and restoration.
Additional acceptance criteria
A manual GPS-only recording may adopt the first connected Board without splitting history.
A different Board never shares the same Ride Recording; it produces a separate history ride.
Board Telemetry Spans and their gaps are visible in history without manufacturing zero values.
A Board Auto dropout that reconnects within two minutes commits buffered GPS; one that does not reconnect discards that tail and ends at the final Board sample.
Manual recording survives Board dropout and intentional Board disconnect until STOP.
Full-screen Speed diagrams compare independent Board and GPS series using blue and green source treatments.
Existing pre-change recordings and free-spin behavior remain valid after migration.
App-wide GPS Speed Alert never double-fires alongside an authoritative Board speed alert.
Domain documentation changed during grill
CONTEXT.md now defines the broadened Ride Recording, Ride Recording Origin, Recording Reconnect Grace, Board Telemetry Span, Speed Source, and GPS-capable Idle Pause.
Live recording control
The REC control must become a compact live recording status surface so the rider can immediately verify that recording is active and progressing.
Presentation
Not recording: retain the compact REC affordance.
Recording: show the accumulated Ride Recording distance as the dominant first line in white.
Show elapsed wall-clock time since REC as a smaller second line in recording red.
Elapsed time includes Idle Pause; Moving Time remains a separate derived History statistic.
Do not render a PAUSED label. Idle Pause may use the control icon/tone without displacing distance or elapsed time.
Keep STOP as an obvious action and preserve an adequate touch target while the control expands.
A manual Ride Recording started without a Board shows a small green GPS badge/dot in the control's upper-right corner.
The GPS badge persists after the first Board attaches because it communicates the recording's manual origin and lifecycle: a later Board disconnect will not stop it.
Color cannot be the only source cue; the GPS badge must include a recognizable icon and accessibility label.
Conceptual hierarchy:
┌────────────────┐
│ ■ 7.4 km │ dominant, white
│ 00:42:18│ secondary, recording red
└────────────────┘
Data and update contract
Native Ride Recording truth exposes startedAt, accumulated distanceM, origin, assigned Board, and Idle Pause/reconnect-grace state in LiveState.recording.
The control always displays accumulated GPS distance for the whole Ride Recording so attaching or losing a Board cannot reset or jump the displayed value.
Board odometer distance remains independently recorded for History comparison and summaries; it does not drive this control.
Native integrates only accepted recording-grade GPS samples into distanceM; JS must not independently integrate coordinates.
JS derives elapsed display from native startedAt and a local one-second UI clock. Do not emit a native bridge event merely to advance each displayed second.
Native publishes distance/state changes at a bounded live cadence. UI updates must avoid React render storms and remain smooth while GPS/telemetry events arrive.
During Idle Pause, elapsed time continues and distance stays at its latest value until trustworthy movement resumes.
During Recording Reconnect Grace, the displayed distance may include the buffered GPS tail provisionally. If grace expires without reconnection for Board Auto, the UI and durable recording resolve back to the committed distance at the final Board sample.
Foreground restoration must reconstruct the same distance/time/origin display from native state without resetting to zero.
Additional acceptance criteria
Active recording visibly shows dominant white distance and smaller red elapsed time.
The timer advances once per second without requiring per-second native bridge events.
Distance updates from native-owned accepted GPS samples and survives Board attach, disconnect, reconnect, JS reload, and foreground restoration.
A manual recording started without a Board retains its green GPS origin badge after a Board attaches.
Idle Pause does not replace distance/time with a text label and does not reset either value.
Accessibility announces recording state, distance, elapsed time, GPS-origin badge, and STOP action without relying on color.
The reusable Floating Action control variant is added to the component showcase with live controls for idle, recording, GPS-origin, Idle Pause, and reconnect-grace states.
Additional likely files
src/modules/board/components/FloatingBar.tsx — current REC/STOP state mapping and Board-connected gate.
Problem
Ride Recording is currently a property of an active Board Session. This prevents the rider from recording a route when:
The user intent is broader than the current implementation: pressing REC should start a durable riding activity. Board telemetry should enrich that activity when available, not define whether it exists.
Current architecture
Native owns durable ride truth. JS renders native state and sends intents.
Today the persistence chain is:
RecordingCoordinatorenablesTelemetryRepositoryfor that Board Session.TelemetryCapture.telemetry_frames, minute buckets, and lifecycle markers.RideHistoryRepositoryderives Ride History sessions by grouping minute buckets.There is no durable Activity/Ride Recording identity independent of a board. There is also no production write path for standalone GPS history.
Concrete blockers found
Recording lifecycle
FloatingBar.canToggleRecording()enables REC only forconnected.BoardSessionController.setTelemetryRecordingEnabled(true)rejects missing/idle/connecting/error sessions withRecording requires a connected board.RecordingCoordinator.setTelemetryRecordingEnabled(true)returnsfalsewhenactiveConfigis absent.RecordingCoordinatorhold active recording state inside Board Session lifetime.LiveState.recording.activeBoardIdmodels one active board, not an independent ride activity with an optional board attachment.Persistence and history projection
TelemetryCapture; no standalone GPS/activity-sample write contract exists.telemetry_framesis board-telemetry-shaped. GPS is an optional field on a frame, not an independent sample stream.deviceIdand derive key stats from board metrics.RideHistoryRepositoryfilters out groups whoseavgSpeedSampleCountis zero, so GPS-only buckets would still be invisible.deviceId + start/end timestamps, not stored as durable activity identity.GPS ownership and background execution
Alerts
abs(RefloatTelemetry.speed), notLocationEvent.speedMps.Product and UX semantics
Recommended domain model
Introduce a native-owned Ride Activity as durable recording truth.
manualorboard-auto), and state independent of Board Session.null/unavailable, never fabricated as zero.Suggested source policy for a mixed ride:
This is a recommended direction, not permission to preserve the current telemetry-frame schema at any cost. Architecture should favor explicit activity identity and separate sample streams over synthetic empty telemetry frames.
Required decisions before implementation slices
Acceptance criteria for the feature
@paritylinks.docs/history.md,docs/connectionState.md,docs/alerts.md,docs/native-api.md, and any warranted ADR/domain terms are updated to describe the new ownership model.Suggested delivery slices
Each implementation slice should be its own issue with one vertical outcome. Do not land a schema-only half-state that existing history readers can misinterpret.
Test matrix
Likely files
docs/history.md— current persistence, grouping, route, and standalone-GPS contract.docs/connectionState.md— native live-state and Board Session recording ownership.modules/vescape-core/src/index.ts— cross-platformLiveState, history, GPS, and recording API contracts.modules/vescape-core/android/src/main/java/expo/modules/vescapecore/recording/RecordingCoordinator.kt— Android recording lifecycle currently scoped to Board Session.modules/vescape-core/ios/recording/RecordingCoordinator.swift— iOS parity owner for recording lifecycle and background flush.modules/vescape-core/android/src/main/java/expo/modules/vescapecore/telemetry/TelemetryRepository.kt— Android persistence currently accepts telemetry-backed captures.modules/vescape-core/{android,ios}/telemetry/RideHistoryRepository.*— history grouping/filtering that assumes board samples.modules/vescape-core/{android,ios}/alerts/AlertEngine.*— speed evaluation currently consumes board telemetry only.Out of scope
Resolved decisions from domain grill
These decisions are authoritative and override any exploratory alternatives earlier in this PRD.
Updated implementation implications
rideRecordingIdandRide Recording Origin(manualorboard-auto).rideRecordingId, includingspeedMps, horizontal accuracy, speed accuracy, timestamp, and acceptance metadata/reason.rideRecordingIdwithout fabricating Board fields.deviceId, time gaps, and lifecycle markers.activeBoardId, including origin, assigned Board, Idle Pause, and reconnect-grace state.Additional acceptance criteria
Domain documentation changed during grill
CONTEXT.mdnow defines the broadened Ride Recording, Ride Recording Origin, Recording Reconnect Grace, Board Telemetry Span, Speed Source, and GPS-capable Idle Pause.Live recording control
The REC control must become a compact live recording status surface so the rider can immediately verify that recording is active and progressing.
Presentation
PAUSEDlabel. Idle Pause may use the control icon/tone without displacing distance or elapsed time.Conceptual hierarchy:
Data and update contract
startedAt, accumulateddistanceM, origin, assigned Board, and Idle Pause/reconnect-grace state inLiveState.recording.distanceM; JS must not independently integrate coordinates.startedAtand a local one-second UI clock. Do not emit a native bridge event merely to advance each displayed second.Additional acceptance criteria
Additional likely files
src/modules/board/components/FloatingBar.tsx— current REC/STOP state mapping and Board-connected gate.src/components/controls/FloatingBar.tsx— reusable Floating Action pill presentation.src/modules/board/store/bleStore.ts— JS mirror of native recording state.modules/vescape-core/src/index.ts— cross-platformLiveState.recordingcontract.src/app/settings/components/— required component showcase previews for the new reusable control state.