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>
This commit is contained in:
@@ -282,6 +282,34 @@ test("SQLite API persists state, passings, and overlay session results", { timeo
|
||||
assert.equal(rowsPayload.rows[0].car_id, "car_1");
|
||||
assert.equal(rowsPayload.rows[0].strength, -52.5);
|
||||
assert.deepEqual(JSON.parse(rowsPayload.rows[0].raw_json), passing);
|
||||
|
||||
// A second, unrelated session's passing must survive purging session_1's rows.
|
||||
await fetch(`${baseUrl}/api/passings`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ sessionId: "session_other", passing: { ...passing, timestamp: 18000 } }),
|
||||
});
|
||||
const purgeResponse = await fetch(`${baseUrl}/api/passings/purge`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ sessionIds: ["session_1"] }),
|
||||
});
|
||||
const purgePayload = await purgeResponse.json();
|
||||
assert.equal(purgeResponse.status, 200);
|
||||
assert.equal(purgePayload.ok, true);
|
||||
assert.equal(purgePayload.deleted, 1);
|
||||
|
||||
const afterPurgeSession1 = await (await fetch(`${baseUrl}/api/passings?sessionId=session_1&limit=10`)).json();
|
||||
assert.equal(afterPurgeSession1.rows.length, 0, "the deleted session's rows are gone");
|
||||
const afterPurgeOtherSession = await (await fetch(`${baseUrl}/api/passings?sessionId=session_other&limit=10`)).json();
|
||||
assert.equal(afterPurgeOtherSession.rows.length, 1, "an unrelated session's rows must be untouched");
|
||||
|
||||
const emptyPurgeResponse = await fetch(`${baseUrl}/api/passings/purge`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ sessionIds: [] }),
|
||||
});
|
||||
assert.deepEqual(await emptyPurgeResponse.json(), { ok: true, deleted: 0 });
|
||||
} finally {
|
||||
await stopServer(child);
|
||||
rmSync(dataDir, { recursive: true, force: true });
|
||||
|
||||
Reference in New Issue
Block a user