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

@@ -99,6 +99,33 @@ test("manual corrections accumulate, reset, and undo to the prior state", () =>
assert.equal(harness.getSaveCount(), 3);
});
test("undoing two time penalties in either order always removes exactly their own contribution", () => {
const forward = createHarness();
applyCompetitorCorrection(forward.session, forward.row, { timeMsDelta: 5000 }, forward.deps);
applyCompetitorCorrection(forward.session, forward.row, { timeMsDelta: 5000 }, forward.deps);
assert.equal(forward.row.manualTimeAdjustmentMs, 10000, "both +5s penalties applied");
const [first, second] = forward.result.adjustments;
// Undo the FIRST one first.
assert.equal(undoJudgingAdjustment(forward.session, first.id, forward.deps), true);
assert.equal(forward.row.manualTimeAdjustmentMs, 5000, "only the first penalty's +5s is removed, the second's stays");
assert.equal(undoJudgingAdjustment(forward.session, second.id, forward.deps), true);
assert.equal(forward.row.manualTimeAdjustmentMs, 0, "both undone, in first-applied order, nets to zero");
const reverse = createHarness();
applyCompetitorCorrection(reverse.session, reverse.row, { timeMsDelta: 5000 }, reverse.deps);
applyCompetitorCorrection(reverse.session, reverse.row, { timeMsDelta: 5000 }, reverse.deps);
const [firstR, secondR] = reverse.result.adjustments;
// Undo the SECOND one first (out of order) - this is exactly the sequence the user
// reported: undoing an earlier penalty used to wipe out both, then undoing the other
// silently re-added one back, leaving a live +5s penalty behind two "undone" entries.
assert.equal(undoJudgingAdjustment(reverse.session, secondR.id, reverse.deps), true);
assert.equal(reverse.row.manualTimeAdjustmentMs, 5000, "only the second penalty's +5s is removed, the first's stays");
assert.equal(undoJudgingAdjustment(reverse.session, firstR.id, reverse.deps), true);
assert.equal(reverse.row.manualTimeAdjustmentMs, 0, "both undone, in reverse order, still nets to zero - order must not matter");
});
test("invalidating and restoring the latest lap recalculates the competitor", () => {
const harness = createHarness();