Group any number of races in freely chosen classes into a series with a
points table (ÖRC/100-80-70/F1/custom), best-N or drop-X counting, a
club filter that re-ranks members without guests, tie-breaks, rounds
created from a date list, and per-class standings with print and PDF.
Also fixes PDF export of wide tables: they are compacted and switched
to landscape instead of truncating names and dropping columns.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds 'Calls during a race' (default: passing tone only, like timing
systems) that drops name/personal best/leader/position calls in race
sessions; practice keeps them. Removes the legacy audio panel and
fallbacks, including its 'Sound on' switch that silently muted every
race-audio voice.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The installer now fetches one male and one female Piper voice per language (Swedish
NST/Lisa, English Ryan/Lessac), with genders confirmed by measuring each voice's pitch
rather than guessed from the name. Settings -> Audio gets a voice selector per language
with its own Test button that always plays the Piper voice. Male is preselected for both.
The integration test now runs its server with an empty home directory, so voices
installed on the developer's own machine can't leak into it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Announcements can use Piper, a free local neural TTS, instead of the browser's system
voice. No account, no paid service, and it keeps working with no internet at the track.
- npm run tts:install (or Settings -> Audio -> Install) downloads the standalone Piper
release for Linux/Windows/macOS plus Swedish and English voices into piper/ once
- server/tts.js generates each line once, one at a time, and caches it as WAV under
data/tts/; a new line takes ~0.3 s with the standalone build
- src/piper_speech.js plays it and falls back to the browser voice if Piper is missing,
errors, or takes over 2.5 s, so no announcement is lost and timing is never held up
- "Pre-generate phrases and driver names" renders every fixed phrase and name in both
languages before race day
Browser voice stays the default. Generation routes fall under the admin PIN.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clubs that reconfigure their track (e.g. running indoors in the same
building) can now add named track layouts under Settings, each with
just a start date/time - layouts are contiguous, so a layout's end is
implicitly the next one's start, with no separate field to keep in
sync. Every session freezes which layout was active when it started,
so the "track record" audio cue and the Results Archive (which now
shows each session's layout) only compare laps run on the same
physical track. With no layouts configured, everything behaves
exactly as before - the feature is fully inert until a club uses it.
Design settled through conversation with the user: global scope (not
per class), start-date-only (no redundant stop date), and no changes
needed to driver/transponder mapping, since that's already fully
independent of track geometry in this app.
Deliberately out of scope for this pass: the mobile/driver training
profile's own cross-session personal-best trend stats still compare
across a driver's whole lap ledger regardless of layout - logged in
ROADMAP.md as a real remaining gap for a later pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Investigated whether two admin browsers open at once can overwrite each
other's changes, per an explicit request to report the risk before
making bigger changes. Found that POST /api/state always replaces the
entire stored document with a client's in-memory copy, with no version
check and no per-field merge - and a browser only ever pushes, never
re-fetching server state after page load except on the Overlay view.
An idle browser's next save, even an unrelated edit, could silently
revert another browser's more recent changes, worst case a live race's
in-progress results.
Two scoped fixes instead of the bigger optimistic-locking rework (that
needs real UX decisions about conflicting saves, deferred separately):
1. server.js now remembers which session results were just written by
a real live decoder passing (/api/passings) and protects that key
for 15 seconds against being reverted by a stale full-state save,
while still applying the rest of that save normally.
2. Every non-Overlay view now quietly refreshes its data in the
background every 15 seconds (skipped while a local edit is
pending), without ever calling renderView(), so it can never
discard an open, unsaved edit form - shrinking how stale a
browser's next save can be from hours to seconds.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>