Commit Graph

210 Commits

Author SHA1 Message Date
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
e9883188fe Test the DNS fix against the other qualifying points systems too
The user asked whether the DNS fix had actually been tested against
the different points systems, not just the default one. It hadn't -
buildQualifyingStandings had never been exercised end to end with
qualifyingPointsTable "field_desc"/"ifmar" (high-points-win scoring)
or qualifyingScoring "best" (ranked by time, not points) at all,
DNS or otherwise.

Added two tests: a DNS driver scored under the high-points field_desc
table (confirms the DNS row still ranks last under an inverted sort
direction, and isn't scored as either zero or the winner's points),
and one under "best time" qualifying (confirms a bad DNS heat can't
contaminate a driver's genuine best result, since bestRank/
bestRoundMetric take the driver's single best heat, not an average).
Both passed immediately, confirming the fix already handled these
correctly - this closes a real, previously-untested gap in confidence
rather than fixing a new bug.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 17:48:23 +02:00
larssand
3232dff25a Add regression test: a DNS heat must not drop a driver's other heats
The user asked specifically to confirm this: a driver who DNS'd in one
qualifying heat but raced normally in others must still be fully
counted across all of them, not dropped. Traced through the fix
(buildQualifyingStandings backfills a DNS row per finished heat,
accumulating into the same per-driver entry across every heat they're
in) and confirmed it already handles this correctly - added a test
that exercises exactly this scenario (DNS in heat 1, wins heat 2 and
3) to lock it in, since this specific multi-heat aggregation path had
no prior coverage.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 17:45:53 +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
larssand
a7272d3844 Scope driver training-profile stats by track layout too
The user pointed out that track layout changes matter most for Open
Practice/training, not races - so the admin Training page and the
driver's own mobile profile (server/public_training_profile.js, the
actual thing most viewed after practice) needed the same layout
scoping already applied to the live "track record" audio cue.

Every stat now splits into two groups: lifetime/commitment numbers
(total laps, sessions, training days, milestone badges) still read a
driver's whole history, since a track rebuild doesn't erase past
practice. Pace/record/consistency numbers (personal best, recent-pace
trend, consistency spread, the "stable pace" control zone, stint
achievements, "best stint") now read only laps tagged with whichever
layout is currently active. Each ledger entry is tagged with
trackLayoutId at the moment it's synced from a session, so it survives
the source session later being deleted.

Verified with new unit tests covering both implementations and a real
headless-browser run confirming both the admin page and the actual
public mobile profile show the current-layout best lap (never a
faster but incomparable old-layout one) while keeping the correct
full lifetime lap count in both places.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 14:37:32 +02:00
larssand
fd6efe2c7e Add track layout tracking so lap-time stats stay comparable
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>
2026-09-10 14:17:06 +02:00
larssand
90df3378a6 Add browser coverage for heat/qualification/final/team_race start sequences
Unlike Open Practice, these session types run the real countdown/light/
audio sequence before a session transitions to "running" - this was
untested beyond Open Practice's instant-start shortcut. Confirms the
Start button shows a real ticking countdown, the right audio events
fire per type (qualifying_start only for qualification), and pause/
resume/finish behave the same as they already do for Open Practice.
All audio effects/voice are silenced via raceAudio.events so the test
depends only on the sequence's own timers, not on real audio/TTS
playback completing in a headless browser with no sound hardware.

Also logs a new roadmap item: indoor clubs that reconfigure the track
layout in the same venue have no way to tell the app a layout change
happened, so PBs/track records/pace baselines silently keep comparing
lap times across incomparable layouts. No design decided yet - open
questions recorded for a future dedicated conversation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 13:53:08 +02:00
larssand
4c463485e0 Add in-app update checker and one-click updater
Settings now shows when a newer version is available, with the real
version number read from the remote's package.json (via the git
binary already required for this deployment model, so it works
against any remote, not just GitHub), and an "Uppdatera nu" button
that stops the server, pulls, reinstalls dependencies and restarts
automatically - warning explicitly if a race is currently active.
The existing app-version poller already reloads the page once a
restart is detected, so no new client-side polling was needed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 22:05:13 +02:00
larssand
2bbbd6e995 Fix Swedish åäö rendering as mojibake in PDF exports
buildSimplePdf's font declared no /Encoding at all, so PDF viewers fell
back to a standard font's built-in encoding, which has no å/ä/ö glyphs
- and separately, the content stream was written as UTF-8 instead of a
single-byte encoding, so even a correctly-encoded font would receive
the wrong bytes (a multi-byte UTF-8 sequence where PDF expects exactly
one byte per character): the classic "åäö" mojibake pattern.

Fixed by declaring /Encoding /WinAnsiEncoding and writing the content
stream through a proper WinAnsi (~CP1252) byte mapper instead of UTF-8.
This is the single shared renderer behind every PDF export (race
results, start lists, heat sheets, team reports, the results archive),
so the fix applies everywhere at once, matching what was reported.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 21:31:01 +02:00
larssand
f1959029c7 Document exercise session history and session-count goals in the Guide
Two new Training walkthrough steps covering the previous commit's
feature: completed-session history (verified server-side, never
just claimed by the mobile page) and the sessions-metric goal option.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 20:46:03 +02:00
larssand
1c4a06b69e Record completed training sessions, credit them, and add session-count goals
A driver completing the practice timer, "Jämna varv", or "Stegträning"
now gets a permanent history entry instead of the outcome only existing
as a one-off notification - visible on both their mobile profile and
the admin Training page, so progress over past sessions is actually
reviewable. Each completion earns a prominent emoji-badged credit.

The completion is verified server-side against the driver's real
recorded laps before being saved, reusing the same progress builders
live tracking already uses - the client can only ask for a completion
to be checked and recorded, never supply the outcome itself, so a
driver can't fabricate a completed session (and the reward tied to it)
any more than they could fabricate a lap time.

Both per-driver and club-wide goals can now target "complete N
sessions of one exercise kind" as an alternative to "N laps", sharing
the existing reward/claim mechanism and forms.

Fixes a real bug found while building this: a pre-existing two-argument
wrapper around getTrainingGoalProgress in app.js silently dropped the
new third argument, so every sessions-metric goal displayed 0 progress
regardless of how many sessions were actually completed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 20:43:47 +02:00
larssand
bed0dfd9f2 Update in-app Guide for this session's features, extend passing purge to Clear data
The in-app Guide page hadn't been updated for the guided practice
exercises, their mobile self-service controls, the end-of-practice
summary, or the new Resultatarkiv view - none of it was documented
anywhere a user would find it. Added three new Training walkthrough
steps and a new Resultatarkiv section (overview card + 5-step guide).

While correcting guide text that claimed raw SQLite passing rows are
always retained, found the same gap the results-archive work already
fixed for deleting a single event/session also applies to "Rensa data"
(Settings -> Clear data): the drivers/cars/races/all-competition-data
scopes all reset resultsBySession for every current session, not just
ones tied to the cleared scope, so every session's raw rows became
orphaned at once. clearDataScope now purges those sessions' rows too,
through the same /api/passings/purge endpoint.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 18:31:03 +02:00
larssand
94794350e5 Add a results archive and purge orphaned passing rows on delete
Until now there was no way to find a completed session without already
knowing which event it belonged to - navigation was strictly Events ->
one event -> its sessions. A new "Resultatarkiv" view lists every
finished session across all events, searchable by event/session name,
class, driver and date range.

Opening a session reuses the existing single-session leaderboard
builder for standings and the existing per-competitor lap lookup for
lap history - no new results logic - plus a session-wide penalties/
corrections list (resultsBySession[id].adjustments), previously only
visible in the live judging action log. Export buttons reuse the
existing per-event PDF and race-package JSON export functions rather
than adding a new format.

Investigated what should happen to raw timing data when a race is
deleted. Training statistics and session replay already read from
their own independent, deliberately-retained copies that survive
deletion by existing design - unaffected either way. The one thing
that outlived deletion with no purpose was the raw `passings` SQLite
rows (write-only; nothing in the app reads them), growing forever with
driver IDs never purged. A new POST /api/passings/purge endpoint now
removes exactly the deleted session(s)' rows, called as a best-effort
side call from both delete-event and delete-session.

Also fixes a real bug found while building this: ensureSessionResult
guarded adjustments/splits/pitEvents/pitStops against being missing on
an already-existing result, but not passings itself, so viewing lap
history for such a result (e.g. an older/imported race package)
crashed outright.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 15:49:10 +02:00
larssand
643902ff15 Add end-of-practice summary to the driver's mobile profile
When a driver's personal 5-minute practice timer finishes, a "Pass
avslutat" card now appears immediately: how this pass's pace and
consistency compared to their recent sessions, any goal or lap-count
milestone crossed during the pass with its reward text made prominent,
and one suggested next goal (reusing the existing pace recommendation,
falling back to the next milestone when there isn't enough data yet).
Scoped to the driver's current class, and explicit about insufficient
sample sizes instead of showing a misleading number.

Adds a per-session consistency calculation to
buildPublicTrainingWindowStats server-side; everything else needed
(stats, goals, recommendation, milestone) was already sent to the
client, so the summary itself is computed there from data already in
hand - no raw laps or driver IDs added to what the public endpoint
exposes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 15:05:49 +02:00
larssand
3373f8dddd Protect live race results and shrink cross-browser staleness
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>
2026-09-09 12:56:58 +02:00
larssand
be9a2de31a Explain a Backend URL/page-origin mismatch instead of a bare fetch error
A real Windows install had sync and backups fail everywhere with
"Failed to fetch" while the server itself was fully healthy - confirmed
via Invoke-WebRequest returning 200 for every endpoint the browser
couldn't reach. The page was opened via a LAN IP while Settings still
had the Backend URL set to 127.0.0.1 from an earlier session; browsers
treat those as different origins, so the server's CORS check correctly
rejected the requests, but a CORS rejection surfaces to fetch() as an
unexplained generic error with no indication of the real cause.

Settings now detects this mismatch on every render and shows the page's
actual address with a one-click button to fix it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 12:06:58 +02:00
larssand
c53f31bf35 Backup directory failures must not crash the whole server
A fresh Windows install reported the server failing around backups.
createBackupService created the backups subdirectory once, synchronously,
at startup with no error handling - any failure there (permissions, a
path beyond Windows' MAX_PATH, a OneDrive/cloud-sync lock, antivirus)
crashed the entire Node process before it started listening, taking
down timing with it instead of just leaving backups unavailable.

Directory creation is now retried lazily and non-fatally on each backup
attempt, and a post-backup file rename retries briefly on EBUSY/EPERM
since a freshly written file is sometimes momentarily locked by
antivirus/cloud-sync scanning on Windows.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 10:59:39 +02:00
larssand
2846b4c0fe Expose guided training exercises on the driver's mobile profile link
"Jämna varv" and "Stegträning" previously only lived on the admin
Training page. A driver's own personal mobile profile link now offers
the same self-service configure/start/stop flow already used for the
5-minute practice timer there, with live progress computed server-side
from the driver's own share token so raw lap rows and driver IDs never
reach the client.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 09:37:38 +02:00
larssand
01e1165748 lägg till stegträning-övning + v1.3.0
Andra guidade träningsövningen i Open Practice: konfigurera en
stegserie (antal varv + lugnt/kontrollerat/snabbt per steg),
förvalt till klassiska pyramiden. Training-sidan visar aktuellt
steg, kvarvarande varv och en progress-pill per steg. När
sekvensen är klar jämförs de två kontrollerade stegens snitt
neutralt (t.ex. "+0.90s"), utan att kalla en försämring för
trötthet.

syncTrainingLedger sprider nu resten av det normaliserade
training-objektet istället för att lista fält manuellt, så
samma klass av bugg som drabbade Jämna varv inte kan upprepas
för framtida fält.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 09:02:44 +02:00
larssand
9122e91f2e lägg till jämna varv-övning + v1.2.0
Ny guidad träningsövning i Open Practice: sätt måltid, tolerans
och antal varv, se aktuell serie och dagens bästa live på
Training-sidan. Ett missat varv startar en ny serie utan att
radera bästa resultatet.

Hittade och fixade två buggar under vägen:
- syncTrainingLedger byggde om training-state vid varje sparning
  utan att föra vidare det nya evenLaps-fältet, så sparade
  konfigurationer försvann direkt
- Training-sidan kraschade för alla förare med färre än 3 varv i
  varje stint (t.ex. de första varven i ett pass) eftersom en
  dedupe-funktion läste en egenskap på ett null-värde

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 08:51:50 +02:00
larssand
d18cd7fb5e decoder reconnect 2026-09-09 08:08:56 +02:00
larssand
09524944dd fix obs popup issues 2026-09-09 07:52:07 +02:00
larssand
acab99ed4d race day tester + fixa open practice start och decoder-omritning
Nytt browser-test för start/paus/fortsätt/finish och byte till
nästa heat i kön, mot en simulerad decoder-socket (ingen riktig
AMMC-hårdvara). Hittade och fixade två buggar under vägen:

- Open Practice på sponsor/track-event kunde inte startas trots
  grönt ljus i förkontrollen (separat, striktare kontroll saknade
  samma undantag)
- Race Day-vyn ritades aldrig om när decodern anslöt/kopplades ner

Testramverket hanterar nu även native alert()/confirm()-dialoger
automatiskt, annars fryser en headless körning helt.
2026-09-09 06:59:54 +02:00
larssand
b3bec531de connecton satus mobil profile 2026-09-08 19:27:37 +02:00
larssand
5559d74846 fix changelog an version 2026-09-08 18:39:04 +02:00
larssand
a0c3d3039e Separat status och mottagningsräknare för varje kanal.
Senaste råmeddelande, fel och ignoreringsorsaker.
Pausa visning, rensa och kopiera logg.
Uppdateras utan att störa inställningsfält eller tidtagning.
2026-09-08 18:37:47 +02:00
larssand
8aedf83826 fix all overlays 2026-09-08 18:26:30 +02:00
larssand
d0889736d0 fix overlay list fields 2026-09-08 17:36:49 +02:00
larssand
443434d569 fix status and roadmap 2026-09-08 17:26:55 +02:00
larssand
9e6fe2386d fix sort and obs panels 2026-09-08 13:21:29 +02:00
larssand
4b5f3422dd update readme 2026-09-07 18:07:01 +02:00
larssand
6e91f921c2 split obs overlay i vänster/höger
Race-namn, logga och klocka till vänster, förarstats-tabellen
för sig till höger. Samma inställningar (rows/showClock/
showFastest/showLaps/showResult/showBest/showGap) styr
fortfarande innehållet, bara positioneringen är ändrad
2026-09-07 17:56:26 +02:00
larssand
1376169cb0 fix overlay sortering, duplicerad esc logik 2026-09-07 17:12:10 +02:00
larssand
a2e29b89dd add sorting 2026-09-05 13:50:11 +02:00
larssand
9c74c1f256 add version 2026-09-05 13:39:50 +02:00
larssand
78c650bde7 raceday kontroller 2026-09-05 09:59:55 +02:00
larssand
bfe13de59c fixrefresh in mbil prof 2026-09-05 09:49:19 +02:00
larssand
130378cc38 fixa font för klass i mobil prof 2026-09-05 09:09:36 +02:00
larssand
4461a947ea fix mobil prfilen 2026-09-05 09:06:35 +02:00
larssand
75ddeccf10 fix fonts 2026-09-04 16:50:23 +02:00
larssand
285babcfcf add more stats to mobile and fix refresh stint 2026-09-04 16:41:51 +02:00
larssand
4068cc7c19 add more stats to mobile profile 2026-09-04 16:17:45 +02:00
larssand
142c45be04 add replay session 2026-09-04 15:58:24 +02:00
larssand
c60b9e0057 fix start script 2026-09-03 20:48:01 +02:00
larssand
1e33e1fffc fix qr code 2026-09-03 20:21:36 +02:00
larssand
2d56b74731 Add local and public QR sharing for training profiles 2026-09-03 20:15:13 +02:00
larssand
4f847610dc Improve training statistics, achievements and stable pace audio 2026-09-03 06:36:17 +02:00
larssand
82d52ed6cd add 2026-09-02 22:04:42 +02:00