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

@@ -431,6 +431,47 @@ app.post("/api/state", (req, res) => {
res.json({ ok: true, updatedAt: nowIso });
});
// Writes one session's results into app_state and marks them "hot" (see
// protectRecentSessionResults), so a stale full-state save can't revert them.
function storeSessionResult(sessionId, sessionResult) {
if (!sessionResult || typeof sessionResult !== "object") return false;
const row = db.prepare("SELECT state_json FROM app_state WHERE id = 1").get();
if (!row?.state_json) return false;
try {
const parsed = JSON.parse(row.state_json);
if (!parsed.resultsBySession || typeof parsed.resultsBySession !== "object") {
parsed.resultsBySession = {};
}
parsed.resultsBySession[String(sessionId)] = sessionResult;
recentSessionResultWrites.set(String(sessionId), Date.now());
db.prepare(
`
INSERT INTO app_state (id, state_json, updated_at)
VALUES (1, ?, ?)
ON CONFLICT(id) DO UPDATE SET
state_json = excluded.state_json,
updated_at = excluded.updated_at
`
).run(JSON.stringify(parsed), new Date().toISOString());
return true;
} catch {
// leave app_state untouched if the stored JSON is invalid
return false;
}
}
// A deliberate correction (a lap removed, restored or approved, a penalty) on a session that
// is getting live passings: the full-state save would be held back by the "hot results"
// protection for up to RESULT_PROTECTION_WINDOW_MS, so the corrected result is sent here.
app.post("/api/session-results", (req, res) => {
const { sessionId, sessionResult } = req.body || {};
if (!sessionId || !sessionResult || typeof sessionResult !== "object" || !Array.isArray(sessionResult.passings)) {
res.status(400).json({ error: "Expected { sessionId, sessionResult }" });
return;
}
res.json({ ok: storeSessionResult(sessionId, sessionResult) });
});
app.post("/api/passings", (req, res) => {
const { sessionId, passing, sessionResult } = req.body || {};
if (!sessionId || !passing || typeof passing !== "object") {
@@ -474,31 +515,7 @@ app.post("/api/passings", (req, res) => {
// Keep app_state hot for overlay clients that hydrate from backend instead of
// having their own direct decoder socket.
if (sessionResult && typeof sessionResult === "object") {
const nowIso = new Date().toISOString();
const row = db.prepare("SELECT state_json FROM app_state WHERE id = 1").get();
if (row?.state_json) {
try {
const parsed = JSON.parse(row.state_json);
if (!parsed.resultsBySession || typeof parsed.resultsBySession !== "object") {
parsed.resultsBySession = {};
}
parsed.resultsBySession[String(sessionId)] = sessionResult;
recentSessionResultWrites.set(String(sessionId), Date.now());
db.prepare(
`
INSERT INTO app_state (id, state_json, updated_at)
VALUES (1, ?, ?)
ON CONFLICT(id) DO UPDATE SET
state_json = excluded.state_json,
updated_at = excluded.updated_at
`
).run(JSON.stringify(parsed), nowIso);
} catch {
// leave app_state untouched if the stored JSON is invalid
}
}
}
storeSessionResult(sessionId, sessionResult);
res.json({ ok: true });
});