diff --git a/src/app.js b/src/app.js
index baa7797..f2dfe00 100644
--- a/src/app.js
+++ b/src/app.js
@@ -8,6 +8,8 @@ import { normalizeRaceTeam as normalizeRaceTeamLogic, normalizeStoredRacePreset
import { getSessionTypeLabel as getSessionTypeLabelLogic, getStatusLabel as getStatusLabelLogic, isUntimedSession as isUntimedSessionLogic, getActiveSession as getActiveSessionLogic, getSessionTargetMs as getSessionTargetMsLogic, getSessionLapWindow as getSessionLapWindowLogic, isCountedPassing as isCountedPassingLogic, getVisiblePassings as getVisiblePassingsLogic, getPassingValidationLabel as getPassingValidationLabelLogic, getSessionTiming as getSessionTimingLogic, ensureSessionResult as ensureSessionResultLogic, buildLeaderboard as buildLeaderboardLogic, formatLapDelta as formatLapDeltaLogic, formatLeaderboardGap as formatLeaderboardGapLogic, getCompetitorElapsedMs as getCompetitorElapsedMsLogic, getCompetitorPassings as getCompetitorPassingsLogic, getCompetitorSeedMetric as getCompetitorSeedMetricLogic, getSessionEntrants as getSessionEntrantsLogic, buildPracticeStandings as buildPracticeStandingsLogic, getQualifyingPointsValue as getQualifyingPointsValueLogic, isHighPointsTable as isHighPointsTableLogic, compareNumberSet as compareNumberSetLogic, buildQualifyingTieBreakNote as buildQualifyingTieBreakNoteLogic, hasQualifyingPrimaryTie as hasQualifyingPrimaryTieLogic, buildQualifyingStandings as buildQualifyingStandingsLogic, formatTeamActiveMemberLabel as formatTeamActiveMemberLabelLogic, buildTeamRaceStandings as buildTeamRaceStandingsLogic, buildTeamStintLog as buildTeamStintLogLogic, getSessionGridEntries as getSessionGridEntriesLogic, getSessionGridOrder as getSessionGridOrderLogic, ensureSessionDriverOrder as ensureSessionDriverOrderLogic, buildFinalStandings as buildFinalStandingsLogic } from "./timing_logic.js";
+import { renderDashboardView, renderClassesView, renderDriversView, renderCarsView } from "./core_views.js";
+
import { renderTeamStintLog as renderTeamStintLogHelper, renderTeamRaceStandings as renderTeamRaceStandingsHelper, getSessionSortWeight as getSessionSortWeightHelper, getDriverDisplayById as getDriverDisplayByIdHelper, renderPositionGrid as renderPositionGridHelper, renderGridEditor as renderGridEditorHelper, getFinalMainLayouts as getFinalMainLayoutsHelper, renderFinalMatrix as renderFinalMatrixHelper, buildPrintBrandBlock as buildPrintBrandBlockHelper, buildRaceStartListsHtml as buildRaceStartListsHtmlHelper, buildRaceResultsHtml as buildRaceResultsHtmlHelper, buildTeamRaceResultsHtml as buildTeamRaceResultsHtmlHelper } from "./race_render_helpers.js";
import { getManualCorrectionSummary as getManualCorrectionSummaryLogic, applyCompetitorCorrection as applyCompetitorCorrectionLogic, recalculateCompetitorFromPassings as recalculateCompetitorFromPassingsLogic, invalidateCompetitorLastLap as invalidateCompetitorLastLapLogic, restoreCompetitorLastInvalidLap as restoreCompetitorLastInvalidLapLogic, findPassingByUndoMarker as findPassingByUndoMarkerLogic, undoJudgingAdjustment as undoJudgingAdjustmentLogic, getJudgeFilteredRows as getJudgeFilteredRowsLogic, getJudgeFilteredLog as getJudgeFilteredLogLogic } from "./judging_logic.js";
@@ -94,6 +96,10 @@ const buildPrintBrandBlock = (branding) => buildPrintBrandBlockHelper(branding,
const buildRaceStartListsHtml = (event) => buildRaceStartListsHtmlHelper(event, { t, state, escapeHtml, resolveEventBranding, getSessionsForEvent, getSessionSortWeight, getClassName, buildPrintBrandBlock, getSessionGridEntries, getSessionTypeLabel, getStartModeLabel, renderTable });
const buildRaceResultsHtml = (event) => buildRaceResultsHtmlHelper(event, { t, escapeHtml, resolveEventBranding, getClassName, buildPrintBrandBlock, renderRaceStandingsTableView, buildPracticeStandings, buildQualifyingStandings, buildFinalStandings, renderTeamRaceStandings });
const buildTeamRaceResultsHtml = (event) => buildTeamRaceResultsHtmlHelper(event, { t, escapeHtml, resolveEventBranding, getClassName, buildPrintBrandBlock, buildTeamRaceStandings, renderTable, formatLap, renderTeamStintLog });
+const renderDashboard = () => renderDashboardView({ state, dom, t, backend, getActiveSession, getStatusLabel, getSessionTypeLabel, getEventName, getModeLabel, getBackendUrl, formatLap, renderSessionsTable, setCurrentView: (view) => { currentView = view; }, renderNav, renderView, connectDecoder, disconnectDecoder, openOverlayWindow, ensureAudioContext, playPassingBeep, playFinishSiren, escapeHtml });
+const renderClasses = () => renderClassesView({ state, dom, t, selectedClassEditId: () => selectedClassEditId, setSelectedClassEditId: (value) => { selectedClassEditId = value; }, uid, saveState, renderView, renderTable, escapeHtml, setFormError, bindModalShell });
+const renderDrivers = () => renderDriversView({ state, dom, t, driverBrandFilter: () => driverBrandFilter, setDriverBrandFilter: (value) => { driverBrandFilter = value; }, selectedDriverEditId: () => selectedDriverEditId, setSelectedDriverEditId: (value) => { selectedDriverEditId = value; }, uid, saveState, renderView, renderTable, escapeHtml, setFormError, bindModalShell, normalizeDriver, getClassName });
+const renderCars = () => renderCarsView({ state, dom, t, carBrandFilter: () => carBrandFilter, setCarBrandFilter: (value) => { carBrandFilter = value; }, selectedCarEditId: () => selectedCarEditId, setSelectedCarEditId: (value) => { selectedCarEditId = value; }, uid, saveState, renderView, renderTable, escapeHtml, setFormError, bindModalShell, normalizeCar });
const applyCompetitorCorrection = (session, row, options = {}) => applyCompetitorCorrectionLogic(session, row, options, { ensureSessionResult, uid, t, formatLap, saveState });
const recalculateCompetitorFromPassings = (session, rowKey) => recalculateCompetitorFromPassingsLogic(session, rowKey, { ensureSessionResult, getCompetitorPassings, isCountedPassing });
const invalidateCompetitorLastLap = (session, row) => invalidateCompetitorLastLapLogic(session, row, { ensureSessionResult, getCompetitorPassings, isCountedPassing, recalculateCompetitorFromPassings, uid, t, formatLap, saveState });
@@ -3181,628 +3187,6 @@ function handleSessionTimerTick() {
return { changed: true };
}
-function renderDashboard() {
- const active = getActiveSession();
- const schedule = getScheduleDriftSummary();
- 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 = `
-
- ${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.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())}
-
-
- `;
-
- document.getElementById("goEvents")?.addEventListener("click", () => {
- currentView = "events";
- renderNav();
- renderView();
- });
-
- document.getElementById("goTiming")?.addEventListener("click", () => {
- currentView = "timing";
- renderNav();
- renderView();
- });
-
- 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);
- }
- });
-}
-
-function getScheduleDriftSummary() {
- 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}
-
- `;
-}
-
-function renderClasses() {
- 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", () => {
- selectedClassEditId = 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", () => {
- selectedClassEditId = null;
- renderView();
- });
-
- document.getElementById("classEditCancelFooter")?.addEventListener("click", () => {
- selectedClassEditId = null;
- renderView();
- });
-
- document.getElementById("classEditModalOverlay")?.addEventListener("click", (event) => {
- if (event.target?.id === "classEditModalOverlay") {
- selectedClassEditId = null;
- renderView();
- }
- });
-
- bindModalShell("classEditModalOverlay", () => {
- selectedClassEditId = null;
- renderView();
- });
-
- document.getElementById("classEditForm")?.addEventListener("submit", (event) => {
- event.preventDefault();
- if (!editingClass) {
- return;
- }
- const form = new FormData(event.currentTarget);
- const cleaned = String(form.get("name") || "").trim();
- if (!cleaned) {
- setFormError("classEditError", t("validation.required_name"));
- return;
- }
- setFormError("classEditError", "");
- editingClass.name = cleaned;
- selectedClassEditId = null;
- saveState();
- renderView();
- });
-}
-
-function renderDrivers() {
- const classOptions = state.classes
- .map((c) => ``)
- .join("");
- const driverSearch = driverBrandFilter.trim().toLowerCase();
- const filteredDrivers = state.drivers.filter((driver) =>
- !driverSearch ||
- [driver.name, 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(),
- })
- );
- saveState();
- renderView();
- });
-
- document.getElementById("driverBrandFilter")?.addEventListener("input", (event) => {
- const input = event.currentTarget;
- if (!(input instanceof HTMLInputElement)) {
- return;
- }
- driverBrandFilter = input.value;
- renderDrivers();
- });
-
- state.drivers.forEach((d) => {
- document.getElementById(`driver-edit-${d.id}`)?.addEventListener("click", () => {
- selectedDriverEditId = 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", () => {
- selectedDriverEditId = null;
- renderView();
- });
-
- document.getElementById("driverEditCancelFooter")?.addEventListener("click", () => {
- selectedDriverEditId = null;
- renderView();
- });
-
- document.getElementById("driverEditModalOverlay")?.addEventListener("click", (event) => {
- if (event.target?.id === "driverEditModalOverlay") {
- selectedDriverEditId = null;
- renderView();
- }
- });
-
- bindModalShell("driverEditModalOverlay", () => {
- selectedDriverEditId = null;
- renderView();
- });
-
- document.getElementById("driverEditForm")?.addEventListener("submit", (event) => {
- event.preventDefault();
- if (!editingDriver) {
- return;
- }
- const form = new FormData(event.currentTarget);
- 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();
- 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", "");
- editingDriver.name = cleanedName;
- editingDriver.classId = cleanedClassId || editingDriver.classId;
- editingDriver.brand = cleanedBrand;
- editingDriver.transponder = cleanedTp;
- selectedDriverEditId = null;
- saveState();
- renderView();
- });
-}
-
-function renderCars() {
- 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;
- }
- carBrandFilter = input.value;
- renderCars();
- });
-
- state.cars.forEach((c) => {
- document.getElementById(`car-edit-${c.id}`)?.addEventListener("click", () => {
- selectedCarEditId = 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", () => {
- selectedCarEditId = null;
- renderView();
- });
-
- document.getElementById("carEditCancelFooter")?.addEventListener("click", () => {
- selectedCarEditId = null;
- renderView();
- });
-
- document.getElementById("carEditModalOverlay")?.addEventListener("click", (event) => {
- if (event.target?.id === "carEditModalOverlay") {
- selectedCarEditId = null;
- renderView();
- }
- });
-
- bindModalShell("carEditModalOverlay", () => {
- selectedCarEditId = null;
- renderView();
- });
-
- document.getElementById("carEditForm")?.addEventListener("submit", (event) => {
- event.preventDefault();
- if (!editingCar) {
- return;
- }
- const form = new FormData(event.currentTarget);
- 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", "");
- editingCar.name = cleanedName;
- editingCar.brand = cleanedBrand;
- editingCar.transponder = cleanedTp;
- selectedCarEditId = null;
- saveState();
- renderView();
- });
-}
-
function renderEvents() {
renderEventWorkspace("track");
}
diff --git a/src/core_views.js b/src/core_views.js
new file mode 100644
index 0000000..7e66c13
--- /dev/null
+++ b/src/core_views.js
@@ -0,0 +1,664 @@
+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,
+ 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 = `
+
+ ${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.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())}
+
+
+ `;
+
+ document.getElementById("goEvents")?.addEventListener("click", () => {
+ setCurrentView("events");
+ renderNav();
+ renderView();
+ });
+
+ document.getElementById("goTiming")?.addEventListener("click", () => {
+ setCurrentView("timing");
+ renderNav();
+ renderView();
+ });
+
+ 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();
+ });
+
+ document.getElementById("classEditForm")?.addEventListener("submit", (event) => {
+ event.preventDefault();
+ if (!editingClass) {
+ return;
+ }
+ const form = new FormData(event.currentTarget);
+ const cleaned = String(form.get("name") || "").trim();
+ if (!cleaned) {
+ setFormError("classEditError", t("validation.required_name"));
+ return;
+ }
+ setFormError("classEditError", "");
+ editingClass.name = cleaned;
+ setSelectedClassEditId(null);
+ saveState();
+ renderView();
+ });
+}
+
+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.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(),
+ })
+ );
+ 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();
+ });
+
+ document.getElementById("driverEditForm")?.addEventListener("submit", (event) => {
+ event.preventDefault();
+ if (!editingDriver) {
+ return;
+ }
+ const form = new FormData(event.currentTarget);
+ 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();
+ 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", "");
+ editingDriver.name = cleanedName;
+ editingDriver.classId = cleanedClassId || editingDriver.classId;
+ editingDriver.brand = cleanedBrand;
+ editingDriver.transponder = cleanedTp;
+ setSelectedDriverEditId(null);
+ saveState();
+ renderView();
+ });
+}
+
+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();
+ });
+
+ document.getElementById("carEditForm")?.addEventListener("submit", (event) => {
+ event.preventDefault();
+ if (!editingCar) {
+ return;
+ }
+ const form = new FormData(event.currentTarget);
+ 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", "");
+ editingCar.name = cleanedName;
+ editingCar.brand = cleanedBrand;
+ editingCar.transponder = cleanedTp;
+ setSelectedCarEditId(null);
+ saveState();
+ renderView();
+ });
+}