Fix staggered heat call-out order and extend its shared clock

getEventDrivers followed the drivers directory's internal creation
order instead of the order participants were actually selected for
the event, so the fallback qualifying/finals grid, the Start List
display, and the staggered-release announcer all read out drivers in
a scrambled order relative to how the event was actually entered.
Fixed to preserve raceConfig.driverIds' own order.

Also, a staggered heat's shared clock/auto-finish target only ever
counted the flat session duration, not the staggered release schedule
itself - so the last-released driver's own personal window could
still be running when the session force-finished. The shared clock
now widens by (driverCount - 1) * staggerGapSec on top of the base
duration for staggered sessions; each driver's own personal quota is
unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
larssand
2026-09-10 21:59:23 +02:00
parent 1565baab2a
commit e356e30eda
7 changed files with 94 additions and 5 deletions

View File

@@ -548,7 +548,16 @@ export function getEventDrivers(event, { state }) {
if (!event?.raceConfig?.participantsConfigured) {
return classDrivers;
}
return classDrivers.filter((driver) => (event.raceConfig.driverIds || []).includes(driver.id));
// Preserves the order participants were selected in (event.raceConfig.driverIds), not
// the club's global driver-directory order - the two rarely match, and anything seeded
// from this list (a fallback qualifying/finals grid with no practice data yet, a
// staggered heat's start-order announcement) should follow the order a club admin
// actually built the entry list in, not an unrelated internal array order.
const classDriverIds = new Set(classDrivers.map((driver) => driver.id));
const byId = new Map(classDrivers.map((driver) => [driver.id, driver]));
return (event.raceConfig.driverIds || [])
.filter((id) => classDriverIds.has(id))
.map((id) => byId.get(id));
}

View File

@@ -79,7 +79,20 @@ export function getPassingValidationLabel(passing, { t }) {
export function getSessionTiming(session, nowTs = Date.now(), { getSessionTargetMs }) {
const targetMs = getSessionTargetMs(session);
const baseTargetMs = getSessionTargetMs(session);
// A staggered heat doesn't release every car at once (see runStartSequence in
// race_audio.js) - the shared clock and auto-finish threshold need to cover the full
// release schedule too, or the last-released driver's own personal window could still
// be running when the session force-finishes and locks everyone out. Each competitor's
// OWN personal duration (used for buildLeaderboard's personalTimeUp) stays exactly
// durationMin - this only widens the session-level clock, not any individual's quota.
const gridCount = Array.isArray(session?.manualGridIds) && session.manualGridIds.length
? session.manualGridIds.length
: Array.isArray(session?.driverIds) ? session.driverIds.length : 0;
const staggerExtensionMs = session?.startMode === "staggered" && baseTargetMs !== null && gridCount > 1
? (gridCount - 1) * Math.max(0, Number(session?.staggerGapSec || 3) || 3) * 1000
: 0;
const targetMs = baseTargetMs === null ? null : baseTargetMs + staggerExtensionMs;
const startedAt = Number(session?.startedAt || 0);
const pausedAt = Number(session?.pausedAt || 0) || 0;
const endedAt = Number(session?.endedAt || 0) || 0;