diff --git a/src/app.js b/src/app.js
index 7156984..5be9189 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 { renderGuideView, renderOverlayPageView } from "./misc_views.js";
+
+
import { renderTimingView, renderJudgingView } from "./timing_views.js";
@@ -103,6 +106,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 renderGuide = () => renderGuideView({ dom, t });
+const renderOverlay = () => renderOverlayPageView({ state, dom, t, escapeHtml, formatLap, formatCountdown, formatElapsedClock, formatPredictedLapDelta, getActiveSession, buildLeaderboard, ensureSessionResult, getSessionTiming, getVisiblePassings, resolveEventBranding, buildPracticeStandings, buildQualifyingStandings, buildFinalStandings, getOverlayModeLabel, getStatusLabel, getObsOverlayConfig, buildOverlayPanels, normalizeStartMode, renderPositionGrid, getEventName, getSessionTypeLabel, getStartModeLabel, renderRaceStandingsTableView, renderTeamOverlay, renderObsOverlay, renderOverlayLeaderboard, renderOverlaySidePanel, formatTeamActiveMemberLabel, getSessionGridEntries, overlayViewMode, overlayMode, publicOverlayMode, overlayEvents, overlayRotationIndex, openOverlayWindow, buildOverlayUrl });
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 });
@@ -5130,334 +5135,6 @@ function renderAssignmentList(eventId) {
});
}
-function renderGuidePanel(titleKey, itemKeys, extras = "") {
- return `
-
-
-
-
- ${itemKeys.map((key) => `- ${t(key)}
`).join("")}
-
- ${extras}
-
-
- `;
-}
-
-function renderGuideOverviewCard(titleKey, blurbKey) {
- return `
-
- ${t(titleKey)}
- ${t(blurbKey)}
-
- `;
-}
-
-function renderGuide() {
- dom.view.innerHTML = `
-
-
-
-
${t("guide.intro")}
-
- ${renderGuideOverviewCard("guide.sponsor_title", "guide.card_sponsor_blurb")}
- ${renderGuideOverviewCard("guide.race_title", "guide.card_race_blurb")}
- ${renderGuideOverviewCard("guide.team_title", "guide.card_team_blurb")}
- ${renderGuideOverviewCard("guide.windows_title", "guide.card_decoder_blurb")}
-
-
-
-
-
- ${renderGuidePanel("guide.sponsor_title", ["guide.sponsor_1", "guide.sponsor_2", "guide.sponsor_3", "guide.sponsor_4", "guide.sponsor_5", "guide.sponsor_6"])}
- ${renderGuidePanel("guide.race_wizard_title", ["guide.race_wizard_1", "guide.race_wizard_2", "guide.race_wizard_3", "guide.race_wizard_4", "guide.race_wizard_5", "guide.race_wizard_6", "guide.race_wizard_7"])}
-
-
-
- ${renderGuidePanel("guide.race_title", ["guide.race_1", "guide.race_2", "guide.race_3", "guide.race_4", "guide.race_4a", "guide.race_5", "guide.race_6", "guide.race_7", "guide.race_8", "guide.race_9", "guide.race_10"])}
- ${renderGuidePanel("guide.manage_steps_title", ["guide.manage_steps_1", "guide.manage_steps_2", "guide.manage_steps_3", "guide.manage_steps_4", "guide.manage_steps_5", "guide.manage_steps_6", "guide.manage_steps_7", "guide.manage_steps_8"])}
-
-
-
- ${renderGuidePanel("guide.race_format_title", ["guide.race_format_0", "guide.race_format_1", "guide.race_format_2", "guide.race_format_3", "guide.race_format_4", "guide.race_format_5", "guide.race_format_6", "guide.race_format_7", "guide.race_format_8", "guide.race_format_9", "guide.race_format_10", "guide.race_format_11", "guide.race_format_11a", "guide.race_format_12", "guide.race_format_13", "guide.race_format_14"])}
- ${renderGuidePanel("guide.validation_title", ["guide.validation_1", "guide.validation_2", "guide.validation_3", "guide.validation_4", "guide.validation_5", "guide.validation_6", "guide.validation_7", "guide.validation_8"])}
-
-
-
- ${renderGuidePanel("guide.free_practice_title", ["guide.free_practice_1", "guide.free_practice_2", "guide.free_practice_3"])}
- ${renderGuidePanel("guide.open_practice_title", ["guide.open_practice_1", "guide.open_practice_2", "guide.open_practice_3"])}
-
-
-
- ${renderGuidePanel("guide.team_title", ["guide.team_1", "guide.team_2", "guide.team_3", "guide.team_4", "guide.team_5", "guide.team_6"])}
- ${renderGuidePanel("guide.qualifying_title", ["guide.qualifying_1", "guide.qualifying_2", "guide.qualifying_3", "guide.qualifying_4", "guide.qualifying_5"])}
-
-
-
- ${renderGuidePanel("guide.dashboard_title", ["guide.dashboard_1", "guide.dashboard_2", "guide.dashboard_3"])}
- ${renderGuidePanel("guide.host_title", ["guide.host_1", "guide.host_2", "guide.host_3", "guide.host_4", "guide.host_5", "guide.host_6", "guide.host_7", "guide.host_8"])}
-
-
-
- ${renderGuidePanel("guide.windows_title", ["guide.windows_1", "guide.windows_2", "guide.windows_3", "guide.windows_4", "guide.windows_5"])}
- ${renderGuidePanel("guide.linux_title", ["guide.linux_1", "guide.linux_2", "guide.linux_3"])}
-
-
-
-
-
-
- - ${t("guide.sqlite_1")}
- - ${t("guide.sqlite_2")}
- - ${t("guide.sqlite_3")}
- - ${t("guide.sqlite_4")}
- - ${t("guide.sqlite_5")}
-
-
${t("guide.ammc_ref")}
-
-
- `;
-}
-
-function renderOverlay() {
- const active = getActiveSession();
- const leaderboard = active ? buildLeaderboard(active).slice(0, 12) : [];
- const result = active ? ensureSessionResult(active.id) : null;
- const sessionTiming = active ? getSessionTiming(active) : null;
- const overlayClock = sessionTiming?.followUpActive
- ? formatCountdown(sessionTiming?.followUpRemainingMs ?? 0)
- : sessionTiming?.untimed
- ? formatElapsedClock(sessionTiming?.elapsedMs ?? 0)
- : formatCountdown(sessionTiming?.remainingMs ?? 0);
- const recent = active && result ? getVisiblePassings(result).slice(-8).reverse() : [];
- const event = active ? state.events.find((item) => item.id === active.eventId) : null;
- const branding = resolveEventBranding(event);
- const practiceRows = event ? buildPracticeStandings(event) : [];
- const qualifyingRows = event ? buildQualifyingStandings(event) : [];
- const finalRows = event ? buildFinalStandings(event) : [];
- const topRow = leaderboard[0] || null;
- const fastestRow =
- [...leaderboard].filter((row) => Number.isFinite(row.bestLapMs)).sort((left, right) => left.bestLapMs - right.bestLapMs)[0] || null;
- const modeLabel = getOverlayModeLabel(overlayViewMode, { t });
- const overlayStatusLabel = sessionTiming?.followUpActive ? t("timing.follow_up_active") : active ? getStatusLabel(active.status) : "";
- const obsConfig = overlayViewMode === "obs" ? getObsOverlayConfig() : null;
- const rotatingPanels = buildOverlayPanels(active, recent, { t, overlayEvents, normalizeStartMode, renderPositionGrid });
- const activePanel = rotatingPanels.length ? rotatingPanels[overlayRotationIndex % rotatingPanels.length] : null;
-
- const denseOverlay = overlayViewMode === "leaderboard" || overlayViewMode === "tv";
-
- dom.view.innerHTML = `
- ${
- overlayMode
- ? ""
- : `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- `
- }
-
- ${
- active
- ? `
- ${
- overlayViewMode === "obs"
- ? ""
- : ``
- }
-
- ${
- overlayViewMode === "speaker"
- ? `
-
-
-
P1
-
${escapeHtml(topRow?.displayName || topRow?.driverName || t("common.unknown_driver"))}
-
${t("table.result")}: ${escapeHtml(topRow?.resultDisplay || "-")}
-
${t("table.best_lap")}: ${formatLap(topRow?.bestLapMs)}
-
-
-
- ${t("overlay.last_passings")}
- ${
- recent.length
- ? recent
- .map(
- (passing) => `
-
- ${escapeHtml(passing.displayName || passing.teamName || passing.driverName || t("common.unknown_driver"))}
- ${formatLap(passing.lapMs)}
-
- `
- )
- .join("")
- : `${t("timing.no_passings")}
`
- }
-
-
- ${t("events.position_grid")}
- ${normalizeStartMode(active.startMode) === "position" ? renderPositionGrid(active) : `${t("events.na")}
`}
-
-
- ${t("overlay.event_markers")}
- ${
- overlayEvents.length
- ? overlayEvents
- .map(
- (item) => `
-
- ${escapeHtml(item.label)}
- ${new Date(item.ts).toLocaleTimeString()}
-
- `
- )
- .join("")
- : `${t("timing.no_passings")}
`
- }
-
-
-
- `
- : overlayViewMode === "results"
- ? `
-
-
-
${t("events.practice_standings")}
- ${renderRaceStandingsTableView(practiceRows, t("events.no_practice_results"))}
-
-
-
${t("events.qualifying_standings")}
- ${renderRaceStandingsTableView(qualifyingRows, t("events.no_qualifying_results"))}
-
-
-
${t("events.final_standings")}
- ${renderRaceStandingsTableView(finalRows, t("events.no_final_results"))}
-
-
- `
- : overlayViewMode === "team"
- ? renderTeamOverlay(leaderboard, result, sessionTiming, { t, escapeHtml, formatLap, formatPredictedLapDelta, formatTeamActiveMemberLabel, getVisiblePassings, renderOverlayLeaderboard })
- : overlayViewMode === "obs"
- ? renderObsOverlay(active, leaderboard, result, sessionTiming, branding, {
- t,
- escapeHtml,
- formatLap,
- getVisiblePassings,
- getSessionTypeLabel,
- getStartModeLabel,
- getStatusLabel,
- getEventName,
- getObsOverlayConfig,
- normalizeStartMode,
- renderPositionGrid,
- getSessionGridEntries,
- formatTeamActiveMemberLabel,
- renderOverlayLeaderboard,
- })
- : overlayViewMode === "tv"
- ? `
-
-
-
-
- ${t("overlay.fastest_lap")}
- ${formatLap(fastestRow?.bestLapMs)}
-
- ${escapeHtml(fastestRow?.displayName || fastestRow?.driverName || "-")}
- ${t("table.laps")}: ${topRow?.laps || 0} | ${t("timing.total_passings")}: ${getVisiblePassings(result).length || 0}
-
-
- ${renderOverlayLeaderboard(leaderboard, { t, escapeHtml, formatTeamActiveMemberLabel, formatLap, formatPredictedLapDelta })}
-
-
-
- `
- : `
-
-
-
-
- ${t("overlay.fastest_lap")}
- ${formatLap(fastestRow?.bestLapMs)}
-
- ${escapeHtml(fastestRow?.displayName || fastestRow?.driverName || "-")}
- ${t("table.laps")}: ${topRow?.laps || 0} | ${t("timing.total_passings")}: ${result?.passings.length || 0}
-
-
- ${renderOverlayLeaderboard(leaderboard, { t, escapeHtml, formatTeamActiveMemberLabel, formatLap, formatPredictedLapDelta })}
-
-
-
-
- `
- }
- `
- : `
-
- ${t("overlay.title")}
- ${t("overlay.no_active")}
-
- `
- }
-
- `;
-
- document.getElementById("overlayFullscreen")?.addEventListener("click", async () => {
- const target = document.documentElement;
- if (!document.fullscreenElement) {
- await target.requestFullscreen?.().catch(() => {});
- return;
- }
- await document.exitFullscreen?.().catch(() => {});
- });
- document.getElementById("overlayLaunchLeaderboard")?.addEventListener("click", () => openOverlayWindow("leaderboard"));
- document.getElementById("overlayLaunchSpeaker")?.addEventListener("click", () => openOverlayWindow("speaker"));
- document.getElementById("overlayLaunchResults")?.addEventListener("click", () => openOverlayWindow("results"));
- document.getElementById("overlayLaunchTv")?.addEventListener("click", () => openOverlayWindow("tv"));
- document.getElementById("overlayLaunchTeam")?.addEventListener("click", () => openOverlayWindow("team"));
- document.getElementById("overlayLaunchObs")?.addEventListener("click", () => openOverlayWindow("obs", { public: true }));
- document.getElementById("overlayCopyObsUrl")?.addEventListener("click", async () => {
- const url = buildOverlayUrl("obs", { public: true });
- if (navigator.clipboard?.writeText) {
- await navigator.clipboard.writeText(url).catch(() => {});
- return;
- }
- window.prompt("Copy OBS URL", url);
- });
-}
-
function getQuickAddState(transponder) {
const normalized = String(transponder || "").trim();
const driver = state.drivers.find((item) => String(item.transponder || "").trim() === normalized) || null;
diff --git a/src/misc_views.js b/src/misc_views.js
new file mode 100644
index 0000000..a7ee221
--- /dev/null
+++ b/src/misc_views.js
@@ -0,0 +1,272 @@
+function renderGuidePanel(titleKey, itemKeys, extras = "", { t }) {
+ return `
+
+
+
+
+ ${itemKeys.map((key) => `- ${t(key)}
`).join("")}
+
+ ${extras}
+
+
+ `;
+}
+
+function renderGuideOverviewCard(titleKey, blurbKey, { t }) {
+ return `
+
+ ${t(titleKey)}
+ ${t(blurbKey)}
+
+ `;
+}
+
+export function renderGuideView({ dom, t }) {
+ dom.view.innerHTML = `
+
+
+
+
${t("guide.intro")}
+
+ ${renderGuideOverviewCard("guide.sponsor_title", "guide.card_sponsor_blurb", { t })}
+ ${renderGuideOverviewCard("guide.race_title", "guide.card_race_blurb", { t })}
+ ${renderGuideOverviewCard("guide.team_title", "guide.card_team_blurb", { t })}
+ ${renderGuideOverviewCard("guide.windows_title", "guide.card_decoder_blurb", { t })}
+
+
+
+
+
+ ${renderGuidePanel("guide.sponsor_title", ["guide.sponsor_1", "guide.sponsor_2", "guide.sponsor_3", "guide.sponsor_4", "guide.sponsor_5", "guide.sponsor_6"], "", { t })}
+ ${renderGuidePanel("guide.race_wizard_title", ["guide.race_wizard_1", "guide.race_wizard_2", "guide.race_wizard_3", "guide.race_wizard_4", "guide.race_wizard_5", "guide.race_wizard_6", "guide.race_wizard_7"], "", { t })}
+
+
+
+ ${renderGuidePanel("guide.race_title", ["guide.race_1", "guide.race_2", "guide.race_3", "guide.race_4", "guide.race_4a", "guide.race_5", "guide.race_6", "guide.race_7", "guide.race_8", "guide.race_9", "guide.race_10"], "", { t })}
+ ${renderGuidePanel("guide.manage_steps_title", ["guide.manage_steps_1", "guide.manage_steps_2", "guide.manage_steps_3", "guide.manage_steps_4", "guide.manage_steps_5", "guide.manage_steps_6", "guide.manage_steps_7", "guide.manage_steps_8"], "", { t })}
+
+
+
+ ${renderGuidePanel("guide.race_format_title", ["guide.race_format_0", "guide.race_format_1", "guide.race_format_2", "guide.race_format_3", "guide.race_format_4", "guide.race_format_5", "guide.race_format_6", "guide.race_format_7", "guide.race_format_8", "guide.race_format_9", "guide.race_format_10", "guide.race_format_11", "guide.race_format_11a", "guide.race_format_12", "guide.race_format_13", "guide.race_format_14"], "", { t })}
+ ${renderGuidePanel("guide.validation_title", ["guide.validation_1", "guide.validation_2", "guide.validation_3", "guide.validation_4", "guide.validation_5", "guide.validation_6", "guide.validation_7", "guide.validation_8"], "", { t })}
+
+
+
+ ${renderGuidePanel("guide.free_practice_title", ["guide.free_practice_1", "guide.free_practice_2", "guide.free_practice_3"], "", { t })}
+ ${renderGuidePanel("guide.open_practice_title", ["guide.open_practice_1", "guide.open_practice_2", "guide.open_practice_3"], "", { t })}
+
+
+
+ ${renderGuidePanel("guide.team_title", ["guide.team_1", "guide.team_2", "guide.team_3", "guide.team_4", "guide.team_5", "guide.team_6"], "", { t })}
+ ${renderGuidePanel("guide.qualifying_title", ["guide.qualifying_1", "guide.qualifying_2", "guide.qualifying_3", "guide.qualifying_4", "guide.qualifying_5"], "", { t })}
+
+
+
+ ${renderGuidePanel("guide.dashboard_title", ["guide.dashboard_1", "guide.dashboard_2", "guide.dashboard_3"], "", { t })}
+ ${renderGuidePanel("guide.host_title", ["guide.host_1", "guide.host_2", "guide.host_3", "guide.host_4", "guide.host_5", "guide.host_6", "guide.host_7", "guide.host_8"], "", { t })}
+
+
+
+ ${renderGuidePanel("guide.windows_title", ["guide.windows_1", "guide.windows_2", "guide.windows_3", "guide.windows_4", "guide.windows_5"], "", { t })}
+ ${renderGuidePanel("guide.linux_title", ["guide.linux_1", "guide.linux_2", "guide.linux_3"], "", { t })}
+
+
+
+
+
+
+ - ${t("guide.sqlite_1")}
+ - ${t("guide.sqlite_2")}
+ - ${t("guide.sqlite_3")}
+ - ${t("guide.sqlite_4")}
+ - ${t("guide.sqlite_5")}
+
+
${t("guide.ammc_ref")}
+
+
+ `;
+}
+
+export function renderOverlayPageView(deps) {
+ const { state, dom, t, escapeHtml, formatLap, formatCountdown, formatElapsedClock, formatPredictedLapDelta, getActiveSession, buildLeaderboard, ensureSessionResult, getSessionTiming, getVisiblePassings, resolveEventBranding, buildPracticeStandings, buildQualifyingStandings, buildFinalStandings, getOverlayModeLabel, getStatusLabel, getObsOverlayConfig, buildOverlayPanels, normalizeStartMode, renderPositionGrid, getEventName, getSessionTypeLabel, getStartModeLabel, renderRaceStandingsTableView, renderTeamOverlay, renderObsOverlay, renderOverlayLeaderboard, renderOverlaySidePanel, formatTeamActiveMemberLabel, getSessionGridEntries, overlayViewMode, overlayMode, publicOverlayMode, overlayEvents, overlayRotationIndex, openOverlayWindow, buildOverlayUrl } = deps;
+
+ const active = getActiveSession();
+ const leaderboard = active ? buildLeaderboard(active).slice(0, 12) : [];
+ const result = active ? ensureSessionResult(active.id) : null;
+ const sessionTiming = active ? getSessionTiming(active) : null;
+ const overlayClock = sessionTiming?.followUpActive
+ ? formatCountdown(sessionTiming?.followUpRemainingMs ?? 0)
+ : sessionTiming?.untimed
+ ? formatElapsedClock(sessionTiming?.elapsedMs ?? 0)
+ : formatCountdown(sessionTiming?.remainingMs ?? 0);
+ const recent = active && result ? getVisiblePassings(result).slice(-8).reverse() : [];
+ const event = active ? state.events.find((item) => item.id === active.eventId) : null;
+ const branding = resolveEventBranding(event);
+ const practiceRows = event ? buildPracticeStandings(event) : [];
+ const qualifyingRows = event ? buildQualifyingStandings(event) : [];
+ const finalRows = event ? buildFinalStandings(event) : [];
+ const topRow = leaderboard[0] || null;
+ const fastestRow = [...leaderboard].filter((row) => Number.isFinite(row.bestLapMs)).sort((left, right) => left.bestLapMs - right.bestLapMs)[0] || null;
+ const modeLabel = getOverlayModeLabel(overlayViewMode, { t });
+ const overlayStatusLabel = sessionTiming?.followUpActive ? t("timing.follow_up_active") : active ? getStatusLabel(active.status) : "";
+ const obsConfig = overlayViewMode === "obs" ? getObsOverlayConfig() : null;
+ const rotatingPanels = buildOverlayPanels(active, recent, { t, overlayEvents, normalizeStartMode, renderPositionGrid });
+ const activePanel = rotatingPanels.length ? rotatingPanels[overlayRotationIndex % rotatingPanels.length] : null;
+ const denseOverlay = overlayViewMode === "leaderboard" || overlayViewMode === "tv";
+
+ dom.view.innerHTML = `
+ ${overlayMode ? "" : `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `}
+
+ ${active ? `
+ ${overlayViewMode === "obs" ? "" : ``}
+
+ ${overlayViewMode === "speaker" ? `
+
+
+
P1
+
${escapeHtml(topRow?.displayName || topRow?.driverName || t("common.unknown_driver"))}
+
${t("table.result")}: ${escapeHtml(topRow?.resultDisplay || "-")}
+
${t("table.best_lap")}: ${formatLap(topRow?.bestLapMs)}
+
+
+
+ ${t("overlay.last_passings")}
+ ${recent.length ? recent.map((passing) => `
+
+ ${escapeHtml(passing.displayName || passing.teamName || passing.driverName || t("common.unknown_driver"))}
+ ${formatLap(passing.lapMs)}
+
+ `).join("") : `${t("timing.no_passings")}
`}
+
+
+ ${t("events.position_grid")}
+ ${normalizeStartMode(active.startMode) === "position" ? renderPositionGrid(active) : `${t("events.na")}
`}
+
+
+ ${t("overlay.event_markers")}
+ ${overlayEvents.length ? overlayEvents.map((item) => `
+
+ ${escapeHtml(item.label)}
+ ${new Date(item.ts).toLocaleTimeString()}
+
+ `).join("") : `${t("timing.no_passings")}
`}
+
+
+
+ ` : overlayViewMode === "results" ? `
+
+
+
${t("events.practice_standings")}
+ ${renderRaceStandingsTableView(practiceRows, t("events.no_practice_results"))}
+
+
+
${t("events.qualifying_standings")}
+ ${renderRaceStandingsTableView(qualifyingRows, t("events.no_qualifying_results"))}
+
+
+
${t("events.final_standings")}
+ ${renderRaceStandingsTableView(finalRows, t("events.no_final_results"))}
+
+
+ ` : overlayViewMode === "team" ? renderTeamOverlay(leaderboard, result, sessionTiming, { t, escapeHtml, formatLap, formatPredictedLapDelta, formatTeamActiveMemberLabel, getVisiblePassings, renderOverlayLeaderboard }) : overlayViewMode === "obs" ? renderObsOverlay(active, leaderboard, result, sessionTiming, branding, { t, escapeHtml, formatLap, getVisiblePassings, getSessionTypeLabel, getStartModeLabel, getStatusLabel, getEventName, getObsOverlayConfig, normalizeStartMode, renderPositionGrid, getSessionGridEntries, formatTeamActiveMemberLabel, renderOverlayLeaderboard }) : overlayViewMode === "tv" ? `
+
+
+
+
+ ${t("overlay.fastest_lap")}
+ ${formatLap(fastestRow?.bestLapMs)}
+
+ ${escapeHtml(fastestRow?.displayName || fastestRow?.driverName || "-")}
+ ${t("table.laps")}: ${topRow?.laps || 0} | ${t("timing.total_passings")}: ${getVisiblePassings(result).length || 0}
+
+
+ ${renderOverlayLeaderboard(leaderboard, { t, escapeHtml, formatTeamActiveMemberLabel, formatLap, formatPredictedLapDelta })}
+
+
+
+ ` : `
+
+
+
+
+ ${t("overlay.fastest_lap")}
+ ${formatLap(fastestRow?.bestLapMs)}
+
+ ${escapeHtml(fastestRow?.displayName || fastestRow?.driverName || "-")}
+ ${t("table.laps")}: ${topRow?.laps || 0} | ${t("timing.total_passings")}: ${result?.passings.length || 0}
+
+
+ ${renderOverlayLeaderboard(leaderboard, { t, escapeHtml, formatTeamActiveMemberLabel, formatLap, formatPredictedLapDelta })}
+
+
+
+
+ `}
+ ` : `
+
+ ${t("overlay.title")}
+ ${t("overlay.no_active")}
+
+ `}
+
+ `;
+
+ document.getElementById("overlayFullscreen")?.addEventListener("click", async () => {
+ const target = document.documentElement;
+ if (!document.fullscreenElement) {
+ await target.requestFullscreen?.().catch(() => {});
+ return;
+ }
+ await document.exitFullscreen?.().catch(() => {});
+ });
+ document.getElementById("overlayLaunchLeaderboard")?.addEventListener("click", () => openOverlayWindow("leaderboard"));
+ document.getElementById("overlayLaunchSpeaker")?.addEventListener("click", () => openOverlayWindow("speaker"));
+ document.getElementById("overlayLaunchResults")?.addEventListener("click", () => openOverlayWindow("results"));
+ document.getElementById("overlayLaunchTv")?.addEventListener("click", () => openOverlayWindow("tv"));
+ document.getElementById("overlayLaunchTeam")?.addEventListener("click", () => openOverlayWindow("team"));
+ document.getElementById("overlayLaunchObs")?.addEventListener("click", () => openOverlayWindow("obs", { public: true }));
+ document.getElementById("overlayCopyObsUrl")?.addEventListener("click", async () => {
+ const url = buildOverlayUrl("obs", { public: true });
+ if (navigator.clipboard?.writeText) {
+ await navigator.clipboard.writeText(url).catch(() => {});
+ return;
+ }
+ window.prompt("Copy OBS URL", url);
+ });
+}