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>
This commit is contained in:
larssand
2026-09-10 17:41:10 +02:00
parent 15b574a37d
commit f0838bc5db
14 changed files with 464 additions and 36 deletions

View File

@@ -221,6 +221,34 @@ Verified 2026-09-10 (sidebar link): a full headless-browser run against a throwa
Verified 2026-09-09: a new integration test (`tests/server_persistence.integration.test.js`) builds a throwaway local bare git repo plus two clones (standing in for "this installation" and "somewhere a newer commit was pushed"), confirms `available: false` initially, pushes a version bump from the second clone, confirms `available: true` with the correct `remoteVersion` read from its `package.json`, and confirms the 10-minute cache is respected until `force=1`. A full headless-browser run went further than any test this session before it: cloned the real project into a throwaway git working copy (own bare "remote", own temporary SQLite data dir - the real installation and its git history were never touched), started the real server from that clone, pushed a version bump to the throwaway remote from a second clone, then in a real browser opened Settings, saw "Version 9.9.9 finns tillgänglig", clicked "Uppdatera nu", accepted the confirmation dialog, and confirmed end-to-end: the server actually stopped, `git pull --ff-only` fast-forwarded, `npm install` ran, the server came back up on the same port serving the new code, and the open browser tab auto-reloaded on its own and displayed the new version - with zero uncaught JS exceptions throughout.
## Race flow correctness: qualifying, finals, bump-up, judging, pause timing
Reported 2026-09-10 by the user, asking specifically about practice/qualifying/heats/finals, bump-up, judging, pause/start, timers and audio. A thorough read-only audit of the actual code (not assumption) found several real, concrete bugs across this area - fixed together, at the user's request ("don't want those bugs").
**Qualifying reseed** (`reseedUpcomingQualifying` in `src/event_race_logic.js`):
- [x] Heats were sorted by comparing the full session name as text ("H10" sorts before "H2" lexicographically), scrambling the grid for any round with 10+ heats. Now sorts by the numeric heat number the naming regex already captured but was discarding.
- [x] If the participant count changed since a round was generated (drivers added or removed), the extra drivers were silently dropped, or an existing heat was left with an empty "ghost" grid - with no indication anything was wrong. Reseed now still updates whichever heats line up, but reports a clear warning naming how many rounds no longer match, telling the admin to regenerate instead of trusting a partial reseed.
- [x] The function's own return shape was inconsistent (a bare `0` on one early-return path, `{updated, locked}` on the normal path) - the caller silently read `undefined` in that case. Now always returns the same shape.
**A driver with zero laps in a heat or final (DNS)** used to vanish entirely rather than showing as a real, last-place result - not just from that round's standings, but silently from every future round they'd otherwise be reseeded into, and from the final grid entirely, because `buildQualifyingStandings`/`buildFinalStandings` (`src/timing_logic.js`) only ever saw drivers who had at least one recorded passing. Both now backfill a driver assigned to a *finished* heat/final who never appears in its results as an explicit "Ej start (DNS)" row, ranked last - a still-running or not-yet-started session's assigned drivers are correctly left alone, since they simply haven't raced yet.
**Bump-up** (`generateFinalsForRace`/`applyBumpsForRace` in `src/event_race_logic.js`):
- [x] With "reserve bump slots" turned off, bump-up did *nothing at all*, silently. Mains are seeded to full capacity when slots aren't reserved, and the old code only ever let a bumped driver in if the main had room below its own capacity - which it never did. Bump-up now reads the event's actual reserve-slots setting to decide whether to cap the main at its nominal size (reserved) or simply grow it by the bumped driver(s) (not reserved), instead of conflating "never reserved any room" with "already used up the room that was reserved."
- [x] A promoted driver stayed gridded in the lower main's still-upcoming leg(s) at the same time as their new, higher main - the same kind of double-entry the team-roster bug had, for multi-leg finals. Bump-up now also removes a promoted driver from the lower main's remaining (not-yet-run) legs; the leg that already produced the promotion decision keeps its real historical result untouched.
- [x] An oversized bump count (relative to the final's size) could seed as few as one driver per main, and with a large enough field that ran the A/B/C... main-letter naming past "Z" into unrelated punctuation characters - which then all silently collapsed onto main "A" when standings were read back. `bumpCount` is now clamped below the final size, and main width is additionally widened as needed so a generation run never produces more than 26 mains.
**Judging** (`applyCompetitorCorrection`/`undoJudgingAdjustment` in `src/judging_logic.js`): undoing a manual correction restored an *absolute* snapshot of the value from just before that specific correction was first applied, rather than reversing only that correction's own contribution. Two +5s penalties undone out of order (the first one undone first) wiped out both penalties on the first undo, then silently reinstated one of them on the second undo - leaving the driver with a live penalty behind two log entries both marked "undone", no warning anywhere. Undo now reverses each correction's own recorded delta from whatever the current cumulative value is, which is correct regardless of undo order.
**Pause timing**:
- [x] A competitor's own elapsed/comparison time (`buildLeaderboard`, `getCompetitorElapsedMs` in `src/timing_logic.js`) didn't exclude paused wall-clock time, even though the session's own countdown already did (`getSessionTiming`). A heat that had a track call wasn't comparable to one that didn't for qualifying ranking purposes, even though nobody actually lost race time to the stoppage. Fixed by subtracting the session's accumulated pause time from a competitor's elapsed time too - correct as long as passings aren't recorded while paused, which they aren't (the decoder already ignores them: `session.status !== "running"` is checked before any passing is processed).
- [x] Finishing a session while it was paused (`finishSession` in `src/session_control.js`) never folded that final, still-open pause segment into `totalPausedMs` the way resuming does - so a finished session's displayed elapsed time kept growing after finish, using how long ago it finished rather than how long it actually ran. `getSessionTiming` also now freezes a finished session's clock at `endedAt` rather than the live clock, so simply reopening or leaving a finished session's timing view open no longer shows a number that keeps climbing.
Also checked and confirmed already correct, no changes needed: the countdown/light start sequence's cancellation (no stuck "ready" state), the `one_minute_remaining`/`thirty_seconds_remaining`/`ten_seconds_remaining`/`final_lap` audio cues (each fires exactly once per start regardless of pauses, since they're gated on `elapsedMs`, which already excluded pause time correctly), and multi-cycle pause/resume accumulation and pause-during-follow-up math.
Not built in this pass, out of scope for a bug-fixing sweep: there is no disqualification/stop-and-go/drive-through judging action at all today (only lap/time corrections and invalidation) - the corresponding audio cues exist in `RACE_AUDIO_EVENT_DEFINITIONS` but nothing in the app ever triggers them. That's a missing feature, not a bug, and would need its own design pass.
Verified 2026-09-10: new unit tests cover every fix above - `tests/race_timing_logic.test.js` (numeric heat sort, field-size mismatch reporting, DNS backfill in both qualifying and final standings without ever inventing a fake 0ms "fastest lap", the reserve/non-reserve bump-up paths, the 26-main cap, pause exclusion in `buildLeaderboard`/`getCompetitorElapsedMs`, the frozen finished-session clock), a new `tests/session_control.test.js` (previously zero coverage for this file - pause/resume accumulation, follow-up pause, and the finish-while-paused fold-in), and `tests/judging_logic.test.js` (undoing two penalties in both orders, confirming the net always reaches zero). A full headless-browser run against a real, isolated server confirmed both the judging undo fix (two +5s penalties applied via the real Judging UI, undone out of order through the real undo buttons, correctly netting to +5000ms after the first undo and exactly 0 after both) and the bump-up fix (a real Event Manager "Apply bumps" click, with `reserveBumpSlots` off, actually grew the upper final's grid instead of silently doing nothing) end to end.
## Documentation maintenance
- [ ] Update the architecture's module/dependency inventory as modules change.