Repository navigation
5.0.0: drag mode, GoPro import, LapWing look, native video - #423
Merged
Merged
Conversation
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)
Deploying with
|
| 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 |
Coverage SummaryLines: 62.67% (9146/14592) · Statements: 61.69% · Functions: 58.51% · Branches: 59.1% Per-file coverage
|
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
…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
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
8 tasks done
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
9 tasks done
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
…jc-low 4.2.0 release review: Low fixes
TheAngryRaven
marked this pull request as ready for review
October 3, 2026 03:29
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 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
…jc-v5 Release as 5.0.0 instead of 4.2.0
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.
Requested by Dove · project thread
LapWing 5.0.0
Release PR: merges everything on
BETAthat isn't onmainyet (plans 0022 to 0029 plus the LapWing branding). Full detail is in the## [5.0.0]section ofCHANGELOG.md.Highlights
Added
Changed
Fixed
Commits in this release
cec9e14,aae49f2,3c70124,226618eplan 0022: drag mode (detector, run-to-lap mapping, orchestration + persistence, UI + i18n)194ed2a,af5ef24plan 0023: unified overlay scene renderer6589973,f97c435,0e84e4d,c0ea128plan 0024: native export bridge and follow-ups9f545ce,7c79d4b,b237b33plan 0025: Insta360 streaming, Wi-Fi hint, missing-picture messagee164529plan 0026: iOS install prompt + persistent storageeb87643plan 0027: two-stage offline cache87432f7,57b491e,1265a23,14252c5plan 0028: setup history1f26637plan 0029: GoPro GPS video import5f06938LapWing brand (Apply the LapWing brand to the web app #447)Before merging
BETAfirst: it relabels the release from 4.2.0 to 5.0.0 (package.jsonand the CHANGELOG heading, dated 2026-10-04).BETA.@perchwerks/eye-in-the-sky0.5.0, so there's nothing to flip.v5.0.0onmain, then mergemainback intoBETA.🤖 Generated with Claude Code
https://claude.ai/code/session_01A9G6PD5bpvqw5cR4v6J8BJ
Generated by Claude Code