feat(layouts): optional min/max lap time per track layout for practice

Each layout can carry its own lap window (set on creation, editable in
Settings, saved from Race Day during practice). Open/Free practice on
that layout uses it; races always keep the event's values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
larssand
2026-10-07 20:33:22 +02:00
parent 8601954ea2
commit 9d79bcb4d3
13 changed files with 174 additions and 15 deletions

View File

@@ -67,6 +67,7 @@ export function renderSettings(deps) {
nextTrackLayoutLabel,
addTrackLayout,
deleteTrackLayout,
updateTrackLayoutLapWindow = () => ({ ok: false }),
updateStatus,
updateStatusLoaded,
updateCheckPending,
@@ -306,14 +307,21 @@ export function renderSettings(deps) {
now.setMinutes(now.getMinutes() - now.getTimezoneOffset());
return now.toISOString().slice(0, 16);
})())}" /></label>
<label><span>${t("layouts.min_lap")}</span><input name="minLapSec" type="number" min="0" step="0.1" placeholder="${escapeHtml(t("layouts.lap_from_event"))}" /></label>
<label><span>${t("layouts.max_lap")}</span><input name="maxLapSec" type="number" min="0" step="0.1" placeholder="${escapeHtml(t("layouts.lap_from_event"))}" /></label>
<div class="actions"><button class="btn btn-primary" type="submit">${t("settings.track_layout_add")}</button></div>
</form>
<p class="hint">${t("layouts.lap_window_hint")}</p>
<div class="backup-list mt-16">
${(trackLayouts || []).length
? trackLayouts.slice().reverse().map((layout) => `
<article class="backup-row">
<article class="backup-row track-layout-row">
<div><strong>${escapeHtml(layout.label || t("settings.track_layout_unnamed"))}</strong><small>${escapeHtml(new Date(layout.startedAt).toLocaleString())}</small></div>
<span></span>
<form class="layout-lap-window" data-layout-lap-window="${escapeHtml(layout.id)}">
<label><span>${t("layouts.min_lap")}</span><input name="minLapSec" type="number" min="0" step="0.1" value="${layout.minLapMs ? (layout.minLapMs / 1000).toFixed(1) : ""}" placeholder="${escapeHtml(t("layouts.lap_from_event"))}" /></label>
<label><span>${t("layouts.max_lap")}</span><input name="maxLapSec" type="number" min="0" step="0.1" value="${layout.maxLapMs ? (layout.maxLapMs / 1000).toFixed(1) : ""}" placeholder="${escapeHtml(t("layouts.lap_from_event"))}" /></label>
<button class="btn btn-mini" type="submit">${t("common.save")}</button>
</form>
<button class="btn btn-mini" type="button" data-delete-track-layout="${escapeHtml(layout.id)}">${t("common.delete")}</button>
</article>`).join("")
: `<p class="hint">${t("settings.track_layout_none")}</p>`}
@@ -760,13 +768,26 @@ Erik Eriksson;654321;4WD;Associated;;</code></pre>
alert(t("settings.track_layout_invalid_date"));
return;
}
const result = addTrackLayout(label, startedAtMs);
const seconds = (name) => Math.round((Number(form.elements.namedItem(name)?.value) || 0) * 1000);
const result = addTrackLayout(label, startedAtMs, { minLapMs: seconds("minLapSec"), maxLapMs: seconds("maxLapSec") });
if (!result.ok) {
alert(result.message);
return;
}
renderView();
});
document.querySelectorAll("[data-layout-lap-window]").forEach((form) => {
form.addEventListener("submit", (event) => {
event.preventDefault();
const seconds = (name) => Math.round((Number(form.elements.namedItem(name)?.value) || 0) * 1000);
const result = updateTrackLayoutLapWindow(form.dataset.layoutLapWindow, seconds("minLapSec"), seconds("maxLapSec"));
if (!result.ok) {
alert(result.message);
return;
}
renderView();
});
});
document.querySelectorAll("[data-delete-track-layout]").forEach((button) => {
button.addEventListener("click", () => {
const id = button.getAttribute("data-delete-track-layout");