Add track layout tracking so lap-time stats stay comparable

Clubs that reconfigure their track (e.g. running indoors in the same
building) can now add named track layouts under Settings, each with
just a start date/time - layouts are contiguous, so a layout's end is
implicitly the next one's start, with no separate field to keep in
sync. Every session freezes which layout was active when it started,
so the "track record" audio cue and the Results Archive (which now
shows each session's layout) only compare laps run on the same
physical track. With no layouts configured, everything behaves
exactly as before - the feature is fully inert until a club uses it.

Design settled through conversation with the user: global scope (not
per class), start-date-only (no redundant stop date), and no changes
needed to driver/transponder mapping, since that's already fully
independent of track geometry in this app.

Deliberately out of scope for this pass: the mobile/driver training
profile's own cross-session personal-best trend stats still compare
across a driver's whole lap ledger regardless of layout - logged in
ROADMAP.md as a real remaining gap for a later pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
larssand
2026-09-10 14:17:06 +02:00
parent 90df3378a6
commit fd6efe2c7e
13 changed files with 328 additions and 27 deletions

View File

@@ -59,6 +59,10 @@ export function renderSettings(deps) {
createManualServerBackup,
createSafetyServerBackup,
refreshServerBackups,
trackLayouts,
nextTrackLayoutLabel,
addTrackLayout,
deleteTrackLayout,
updateStatus,
updateStatusLoaded,
updateCheckPending,
@@ -343,6 +347,33 @@ export function renderSettings(deps) {
</div>
</section>
<section class="panel mt-16">
<div class="panel-header"><h3>${t("settings.track_layouts")}</h3></div>
<div class="panel-body">
<p class="hint">${t("settings.track_layouts_hint")}</p>
<form id="trackLayoutForm" class="form-grid cols-3 settings-grid">
<label><span>${t("settings.track_layout_label")}</span><input name="label" placeholder="${escapeHtml(nextTrackLayoutLabel())}" /></label>
<label><span>${t("settings.track_layout_started_at")}</span><input name="startedAt" type="datetime-local" value="${escapeHtml((() => {
const now = new Date();
now.setSeconds(0, 0);
now.setMinutes(now.getMinutes() - now.getTimezoneOffset());
return now.toISOString().slice(0, 16);
})())}" /></label>
<div class="actions"><button class="btn btn-primary" type="submit">${t("settings.track_layout_add")}</button></div>
</form>
<div class="backup-list mt-16">
${(trackLayouts || []).length
? trackLayouts.slice().reverse().map((layout) => `
<article class="backup-row">
<div><strong>${escapeHtml(layout.label || t("settings.track_layout_unnamed"))}</strong><small>${escapeHtml(new Date(layout.startedAt).toLocaleString())}</small></div>
<span></span>
<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>`}
</div>
</div>
</section>
<section class="panel mt-16">
<div class="panel-header"><h3>${t("overlay.obs_config")}</h3></div>
<div class="panel-body">
@@ -779,6 +810,38 @@ Erik Eriksson;654321;4WD;Associated;;</code></pre>
});
document.getElementById("refreshServerBackups")?.addEventListener("click", () => refreshServerBackups());
document.getElementById("trackLayoutForm")?.addEventListener("submit", (event) => {
event.preventDefault();
const form = event.currentTarget;
if (!(form instanceof HTMLFormElement)) return;
const label = String(form.elements.namedItem("label")?.value || "").trim();
const startedAtRaw = String(form.elements.namedItem("startedAt")?.value || "");
const startedAtMs = startedAtRaw ? new Date(startedAtRaw).getTime() : Date.now();
if (!Number.isFinite(startedAtMs)) {
alert(t("settings.track_layout_invalid_date"));
return;
}
const result = addTrackLayout(label, startedAtMs);
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");
const layout = (trackLayouts || []).find((item) => item.id === id);
if (!window.confirm(t("settings.track_layout_delete_confirm", { label: layout?.label || t("settings.track_layout_unnamed") }))) return;
const result = deleteTrackLayout(id);
if (!result.ok) {
alert(result.message);
return;
}
renderView();
});
});
document.getElementById("settingsCheckUpdate")?.addEventListener("click", async () => {
setUpdateNotice("");
await refreshUpdateStatus({ render: false, force: true });