Reconciles PR #37 with master after #36 and #39. Worker changes are now
identical to master; only the Dart sync recovery and tests remain.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- Handle loading, unauthenticated, permission-denied, and unavailable states truthfully in metric-card-lean (Peak Lean Angle KPI) rather than falling back to '--' and 'No IMU roll data'
- Reset metrics state to null on auth change and in onStatusChange loading/error branches to prevent stale metrics retention
- Add race condition guard in AppCloudData.loadData to discard in-flight results if firebaseAuth.currentUser changed during asynchronous fetch
- Prioritize Disconnected and Connecting states over sample example messages in real club mode in founder-chat.tsx
- Clear chat messages on channel switch in founder-chat.tsx to eliminate cross-channel message bleed, and fix invalid py-0.2 Tailwind classes to py-0.5
- Add regression tests in account-metrics-truthfulness.test.tsx and founder-routes-chat.test.tsx
- Replace false zero metrics (0 miles / 0 rides / 100% Cloud Synced) on loading, unauthenticated, permission-denied, or network errors with truthful states
- Update AppCloudData to query motorcycles and rides via Promise.allSettled and report granular CloudDataStatus
- Update AccountPage KPI cards to display Loading…, —, or Unavailable with explicit contextual status and error recovery
- Fix founder demo chat to permanently preserve Example thread and example badges during preview=demo simulation instead of flipping to Live Connected
- Add 8 unit/component tests in account-metrics-truthfulness.test.tsx and 6 tests in founder-routes-chat.test.tsx
- Guard advancePlaybackIndex against spuriously advancing when budgetMs <= 0 on duplicate timestamps
- Use monotonic millisecond timestamp in commitGroupRide to eliminate timestamp corruption and collision hazards
- Preserve CreateGroupRideModal form inputs on failed commit so users do not lose typed data
- Add regression tests in vitest suites for zero-budget duplicate timestamps, timestamp fidelity, and input preservation
- Add replay regression test suite covering irregular/jittered timestamps, identical/zero dt, inverted timestamps, large gaps, pause/resume timing reconciliation, manual seek, speed changes, and large datasets (144k points).
- Harden advancePlaybackIndex against non-finite/NaN budget and timestamp delta anomalies.
- Reset lastTimeRef and cap frameDelta in WebTelemetryReplayModal to eliminate sudden jumps on speed switching or background tab resumes.
- Guard group-ride creation with in-flight isSubmitting state and disabled submit button in CreateGroupRideModal; guarantee unique collision-free ride IDs across concurrent submissions.
- Remove obsolete Vercel reference in website/.env.example in favor of Vultr VPS and Caddy; add npm test script.
Phase 1 — group ride creation (no phantom rides):
- Check authentication and Firestore availability BEFORE touching UI state; an unauthenticated
user (or unavailable Firestore) now gets an actionable error and no phantom ride, instead of a
local entry that was never saved.
- Insert the ride into UI state ONLY after batch.commit() confirms the write; on commit failure
nothing is inserted and an error is shown.
- Prevent duplicate submissions: the create modal disables its submit button while in flight, and
the hub guards with a ref.
- The join code remains the real Firestore document id (no fabricated ids).
- Extract commitGroupRide (pure, dependency-injected) so every path is unit-testable.
Phase 2 — replay playback timing:
- The elapsed clock and rider marker both derive from the same cursor, so they cannot drift.
- Manual seeks (scrub, restart, corner jump, arrow keys) go through seekTo(), which reconciles
playback timing (clears carry/last-frame) so no stale budget is applied after a seek.
- Make gap compression explicit: a caption labels the timeline as recorded elapsed time and warns
that dead time in multi-segment recordings is skipped during playback.
Tests: commitGroupRide (unauthenticated / unavailable / commit-ok / commit-failed); group-rides
hub now uses an authenticated mock with deterministic Firebase — authenticated create persists,
unauthenticated create is blocked with no phantom, a failed commit leaves no phantom, and the
create modal blocks duplicate submits.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Creating a ride from the hub had two integrity problems:
- The "Created … success" toast fired optimistically before the Firestore write, and a
commit failure was only console.error'd — so a user could be told the ride was created
when nothing persisted (the ride would then vanish on the next snapshot).
- Every created ride wrote a hardcoded meetingPoint at 34.5378, -83.9849 (Helen, GA)
regardless of the typed location — fabricated GPS attached to real rides.
Fixes:
- Report success only after `batch.commit()` resolves; on failure, roll back the optimistic
insert and show an error. When there is no authenticated session / Firestore, keep the
local entry for responsiveness but make no cloud-success claim.
- Extract buildWebGroupRidePayload (pure): stores the rally point as displayName/address
text and omits latitude/longitude (the web form has no map picker / geocoder, so we never
fabricate coordinates). mapFirestoreRideToSummary already treats a missing coordinate as
"not pinned yet".
Join-by-code, join/leave, edit, and delete flows were reviewed and persist/authorize
correctly; no changes needed there.
Tests: buildWebGroupRidePayload asserts no fabricated lat/lon, rally-point text/fallback,
authoritative organizer/access fields, scheduled-time handling, and club identity. Existing
group-ride UI tests remain green.
Known limitation: true address autocomplete and automatic route-distance calculation require
a geocoding/routing API that does not exist in this codebase; distance and rally point remain
free-text until that backend lands.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The replay loop advanced the cursor by one sample every 50 ms / speed, assuming a uniform
20 Hz stream. Recorded telemetry is not always uniform (segment breaks and GPS-loss gaps up
to 5 s), so the rider marker drifted out of sync with the elapsed-time readout — which is
derived from real timestamps — and "1x" did not match real time across gaps.
Make playback timestamp-driven:
- Add advancePlaybackIndex(points, fromIndex, budgetMs): a pure helper that consumes a
budget of virtual recorded time, advancing by the real delta between samples so the
marker tracks the elapsed-time clock. Disconnected transitions (per
areTelemetryPointsConnected) are compressed to a single 50 ms step so playback never
stalls across multi-second dead time. No coordinates are altered.
- Drive the rAF loop with frameDelta * playbackSpeed plus carry-over, resuming from an
index ref so manual scrubs are honored.
Verified sound without changes: SHA-256-checked R2/inline telemetry load, CSV/GPX parse and
export integrity, loading/error/empty overlays, and tile-provider fallback.
Tests: advancePlaybackIndex covers uniform cadence, carry-over, gap compression, end-clamp,
and empty input. Existing replay/export/corner tests remain green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Documents the blocker: a richer connected-devices panel (platform, app version, last-seen,
accessory pairing) needs a FastAPI endpoint that does not exist yet. Records the proposed
GET /api/v1/account/connections contract and the website wiring to adopt it, and notes what
already shipped truthfully without a backend change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Support page was a single static line plus a mailto link. There is no public
ticket backend (only the internal ops console), so rather than simulate ticket
submission, improve the page truthfully:
- Add an FAQ grounded in features that actually ship (offline recording/sync,
billing + 2FA managed in the mobile app, GPX/CSV export, session revocation,
radar live-presence rules, public vs authenticated group rides).
- Add a validated contact form (name/email/topic/message) that opens a prefilled
email in the visitor's own client. It never claims a ticket was filed and states
plainly that the message is not sent until the visitor sends it.
- Keep support@motorsignal.app as the real, working fallback channel.
- Preserve the existing public-page visual system (content-page / eyebrow / faq-section).
Tests: FAQ + real mailto render; invalid input shows validation errors and no
submission claim; valid input opens a mailto (no simulated ticket) with the topic
and email encoded.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When a club has no curated routes or a chat channel has no messages, the data layer
returned fixture content with no indication it was sample data, so riders could mistake
MotorSignal's "Coastal Marshes" route or canned chat messages for their club's real data.
Flag fixture-sourced content and label it in the UI:
- Add isExample to ClubRoute and ChatMessage; set it on every fixture fallback path
(empty Firestore collection/channel and the dev demo preview).
- Founder Routes: amber "Examples" count, an explanatory banner, and a per-card "Example"
badge when the routes shown are samples.
- Founder Chat: an "Example thread" pill (instead of "Live Connected") and a banner when
the only messages are examples, so the header never claims a live conversation that
does not exist.
Tests: example-labelled routes render the banner/badge/count; example-labelled chat
renders the banner and "Example thread" pill and does not claim "Live Connected".
Existing route/chat tests (real data, no isExample) remain green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The account page asserted several things that were not backed by real state:
- Two-Factor Authentication was hardcoded to "ENABLED" for every signed-in user.
Now derives from the real Firebase multiFactor enrollment and shows "NOT ENABLED"
(enroll in the mobile app) unless a factor is actually enrolled.
- "App Connections & Sync" showed a hardcoded connected iOS app (iPhone 16 Pro, v4.1.0,
"14m ago") and a paired "Cardo Packtalk Edge". Replaced with truthful states: real
Google account link, authenticated-device count from the real /api/account/sessions
data, and an honest "Unavailable on web" for Bluetooth comms/accessories (a browser
cannot observe hardware pairing). Header status reflects real sign-in state.
- Dashboard metrics initialised to fabricated totals (12,480 mi / 184 rides / 56.4 deg).
Now initialise to zero; AppCloudData replaces them with the rider's real ride totals.
- Dead controls fixed: "Manage Devices" and "Review active sessions" now anchor the real
Active Device Sessions panel; the telemetry-privacy control no longer presents an
unverified setting with a no-op "Change" button (managed in the mobile app).
Also adds the Phase 1 website feature audit matrix (docs/).
Tests: account-truthful-states asserts no fabricated device/pairing/sync claims render
and that 2FA is never shown as ENABLED without a real enrolled factor.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Ranks & Roles "Operational Permission Matrix" previously waited ~400ms and
showed a "Saved" state without persisting anything. No Firestore document, API
route, or security rule stores or enforces a per-rank capability matrix; club
authorization is role-based (Owner/Admin via getPortalPermissions). Persisting
the matrix would require frozen security-rule changes and would create an
unenforced permission model that misleads owners.
Make the control truthful instead of faking success:
- Render the matrix as a read-only reference of the standard governance model
by default (persistenceAvailable=false): no Save/Reset controls, disabled
toggles, a "View-only reference" indicator, and a notice stating that access
is enforced by Owner/Admin roles and per-rank customization is planned.
- Remove the fake setTimeout save handler and the unconditional success state.
- Retain a forward-compatible editable path behind persistenceAvailable for
when a backend contract ships.
- Document the precise backend contract required to enable persistence.
Tests: read-only matrix renders no Save control, reports no save, and does not
mutate permissions even for Owner/Admin; editable path still fires toggle/save
/reset when persistenceAvailable=true.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Enforce positive bundle size, matching SHA-256, user-scoped object keys, and legacyUnverified status for historical bundles in R2UploadService and FirebaseSyncProvider
- Update SyncManager reconciliation to accept legacyUnverified status, mark historical rides as synced without re-upload, and track legacy unverified counts
- Add 18-test vitest suite for Cloudflare Worker /r2/verify endpoint covering CORS, auth, validation, ownership, HEAD/GET, legacy bundles, and errors
- Harden test/disposable_backup_recovery_verification_test.dart with candidate path discovery and CI skip guard
- Fixed Zyla request accounting to record each actual upstream HTTP attempt exactly once, eliminating double-counting when body parsing, JSON decoding, or normalization fails
- Fix RenderFlex overflow at short landscape breakpoint (640x320) by scaling center controls in ResponsiveDashboardLayout and clamping LeanAngleIndicator bounds