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>
This commit is contained in:
larssand
2026-10-07 19:53:23 +02:00
parent 3cc4fa0845
commit 8601954ea2
24 changed files with 556 additions and 60 deletions

View File

@@ -0,0 +1,44 @@
import test from "node:test";
import assert from "node:assert/strict";
import { isSuspectLap, getSuspectReferenceLaps, countsForBestLap, listPendingSuspectLaps, SUSPECT_LAP_RATIO, MIN_REFERENCE_LAPS } from "../src/suspect_laps_logic.js";
import { syncTrainingLedger } from "../src/training_logic.js";
const lap = (lapMs, extra = {}) => ({ competitorKey: "d1", lapMs, timestamp: lapMs, validLap: true, ...extra });
test("a lap well under 85 % of the driver's own median is suspect - only once there are 5 normal laps", () => {
assert.equal(SUSPECT_LAP_RATIO, 0.85);
assert.equal(MIN_REFERENCE_LAPS, 5);
const reference = [20000, 20500, 19800, 20200, 21000]; // median 20 200 -> limit 17 170
assert.equal(isSuspectLap(17000, reference), true, "a shortcut");
assert.equal(isSuspectLap(17500, reference), false, "a genuinely good lap");
assert.equal(isSuspectLap(12000, reference.slice(0, 4)), false, "too little to compare with yet");
});
test("the reference is the driver's own recent normal laps on the same layout", () => {
const passings = [
lap(20000), lap(20000), lap(30000, { stintBoundary: true }), lap(9000, { validLap: false }),
lap(15000, { suspect: true }), lap(25000, { competitorKey: "other" }), lap(40000, { trackLayoutId: "old" }), lap(21000),
];
assert.deepEqual(getSuspectReferenceLaps(passings, "d1", ""), [20000, 20000, 21000]);
});
test("suspect and removed laps never count for best lap; pending ones are listed newest first", () => {
assert.equal(countsForBestLap(lap(15000, { suspect: true })), false);
assert.equal(countsForBestLap(lap(15000, { validLap: false })), false);
assert.equal(countsForBestLap(lap(15000, { suspectApproved: true })), true);
const result = { passings: [lap(15000, { suspect: true, timestamp: 1 }), lap(16000, { suspect: true, validLap: false, timestamp: 2 }), lap(14000, { suspect: true, timestamp: 3 })] };
assert.deepEqual(listPendingSuspectLaps(result).map((passing) => passing.lapMs), [14000, 15000], "a removed suspect lap is no longer pending");
});
test("a suspect lap stays out of the training stats until it is approved", () => {
let n = 0;
const uid = (prefix) => `${prefix}_${++n}`;
const state = {
sessions: [{ id: "op", name: "Open Practice", eventId: "e" }],
events: [{ id: "e", name: "Club" }],
resultsBySession: { op: { passings: [{ ...lap(20000), driverId: "d1", timestamp: 1000 }, { ...lap(15000, { suspect: true }), driverId: "d1", timestamp: 2000 }] } },
};
assert.deepEqual(syncTrainingLedger({}, state, { uid }).ledger.map((entry) => entry.lapMs), [20000]);
Object.assign(state.resultsBySession.op.passings[1], { suspect: false, suspectApproved: true });
assert.deepEqual(syncTrainingLedger({}, state, { uid }).ledger.map((entry) => entry.lapMs), [20000, 15000]);
});