fix(lap): keep best laps per track layout and count the current session for records

A layout switch during a running open practice kept every driver's
old-layout best lap, blocking personal-best calls and skewing the
practice leaderboard. Best laps now restart on a new layout and each
lap records its layout. The track record baseline now includes the
current session so the same record isn't called twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
larssand
2026-10-03 21:14:19 +02:00
parent ebd0f74f40
commit 8b63dd2372
12 changed files with 125 additions and 8 deletions

View File

@@ -5046,9 +5046,17 @@ function addTrackLayout(label, startedAtMs = Date.now()) {
state.trackLayouts = normalizeTrackLayouts(layouts, { uid });
// A practice session that keeps running across the switch moves onto the new layout;
// its earlier laps stay on the old one because training laps are tagged per lap.
// Its best laps start over too: personal bests, session fastest and the leaderboard's best
// lap are per layout, so the old layout's times can't block or fake a new best.
if (startedAtMs <= Date.now()) {
state.sessions.forEach((session) => {
if (isPracticeSessionType(session.type) && ["running", "paused"].includes(session.status)) session.trackLayoutId = layout.id;
if (!isPracticeSessionType(session.type) || !["running", "paused"].includes(session.status)) return;
session.trackLayoutId = layout.id;
Object.values(state.resultsBySession?.[session.id]?.competitors || {}).forEach((entry) => {
entry.bestLapMs = null;
entry.bestLapLayoutId = layout.id;
delete lastOverlayBestLapByKey[`${session.id}:${entry.key}`];
});
});
}
saveState();

View File

@@ -337,6 +337,7 @@ export function processDecoderMessageHelper(msg, deps, decoderRole = "main") {
laps: 0,
lastLapMs: null,
bestLapMs: null,
bestLapLayoutId: String(session.trackLayoutId || ""),
startTimestamp: null,
lastTimestamp: null,
};
@@ -494,6 +495,7 @@ export function processDecoderMessageHelper(msg, deps, decoderRole = "main") {
stintBoundary: validationWarning === "above_max_stint",
strength: msg.strength,
loopId: String(msg.loop_id || ""),
trackLayoutId: String(session.trackLayoutId || ""),
resend: Boolean(msg.resend),
};
@@ -505,6 +507,12 @@ export function processDecoderMessageHelper(msg, deps, decoderRole = "main") {
return true;
}
// Best laps are per track layout: an open practice that runs all season moves onto a new
// layout when one is added (addTrackLayout resets every best lap then), and this catches a
// layout change made elsewhere. Entries from before this field existed adopt the current one.
const layoutId = String(session.trackLayoutId || "");
if (entry.bestLapLayoutId === undefined) entry.bestLapLayoutId = layoutId;
else if (entry.bestLapLayoutId !== layoutId) { entry.bestLapMs = null; entry.bestLapLayoutId = layoutId; }
const previousPersonalBest = Number(entry.bestLapMs || 0) || null;
const leaderboardBefore = buildLeaderboard(session);
const previousPosition = leaderboardBefore.findIndex((row) => row.key === key);

View File

@@ -83,7 +83,12 @@ export function recalculateCompetitorFromPassings(session, rowKey, { ensureSessi
entry.lastTimestamp = validPassings.length
? Number(validPassings[validPassings.length - 1].timestamp || 0) || entry.startTimestamp || session.startedAt || null
: entry.startTimestamp || session.startedAt || null;
const bestLapCandidates = validPassings.map((passing) => Number(passing.lapMs || 0)).filter((lapMs) => lapMs > 500);
// Only laps on the session's current track layout count for the best lap (passings from
// before layouts were tagged have no trackLayoutId and still count).
const layoutId = String(session.trackLayoutId || "");
const bestLapCandidates = validPassings
.filter((passing) => passing.trackLayoutId === undefined || String(passing.trackLayoutId) === layoutId)
.map((passing) => Number(passing.lapMs || 0)).filter((lapMs) => lapMs > 500);
entry.bestLapMs = bestLapCandidates.length ? Math.min(...bestLapCandidates) : null;
}

View File

@@ -165,9 +165,10 @@ export function ensureSessionResult(sessionId, { state }) {
export function getTrackRecordMs(session, { state }) {
const event = state.events.find((item) => item.id === session?.eventId);
if (!event?.classId) return null;
// The current session counts too: once someone beats the old record in this session, the
// next driver must beat that new time - not the old record again - to be called a record.
const candidates = state.sessions.filter((item) =>
item.id !== session?.id
&& String(item.trackLayoutId || "") === String(session?.trackLayoutId || "")
String(item.trackLayoutId || "") === String(session?.trackLayoutId || "")
&& state.events.find((race) => race.id === item.eventId)?.classId === event.classId);
const values = candidates
.flatMap((item) => Object.values(state.resultsBySession[item.id]?.competitors || {}).map((row) => Number(row.bestLapMs || 0)))