BETA → main [ON HOLD — do not merge]#41
Draft
TheAngryRaven wants to merge 159 commits into
Draft
Conversation
First concrete extension point for the plugin system: plugins contribute self-contained React panels to a named slot, and the host mounts them. Built so future personal/third-party plugins plug in the same way. - panels.ts: PluginPanel/PluginPanelProps contract, PANELS_POINT, PanelSlot, getPanelsForSlot. PluginPanelProps is a curated, read-only session snapshot so plugins never depend on the host's internal session context. - PluginPanelHost: mounts every panel for a slot in a titled card, each behind a per-panel error boundary so a buggy plugin can't crash the tab. - LabsTab renders the "labs" slot; Index shows the Labs tab automatically when a plugin contributes a labs panel, even with the experimental setting off. https://claude.ai/code/session_01QF56Xjp5ZMgXrqfTWD14Le
…plugins-vIcuh Add plugin UI panel framework, surfaced in the Labs tab
First in-repo plugin built on the panel framework: a Labs panel that signs the user in and does manual, directional push/pull of local IndexedDB data to Supabase. Structured stores sync as jsonb documents in a new sync_records table; raw session blobs round-trip through a private per-user Storage bucket. Both are RLS-scoped to the owner. Sync is additive (no deletion propagation yet) and online-only — the core app stays fully offline without it. - supabase migration: sync_records table + user-files bucket, owner-scoped RLS - syncStores.ts holds the pure store/key config (unit-tested); syncEngine.ts does the IDB <-> cloud I/O; cloudClient.ts isolates the typed-client escape hatch until Supabase types regenerate - PluginPanelHost now wraps panels in Suspense so panel components can be lazy; CloudSyncPanel is lazy-loaded to keep it off the initial bundle - sign-in only for now (Google to be added via Lovable); auth UI is a thin stub https://claude.ai/code/session_01QF56Xjp5ZMgXrqfTWD14Le
Coverage SummaryLines: 35.5% (2397/6751) · Statements: 34.54% · Functions: 28.88% · Branches: 36.05% Per-file coverage
|
…plugins-vIcuh Add Cloud Sync first-party plugin (Supabase file + garage sync)
Phase 1 of the pacing rework: a standalone, unit-tested lib module porting the DovesLapTimer issue #29 design to the web tool. Not yet wired into the app — this lands the core math so it can be reviewed in isolation. - resampleByDistance(): canonical arc-length grid (one point per N meters), independent of GPS rate and lap duration — fixes the legacy distance method's cumulative-noise drift and gives uniform spatial resolution for the coach. - computePositionDelta(): projects each native current fix onto the nearest reference segment (interpolating the closest point so the gap doesn't snap), with a monotonic windowed search to defeat hairpins/self-crossings, an EMA (issue #29 convention) + optional zero-lag forward-backward smoother, and a sanity guard. Exposes matchIndex/matchFrac as a cross-lap alignment map. - 10 tests: grid uniformity, GPS-rate independence, zero gap vs self, growing gap for slower laps, segment interpolation, sanity guard, smoothing. https://claude.ai/code/session_01QF56Xjp5ZMgXrqfTWD14Le
X-Lovable-Edit-ID: edt-b7983c69-b33b-4805-b644-fb0536a2cc26 Co-authored-by: TheAngryRaven <2923950+TheAngryRaven@users.noreply.github.com>
…elta Add plugin UI panel framework, surfaced in the Labs tab
Edited UI in Lovable Co-authored-by: TheAngryRaven <2923950+TheAngryRaven@users.noreply.github.com>
X-Lovable-Edit-ID: edt-be5ebb3b-4f23-42f8-b8a7-b92bd8ccacd4 Co-authored-by: TheAngryRaven <2923950+TheAngryRaven@users.noreply.github.com>
Routes the app's pace computation through the new position delta behind a
user setting. paceData keeps its exact shape, so every consumer (charts,
race-line, overlays, video export, headline gap) upgrades transparently.
- useReferenceLap now calls computeLapPace() for both the reference and
best-lap-fallback paths, selecting position (default, zero-lag) vs the legacy
distance method.
- New settings: deltaMethod ('position' | 'distance', default position) and
deltaSampleMeters (default 2), with a Settings -> Lap Delta toggle.
- computeLapPace selector added to lapDelta.ts with 2 tests (distance delegates
to the legacy path; position resamples + projects).
https://claude.ai/code/session_01QF56Xjp5ZMgXrqfTWD14Le
…ures-SfQKQ GDPR: self-service data export, account deletion & IP retention
…tion-setup-SXPrV # Conflicts: # README.md # src/pages/Register.tsx # supabase/config.toml
…etup-SXPrV Complete Stripe integration: monthly/annual, signup plan select, grace + log trimming
Capture a single lap (GPS samples ± a 5s buffer, course geometry, engine, and a copy of the vehicle/setup) as an immutable baseline for cross-session comparison and future AI coaching. - One snapshot per (course, engine): assigning an engine+setup prompts to save/update the course fastest lap when it's faster; a manual save lives in the lap-list Snapshots picker. A faster lap replaces it in place. - Loaded as a comparison overlay through the external-reference slot, so it never auto-plays and is excluded from playback and the video player. - Local-first and unlimited on-device; cloud-synced via a dedicated lap_snapshots table with per-tier COUNT limits (free 5 / plus 10 / premium 20 / pro 50), not byte document storage. Always pushes on save; a local delete never propagates to the cloud (cloud copy removed only from Profile -> Lap snapshots, like the log menu), with tombstones to prevent reconcile resurrection. https://claude.ai/code/session_01L9h3QDcyTEXmVe6tWMio6T
…-VBWDI Lap snapshots: frozen "course fastest lap" per engine
X-Lovable-Edit-ID: edt-8e5142e8-7839-483a-af55-0de6d5be440c Co-authored-by: TheAngryRaven <2923950+TheAngryRaven@users.noreply.github.com>
Selecting a snapshot already routed it into the reference-overlay slot, but it wasn't discoverable as "the reference" and had no entry point on the lap page. - The Snapshots menu now reads "Load as reference lap" and selecting a snapshot sets it as the reference comparison. - Add a "Load snapshot as reference" button next to the external-reference loader (Choose Log) on the lap times page, opening the same per-course snapshot picker in load-only mode. Threaded through SessionContext + ExternalRefBar's new trailing slot. https://claude.ai/code/session_01L9h3QDcyTEXmVe6tWMio6T
…-VBWDI Load lap snapshots as the reference lap
The 20260526+ migration batch (subscription_tiers, user_subscriptions, lap_snapshots, account_deletions + RPCs) was applied to Postgres but PostgREST kept serving a stale schema cache. Since the REST API backs both the client and the edge functions' service-role queries, those objects were invisible over REST — breaking checkout (non-2xx), lap-snapshot sync, and the snapshot usage meter, while Stripe price loading (no PostgREST) still worked. Add a migration that issues `notify pgrst, 'reload schema'` so the cache reloads on deploy (and last in sequence on a fresh DB). Also harden two failure modes the incident exposed: document and snapshot auto-sync now reconcile independently, and the Profile snapshot panel no longer blanks when its best-effort usage meter can't load. https://claude.ai/code/session_017wyJik7iHRfxTKeuKFc4Xs
Plugin panels (the AI coach) only saw data/laps/selectedLapNumber/course/useKph, so they had no awareness of lap snapshots even when one was loaded as the reference. Add `activeSnapshot` to PluginPanelProps: the loaded reference snapshot as a curated PluginSnapshot with clean-lap samples (capture buffer trimmed) plus the frozen engine/course/vehicle/setup. Computed once in Index from the active snapshot id and threaded through SessionContext to the Coach, Labs, and Profile panel hosts. https://claude.ai/code/session_01L9h3QDcyTEXmVe6tWMio6T
The auto-upload of local snapshots is wired through autoSync's reconcile on app boot / sign-in, but a reconcile that fails (e.g. the schema-cache outage) is swallowed and never retried in the same session, leaving local snapshots stuck off the cloud. Have the Lap snapshots panel re-run reconcileSnapshots when it opens and refresh live on snapshot-store changes, so viewing the panel uploads any local-only snapshots without an app reload. https://claude.ai/code/session_017wyJik7iHRfxTKeuKFc4Xs
…g-fixes Fix stale PostgREST schema cache breaking subscriptions + snapshot sync
The active snapshot already carries its frozen setup (the baseline), but the setup the driver is currently running wasn't being passed to plugins at all. Add `sessionSetup: VehicleSetup | null` to PluginPanelProps, resolved once in Index from the session's assigned setup id, threaded through SessionContext to the Coach / Labs / Profile hosts. A coach panel can now compare the current setup against the snapshot's frozen baseline. https://claude.ai/code/session_01L9h3QDcyTEXmVe6tWMio6T
…-VBWDI Expose active reference snapshot + session setup to plugin panels
pushSnapshot upserts ON CONFLICT (user_id, course_key, engine_key), but the inline `unique (...)` in the CREATE TABLE was silently skipped when the table pre-existed the snapshots migration, so the upsert errored with "there is no unique or exclusion constraint matching the ON CONFLICT specification". Add the constraint as an idempotent unique index so existing deployments self-repair on apply, and reload the PostgREST cache so it picks it up. https://claude.ai/code/session_017wyJik7iHRfxTKeuKFc4Xs
…-index Add missing lap_snapshots (user, course, engine) unique index
X-Lovable-Edit-ID: edt-e538d4fb-fe01-48f0-8b89-49b2dedd48b8 Co-authored-by: TheAngryRaven <2923950+TheAngryRaven@users.noreply.github.com>
Same root pattern as the missing unique constraint: when the lap_snapshots table pre-existed the snapshots migration, CREATE TABLE IF NOT EXISTS skipped the whole declaration, so `id uuid primary key default gen_random_uuid()` and `updated_at timestamptz not null default now()` came in without their defaults. With the unique index in place the upsert now reaches the INSERT and fails with "null value in column id violates not-null constraint" because pushSnapshot doesn't (and shouldn't) send an id. Idempotent ALTER COLUMN ... SET DEFAULT for both, plus a PostgREST reload. https://claude.ai/code/session_017wyJik7iHRfxTKeuKFc4Xs
Updates the optional AI coach plugin from 0.2.5 to 0.3.0 in package.json/package-lock.json and notes the bump under CHANGELOG [Unreleased] → Changed.
…-defaults Re-set lap_snapshots column defaults (id, updated_at)
Bump @perchwerks/eye-in-the-sky to 0.3.0
X-Lovable-Edit-ID: edt-5c74e9c1-1d5f-4af3-97dc-ed3bea2aa006 Co-authored-by: TheAngryRaven <2923950+TheAngryRaven@users.noreply.github.com>
The Profile-tab Storage section showed bytes for documents + logs but had no sign of the snapshot count quota (free 5 / plus 10 / premium 20 / pro 50), so users couldn't see how close they were to the limit before a save was rejected. - Add a count-based meter for "Lap snapshots" beneath the byte meters, fetched from the snapshot_usage RPC (best-effort, doesn't hide the byte meters if it fails). - Add inline "Local storage is always free." subtext next to the Storage label so the byte+count limits read as cloud-only. https://claude.ai/code/session_01L9h3QDcyTEXmVe6tWMio6T
…-VBWDI Add snapshot quota meter + "local is free" hint to the Storage section
The snapshot quota meter (PR #80) was gated on fetchSnapshotUsage(), which silently returns null in production — likely a stale PostgREST schema cache. Both meters that depended on it (the Storage panel's count bar and the Lap snapshots panel's "X of Y synced" header) were rendering nothing or the fallback "Synced snapshots" instead. - Drop the RPC dependency in both panels. - Use listCloudSnapshots(user.id).length for the count (the same call those panels were already making to render the list). - Use the user's tier's snapshot_count from useSubscription's tier catalogue for the limit. The snapshot_usage RPC stays in cloudClient for now in case other callers appear; the panels just don't need it. https://claude.ai/code/session_01L9h3QDcyTEXmVe6tWMio6T
…-VBWDI Fix snapshot quota meter: derive count + limit client-side
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🚧 ON HOLD — do not merge
Tracking PR for everything staged on
BETAahead ofmain. Kept as a draft;coaching is still under active development and a few verification passes remain
(see Still to do). This description is the running ledger of what's landed.
What's in BETA
🧩 Plugin framework (new) — #40, #42, #47, #49–#52
host renders them in titled cards, each isolated by an error boundary + Suspense
(so panels can be
React.lazy). Slots are self-gating.for panels that own their layout (e.g. the coach dashboard).
🏎️ AI Coach — #47, coach
@perchwerks/eye-in-the-skyv0.2.4 (#55)debrief, uPlot charts, corner/sector breakdowns, Leaflet race-line map),
all lazy. Ships from the public npm registry.
🔁 Canonical telemetry channels — #48
defaults + saved graph/overlay selections across every logger format. G-force
distinct per source. Existing files migrate transparently.
📈 Position-based lap delta / pace — #43, #44
method still selectable. Upgrades pace everywhere.
📸 Lap snapshots — #73
Frozen "course fastest lap" captures — an immutable single-lap baseline for
cross-session comparison (and future AI coaching). Local-first, cloud-synced.
deterministic id. A faster lap upserts in place, so the count never inflates.
Engine is the layman's "primary key"; the chassis travels inside the frozen setup.
buffer on each side, the course geometry, lap time, source file/lap, and a copy
of the vehicle + setup.
course fastest lap when its best lap beats (or has no) stored snapshot; a manual
Save as snapshot also lives in the lap-list Snapshots picker (header, so
it serves simple + pro mode).
it's excluded from playback and the video player and never an appended lap.
The overlay label shows the engine (2-stroke vs 4-stroke reads clearly).
lap_snapshotstable with a per-tier COUNT quota(not byte document storage — snapshots are chunky and AI-valuable). Always pushes
on save; a local delete never propagates to the cloud (cloud copy removed only
explicitly from Profile → Lap snapshots, like the log menu), with tombstones so
reconcile won't resurrect a surviving local copy. Local storage stays unlimited.
☁️ Cloud Sync, accounts, storage & profiles — #42, #45, #46, #53, #54, #56, #57, #58, #59, #60
VITE_ENABLE_CLOUD, default off): email/Google,forgot/reset password.
storage type (5 MB); logs are a separate 20 MB type. Propagation deletes
for vehicles/setups (loud warning). Limits enforced server-side. Far-right
Profile tab with usage meters. Anon local garage migrates on first sign-in.
SpeedyRac3r-546)when blank; Profile-tab editor with "that name's taken" handling.
size) and deletes them — cloud copy only (other devices keep their download),
with an opt-in toggle to also delete the local copy on this device.
updatedAt;sync is pending-wins + last-write-wins, so a newer change is never stomped
and offline edits take priority on reconnect. Profile tab flags offline state
(auto-sync, delete propagation, the same timestamp merge), under the documents
type. They live in localStorage, so the engine grew a store-accessor seam
(IndexedDB for most stores, localStorage for tracks). Built-in public tracks
never sync.
offers an opt-in "also delete the cloud copy" switch (off by default — the cloud
copy is a backup; offline → queued, propagates on reconnect), via a generic
FileDeleteConfirmhost mount. Uploads no longer orphan a blob if the quotatrigger rejects the index write (rollback), and pre-existing orphans are reclaimed
when the Cloud logs panel opens.
the documents limit, sync no longer fails wholesale — it pushes the batch, and on
a quota rejection falls back to per-record upserts so everything that fits still
syncs, reporting how many items were skipped (manual-push toast + background
reconcile notice).
💳 Paid subscription tiers — Stripe — #62 (backend), #66 (UI), #68 (premium), #71 (monthly/annual · grace · full integration)
Stripe-backed paid tiers that scale the cloud-sync logs quota on top of the free
type limits above. Each paid tier bills monthly or annual.
freepluspremiumpro(AI, coming soon)(prices provisional; numbers match the in-app
PricingCards, which now read live prices from Stripe. The lap-snapshot count column is the per-tier limit added in #73.)subscription_tiers; changing alimit is an
UPDATE.premium(Add a Premium subscription tier ($3/mo, Pro storage, no AI) #68) slots between plus and pro: pro's storage, no AI.${tier}_${interval}(plus_monthly,plus_annual,premium_*,pro_*). Checkoutand the pricing catalogue resolve prices live by lookup_key, so the Stripe
dashboard is the single source of truth — no Price ids in code or DB.
user_subscriptionsmaps each user → tier and is read-only to users (RLS); onlythe service role writes it, so entitlements come solely from the signature-verified
Stripe webhook — the client can never self-upgrade. It now also tracks
cancel_at_period_end,billing_interval,grace_until,logs_trimmed_at(Complete Stripe integration: monthly/annual, signup plan select, grace + log trimming #71).sync_storage_usage()RPC resolve the caller's tier limit viatier_limit(), falling back tofree→ the legacyquota_limitsbaseline.stripe-prices(public; reports configured? + live prices — Complete Stripe integration: monthly/annual, signup plan select, grace + log trimming #71),create-checkout-session(tier + interval),stripe-webhook(the sole entitlementwriter; sets grace on cancel),
create-portal-session(Stripe-hosted manage/cancel/renewal).STRIPE_SECRET_KEYis absent the pricing UI showsonly the two free cards (Guest + Free) and hides the paid tiers entirely.
Account-first: a paid choice creates the account, stashes the intent, and resumes to
Stripe Checkout on first sign-in after email confirmation; the webhook then provisions it.
useSubscription()+useStripePrices()hooks; liveUpgrade / Current plan actions + a monthly/annual toggle on
PricingCards; aManage subscription portal link + renewal/cancellation/grace date on the Profile tab.
Billing logic is core + split pure (
lib/billing.ts, unit-tested) vs I/O (lib/billingClient.ts).self-service purchasable (not selectable at sign-up, no Upgrade, checkout rejects it). It
can still be comped to a tester via a Stripe-created subscription (the webhook honours
it). One
COMING_SOON_TIERSset inlib/billing.ts(mirrored in the edge fn).boundary (drops to free limits immediately) but keeps the user's logs for a 60-day grace
window; after it expires,
trim_expired_logs()(daily pg_cron) trims synced logsnewest-first to the free allowance.
with one recurring Price per interval, tagged with the lookup_keys above (test mode
first) → set
STRIPE_SECRET_KEY+STRIPE_WEBHOOK_SECRET→ register the webhook(
checkout.session.completed,customer.subscription.created|updated|deleted) → enable thepg_cronextension (for the trim job).🔐 Privacy, GDPR & data retention — #70, #72
(one ZIP of all server-side + browser-local data;
export-account-datafn) and Delete myaccount (email one-time-code confirmed, scheduled 7 days out and cancellable;
request-account-deletion+ cronprocess-account-deletions).days, deletes them after 1 year (submissions only once reviewed), and clears expired IP bans
panel (selectable TTL, default 90 days).
combobox; the list syncs with the rest of the garage.
🛠️ Build / env / PWA infra
VITE_ENABLE_CLOUDgates all cloud auth + sync;HTT_env mirror prefix;preview SW cache eviction.
New env vars
VITE_ENABLE_CLOUDfalseHTT_*mirrorsHTT_SUPABASE_URL/PUBLISHABLE_KEY/PROJECT_ID,HTT_ENABLE_CLOUD,HTT_ENABLE_ADMINDOVE_PLUGIN_PACKAGES@perchwerks/eye-in-the-skySTRIPE_SECRET_KEYSTRIPE_WEBHOOK_SECRETDELETION_CRON_SECRETprocess-account-deletionscron worker (#70)Backend / migrations
sync_recordstable + privateuser-filesbucket (RLS per owner).quota_limitstable (storage_type,max_bytes),enforce_sync_quotatrigger,sync_storage_usage()RPC.profilestable (uniquedisplay_name) +handle_new_usertrigger (auto-generates a name) + existing-user backfill.
subscription_tiers+user_subscriptionstables (thelatter now carrying
cancel_at_period_end/billing_interval/grace_until/logs_trimmed_at),user_tier()/tier_limit()functions; quota trigger + usage RPC madetier-aware; premium seed (Add a Premium subscription tier ($3/mo, Pro storage, no AI) #68). The
subscription_grace_trimmigration addsencode_uri_component()+trim_expired_logs()+ a daily pg_cron schedule (Complete Stripe integration: monthly/annual, signup plan select, grace + log trimming #71). Four edgefunctions (prices / checkout / webhook / portal).
account_deletionstable +purge_expired_personal_data()+process-account-deletionsworker; daily pg_cron / pg_net wiring.lap_snapshotstable (one row peruser_id, course_key, engine_key,RLS per owner) + a per-tier
subscription_tiers.snapshot_countcolumn (free 5 / plus 10 /premium 20 / pro 50). A count-based
enforce_snapshot_quotatrigger (excludes thereplaced row, so faster-lap upserts never count),
snapshot_limit()+snapshot_usage()functions, and an
updated_attouch trigger. IndexedDB bumped to v11 (newlap-snapshotsstore, indexed bycourseKey/engineKey).Quality
[Unreleased]current. Four CI gates green on contributing PRs(lint / typecheck / 737 tests / build — Lap snapshots: frozen "course fastest lap" per engine #73 adds 10 snapshot tests). Bundle
budget respected (uPlot, Leaflet, and the sync engine all stay off the initial path).
Still to do (why this is on hold)
offline/pending flag + Profile meters + display-name edit + tracks sync +
log-delete toggle + over-limit partial push end-to-end).
(excluded from playback/video), faster-lap replace, signed-out (local) vs signed-in
(cloud) Profile management, and the cloud count-quota / delete-doesn't-propagate flow
end-to-end.
lookup_keys, no-Stripe failback, signup plan selection, cancellation grace + pg_cron log
trimming, Pro coming-soon). Still need to validate checkout → webhook → tier flip →
portal cancel → grace/trim in Stripe test mode, and create the Products/Prices (with
lookup_keys) + secrets + webhook + enable pg_cron before the paid cards go live.
grace → cron erasure flow end-to-end; set
DELETION_CRON_SECRET+ enable pg_cron/pg_net.Cloud Sync follow-ups— all shipped:Timestamp-based merge— Offline-aware, conflict-safe document sync (timestamp merge + pending set) #57 (pending-wins + LWW).Tracks/courses auto-sync— Sync user tracks & courses (documents storage type) #58.Auto log-delete propagation + orphan cleanup— Propagate log deletes to the cloud + prevent orphaned blobs #59.Over-limit batch push— Partial document push when over the cloud quota #60 (partial push + skipped count).📌 Roadmap notes (owner)
This repo (host) / cloud & accounts
Coaching plugin (other repo)
Open questions — monetization / infra
plan landed (Add Stripe subscription backend (tiers, webhook, quota wiring) #62): Stripe subscriptions, tiers are DB rows (
subscription_tiers),and each tier carries an
ai_creditsallowance (0 for now) so the coach plugin canmeter against it later. Entitlements are webhook-granted, never client-side. The AI
(
pro) tier ships coming-soon (Complete Stripe integration: monthly/annual, signup plan select, grace + log trimming #71) — comp-only via Stripe until self-service opens.on top? → resolved (Add Stripe subscription backend (tiers, webhook, quota wiring) #62): storage IS the subscription — the
logsquotascales by tier (free 20 MB → plus 500 MB → premium/pro 1 GB); docs stay 5 MB. AI credits
ride on the same tier rows (
ai_credits), to be charged/metered on top later.(Reminder: storage limits are "storage types" — documents/logs; "tier" =
these paid subscription levels. Lap snapshots are a third, count-limited type — Lap snapshots: frozen "course fastest lap" per engine #73.)
toggle on PricingCards + Manage-subscription portal link on the Profile tab.
https://claude.ai/code/session_012D8zxba3CCmUdqgT16zZav