Min varvtid (sek)
Max varvtid (sek)
This commit is contained in:
101
src/app.js
101
src/app.js
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user