Add a time-of-day race day schedule to Event Manager
Events can now be given a start time and a gap between sessions on a new "Schedule" panel; every session's planned clock time cascades automatically in running order (practice, qualifying, finals), and once a session actually starts or finishes, everything after it re-anchors to that real time - a late start or a session that ran long (including any pause) pushes the rest of the day forward with no manual reschedule step. The scheduled time now also shows on the printed/PDF start lists. Printable start lists and manually moving a driver to start last in their own heat (for a mechanic-time request) already existed, so this only builds the missing piece: the schedule itself. Also fixes a real pre-existing bug in the same session-ordering logic this feature relies on: getSessionSortWeight used `order[type] || 99`, which silently treated open_practice's own weight of 0 as falsy and sorted practice sessions dead last instead of first in print/PDF output. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -231,6 +231,10 @@ export function normalizeEvent(event, { normalizeBrandingConfig, normalizeStartM
|
||||
finalsSource: event?.raceConfig?.finalsSource === "practice" ? "practice" : "qualifying",
|
||||
teams: Array.isArray(event?.raceConfig?.teams) ? event.raceConfig.teams.map((team) => normalizeRaceTeam(team)).filter((team) => team.name) : [],
|
||||
practiceOnly: event?.raceConfig?.practiceOnly === true,
|
||||
scheduleStartAt: Number(event?.raceConfig?.scheduleStartAt) > 0 ? Number(event.raceConfig.scheduleStartAt) : null,
|
||||
// 0 is a valid gap (back-to-back sessions), so this can't use || - only a genuinely
|
||||
// non-numeric/missing value should fall back to the 3-minute default.
|
||||
scheduleGapMin: Math.max(0, Number.isFinite(Number(event?.raceConfig?.scheduleGapMin)) ? Number(event.raceConfig.scheduleGapMin) : 3),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -950,3 +954,50 @@ export function applyBumpsForRace(event, { buildFinalStandings, getSessionsForEv
|
||||
|
||||
return applied;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// A planned run-of-day schedule: given a start time and a gap between sessions, each
|
||||
// session gets a planned clock time in running order (practice, then qualifying, then
|
||||
// finals - the same order the printed start lists already use). Once a session has
|
||||
// actually started or finished, everything after it re-anchors to that real time instead
|
||||
// of the original plan - a heat that started late or ran long (including any pause; a
|
||||
// paused session's real endedAt already reflects the full wall-clock time it took)
|
||||
// automatically pushes the rest of the day forward, with no separate "reschedule" step.
|
||||
export function buildEventSchedule(event, sessions, { getSessionSortWeight }) {
|
||||
const scheduleStartAt = Number(event?.raceConfig?.scheduleStartAt || 0) || null;
|
||||
if (!scheduleStartAt) {
|
||||
return [];
|
||||
}
|
||||
const gapMs = Math.max(0, Number(event?.raceConfig?.scheduleGapMin ?? 3) || 0) * 60000;
|
||||
const ordered = [...sessions]
|
||||
.filter((session) => session.mode === "race")
|
||||
.sort((left, right) => {
|
||||
const weightDiff = getSessionSortWeight(left) - getSessionSortWeight(right);
|
||||
if (weightDiff !== 0) {
|
||||
return weightDiff;
|
||||
}
|
||||
return String(left.name || "").localeCompare(String(right.name || ""));
|
||||
});
|
||||
|
||||
let cursor = scheduleStartAt;
|
||||
return ordered.map((session) => {
|
||||
const plannedStart = cursor;
|
||||
const durationMs = Math.max(1, Number(session.durationMin || 0)) * 60000 + Math.max(0, Number(session.followUpSec || 0)) * 1000;
|
||||
const plannedEnd = plannedStart + durationMs;
|
||||
const actualStart = Number(session.startedAt || 0) || null;
|
||||
const actualEnd = session.status === "finished" ? Number(session.endedAt || 0) || null : null;
|
||||
const effectiveEnd = actualEnd || (actualStart ? actualStart + durationMs : plannedEnd);
|
||||
cursor = effectiveEnd + gapMs;
|
||||
return {
|
||||
sessionId: session.id,
|
||||
plannedStart,
|
||||
plannedEnd,
|
||||
actualStart,
|
||||
actualEnd,
|
||||
// A session that actually started more than a minute after its own planned time is
|
||||
// worth flagging - small drift is normal and not worth calling out row by row.
|
||||
delayed: Boolean(actualStart && actualStart > plannedStart + 60000),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user