diff --git a/src/app.js b/src/app.js index 3c845f1..7156984 100644 --- a/src/app.js +++ b/src/app.js @@ -10,6 +10,9 @@ import { getSessionTypeLabel as getSessionTypeLabelLogic, getStatusLabel as getS import { renderDashboardView, renderClassesView, renderDriversView, renderCarsView } from "./core_views.js"; +import { renderTimingView, renderJudgingView } from "./timing_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"; @@ -100,6 +103,8 @@ const renderDashboard = () => renderDashboardView({ state, dom, t, backend, getA 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 renderTiming = () => renderTimingView({ state, dom, t, escapeHtml, formatLap, formatCountdown, formatElapsedClock, formatRaceClock, renderTable, uid, getActiveSession, ensureSessionResult, buildLeaderboard, getSessionTiming, getVisiblePassings, getEventName, getSessionTypeLabel, getStatusLabel, normalizeStartMode, getStartModeLabel, renderPositionGrid, connectDecoder, disconnectDecoder, applyCompetitorCorrection, invalidateCompetitorLastLap, restoreCompetitorLastInvalidLap, ensureAudioContext, validateTrackSessionForStart, pushOverlayEvent, saveState, updateHeaderState, renderView, normalizeDriver, normalizeCar, processDecoderMessage, getSelectedLeaderboardKey: () => selectedLeaderboardKey, setSelectedLeaderboardKey: (value) => { selectedLeaderboardKey = value; }, getQuickAddDraft: () => quickAddDraft, setQuickAddDraft: (value) => { quickAddDraft = value; }, getPassingValidationLabel, isCountedPassing, getCompetitorPassings, getManualCorrectionSummary, formatTeamActiveMemberLabel, lastFinishAnnouncementSessionId: () => lastFinishAnnouncementSessionId, setLastFinishAnnouncementSessionId: (value) => { lastFinishAnnouncementSessionId = value; }, lastOverlayLeaderKeyBySession, lastOverlayTop3BySession, overlayEvents: () => overlayEvents, setOverlayEvents: (value) => { overlayEvents = value; } }); +const renderJudging = () => renderJudgingView({ state, dom, t, escapeHtml, formatLap, renderTable, getActiveSession, ensureSessionResult, buildLeaderboard, getSessionTypeLabel, getJudgeFilteredRows, getJudgeFilteredLog, getCompetitorPassings, isCountedPassing, getPassingValidationLabel, undoJudgingAdjustment, applyCompetitorCorrection, invalidateCompetitorLastLap, restoreCompetitorLastInvalidLap, renderView, getSelectedJudgeKey: () => selectedJudgeKey, setSelectedJudgeKey: (value) => { selectedJudgeKey = value; }, getJudgingCompetitorFilter: () => judgingCompetitorFilter, setJudgingCompetitorFilter: (value) => { judgingCompetitorFilter = value; }, getJudgingLogFilter: () => judgingLogFilter, setJudgingLogFilter: (value) => { judgingLogFilter = value; } }); 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 }); @@ -5125,659 +5130,6 @@ function renderAssignmentList(eventId) { }); } -function renderTiming() { - const active = getActiveSession(); - const result = active ? ensureSessionResult(active.id) : null; - const leaderboard = active ? buildLeaderboard(active) : []; - const sessionTiming = active ? getSessionTiming(active) : null; - const clockLabel = active && sessionTiming?.followUpActive ? t("timing.follow_up") : active && sessionTiming?.untimed ? t("timing.elapsed") : t("timing.remaining"); - const clockValue = sessionTiming?.followUpActive - ? formatCountdown(sessionTiming?.followUpRemainingMs ?? 0) - : sessionTiming?.untimed - ? formatElapsedClock(sessionTiming?.elapsedMs ?? 0) - : formatCountdown(sessionTiming?.remainingMs ?? 0); - const showFinishedBanner = Boolean(active && active.status === "finished" && active.finishedByTimer); - const showFollowUpBanner = Boolean(active && sessionTiming?.followUpActive); - const selectedRow = leaderboard.find((row) => row.key === selectedLeaderboardKey) || null; - if (selectedLeaderboardKey && !selectedRow) { - selectedLeaderboardKey = null; - } - - dom.view.innerHTML = ` -
-

${t("timing.decoder_connection")}

-
-
- - -
- - - -
-
-
- ${t("timing.status")} - ${state.decoder.connected ? t("timing.connected") : t("timing.disconnected")} - ${t("timing.last_message")}: ${state.decoder.lastMessageAt ? new Date(state.decoder.lastMessageAt).toLocaleString() : "-"} -

${escapeHtml(state.decoder.lastError || "")}

-
-
-
- -
-

${t("timing.control")}

-
-
- - -
- - - - -
-
-
-
- ${ - active - ? `
- ${escapeHtml(active.name)} - ${escapeHtml(getSessionTypeLabel(active.type))} - ${escapeHtml(getEventName(active.eventId))} - ${escapeHtml(getStatusLabel(active.status))} -
-
-
${clockLabel}${clockValue}
-
${t("timing.started")}${active.startedAt ? new Date(active.startedAt).toLocaleTimeString() : "-"}
-
${t("table.start_mode")}${escapeHtml(getStartModeLabel(active.startMode))}
-
${t("timing.seeding_mode")}${active.seedBestLapCount > 0 ? `${active.seedBestLapCount}` : "-"}
-
${t("timing.total_passings")}${getVisiblePassings(result).length}
-
- ${ - active.type === "free_practice" - ? `

${t("events.free_practice_note")}

` - : active.type === "open_practice" - ? `

${t("events.open_practice_note")}

` - : "" - }` - : `

${t("timing.no_active")}

` - } - ${showFollowUpBanner ? `

${t("timing.follow_up_active")}

` : ""} - ${showFinishedBanner ? `

${t("timing.race_finished")}

` : ""} - ${active && normalizeStartMode(active.startMode) === "position" ? renderPositionGrid(active) : ""} -
-
- - ${active ? renderQuickAddPanel(active) : ""} - -
-

${t("timing.speaker_panel")}

-
-

${t("timing.speaker_panel_hint")}

-
- ${renderSpeakerToggle("speakerPassingCueEnabled", "settings.speaker_passing_cue")} - ${renderSpeakerToggle("speakerLeaderCueEnabled", "settings.speaker_leader_cue")} - ${renderSpeakerToggle("speakerBestLapCueEnabled", "settings.speaker_bestlap_cue")} - ${renderSpeakerToggle("speakerTop3CueEnabled", "settings.speaker_top3_cue")} - ${renderSpeakerToggle("speakerSessionStartCueEnabled", "settings.speaker_start_cue")} - ${renderSpeakerToggle("speakerFinishCueEnabled", "settings.speaker_finish_cue")} -
-
-
- -
-

${t("timing.leaderboard")}

-
- ${renderLeaderboard(leaderboard)} -
-
- -
-

${t("timing.recent_passings")}

-
- ${renderRecentPassings(active)} -
-
- - ${selectedRow && active ? renderLeaderboardModal(active, selectedRow) : ""} - `; - - document.getElementById("timingConnect")?.addEventListener("click", () => { - const input = document.getElementById("timingWsUrl"); - if (input && input.value.trim()) { - state.settings.wsUrl = input.value.trim(); - saveState(); - } - connectDecoder(); - }); - - document.getElementById("timingDisconnect")?.addEventListener("click", disconnectDecoder); - - leaderboard.forEach((row) => { - document.getElementById(`leaderboard-detail-${row.key}`)?.addEventListener("click", () => { - selectedLeaderboardKey = row.key; - renderView(); - }); - }); - - if (active && selectedRow) { - bindQuickAddActions(active, selectedRow.transponder, "leaderboardModal"); - document.getElementById("corrLapPlus")?.addEventListener("click", () => { - applyCompetitorCorrection(active, selectedRow, { lapDelta: 1 }); - renderView(); - }); - document.getElementById("corrLapMinus")?.addEventListener("click", () => { - applyCompetitorCorrection(active, selectedRow, { lapDelta: -1 }); - renderView(); - }); - document.getElementById("corrSecPlus")?.addEventListener("click", () => { - applyCompetitorCorrection(active, selectedRow, { timeMsDelta: 1000 }); - renderView(); - }); - document.getElementById("corr5SecPlus")?.addEventListener("click", () => { - applyCompetitorCorrection(active, selectedRow, { timeMsDelta: 5000 }); - renderView(); - }); - document.getElementById("corrSecMinus")?.addEventListener("click", () => { - applyCompetitorCorrection(active, selectedRow, { timeMsDelta: -1000 }); - renderView(); - }); - document.getElementById("corrInvalidateLast")?.addEventListener("click", () => { - invalidateCompetitorLastLap(active, selectedRow); - renderView(); - }); - document.getElementById("corrRestoreInvalid")?.addEventListener("click", () => { - restoreCompetitorLastInvalidLap(active, selectedRow); - renderView(); - }); - document.getElementById("corrReset")?.addEventListener("click", () => { - applyCompetitorCorrection(active, selectedRow, { reset: true }); - renderView(); - }); - } - - if (active) { - ensureSessionResult(active.id) - .passings.slice(-20) - .reverse() - .forEach((passing, index) => { - bindQuickAddActions(active, passing.transponder, `recentPassing-${index}`); - }); - } - - document.getElementById("quickAddCancel")?.addEventListener("click", () => { - quickAddDraft = null; - renderView(); - }); - - document.getElementById("quickAddForm")?.addEventListener("submit", (event) => { - event.preventDefault(); - if (!active || !quickAddDraft) { - return; - } - const form = new FormData(event.currentTarget); - const name = String(form.get("name") || "").trim(); - if (!name) { - return; - } - const transponder = String(form.get("transponder") || "").trim(); - const brand = String(form.get("brand") || "").trim(); - if (!transponder) { - return; - } - if (quickAddDraft.type === "driver") { - if (!state.drivers.some((item) => String(item.transponder || "").trim() === transponder)) { - state.drivers.push( - normalizeDriver({ - id: uid("driver"), - name, - classId: String(form.get("classId") || getPreferredClassId(active)), - brand, - transponder, - }) - ); - } - } else if (!state.cars.some((item) => String(item.transponder || "").trim() === transponder)) { - state.cars.push( - normalizeCar({ - id: uid("car"), - name, - brand, - transponder, - }) - ); - } - quickAddDraft = null; - saveState(); - renderView(); - }); - - document.getElementById("leaderboardModalClose")?.addEventListener("click", () => { - selectedLeaderboardKey = null; - renderView(); - }); - - document.getElementById("leaderboardModalOverlay")?.addEventListener("click", (event) => { - if (event.target?.id === "leaderboardModalOverlay") { - selectedLeaderboardKey = null; - renderView(); - } - }); - - document.getElementById("timingSimPass")?.addEventListener("click", () => { - const tp = prompt(t("timing.prompt_transponder"), "232323"); - if (!tp) { - return; - } - processDecoderMessage({ - msg: "PASSING", - transponder: tp, - rtc_time: new Date().toISOString(), - strength: 0, - resend: false, - loop_id: "sim", - }); - }); - - document.getElementById("setActiveSession")?.addEventListener("click", () => { - const select = document.getElementById("activeSessionSelect"); - if (!select || !select.value) { - return; - } - state.activeSessionId = select.value; - saveState(); - updateHeaderState(); - renderView(); - }); - - document.getElementById("startSession")?.addEventListener("click", () => { - const session = getActiveSession(); - if (!session) { - return; - } - ensureAudioContext(); - - if (session.mode === "track") { - const trackValidation = validateTrackSessionForStart(session); - if (!trackValidation.ok) { - alert(trackValidation.message); - return; - } - } - - session.status = "running"; - session.startedAt = Date.now(); - session.endedAt = null; - session.finishedByTimer = false; - session.followUpStartedAt = null; - lastFinishAnnouncementSessionId = null; - lastOverlayLeaderKeyBySession[session.id] = null; - lastOverlayTop3BySession[session.id] = []; - overlayEvents = []; - ensureSessionResult(session.id); - pushOverlayEvent("start", `${session.name} • ${t("timing.start")}`); - saveState(); - updateHeaderState(); - renderView(); - }); - - document.getElementById("stopSession")?.addEventListener("click", () => { - const session = getActiveSession(); - if (!session) { - return; - } - session.status = "finished"; - session.endedAt = Date.now(); - session.finishedByTimer = false; - session.followUpStartedAt = null; - saveState(); - updateHeaderState(); - renderView(); - }); - - document.getElementById("resetSession")?.addEventListener("click", () => { - const session = getActiveSession(); - if (!session) { - return; - } - if (!confirm(t("timing.clear_confirm"))) { - return; - } - delete state.resultsBySession[session.id]; - session.followUpStartedAt = null; - lastFinishAnnouncementSessionId = null; - delete lastOverlayLeaderKeyBySession[session.id]; - delete lastOverlayTop3BySession[session.id]; - overlayEvents = []; - saveState(); - renderView(); - }); - - document.querySelectorAll("[data-speaker-setting]").forEach((node) => { - node.addEventListener("change", (event) => { - const input = event.currentTarget; - if (!(input instanceof HTMLInputElement)) { - return; - } - state.settings[input.dataset.speakerSetting] = input.checked; - saveState(); - }); - }); -} - -function renderJudging() { - const active = getActiveSession(); - if (!active) { - dom.view.innerHTML = ` -
-

${t("judging.title")}

-

${t("judging.no_active_session")}

-
- `; - return; - } - - const result = ensureSessionResult(active.id); - const leaderboard = buildLeaderboard(active); - const filteredRows = getJudgeFilteredRows(leaderboard, judgingCompetitorFilter); - if (selectedJudgeKey && !filteredRows.some((row) => row.key === selectedJudgeKey)) { - selectedJudgeKey = null; - } - if (!selectedJudgeKey && filteredRows.length) { - selectedJudgeKey = filteredRows[0].key; - } - const selectedRow = leaderboard.find((row) => row.key === selectedJudgeKey) || null; - const selectedPassings = selectedRow ? getCompetitorPassings(active, selectedRow, { includeInvalid: true }) : []; - const actionLog = getJudgeFilteredLog(Array.isArray(result.adjustments) ? result.adjustments.slice(-50).reverse() : [], judgingLogFilter); - const latestUndoable = (result.adjustments || []).slice().reverse().find((entry) => !entry.undoneAt && entry.undo); - - dom.view.innerHTML = ` -
-
-

${t("judging.title")}

- ${escapeHtml(active.name)} -
-
-

${t("judging.active_session")}: ${escapeHtml(active.name)} • ${escapeHtml(getSessionTypeLabel(active.type))}

-
-
- -
-
-
-

${t("judging.select_competitor")}

- -
-
- ${renderTable( - [t("table.pos"), t("table.driver"), t("table.result"), t("table.best_lap"), ""], - filteredRows.map( - (row, index) => ` - - ${index + 1} - -
${escapeHtml(row.displayName || row.driverName)}
-
${escapeHtml(row.subLabel || row.transponder || "-")}
- - ${escapeHtml(row.resultDisplay || "-")} - ${formatLap(row.bestLapMs)} - - - ` - ) - )} -
-
- -
-

${t("judging.manual_actions")}

-
- ${ - selectedRow - ? ` -

${escapeHtml(selectedRow.displayName || selectedRow.driverName)} • ${escapeHtml(selectedRow.subLabel || selectedRow.carName || "-")}

-

${t("table.laps")}: ${selectedRow.laps}

-

${t("table.best_lap")}: ${formatLap(selectedRow.bestLapMs)}

-

${t("table.last_lap")}: ${formatLap(selectedRow.lastLapMs)}

-
- - - - - - - - -
-
-

${t("timing.lap_history")}

- ${ - selectedPassings.length - ? renderTable( - [t("table.lap"), t("table.last_lap"), t("table.status")], - selectedPassings.map( - (passing, index) => ` - - ${index + 1} - ${formatLap(passing.lapMs)} - ${escapeHtml(getPassingValidationLabel(passing))} - - ` - ) - ) - : `

${t("timing.no_lap_history")}

` - } -
- ` - : `

${t("judging.selected_none")}

` - } -
-
-
- -
-
-

${t("judging.action_log")}

-
- - - -
-
-
- ${ - actionLog.length - ? renderTable( - [t("table.time"), t("table.driver"), t("events.actions"), t("table.status"), ""], - actionLog.map( - (entry) => ` - - ${new Date(entry.ts).toLocaleTimeString()} - ${escapeHtml(entry.displayName || "-")} - ${escapeHtml(entry.action || "-")} - ${escapeHtml(entry.detail || "-")}${entry.undoneAt ? ` • ${escapeHtml(t("judging.filter_log_undo"))}` : ""} - ${entry.undo && !entry.undoneAt ? `` : ""} - - ` - ) - ) - : `

${t("judging.no_action_log")}

` - } -
-
- `; - - leaderboard.forEach((row) => { - document.getElementById(`judge-select-${row.key}`)?.addEventListener("click", () => { - selectedJudgeKey = row.key; - renderJudging(); - }); - }); - - document.getElementById("judgingCompetitorFilter")?.addEventListener("change", (event) => { - const input = event.currentTarget; - if (!(input instanceof HTMLSelectElement)) { - return; - } - judgingCompetitorFilter = input.value; - renderJudging(); - }); - - document.getElementById("judgingLogFilter")?.addEventListener("change", (event) => { - const input = event.currentTarget; - if (!(input instanceof HTMLSelectElement)) { - return; - } - judgingLogFilter = input.value; - renderJudging(); - }); - - document.getElementById("judgingExportLog")?.addEventListener("click", () => { - const rows = (result.adjustments || []).map((entry) => ({ - time: new Date(entry.ts).toISOString(), - competitor: entry.displayName || "-", - action: entry.action || "-", - detail: entry.detail || "-", - category: entry.category || "-", - undoneAt: entry.undoneAt ? new Date(entry.undoneAt).toISOString() : "", - })); - const blob = new Blob([JSON.stringify({ session: active.name, items: rows }, null, 2)], { type: "application/json" }); - const url = URL.createObjectURL(blob); - const link = document.createElement("a"); - link.href = url; - link.download = `${active.name.replaceAll(/\s+/g, "_")}_judging_log.json`; - link.click(); - URL.revokeObjectURL(url); - }); - - document.getElementById("judgingUndoLast")?.addEventListener("click", () => { - if (!latestUndoable) { - alert(t("judging.no_undo")); - return; - } - undoJudgingAdjustment(active, latestUndoable.id); - renderJudging(); - }); - - actionLog.forEach((entry) => { - if (!entry.undo || entry.undoneAt) { - return; - } - document.getElementById(`judge-undo-${entry.id}`)?.addEventListener("click", () => { - undoJudgingAdjustment(active, entry.id); - renderJudging(); - }); - }); - - if (!selectedRow) { - return; - } - - document.getElementById("judgeLapPlus")?.addEventListener("click", () => { - applyCompetitorCorrection(active, selectedRow, { lapDelta: 1 }); - renderJudging(); - }); - document.getElementById("judgeLapMinus")?.addEventListener("click", () => { - applyCompetitorCorrection(active, selectedRow, { lapDelta: -1 }); - renderJudging(); - }); - document.getElementById("judgeSecPlus")?.addEventListener("click", () => { - applyCompetitorCorrection(active, selectedRow, { timeMsDelta: 1000 }); - renderJudging(); - }); - document.getElementById("judgeFivePlus")?.addEventListener("click", () => { - applyCompetitorCorrection(active, selectedRow, { timeMsDelta: 5000 }); - renderJudging(); - }); - document.getElementById("judgeSecMinus")?.addEventListener("click", () => { - applyCompetitorCorrection(active, selectedRow, { timeMsDelta: -1000 }); - renderJudging(); - }); - document.getElementById("judgeInvalidate")?.addEventListener("click", () => { - invalidateCompetitorLastLap(active, selectedRow); - renderJudging(); - }); - document.getElementById("judgeRestore")?.addEventListener("click", () => { - restoreCompetitorLastInvalidLap(active, selectedRow); - renderJudging(); - }); - document.getElementById("judgeReset")?.addEventListener("click", () => { - applyCompetitorCorrection(active, selectedRow, { reset: true }); - renderJudging(); - }); -} - -function renderSpeakerToggle(settingKey, labelKey) { - return ` - - `; -} - -function renderQuickAddPanel(session) { - if (!quickAddDraft || !quickAddDraft.transponder) { - return ""; - } - const classOptions = state.classes - .map( - (item) => `` - ) - .join(""); - const isDriver = quickAddDraft.type === "driver"; - return ` -
-

${t(isDriver ? "timing.quick_add_driver_title" : "timing.quick_add_car_title")}

-
- - - - ${ - isDriver - ? `` - : `
${t("timing.quick_add_hint")}
` - } -
- - -
-
-
- `; -} - function renderGuidePanel(titleKey, itemKeys, extras = "") { return `
diff --git a/src/timing_views.js b/src/timing_views.js new file mode 100644 index 0000000..c736a28 --- /dev/null +++ b/src/timing_views.js @@ -0,0 +1,887 @@ +function renderSpeakerToggle(settingKey, labelKey, { state, t }) { + return ` + + `; +} + +function getQuickAddState(transponder, { state }) { + const normalized = String(transponder || "").trim(); + const driver = state.drivers.find((item) => String(item.transponder || "").trim() === normalized) || null; + const car = state.cars.find((item) => String(item.transponder || "").trim() === normalized) || null; + return { + transponder: normalized, + hasDriver: Boolean(driver), + hasCar: Boolean(car), + }; +} + +function getPreferredClassId(session, { state }) { + const event = state.events.find((item) => item.id === session?.eventId); + if (event?.classId) { + return event.classId; + } + return state.classes[0]?.id || ""; +} + +function beginQuickAddDraft(session, type, transponder, deps) { + const { state, setQuickAddDraft, renderView, getPreferredClassId } = deps; + const normalized = String(transponder || "").trim(); + if (!normalized) { + return; + } + if (type === "driver" && state.drivers.some((item) => String(item.transponder || "").trim() === normalized)) { + return; + } + if (type === "car" && state.cars.some((item) => String(item.transponder || "").trim() === normalized)) { + return; + } + setQuickAddDraft({ + type, + transponder: normalized, + classId: getPreferredClassId(session), + name: type === "driver" ? normalized : `Car ${normalized}`, + }); + renderView(); +} + +function renderQuickAddActions(session, transponder, idPrefix, deps) { + const { t, getQuickAddState } = deps; + const quickState = getQuickAddState(transponder); + if (!quickState.transponder || (quickState.hasDriver && quickState.hasCar)) { + return ""; + } + return ` +
+ ${!quickState.hasDriver ? `` : ""} + ${!quickState.hasCar ? `` : ""} +
+ `; +} + +function bindQuickAddActions(session, transponder, idPrefix, deps) { + const { beginQuickAddDraft } = deps; + document.getElementById(`${idPrefix}-add-driver`)?.addEventListener("click", () => { + beginQuickAddDraft(session, "driver", transponder); + }); + document.getElementById(`${idPrefix}-add-car`)?.addEventListener("click", () => { + beginQuickAddDraft(session, "car", transponder); + }); +} + +function renderQuickAddPanel(session, deps) { + const { state, t, escapeHtml, getQuickAddDraft, getPreferredClassId } = deps; + const quickAddDraft = getQuickAddDraft(); + if (!quickAddDraft || !quickAddDraft.transponder) { + return ""; + } + const classOptions = state.classes + .map( + (item) => `` + ) + .join(""); + const isDriver = quickAddDraft.type === "driver"; + return ` +
+

${t(isDriver ? "timing.quick_add_driver_title" : "timing.quick_add_car_title")}

+
+ + + + ${ + isDriver + ? `` + : `
${t("timing.quick_add_hint")}
` + } +
+ + +
+
+
+ `; +} + +function renderLeaderboardModal(session, row, deps) { + const { t, escapeHtml, formatLap, formatRaceClock, renderTable, getCompetitorPassings, renderQuickAddActions } = deps; + const passings = getCompetitorPassings(session, row); + return ` + + `; +} + +function renderLeaderboard(rows, deps) { + const { t, escapeHtml, renderTable, getManualCorrectionSummary, formatLap, formatTeamActiveMemberLabel } = deps; + if (!rows.length) { + return `

${t("timing.no_laps")}

`; + } + + return renderTable( + [ + t("table.pos"), + t("table.driver"), + t("table.car"), + t("table.transponder"), + t("table.laps"), + t("table.result"), + t("table.last_lap"), + t("table.best_lap"), + t("table.leader_gap"), + t("table.ahead_gap"), + t("table.own_delta"), + "", + ], + rows.map((row, idx) => { + const posClass = idx === 0 ? "pos-1" : idx === 1 ? "pos-2" : idx === 2 ? "pos-3" : ""; + return ` + + ${idx + 1} + +
${escapeHtml(row.displayName || row.driverName)}
+ ${row.teamId ? `
${t("overlay.active_member")}: ${escapeHtml(formatTeamActiveMemberLabel(row))}
` : ""} + ${getManualCorrectionSummary(row) ? `
${escapeHtml(getManualCorrectionSummary(row))}
` : ""} + ${row.invalidPending ? `
${escapeHtml(row.invalidLabel)}${row.invalidLapMs ? ` • ${formatLap(row.invalidLapMs)}` : ""}
` : ""} + + ${escapeHtml(row.subLabel || row.carName)} + ${escapeHtml(row.transponder)} + ${row.laps} + ${escapeHtml(row.resultDisplay)} + ${formatLap(row.lastLapMs)} + ${formatLap(row.bestLapMs)} + ${escapeHtml(row.leaderGap || row.gap || "-")} + ${escapeHtml(row.gapAhead || "-")} + ${escapeHtml(row.lapDelta || "-")} + + + `; + }) + ); +} + +function renderRecentPassings(session, deps) { + const { t, escapeHtml, renderTable, ensureSessionResult, formatLap, getPassingValidationLabel, isCountedPassing, renderQuickAddActions } = deps; + if (!session) { + return `

${t("timing.no_session_selected")}

`; + } + const result = ensureSessionResult(session.id); + const items = result.passings.slice(-20).reverse(); + if (!items.length) { + return `

${t("timing.no_passings")}

`; + } + + return renderTable( + [t("table.time"), t("table.transponder"), t("table.driver"), t("table.car"), t("table.last_lap"), t("table.status"), ""], + items.map((p, index) => { + return ` + + ${new Date(p.timestamp).toLocaleTimeString()} + ${escapeHtml(p.transponder)} + ${escapeHtml(p.teamName ? `${p.teamName} • ${p.driverName || t("common.unknown_driver")}` : p.driverName || t("common.unknown_driver"))} + ${escapeHtml(p.carName || p.subLabel || "-")} + ${formatLap(p.lapMs)} + ${escapeHtml(getPassingValidationLabel(p))} + ${renderQuickAddActions(session, p.transponder, `recentPassing-${index}`)} + + `; + }) + ); +} + +export function renderTimingView(deps) { + const { + state, dom, t, escapeHtml, formatLap, formatCountdown, formatElapsedClock, renderTable, uid, + getActiveSession, ensureSessionResult, buildLeaderboard, getSessionTiming, getVisiblePassings, + getEventName, getSessionTypeLabel, getStatusLabel, normalizeStartMode, getStartModeLabel, renderPositionGrid, + connectDecoder, disconnectDecoder, applyCompetitorCorrection, invalidateCompetitorLastLap, restoreCompetitorLastInvalidLap, + ensureAudioContext, validateTrackSessionForStart, pushOverlayEvent, saveState, updateHeaderState, renderView, + normalizeDriver, normalizeCar, processDecoderMessage, getSelectedLeaderboardKey, setSelectedLeaderboardKey, + getQuickAddDraft, setQuickAddDraft, getPassingValidationLabel, isCountedPassing, getCompetitorPassings, + getManualCorrectionSummary, formatTeamActiveMemberLabel, formatRaceClock, + setLastFinishAnnouncementSessionId, lastOverlayLeaderKeyBySession, lastOverlayTop3BySession, overlayEvents, setOverlayEvents, + } = deps; + + const helperDeps = { + state, t, escapeHtml, renderTable, formatLap, formatRaceClock, getCompetitorPassings, + getQuickAddDraft, getPassingValidationLabel, isCountedPassing, getManualCorrectionSummary, + formatTeamActiveMemberLabel, + }; + helperDeps.getPreferredClassId = (session) => getPreferredClassId(session, { state }); + helperDeps.getQuickAddState = (transponder) => getQuickAddState(transponder, { state }); + helperDeps.setQuickAddDraft = setQuickAddDraft; + helperDeps.renderView = renderView; + helperDeps.beginQuickAddDraft = (session, type, transponder) => beginQuickAddDraft(session, type, transponder, helperDeps); + helperDeps.renderQuickAddActions = (session, transponder, idPrefix) => renderQuickAddActions(session, transponder, idPrefix, helperDeps); + + const active = getActiveSession(); + const result = active ? ensureSessionResult(active.id) : null; + const leaderboard = active ? buildLeaderboard(active) : []; + const sessionTiming = active ? getSessionTiming(active) : null; + const clockLabel = active && sessionTiming?.followUpActive ? t("timing.follow_up") : active && sessionTiming?.untimed ? t("timing.elapsed") : t("timing.remaining"); + const clockValue = sessionTiming?.followUpActive + ? formatCountdown(sessionTiming?.followUpRemainingMs ?? 0) + : sessionTiming?.untimed + ? formatElapsedClock(sessionTiming?.elapsedMs ?? 0) + : formatCountdown(sessionTiming?.remainingMs ?? 0); + const showFinishedBanner = Boolean(active && active.status === "finished" && active.finishedByTimer); + const showFollowUpBanner = Boolean(active && sessionTiming?.followUpActive); + const selectedRow = leaderboard.find((row) => row.key === getSelectedLeaderboardKey()) || null; + if (getSelectedLeaderboardKey() && !selectedRow) { + setSelectedLeaderboardKey(null); + } + + dom.view.innerHTML = ` +
+

${t("timing.decoder_connection")}

+
+
+ + +
+ + + +
+
+
+ ${t("timing.status")} + ${state.decoder.connected ? t("timing.connected") : t("timing.disconnected")} + ${t("timing.last_message")}: ${state.decoder.lastMessageAt ? new Date(state.decoder.lastMessageAt).toLocaleString() : "-"} +

${escapeHtml(state.decoder.lastError || "")}

+
+
+
+ +
+

${t("timing.control")}

+
+
+ + +
+ + + + +
+
+
+
+ ${ + active + ? `
+ ${escapeHtml(active.name)} + ${escapeHtml(getSessionTypeLabel(active.type))} + ${escapeHtml(getEventName(active.eventId))} + ${escapeHtml(getStatusLabel(active.status))} +
+
+
${clockLabel}${clockValue}
+
${t("timing.started")}${active.startedAt ? new Date(active.startedAt).toLocaleTimeString() : "-"}
+
${t("table.start_mode")}${escapeHtml(getStartModeLabel(active.startMode))}
+
${t("timing.seeding_mode")}${active.seedBestLapCount > 0 ? `${active.seedBestLapCount}` : "-"}
+
${t("timing.total_passings")}${getVisiblePassings(result).length}
+
+ ${ + active.type === "free_practice" + ? `

${t("events.free_practice_note")}

` + : active.type === "open_practice" + ? `

${t("events.open_practice_note")}

` + : "" + }` + : `

${t("timing.no_active")}

` + } + ${showFollowUpBanner ? `

${t("timing.follow_up_active")}

` : ""} + ${showFinishedBanner ? `

${t("timing.race_finished")}

` : ""} + ${active && normalizeStartMode(active.startMode) === "position" ? renderPositionGrid(active) : ""} +
+
+ + ${active ? renderQuickAddPanel(active, helperDeps) : ""} + +
+

${t("timing.speaker_panel")}

+
+

${t("timing.speaker_panel_hint")}

+
+ ${renderSpeakerToggle("speakerPassingCueEnabled", "settings.speaker_passing_cue", { state, t })} + ${renderSpeakerToggle("speakerLeaderCueEnabled", "settings.speaker_leader_cue", { state, t })} + ${renderSpeakerToggle("speakerBestLapCueEnabled", "settings.speaker_bestlap_cue", { state, t })} + ${renderSpeakerToggle("speakerTop3CueEnabled", "settings.speaker_top3_cue", { state, t })} + ${renderSpeakerToggle("speakerSessionStartCueEnabled", "settings.speaker_start_cue", { state, t })} + ${renderSpeakerToggle("speakerFinishCueEnabled", "settings.speaker_finish_cue", { state, t })} +
+
+
+ +
+

${t("timing.leaderboard")}

+
+ ${renderLeaderboard(leaderboard, helperDeps)} +
+
+ +
+

${t("timing.recent_passings")}

+
+ ${renderRecentPassings(active, { ...helperDeps, ensureSessionResult, renderQuickAddActions: (session, transponder, idPrefix) => renderQuickAddActions(session, transponder, idPrefix, helperDeps) })} +
+
+ + ${selectedRow && active ? renderLeaderboardModal(active, selectedRow, { ...helperDeps, renderQuickAddActions: (session, transponder, idPrefix) => renderQuickAddActions(session, transponder, idPrefix, helperDeps) }) : ""} + `; + + document.getElementById("timingConnect")?.addEventListener("click", () => { + const input = document.getElementById("timingWsUrl"); + if (input && input.value.trim()) { + state.settings.wsUrl = input.value.trim(); + saveState(); + } + connectDecoder(); + }); + + document.getElementById("timingDisconnect")?.addEventListener("click", disconnectDecoder); + + leaderboard.forEach((row) => { + document.getElementById(`leaderboard-detail-${row.key}`)?.addEventListener("click", () => { + setSelectedLeaderboardKey(row.key); + renderView(); + }); + }); + + if (active && selectedRow) { + bindQuickAddActions(active, selectedRow.transponder, "leaderboardModal", helperDeps); + document.getElementById("corrLapPlus")?.addEventListener("click", () => { + applyCompetitorCorrection(active, selectedRow, { lapDelta: 1 }); + renderView(); + }); + document.getElementById("corrLapMinus")?.addEventListener("click", () => { + applyCompetitorCorrection(active, selectedRow, { lapDelta: -1 }); + renderView(); + }); + document.getElementById("corrSecPlus")?.addEventListener("click", () => { + applyCompetitorCorrection(active, selectedRow, { timeMsDelta: 1000 }); + renderView(); + }); + document.getElementById("corr5SecPlus")?.addEventListener("click", () => { + applyCompetitorCorrection(active, selectedRow, { timeMsDelta: 5000 }); + renderView(); + }); + document.getElementById("corrSecMinus")?.addEventListener("click", () => { + applyCompetitorCorrection(active, selectedRow, { timeMsDelta: -1000 }); + renderView(); + }); + document.getElementById("corrInvalidateLast")?.addEventListener("click", () => { + invalidateCompetitorLastLap(active, selectedRow); + renderView(); + }); + document.getElementById("corrRestoreInvalid")?.addEventListener("click", () => { + restoreCompetitorLastInvalidLap(active, selectedRow); + renderView(); + }); + document.getElementById("corrReset")?.addEventListener("click", () => { + applyCompetitorCorrection(active, selectedRow, { reset: true }); + renderView(); + }); + } + + if (active) { + ensureSessionResult(active.id) + .passings.slice(-20) + .reverse() + .forEach((passing, index) => { + bindQuickAddActions(active, passing.transponder, `recentPassing-${index}`, helperDeps); + }); + } + + document.getElementById("quickAddCancel")?.addEventListener("click", () => { + setQuickAddDraft(null); + renderView(); + }); + + document.getElementById("quickAddForm")?.addEventListener("submit", (event) => { + event.preventDefault(); + const quickAddDraft = getQuickAddDraft(); + if (!active || !quickAddDraft) { + return; + } + const form = new FormData(event.currentTarget); + const name = String(form.get("name") || "").trim(); + if (!name) { + return; + } + const transponder = String(form.get("transponder") || "").trim(); + const brand = String(form.get("brand") || "").trim(); + if (!transponder) { + return; + } + if (quickAddDraft.type === "driver") { + if (!state.drivers.some((item) => String(item.transponder || "").trim() === transponder)) { + state.drivers.push( + normalizeDriver({ + id: uid("driver"), + name, + classId: String(form.get("classId") || getPreferredClassId(active, { state })), + brand, + transponder, + }) + ); + } + } else if (!state.cars.some((item) => String(item.transponder || "").trim() === transponder)) { + state.cars.push( + normalizeCar({ + id: uid("car"), + name, + brand, + transponder, + }) + ); + } + setQuickAddDraft(null); + saveState(); + renderView(); + }); + + document.getElementById("leaderboardModalClose")?.addEventListener("click", () => { + setSelectedLeaderboardKey(null); + renderView(); + }); + + document.getElementById("leaderboardModalOverlay")?.addEventListener("click", (event) => { + if (event.target?.id === "leaderboardModalOverlay") { + setSelectedLeaderboardKey(null); + renderView(); + } + }); + + document.getElementById("timingSimPass")?.addEventListener("click", () => { + const tp = prompt(t("timing.prompt_transponder"), "232323"); + if (!tp) { + return; + } + processDecoderMessage({ + msg: "PASSING", + transponder: tp, + rtc_time: new Date().toISOString(), + strength: 0, + resend: false, + loop_id: "sim", + }); + }); + + document.getElementById("setActiveSession")?.addEventListener("click", () => { + const select = document.getElementById("activeSessionSelect"); + if (!select || !select.value) { + return; + } + state.activeSessionId = select.value; + saveState(); + updateHeaderState(); + renderView(); + }); + + document.getElementById("startSession")?.addEventListener("click", () => { + const session = getActiveSession(); + if (!session) { + return; + } + ensureAudioContext(); + + if (session.mode === "track") { + const trackValidation = validateTrackSessionForStart(session); + if (!trackValidation.ok) { + alert(trackValidation.message); + return; + } + } + + session.status = "running"; + session.startedAt = Date.now(); + session.endedAt = null; + session.finishedByTimer = false; + session.followUpStartedAt = null; + setLastFinishAnnouncementSessionId(null); + lastOverlayLeaderKeyBySession[session.id] = null; + lastOverlayTop3BySession[session.id] = []; + setOverlayEvents([]); + ensureSessionResult(session.id); + pushOverlayEvent("start", `${session.name} • ${t("timing.start")}`); + saveState(); + updateHeaderState(); + renderView(); + }); + + document.getElementById("stopSession")?.addEventListener("click", () => { + const session = getActiveSession(); + if (!session) { + return; + } + session.status = "finished"; + session.endedAt = Date.now(); + session.finishedByTimer = false; + session.followUpStartedAt = null; + saveState(); + updateHeaderState(); + renderView(); + }); + + document.getElementById("resetSession")?.addEventListener("click", () => { + const session = getActiveSession(); + if (!session) { + return; + } + if (!confirm(t("timing.clear_confirm"))) { + return; + } + delete state.resultsBySession[session.id]; + session.followUpStartedAt = null; + setLastFinishAnnouncementSessionId(null); + delete lastOverlayLeaderKeyBySession[session.id]; + delete lastOverlayTop3BySession[session.id]; + setOverlayEvents([]); + saveState(); + renderView(); + }); + + document.querySelectorAll("[data-speaker-setting]").forEach((node) => { + node.addEventListener("change", (event) => { + const input = event.currentTarget; + if (!(input instanceof HTMLInputElement)) { + return; + } + state.settings[input.dataset.speakerSetting] = input.checked; + saveState(); + }); + }); +} + +export function renderJudgingView(deps) { + const { + dom, t, escapeHtml, formatLap, renderTable, getActiveSession, ensureSessionResult, buildLeaderboard, + getSessionTypeLabel, getJudgeFilteredRows, getJudgeFilteredLog, getCompetitorPassings, isCountedPassing, + getPassingValidationLabel, undoJudgingAdjustment, applyCompetitorCorrection, invalidateCompetitorLastLap, + restoreCompetitorLastInvalidLap, renderView, getSelectedJudgeKey, setSelectedJudgeKey, + getJudgingCompetitorFilter, setJudgingCompetitorFilter, getJudgingLogFilter, setJudgingLogFilter, + } = deps; + + const active = getActiveSession(); + if (!active) { + dom.view.innerHTML = ` +
+

${t("judging.title")}

+

${t("judging.no_active_session")}

+
+ `; + return; + } + + const result = ensureSessionResult(active.id); + const leaderboard = buildLeaderboard(active); + const filteredRows = getJudgeFilteredRows(leaderboard, getJudgingCompetitorFilter()); + if (getSelectedJudgeKey() && !filteredRows.some((row) => row.key === getSelectedJudgeKey())) { + setSelectedJudgeKey(null); + } + if (!getSelectedJudgeKey() && filteredRows.length) { + setSelectedJudgeKey(filteredRows[0].key); + } + const selectedRow = leaderboard.find((row) => row.key === getSelectedJudgeKey()) || null; + const selectedPassings = selectedRow ? getCompetitorPassings(active, selectedRow, { includeInvalid: true }) : []; + const actionLog = getJudgeFilteredLog(Array.isArray(result.adjustments) ? result.adjustments.slice(-50).reverse() : [], getJudgingLogFilter()); + const latestUndoable = (result.adjustments || []).slice().reverse().find((entry) => !entry.undoneAt && entry.undo); + + dom.view.innerHTML = ` +
+
+

${t("judging.title")}

+ ${escapeHtml(active.name)} +
+
+

${t("judging.active_session")}: ${escapeHtml(active.name)} • ${escapeHtml(getSessionTypeLabel(active.type))}

+
+
+ +
+
+
+

${t("judging.select_competitor")}

+ +
+
+ ${renderTable( + [t("table.pos"), t("table.driver"), t("table.result"), t("table.best_lap"), ""], + filteredRows.map( + (row, index) => ` + + ${index + 1} + +
${escapeHtml(row.displayName || row.driverName)}
+
${escapeHtml(row.subLabel || row.transponder || "-")}
+ + ${escapeHtml(row.resultDisplay || "-")} + ${formatLap(row.bestLapMs)} + + + ` + ) + )} +
+
+ +
+

${t("judging.manual_actions")}

+
+ ${ + selectedRow + ? ` +

${escapeHtml(selectedRow.displayName || selectedRow.driverName)} • ${escapeHtml(selectedRow.subLabel || selectedRow.carName || "-")}

+

${t("table.laps")}: ${selectedRow.laps}

+

${t("table.best_lap")}: ${formatLap(selectedRow.bestLapMs)}

+

${t("table.last_lap")}: ${formatLap(selectedRow.lastLapMs)}

+
+ + + + + + + + +
+
+

${t("timing.lap_history")}

+ ${ + selectedPassings.length + ? renderTable( + [t("table.lap"), t("table.last_lap"), t("table.status")], + selectedPassings.map( + (passing, index) => ` + + ${index + 1} + ${formatLap(passing.lapMs)} + ${escapeHtml(getPassingValidationLabel(passing))} + + ` + ) + ) + : `

${t("timing.no_lap_history")}

` + } +
+ ` + : `

${t("judging.selected_none")}

` + } +
+
+
+ +
+
+

${t("judging.action_log")}

+
+ + + +
+
+
+ ${ + actionLog.length + ? renderTable( + [t("table.time"), t("table.driver"), t("events.actions"), t("table.status"), ""], + actionLog.map( + (entry) => ` + + ${new Date(entry.ts).toLocaleTimeString()} + ${escapeHtml(entry.displayName || "-")} + ${escapeHtml(entry.action || "-")} + ${escapeHtml(entry.detail || "-")}${entry.undoneAt ? ` • ${escapeHtml(t("judging.filter_log_undo"))}` : ""} + ${entry.undo && !entry.undoneAt ? `` : ""} + + ` + ) + ) + : `

${t("judging.no_action_log")}

` + } +
+
+ `; + + leaderboard.forEach((row) => { + document.getElementById(`judge-select-${row.key}`)?.addEventListener("click", () => { + setSelectedJudgeKey(row.key); + renderJudgingView(deps); + }); + }); + + document.getElementById("judgingCompetitorFilter")?.addEventListener("change", (event) => { + const input = event.currentTarget; + if (!(input instanceof HTMLSelectElement)) { + return; + } + setJudgingCompetitorFilter(input.value); + renderJudgingView(deps); + }); + + document.getElementById("judgingLogFilter")?.addEventListener("change", (event) => { + const input = event.currentTarget; + if (!(input instanceof HTMLSelectElement)) { + return; + } + setJudgingLogFilter(input.value); + renderJudgingView(deps); + }); + + document.getElementById("judgingExportLog")?.addEventListener("click", () => { + const rows = (result.adjustments || []).map((entry) => ({ + time: new Date(entry.ts).toISOString(), + competitor: entry.displayName || "-", + action: entry.action || "-", + detail: entry.detail || "-", + category: entry.category || "-", + undoneAt: entry.undoneAt ? new Date(entry.undoneAt).toISOString() : "", + })); + const blob = new Blob([JSON.stringify({ session: active.name, items: rows }, null, 2)], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = `${active.name.replaceAll(/\s+/g, "_")}_judging_log.json`; + link.click(); + URL.revokeObjectURL(url); + }); + + document.getElementById("judgingUndoLast")?.addEventListener("click", () => { + if (!latestUndoable) { + alert(t("judging.no_undo")); + return; + } + undoJudgingAdjustment(active, latestUndoable.id); + renderJudgingView(deps); + }); + + actionLog.forEach((entry) => { + if (!entry.undo || entry.undoneAt) { + return; + } + document.getElementById(`judge-undo-${entry.id}`)?.addEventListener("click", () => { + undoJudgingAdjustment(active, entry.id); + renderJudgingView(deps); + }); + }); + + if (!selectedRow) { + return; + } + + document.getElementById("judgeLapPlus")?.addEventListener("click", () => { + applyCompetitorCorrection(active, selectedRow, { lapDelta: 1 }); + renderJudgingView(deps); + }); + document.getElementById("judgeLapMinus")?.addEventListener("click", () => { + applyCompetitorCorrection(active, selectedRow, { lapDelta: -1 }); + renderJudgingView(deps); + }); + document.getElementById("judgeSecPlus")?.addEventListener("click", () => { + applyCompetitorCorrection(active, selectedRow, { timeMsDelta: 1000 }); + renderJudgingView(deps); + }); + document.getElementById("judgeFivePlus")?.addEventListener("click", () => { + applyCompetitorCorrection(active, selectedRow, { timeMsDelta: 5000 }); + renderJudgingView(deps); + }); + document.getElementById("judgeSecMinus")?.addEventListener("click", () => { + applyCompetitorCorrection(active, selectedRow, { timeMsDelta: -1000 }); + renderJudgingView(deps); + }); + document.getElementById("judgeInvalidate")?.addEventListener("click", () => { + invalidateCompetitorLastLap(active, selectedRow); + renderJudgingView(deps); + }); + document.getElementById("judgeRestore")?.addEventListener("click", () => { + restoreCompetitorLastInvalidLap(active, selectedRow); + renderJudgingView(deps); + }); + document.getElementById("judgeReset")?.addEventListener("click", () => { + applyCompetitorCorrection(active, selectedRow, { reset: true }); + renderJudgingView(deps); + }); +}