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>
Practice laps under 85% of the driver's own median (a likely shortcut)
are flagged: counted, but never a best lap, record or training stat
until approved. Timing and Race Day get a one-click Approve/Remove
panel, overlays show a warning mark, and any lap can be removed or
restored from Timing and Judging with undo.
Also sends judge corrections straight to the server so its hot-results
protection can no longer hold back a correction made right after a
passing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Race Day Start now runs a count-down (Go, 30 s - 5 min, default per
event) with calls every minute, 30 s and 10 s, grid call and 9..3
count for grid starts, hold/resume/+1 min/to 30 s/to 10 s/Go now, and
starts the heat by itself when it runs out. The transponder check runs
during the count-down, replacing the separate warm-up.
Adds a flying start mode (horn + clock is running, own clock per car)
as the qualifying default, and the latest personal clock start rules
from EFRA 9.3 / IFMAR 4.2.3 for flying and staggered starts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Race Day gets a 1-3 minute warm-up per heat with a green check per
driver when their transponder is seen, countdown announcements
(sv/en), quick transponder fix from the panel, and a start warning
that still allows starting when someone is missing.
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>
Off by default. Once a PIN is set in Settings -> System & backup, every non-GET /api/*
request (plus GET /api/ammc/config) needs a signed 30-day HttpOnly cookie, which also
closes the unauthenticated /api/update/run restart trigger. A 401 opens a PIN dialog
and the pending save is retried after login. Reads stay open because the public overlay
hydrates through GET /api/state. Optional RC_ADMIN_ALLOWED_CIDRS restricts the same
routes by client address.
The PIN hash and signing secret live in data/, outside the app_state blob, so they
never leak through GET /api/state, exports or backups.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Registering or editing a driver with a transponder already assigned
to a different driver (e.g. a wrong-class typo) previously saved
silently - the only symptom was that driver's laps never registering
during their heat, with no indication why. Now a confirm dialog names
the conflicting driver and their class before saving; the same
person's own multiple class entries sharing one physical transponder
are correctly never flagged.
Also confirms two things the user asked to verify: the race clock
already counts down for all timed session types (only open practice
counts up unbounded), and a driver not entered in a heat already has
their laps fully discarded rather than recorded - both were already
correct, no changes needed there.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A thorough audit of practice/qualifying/heats/finals, bump-up,
judging and pause/timer code (requested by the user) found several
real, concrete bugs, fixed together:
- Qualifying reseed sorted heats as text ("H10" before "H2"),
scrambling grids at 10+ heats; now sorts numerically and reports a
clear warning instead of silently dropping/ghosting heats when the
participant count changes.
- A driver with zero laps in a finished heat/final (DNS) vanished
entirely - not just from that round, but silently from every future
reseeded round and the final grid. Now backfilled as a real,
last-place "DNS" row.
- Bump-up did nothing at all with "reserve bump slots" off (mains
were already seeded full, so the old capacity check always
blocked it), and a promoted driver stayed double-entered in their
old main's upcoming leg. An oversized bump count could also blow
the main-letter naming past "Z". All three fixed.
- Judging undo restored an absolute snapshot instead of reversing its
own delta - undoing two penalties out of order could leave a live
penalty behind two "undone" log entries. Now always nets correctly
regardless of undo order.
- A competitor's elapsed time didn't exclude paused time (only the
session clock did), making a paused heat's results incomparable to
an unpaused one in qualifying ranking. Finishing while paused also
dropped the final pause segment, so the displayed elapsed time kept
growing after finish. Both fixed.
Verified with new unit tests for every fix (including a new
session_control.test.js - that file had zero prior coverage) and a
real headless-browser run confirming the judging-undo and bump-up
fixes end to end through the actual UI.
Co-Authored-By: Claude Sonnet 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>