Commit Graph

72 Commits

Author SHA1 Message Date
larssand
72d83ad183 feat(overlay): show places gained since the start in the OBS tower
Heats and finals started together show ▲/▼ against the start order
(new OBS option, on by default). In practice the best lap is no longer
printed twice as both result and best.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 20:13:10 +02:00
larssand
533f59adf9 feat(settings): one public address (domain / DynDNS) for all mobile profiles
Moves the public address from each driver's share panel to Settings ->
System & backup, with Save and Detect public IP. The share panel shows
the address in use and links to the setting; empty keeps using the
local network address.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:45:53 +02:00
larssand
42e53fd356 docs(guide): add a Series / cup tab to the in-app guide
Explains creating a series, running each round from the series page,
points and standings, and a worked winter-series example.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:31:19 +02:00
larssand
b5707f92d1 feat(series): run each round from the series page
Step 3 becomes a rounds table: pick drivers inline (copy from the
previous round, add a late entry), set qualifying rounds/finals/max
cars per heat per round, see the status, and create qualifying and
finals from there. Qualifying heats are now split evenly so nobody
drives alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 18:55:36 +02:00
larssand
581ec2c781 fix(series): clearer step layout, sorted class list and autosave
The series page is split into numbered steps; classes are a sorted list
with the tick on the left and All/None, new series start with none, and
every change is saved straight away so ticks are no longer lost. Also
gives primary/danger buttons white text in the light theme.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 18:28:05 +02:00
larssand
ec200aef2b feat(series): choose open series or club members only for series points
Who scores is now an explicit per-series choice: every driver (default)
or club members only, with the club field shown only for the latter.
Series saved with a club keep members-only scoring. README updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 18:12:18 +02:00
larssand
efa082c2b5 fix(training): keep a linked driver until the first server load finishes
A ?driver= link to Training stats was cleared when the page rendered
from the local copy before the server data arrived. The selection now
survives until the first load settles, then the driver opens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 20:21:19 +02:00
larssand
af79b610db feat(training): base training stats on practice, keep race laps separate
Training stats count Open/Free practice by default; laps from
qualifying, heats and finals are kept and shown under Based on: Races
or All, marked with race, session and series. Races are listed per race
session and never split into stints. Same picker in the mobile profile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 20:08:17 +02:00
larssand
9a9379aa83 feat(series): add series/cup championships with points tables and standings
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>
2026-10-10 19:49:23 +02:00
larssand
f65e526948 feat(drivers): test a driver's spoken name and keep search focus while typing
Adds a Test pronunciation button (forms and list) that plays the
passing sound and real announcements with the spoken name through race
audio. Fixes the Drivers/Cars search losing focus after one letter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 08:35:47 +02:00
larssand
a13c9a15f9 feat(judging): keep penalties and race lap changes in the Judging view only
Race sessions: Timing driver details become read-only with an Open in
Judging button; penalties and lap changes happen only in Judging.
Practice: no penalties anywhere, per-lap clean-up stays on Timing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 21:20:03 +02:00
larssand
7f267e17fe fix(settings): open the right Settings tab from the update link and Race Day
The sidebar update notice now lands on System & backup at Updates, and
Race Day's decoder/backend/audio fix buttons open the matching tab,
instead of whichever tab was used last.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 20:44:28 +02:00
larssand
9d79bcb4d3 feat(layouts): optional min/max lap time per track layout for practice
Each layout can carry its own lap window (set on creation, editable in
Settings, saved from Race Day during practice). Open/Free practice on
that layout uses it; races always keep the event's values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 20:33:22 +02:00
larssand
8601954ea2 feat(practice): flag suspiciously short laps and remove any lap in one click
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>
2026-10-07 19:53:23 +02:00
larssand
3cc4fa0845 fix(training): keep practice laps in training stats after Reset data
Resetting a running Open Practice deleted its results, the Timing view
recreated an empty result at once, and the next save rebuilt that
session's training laps from nothing - wiping every earlier lap from
the drivers' stats. Reset now syncs first and records resultsResetAt;
laps from before it are kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 23:13:58 +02:00
larssand
8b63dd2372 fix(lap): keep best laps per track layout and count the current session for records
A layout switch during a running open practice kept every driver's
old-layout best lap, blocking personal-best calls and skewing the
practice leaderboard. Best laps now restart on a new layout and each
lap records its layout. The track record baseline now includes the
current session so the same record isn't called twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 21:14:19 +02:00
larssand
ebd0f74f40 feat(audio): add downloadable extra Piper voices with accents
Adds Alma (sv), Alan, Northern English, Cori, Alba (British/Scottish)
and Joe, John, Norman (American) as optional voices downloaded one at
a time from Settings, with gender (measured pitch) and accent shown in
the voice lists. The standard install is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 21:58:08 +02:00
larssand
aaca5e8c63 feat(audio): passing tone only during races and remove legacy audio
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>
2026-09-30 21:47:40 +02:00
larssand
5bfb67ea02 feat(race-day): start heats with an EFRA/IFMAR count-down and flying start
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>
2026-09-30 21:27:20 +02:00
larssand
1bc75f8ec1 feat(race-day): add warm-up transponder check before a heat
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>
2026-09-30 20:47:15 +02:00
larssand
627179b95a feat(layouts): switch track layout mid-practice and view stats per layout
A club can run one Open Practice all season and re-lay the track now and then.
Previously a session's layout was frozen at start and every training lap inherited
it, so a season-long practice stayed on its first layout forever, and changing layout
was hidden under Settings -> Appearance.

- Each practice lap now counts toward the layout active when it was driven; a running
  practice session moves onto a new layout when one is added
- "New layout" strip on Training stats, Race Day and the Dashboard, offered only around
  practice; a running race session blocks layout changes everywhere
- "Stats for" selector (current / older layout / before layouts / all layouts) on
  Training stats and in the driver's mobile profile (?layout= on the public API)
- A layout that has practice laps on it can no longer be deleted
- Fix: the buildDriverTrainingStats wrapper in app.js dropped extra arguments

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 21:24:44 +02:00
larssand
196437d25b feat(audio): choose a male or female local voice per language
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>
2026-09-27 20:29:59 +02:00
larssand
e8a2799a92 feat(audio): add offline local neural voice via Piper (sv + en)
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>
2026-09-27 19:28:15 +02:00
larssand
9016ce02e4 feat(auth): add optional admin PIN for all API writes
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>
2026-09-24 22:14:58 +02:00
larssand
bcd6e12a6c Document a concrete auth design for the deferred permissions work
Planning only, no code yet. Per-role PIN codes (not named accounts - a shared
trackside laptop doesn't want individual logins), an optional IP/host allowlist as a
separate complementary layer, and 2FA deliberately left out unless a club actually
exposes the server past their own LAN. Flags one open question for the user to decide
before implementation starts: auth off by default vs. on by default with an
auto-generated Admin PIN on first run.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 20:28:07 +02:00
larssand
ae445097b0 Re-run the security audit against everything shipped since 2026-08-28
npm audit stays clean (0 vulnerabilities). Checked every new db.prepare()/spawn()/
execFileSync() call added since the last audit for injection risk - none found, all
parameterized SQL and array-form process args. Confirmed the CORS/security-header
middleware still covers every route added since, including the new ones.

One new notable finding: POST /api/update/run unconditionally stops the server before
checking whether an update is even available, making it a more direct denial-of-service
primitive than the app's other already-documented unauthenticated endpoints. No code
change made - tracked alongside the deferred permissions/auth work.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 20:22:31 +02:00
larssand
922bd704eb Split the Guide page into tabs, matching the Settings/Event Manager pattern
The page had grown to ~20 stacked panels on one very long scroll - the same problem
already fixed twice this session for Settings and Event Manager. Grouped into 5 tabs
(Getting started, Create a race, Run a race, Training & results, Video guides) using
the same activeGuideTab-as-module-state approach so the selection survives an in-place
re-render (e.g. a language switch). The always-visible overview cards at the top are
unchanged.

Also investigated a "feels slow" concern raised alongside this: measured the page's
actual render cost in a real browser (median 0.6ms, on par with or faster than Settings/
Timing) and confirmed the periodic tickClock/updateSyncStatus intervals never re-render
an idle Guide page. No performance issue found - the perceived slowness was the page's
length, which this change addresses directly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 19:59:15 +02:00
larssand
44f9b62610 Bring the architecture doc back up to date with this session's changes
Closes both open "Documentation maintenance" roadmap items. Used a research-only
Explore pass to get a concrete diff against the actual code before editing, rather
than guessing what was stale.

Documented: src/track_layouts_logic.js (previously unmentioned anywhere),
buildEventSchedule, the Settings/Event Manager tab layouts, new state fields
(raceConfig.scheduleStartAt/scheduleGapMin, Session.trackLayoutId, top-level
trackLayouts/audioEventLog), a real SQLite table contract section, a full
field-by-field Settings shape, several previously-undocumented backend routes,
and the complete list of files tests/browser_smoke.js actually invokes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 17:40:58 +02:00
larssand
d76b0e8eef Refresh stale screenshots and add one for Event Manager's new tabs
guide.jpg was captured before this session's Schedule feature and video guides existed,
so it was missing both. Event Manager's tabbed layout (also from this session) had never
been pictured in the README at all. Added scripts/capture-screenshots.mjs as a reusable
tool for this instead of another throwaway script, since screenshot refreshes are clearly
a recurring need - reuses the same isolated-server/headless-Chrome pattern as the video
recorder and test suite, with one shared seeded demo dataset so future captures stay
visually consistent.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 17:30:44 +02:00
larssand
3e02ba9aad Link the video guides from README, README.sv, and the in-app Guide page
Commit all 7 recorded clips to docs/videos/ (alongside the existing docs/screenshots/)
instead of leaving them in the gitignored video-guides/ folder from the earlier
feasibility spike - they're meant to be shipped, discoverable assets now, not scratch
output. scripts/record-video-guides.mjs writes there by default going forward.

Added a "Video Guides" section to README.md/README.sv.md (Getting started + Interface
tour groups) and a matching in-app "Video guides" panel on the Guide page, right after
the overview cards - the most discoverable spot for a brand-new user. Links point at
/docs/videos/<name>.mkv, served by the server's existing /docs static route.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 18:29:32 +02:00
larssand
337eca2d53 Add 4 more video-guide clips covering core new-user onboarding
connect-decoder, add-drivers-cars, create-race, and run-a-session join the 3 existing
tab-focused clips, so the video guides now cover the actual first-run path a new user
hits, not just this session's UI restructuring.

record-video-guides.mjs gained a prepare() hook (for clips needing a live dependency,
like a fake decoder socket, before the app is seeded) and a clickNav() helper that
exercises the app's real client-side navigation instead of a full page reload, so
switching views in a clip looks like the smooth transition a real user sees.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 18:17:17 +02:00
larssand
a17054d8d0 Switch the video guide narrator from espeak-ng to Piper (neural TTS)
espeak-ng's formant synthesis was reported as barely intelligible. Piper is a small
local neural TTS with the same offline, no-cloud-service properties but far more
natural speech; its default WAV output format already matched what the sync pipeline
assumed, so only the synthesis call itself needed to change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 17:47:31 +02:00
larssand
788319d098 Add spoken narration to the video guide recorder
The clips were silent screen captures - the user's actual ask was for someone talking
through what's happening. Each clip step now carries a narration line synthesized
offline via espeak-ng; the recording paces itself to each line's real spoken duration,
and the final audio track is built from the real measured per-line start times (adelay +
amix) so narration and on-screen action stay in sync over a 30+ second clip instead of
drifting against an idealized schedule.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 17:36:42 +02:00
larssand
4967299b04 Add a video-guide recorder for the UI (Settings tabs, Event Manager tabs, Schedule panel)
CDP screencast -> ffmpeg -> MKV, driven against the same isolated server/browser pattern
the test suite already uses. Recorded three short clips demonstrating this session's tab
and schedule features; output goes to the gitignored video-guides/ folder rather than the
repo since these are sizeable binaries.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 16:56:00 +02:00
larssand
05f0291ee9 Add permanent browser tests for the Settings tabs, Event Manager tabs, and Schedule panel
These three UIs (all shipped earlier this session) had only been verified with throwaway
scratch scripts during development. Adds tests/browser_settings_tabs.js,
browser_event_manager_tabs.js and browser_event_schedule.js, wired into the existing
test:browser suite.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 16:45:24 +02:00
larssand
2624ce8262 Update the in-app Guide for this session's shipped features
Documents the new race-day Schedule feature (new section + overview
card), staggered-start car call-outs and the per-driver "Done" flag,
the new Settings and Event Manager tab layouts, and the driver
transponder-conflict warning. Also corrects a bullet that had gone
stale: it claimed Open/Free Practice was the only thing that skipped
the start ceremony, which stopped being true once scheduled "Practice"
sessions started getting the full sequence too.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 13:54:42 +02:00
larssand
ba4ed30b11 Split Event Manager's race-mode page into tabs
The race-mode management page (participants, teams, race format,
live grid/standings, and now the schedule/print tools) had grown into
a long scroll. Grouped into four tabs - Participants & teams, Format,
Live & results, Schedule & print - matching the tab pattern already
used for Settings. Only wrapper divs were added; no section's markup
or event bindings moved. The existing step-card overview and warning
links now switch to the right tab before jumping to it, and Open Grid
switches to the Live tab so its target is actually visible. Track/
sponsor events, which never had this length problem, are unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 13:47:35 +02:00
larssand
85e18db0ed Give scheduled "Practice" sessions the real start ceremony too
A "practice" session used as a scheduled slot in a structured
race/sponsor event's session plan silently skipped the entire
countdown/lights/start-tone ceremony and jumped straight to running,
unlike qualifying/heat/final. Narrowed the ceremony-skip check to
only the standalone "Quick Open Practice" shortcut and free_practice
(both rolling/best-lap ranked with no fixed roster) - "practice" is
already ranked like a real timed session in every other respect and
now gets the same start sequence.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 23:00:31 +02:00
larssand
cf89086dff Add a time-of-day race day schedule to Event Manager
Events can now be given a start time and a gap between sessions on a
new "Schedule" panel; every session's planned clock time cascades
automatically in running order (practice, qualifying, finals), and
once a session actually starts or finishes, everything after it
re-anchors to that real time - a late start or a session that ran
long (including any pause) pushes the rest of the day forward with
no manual reschedule step. The scheduled time now also shows on the
printed/PDF start lists.

Printable start lists and manually moving a driver to start last in
their own heat (for a mechanic-time request) already existed, so
this only builds the missing piece: the schedule itself.

Also fixes a real pre-existing bug in the same session-ordering logic
this feature relies on: getSessionSortWeight used `order[type] || 99`,
which silently treated open_practice's own weight of 0 as falsy and
sorted practice sessions dead last instead of first in print/PDF
output.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 22:33:57 +02:00
larssand
a449bec07d Group the session picker by event instead of one long flat list
The "select session" dropdown in Timing, and the session replay
picker, listed every session across every event in one long flat
list - unwieldy once a club has generated several races' worth of
practice/qualifying/finals sessions. Both now group into <optgroup>s
by event, most recently created event first.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 22:20:34 +02:00
larssand
e356e30eda Fix staggered heat call-out order and extend its shared clock
getEventDrivers followed the drivers directory's internal creation
order instead of the order participants were actually selected for
the event, so the fallback qualifying/finals grid, the Start List
display, and the staggered-release announcer all read out drivers in
a scrambled order relative to how the event was actually entered.
Fixed to preserve raceConfig.driverIds' own order.

Also, a staggered heat's shared clock/auto-finish target only ever
counted the flat session duration, not the staggered release schedule
itself - so the last-released driver's own personal window could
still be running when the session force-finished. The shared clock
now widens by (driverCount - 1) * staggerGapSec on top of the base
duration for staggered sessions; each driver's own personal quota is
unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 21:59:23 +02:00
larssand
1565baab2a Add staggered-start car call-outs and a per-driver done indicator
Staggered-start qualifying heats now announce each car's transponder
number in grid order (~3s apart) right after the official start,
matching what a track announcer does when releasing cars one at a
time; mass/position starts are unaffected. The Timing leaderboard and
every overlay variant now also show a green "Done" flag next to a
driver's name once their own personal timing window (which already
started at their own first crossing) has elapsed, even while the
session is still running for other drivers.

Also confirmed two things already worked correctly: finals already
always start everyone together with the shared countdown/lights/tone
sequence, and practice's time-up announcement already says "finish
the lap you are on" - only a "2 minutes remaining" cue is genuinely
missing from the existing 1min/30s/10s ladder, left as an open
question for the user.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 21:31:01 +02:00
larssand
ba2d78779f Warn when a driver's transponder collides with a different driver
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>
2026-09-10 20:20:40 +02:00
larssand
50586e8859 Fix malformed race clock format and refresh docs screenshots
formatRaceClock rendered a doubled-up, garbled time (e.g.
"02:48.20:207") everywhere a race clock is shown - Timing, Overlay,
Results archive, Judging, Training stint logs - because it
concatenated both a centiseconds and a milliseconds computation into
the same string. Only the milliseconds part was ever meant to be
there. Found while building realistic demo data for new screenshots,
since every prior test/demo session was empty and never exercised
this code path with a real result.

Also regenerates all screenshots in docs/screenshots against a
realistic seeded race (drivers, cars, qualifying heats, finals,
practice history, a real judging correction) instead of empty demo
data, adds two new ones (Managed AMMC config, the in-app Guide page)
aimed at helping a new user understand initial setup, removes 6
screenshots orphaned by an earlier README revision, and updates both
README.md and README.sv.md's screenshot tables accordingly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 20:03:10 +02:00
larssand
f988ad2690 Fix race-audio Test buttons silently saving settings to the database
Clicking a Test button (general or per-event) in the race-sounds table
called persistRaceAudioFormValues, which both applied the form's
current values and saved them to the backend on every click. A sound
preview should only apply values in memory, not persist them - split
into an apply-only helper used by the Test buttons, keeping the
existing save behavior for the explicit Save button and field changes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 19:28:46 +02:00
larssand
cc9f5c380b Group Settings into tabs instead of one long scroll
Splits the eleven stacked panel sections into four tabs (Connection,
Audio, Appearance & overlay, System & backup) so a specific setting
is a click away rather than a long scroll, per user feedback. Purely
a layout change - no section's markup, ids, or event bindings moved.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 19:19:34 +02:00
larssand
b1b65a9cf9 Add moving a driver between qualifying heats
The user asked whether a lone driver left in the last heat (e.g. 7
drivers at 3 per heat) could be moved into another heat manually.
Investigated first: neither "Reseed" (redistributes every not-yet-run
heat by current standings, not a targeted move) nor the grid editor
(reorders start position within one heat, never moves a driver
between heats) could do this - there was genuinely no path to it.

moveDriversIntoQualifyingHeat (src/event_race_logic.js) adds the same
"checking them elsewhere is the move" pattern already used for team
rosters: a new driver checklist on the qualification session edit
form pulls a checked driver out of whichever other heat they're
currently on, scoped to the same round only ("Q2 H2" never touches
"Q1"/"Q3", since heat composition can legitimately differ round to
round after a reseed). The checklist names the driver's current heat
before saving, mirroring the team-roster warning.

Also confirmed, per the user's earlier questions, already correct
with no changes needed: practice's per-driver "best 3 laps"/"best
5-minute result" stats, and that heats are already seeded fastest-
first from practice results. Flagged one real, deliberately unbuilt
gap while investigating: a true per-driver rolling "5 minutes from
your own first crossing" qualifying *heat* clock isn't supported -
only the practice stats work that way today.

Verified with new unit tests for moveDriversIntoQualifyingHeat and
the extracted parseQualifyingHeatName helper, and a real headless-
browser run reproducing the exact reported scenario end to end
through the actual UI.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 18:18:01 +02:00
larssand
f0838bc5db Fix qualifying/finals/bump-up/judging/pause-timing bugs
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>
2026-09-10 17:41:10 +02:00
larssand
15b574a37d Fix Team Race duplicate-membership bug and two related team-roster gaps
Reported by the user: creating a team, adding the wrong driver, then
moving that driver to another team by ticking them on the new team's
edit form left the driver on both teams at once. Root cause: there
was never a "move" action - the edit form only ever added a driver to
the team being saved, never removed them from wherever they already
were. Live laps from a driver on two teams were then silently
credited to only the first-matching team, with no clear error.

reassignTeamMembers (event_race_logic.js) now strips a driver/car
being saved onto one team out of every other team in the same event,
and the team checkbox list shows which team a driver is already on
before you move them.

Also fixed two related gaps found while investigating, at the user's
request to fix everything: getTeamDriverPool had regressed to
ignoring the event's class/participant scoping entirely (showing
every driver in the club), and deleting a driver or car left them
behind in team rosters forever. Added a duplicate_team_members Race
Day preflight check as defense in depth for any already-corrupted
saved state.

Verified with new unit tests for reassignTeamMembers, getTeamDriverPool
and the new preflight check, plus a new permanent browser test
reproducing the user's exact reported scenario end to end.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 17:07:45 +02:00
larssand
71b3451b13 Show update-available link in the sidebar, not only in Settings
The update card lived inside Settings, easy to miss on an app people
mostly leave on the Dashboard or Timing view. A small link now sits
next to the version number in the sidebar footer, on every view, and
appears automatically from the same startup update check Settings
already used - no new endpoint. Clicking it jumps straight into
Settings with the update ready to run.

Verified with a real headless-browser run against a throwaway git
clone: the link appears on its own without ever opening Settings,
names the real new version, survives navigating to another view, and
clicking it lands on Settings with the update already available there.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 14:51:46 +02:00