Min varvtid (sek)

Max varvtid (sek)
This commit is contained in:
larssand
2026-03-15 15:17:27 +01:00
parent 506d81a1ed
commit 604ec28030
2 changed files with 112 additions and 25 deletions

View File

@@ -207,6 +207,10 @@ const TRANSLATIONS = {
"events.finals_from_qualifying": "Kval-ranking",
"events.finals_from_practice": "Practice-ranking",
"events.finals_source_hint": "Välj om finalerna ska seedas från practice eller kval.",
"events.min_lap_time": "Min varvtid (sek)",
"events.min_lap_time_hint": "Varv snabbare än detta ignoreras som shortcut eller felträff.",
"events.max_lap_time": "Max varvtid (sek)",
"events.max_lap_time_hint": "Varv långsammare än detta räknas inte som giltigt varv och bryter lap-basen för nästa varv.",
"events.race_driver_scope": "Race i denna klass använder alla förare i vald klass om sessionen inte har egen deltagarlista.",
"events.reserve_bump_slots_hint": "Reserverar tomma platser i högre finaler så bumpade förare kan flyttas in utan att skriva över seedade platser.",
"events.team_race": "Lagrace",
@@ -724,6 +728,10 @@ const TRANSLATIONS = {
"events.finals_from_qualifying": "Qualifying standings",
"events.finals_from_practice": "Practice standings",
"events.finals_source_hint": "Choose whether finals should be seeded from practice or qualifying.",
"events.min_lap_time": "Min lap time (sec)",
"events.min_lap_time_hint": "Laps faster than this are ignored as shortcuts or false hits.",
"events.max_lap_time": "Max lap time (sec)",
"events.max_lap_time_hint": "Laps slower than this are not counted as valid laps and reset the lap base for the next lap.",
"events.race_driver_scope": "Race mode uses all drivers in the event class unless a session has its own participant list.",
"events.reserve_bump_slots_hint": "Reserve empty slots in higher finals so bumped drivers can be inserted without overwriting seeded spots.",
"events.team_race": "Team Race",
@@ -1570,6 +1578,8 @@ function normalizeEvent(event) {
countedFinalLegs: Math.max(1, Number(event?.raceConfig?.countedFinalLegs || 1) || 1),
finalDurationMin: Math.max(1, Number(event?.raceConfig?.finalDurationMin || 5) || 5),
finalStartMode: normalizeStartMode(event?.raceConfig?.finalStartMode || "position"),
minLapMs: Math.max(0, Number(event?.raceConfig?.minLapMs || 0) || 0),
maxLapMs: Math.max(0, Number(event?.raceConfig?.maxLapMs || 60000) || 60000),
bumpCount: Math.max(0, Number(event?.raceConfig?.bumpCount || 0) || 0),
reserveBumpSlots: event?.raceConfig?.reserveBumpSlots !== false,
driverIds: Array.isArray(event?.raceConfig?.driverIds) ? event.raceConfig.driverIds : [],
@@ -3118,6 +3128,16 @@ function renderEventManager(eventId) {
<option value="staggered" ${event.raceConfig.finalStartMode === "staggered" ? "selected" : ""}>${t("events.start_mode_staggered")}</option>
</select>`
)}
${renderRaceFormatField(
"events.min_lap_time",
"events.min_lap_time_hint",
`<input type="number" min="0" step="0.1" name="minLapSec" value="${((event.raceConfig.minLapMs || 0) / 1000).toFixed(1)}" />`
)}
${renderRaceFormatField(
"events.max_lap_time",
"events.max_lap_time_hint",
`<input type="number" min="1" step="0.1" name="maxLapSec" value="${((event.raceConfig.maxLapMs || 60000) / 1000).toFixed(1)}" />`
)}
${renderRaceFormatField(
"events.bump_count",
"events.bump_count_hint",
@@ -3672,6 +3692,8 @@ function renderEventManager(eventId) {
countedFinalLegs: Math.max(1, Number(form.get("countedFinalLegs") || 1) || 1),
finalDurationMin: Math.max(1, Number(form.get("finalDurationMin") || 5) || 5),
finalStartMode: normalizeStartMode(String(form.get("finalStartMode") || "position")),
minLapMs: Math.max(0, Math.round((Number(form.get("minLapSec") || 0) || 0) * 1000)),
maxLapMs: Math.max(1000, Math.round((Number(form.get("maxLapSec") || 60) || 60) * 1000)),
bumpCount: Math.max(0, Number(form.get("bumpCount") || 0) || 0),
reserveBumpSlots: form.get("reserveBumpSlots") === "on",
driverIds: event.raceConfig.driverIds || [],
@@ -3950,7 +3972,7 @@ function renderTiming() {
<article class="timing-session-stat"><span>${t("timing.started")}</span><strong>${active.startedAt ? new Date(active.startedAt).toLocaleTimeString() : "-"}</strong></article>
<article class="timing-session-stat"><span>${t("table.start_mode")}</span><strong>${escapeHtml(getStartModeLabel(active.startMode))}</strong></article>
<article class="timing-session-stat"><span>${t("timing.seeding_mode")}</span><strong>${active.seedBestLapCount > 0 ? `${active.seedBestLapCount}` : "-"}</strong></article>
<article class="timing-session-stat"><span>${t("timing.total_passings")}</span><strong>${result.passings.length}</strong></article>
<article class="timing-session-stat"><span>${t("timing.total_passings")}</span><strong>${getVisiblePassings(result).length}</strong></article>
</div>
${
active.type === "free_practice"
@@ -4381,7 +4403,7 @@ function renderOverlay() {
const overlayClock = sessionTiming?.untimed
? formatElapsedClock(sessionTiming?.elapsedMs ?? 0)
: formatCountdown(sessionTiming?.remainingMs ?? 0);
const recent = active && result ? result.passings.slice(-8).reverse() : [];
const recent = active && result ? getVisiblePassings(result).slice(-8).reverse() : [];
const event = active ? state.events.find((item) => item.id === active.eventId) : null;
const branding = resolveEventBranding(event);
const practiceRows = event ? buildPracticeStandings(event) : [];
@@ -4442,7 +4464,7 @@ function renderOverlay() {
(passing) => `
<div class="overlay-passing">
<strong>${escapeHtml(passing.displayName || passing.teamName || passing.driverName || t("common.unknown_driver"))}</strong>
<span>${new Date(passing.timestamp).toLocaleTimeString()}</span>
<span>${formatLap(passing.lapMs)}</span>
</div>
`
)
@@ -4503,7 +4525,7 @@ function renderOverlay() {
<strong>${formatLap(fastestRow?.bestLapMs)}</strong>
</div>
<div class="overlay-fastest-driver">${escapeHtml(fastestRow?.displayName || fastestRow?.driverName || "-")}</div>
<div class="overlay-fastest-meta">${t("table.laps")}: ${topRow?.laps || 0} | ${t("timing.total_passings")}: ${result?.passings.length || 0}</div>
<div class="overlay-fastest-meta">${t("table.laps")}: ${topRow?.laps || 0} | ${t("timing.total_passings")}: ${getVisiblePassings(result).length || 0}</div>
</section>
<div class="overlay-leaderboard-card overlay-leaderboard-card-tv overlay-leaderboard-card-dense">
${renderOverlayLeaderboard(leaderboard)}
@@ -4563,7 +4585,7 @@ function buildOverlayPanels(active, recent) {
(passing) => `
<div class="overlay-passing">
<strong>${escapeHtml(passing.displayName || passing.teamName || passing.driverName || passing.transponder || t("common.unknown_driver"))}</strong>
<span>${new Date(passing.timestamp).toLocaleTimeString()}</span>
<span>${formatLap(passing.lapMs)}</span>
</div>
`
)
@@ -4840,7 +4862,7 @@ function renderTeamOverlay(rows, result, sessionTiming) {
</article>
<article class="overlay-stat-card">
<span>${t("timing.total_passings")}</span>
<strong>${result?.passings.length || 0}</strong>
<strong>${getVisiblePassings(result).length || 0}</strong>
<small>${sessionTiming?.untimed ? t("timing.elapsed") : t("timing.remaining")}</small>
</article>
<article class="overlay-stat-card">
@@ -4868,7 +4890,7 @@ function renderRecentPassings(session) {
return `<p>${t("timing.no_session_selected")}</p>`;
}
const result = ensureSessionResult(session.id);
const items = result.passings.slice(-20).reverse();
const items = getVisiblePassings(result).slice(-20).reverse();
if (!items.length) {
return `<p>${t("timing.no_passings")}</p>`;
}
@@ -5245,6 +5267,25 @@ function getSessionTargetMs(session) {
return Math.max(1, Number(session?.durationMin || 0)) * 60 * 1000;
}
function getSessionLapWindow(session) {
const event = state.events.find((item) => item.id === session?.eventId);
if (event?.mode !== "race") {
return { minLapMs: 0, maxLapMs: Number.POSITIVE_INFINITY };
}
const minLapMs = Math.max(0, Number(event?.raceConfig?.minLapMs || 0) || 0);
const configuredMaxLapMs = Math.max(0, Number(event?.raceConfig?.maxLapMs || 60000) || 60000);
const maxLapMs = configuredMaxLapMs > 0 ? Math.max(configuredMaxLapMs, minLapMs || 0) : Number.POSITIVE_INFINITY;
return { minLapMs, maxLapMs };
}
function isCountedPassing(passing) {
return passing?.validLap !== false;
}
function getVisiblePassings(result) {
return Array.isArray(result?.passings) ? result.passings.filter((passing) => isCountedPassing(passing)) : [];
}
function getSessionTiming(session, nowTs = Date.now()) {
const targetMs = getSessionTargetMs(session);
const startedAt = Number(session?.startedAt || 0);
@@ -5439,13 +5480,15 @@ function processDecoderMessage(msg) {
const baseTs = entry.lastTimestamp || entry.startTimestamp || session.startedAt || timestamp;
const lapMs = Math.max(0, timestamp - baseTs);
entry.laps += 1;
entry.lastLapMs = lapMs;
entry.lastTimestamp = timestamp;
if (lapMs > 500 && (!entry.bestLapMs || lapMs < entry.bestLapMs)) {
entry.bestLapMs = lapMs;
const { minLapMs, maxLapMs } = getSessionLapWindow(session);
let validLap = true;
let invalidReason = "";
if (minLapMs > 0 && lapMs > 0 && lapMs < minLapMs) {
validLap = false;
invalidReason = "below_min";
} else if (Number.isFinite(maxLapMs) && maxLapMs > 0 && lapMs > maxLapMs) {
validLap = false;
invalidReason = "above_max";
}
const passing = {
@@ -5461,11 +5504,31 @@ function processDecoderMessage(msg) {
carName: entry.carName,
competitorKey: key,
lapMs,
validLap,
invalidReason,
strength: msg.strength,
loopId: String(msg.loop_id || ""),
resend: Boolean(msg.resend),
};
if (!validLap) {
result.passings.push(passing);
if (invalidReason === "above_max") {
entry.lastTimestamp = timestamp;
}
persistPassingToBackend(session.id, passing);
saveState();
return;
}
entry.laps += 1;
entry.lastLapMs = lapMs;
entry.lastTimestamp = timestamp;
if (lapMs > 500 && (!entry.bestLapMs || lapMs < entry.bestLapMs)) {
entry.bestLapMs = lapMs;
}
result.passings.push(passing);
persistPassingToBackend(session.id, passing);
pushOverlayEvent("passing", `${entry.displayName || entry.driverName}${formatLap(entry.lastLapMs)}`);
@@ -5887,10 +5950,13 @@ function getCompetitorElapsedMs(session, row) {
return Math.max(0, row.lastTimestamp - startTs);
}
function getCompetitorPassings(session, row) {
function getCompetitorPassings(session, row, options = {}) {
const result = ensureSessionResult(session.id);
return result.passings
.filter((passing) => {
if (!options.includeInvalid && !isCountedPassing(passing)) {
return false;
}
if (passing.competitorKey) {
return passing.competitorKey === row.key;
}
@@ -6121,13 +6187,16 @@ function buildTeamStintLog(session, row) {
return [];
}
const { maxLapMs } = getSessionLapWindow(session);
const stintGapMs = Number.isFinite(maxLapMs) ? maxLapMs : Number.POSITIVE_INFINITY;
const stints = [];
let current = null;
passings.forEach((passing) => {
const memberLabel = formatTeamActiveMemberLabel(passing);
const memberKey = `${passing.driverId || passing.driverName || "-"}|${passing.carId || passing.carName || "-"}`;
if (!current || current.memberKey !== memberKey) {
const gapBreak = current && Number.isFinite(stintGapMs) && Math.max(0, passing.timestamp - current.endTs) > stintGapMs;
if (!current || current.memberKey !== memberKey || gapBreak) {
current = {
memberKey,
memberLabel,