function getScheduleDriftSummary(state) {
const scheduledSessions = state.sessions
.filter((session) => Number(session.startedAt || 0) > 0)
.sort((left, right) => Number(left.startedAt || 0) - Number(right.startedAt || 0));
if (!scheduledSessions.length) {
return null;
}
const nowTs = Date.now();
const plannedMs = scheduledSessions.reduce(
(sum, session) => sum + Math.max(1, Number(session.durationMin || 0) || 0) * 60000 + Math.max(0, Number(session.followUpSec || 0) || 0) * 1000,
0
);
const actualMs = scheduledSessions.reduce((sum, session) => {
const startedAt = Number(session.startedAt || 0) || 0;
const endedAt = Number(session.endedAt || 0) || (session.status === "finished" ? nowTs : 0);
if (!startedAt) {
return sum;
}
const effectiveEnd = endedAt || nowTs;
return sum + Math.max(0, effectiveEnd - startedAt);
}, 0);
return {
plannedMs,
actualMs,
driftMs: actualMs - plannedMs,
};
}
function statCard(label, value, note) {
return `
${label}
${value}
${note}
`;
}
export function renderDashboardView(deps) {
const {
state,
dom,
t,
backend,
getActiveSession,
getStatusLabel,
getSessionTypeLabel,
getEventName,
getModeLabel,
getBackendUrl,
formatLap,
renderSessionsTable,
setCurrentView,
updateViewUrl,
renderNav,
renderView,
connectDecoder,
disconnectDecoder,
openOverlayWindow,
ensureAudioContext,
playPassingBeep,
playFinishSiren,
escapeHtml,
} = deps;
const active = getActiveSession();
const schedule = getScheduleDriftSummary(state);
const totalPassings = Object.values(state.resultsBySession || {}).reduce((sum, x) => sum + (x?.passings?.length || 0), 0);
const backendUrl = getBackendUrl();
const decoderUrl = state.settings.wsUrl || "-";
const audioProfile =
state.settings.passingSoundMode === "name"
? t("settings.passing_sound_name")
: state.settings.passingSoundMode === "beep"
? t("settings.passing_sound_beep")
: t("settings.passing_sound_off");
dom.view.innerHTML = `
1
${t("dashboard.step_system")}${t("dashboard.step_system_hint")}
2
${t("dashboard.step_register")}${t("dashboard.step_register_hint")}
3
${t("dashboard.step_create")}${t("dashboard.step_create_hint")}
4
${t("dashboard.step_run")}${t("dashboard.step_run_hint")}
${statCard(t("dashboard.events"), String(state.events.length), t("dashboard.created"))}
${statCard(t("dashboard.drivers"), String(state.drivers.length), t("dashboard.registered"))}
${statCard(t("dashboard.cars"), String(state.cars.length), t("dashboard.track_fleet"))}
${statCard(t("dashboard.passings"), String(totalPassings), t("dashboard.captured"))}
${
active
? `
${active.name} (${getSessionTypeLabel(active.type)})
${getEventName(active.eventId)} • ${getModeLabel(active.mode)}
${t("dashboard.duration")}: ${active.type === "open_practice" ? t("dashboard.untimed") : `${active.durationMin} min`}
`
: `
${t("dashboard.no_session")}
`
}
${t("dashboard.live_note")}
${t("dashboard.decoder_feed")}
${state.decoder.connected ? t("timing.connected") : t("timing.disconnected")}
${escapeHtml(decoderUrl)}
${t("dashboard.backend_link")}
${backend.available ? t("settings.online") : t("settings.offline")}
${escapeHtml(backendUrl)}
${t("dashboard.audio_profile")}
${state.settings.audioEnabled ? audioProfile : t("settings.passing_sound_off")}
${state.settings.finishVoiceEnabled ? t("settings.finish_voice") : "-"}
${t("dashboard.schedule_drift")}
${
schedule
? `${schedule.driftMs === 0 ? t("dashboard.on_time") : schedule.driftMs < 0 ? t("dashboard.ahead") : t("dashboard.behind")} ${formatLap(Math.abs(schedule.driftMs))}`
: "-"
}
${
schedule
? `${t("dashboard.schedule_plan")}: ${formatLap(schedule.plannedMs)} • ${t("dashboard.schedule_actual")}: ${formatLap(schedule.actualMs)}`
: "-"
}
${renderSessionsTable(state.sessions.slice(-8).reverse())}
`;
const navigateTo = (view) => {
setCurrentView(view);
updateViewUrl(view);
renderNav();
renderView();
};
document.getElementById("workflowSettings")?.addEventListener("click", () => navigateTo("settings"));
document.getElementById("workflowDrivers")?.addEventListener("click", () => navigateTo("drivers"));
document.getElementById("workflowRaceSetup")?.addEventListener("click", () => navigateTo("race_setup"));
document.getElementById("workflowTiming")?.addEventListener("click", () => navigateTo("timing"));
document.getElementById("workflowOverlay")?.addEventListener("click", () => navigateTo("overlay"));
document.getElementById("goEvents")?.addEventListener("click", () => navigateTo("events"));
document.getElementById("goTiming")?.addEventListener("click", () => navigateTo("timing"));
document.getElementById("connectNow")?.addEventListener("click", connectDecoder);
document.getElementById("disconnectNow")?.addEventListener("click", disconnectDecoder);
document.getElementById("openDashboardOverlay")?.addEventListener("click", openOverlayWindow);
document.getElementById("dashboardTestAudio")?.addEventListener("click", () => {
ensureAudioContext();
playPassingBeep();
if (state.settings.finishVoiceEnabled) {
setTimeout(playFinishSiren, 220);
}
});
}
export function renderClassesView(deps) {
const { state, dom, t, selectedClassEditId, setSelectedClassEditId, uid, saveState, renderView, renderTable, escapeHtml, setFormError, bindModalShell } = deps;
const editingClass = state.classes.find((item) => item.id === selectedClassEditId()) || null;
dom.view.innerHTML = `
${renderTable(
[t("table.name"), t("events.actions")],
state.classes.map(
(c) => `
| ${escapeHtml(c.name)} |
|
`
)
)}
${
editingClass
? `
`
: ""
}
`;
document.getElementById("classForm")?.addEventListener("submit", (e) => {
e.preventDefault();
const form = new FormData(e.currentTarget);
state.classes.push({ id: uid("class"), name: String(form.get("name")).trim() });
saveState();
renderView();
});
state.classes.forEach((item) => {
document.getElementById(`class-edit-${item.id}`)?.addEventListener("click", () => {
setSelectedClassEditId(item.id);
renderView();
});
document.getElementById(`class-delete-${item.id}`)?.addEventListener("click", () => {
state.classes = state.classes.filter((x) => x.id !== item.id);
saveState();
renderView();
});
});
document.getElementById("classEditCancel")?.addEventListener("click", () => {
setSelectedClassEditId(null);
renderView();
});
document.getElementById("classEditCancelFooter")?.addEventListener("click", () => {
setSelectedClassEditId(null);
renderView();
});
document.getElementById("classEditModalOverlay")?.addEventListener("click", (event) => {
if (event.target?.id === "classEditModalOverlay") {
setSelectedClassEditId(null);
renderView();
}
});
bindModalShell("classEditModalOverlay", () => {
setSelectedClassEditId(null);
renderView();
});
const commitClassEdit = () => {
if (!editingClass) {
return;
}
const formNode = document.getElementById("classEditForm");
if (!(formNode instanceof HTMLFormElement)) {
return;
}
const form = new FormData(formNode);
const cleaned = String(form.get("name") || "").trim();
if (!cleaned) {
setFormError("classEditError", t("validation.required_name"));
return;
}
setFormError("classEditError", "");
state.classes = state.classes.map((item) => (item.id === editingClass.id ? { ...item, name: cleaned } : item));
setSelectedClassEditId(null);
saveState();
renderView();
};
document.getElementById("classEditForm")?.addEventListener("submit", (event) => {
event.preventDefault();
commitClassEdit();
});
document.getElementById("classEditSave")?.addEventListener("click", commitClassEdit);
}
export function renderDriversView(deps) {
const {
state,
dom,
t,
driverBrandFilter,
setDriverBrandFilter,
selectedDriverEditId,
setSelectedDriverEditId,
uid,
saveState,
renderView,
renderTable,
escapeHtml,
setFormError,
bindModalShell,
normalizeDriver,
getClassName,
} = deps;
const classOptions = state.classes.map((c) => ``).join("");
const driverSearch = driverBrandFilter().trim().toLowerCase();
const filteredDrivers = state.drivers.filter((driver) =>
!driverSearch || [driver.name, driver.speechName, driver.transponder, driver.brand].map((value) => String(value || "").toLowerCase()).some((value) => value.includes(driverSearch))
);
const editingDriver = state.drivers.find((driver) => driver.id === selectedDriverEditId()) || null;
dom.view.innerHTML = `
${
editingDriver
? `
`
: ""
}
`;
document.getElementById("driverForm")?.addEventListener("submit", (e) => {
e.preventDefault();
const form = new FormData(e.currentTarget);
state.drivers.push(
normalizeDriver({
id: uid("driver"),
name: String(form.get("name")).trim(),
classId: String(form.get("classId")),
brand: String(form.get("brand") || "").trim(),
transponder: String(form.get("transponder") || "").trim(),
speechName: String(form.get("speechName") || "").trim(),
})
);
saveState();
renderView();
});
document.getElementById("driverBrandFilter")?.addEventListener("input", (event) => {
const input = event.currentTarget;
if (!(input instanceof HTMLInputElement)) {
return;
}
setDriverBrandFilter(input.value);
renderDriversView(deps);
});
state.drivers.forEach((d) => {
document.getElementById(`driver-edit-${d.id}`)?.addEventListener("click", () => {
setSelectedDriverEditId(d.id);
renderView();
});
document.getElementById(`driver-delete-${d.id}`)?.addEventListener("click", () => {
state.drivers = state.drivers.filter((x) => x.id !== d.id);
state.sessions.forEach((s) => {
s.assignments = (s.assignments || []).filter((a) => a.driverId !== d.id);
});
saveState();
renderView();
});
});
document.getElementById("driverEditCancel")?.addEventListener("click", () => {
setSelectedDriverEditId(null);
renderView();
});
document.getElementById("driverEditCancelFooter")?.addEventListener("click", () => {
setSelectedDriverEditId(null);
renderView();
});
document.getElementById("driverEditModalOverlay")?.addEventListener("click", (event) => {
if (event.target?.id === "driverEditModalOverlay") {
setSelectedDriverEditId(null);
renderView();
}
});
bindModalShell("driverEditModalOverlay", () => {
setSelectedDriverEditId(null);
renderView();
});
const commitDriverEdit = () => {
if (!editingDriver) {
return;
}
const formNode = document.getElementById("driverEditForm");
if (!(formNode instanceof HTMLFormElement)) {
return;
}
const form = new FormData(formNode);
const cleanedName = String(form.get("name") || "").trim();
const cleanedClassId = String(form.get("classId") || "").trim();
const cleanedBrand = String(form.get("brand") || "").trim();
const cleanedTp = String(form.get("transponder") || "").trim();
const cleanedSpeechName = String(form.get("speechName") || "").trim();
if (!cleanedName) {
setFormError("driverEditError", t("validation.required_name"));
return;
}
if (cleanedClassId && !state.classes.some((item) => item.id === cleanedClassId)) {
setFormError("driverEditError", t("validation.invalid_selection"));
return;
}
setFormError("driverEditError", "");
state.drivers = state.drivers.map((item) =>
item.id === editingDriver.id
? normalizeDriver({
...item,
name: cleanedName,
classId: cleanedClassId || item.classId,
brand: cleanedBrand,
transponder: cleanedTp,
speechName: cleanedSpeechName,
})
: item
);
setSelectedDriverEditId(null);
saveState();
renderView();
};
document.getElementById("driverEditForm")?.addEventListener("submit", (event) => {
event.preventDefault();
commitDriverEdit();
});
document.getElementById("driverEditSave")?.addEventListener("click", commitDriverEdit);
}
export function renderCarsView(deps) {
const {
state,
dom,
t,
carBrandFilter,
setCarBrandFilter,
selectedCarEditId,
setSelectedCarEditId,
uid,
saveState,
renderView,
renderTable,
escapeHtml,
setFormError,
bindModalShell,
normalizeCar,
} = deps;
const carSearch = carBrandFilter().trim().toLowerCase();
const filteredCars = state.cars.filter((car) =>
!carSearch || [car.name, car.transponder, car.brand].map((value) => String(value || "").toLowerCase()).some((value) => value.includes(carSearch))
);
const editingCar = state.cars.find((car) => car.id === selectedCarEditId()) || null;
dom.view.innerHTML = `
${
editingCar
? `
`
: ""
}
`;
document.getElementById("carForm")?.addEventListener("submit", (e) => {
e.preventDefault();
const form = new FormData(e.currentTarget);
state.cars.push(
normalizeCar({
id: uid("car"),
name: String(form.get("name")).trim(),
brand: String(form.get("brand") || "").trim(),
transponder: String(form.get("transponder")).trim(),
})
);
saveState();
renderView();
});
document.getElementById("carBrandFilter")?.addEventListener("input", (event) => {
const input = event.currentTarget;
if (!(input instanceof HTMLInputElement)) {
return;
}
setCarBrandFilter(input.value);
renderCarsView(deps);
});
state.cars.forEach((c) => {
document.getElementById(`car-edit-${c.id}`)?.addEventListener("click", () => {
setSelectedCarEditId(c.id);
renderView();
});
document.getElementById(`car-delete-${c.id}`)?.addEventListener("click", () => {
state.cars = state.cars.filter((x) => x.id !== c.id);
state.sessions.forEach((s) => {
s.assignments = (s.assignments || []).filter((a) => a.carId !== c.id);
});
saveState();
renderView();
});
});
document.getElementById("carEditCancel")?.addEventListener("click", () => {
setSelectedCarEditId(null);
renderView();
});
document.getElementById("carEditCancelFooter")?.addEventListener("click", () => {
setSelectedCarEditId(null);
renderView();
});
document.getElementById("carEditModalOverlay")?.addEventListener("click", (event) => {
if (event.target?.id === "carEditModalOverlay") {
setSelectedCarEditId(null);
renderView();
}
});
bindModalShell("carEditModalOverlay", () => {
setSelectedCarEditId(null);
renderView();
});
const commitCarEdit = () => {
if (!editingCar) {
return;
}
const formNode = document.getElementById("carEditForm");
if (!(formNode instanceof HTMLFormElement)) {
return;
}
const form = new FormData(formNode);
const cleanedName = String(form.get("name") || "").trim();
const cleanedBrand = String(form.get("brand") || "").trim();
const cleanedTp = String(form.get("transponder") || "").trim();
if (!cleanedName) {
setFormError("carEditError", t("validation.required_name"));
return;
}
if (!cleanedTp) {
setFormError("carEditError", t("validation.required_transponder"));
return;
}
setFormError("carEditError", "");
state.cars = state.cars.map((item) =>
item.id === editingCar.id
? normalizeCar({
...item,
name: cleanedName,
brand: cleanedBrand,
transponder: cleanedTp,
})
: item
);
setSelectedCarEditId(null);
saveState();
renderView();
};
document.getElementById("carEditForm")?.addEventListener("submit", (event) => {
event.preventDefault();
commitCarEdit();
});
document.getElementById("carEditSave")?.addEventListener("click", commitCarEdit);
}