Skip to content

Beta - #398

Merged
TheAngryRaven merged 66 commits into
mainfrom
BETA
Aug 10, 2026
Merged

Beta#398
TheAngryRaven merged 66 commits into
mainfrom
BETA

Conversation

@TheAngryRaven

@TheAngryRaven TheAngryRaven commented Aug 10, 2026

Copy link
Copy Markdown
Owner

DovesDataViewer 4.0.0 — Sprint mode lands, end to end

The companion release to BirdsEye firmware 4.0.0. The headline: sprint/autocross support goes from "the logger can time it" to a complete loop — author a course in the app or walk it on the device, sync it over Bluetooth, drive it, and read the runs back. Around that: a sync system that finally understands device-authored tracks, smarter track-capacity management, connect-time prompts, and a long list of fixes.

Sprint mode, complete

  • Sprint courses can be created and edited. The course editor gained a Circuit / Sprint picker; a sprint course gets a separate finish-line handle on the map, the sector list grows a Finish row, drops the meaningless Major switch, and relabels its rows Split 1/2. Selecting Sprint drops a finish line for you rather than making you hunt for a button. The type is fixed once a course has geometry — retyping would silently invalidate it.
  • Sprint sessions can finally be read back. Load a log recorded on a sprint course and the app lists one row per run, timed exactly the way the logger times them (re-crossing the start cancels and restarts a run). Split times, the fastest-run trophy, map overlay, video sync and leaderboards all keep working. Split lines between start and finish now render as sector times — previously they never appeared at all. The log header's race_mode and device name are finally read out (they were being written and thrown away), and race_mode steers course detection so a sprint log can't mis-match the circuit track at a shared venue. "Lap" wording is kept — autocross drivers call them laps, and so does the logger.
  • Sprint tracks appear in the Device tab and sync over Bluetooth. The tab loads both device folders, tags each track's kind, and badges sprint tracks (a circuit and a sprint track can legitimately share a short name). Upload, download and delete target the folder the track actually came from. Under the hood the BLE client speaks the firmware's TS* verbs, the folder is chosen by opcode (never by a path in a filename), and sync entries are keyed on (kind, short name) so same-named tracks of different kinds can't collide. Sprint tracks are Bluetooth-only for now — the native Android bridge doesn't speak TS* yet and says so instead of showing an empty list.
  • Sprint finish lines are drawn on the maps, not just in the editor — and the start line turns green on sprint courses so start→finish reads at a glance. Circuit maps unchanged.
  • Foundation work: courses are typed circuit/sprint, a sprint course carries its required finish line and a fixed sortable date_created stamp (the logger picks the newest by comparing these as plain strings). Existing courses need no migration — absent type means circuit.

Sync that understands the logger

  • Name the courses you walked on the logger, without a laptop. A device-authored course is named from its GPS clock (N260803_1432) because the logger has no keyboard. A new sync wizard lists everything that differs between app and logger, marks each row Upload or Download, and gives every device-named track and course a name box — short name auto-filled as you type. The new names are written back to the logger, so both sides agree and the prompt doesn't return. Tracks that can never sync are listed with the reason instead of retried forever.
  • Connecting a logger now offers what it needs. On connect the app checks for a firmware update and for track differences — no digging through the drawer. The firmware prompt gains Remind me tomorrow (remembered per logger and per version, so a new release still asks straight away), the automatic check stays quiet when there's nothing to do, and the track prompt only appears when something actually differs.
  • A course created on the logger now actually arrives when you sync it. The device-sync reader only understood the old bare-list track format, so a device-authored track came across with no courses in it. Both shapes are now read.
  • A track downloaded from the logger can be sent back to it. Import dropped the short name and sync matched on filename instead of the declared short name — an imported track was invisible to every later sync, permanently listed "on device only".
  • Tracks sent to the logger keep their name and course lengths. Uploads used the old bare-list format, which carries no track name, no short name, and no course length — the very thing the logger ranks courses by. A track sent from the app could never be recognised at the venue. Uploads (and single-course edits) now use the full format both sides already speak.
  • A logger holding fewer courses than the app no longer asks to sync on every connect — a subset is now the normal state for sprint tracks, and it reads as synced.
  • "Resync All" no longer stacks duplicate tracks on the logger — it writes to the file that's actually there instead of deleting nothing and adding a copy each time.
  • The sync wizard's course screen no longer pre-fills the track's name into every course box — each course shows its own name, its track, and the date it was walked.
  • Device sync can't report a changed course as "synced" — it now compares the full device-visible projection per course type (including sprint finish lines) and treats a type change as a different file.

Fitting more on the card

  • Tracks are sent to the logger without formatting whitespace — indentation was a quarter of every file and nothing reads it. Roughly a quarter more courses per track before the size limit bites.
  • Sprint tracks send only their newest course to the logger — every course stays in the app and your account; only the card is trimmed. Circuit tracks unaffected.
  • You can choose which courses go on the logger. A new Courses button opens a per-track checklist with a live count of the logger's room. Unchecked courses stay in the app.
  • Tracks can be selected and sent in one go, respecting your course picks per track.
  • A track too big for the logger is reported by name instead of silently overflowing into an unreadable file.

Device settings

  • Settings with a fixed set of choices are now dropdowns — a typo in a free-text box used to silently revert the logger to its default. Race Mode gets a proper Circuit/Sprint picker; Spark Mode and Cylinders (multi-cylinder RPM correction) and the new Debug Pages and Display Colours settings appear once your logger's firmware reports them. Unknown values from other firmware are shown as-is, never masked.

Firmware updates

  • A firmware update that fails with "SIZE" now says what to do about it — install v3.1.0 first (it fits under the old limit and carries the bigger staging region), then retry; newer devices that still refuse are pointed at USB instead of a pointless staged upgrade.

Native Android app (LapWing)

  • The Device tab works in the native app — settings, track management and battery ride the native logger IPC, with an in-app scan picker and the single connection slot guarded so two screens can't steal the logger from each other.
  • Session dates show in the logger file lists — Alfano sessions are no longer bare hex ids.
  • Download dialogs respect notches on edge-to-edge phones.
  • MyChron downloads no longer double the extension (.xrz.xrk.xrk); Alfano downloads save as .csv so the importer routes them correctly.
  • Auth emails from the app now link to lapwingdata.com instead of the WebView's dead internal origin, and the register/reset flows explain the confirm-in-browser step.

And one login fix

  • Login no longer fails on a rate-check outage — if the rate-limit pre-check itself errors, sign-in proceeds (fail open); a genuine rate-limit rejection still blocks with its message.

claude and others added 30 commits July 30, 2026 00:39
- Pad the MyChron/Alfano/DovesLogger/Fledgling download dialogs with
  safe-area-modal so they clear notches on edge-to-edge Android.
- Pass the native FileEntry date/meta through the adapters into LoggerFile
  (new optional fields) and show the session date in FileListPanel, so
  Alfano sessions aren't bare hex ids.
- Normalize download filenames (new pure fileNaming helpers): MyChron
  swaps the device's .xrz for .xrk instead of stacking extensions, and
  Alfano appends .csv to its extensionless session ids for save/import.
- Update the adapter tests for the pass-through, add fileNaming tests,
  and record it all in the CHANGELOG under Unreleased.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdpAnoQ6Y1dqDX3ScnQekD
Inside the Tauri/Android WebView, window.location.origin is the shell's
synthetic http://tauri.localhost, so the redirectTo/emailRedirectTo URLs
baked into Supabase registration-confirmation and password-reset emails
were dead links. New platform helper authRedirectOrigin() returns the
canonical https://lapwingdata.com origin when isNativeApp() (emailed
auth links open in a mail client / real browser, never the app WebView)
and the current origin on the web; authBackend's resetPassword and
signUp now use it.

Since on native the emailed link completes in the browser rather than
the app, the Register success and Forgot-Password sent toasts gain
native-specific copy (all locales) telling the user to confirm via the
email link in their browser, then return to the app and sign in.

Also on the same screen: the check-login-rate pre-check now fails open —
an infrastructure failure of the edge function (network error/throw) no
longer aborts sign-in with a generic error, while a genuine rate-limit
rejection still blocks with its message.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdpAnoQ6Y1dqDX3ScnQekD
The Device tab (settings, tracks, battery) was dead in the native app:
FileManagerDrawer gated it on isBleSupported(), the Tauri webview has no
Web Bluetooth, and DeviceContext could only connect via
navigator.bluetooth. Settings/tracks/battery now ride the native
logger_* IPC that LapWing exposes.

- New transport-neutral DeviceDetails seam (types.ts): battery,
  listSettings/setSetting/resetSettings, listTracks/getTrack/putTrack/
  deleteTrack. Two implementations: createBleDeviceDetails (wraps the
  existing @/lib/ble functions — web behavior unchanged) and
  createNativeDeviceDetails (wraps new doveslogger ipc invokers for
  logger_battery / logger_list_settings / logger_set_setting /
  logger_reset_settings / logger_list_tracks / logger_download_track /
  logger_upload_track / logger_delete_track).
- DeviceContext is transport-aware: scanNative/connectNative drive the
  IPC with an in-app picker (BLE has no OS chooser), expose isConnected +
  details on both transports, and scope the native connection to the
  drawer (closing it disconnects — the backend has one global slot).
- The single native slot is guarded by an ownership token
  (loggers/native/owner.ts): the Device tab and the three download
  dialogs claim it before logger_connect and a second claimant is
  refused with a clear message instead of silently stealing the device.
- DeviceSettingsTab / DeviceTracksTab take DeviceDetails instead of a
  raw BleConnection (deviceSettingsSchema was already transport-free).
  The firmware OTA section stays web-only; on native the settings tab
  points at the Fledgling download screen where native firmware update
  already lives.
- dovesloggerConnection flips supportsDeviceDetails to true (the flag
  finally has a reader); fledglingConnection carries the BLE details.
- i18n: scan-picker + busy-slot + firmware-hint strings in all 7 locales.
- Tests: device-tab ipc invoker contracts, ownership token, updated
  adapter expectations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdpAnoQ6Y1dqDX3ScnQekD
feat(native): make the Device tab work in the LapWing app (settings + tracks + battery)
fix(auth): point emailed auth links at lapwingdata.com on native
fix(native): mobile polish — safe areas, session dates, file naming
The logger and DovesLapTimer have shipped sprint (autocross / point-to-point)
support for a while, but NOTHING can author a sprint track: the device reads
/TRACKS/SPRINT/*.json, picks the newest course by date_created and stands up
SprintTimer, and the only thing that would write those files is this app --
which had no concept of a course type. Sprint mode is currently testable only
by hand-writing JSON onto an SD card. This is the first of four PRs closing
that loop; it is model + validation + wire format, with no UI.

Course gains three optional fields:

  type?: 'circuit' | 'sprint'   absent means circuit, so every existing saved,
                                bundled and cloud-synced course keeps working
                                with no migration
  finish?: SectorLine           sprint only and REQUIRED there -- the device
                                cannot time a run without it, so validation
                                rejects it rather than shipping a course the
                                device will silently ignore
  dateCreated?: string          sprint only, sortable 'YYYY-MM-DDTHH:MM'

date_created is not cosmetic: the firmware chooses which sprint course to load
by comparing these stamps as plain STRINGS (sprint_select), so the zero-padded
shape is load-bearing -- an unpadded stamp collates wrong and the device runs
the wrong cone layout. New src/lib/sprintCourse.ts owns the format, and stamps
once and preserves, so editing a course cannot make it jump the device queue.
newestSprintCourseIndex mirrors the firmware's choice, sorting unstamped
courses oldest rather than hiding them -- the device would still see the file,
and hiding it here would make the app disagree with the card.

validateCourseSectors branches on type instead of growing a parallel
validator, so its three call sites keep working and cannot drift:

  circuit  0 sectors, or exactly 3 majors total  (unchanged)
  sprint   0-2 splits, `major` ignored, finish line required

Sprint splits are stored unflagged. `major` is meaningless point-to-point, and
storing them as majors would let a course retyped to circuit silently look
like a valid three-major layout it never had. That means legacyMirror would
drop them on export, so the device projection is now type-aware
(deviceSectorProjection) and maps splits into the sector_2/sector_3 slots
positionally.

Also fixes a latent bug in coursesMatch: it compared only start/finish and the
two legacy sector lines, so ANY course carrying data outside that projection
read as "synced". For a sprint course that includes the finish line -- the
single most likely edit. It now compares the full per-type projection, and
treats a course that changed type as a different file rather than an edit,
since the two kinds live in separate folders on the device.

Tests: 25 new for sprintCourse (including that the stamps sort chronologically
as plain strings, the 09:00-vs-10:00 trap), 9 for the sprint validation
branch, 10 round-tripping the wire format and pinning the coursesMatch fix.
Full suite 2464 pass; lint, typecheck and build green.
plan 0015: sprint course model, validation and device wire format
Second of four. PR #375 added the model; this makes it reachable — you can
now actually author the sprint track the logger has been able to read for
weeks but nothing could produce.

The editor gains a Circuit/Sprint picker, and a sprint course gets a second
map handle for its separate finish line. LineId grows a 'finish' variant
alongside 'sf' | number, and the finish is enumerated LAST in allLineIds
because that is the order a driver crosses the lines — the same order the
sector list renders, so map and list agree without a second sort.

Sprint-specific behaviour in the list editor: the Finish row sits at the
bottom, the Major switch disappears (meaningless point-to-point), rows read
"Split 1/2" instead of "Sector n", the limit becomes MAX_SPRINT_SPLITS
rather than the circuit line budget, and the guidance text explains the
point-to-point shape. Picking Sprint drops a finish line north of the start
rather than making the user hunt for a button, mirroring how a new course
auto-drops its start/finish.

Two deliberate scope calls:

- The type picker is CREATE-ONLY. Retyping a course that already has
  geometry would silently invalidate it — a circuit's three majors are not
  a sprint's splits, and vice versa — so editing keeps the saved type.

- TrackPromptDialog and the admin CoursesTab stay circuit-only. Both keep
  their own copy of the editor state, and courseType defaults to 'circuit',
  so they compile and behave exactly as before. Community submission of
  sprint courses is already out of scope for this plan, and the log-import
  prompt wants the runs view (PR 4) before it can do anything useful with a
  sprint log.

The save-time fork lives in sprintCourse.finalizeCourseForSave rather than
inside the hook: circuit normalizes (writing the legacy sector2/sector3
mirror), sprint stamps date_created and deliberately skips normalization,
since that mirror is derived from MAJORS and sprint splits are unflagged —
normalizing would emit an empty mirror and imply a sector layout the course
does not have. Extracted because this repo's coverage config excludes
src/components/**/*.tsx by design, so logic left in a .tsx is logic that
cannot be tested. 4 new tests cover it.

All 13 new UI strings are translated into all six shipped languages rather
than left English — the locale-parity test enforces it, and each string
follows the terminology already used in that language's file (de "Zwischen-
zeit", fr "intermédiaire", ja "スプリット", etc.).

2468 tests pass; lint, typecheck and build green.
plan 0015: create and edit sprint courses in the track editor
Third of five. The client can now address the logger's /TRACKS/SPRINT folder
alongside /TRACKS — list, download, upload, delete.

New trackOpcodes.ts owns the verb table rather than leaving opcode strings as
inline literals at four call sites. Every trackSync.ts function takes an
optional `kind` defaulting to 'circuit', so nothing existing changes on the
wire.

The folder is chosen by the OPCODE, never by a path in the filename. That is
the firmware's design, not an accident: its filename validator rejects '/' and
'..' so a BLE client stays jailed to the tracks folders, and a path-carrying
filename would have punched a hole in that. The cost is a parallel verb set.

Only the LIST replies differ between kinds -- TSFILE:/TSEND vs TFILE:/TEND, so
a sprint enumeration can never be mistaken for a circuit one; get/put/delete
reuse the circuit replies verbatim. Verified by reading bluetooth.ino (the
dispatch at :557-604, the listing tokens at :325-326) rather than trusting the
protocol doc, which is what the doc now documents.

Also fixes a collision that sprint mode would otherwise have introduced:
buildMergedTrackList keyed entries on shortName alone, but a circuit "OKC" and
a sprint "OKC" are two distinct files in two distinct folders. Keying on
(kind, shortName) keeps them separate; without it one would have been reported
as a mismatched version of the other and "syncing" it would have overwritten
the wrong file. DeviceTrackFile.kind is optional and absent means circuit, so
existing callers are unaffected.

trackKind() derives which folder a track belongs in (any sprint course makes
the track sprint) and isMixedKindTrack() flags the one shape the device cannot
represent -- a track carrying both kinds would have to be split across two
files. The editor cannot create one, but a cloud-synced or hand-edited track
could be, so it is surfaced rather than silently half-pushed.

NOT reachable from the Device tab yet, deliberately: that path goes through
the transport-neutral DeviceDetails seam, which has two implementations --
Web Bluetooth and the native Android IPC bridge. The native side cannot speak
TS* until the Android app does, so that needs an explicit decision rather than
being smuggled in here. Recorded as PR 4 in the plan.

15 new tests (opcode table incl. the prefix-collision check, kind derivation,
mixed-kind detection, merge namespacing in both directions). 2483 pass; lint,
typecheck and build green.
plan 0015: sprint track sync over the device's TS* verbs
Fourth of five, and the one that closes the loop: a sprint course authored in
the track editor can now be pushed to the logger and driven.

The DeviceDetails seam gains an optional `kind` on listTracks/getTrack/
putTrack/deleteTrack (absent = circuit, so every existing caller is unchanged)
plus a `supportsSprintTracks` capability flag. The tab loads both folders,
tags each file with the folder it came from, and threads that kind through
every upload, download and delete so an operation can never land in the wrong
folder.

Two places needed disambiguating now that a circuit and a sprint track can
share a short name: the list row key (React would otherwise collapse them into
one row) and the post-refresh re-find after downloading a course, which
matched on shortName alone.

NATIVE ANDROID IS DEFERRED TO THE END OF THE PROJECT, deliberately. The
bridge does not speak the TS* verbs, and it is gated on that app's release
rather than on anything in this repo — the app is not live yet, so nothing
regresses by waiting. Rather than throwing or silently returning nothing, the
native implementation reports supportsSprintTracks: false and the tab skips
the sprint folder entirely: a missing capability must not render as an empty
folder that reads "no sprint tracks on the device". Recorded as an
end-of-project follow-up in the plan, with the exact verb mapping the bridge
will need.

3 new tests pin the native behaviour — the capability flag, that a sprint list
resolves empty WITHOUT touching the bridge, and that circuit listing still
goes through it (explicit and defaulted). 2486 pass; lint, typecheck and build
green.
plan 0015: sprint tracks in the Device tab, over Web Bluetooth
The firmware emits eight metadata columns; the parser read six, so the
`race_mode` column that tells us a session was timed point-to-point was
already on the wire and being silently dropped. Column names are taken
from BirdsEye/dovex_header.cpp rather than the firmware's own docs, which
still list the pre-rename `driver_name`/`course_name`/`optimal_lap_ms`.

The lookup stays keyed by column name, which is what makes appending the
two trailing columns readable from new logs while six-column logs keep
parsing untouched.

`parseRaceMode` returns undefined — not 'circuit' — for an absent, empty
or unrecognized value. An unknown mode should leave downstream behaviour
exactly as it was rather than assert a timing model the log never
claimed, and callers already treat "unknown" as circuit.

`lib/gps/dovepWriter.ts` is deliberately left emitting six columns: the
phone laptimer records circuit sessions only, and an absent `race_mode`
already means circuit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ESkRRtF4vRrANPL6huSgmD
calculateLaps was circuit-only by construction: it detected crossings of
one line and paired *consecutive* ones into laps. A sprint run is one
start crossing to a crossing of a different line, so a sprint log yielded
zero laps.

Pairing is the only thing that differs, so the per-lap body — speed stats
and the sector-boundary walk — is extracted into a shared `buildLap`; the
walk was already correct for a run once `end` is the finish crossing,
since its last segment simply closes on `end`. The existing circuit
assertions pass unmodified, which is the proof that extraction changed
nothing.

`pairSprintRuns` mirrors the device's SprintTimer (DovesDataLogger plan
0002 §7 Q4): a start crossing opens a run and cancels any run already in
progress — the botched-course re-launch rule — a finish crossing
completes it, and a finish with no armed run is ignored. Equivalently,
each run opens at the LAST start crossing before its finish, which is
also what makes the derivation robust to a driver crossing the start line
on the way back to grid.

The signature is unchanged, so all six call sites (course detection,
share sessions, lap management, reference laps, overlays, the live timer)
gain sprint without touching any of them. Runs stay `Lap[]` on purpose —
every consumer keeps working.

A sprint course with no finish line returns [] rather than pairing runs
off the start line alone; validation already blocks saving one and the
device ignores it too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ESkRRtF4vRrANPL6huSgmD
Sprint splits are stored `major: false` on purpose — the flag is
meaningless point-to-point, and flagging them would let a course retyped
to circuit look like a valid three-major layout it never had. But both
readers of that flag treat its absence as "no sectors", so splits the
driver deliberately placed rendered as em-dashes: courseHasSectors
returned false for every sprint course, and rollupMajorSectors bailed on
the fewer-than-three-majors check.

Fixed in the readers, not the data. `displayedSectorIndices` names the
timing lines that open a displayed sector — the flagged majors on a
circuit, every split on a sprint course — and rollupMajorSectors consumes
it. With MAX_SPRINT_SPLITS = 2 a run has at most three segments, which is
exactly the S1/S2/S3 the lap table, video overlays and snapshots already
render; one split yields {s1, s2} and no splits yields nothing, since
then the single segment IS the run.

courseHasSectors gets the matching branch. Circuit behaviour, including
the legacy sector2/sector3 fallback, is unchanged.

sectorLabels is left alone: it also drives the track editor and
SectorCropSelect, so renumbering sprint splits there is a cosmetic change
to landed UI, not part of reading runs back.

racing.test.ts is new — these two predicates own the "absent type means
circuit" default and the sector-column decision for every view, so their
edges are worth pinning independently of the callers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ESkRRtF4vRrANPL6huSgmD
Parsing race_mode was not enough on its own: nothing in the app read
dovexMetadata at all, so the column went from being dropped by the parser
to being dropped by every consumer.

A venue can carry both a circuit layout and a sprint one — separate
tracks that may even share a short name. Left unfiltered, findNearestTrack
can pick the wrong track outright and the session reports zero laps, with
nothing on screen to explain why.

tracksForRaceMode drops the courses that cannot be what was driven, then
the tracks left empty, before auto-detection runs. It is conservative in
both directions: an unknown mode — every log predating the column — and a
filter that would leave nothing both return the input unchanged, so no
log detects worse than it did before this existed.

Wired into the file-load path only. The full track list still backs
allTracks and the nearest-track prompt, and the live timer is untouched
since there is no log header during a session.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ESkRRtF4vRrANPL6huSgmD
"Lap" stays, in the code and on screen — autocross drivers call them
laps and the logger kept the wording on-device for the same reason. Only
the strings that are factually wrong for a point-to-point run fork on
course type: the empty state told the user to pick a track with a
start/finish line, and a run is not a loop, so "Avg Lap Length" becomes
"Avg Run Length". The lap-number column header is unchanged.

The Simple/Full toggle is suppressed for sprint. A sprint course caps at
two splits, so its three segments already ARE the S1/S2/S3 of the simple
view — and since every split is stored unflagged, the toggle's
"has sub-sectors" test would otherwise say yes and label them
"1 / 1.1 / 1.2".

Both new keys are seeded to all seven locales, matching each file's
existing sprint terminology from the track editor (manga / manche / Lauf
/ manche / 走行 / bateria).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ESkRRtF4vRrANPL6huSgmD
Marks PR 5 done in the plan with what actually landed and why: the
firmware-derived run-pairing rule, splits-as-displayed-sectors, the
detection narrowing, and the two deliberate non-changes (dovepWriter's
six-column output and sectorLabels' sprint numbering).

subsystems.md's DOVEX section still showed the six-column header; it now
shows all eight, notes that columns are read by name rather than
position, and documents what race_mode means to this app.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ESkRRtF4vRrANPL6huSgmD
…ing-a6x07o

plan 0015 (PR 5/5): read sprint runs back out of a DOVEX log
The size cap lives in the firmware ALREADY INSTALLED on the logger, not
in the image being delivered and not in this app — which has never
enforced a cap of its own. Firmware older than v3.1.0 carved a 320 KiB
staging region out of flash and rejects anything larger at the FWBEGIN
handshake, before a byte is uploaded.

So the user saw "Firmware update failed: SIZE" with no way to know that
nothing is wrong with the image, and that the fix is to install v3.1.0
first — it fits under the old cap and carries the larger staging region,
after which the current build installs normally. Hit for real on a unit
that had been left a version behind.

The three FWERR: sites now throw a typed FirmwareProtocolError carrying
the raw token, so callers can act on the protocol rather than re-parse an
English sentence. explainFirmwareFailure turns a SIZE into an actionable
message and returns null for everything else, so every other token keeps
its existing wording.

Deliberately not a version->capability table: that mapping is its own
piece of work. This is the single rule that strands people today, and the
two predicates behind it (needsOtaLayoutUpgrade, exceedsLegacyOtaCap) are
exported so a pre-flight warning in the confirm dialog is a one-line call
when that lands.

A device reporting v3.1.0+ that still refuses is NOT sent down the staged
upgrade path — it gets pointed at USB, because telling that user to
install 3.1.0 would be a wild goose chase.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ESkRRtF4vRrANPL6huSgmD
…n maps

Two bugs found on the first real bench run of the on-device course
creator. Neither is in the firmware — the file it writes is well-formed
383-byte JSON with the course and finish line present.

1. parseDeviceCourseJson only accepted the legacy bare-array shape. The
   device writes the OBJECT shape — the same one this app's own track
   files use and that the firmware's parseTrackFile() reads — so
   JSON.parse succeeded, the result wasn't an array, and it fell through
   to [] with nothing logged. A walked course synced back as a track with
   no courses at all. Both shapes are now accepted, which is what the
   device has always done in the other direction.

   An existing test asserted the broken behaviour ("returns [] when JSON
   is valid but not an array (e.g. wrapping object)"), which is how this
   survived review: the bug was pinned as the contract. Replaced with
   tests for the object format.

2. The sprint finish line was rendered in the track editor and nowhere
   else. RaceLineView and SimMap each carried their own hardcoded
   start/finish colour and no concept of a separate finish, so a
   point-to-point course was indistinguishable from a circuit one on
   every map outside the editor.

   Rather than paste the editor's colours into two more files — which is
   exactly the drift that caused this — the palette and the two decisions
   that go with it (which colour opens the course, is there a finish line
   to draw) move into a shared courseLineStyle unit. Sprint start goes
   green so green-to-red reads as start-to-end; circuit start/finish
   stays red, since there it is one line doing both jobs and recolouring
   it would change every existing session map for nothing.

   finishLineOf() guards on the course type as well as the field: a stray
   finish left on a retyped course is stale data, not a phantom red line
   to paint across a circuit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ESkRRtF4vRrANPL6huSgmD
…ing-a6x07o

fix: explain the OTA "SIZE" rejection instead of relaying the raw token
buildTrackJsonForUpload emitted a bare JSON array of courses. The firmware
parses that — but its array branch (sd_functions.ino parseTrackFile) blanks
longName, shortName and defaultCourse, and every course falls back to
lengthFt = 0. lengthFt is what CourseDetector ranks courses by, so a track
uploaded from this app could never be course-detected and dropped straight to
Lap Anything, and the blank shortName reached the DOVEX header's short_name
column.

Emit the object form instead — the same shape the app's own track files and
the on-device course creator already write, and one the firmware has parsed
since well before any shipped release, so no version gate is needed.

Also add parseDeviceTrackFile(), which keeps the wrapper's longName/shortName/
type/defaultCourse rather than discarding them; parseDeviceCourseJson stays as
a thin wrapper over it for the callers that only want courses. The rename flow
needs longName, and needs shortName because for a device-authored track the
FILENAME is the 12-char longName (N260803_1432.json) while the 8-char
shortName the sync merge keys on lives inside the file.

The old "emits a JSON array of courses (not a wrapping object)" test asserted
the lossy shape as the contract, which is how this survived review; it is
replaced with assertions on the metadata the device actually consumes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ESkRRtF4vRrANPL6huSgmD
A track the on-device course creator wrote is stored at N260803_1432.json but
declares shortName "08031432" — 8 characters, chosen by the firmware precisely
because that is this app's Track.shortName budget and the key its sync merge
uses. buildMergedTrackList keyed on the FILENAME instead, so a track imported
from the device could never be matched to the file it came from: it stayed
"device_only" forever and the sync kept re-offering it.

Separate the two concepts. DeviceTrackFile.shortName is now the identity (the
declared shortName, falling back to the filename base only for legacy
bare-array files that declare nothing), and the new fileName / deviceFileName
carry the location. deviceTrackFileFrom() owns that rule so it is unit-tested
rather than buried in the tab, and every write path now targets the real file
instead of `shortName + ".json"` — which would otherwise orphan the original
and leave two copies on the card.

Also fixes the other half of the same nag: handleDownloadToApp never passed a
shortName to addTrack, and buildMergedTrackList skips app tracks that have
none, so downloaded tracks were invisible to the merge whatever the key was. It
now carries the shortName over and names the track from the file's longName.

The two course-level writers went through rebuildDeviceTrackJson so editing one
course stops stripping the file's wrapper metadata and resetting every lengthFt
— the same loss the bare-array uploader caused, reached from a different button.

Verified by reverting the identity rule and watching the round-trip test report
2 merged entries instead of 1 — literally the app_only/device_only split that
made the prompt re-fire. The first draft of that test derived its input from the
value under test and passed either way; it now spells the expectation out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ESkRRtF4vRrANPL6huSgmD
TheAngryRaven and others added 18 commits August 5, 2026 20:58
plan 0016 (2/3): the two-screen sync + rename wizard
plan 0016 (3/3): prompt for firmware and tracks on connect
…e original

Two problems, one screen.

The bug: every generated course name came up pre-filled with the track name the
user had just typed on the previous screen. That was my reading of "new courses
get the same text box, auto-populated by the name", and it was wrong — a course
is not its track, so the screen read as broken.

The box now starts holding what would actually be saved if you touched nothing.
A sprint course keeps its date stamp, which is a valid final answer there since
a sprint venue re-lays its course every event. A circuit course starts EMPTY,
because the stamp is not valid for it and pre-filling anything invites clicking
straight past the one thing the screen exists to ask. Course names no longer
follow the track name at all, so retargetCourseDraft is gone and goToCourses is
a plain step change.

The layout: the course row now mirrors the track row — the ORIGINAL name first,
then the circuit/sprint badge, then the track it belongs to (several tracks can
be on this screen at once, so a bare course name is ambiguous), then the walked
date decoded to readable text, then the box. It previously showed the track name
where the course name should have been, which is why the copied value looked
like it belonged there.

Verified the new tests bite by restoring the copy behaviour: five fail,
including canSave going true when a circuit course is still unnamed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ESkRRtF4vRrANPL6huSgmD
fix(sync): course screen copied the track name, and hid the original
The device settings schema only knew 'string' and 'number', so anything with a
fixed set of values was a free-text box. That is worse than it sounds: the
firmware compares the literal, so a typo doesn't error — it silently falls back
to the default, which reads as the setting not working at all.

race_mode is the case that already existed. It lives on the logger but was never
in this schema, so it rendered as a raw text field with no hint of what it does.
It is now a Circuit / Sprint picker, described accurately as a tiebreak that
only applies when both kinds of track are in range.

spark_mode and cylinder_count are added ahead of the firmware that introduces
them (plan 0003). Rows are built from what the DEVICE reports, not from the
schema, so they stay invisible until a logger actually has them — the schema
entry just means they render properly when it does.

A stored value this build doesn't recognise — older or newer firmware, or a
hand-edited SETTINGS.json — is shown verbatim rather than rendered as one of the
options we do know. settingDisplayValue owns that rule so it is testable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ESkRRtF4vRrANPL6huSgmD
feat(settings): enum control type, and surface race_mode
Adds display_invert to the settings schema as a Normal / Inverted dropdown,
reusing the enum control from the previous commit.

Validation matters more than usual here: the firmware treats anything that is
not an exact "inverted" as normal, so a value let through unchecked would read
as the setting silently doing nothing. "invert", "Inverted" and "1" are all
rejected.

Rows are built from what the DEVICE reports, so this stays hidden until a
logger has the firmware that adds the key.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ESkRRtF4vRrANPL6huSgmD
feat(settings): expose the logger's display-colour setting
…(plan 0017)

`deviceFileOf()` exists precisely so a write lands on the file the device
actually has, and its comment says why: for a track the on-device course
creator wrote, the identity (`08031432`) and the filename
(`N260803_1432.json`) are different strings. Five call sites use it.
`handleResyncAll` did not — it rebuilt `shortName + ".json"` for both the
delete and the put.

So on every device-created track, Resync All deleted nothing, wrote a
second file under the identity name, and left the original behind. Each
resync added one more, growing the number of files and the bytes on the
card — the same budget the track JSON has to fit when the device parses
it on the next boot, which is what the rest of plan 0017 is about.

Not reachable by test: the call sites live in a component, and the suite
runs in `node` with no DOM renderer. The helper it should have been using
is one line and already documented.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ESkRRtF4vRrANPL6huSgmD
…n (plan 0017)

The device reads a whole track file into a fixed buffer and parses it
there. Past that buffer the read is cut mid-JSON, the parse fails,
buildTrackList() adds no manifest entry, and the track stops being
detected at the venue entirely — the failure is total, not partial.

Uploads were written with `JSON.stringify(file, null, '\t')`. In a file
of this shape the tabs and newlines are roughly a quarter of the bytes,
and nothing reads them: the firmware parses with ArduinoJson, which is
whitespace-insensitive, and a human wanting to read one has a formatter.
Dropping the indent is about a quarter more courses per track, for free,
with no change asked of any device already in the field.

This is the single choke point all three upload writers funnel through,
so it is also what the size projection in the rest of plan 0017 will
measure — the number shown to the user has to be the number written.

The test that pinned tab indentation asserted it "matches device
expectation", which was never true; replaced with one that pins compact
output and one that pins the saving being worth having.

Also adds plan 0017 and the changelog entries for both fixes in this PR.

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

fix(sync): stop Resync All duplicating tracks, and stop uploads wasting a quarter of the device budget (plan 0017)
Pure logic only — nothing imports these yet. The wiring, the capability
gate and the picker UI follow; splitting them keeps the part that decides
what lands on a track day reviewable as maths rather than as UI.

`deviceCourseSelection` is the single resolver, so the sync plan, the
upload writers, the "is it synced?" check and the picker can never
disagree about the subset — a disagreement between any two of those is a
track that re-prompts on every connect, which is the trap plan 0016 was
written to close. Circuit tracks keep every course: those are layouts of
a fixed track and the driver still runs them. Sprint tracks keep only the
newest by `dateCreated`, because a sprint venue re-lays its cones every
event and the on-device creator mints a dated course each time, so the
file grows without bound. Stamps compare as plain strings, the same
comparison the firmware makes; a course with no stamp sorts oldest rather
than newest, so an old import cannot displace this morning's course.

Overrides are stored by name rather than index — courses get reordered
and re-saved — and only genuine deviations are recorded, so a track the
user never curated follows the rule forever instead of being frozen to
whatever it looked like when they last opened the picker. An explicit
exclude beats an explicit include: exclude is the direction that keeps a
file under the buffer.

`deviceTrackBudget` measures rather than estimates. It runs the real
upload writer and the real TextEncoder, because the number shown to the
user as the reason to drop a course has to be the number written. A
per-course byte table would drift the first time the writer changed. An
unknown capability assumes the SMALL buffer: guessing high overfills the
card and takes the track out of detection at the venue, guessing low
costs one course.

`deviceCourseOverrides` is device-local and deliberately not a synced doc
store — it describes one physical SD card, and pushing one card's
contents onto every logger the user owns is how a file ends up over the
buffer. The courses themselves already ride cloud sync, so nothing is
lost by keeping it local. It also has no time expiry, unlike the firmware
reminder it is otherwise modelled on: an expiring curation silently
re-adds courses to a card that was deliberately trimmed, and the user
finds out at the venue. Growth is bounded by count instead.

The test that matters most is that an empty store is a working
configuration: on a browser that has never seen this logger, the default
rule alone still produces a set the device can hold.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ESkRRtF4vRrANPL6huSgmD
feat(sync): the model for which courses belong on the device (plan 0017)
…budget (plan 0017)

Wires the model from the previous PR into the three places a curated
device set was broken, and adds the one firmware capability it needs.

**The nag.** `allSynced` compared every app course against the device, so
a track holding a deliberate subset read `mismatch` forever and the
on-connect prompt fired every single time. The merge now carries
`plannedOnDevice` per course and settles on `courseIsSettled`, which is
not the same question as `status === 'synced'`: a course kept off the
device is settled by being absent, and one the user excluded that is
still on the card is unsettled until it's removed. A `device_only` course
stays outstanding exactly as before — it wants importing, which is what
the wizard is for.

**The guards.** The count guard now measures the planned set, so an
eleven-course sprint venue is no longer skipped outright when only one
course was ever going on the card. It reads the curation off the merge
but still counts `appCourses`, because an `app_only` entry carries no
merged courses at all and deriving the list from the merge would count it
as empty and wave through a track that cannot fit. Alongside it, a byte
guard: on the smaller buffer a sprint track overflows at about seven
courses, under the ten-course cap, which is why the cap alone never
caught it. Neither guard subsumes the other, so both stay. A caller that
passes no budget gets no size check rather than a guessed one.

**The upload.** `planOperations` now writes the subset to the device
while the app keeps every course — two deliberately different lists.
Writing `courses` to both is what made accepting the wizard silently undo
the curation and put the file straight back over the buffer. The two
manual upload paths in the tracks tab go through the same subset.

**The capability.** `supportsLargeTrackBuffer` mirrors
`needsOtaLayoutUpgrade` over the existing `compareVersions`, and is the
only place a firmware version becomes this boolean. It inverts that
function's treatment of an unknown version, deliberately: that one must
never nag without certainty, this one must never overfill a card, because
a file past the buffer doesn't degrade the track — it takes it out of
detection at the venue. The version comes from the shared firmware
context, which already owns the one DIS read.

A track that still doesn't fit is reported as a skip with a reason, the
way `too_many_courses` already was — never raised as a dialog. With
nothing stored, on a new browser, the default rule alone has to settle,
and it does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ESkRRtF4vRrANPL6huSgmD
…ion-wiring

feat(sync): apply the course curation, and gate on the device's real budget (plan 0017)
The last piece: somewhere to actually do the curation the previous PRs
made possible. A Courses button on any track with more than one course
opens a checklist with a live byte count — how much of the logger's room
the current selection uses, and how much there is.

The number is measured, not estimated: buildPickerState runs the real
upload writer and the real TextEncoder, because a user is being asked to
drop a course on the strength of it.

The picker lives in the TRACKS LIST, not in the sync wizard. The wizard
reports an oversized track as a skip whose message points here, which
keeps the rule the rest of this plan rests on: curation is only ever
reached by a deliberate action. A dialog that appeared mid-sync would be
the nag the whole plan exists to end.

An empty selection cannot be confirmed. A track file with no courses is
not "a smaller track" — the logger parses it, finds nothing, and never
detects it, which looks exactly like the overflow being avoided.
Deleting a track is a different button, and it already exists.

The newest sprint course is badged rather than pinned. The default rule
keeps it, but a driver about to walk its replacement may well want it
gone, and a checkbox that refuses to move is worse than no hint.

Track rows also gain checkboxes and a bulk send. Every track in a bulk
send goes through deviceJsonFor(), so it carries whatever was chosen for
it — a bulk action that quietly undid the curation would be worse than
having none.

All logic is in deviceCoursePicker with 19 tests; the dialog only draws
it. That is not a preference — the suite runs in node with no renderer
and src/components is coverage-excluded, so anything deciding anything
has to live in src/lib to be covered at all.

Two small things fixed in passing: the app_only row's "Send" label was
hardcoded English while the identical button elsewhere was translated,
and the picker reuses the wizard's existing walkedOn string rather than
duplicating the same sentence across seven locale files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ESkRRtF4vRrANPL6huSgmD
feat(sync): choose which courses go on the logger (plan 0017)
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 10, 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 567e060 Commit Preview URL

Branch Preview URL
Aug 10 2026, 04:43 PM

@github-actions

Copy link
Copy Markdown

Coverage Summary

Lines: 58.66% (7601/12957) · Statements: 57.79% · Functions: 55.84% · Branches: 55.69%

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 94.73% 100% 84.37%
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 14.11% 20% 17.02%
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 0% 0% 0%
src/hooks/useNativeFirmwareUpdate.ts 0% 0% 0%
src/hooks/useNoteManager.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/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 96.92% 92.85% 93.18%
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 47.61% 50% 43.75%
src/lib/courseDetection.ts 97.34% 100% 81.11%
src/lib/courseLineStyle.ts 100% 100% 100%
src/lib/courseSectors.ts 100% 100% 94.73%
src/lib/datalogParser.ts 83.56% 80% 81.81%
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 95.45% 100% 95%
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/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/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.79% 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/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/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.2% 100% 90.57%
src/lib/lapDelta.ts 99.2% 100% 85.07%
src/lib/lapOverlays.ts 100% 100% 86%
src/lib/lapSnapshot.ts 100% 100% 88.46%
src/lib/lapSnapshotStorage.ts 100% 83.33% 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 56.25% 53.33% 100%
src/lib/loggers/doveslogger/firmwareInfo.ts 100% 100% 100%
src/lib/loggers/doveslogger/ipc.ts 100% 91.66% 66.66%
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/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/overlayCanvasRenderer.ts 0% 0% 0%
src/lib/parseReport.ts 68.18% 85.71% 72.22%
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/platform.ts 100% 88.88% 89.65%
src/lib/profanity.ts 100% 100% 75%
src/lib/referenceUtils.ts 98.29% 100% 86.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.17% 100% 72.51%
src/lib/setupRevision.ts 100% 100% 94.11%
src/lib/setupRevisionStorage.ts 84.48% 78.26% 36.84%
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/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% 90%
src/plugins/cloud-sync/syncEngine.ts 99.05% 91.3% 80%
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/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 10, 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 6 commits August 10, 2026 03:18
The firmware now hides the two diagnostic pages at the front of the race
rotation behind a debug_pages setting (default "hide"). Add the enum
schema entry so it renders as a proper dropdown instead of free text —
the firmware only shows the pages on an exact "show", so a typo would
read as the setting silently not working.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDNr2DbMapSHHyieJigF3s
package.json 3.3.0 -> 4.0.0 and the [Unreleased] block becomes
[4.0.0] - 2026-08-10, with the merge-accreted duplicate sections
(three Addeds, four Fixeds) consolidated into one Added/Fixed set —
content moved verbatim, nothing reworded. Version-aligned with the
BirdsEye 4.0.0 firmware release.

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

feat(settings): expose the logger's debug-pages setting
chore: cut v4.0.0 — version stamp + CHANGELOG cut
Merging BETA into the release-cut branch re-imported the pre-cut
[Unreleased] sections next to the consolidated ones: six Fixed entries
were duplicated verbatim and the section headings interleaved again.
One Added/Changed/Fixed set, 31 unique entries, nothing reworded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDNr2DbMapSHHyieJigF3s
@TheAngryRaven
TheAngryRaven marked this pull request as ready for review August 10, 2026 16:40
…qpkw

fix(changelog): dedupe the 4.0.0 block after the release-cut merge
@TheAngryRaven
TheAngryRaven merged commit 485f7ab into main Aug 10, 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