diff --git a/website/app/account/page.tsx b/website/app/account/page.tsx index c500117..bd29499 100644 --- a/website/app/account/page.tsx +++ b/website/app/account/page.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react'; import Link from 'next/link'; -import { onAuthStateChanged, User } from 'firebase/auth'; +import { onAuthStateChanged, multiFactor, User } from 'firebase/auth'; import { firebaseAuth } from '../../lib/firebase-client'; import { AppCloudData } from '../../components/app-cloud-data'; import { FirebaseLink } from '../../components/firebase-link'; @@ -22,7 +22,10 @@ export default function AccountPage() { const [syncTrigger, setSyncTrigger] = useState(0); const [isSyncing, setIsSyncing] = useState(false); const [lastSynced, setLastSynced] = useState(null); - const [metrics, setMetrics] = useState({ totalMiles: 12480, rideCount: 184, peakLean: 56.4 }); + // Initialise to zero; AppCloudData replaces these with real totals from the rider's + // Firestore rides (or leaves them at zero when signed out / no rides). Never seed + // fabricated stats. + const [metrics, setMetrics] = useState({ totalMiles: 0, rideCount: 0, peakLean: 0 }); // Active Device Sessions State const [sessions, setSessions] = useState([]); @@ -42,6 +45,18 @@ export default function AccountPage() { return () => unsubscribe(); }, []); + // Truthful 2FA state: reflect the user's actual Firebase multi-factor enrollment. + // The web app has no MFA enrollment flow, so this is read-only; enrollment is done in + // the mobile app. Never claim 2FA is enabled unless a factor is actually enrolled. + const twoFactorEnrolled = (() => { + if (!user) return false; + try { + return multiFactor(user).enrolledFactors.length > 0; + } catch { + return false; + } + })(); + async function loadSessions() { setIsLoadingSessions(true); try { @@ -438,14 +453,14 @@ export default function AccountPage() {
App Connections & Sync - - -All Synced + + +{user ? 'Account linked' : 'Not signed in'}
{/* Sync List */}
-{/* Google App Account */} +{/* Google App Account (real: reflects signed-in Firebase user) */}
@@ -460,37 +475,41 @@ export default function AccountPage() { {user ? 'Connected' : 'Not Connected'}
-{/* MotorSignal Mobile App */} +{/* Authenticated devices (real: from /api/account/sessions) */}
-iOS Mobile App -iPhone 16 Pro • v4.1.0 +Authenticated Devices + + {!user ? 'Sign in to view linked devices' : isLoadingSessions ? 'Checking sessions…' : 'Manage in Active Device Sessions below'} +
-14m ago + 0 ? 'text-brand-emerald bg-emerald-500/10 border-emerald-500/20' : 'text-slate-400 bg-slate-800 border-slate-700'}`}> + {!user ? '—' : isLoadingSessions ? '…' : `${sessions.length} active`} +
-{/* Comms / Cardo */} +{/* Helmet comms / accessories — Bluetooth pairing is not observable from a browser */}
-Helmet Comms Link -Cardo Packtalk Edge +Helmet Comms & Accessories +Bluetooth pairing is managed in the mobile app
-Paired +Unavailable on web
Apple Watch Companion: Not Connected (Unavailable on Web) - +Manage Devices
@@ -498,7 +517,7 @@ export default function AccountPage() { {/* BEGIN: SecurityAndSessionsGrid */}
{/* Sessions (7 cols) */} -
+

Active Device Sessions

@@ -600,27 +619,31 @@ export default function AccountPage() {
-{/* 2FA State */} +{/* 2FA State — reflects real Firebase multi-factor enrollment; enrollment is managed in the mobile app */}
Two-Factor Authentication (2FA) -Hardware TOTP Authenticator +{twoFactorEnrolled ? 'Authenticator enrolled' : 'Enroll in the mobile app'}
+{twoFactorEnrolled ? ( ENABLED +) : ( +NOT ENABLED +)}
-{/* Leaderboard Privacy */} +{/* Leaderboard Privacy — managed in the mobile app; the web app has no privacy toggle backend */}
Telemetry Privacy Mode -Public Leaderboard (Club Trophies) +Leaderboard visibility is managed in the mobile app
- +In mobile app
Telemetry Data Retention: 100% Retained -Security Logs +Review active sessions
diff --git a/website/docs/WEBSITE_FEATURE_AUDIT_2026-10-14.md b/website/docs/WEBSITE_FEATURE_AUDIT_2026-10-14.md new file mode 100644 index 0000000..59133f7 --- /dev/null +++ b/website/docs/WEBSITE_FEATURE_AUDIT_2026-10-14.md @@ -0,0 +1,124 @@ +# MotorSignal Website — Feature Audit Matrix + +Date: 2026-10-14 +Branch: `fix/website-production-hardening-20261014` +Baseline: PR #36 integration HEAD `4931920` (`codex/master-consolidation-20261012`) +Scope: `website/` only (Next.js 16.3.4 / React 19). Flutter, Drift, Firebase rules, +Cloudflare Worker, and backend production code are out of scope. + +Method: classifications below are grounded in the current source (file:line evidence), +not in filenames or prior docs. "Verified truthful" means the code was read and does a +real backend operation (or honestly reports unavailability); it does **not** claim any +live device / motorcycle validation. + +Legend — status: ✅ implemented & truthful · 🟠 partial · 🔴 broken / misleading · +🏷️ UI-only placeholder · 🔌 backend-dependent · 🗓️ deferred/roadmap. +Severity: P0 security / data-integrity / misleading success · P1 broken core workflow · +P2 incomplete / usability · P3 roadmap. + +## Summary + +The PR #36 baseline is substantially real. Club governance writes, group-ride +create/edit/delete/join, the garage/ride-history/export surface, device sessions, the +billing-portal fallback, and the Formation Radar are all implemented against real +Firestore/API paths or honestly report unavailability. The genuine defects are a focused +set of **fake success / fabricated-as-real** spots plus the static Support page. + +| # | Defect | File | Severity | Status | +|---|--------|------|----------|--------| +| W1 | Rank "permission matrix" faked a save (setTimeout, no write) | `app/clubs/[clubId]/manage/ranks-roles/page.tsx`, `components/founder-portal/club-permission-matrix.tsx` | P0 | ✅ FIXED (commit c014c2f) | +| W-2FA | Account shows "Two-Factor Authentication ENABLED" hardcoded for every signed-in user | `app/account/page.tsx:603-610` | P0 | 🔴 confirmed | +| W-devices | Account shows hardcoded "iOS Mobile App iPhone 16 Pro v4.1.0 · 14m ago" and "Cardo Packtalk Edge · Paired" as real connected devices | `app/account/page.tsx:463-488` | P1 | 🔴 confirmed | +| W-routes | Club routes: empty Firestore collection falls back to `FIXTURE_CLUB_ROUTES` (fake "Coastal Marshes" route + `/fixtures/routes/*.gpx`) in production | `lib/club-community.ts:265-268` | P1 | 🔴 confirmed | +| W-chat | Club chat: empty channel falls back to `FIXTURE_CHAT_MESSAGES` in production | `lib/club-community.ts:443-447` | P1 | 🔴 confirmed | +| W-metrics | Account dashboard `metrics` initialises to fabricated `12480 mi / 184 rides / 56.4°` before real data loads (persists if load fails) | `app/account/page.tsx:25` | P2 | 🟠 confirmed | +| W2 | Support Center is a static hero + `mailto:` only — no FAQ, no validation, no ticketing | `app/support/page.tsx` | P2 | 🔴 confirmed | +| W-deadctl | Dead/no-op controls: "Change" (telemetry privacy), "Manage Devices", "Security Logs"→`/account`, static "All Synced" | `app/account/page.tsx` | P2/P3 | 🟠 confirmed | + +## Feature matrix (by area) + +### Public site & navigation +- Homepage `app/page.tsx` + `components/landing/*` — ✅ marketing content. + - 🟠 `components/landing/TelemetryStrip.tsx:10-11` and `app/features/page.tsx:15-20` use + `Math.random` for animated counters. Decorative marketing, not presented as a user's + real telemetry. P3 — acceptable but worth a "demo" label. +- Navigation / responsive mobile nav `app/layout.tsx` — ⏳ not yet width-tested (Phase 8). + +### Authentication & account +- Login / register / forgot-password / verify-email — ✅ real (`components/auth-form.tsx`, + `api/auth/[action]`); covered by `tests/auth-form.test.tsx`, `auth-recovery-verification`. +- Account settings `app/account/page.tsx` — ✅ mostly real, with the W-2FA / W-devices / + W-metrics / W-deadctl defects above. +- Device sessions `app/account/page.tsx` + `api/account/sessions` — ✅ real list, revoke & + revoke-all, pagination, truthful empty/loading states. +- Subscription & billing — ✅ truthful: `/api/billing/portal` is unconfigured and the page + says so ("managed through the mobile app"); no fake success (`app/account/page.tsx:107-141`). + +### Garage & motorcycle management +- `components/app-cloud-data.tsx` — ✅ real Firestore: add / edit / delete / make-primary / + photo upload (`users/{uid}/motorcycles`), image via `/api/motorcycle-image`. + +### Ride history, telemetry sessions & export +- `components/app-cloud-data.tsx` — ✅ real rides from `users/{uid}/rides`, GPX & CSV export + handlers, delete, "Show more" pagination. CSV/GPX byte-integrity not yet re-verified (Phase 4/9). +- Telemetry Forensic Replay `components/web-telemetry-replay-modal.tsx` — ⏳ not yet fully + audited (map tiles, progress, playback smoothness — Phase 4). Tests exist + (`tests/telemetry-replay-export.test.tsx`, `replay-recorded-data.test.tsx`). + +### Clubs / Founder Portal +- Profile / access-settings / ownership-transfer / archive — ✅ real writes with owner/admin + authorization (`lib/club-data.ts`; demo branch uses labelled simulated latency only). +- Applications approve/decline — ✅ real transactional writes. +- Member rank assignment — ✅ real (`updateMemberRank`). +- Rank permission matrix — ✅ FIXED (W1): now a truthful read-only reference. +- Club routes — 🔴 W-routes fixture fallback. +- Club chat / wall — 🔴 W-chat fixture fallback (wall send path not yet fully audited). + +### Group Rides +- Public page `app/group-rides/page.tsx` — ✅ splits by auth: signed-out → + `GroupRidesShowcase` (clearly labelled "Sample Pack Formations" / "Radar Simulation"), + signed-in → `GroupRidesManagement`. Public path exposes no real private rides. +- Authenticated Hub `components/group-rides/group-rides-management.tsx` — ✅ real: + `/api/group-rides` discovery + Firestore `onSnapshot`; create/edit/delete/join/leave write + to `group_rides`. 🟠 join/ride identifier generated client-side with `Math.random` + (`:1008`) — weak/non-authoritative; 🟠 `INITIAL_GROUP_RIDES` seeds initial state (replaced + on first snapshot; can linger if no Firestore). P2. +- Address autocomplete / route distance / scheduling / join codes / QR — ⏳ not yet fully + audited (Phase 3). + +### Live Formation Radar +- `components/group-rides/live-pack-radar.tsx` — ✅ truthful. Real rides subscribe to + Firestore `live_presence` with authorization + loading + error states and **no** fixture + fallback; `INITIAL_RADAR_RIDERS` is used only for the showcase/demo and is explicitly + labelled "Public Demonstration (Simulated Pack)" with "[Simulation Demo]" ping copy. The + historical dummy-data concern is already remediated in this baseline. + +### Device connections / hardware +- 🔴 W-devices: account "App Connections" block hardcodes a connected iOS app and a paired + Cardo unit. Apple Watch row is truthful ("Not Connected (Unavailable on Web)"). +- Helmet Comms / GoPro / Apple Watch as first-class features — 🗓️ roadmap; must be labelled. + +### Support Center +- 🔴 W2: `app/support/page.tsx` is one static hero + `mailto:`. No public ticket backend + exists (the `api/ops/support/tickets/*` routes are the internal operations console, not a + public endpoint). Phase 6 remediation: keep email fallback, add grounded FAQ + validated + contact UX; do not simulate ticket submission. + +### Operations console (`app/api/ops/*`, `components/ops/*`, `app/admin`) +- Out of the brief's feature list; appears to be a separate internal console. Not modified. + +## Remaining P0–P3 work (website) + +- **P0** — W-2FA (false security state). Fix: show real MFA state / "managed in mobile app", + never a hardcoded "ENABLED". +- **P1** — W-devices, W-routes, W-chat (fabricated-as-real). Fix: real state or truthful + empty/unavailable; never fixtures in production paths. +- **P2** — W2 Support (FAQ + validation, email fallback), W-metrics (init to loading/0), + W-deadctl (wire or remove dead controls), group-ride client-side id generation, mobile + responsiveness pass (Phase 8), telemetry replay deep audit (Phase 4). +- **P3** — marketing `Math.random` counters (label as demo), hardware roadmap labelling. + +## Open product questions (do not block P0/P1 truthfulness fixes) +1. Support: confirm no public ticket backend is planned for this release (default: email + FAQ). +2. Device-connections block: remove, or convert to a truthful "manage in mobile app" state? +3. Club routes/chat empty states: plain empty state, or keep clearly-labelled "example" content? diff --git a/website/tests/account-truthful-states.test.tsx b/website/tests/account-truthful-states.test.tsx new file mode 100644 index 0000000..cc47479 --- /dev/null +++ b/website/tests/account-truthful-states.test.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { describe, it, expect, vi } from 'vitest'; +import { render, screen, waitFor } from '@testing-library/react'; +import AccountPage from '../app/account/page'; + +// Account page only needs next/navigation stubbed; firebase-client initialises under the +// staging test env and reports a signed-out user (onAuthStateChanged -> null). +vi.mock('next/navigation', () => ({ + usePathname: () => '/account', + useRouter: () => ({ push: vi.fn(), replace: vi.fn(), back: vi.fn() }), +})); + +describe('WEB-W1b/W-2FA/W-devices: account page truthful states', () => { + it('does not fabricate a connected mobile device, paired comms, or sync status', async () => { + render(); + + await waitFor(() => { + expect(screen.getByText('App Connections & Sync')).toBeDefined(); + }); + + // Fabricated device/pairing claims must be gone. + expect(screen.queryByText(/iPhone 16 Pro/)).toBeNull(); + expect(screen.queryByText(/Cardo Packtalk Edge/)).toBeNull(); + expect(screen.queryByText(/^Paired$/)).toBeNull(); + expect(screen.queryByText(/^14m ago$/)).toBeNull(); + expect(screen.queryByText(/^All Synced$/)).toBeNull(); + + // Truthful states are present instead. + expect(screen.getByText('Authenticated Devices')).toBeDefined(); + expect(screen.getByText('Helmet Comms & Accessories')).toBeDefined(); + expect(screen.getAllByText(/Unavailable on web/i).length).toBeGreaterThan(0); + }); + + it('never shows 2FA as ENABLED unless a factor is actually enrolled (signed-out = NOT ENABLED)', async () => { + render(); + + await waitFor(() => { + expect(screen.getByText('Two-Factor Authentication (2FA)')).toBeDefined(); + }); + + // Exact-match: the badge node reads "NOT ENABLED" for a user with no enrolled factor, + // so an exact "ENABLED" node must not exist. + expect(screen.getByText('NOT ENABLED')).toBeDefined(); + expect(screen.queryByText('ENABLED')).toBeNull(); + }); +});