Skip to content

5.0.0: drag mode, GoPro import, LapWing look, native video - #423

Merged
TheAngryRaven merged 72 commits into
mainfrom
BETA
Oct 4, 2026
Merged

TheAngryRaven merged 72 commits into
mainfrom
BETA

Conversation

@TheAngryRaven

@TheAngryRaven TheAngryRaven commented Aug 29, 2026 •

Copy link
Copy Markdown
Owner

Requested by Dove · project thread

LapWing 5.0.0

Release PR: merges everything on BETA that isn't on main yet (plans 0022 to 0029 plus the LapWing branding). Full detail is in the ## [5.0.0] section of CHANGELOG.md.

Highlights

  • Drag mode. Drop in a drag-strip log (or any unknown venue) and standing-start runs are detected automatically. Pick 1/8 mile, 1000 ft or 1/4 mile to get a time-slip breakdown: ET plus 60 / 330 / 660 / 1000 ft splits. Early lifts stay listed as incomplete with the splits they reached, and the chosen distance is remembered per file. (Plan 0022)
  • Import a GPS-tagged GoPro video as a session. HERO5 to HERO11+ embed GPS in the MP4; dropping the video (or all chapters of a split recording) turns that telemetry into a normal session with laps, sectors and the map. Only the telemetry track is read, the video opens already synced and locked, and the session is saved as a Dove log. (Plan 0029)
  • New LapWing look. LAPWING wordmark, the PerchWerks violet palette in light and dark mode (dark is now deep indigo), new icon, favicon and link-preview image, tighter corners and a refreshed home hero. Charts and speed colours are unchanged. (Apply the LapWing brand to the web app #447)

Added

  • Setup history records every edit. Saving a setup freezes a revision, newest first, with a Used / All toggle. Untagged revisions stay on the device and are swept after 3 days, always keeping the latest. (Plan 0028)
  • Roll back or duplicate a setup from its history. Duplicate on any revision that ran on a session; Roll back on the most recent one when the live setup has drifted. (Plan 0028)
  • Offline readiness in Settings. Shows whether this device can open LapWing with no signal, how much of the extras are cached, and a Finish download button. (Plan 0027)
  • Install prompt on iPhone and iPad. Shows the manual Share → Add to Home Screen steps, since iOS never fires the install event and Safari evicts an uninstalled site's data after about a week. (Plan 0026)
  • Stream an Insta360 recording (Android app). Connect over the camera's Wi-Fi, list its recordings and play one without downloading it; overlays, sync lock and scrubbing work, and 360° footage gets a draggable, lockable flat view. Needs a LapWing build with the Insta360 SDK. (Plan 0025)
  • Hardware-speed export in the Android app. Export with overlays hands the job to the shell's platform codecs with a GPU overlay composite, seconds instead of minutes. Older shells and multi-file recordings fall back to the in-app exporter. (Plan 0024)
  • Save to Gallery (Android app). Exports land in Movies/LapWing. (Plan 0024)
  • The Android app remembers your video per session, like the web version's file handles, which also makes re-exports faster. (Plan 0024)
  • Videos on this device card in the Profile tab lists the copies the Android app keeps, with per-video delete and clear-all. (Plan 0024)

Changed

  • One overlay renderer for preview and export. Exports now match the preview exactly: grid lines, glows, pace-bar labels, translated labels, and the sector sparkle on the right frames. (Plan 0023)
  • Offline cache no longer wiped by a flaky connection. Caching is split into a 4.98 MB core (was a 10.16 MB all-or-nothing download) plus extras that retry on their own; samples and logger photos are no longer downloaded twice. (Plan 0027)
  • Persistent storage requested on startup so the browser doesn't discard the cached app and saved sessions. (Plan 0026)
  • Dismissing the install prompt snoozes it for 30 days. (Plan 0026)
  • The shared-session read-only banner text is readable again. (Apply the LapWing brand to the web app #447)

Fixed

  • Android export / Save to Gallery did nothing. Video bytes now cross the native bridge as chunked base64, which Android's WebView supports, and a failed export says why instead of going idle. (Plan 0024)
  • Insta360 camera picture. The player explains why the picture is missing instead of showing a broken image, and the connect screen explains how to reach the camera's Wi-Fi since Android has no "any network" picker. (Plan 0025)

Commits in this release

  • cec9e14, aae49f2, 3c70124, 226618e plan 0022: drag mode (detector, run-to-lap mapping, orchestration + persistence, UI + i18n)
  • 194ed2a, af5ef24 plan 0023: unified overlay scene renderer
  • 6589973, f97c435, 0e84e4d, c0ea128 plan 0024: native export bridge and follow-ups
  • 9f545ce, 7c79d4b, b237b33 plan 0025: Insta360 streaming, Wi-Fi hint, missing-picture message
  • e164529 plan 0026: iOS install prompt + persistent storage
  • eb87643 plan 0027: two-stage offline cache
  • 87432f7, 57b491e, 1265a23, 14252c5 plan 0028: setup history
  • 1f26637 plan 0029: GoPro GPS video import
  • 5f06938 LapWing brand (Apply the LapWing brand to the web app #447)

Before merging

🤖 Generated with Claude Code

https://claude.ai/code/session_01A9G6PD5bpvqw5cR4v6J8BJ


Generated by Claude Code

claude and others added 5 commits August 28, 2026 03:29
A GPS log from a drag strip matches no known track, and worse, the return
road loops back near the staging lanes so waypoint mode falsely detects
out-and-back passes as laps. This adds the pure detection layer for drag
mode: a staged/launch/run state machine that finds standing-start passes,
times every traditional mark (60/330/660/1000/1320 ft) with sub-sample
interpolation, and gates on "at least one straight, fast 660" so unknown
circuits and autocross keep falling through to waypoint mode. Orchestration
and UI land in follow-up commits on this plan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012urKN2QdPpfSfjQ4Buux3g
dragRunsToLaps scores detected runs at a chosen distance and expresses them
as ordinary Laps (splits in sectorTimes/sectorBoundaries under the circuit
timing-line contract), so the whole lap UI works unchanged. A run that
lifted before the scoring mark keeps its reached splits but is flagged with
the new Lap.incomplete: its lapTimeMs is a data window that can undercut a
real ET, so ranking goes through the new fastestRankedLap helper and
calculateOptimalLap's deltaToFastest now skips incomplete laps — while
their partial splits still feed the optimal, since the best 60 ft of the
day is valid from any pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012urKN2QdPpfSfjQ4Buux3g
useDataLoader now consults detectDragRuns before accepting a waypoint
result — the drag false-positive fix, since a strip's return road loops
back near staging and waypoint mode would mis-time passes as laps. Detected
runs pre-apply at the suggested distance (unpersisted, mirroring the
waypoint branch) pending the user's prompt choice; applying persists the
new FileMetadata.dragDistanceFt (feet, matching the lengthFt convention)
so the session restores silently on reopen, and assigning a real course
clears it. Distance switching re-maps the held runs without re-detection.
Fastest-lap picks in useLapManagement and useReferenceLap now route
through fastestRankedLap so incomplete runs can't be ranked.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012urKN2QdPpfSfjQ4Buux3g
The no-track prompt grows a "Drag Runs Detected" branch (precedence:
course step > drag > waypoint > no-track) with a scoring-distance picker
pre-seeded from the longest distance a run actually covered; the session
header gains a 1/8 / 1000 ft / 1/4 switcher while drag mode is active. The
lap table renders the classic time slip — cumulative 60/330/660/1000 ft
columns with best-per-mark highlights — labels rows "Run N" through the
existing lapLabels plumbing, hides Min Speed (meaningless at a standing
start), badges incomplete passes instead of showing their window as an ET,
and its footer forks to Best ET with the optimal built from the best
splits of the day. The crop-to-segment dropdown self-derives drag mode
(only drag laps carry sectorBoundaries without a course). All new strings
ship in the seven locales.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012urKN2QdPpfSfjQ4Buux3g
…-j5vmu1

Drag mode: standing-start run detection + time slips at unknown venues (plan 0022)
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 29, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
lapwing 4813ff2 Commit Preview URL

Branch Preview URL
Oct 04 2026, 01:40 AM

@github-actions

github-actions Bot commented Aug 29, 2026 •

Copy link
Copy Markdown

Coverage Summary

Lines: 62.67% (9146/14592) · Statements: 61.69% · Functions: 58.51% · Branches: 59.1%

Per-file coverage
File Lines Functions Branches
src/components/admin/supportAttachment.ts 0% 0% 0%
src/components/map/positionArrowMarker.ts 0% 0% 0%
src/components/video-overlays/dataSourceResolver.ts 85.07% 71.42% 80.82%
src/components/video-overlays/overlayUtils.ts 100% 100% 100%
src/components/video-overlays/registry.ts 100% 100% 100%
src/components/video-overlays/sectorUtils.ts 93.33% 100% 79.09%
src/components/video-overlays/themes.ts 100% 100% 100%
src/components/video-overlays/types.ts 100% 100% 100%
src/hooks/use-mobile.tsx 0% 0% 100%
src/hooks/use-toast.ts 0% 0% 0%
src/hooks/useAuth.ts 100% 100% 100%
src/hooks/useDataLoader.ts 16.79% 27.77% 24.32%
src/hooks/useDocumentHead.ts 0% 0% 0%
src/hooks/useEngineManager.ts 0% 0% 0%
src/hooks/useFileManager.ts 0% 0% 0%
src/hooks/useFirmwareUpdate.ts 0% 0% 0%
src/hooks/useKartManager.ts 100% 100% 100%
src/hooks/useLapManagement.ts 0% 0% 0%
src/hooks/useLapOverlays.ts 0% 0% 0%
src/hooks/useLapSnapshots.ts 1.53% 3.84% 5.79%
src/hooks/useNativeFirmwareUpdate.ts 0% 0% 0%
src/hooks/useNoteManager.ts 0% 0% 0%
src/hooks/useOfflineReadiness.ts 0% 0% 0%
src/hooks/useOnlineStatus.ts 0% 0% 0%
src/hooks/usePlayback.ts 0% 0% 0%
src/hooks/useReferenceLap.ts 0% 0% 0%
src/hooks/useSessionData.ts 0% 0% 0%
src/hooks/useSessionMetadata.ts 0% 0% 0%
src/hooks/useSettings.ts 0% 0% 0%
src/hooks/useSetupManager.ts 0% 0% 100%
src/hooks/useSimPlayback.ts 0% 0% 0%
src/hooks/useStripePrices.ts 0% 0% 0%
src/hooks/useSubscription.ts 0% 0% 0%
src/hooks/useTemplateFields.ts 0% 0% 0%
src/hooks/useTemplateManager.ts 0% 0% 0%
src/hooks/useTrackEditorForm.ts 0% 0% 0%
src/hooks/useVehicleManager.ts 0% 0% 100%
src/hooks/useVideoSync.ts 0% 0% 0%
src/hooks/useWakeLock.ts 0% 0% 0%
src/hooks/useWaybackImagery.ts 0% 0% 0%
src/lib/test/idb.ts 100% 100% 100%
src/lib/aimParser.ts 96.21% 100% 83.05%
src/lib/alfanoParser.ts 93.44% 100% 75.66%
src/lib/analytics.ts 94.87% 100% 96.59%
src/lib/appActivity.ts 0% 0% 100%
src/lib/billing.ts 97.36% 100% 98.21%
src/lib/billingClient.ts 0% 0% 0%
src/lib/ble/test/mockBle.ts 96% 90% 50%
src/lib/ble/battery.ts 93.33% 100% 87.5%
src/lib/ble/connection.ts 0% 0% 0%
src/lib/ble/dfu/dfuPackage.ts 96.66% 100% 81.25%
src/lib/ble/dfu/dfuTypes.ts 100% 100% 100%
src/lib/ble/dfu/firmwareImage.ts 100% 100% 100%
src/lib/ble/dfu/firmwareManifest.ts 97.87% 94.44% 94.69%
src/lib/ble/dfu/firmwareUpdateError.ts 100% 100% 100%
src/lib/ble/dfu/index.ts 100% 100% 100%
src/lib/ble/dfu/version.ts 96.77% 100% 83.33%
src/lib/ble/fileTransfer.ts 90.69% 95% 72.91%
src/lib/ble/firmwareCrc.ts 100% 100% 100%
src/lib/ble/firmwareUpload.ts 90.62% 88% 84.31%
src/lib/ble/format.ts 100% 100% 100%
src/lib/ble/index.ts 100% 100% 100%
src/lib/ble/internal.ts 100% 100% 50%
src/lib/ble/settings.ts 93.6% 100% 85.29%
src/lib/ble/trackOpcodes.ts 100% 100% 100%
src/lib/ble/trackSync.ts 89.89% 90.9% 72.72%
src/lib/ble/types.ts 100% 100% 100%
src/lib/bleDatalogger.ts 100% 100% 100%
src/lib/blogPosts.ts 100% 100% 97.22%
src/lib/brakingZones.ts 97.14% 100% 86.11%
src/lib/browserCompat.ts 0% 0% 0%
src/lib/buildInfo.ts 100% 100% 100%
src/lib/canvas2d.ts 100% 100% 91.66%
src/lib/channels.ts 100% 100% 84.61%
src/lib/chartAxis.ts 98% 100% 82.92%
src/lib/chartColors.ts 100% 100% 100%
src/lib/chartUtils.ts 100% 100% 97.05%
src/lib/contactMessage.ts 57.69% 50% 58.33%
src/lib/courseDetection.ts 97.34% 100% 82.22%
src/lib/courseLineStyle.ts 100% 100% 100%
src/lib/courseSectors.ts 100% 100% 94.73%
src/lib/datalogParser.ts 87.17% 100% 81.94%
src/lib/db/index.ts 0% 0% 0%
src/lib/db/submissionMaterialize.ts 100% 100% 97.5%
src/lib/db/supabaseAdapter.ts 0% 0% 0%
src/lib/db/types.ts 100% 100% 100%
src/lib/dbUtils.ts 76.23% 80% 17.2%
src/lib/debugConsole.ts 57.74% 61.11% 47.72%
src/lib/deviceCourseOverrides.ts 100% 100% 97.29%
src/lib/deviceCoursePicker.ts 100% 100% 100%
src/lib/deviceCourseSelection.ts 100% 100% 100%
src/lib/deviceGeneratedNames.ts 100% 100% 100%
src/lib/deviceSettingsSchema.ts 97.87% 100% 97.4%
src/lib/deviceSyncFetch.ts 100% 100% 100%
src/lib/deviceSyncNames.ts 100% 100% 100%
src/lib/deviceSyncOps.ts 100% 100% 93.33%
src/lib/deviceSyncPlan.ts 100% 100% 100%
src/lib/deviceSyncRunner.ts 95.65% 100% 90%
src/lib/deviceSyncWizard.ts 100% 100% 70.96%
src/lib/deviceTimezones.ts 89.28% 92.85% 90.9%
src/lib/deviceTrackBudget.ts 100% 100% 100%
src/lib/deviceTrackSync.ts 100% 100% 96.4%
src/lib/doveParser.ts 89.6% 72.72% 77.27%
src/lib/dovexParser.ts 88.31% 100% 82.85%
src/lib/dragRunDetection.ts 100% 100% 92.2%
src/lib/driverProfileGroups.ts 100% 100% 83.33%
src/lib/emailValidation.ts 100% 100% 100%
src/lib/engineStorage.ts 100% 75% 100%
src/lib/engineUtils.ts 100% 100% 91.66%
src/lib/fieldResolver.ts 100% 100% 83.33%
src/lib/fileBrowserTree.ts 98.87% 97.5% 89.36%
src/lib/fileLoadingState.ts 100% 100% 100%
src/lib/fileStorage.ts 82.97% 78.12% 72.22%
src/lib/firmwareUpdateReminder.ts 100% 100% 100%
src/lib/fnv1a.ts 100% 100% 100%
src/lib/garageEvents.ts 100% 100% 100%
src/lib/gforceCalculation.ts 100% 100% 97.22%
src/lib/ggDiagram.ts 100% 100% 94.73%
src/lib/gopro/goproDetect.ts 100% 100% 100%
src/lib/gopro/goproImport.ts 100% 100% 87.5%
src/lib/gopro/goproTelemetry.ts 100% 100% 92.59%
src/lib/gopro/gpmf.ts 99.15% 100% 90%
src/lib/gopro/mp4Boxes.ts 93.95% 100% 77.55%
src/lib/gopro/testFixtures.ts 99.02% 100% 95.74%
src/lib/gopro/videoHandoff.ts 100% 100% 100%
src/lib/gps/customGps.ts 100% 100% 82.05%
src/lib/gps/dovepWriter.ts 100% 100% 92%
src/lib/gps/gpsFix.ts 100% 100% 100%
src/lib/gps/index.ts 100% 100% 100%
src/lib/gps/observationSample.ts 100% 100% 100%
src/lib/gps/realtimeTimer.ts 90.07% 100% 76.04%
src/lib/gps/sessionGate.ts 100% 100% 100%
src/lib/gpsQualityFilter.ts 100% 100% 96.77%
src/lib/graphPrefsStorage.ts 100% 100% 100%
src/lib/i18n/config.ts 100% 100% 100%
src/lib/i18n/format.ts 100% 100% 85.71%
src/lib/i18n/pluginLocales.ts 0% 0% 100%
src/lib/i18n/seedUtils.ts 100% 100% 90.47%
src/lib/imageCrop.ts 12.19% 10% 0%
src/lib/insta360/ipc.ts 0% 0% 0%
src/lib/insta360/nativePlayer.ts 90.09% 82.75% 77.77%
src/lib/insta360/playerClock.ts 100% 100% 90%
src/lib/insta360/pose.ts 100% 100% 100%
src/lib/insta360/resumeSession.ts 100% 100% 100%
src/lib/insta360/types.ts 100% 100% 100%
src/lib/iracingParser.ts 91.93% 80% 76.47%
src/lib/kartStorage.ts 100% 75% 100%
src/lib/lapAlignment.ts 100% 80% 76.92%
src/lib/lapCalculation.ts 96.31% 100% 91.09%
src/lib/lapDelta.ts 99.2% 100% 85.07%
src/lib/lapOverlays.ts 100% 100% 86%
src/lib/lapSnapshot.ts 100% 100% 86.36%
src/lib/lapSnapshotStorage.ts 100% 83.33% 100%
src/lib/latestGate.ts 100% 100% 100%
src/lib/leaderboardBrowse.ts 100% 92.3% 80.76%
src/lib/leaderboardHandoff.ts 100% 100% 100%
src/lib/leaderboardSession.ts 97.67% 83.33% 67.85%
src/lib/leaderboardTypes.ts 100% 100% 100%
src/lib/logFileType.ts 100% 100% 100%
src/lib/loggers/alfano/alfanoConnection.ts 100% 100% 100%
src/lib/loggers/alfano/ipc.ts 100% 100% 100%
src/lib/loggers/bleDetails.ts 11.11% 11.11% 100%
src/lib/loggers/doveslogger/dovesloggerConnection.ts 75% 73.33% 100%
src/lib/loggers/doveslogger/firmwareInfo.ts 100% 100% 100%
src/lib/loggers/doveslogger/ipc.ts 100% 91.66% 85.71%
src/lib/loggers/downloadFlow.ts 100% 100% 100%
src/lib/loggers/errors.ts 100% 100% 100%
src/lib/loggers/fileNaming.ts 100% 100% 100%
src/lib/loggers/fledglingConnection.ts 100% 100% 100%
src/lib/loggers/index.ts 100% 100% 100%
src/lib/loggers/mychron/ipc.ts 100% 100% 100%
src/lib/loggers/mychron/mychronConnection.ts 100% 100% 100%
src/lib/loggers/native/ipc.ts 100% 100% 100%
src/lib/loggers/native/owner.ts 100% 100% 100%
src/lib/loggers/progress.ts 94.73% 100% 96.15%
src/lib/loggers/types.ts 100% 100% 100%
src/lib/mapMarker.ts 100% 100% 100%
src/lib/motecParser.ts 89.61% 81.48% 55.71%
src/lib/nativeBytes.ts 89.47% 100% 62.5%
src/lib/nativeUnavailable.ts 100% 100% 100%
src/lib/nativeVideoExport.ts 100% 66.66% 86.66%
src/lib/nativeVideoStore.ts 51.21% 71.42% 40.9%
src/lib/nativeVideoStoreEvents.ts 100% 100% 100%
src/lib/navBack.ts 100% 100% 71.42%
src/lib/nmeaParser.ts 85.62% 92.85% 71.22%
src/lib/noteStorage.ts 100% 80% 100%
src/lib/offlineReadiness.ts 100% 100% 100%
src/lib/offlineWarmup.ts 69.09% 66.66% 65.62%
src/lib/overlayCanvasRenderer.ts 63.28% 71.42% 49.75%
src/lib/parseReport.ts 69.56% 87.5% 74.35%
src/lib/parserUtils.ts 100% 100% 98.93%
src/lib/passwordStrength.ts 100% 100% 100%
src/lib/pendingCheckout.ts 58.82% 25% 100%
src/lib/persistentStorage.ts 100% 100% 90.9%
src/lib/platform.ts 100% 88.88% 93.1%
src/lib/profanity.ts 100% 100% 75%
src/lib/pwaInstall.ts 75% 80% 79.16%
src/lib/referenceUtils.ts 100% 100% 90.66%
src/lib/rssFeed.ts 0% 100% 100%
src/lib/sampleData.ts 100% 100% 100%
src/lib/satelliteImagery.ts 100% 100% 90%
src/lib/setupHistory.ts 94.69% 100% 75.91%
src/lib/setupRevision.ts 100% 100% 95.65%
src/lib/setupRevisionStorage.ts 92.45% 79.16% 91.66%
src/lib/setupStatus.ts 100% 100% 100%
src/lib/setupStorage.ts 81.35% 58.62% 100%
src/lib/shareSession.ts 100% 100% 100%
src/lib/sim/simClient.ts 0% 0% 0%
src/lib/sim/simPlayback.ts 100% 100% 97.22%
src/lib/sim/simSession.ts 100% 100% 100%
src/lib/speedBounds.ts 94.28% 66.66% 87.17%
src/lib/speedEvents.ts 86.56% 100% 76%
src/lib/speedHeatmap.ts 100% 100% 100%
src/lib/sprintCourse.ts 100% 100% 92.3%
src/lib/submittedTracksStorage.ts 0% 0% 0%
src/lib/supportTrackData.ts 100% 100% 75%
src/lib/templateEdit.ts 97.82% 88.88% 86.95%
src/lib/templateStorage.ts 93.54% 69.23% 100%
src/lib/trackStorage.ts 15.57% 26.78% 17.77%
src/lib/trackSubmission.ts 100% 100% 92.64%
src/lib/trackUtils.ts 100% 100% 97.05%
src/lib/ubxParser.ts 99% 100% 89.58%
src/lib/units.ts 100% 100% 100%
src/lib/updateFlow.ts 100% 100% 100%
src/lib/utils.ts 100% 100% 100%
src/lib/vboParser.ts 91.71% 100% 77.83%
src/lib/vehicleHistory.ts 97.29% 100% 80%
src/lib/vehicleStorage.ts 100% 75% 100%
src/lib/versionCheck.ts 19.44% 12.5% 44.73%
src/lib/videoExport.ts 0% 0% 0%
src/lib/videoExportTarget.ts 100% 100% 84.61%
src/lib/videoFileStorage.ts 100% 76% 61.11%
src/lib/videoPlaylist.ts 95.71% 100% 90%
src/lib/videoStorage.ts 100% 76.92% 77.77%
src/lib/videoTimeline.ts 100% 100% 100%
src/lib/wakeLock.ts 100% 80% 100%
src/lib/weatherCacheStorage.ts 100% 100% 100%
src/lib/weatherService.ts 23.8% 35.29% 25.89%
src/lib/xrk/xrkClient.ts 3.57% 0% 0%
src/lib/xrk/xrkConfig.ts 100% 100% 100%
src/lib/xrk/xrkImporter.ts 70% 75% 80%
src/lib/xrk/xrkMapping.ts 98.7% 100% 87.5%
src/lib/xrk/xrkResample.ts 100% 100% 94%
src/lib/xrk/xrkTypes.ts 100% 100% 100%
src/lib/xrk/xrkWorker.ts 0% 0% 0%
src/plugins/cloud-sync/accountDeletion.ts 100% 100% 100%
src/plugins/cloud-sync/accountExport.ts 0% 0% 0%
src/plugins/cloud-sync/accountImport.ts 13.51% 25% 26.08%
src/plugins/cloud-sync/activeUser.ts 100% 100% 100%
src/plugins/cloud-sync/autoSync.ts 0% 0% 0%
src/plugins/cloud-sync/cloudClient.ts 55% 25% 87.5%
src/plugins/cloud-sync/CloudLogsPanel.tsx 0% 0% 0%
src/plugins/cloud-sync/DataPrivacyPanel.tsx 0% 0% 0%
src/plugins/cloud-sync/exportManifest.ts 100% 100% 100%
src/plugins/cloud-sync/FileDeleteToggle.tsx 0% 0% 0%
src/plugins/cloud-sync/fileSync.ts 91.66% 85.71% 100%
src/plugins/cloud-sync/FileSyncToggle.tsx 0% 0% 0%
src/plugins/cloud-sync/index.ts 0% 0% 0%
src/plugins/cloud-sync/LapSnapshotsPanel.tsx 0% 0% 0%
src/plugins/cloud-sync/leaderboardClient.ts 8.92% 10.52% 1.56%
src/plugins/cloud-sync/leaderboardSubmission.ts 100% 100% 94.11%
src/plugins/cloud-sync/LeaderboardSubmitPanel.tsx 0% 0% 0%
src/plugins/cloud-sync/localUsage.ts 100% 100% 50%
src/plugins/cloud-sync/merge.ts 100% 100% 100%
src/plugins/cloud-sync/pendingSync.ts 100% 100% 100%
src/plugins/cloud-sync/postsClient.ts 0% 0% 0%
src/plugins/cloud-sync/profile.ts 61.53% 50% 63.33%
src/plugins/cloud-sync/publicProfile.ts 6.25% 28.57% 0%
src/plugins/cloud-sync/publicShare.ts 100% 100% 100%
src/plugins/cloud-sync/publicVehicleSync.ts 92.85% 100% 78.57%
src/plugins/cloud-sync/sessionShare.ts 92.15% 76.92% 91.66%
src/plugins/cloud-sync/setupRevisionTombstones.ts 100% 100% 83.33%
src/plugins/cloud-sync/ShareFileButton.tsx 0% 0% 0%
src/plugins/cloud-sync/ShareSessionDialog.tsx 0% 0% 0%
src/plugins/cloud-sync/shareState.ts 100% 100% 100%
src/plugins/cloud-sync/shareToken.ts 100% 100% 100%
src/plugins/cloud-sync/snapshotSync.ts 97.22% 87.5% 75%
src/plugins/cloud-sync/snapshotTombstones.ts 100% 100% 100%
src/plugins/cloud-sync/StoragePanel.tsx 0% 0% 0%
src/plugins/cloud-sync/storageTypes.ts 100% 100% 100%
src/plugins/cloud-sync/storeAccessors.ts 100% 100% 91.66%
src/plugins/cloud-sync/syncEngine.ts 99.1% 91.3% 81.63%
src/plugins/cloud-sync/syncStores.ts 100% 100% 100%
src/plugins/cloud-sync/trackAutoSubmit.ts 100% 100% 78.57%
src/plugins/fileSources.ts 0% 0% 100%
src/plugins/index.ts 0% 0% 0%
src/plugins/mounts.ts 71.42% 75% 100%
src/plugins/native-storage/deviceVideos.ts 100% 100% 100%
src/plugins/native-storage/DeviceVideosPanel.tsx 0% 0% 0%
src/plugins/native-storage/index.ts 0% 0% 0%
src/plugins/panels.ts 75% 83.33% 100%
src/plugins/PluginMount.tsx 0% 0% 0%
src/plugins/PluginPanelHost.tsx 0% 0% 0%
src/plugins/registry.ts 77.27% 50% 100%
src/plugins/storage.ts 90.32% 70.83% 66.66%
src/plugins/tools/i18n.ts 0% 0% 100%
src/plugins/tools/index.ts 0% 0% 100%
src/plugins/tools/laptimer/lapTimerSession.ts 89.06% 80% 84.37%
src/plugins/tools/laptimer/LapTimerTool.tsx 0% 0% 0%
src/plugins/tools/laptimer/useLapTimer.ts 0% 0% 0%
src/plugins/tools/pill-alignment/CalibrationPanel.tsx 0% 0% 0%
src/plugins/tools/pill-alignment/envelope.ts 100% 100% 90.9%
src/plugins/tools/pill-alignment/EnvelopePlot.tsx 0% 0% 0%
src/plugins/tools/pill-alignment/FindSetupPanel.tsx 0% 0% 0%
src/plugins/tools/pill-alignment/inverse.ts 95.83% 100% 88.52%
src/plugins/tools/pill-alignment/model.ts 100% 100% 92.85%
src/plugins/tools/pill-alignment/OverheadToeView.tsx 0% 0% 100%
src/plugins/tools/pill-alignment/PillAlignmentTool.tsx 0% 0% 0%
src/plugins/tools/pill-alignment/PillDial.tsx 0% 0% 0%
src/plugins/tools/pill-alignment/profiles.ts 100% 100% 84.61%
src/plugins/tools/pill-alignment/toe.ts 100% 100% 100%
src/plugins/tools/seat-position/model.ts 100% 100% 60%
src/plugins/tools/seat-position/SeatDiagram.tsx 0% 0% 0%
src/plugins/tools/seat-position/SeatPositionTool.tsx 0% 0% 0%
src/plugins/tools/shared/NumRow.tsx 0% 0% 0%
src/plugins/tools/toolList.ts 0% 0% 100%
src/plugins/tools/ToolsLandingTile.tsx 0% 0% 0%
src/plugins/tools/ToolsPanel.tsx 0% 0% 0%
src/plugins/types.ts 100% 100% 100%
src/types/racing.ts 100% 100% 100%

@supabase

supabase Bot commented Aug 29, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project tdxloldxzvnzdivdazzd because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

claude and others added 22 commits August 31, 2026 04:59
…ls, data-time sweep

The export renderer becomes the single drawing surface: per-type draw
functions are exported with an OverlayLayout, measureOverlay() shares the
draws' box math, OverlayLabels carries the user-facing strings (translated
by React callers, English defaults elsewhere), and the sector state machine
moves to sectorUtils.computeSectorDisplayStates — including completedAtMs,
which drives the completion sweep in DATA time so it lands on the same
frames in preview and export. Fidelity that existed on only one side (graph
grid lines, needle/dot glow, bubble center dot, pace SLOW/FAST labels,
preview's map framing and pace-bar stacking) is now in the shared draws.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LzfeVvASfX1hefekpHPUq
…omponents

OverlayCanvas is the one preview host — a canvas sized by measureOverlay and
drawn by the same per-type functions the export uses, with the theme glow
filter and (for the formerly backdrop-blurred DOM widgets) a backdrop blur
as the only CSS extras. VideoPlayer's per-type switch collapses to it; the
nine overlay components and the wall-clock sparkle CSS are gone. The export
context now threads the caller's translated labels, so exported videos stop
being English-only. Changelog + plan doc.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LzfeVvASfX1hefekpHPUq
…enderer-unification

plan 0023: one overlay scene renderer for preview + export (native phase 0)
startNativeVideoExport stages the job over LapWing's video_export_* IPC —
begin (same quality mapping as the web path), the source blob in 8 MB
raw-body chunks, overlay layers from the plan-0023 unified renderer at
15 Hz telemetry cadence (PNG at output resolution, timestamps relative to
export start) — then drives the hardware transcode with streamed progress
and hands the MP4 to the existing save/share flows. Resolves null (and
the WebView exporter takes over unchanged) off the shell, on the desktop
stub's `unsupported:` sentinel, on an older shell, or for multi-chunk
playlists. VideoPlayer tries native first; web/desktop byte-identical.

The renderer's per-frame invariant work is now memoized per session via
WeakMap on array identity: per-source ranges (validated against the
pace/braking arrays for special sources), map bounds, the pace scale, and
the digital widget's double value-resolve. Speeds up preview, the WebView
exporter, and native layer generation alike; draw output is bit-identical
(existing tests unchanged, +1 counting-fixture memo test).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LzfeVvASfX1hefekpHPUq
…port-bridge

plan 0024: native export bridge — hardware-speed overlay export in the shell
"Save to device" inside the shell fell into downloadBlob() — an
<a download href="blob:"> click the Android WebView has no download
handler for — so the export completed and the file went nowhere. On native,
destination "device" now means the gallery: the bridge calls
video_export_save and the shell copies the finished MP4 straight out of
its job dir into MediaStore Movies/LapWing (never through the WebView);
the dialog's button reads "Save to Gallery" on native and VideoPlayer
toasts export.savedToGallery. New ExportCallbacks.onSavedToDevice; the
web path is untouched.

The web remembers a session's video via a Chrome-only FileSystemFileHandle;
the shell had nothing. nativeVideoStore.ts is its equivalent over the
shell's video_store_* IPC: loadRecording copies a single-file recording
into the shell's store in the background (8 MB raw-body chunks; the blob
URL keeps playing meanwhile), and the restore effect tries the store before
the IndexedDB fallback — the <video> then streams from app storage over the
asset protocol. The key rides VideoSyncState.nativeStoredKey and the export
bridge passes it as sourceKey, so exporting a remembered video skips the
source upload entirely (a stale key silently falls back to uploading).

i18n keys in all seven locales; plan 0024 + changelog updated. Gates:
2994 tests, tsc, eslint, build.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LzfeVvASfX1hefekpHPUq
…tore-gallery

plan 0024 follow-up: save to gallery + the remembered video in the shell
…) + visible export failures

"Save to Gallery" on-device stuttered for a second and went idle — no
file, no message. The shell's upload commands took raw invoke bodies
(invoke(cmd, bytes, { headers })), and Tauri never uses that IPC on
Android: the WebView can't hand the app a request body, so Tauri falls
back to window.ipc.postMessage and serializes a Uint8Array payload as a
JSON array of numbers, which the raw-body commands rejected. The first
chunk of every export (and of every video-store copy) failed, and
VideoPlayer.onError only console.error'd it.

- src/lib/nativeBytes.ts: bulk bytes now cross the bridge as base64
  strings in ordinary JSON args, in 4 MB chunks — blobToBase64 via
  FileReader.readAsDataURL (native, off the main thread), with a pure
  chunked-btoa fallback (bytesToBase64). Unit-tested, including the
  FileReader path through a stubbed reader.
- nativeVideoExport.ts / nativeVideoStore.ts send { jobId, offset|tMs,
  data } / { key, offset, data } to the matching LapWing shell. Staging
  progress now weights an overlay layer like one chunk push, which is
  what it costs.
- Export failures are visible: onError toasts export.failed (7 locales)
  with the shell's message, destructive variant — the WebView exporter's
  errors get the same toast.
- Plan 0024 doc: follow-up 2 section; CHANGELOG under 4.2.0 Fixed.

Gates: tsc -b, eslint, vitest (3002 tests, +7).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LzfeVvASfX1hefekpHPUq
…chunk-transport

plan 0024 follow-up 2: base64 chunk bridge (Android has no raw bodies) + visible export failures
The shell's copy per session's video (the remembered video) is the one
thing in the app that grows by gigabytes, and nothing pruned it — the
delete command existed but no UI called it.

- New first-party plugin src/plugins/native-storage: on the native shell it
  contributes a "Videos on this device" panel to the Profile tab (order 3,
  between the account meters and lap snapshots) — every stored copy with
  file name, owning session, size and date, a per-copy delete with inline
  confirm, and a clear-all, each toasting the bytes freed. On a shell that
  predates the listing the card says it needs a newer build. Contributing
  the panel is also what makes the Profile tab exist in a native build
  without the cloud plugin.
- nativeVideoStore.ts: listNativeStoredVideos / removeNativeStoredVideo /
  clearNativeVideoStore over LapWing's video_store_list / _remove / _clear;
  removals fire a native-video-store-changed window event.
- useVideoSync listens for it: a session playing from the deleted copy (a
  restore) unloads its video exactly as deleting the in-app stored video
  does; one still holding the picked blob only loses the export shortcut.
- Pure parts (ordering, totals, session label, applying a removal, byte
  formatting) in deviceVideos.ts with tests; the bridge functions tested
  against a mocked invoke. 7 locales. Plan doc + changelog.

tsc/eslint clean; 3018 tests pass (+24).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LzfeVvASfX1hefekpHPUq
Android app only: "From Insta360 camera" joins the camera's Wi-Fi, lists its
recordings and plays one without downloading it — the LapWing shell's
native player streams the file off the camera and serves the frames as an
MJPEG stream the player shows in an <img>. NativePlayerElement implements
the <video> subset useVideoSync relies on (extrapolated clock between the
shell's reports, seek watchdog), so sync lock, offset, scrubbing and the
overlay canvas work on the stream unchanged. 360° recordings get a
drag-to-point layer with lock/reset; export is disabled for streams.

- lib/insta360: types, ipc (insta360_* over the lazy Tauri loader), pose +
  playerClock (pure, tested), nativePlayer (tested with a mocked bridge)
- components/insta360: import dialog (Wi-Fi prefix/password, connect, list,
  load, disconnect), 360° view layer
- useVideoSync: source-agnostic VideoSurface ref, loadCameraRecording,
  unloadVideo, nativeSource; VideoPlayer: <img> surface, camera buttons,
  360° lock/reset; video namespace strings in all seven languages
- docs/plans/0025, CHANGELOG, CLAUDE.md map

Companion: LapWing plan 0002 (tauri-plugin-insta360 + docs/insta360.md).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sK5Tb8pa8bZEtUNiprYiH
…ore-ui

plan 0024 follow-up 3: see and clear the videos the native app keeps
…camera-preview

plan 0025: stream an Insta360 camera recording into the player (native)
…ne cache

Demoing at a track on an iPhone with no signal came up empty. The service
worker is fine — the whole app shell is precached and a warm Safari tab does
run offline. Two WebKit policies take it away:

1. Safari deletes all script-writable storage for a site not interacted with
   for ~7 days of browser use. That sweep takes the SW registration, the Cache
   API precache AND our IndexedDB sessions, silently. A phone that last opened
   LapWing before the previous race weekend arrives with nothing cached.
2. iOS never fires `beforeinstallprompt`, and InstallPrompt was built entirely
   around that event — so on the one platform that most needs the nudge, the
   card rendered nothing at all.

A Home Screen install is the documented escape hatch (far larger quota, and an
idle counter that only advances on days the app is opened), and it has its own
storage partition — so the instructions must end with "open it once while you
still have signal", or the user installs an icon that is just as empty.

- lib/pwaInstall.ts: pure detection of installed / ios-manual / prompt-capable,
  including the iPad's desktop-class UA (MacIntel + maxTouchPoints > 1) without
  catching real Macs. Dismissal moves from a session-only flag to a 30-day
  localStorage snooze — this hint is worth repeating, not silencing.
- lib/persistentStorage.ts: request the Storage API's persistent mode once at
  startup so the cache and IndexedDB stop being evictable. Fire-and-forget;
  every outcome is a return value, never a throw.
- InstallPrompt: same card, plus the iOS share-sheet steps and a one-line why.
  Strings moved to common:install.* and translated across all six locales.

Vitest covers both new modules (23 cases). Also noted in the plan: the
`app-html` NetworkFirst route is dead code — Workbox matches the precache
NavigationRoute first — left as-is since precache-first is the more
offline-robust behaviour, but the config reads misleadingly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKLfticLxeFfKK2WFDNYdd
The reported bug — load the app at a track, airplane mode, refresh, nothing
loads — is not iOS-specific and not eviction. Workbox's precache install is
all-or-nothing: one failed request rejects `install`, the service worker is
discarded, and NOTHING is cached. Reproduced in Chromium against a real dist
build by cutting the network 4s into the first load:

  SW state at cut:      { installing: 'installing', active: null }
  cache entries:        53 of 267
  after install failed: { installing: null, waiting: null, active: null }
  offline reload →      net::ERR_INTERNET_DISCONNECTED

The install was 10.16 MB across 267 entries, and the bundled demo datalogs
alone were 44% of it. Seven entries were precached twice on top of that
(includeAssets listed files globPatterns already matched), so every install
re-downloaded the 1.25 MB sample .nmea. On a congested cell connection that is
a long window in which any hiccup costs the entire offline cache, silently.

- vite.config.ts: DEFERRED_ASSET_DIRS (samples/, loggers/) is one source of
  truth feeding globIgnores, a CacheFirst runtime route, and a build-emitted
  offline-assets.json (generated from the filesystem, so a new logger photo
  needs no code change). Install drops to 257 entries / 4.98 MB, almost
  entirely the app itself. includeAssets cut to robots.txt — everything else
  was already covered, which is where the duplicates came from.
- lib/offlineWarmup.ts: pulls the deferred assets in afterwards, per-URL, so a
  failure costs that asset and retries next visit instead of everything. Writes
  via `cache.add`, NOT fetch: on a first visit the worker is registered but not
  yet controlling the page, so page-initiated requests bypass it and cached
  nothing at all — the integration test caught exactly that. `addAll` would
  have reintroduced the all-or-nothing behaviour this plan removes.
- lib/offlineReadiness.ts + useOfflineReadiness + a Settings row: the deeper
  failure was that none of this was visible. You can now check readiness in the
  paddock, and finish the download on demand, instead of finding out on track.

Verified end-to-end (scratchpad harness, real dist over a throttled server):
killing every deferred-asset request outright now leaves the worker activated,
the shell cached (254 entries) and the offline reload rendering — the same
failures that previously discarded the whole install. 18 new Vitest cases.
lint, typecheck, 3064 tests and build all green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKLfticLxeFfKK2WFDNYdd
…i-cache-x5ny5i

plan 0026: iOS install hint + persistent storage (offline hardening, part 1)
…resilience

plan 0027: stop a flaky connection from wiping the whole offline cache
… no "any network" picker

The dialog offered "leave empty to pick from every network in range", but
Android rejects a Wi-Fi specifier that matches everything, so pressing
Connect with the field empty failed with "cannot set match-all pattern for
specifier". The shell now reads an empty field as "bind to the Wi-Fi this
phone is already on" (LapWing side), which is the mode that actually
works — so the hints have to tell the user to join the camera's network in
the phone's settings first, and that a typed name is what brings up the
Android picker. The password only applies to the typed-name path.

Strings only, in all seven locales, plus the matching sentence in the
unreleased 4.2.0 entry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sK5Tb8pa8bZEtUNiprYiH
…nt-network

Insta360: correct the Wi-Fi hints — Android has no "any network" picker
Setup history only ever showed the states assigned to a session, because a
revision was frozen solely on assignment and the orphan sweep removed any
revision no session referenced. Plain edits left no trace.

- Freeze a content-addressed revision on every setup save (add + update in
  useSetupManager); dedup makes an unchanged save a no-op.
- Narrow the orphan prune: a revision is swept only when no session references
  it AND its live setup has been deleted, so a setup's edit history persists.
- buildSetupHistory gains a `view` filter ("used" default | "all"), an
  `entry.used` marker that follows the kart/course filter, and used/total
  counts. Diffs stay relative to the previous displayed entry.
- SetupHistoryPanel: Used / All segmented toggle with counts, a Used badge on
  cards in the All view, and an empty-state hint pointing at All when only
  unused edits exist. New keys in all seven drawer locales.
- Tests for the new prune rule and the view modes; docs, CHANGELOG, plan 0028.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014945fnvJ5WPFRCtTszkdnS
claude added 7 commits October 2, 2026 00:59
loadRecording's background storeNativeVideo copy (and the async
tryLoadNativeStored lookup) set nativeStoredKey whenever it resolved, so
a copy started for session A that landed after switching to session B
attached A's key to B — and VideoPlayer then exported A's footage under
B's overlays. A small latest-wins gate (lib/latestGate) is invalidated on
every session/video change; stale results are dropped.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZ3Pt89yubdPtYn5Wc8fjW
useOfflineReadiness only checked "serviceWorker" in navigator, which
Android's WebView exposes even though the native shell never registers
a worker (main.tsx), so Settings read "not ready" forever. A new
"native" readiness state is checked first via isNativeApp(); the row
shows it as done with no download button, and the hook skips the
deferred-asset counting there. i18n key added in every locale.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZ3Pt89yubdPtYn5Wc8fjW
deleteNativeStoredVideo had no callers, so every session deleted in the
native app orphaned its stored video (gigabytes) in app data, and the
function was dead code. fileStorage.deleteFile already cascades the
session's per-file weather cache; it now drops the native copy the same
best-effort way, covering both the file manager and cloud-sync's
remove-local path.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZ3Pt89yubdPtYn5Wc8fjW
Moving samples/ and loggers/ out of the precache also moved them out of
Workbox's per-file revisioning, and their runtime route is CacheFirst
with no expiry, so an updated sample log or logger photo never reached
a device that had already cached it. offline-assets.json now carries a
content hash per file; the warm-up stamps each stored response with it
(X-Asset-Revision, fetched with cache: reload), treats a mismatched or
unstamped entry as missing, and prunes paths the build no longer ships
(never against an empty manifest). The readiness count uses the same
check. Plan 0027 and CLAUDE.md updated.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZ3Pt89yubdPtYn5Wc8fjW
nativeVideoExport.ts carried the IPC sequencing, the stale stored-key
retry and the fallback-vs-error classifier behind a 4.2.0 Fixed entry
with no tests (release review T1). Pins: declines (web, multi-chunk,
no dimensions), the begin-failure classifier as it stands today
(unsupported:/unknown/not found/not allowed fall back; anything else
reports), chunked upload -> run -> collect/save -> dispose order, the
quality mapping, 15 Hz overlay layers, mid-job error + dispose, cancel,
and the stored-source path including the retry without the key.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZ3Pt89yubdPtYn5Wc8fjW
src/assets/fonts/ carries the two Archivo woff2 files but not their
licence; OFL 1.1 condition 2 requires the copyright notice and licence
to travel with redistributed copies. Add OFL.txt (Archivo's copyright
line + the verbatim OFL 1.1 text) next to the fonts and point the
README credit at it. Credits already list Archivo in both places.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZ3Pt89yubdPtYn5Wc8fjW
CLAUDE.md, docs/subsystems.md and src/plugins/README.md had no mention
of the native-storage plugin, nativeVideoExport / nativeVideoStore /
nativeBytes, persistentStorage or pwaInstall (release review DOC-4,
Rule 5). Adds them to the architecture map and first-party plugin list,
a subsystems section for the native video bridge and one for offline
durability, and the platform-gated plugin example. Plans 0022 and 0024
record this branch's review fixes (course-less selection clearing,
range memo keyed on data sources, latest-wins key gate, delete cascade).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZ3Pt89yubdPtYn5Wc8fjW
@TheAngryRaven TheAngryRaven mentioned this pull request Oct 2, 2026
8 tasks done
claude added 11 commits October 2, 2026 01:10
Release review A3: the history load had no catch, so a failed IndexedDB
read left the panel on "Loading…" forever, and a rollback or duplicate
that threw was swallowed (the busy flag cleared, nothing said why). The
panel now shows an error with a Retry button when the load fails, and a
failed rollback/duplicate raises an error toast instead of a silent
no-op. Strings added in every locale.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZ3Pt89yubdPtYn5Wc8fjW
Release review A4 + SEC-1.

A4: an unknown TYPE character in one GPS9 stream threw out of
extractGpsPayload and aborted the whole GoPro import. A camera writes one
payload per second, so a single corrupt one should cost one second, not
the session. Each stream decode is now isolated (a bad GPS9 can still
fall back to GPS5 in the same payload), and readComplex rejects a TYPE
wider than its record instead of reading into the next one.

SEC-1: a uniform-size stsz box is just a size and a uint32 count, so a
hostile file could ask new Array() for ~4 billion entries and OOM the
tab (the regression test OOMs the worker without the fix). Every sample
occupies `size` bytes of the file, so the count is capped at
fileSize / size. The other sample tables were already bounded by their
box length.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZ3Pt89yubdPtYn5Wc8fjW
Release review B6: the shell runs one native player and
insta360_player_close carries no player id, so an earlier
NativePlayerElement closing late (its close() after a newer one opened,
or an open that resolved after it was closed) killed whichever stream
was live. Elements now record which one last opened the player and only
that one may release it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZ3Pt89yubdPtYn5Wc8fjW
Release review T2: the check that decides "this shell can't do it, fall
back quietly" was copied into nativeVideoExport.ts, nativeVideoStore.ts
and insta360/ipc.ts, untested, and matched any message containing
"unknown", "not found" or "not allowed" — so a real failure such as
"file not found" or "unknown error" could be mistaken for an old shell
and silently take the fallback path. It now lives once in
lib/nativeUnavailable.ts and only accepts the `unsupported:` sentinel
prefix or Tauri's own missing-command / ACL rejection shapes. The
Insta360 copy had no callers and is removed (insta360SdkInfo already
treats any error as unavailable). nativeVideoExport's exports and its
handling of every real shell error string are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZ3Pt89yubdPtYn5Wc8fjW
Release review T3: the runtime-cache route matched a hand-inlined
/^\/(?:samples|loggers)\// because workbox-build serializes the
urlPattern callback by its source text, so a closure over the constant
would be empty in the worker. Adding a directory to the list without
editing the regex would precache-exclude it and then never cache it at
runtime. The list moves to scripts/deferredAssets.ts with a builder that
derives the regex from it and splices it into the matcher's source; both
are unit-tested, including the toString round-trip workbox does. The
generated service worker route is byte-identical.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZ3Pt89yubdPtYn5Wc8fjW
Release review T4: the order a fresh load resolves in (confident course,
then drag runs, then waypoint, then the nearest-track prompt) and the
rule that a restored track/course shadows a saved drag tag lived only
inline in useDataLoader. Both are extracted as pure functions
(chooseAutoDetectPath, shouldRestoreDragSession) and covered, including
that drag detection never runs when a course already matched. Extract
only: the branch bodies are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZ3Pt89yubdPtYn5Wc8fjW
Release review T5: useOfflineReadiness's only logic not already in the
tested lib/offlineReadiness was the "how many deferred assets are
cached" count. It moves there as countCachedAssets (injectable cache,
a throwing lookup counts as a miss, no Cache API means zero) with
tests; the hook keeps only browser plumbing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZ3Pt89yubdPtYn5Wc8fjW
Release review PERF-3: the only font-brand use (the landing hero) is
italic, so the upright variable face was ~35 KB in every offline
install that no page ever drew. Only the italic face ships now; the
index.css comment says how to bring the upright one back. Precache
4251.83 -> 4222.66 KiB (together with the PERF-2 split).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZ3Pt89yubdPtYn5Wc8fjW
Release review PERF-2 (+ REL-5). The eager graph statically imported
the GoPro MP4/GPMF extractor (via FileImport and datalogParser), the
Insta360 camera player and the native video-store bridge (via
useVideoSync), so every web visitor downloaded code only a GoPro import
or the LapWing shell ever runs.

- goproDetect.ts holds the cheap name/ftyp gates the routers need on
  every import; goproImport (and mp4Boxes/gpmf behind it) is now
  dynamic-imported when a video actually arrives.
- useVideoSync loads nativeVideoStore and NativePlayerElement on first
  use (type-only imports otherwise); the store's change-event constant
  moves to nativeVideoStoreEvents.ts so the hook can listen without
  pulling in the bridge.
- VideoPlayer (lazy GraphViewTab chunk) loads the Insta360 IPC and the
  native export bridge only inside the native app.

Main chunk: 754.68 -> 738.13 kB raw, 231.04 -> 225.82 kB gzip. None of
the insta360_*/video_store_*/GPMF code remains in it.

Also drops the two leftover console.log calls in VideoPlayer's save
paths (Rule 7).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZ3Pt89yubdPtYn5Wc8fjW
- REL-3/DOC-2: fold the stray second "### Added" block (drag mode) into
  the first one and cite plan 0022.
- REL-4: the read-only banner fix moves from Changed to Fixed.
- Fixed entries for this pass's user-visible fixes (setup history
  errors, malformed/hostile GoPro files, Insta360 stream replacement).
- DOC-5: plan 0022 is done, not IN PROGRESS.
- DOC-6: CLAUDE.md's video-overlays entry described the per-widget
  *Overlay components plan 0023 deleted; it now names the single scene
  renderer.
- DOC-7: README marks .360 GoPro import experimental, matching plan
  0029's "untested".
- CLAUDE.md / plan 0029 updated for goproDetect, the dynamic GoPro
  import, the deferredAssets module and the italic-only Archivo.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZ3Pt89yubdPtYn5Wc8fjW
The README already flags .360 (MAX/Fusion) as untested; the in-app supported-files
list promised it unqualified in every language.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZ3Pt89yubdPtYn5Wc8fjW
@TheAngryRaven TheAngryRaven mentioned this pull request Oct 2, 2026
9 tasks done
TheAngryRaven and others added 4 commits October 2, 2026 21:02
4.2.0 release review: High fixes (version bump, stuck Insta360 dialog)
…jc-medium

4.2.0 release review: Medium fixes
Resolves the conflicts left by #450 and #451 landing first. CHANGELOG keeps
both Fixed lists. useDataLoader tests keep the load-precedence tests and the
course-clearing ones. countCachedAssets (plan 0027) now takes a predicate, so
the readiness hook keeps the revision-aware isDeferredAssetCached check from
#451 while the counting stays in the tested lib.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZ3Pt89yubdPtYn5Wc8fjW
@TheAngryRaven
TheAngryRaven marked this pull request as ready for review October 3, 2026 03:29

TheAngryRaven commented Oct 3, 2026 •

Copy link
Copy Markdown
Owner Author

Release post for 5.0.0, ready to paste into the admin Updates tab. Use the copy button on the block below to get the raw markdown.

Suggested title: LapWing 5.0: drag mode, GoPro import, and a new look
Suggested tags: web update, release

LapWing 5.0 is out. It's our biggest update yet: a whole new way to time runs, a new way to get data in without a logger, a fresh coat of paint, and a lot of work making the app dependable when you're at a track with no signal.

## Drag mode

Drop in a log from a drag strip, or from any venue the app doesn't know, and LapWing now finds your standing-start runs on its own. Pick **1/8 mile, 1000 ft or 1/4 mile** and every pass gets a time-slip style breakdown: ET at your scoring distance, plus 60 ft, 330 ft, 660 ft and 1000 ft splits.

Passes where you lifted early stay in the list, marked incomplete, with the splits they did reach. They never count as your fastest run. Your chosen distance is remembered for each file, and you can switch it from the session header.

## Your GoPro is already a datalogger

Every GoPro since the HERO5 records GPS inside the video file. LapWing can now read it. Drop a GPS-tagged GoPro video on the home page (select every chapter of a split recording together) and the embedded GPS becomes a normal session, with laps, sectors and the map.

Only the small telemetry track is read; your video is never loaded whole or uploaded. Because the footage and the GPS come from the same camera clock, the video opens in the player **already synced**. HERO5 through HERO10 and HERO11 and newer are supported (the HERO12 has no GPS). `.360` files from MAX and Fusion cameras are experimental for now.

## Setup history that keeps up with you

Every time you save a setup, LapWing now keeps a copy, so the history shows every change you made during a track day, newest first, not just the ones you tagged to a session.

- A **Used / All** toggle switches between setups you actually ran and every saved version.
- **Roll back** returns you to the last setup you ran, in one tap, when your experiments didn't work out.
- **Duplicate** turns any setup you ran into a new one.

Untagged edits stay on your device for 3 days, and the latest one is always kept.

## A new look

LapWing now wears its own brand: the LAPWING wordmark, a violet palette in both light and dark mode (dark mode is now a deep indigo), a new app icon, and a refreshed home page. Your charts and speed colours haven't changed.

## Works offline, for real this time

The app used to save itself for offline use in one big all-or-nothing download. Lose signal partway through and nothing was saved, with no warning. Now:

- The core app is less than half the size it was and saves first, so it works offline on its own. Sample data and logger photos follow in the background, and if one fails, only that piece tries again later.
- **Settings → Offline readiness** tells you whether this device is ready to go with no signal, with a **Finish download** button. Check it in the paddock, not on the grid.
- LapWing asks your browser to keep its offline data, so it isn't cleared when space runs low.
- **iPhone and iPad** users now see install instructions (Share → *Add to Home Screen*). This matters on iOS: Safari deletes a site's saved data after about a week away, including your sessions. Installing to the Home Screen keeps everything.

## Better video overlays

The overlay preview and the exported video used to be drawn by two different pieces of code that had drifted apart. Exports were missing grid lines, glows and labels, and always printed in English. They now share one renderer, so **what you see while scrubbing is exactly what gets exported**, in your language. Overlay gauges now also follow the MPH/KPH switch correctly.

## Android app

If you use the LapWing Android app, update it alongside this release to get:

- **Hardware-speed export.** Exporting with overlays now runs on your phone's hardware, so it takes seconds instead of minutes.
- **Save to Gallery** now actually works. Exports land in *Movies/LapWing*.
- **The app remembers your video.** Load it once for a session and it's there the next time you open that session.
- **Videos on this device** in the Profile tab shows the space your saved videos use, and lets you delete them.
- **Stream straight from an Insta360 camera.** Connect over the camera's Wi-Fi, pick a recording and play it without downloading it, overlays and all. You can point the view around in 360° recordings by dragging.

## Anonymous usage stats

So we can tell whether people actually use LapWing, the website now counts visits, and nothing more. There's no click tracking, no recordings, no cookies, and none of your data, files, lap times or location. You can turn it off at any time in **Settings → Privacy**, and if your browser has Do Not Track or Global Privacy Control turned on, it never runs at all. The Android app sends nothing.

## Fixes

- Opening a drag or waypoint session right after a circuit session could overwrite that course's best lap with a drag run. That's fixed.
- Incomplete drag runs no longer show up as your "fastest" anywhere.
- The "read-only" banner on shared sessions is readable again.
- Setup history no longer gets stuck on "Loading…", and it tells you when something fails.
- A single damaged packet in a GoPro file no longer stops the whole import.
- Sample logs and logger photos saved for offline use now update when we change them.
- Android app: export and Save to Gallery work again. An export can no longer use the wrong session's video. Deleting a session now frees its video copy. Settings no longer says "not cached" forever. The Insta360 dialog no longer spins forever when you reopen it.

Thanks for racing with LapWing. If something doesn't look right, let us know.

Generated by Claude Code

Drag mode, GoPro import, the LapWing rebrand and the native video pipeline
together are a major release, so the version, the CHANGELOG heading (dated
to the release day) and the plans' release-review references move to 5.0.0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZ3Pt89yubdPtYn5Wc8fjW
@TheAngryRaven TheAngryRaven changed the title 4.2.0: drag mode, GoPro import, LapWing look, native video 5.0.0: drag mode, GoPro import, LapWing look, native video Oct 4, 2026
@TheAngryRaven
TheAngryRaven merged commit 560c9fd into main Oct 4, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants