From 456ad4b44428a1cc7c7524294f1e503d22d547fe Mon Sep 17 00:00:00 2001 From: Ride Control Date: Thu, 23 Jul 2026 14:42:04 -0700 Subject: [PATCH 1/2] Add footer legal and version details --- .github/workflows/ci.yml | 7 + README.md | 1 + src/app.tsx | 21 +- src/build-env.d.ts | 1 + src/components/app-footer.tsx | 62 +++-- src/components/build-details-dialog.tsx | 154 +++++++++++++ src/components/legal-dialog.tsx | 294 ++++++++++++++++++++++++ src/lib/app-overlay.ts | 3 + src/lib/build-info.ts | 68 ++++++ tests/build-info.test.ts | 37 ++- tests/components.test.tsx | 85 ++++++- vite.config.ts | 83 ++++++- 12 files changed, 779 insertions(+), 37 deletions(-) create mode 100644 src/components/build-details-dialog.tsx create mode 100644 src/components/legal-dialog.tsx diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 11b17dd..72fbaa1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,6 +61,13 @@ jobs: build_pr_url="https://github.com/${GITHUB_REPOSITORY}/pulls?q=is%3Apr+is%3Aclosed" fi echo "VITE_BUILD_PR_URL=${build_pr_url}" >> "$GITHUB_ENV" + { + echo "VITE_BUILD_RECENT_PRS<> "$GITHUB_ENV" - name: Build Worker run: bun run build - name: Deploy Worker diff --git a/README.md b/README.md index 6cdcb19..6de6928 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ Bike trainer control web app using Web Bluetooth. Tested with Wahoo KICKR Core 2 - Automatically records while pedaling, auto-pauses during inactivity, supports manual pause and resume, and allows a session to end at any time—even before trainer data arrives. Finishing a ride smoothly returns a connected trainer to 10% resistance; if it is disconnected, 10% is remembered and applied when it reconnects. - Tracks every time-series sample plus averages and maximums for power, cadence, heart rate, speed, resistance, and virtual gear, with no duration-based truncation during recording or FIT/TCX import. Large, high-visibility numbers appear in space-efficient live metric and ride-summary cards, with oversized ride totals and subdued unit labels. Focused or combined charts use a responsive display-only sample of long histories without changing the complete data retained for summaries and exports. The resistance chart starts at a useful 50% scale and expands in ten-point steps as samples approach its ceiling. Workout grade and elevation are graphed in their own distinct colors, resistance remains visible alongside gear during virtual shifting, and the gear graph stays hidden outside gear mode unless the session contains recorded gear data. Workout elevation is recorded across the entire ride, so the course profile repeats for every completed loop. Saved sessions reference immutable, content-addressed workout snapshots in a separate IndexedDB store: identical course definitions share one snapshot, edited definitions retain their historical versions, and deleting a workout from the selectable library cannot break an older session's maps or terrain details. - Keeps the complete dashboard usable at phone widths: ride totals reflow when necessary, chart controls and plots shrink within the viewport, virtual shifting uses the available width, and the footer remains below the controls with device safe-area spacing. +- Provides clear footer access to email contact, the privacy policy, terms of service, and the current deployment version in responsive in-app dialogs. Production version details include links and merge dates for the ten most recent frontend pull requests. - Lets riders explicitly save a completed session or end it without saving, while keeping start-new and continue-session choices to two clear, context-aware actions. Saved and in-progress sessions use browser-managed IndexedDB storage, and active rides are checkpointed in small sample chunks so recovery does not repeatedly rewrite the complete history. Existing localStorage recovery data is migrated once and removed only after IndexedDB has accepted it. Saved sessions support optional comments and ride feeling, and persistent browser storage is requested when supported. - Opens saved rides from the dashboard's Sessions button and organizes them by local date and time in a slide-out Sessions tray with a compact inline session count, clear date ranges for rides that span midnight, paginated loading, detailed metrics and charts, keyboard navigation with grouped shortcut help, and permanent deletion. The tray restores the selected session, the session-list scroll position, and each session's independent detail-pane scroll position after a page reload, falling back to the newest available session when a remembered ride no longer exists. - Downloads saved rides as standards-compliant FIT activities for direct upload to Strava and other fitness services, including indoor-cycling and creator metadata, UTC and local timestamps, distance, speed, power, cadence, estimated crank revolutions and work, heart rate, resistance, elevation, calories, and ride totals. Each FIT filename includes a stable session token for reliable upload identity. TCX export remains available for the richer Ride Control round trip, including virtual gear, terrain workout metadata, ride feeling, comments, and the original session identifier. diff --git a/src/app.tsx b/src/app.tsx index b90848a..cb7b9e2 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -2,10 +2,12 @@ import { useNavigate, useRouterState } from '@tanstack/react-router'; import { useSelector } from '@tanstack/react-store'; import { useCallback, useEffect, useRef, useState } from 'react'; import { AppFooter } from './components/app-footer'; +import { BuildDetailsDialog } from './components/build-details-dialog'; import { Dashboard, DashboardToolbar, DashboardWorkspace } from './components/dashboard-layout'; import { DashboardTools } from './components/dashboard-tools'; import { DevicePairingPanel } from './components/device-pairing'; import { KeyboardShortcutsDialog } from './components/keyboard-shortcuts-dialog'; +import { PrivacyPolicyDialog, TermsOfServiceDialog } from './components/legal-dialog'; import { Notification } from './components/notification'; import { RideMetrics } from './components/ride-metrics'; import { SessionControls } from './components/session-controls'; @@ -575,7 +577,12 @@ export function App({ initialSession = emptySession }: { initialSession?: Stored /> - setActiveOverlay(APP_OVERLAY.WELCOME)} /> + setActiveOverlay(APP_OVERLAY.PRIVACY)} + onOpenTerms={() => setActiveOverlay(APP_OVERLAY.TERMS)} + onOpenVersion={() => setActiveOverlay(APP_OVERLAY.BUILD)} + onOpenWelcome={() => setActiveOverlay(APP_OVERLAY.WELCOME)} + /> + setActiveOverlay(undefined)} + open={activeOverlay === APP_OVERLAY.BUILD} + /> + setActiveOverlay(undefined)} + open={activeOverlay === APP_OVERLAY.PRIVACY} + /> + setActiveOverlay(undefined)} + open={activeOverlay === APP_OVERLAY.TERMS} + /> ); diff --git a/src/build-env.d.ts b/src/build-env.d.ts index 6143f68..6294411 100644 --- a/src/build-env.d.ts +++ b/src/build-env.d.ts @@ -1,5 +1,6 @@ interface ImportMetaEnv { readonly RIDE_CONTROL_BUILD_PR_URL: string; + readonly RIDE_CONTROL_BUILD_RECENT_PRS: string; readonly RIDE_CONTROL_BUILD_TIMESTAMP_UTC: string; readonly VITE_RIDECONTROL_API_URL?: string; } diff --git a/src/components/app-footer.tsx b/src/components/app-footer.tsx index a2ee94a..15cb135 100644 --- a/src/components/app-footer.tsx +++ b/src/components/app-footer.tsx @@ -1,10 +1,23 @@ -import { BUILD_PR_URL, BUILD_TIMESTAMP_UTC, formatBuildTimestamp } from '../lib/build-info'; +import { BUILD_TIMESTAMP_UTC, formatBuildTimestamp } from '../lib/build-info'; -export function AppFooter({ onOpenWelcome }: { onOpenWelcome: () => void }) { +const linkClass = + 'rounded-sm transition hover:text-slate-200 focus-visible:outline-2 focus-visible:outline-mint focus-visible:outline-offset-2'; + +export function AppFooter({ + onOpenPrivacy, + onOpenTerms, + onOpenVersion, + onOpenWelcome, +}: { + onOpenPrivacy: () => void; + onOpenTerms: () => void; + onOpenVersion: () => void; + onOpenWelcome: () => void; +}) { return ( -