diff --git a/.github/workflows/deploy-beta-proxy.yml b/.github/workflows/deploy-beta-proxy.yml new file mode 100644 index 00000000..be2b74a3 --- /dev/null +++ b/.github/workflows/deploy-beta-proxy.yml @@ -0,0 +1,66 @@ +name: Deploy beta-proxy + +# beta-proxy/ is the reverse-proxy Worker that serves beta.lapwingdata.com (it +# forwards to the lapwing `beta` branch preview). It is a SEPARATE Worker from +# the main app — Cloudflare's connected-repo CI builds the app but nothing +# deploys this proxy. This workflow does: +# • validate it on PRs that touch it (dry-run, no secrets — runs on forks too) +# • deploy it on BETA pushes that change it, or on demand (workflow_dispatch) +# +# Required repo secrets for the deploy job: +# • CLOUDFLARE_API_TOKEN — token with "Edit Cloudflare Workers" on the account +# • CLOUDFLARE_ACCOUNT_ID — the account that owns the lapwingdata.com zone + +on: + push: + branches: [BETA] + paths: + - "beta-proxy/**" + - ".github/workflows/deploy-beta-proxy.yml" + pull_request: + paths: + - "beta-proxy/**" + - ".github/workflows/deploy-beta-proxy.yml" + workflow_dispatch: + +permissions: + contents: read + +# Pin the wrangler version used to both validate and deploy so runs are +# reproducible (beta-proxy has no committed lockfile). +env: + WRANGLER_VERSION: "4.103.0" + +# Never let two deploys of the same ref overlap; queue rather than cancel so an +# in-flight deploy always finishes. +concurrency: + group: deploy-beta-proxy-${{ github.ref }} + cancel-in-progress: false + +jobs: + # Bundle + config check for PRs. Needs no Cloudflare secrets, so it catches a + # broken wrangler.toml before it can reach BETA (and works for fork PRs). + validate: + if: github.event_name == 'pull_request' + name: Dry-run + runs-on: ubuntu-latest + defaults: + run: + working-directory: beta-proxy + steps: + - uses: actions/checkout@v4 + - run: npx --yes wrangler@${{ env.WRANGLER_VERSION }} deploy --dry-run --config ./wrangler.toml + + deploy: + if: github.event_name != 'pull_request' + name: Deploy Worker + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + workingDirectory: beta-proxy + wranglerVersion: ${{ env.WRANGLER_VERSION }} + command: deploy --config ./wrangler.toml diff --git a/.lovable/plan.md b/.lovable/plan.md deleted file mode 100644 index f30e9618..00000000 --- a/.lovable/plan.md +++ /dev/null @@ -1,78 +0,0 @@ - -## Goal - -Let any user (not just admins) sign up, sign in (email or Google), and reset their password so they can use Cloud Sync and future user features — **but keep the entire cloud/auth surface behind a single build-time flag** so the offline-first repo can ship with zero cloud code paths active. Admin status stays a separate concept driven by `user_roles`. - -## Build-time gating model - -One new flag controls all user-facing cloud auth: **`VITE_ENABLE_CLOUD`** (default `"false"`). - -| Flag | Controls | -|------|----------| -| `VITE_ENABLE_CLOUD` | Public auth routes (`/login`, `/register`, `/forgot-password`, `/reset-password`, `/auth/callback`), header "Sign in" entry, Cloud Sync Labs panel registration, Google OAuth button | -| `VITE_ENABLE_ADMIN` | `/admin` route + admin UI (unchanged) | -| `VITE_ENABLE_REGISTRATION` | **Retired** — registration follows `VITE_ENABLE_CLOUD` | - -When `VITE_ENABLE_CLOUD !== 'true'`: -- None of the new auth pages are imported (lazy boundaries + conditional `` mounting, same pattern as `/admin` today). -- The cloud-sync plugin's `index.ts` early-returns from `setup()` so it never contributes a panel — Labs tab stays absent unless something else contributes. -- The header "Sign in" affordance is not rendered. -- `AuthContext` still mounts (admin build needs it), but the new `signUp` / `signInWithGoogle` methods are no-ops behind the same flag check — or, cleaner, the Google-specific lovable client import stays inside the lazy page modules so it never lands in the main chunk. - -Admin builds independently set `VITE_ENABLE_ADMIN=true`; they continue to work whether cloud is on or off (admin login uses the existing `supabase.auth.signInWithPassword` path). - -## Scope - -1. **Routing (`src/App.tsx`)** - - Add `const enableCloud = import.meta.env.VITE_ENABLE_CLOUD === 'true';` - - Mount the new public auth routes only when `enableCloud`. `/login` is mounted when `enableCloud || enableAdmin` (admin still needs it). Drop the `VITE_ENABLE_REGISTRATION` check. - - All new pages lazy-loaded so the disabled build never downloads them. - -2. **New pages (all lazy)** - - `src/pages/ForgotPassword.tsx` — email → `supabase.auth.resetPasswordForEmail(email, { redirectTo: origin + '/reset-password' })`. - - `src/pages/ResetPassword.tsx` — public route, detects `type=recovery` hash, calls `supabase.auth.updateUser({ password })`, routes to `/`. - - `src/pages/AuthCallback.tsx` at `/auth/callback` — waits for `onAuthStateChange`, then redirects to `?next=` or `/`. - -3. **Rework `Login.tsx` / `Register.tsx`** - - Reframe copy from "Admin Login" → "Sign in". Add a "Continue with Google" button at the top (rendered only when `enableCloud`). - - Forgot-password becomes a link to `/forgot-password` instead of an inline toggle. - - Keep the per-IP rate-limit edge function. Success redirects to `?next=` or `/` (admins land on `/admin` via the next param, set by header link). - - Registration always-on under cloud flag; keep email-confirm flow (no auto-confirm). - -4. **Google sign-in via Lovable Cloud managed OAuth** - - Run `supabase--configure_social_auth` with `providers: ["google"]` (keep email). - - Use `lovable.auth.signInWithOAuth("google", { redirect_uri: origin + "/auth/callback" })` from the scaffolded `src/integrations/lovable/`. Import only from the cloud-flagged pages so it tree-shakes out otherwise. - - PWA: add `/^\/~oauth/` to `navigateFallbackDenylist` in `vite.config.ts` so OAuth redirects bypass the service worker. - -5. **`AuthContext` additions** - - Add `signUp(email, password)` and `signInWithGoogle()` wrappers next to existing `login` / `resetPassword`. Admin role detection via `has_role` stays unchanged — regular users have no `user_roles` row, so `isAdmin` is `false`. - -6. **Cloud Sync plugin (`src/plugins/cloud-sync/index.ts`)** - - In `setup()`, early-return when `VITE_ENABLE_CLOUD !== 'true'` so the Labs panel isn't contributed. `CloudSyncPanel.tsx` stays lazy and never imports. - - When enabled, replace the inline email/password form with: blurb + "Sign in" / "Create account" buttons routing to `/login?next=/` and `/register`, plus a "Continue with Google" shortcut using the new context method. Smaller panel, no duplicated auth UI. - -7. **Header / nav affordance (`LandingPage.tsx` or top of `Index.tsx`)** - - Render a "Sign in" / account menu **only when `enableCloud`**. Signed-in users see email + Sign out; admins additionally see an Admin link (still gated by `isAdmin && enableAdmin`). - -8. **Auth settings** - - Call `supabase--configure_auth`: `disable_signup: false`, `auto_confirm_email: false`, `external_anonymous_users_enabled: false`, `password_hibp_enabled: true`. - -9. **Docs** - - `README.md`: add `VITE_ENABLE_CLOUD` row (default false; enables public auth + cloud sync); remove `VITE_ENABLE_REGISTRATION`; clarify `VITE_ENABLE_ADMIN` only gates `/admin`. Note offline-first invariant: with the flag off, no auth/cloud code runs. - - `CLAUDE.md`: update env vars section, architecture map, and plugin notes (cloud-sync now flag-gated). - - `CHANGELOG.md`: `[Unreleased]` entry under "Added" + "Changed" (flag rename). - - Update memory `mem://config/environment-variables` and `mem://architecture/cloud-sync-strategy` to reflect the new flag. - -## Out of scope - -- Apple / other social providers (easy follow-up; Lovable Cloud supports Apple). -- Profiles table / display names / avatars — current features key off `auth.uid()` only. -- Branded auth emails (`scaffold_auth_email_templates`) and custom email domain. - -## Technical notes - -- Files added: `src/pages/ForgotPassword.tsx`, `src/pages/ResetPassword.tsx`, `src/pages/AuthCallback.tsx`. -- Files changed: `src/App.tsx`, `src/pages/Login.tsx`, `src/pages/Register.tsx`, `src/contexts/AuthContext.tsx`, `src/plugins/cloud-sync/index.ts`, `src/plugins/cloud-sync/CloudSyncPanel.tsx`, `vite.config.ts` (PWA denylist), `src/components/LandingPage.tsx` (or `Index.tsx` header), `README.md`, `CLAUDE.md`, `CHANGELOG.md`. -- Tool-generated (do not hand-edit): `src/integrations/lovable/` from `configure_social_auth`. Even when generated, it only runs at import time from cloud-flagged pages → stays out of the disabled build's bundle via lazy boundaries. -- No DB migration required. -- Verification: `npm run build` once with `VITE_ENABLE_CLOUD` unset (confirm auth chunks absent, Labs tab not auto-mounted), once with `VITE_ENABLE_CLOUD=true` (confirm routes + Google flow work). diff --git a/CHANGELOG.md b/CHANGELOG.md index ef60b3b0..a16097d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,131 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 > from git history and grouped by theme rather than exhaustive per-commit > detail. +## [2.9.0] - 2026-06-22 + +### Added +- **Password strength rules + a realtime checker on sign-up.** The registration + form now enforces stronger passwords (at least 8 characters, with a lowercase + letter, an uppercase letter, a number, and a symbol) and shows a live strength + meter with a per-rule checklist that updates as you type. The check is fully + offline (`lib/passwordStrength.ts`). +- **"Update available" check that doesn't rely on the service worker.** The app + now also compares the running build against a build-emitted `version.json` + fetched fresh from the server, so a stale tab reliably surfaces the "Update + ready" prompt even when the service worker's own update detection stalls behind + HTTP/CDN caching. The Refresh button hard-reboots onto the new build. +- **Separate cloud + local storage bars in the profile.** When signed in, the + profile now shows two storage meters — one for cloud quota and one for on-device + (local) storage — instead of just the cloud one. The local bar carries an (i) + bubble explaining that the device's real free space isn't visible to the app, so + the bar is marked against an arbitrary 10 GB and can be safely ignored (local + storage has no limit). +- **Generic logger-connection layer.** Downloads now go through one + `LoggerConnection` interface (`listLogs` / `downloadLog` / `disconnect`) in + `src/lib/loggers/`, with `createFledglingConnection()` adapting the Web Bluetooth + transport. This is the seam future loggers plug into — MyChron over the native + (Tauri) shell, Alfano over BLE — without the download UI having to branch on + transport. `DeviceContext` now tracks the connected `loggerKind`. +- **Logger picker.** "Download from logger" now opens an image-based chooser of + supported loggers instead of jumping straight to Bluetooth: **PerchWerks + Fledgling** (DIY, Bluetooth), **AiM MyChron 5 / 5S / 2T** (Wi-Fi, native app + required), and **Alfano 6 / 7** (Bluetooth, coming soon). The Fledgling runs the + existing Bluetooth download flow; MyChron and Alfano open explanatory dialogs for + now (MyChron's copy differs between the native app and the web). Placeholder + artwork lives in `public/loggers/` for easy swap-out. New `logger` i18n namespace. +- **Download from a MyChron over Wi-Fi (native app).** In the native (Tauri) app, + the MyChron tile in the logger picker now runs a real download: it connects to the + logger over Wi-Fi (on Android it prompts you to pick the MyChron in the system + Wi-Fi dialog), lists the sessions on the device, and downloads + opens the one you + choose — mirroring the existing Bluetooth flow. The web app is unchanged (browsers + can't reach the logger's Wi-Fi) and still shows the explanatory dialog. Built on a + new `createMychronConnection()` adapter behind the existing `LoggerConnection` + interface; the native Tauri IPC (`@tauri-apps/api`) is lazy-loaded so it never + enters the web bundle. A native-only **Settings → MyChron** field lets you set the + Wi-Fi name prefix the system picker filters on, in case your logger broadcasts a + different name. +- **Safe-area padding on native / installed PWA.** Every full-screen surface now + respects the device's safe-area insets (status bar / notch) via + `env(safe-area-inset-*)` and `viewport-fit=cover`, so chrome no longer sits under + the phone's status bar — the app shell + landing, the full-page routes + (login/register/admin/legal), and the file-manager drawer (its garage/profile/ + device header). A no-op on browsers and desktops without a safe area. +- **Privacy / Terms links on the sign-in page.** The login screen now links to the + Privacy Policy and Terms of Service. +- **"Back to app" returns you where you were.** The back button on the Privacy and + Terms pages now steps back to the page you came from (e.g. the sign-in or register + screen) instead of always jumping to the home screen. + +### Changed +- **Picker/landing polish.** Logger names shortened to "AiM MyChron 5+" and + "Alfano 6+" (single-line, and future-proof for newer models); the account- + deletion page picked up the shared sticky brand header; and the landing + "Telemetry Data Viewer" tagline was removed for now. +- **Open-source links moved into About.** The GitHub repository links left the + landing page and now live in the About dialog under "Free & Open Source", above + the feature list. The About feature list was also refreshed with the headline + additions from the last month (7 languages, native Android app + MyChron Wi-Fi + download, phone-as-logger GPS timing, optional cloud sync, AI driving coach). +- **Logger picker polish.** Real product photos replace the placeholder art, and + the picker is now mobile-friendly: compact horizontal cards on phones (instead of + full-height ones that filled the screen), a height cap so it scrolls within the + viewport, native safe-area padding, an explicit Close button on mobile, and a + trademark/ownership note for the MyChron and Alfano brands. +- **Sticky headers across the landing and auth/legal pages.** The landing banner + is now pinned to the top of the screen, so the Login button is always reachable + and content scrolls cleanly under the status bar on mobile. The login, register, + privacy, and terms pages share one sticky, clickable brand header (`BrandHeader`) + that returns you home — and the privacy/terms pages now carry the mobile + safe-area padding the rest of the app has. +- **Native build hides paid-plan UI.** On the native app (where paid plans aren't + sold per Google Play policy), the registration page no longer shows the pricing + cards at all — just the free sign-up form. (Profile billing buttons were already + hidden on native.) +- **"Download from logger" relabel.** The file-manager button previously labelled + "Download from DovesDataLogger" is now "Download from logger" and opens the new + logger picker. +- **Logger picker is now eager.** The picker menu was split out of the lazy + Bluetooth chunk into a lightweight host (`LoggerDownload`), so the home-screen + "Download from logger" button opens the menu instantly instead of waiting on (and + silently failing when) the BLE chunk stalls. The heavy BLE flow still loads lazily + — only once the Fledgling is picked. +- **Device tab is Fledgling-gated.** The drawer's Device tab (settings / tracks / + firmware) now shows a "Fledgling only" notice when the connected logger isn't a + Fledgling, groundwork for MyChron/Alfano connections that don't expose those + surfaces. +- **Landing page upload is now a 50/50 split.** The big drag-and-drop dropzone now + shares the top of the home screen with an equal-billing "Download from logger" + panel, since most users pull logs straight off a logger rather than dragging files + in. The separate "Download from logger" tile was removed (its action moved into the + split). +- **Landing footer / link cleanup.** Privacy Policy and Terms of Service moved out of + the mid-page link row: on the stable build they now flank the operated-by / version + block in the footer (Privacy left, Terms right). Credits moved up next to the + Browser Compatibility button. +- **Leaner native landing page.** On the native (Android/Tauri) shell the landing + page now hides the marketing hero, the feature roadmap, the GitHub repo links and + the sponsor button — the user has already installed the app. The "Build your own + logger" tile stays. +- **Header logo returns to the home screen.** Tapping the LapWing logo (top-left) + in an open session now closes the session and returns to the landing page. +- **Purple theme + new logo.** The brand accent moved from teal to **purple** + (`--primary` and its mirrors `--ring`/`--sidebar-primary`/`--sidebar-ring`, in + both light and dark), and the gauge glyph in the app/page headers is replaced + with the new **LapWing logo** (`web-logo.png`, via a shared `BrandLogo` + component). Pairs with the refreshed app icons and favicon. Data-viz colors + (speed/telemetry scales) are unchanged. + +### Fixed +- **External links opened inside the native app.** Links like "Build your own + logger" now reliably open in the system browser on the native (Tauri) app + instead of navigating the in-app WebView, by routing through Tauri's opener + plugin when the shell hasn't wired the `__HTT_NATIVE__` bridge (`platform.ts`). +- **Login rate limiter locked out valid sign-ins.** The limiter counted *every* + sign-in attempt — successes included — as a failure on a pre-login check, so + after five tries an IP was locked for an hour even with the correct password. + It now only counts genuine failures, clears the counter on a successful login, + and ages failures out over a 15-minute sliding window. + ## [2.8.0] - 2026-06-20 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index 09f19af3..97616c91 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -86,7 +86,7 @@ src/ │ ├── RaceLineView.tsx # Leaflet map: race line, speed heatmap, braking zones │ ├── TelemetryChart.tsx # Canvas speed/telemetry chart (simple mode) │ ├── VideoPlayer.tsx # Synced video playback + overlay system (multi-chunk GoPro playlists via lib/videoPlaylist) -│ └── … # FileImport, DataloggerDownload (BLE entry, lazy), LapSnapshot*, … +│ └── … # FileImport, LoggerDownload (eager picker host) + LoggerPicker (image chooser) + DataloggerDownload (lazy Fledgling BLE flow), LapSnapshot*, … ├── hooks/ # One concern each; Index.tsx orchestrates. │ ├── useSessionData # Parses imported file → ParsedData │ ├── useLapManagement # Lap calc, selection, visible range @@ -116,6 +116,7 @@ src/ │ ├── fileLoadingState.ts # ★ Host pub/sub for the global file-load overlay │ ├── *Storage.ts # IDB/localStorage store modules (file, vehicle, engine, template, note, setup, …) │ ├── gps/ # ★ Phone-as-datalogger layer: gpsFix, customGps, sessionGate, realtimeTimer, dovepWriter +│ ├── loggers/ # ★ Generic LoggerConnection (listLogs/downloadLog/disconnect) + per-logger adapters — Fledgling=BLE, mychron/=MyChron over native (Tauri) Wi-Fi IPC (lazy; @tauri-apps/api dynamic-imported, native-only); Alfano later. progress.ts = transport-neutral formatters + computeProgress (→ docs/ble.md) │ ├── speedHeatmap.ts / mapMarker.ts / brakingZones / gforceCalculation / … # racing math │ ├── chartUtils / canvas2d / chartAxis / chartColors / videoExport / overlayCanvasRenderer # charts/video │ ├── videoPlaylist.ts # ★ Pure GoPro chunked-video model: parse/order GH/GX/GP/GOPR chunk names, build a virtual timeline (cumulative offsets) + virtual↔local time mapping + planAudioSegments (export audio stitch). useVideoSync swaps the