feat(race-day): add warm-up transponder check before a heat

Race Day gets a 1-3 minute warm-up per heat with a green check per
driver when their transponder is seen, countdown announcements
(sv/en), quick transponder fix from the panel, and a start warning
that still allows starting when someone is missing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
larssand
2026-09-30 20:47:15 +02:00
parent 627179b95a
commit 1bc75f8ec1
20 changed files with 489 additions and 11 deletions

View File

@@ -0,0 +1,52 @@
import test from "node:test";
import assert from "node:assert/strict";
import { recordTransponderSighting, buildWarmupCheck, getWarmupRemainingMs } from "../src/warmup_logic.js";
const entrants = [
{ id: "a", name: "Anna", transponder: "1001" },
{ id: "b", name: "Bo", transponder: "1002" },
{ id: "c", name: "Cia", transponder: "" },
];
test("a driver is green only once their transponder crosses after the warm-up started", () => {
const sightings = new Map();
recordTransponderSighting(sightings, "1001", 500); // before the warm-up - must not count
recordTransponderSighting(sightings, "1002", 1500);
let check = buildWarmupCheck({ entrants, sightings, since: 1000 });
assert.deepEqual(check.rows.map((row) => row.seen), [false, true, false]);
assert.equal(check.seenCount, 1);
assert.equal(check.total, 3);
assert.deepEqual(check.missing.map((row) => row.name), ["Anna", "Cia"], "a driver with no transponder is never green");
recordTransponderSighting(sightings, "1001", 2000);
check = buildWarmupCheck({ entrants, sightings, since: 1000 });
assert.equal(check.rows[0].seen, true);
assert.equal(check.rows[0].lastSeenAt, 2000);
});
test("unknown transponders are listed newest first, naming the owner if it belongs to another driver", () => {
const sightings = new Map();
recordTransponderSighting(sightings, "9999", 1100);
recordTransponderSighting(sightings, "7777", 1200);
recordTransponderSighting(sightings, "1001", 1300);
recordTransponderSighting(sightings, "5555", 900); // before the warm-up
const check = buildWarmupCheck({ entrants, drivers: [...entrants, { id: "x", name: "Xerxes", transponder: "7777" }], sightings, since: 1000 });
assert.deepEqual(check.unknownTransponders.map((item) => [item.transponder, item.ownerName]), [["7777", "Xerxes"], ["9999", ""]]);
});
test("sightings count repeat passings and stay bounded", () => {
const sightings = new Map();
recordTransponderSighting(sightings, " 42 ", 1);
recordTransponderSighting(sightings, "42", 2);
assert.equal(sightings.get("42").count, 2);
recordTransponderSighting(sightings, "", 3);
assert.equal(sightings.size, 1);
for (let i = 0; i < 600; i += 1) recordTransponderSighting(sightings, `t${i}`, 10 + i);
assert.equal(sightings.size, 500);
assert.equal(sightings.has("t599"), true, "the newest are kept");
});
test("warm-up countdown", () => {
assert.equal(getWarmupRemainingMs(null), 0);
assert.equal(getWarmupRemainingMs({ startedAt: 1000, durationMs: 60000 }, 31000), 30000);
assert.equal(getWarmupRemainingMs({ startedAt: 1000, durationMs: 60000 }, 99000), 0);
});