diff --git a/src/app.js b/src/app.js
index cdf1244..5ffd723 100644
--- a/src/app.js
+++ b/src/app.js
@@ -1,3 +1,5 @@
+import { getOverlayModeLabel, buildOverlayPanels, renderOverlaySidePanel, renderOverlayLeaderboard, renderTeamOverlay, renderObsOverlay } from "./overlays.js";
+
const NAV_ITEMS = [
{ id: "dashboard", titleKey: "nav.dashboard", subtitleKey: "nav.dashboard_sub" },
{ id: "events", titleKey: "nav.events", subtitleKey: "nav.events_sub" },
@@ -6594,10 +6596,10 @@ function renderOverlay() {
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);
+ 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);
+ const rotatingPanels = buildOverlayPanels(active, recent, { t, overlayEvents, normalizeStartMode, renderPositionGrid });
const activePanel = rotatingPanels.length ? rotatingPanels[overlayRotationIndex % rotatingPanels.length] : null;
const denseOverlay = overlayViewMode === "leaderboard" || overlayViewMode === "tv";
@@ -6722,9 +6724,24 @@ function renderOverlay() {
`
: overlayViewMode === "team"
- ? renderTeamOverlay(leaderboard, result, sessionTiming)
+ ? renderTeamOverlay(leaderboard, result, sessionTiming, { t, escapeHtml, formatLap, formatTeamActiveMemberLabel, getVisiblePassings, renderOverlayLeaderboard })
: overlayViewMode === "obs"
- ? renderObsOverlay(active, leaderboard, result, sessionTiming, branding)
+ ? renderObsOverlay(active, leaderboard, result, sessionTiming, branding, {
+ t,
+ escapeHtml,
+ formatLap,
+ getVisiblePassings,
+ getSessionTypeLabel,
+ getStartModeLabel,
+ getStatusLabel,
+ getEventName,
+ getObsOverlayConfig,
+ normalizeStartMode,
+ renderPositionGrid,
+ getSessionGridEntries,
+ formatTeamActiveMemberLabel,
+ renderOverlayLeaderboard,
+ })
: overlayViewMode === "tv"
? `
@@ -6738,7 +6755,7 @@ function renderOverlay() {
${t("table.laps")}: ${topRow?.laps || 0} | ${t("timing.total_passings")}: ${getVisiblePassings(result).length || 0}
- ${renderOverlayLeaderboard(leaderboard)}
+ ${renderOverlayLeaderboard(leaderboard, { t, escapeHtml, formatTeamActiveMemberLabel, formatLap, formatPredictedLapDelta })}
@@ -6755,11 +6772,11 @@ function renderOverlay() {
${t("table.laps")}: ${topRow?.laps || 0} | ${t("timing.total_passings")}: ${result?.passings.length || 0}
- ${renderOverlayLeaderboard(leaderboard)}
+ ${renderOverlayLeaderboard(leaderboard, { t, escapeHtml, formatTeamActiveMemberLabel, formatLap, formatPredictedLapDelta })}
`
@@ -6799,201 +6816,6 @@ function renderOverlay() {
});
}
-function renderObsOverlay(active, leaderboard, result, sessionTiming, branding) {
- const obsConfig = getObsOverlayConfig();
- const compactRows = leaderboard.slice(0, obsConfig.rows);
- const readyPositionGrid = active && active.status === "ready" && normalizeStartMode(active.startMode) === "position";
- const showStartGrid = obsConfig.showGrid && readyPositionGrid;
- const leadRow = compactRows[0] || null;
- const fastestRow =
- [...compactRows].filter((row) => Number.isFinite(row.bestLapMs)).sort((left, right) => left.bestLapMs - right.bestLapMs)[0] || null;
- const visiblePassings = getVisiblePassings(result);
- const elapsedOrRemaining = sessionTiming?.untimed
- ? formatElapsedClock(sessionTiming?.elapsedMs ?? 0)
- : sessionTiming?.followUpActive
- ? formatCountdown(sessionTiming?.followUpRemainingMs ?? 0)
- : formatCountdown(sessionTiming?.remainingMs ?? 0);
-
- if (obsConfig.layout === "grid") {
- return `
-
-
-
- ${branding.logoDataUrl ? `
})
` : ""}
-
-
${escapeHtml(getEventName(active.eventId))}
-
${escapeHtml(active.name)}
-
- ${escapeHtml(getSessionTypeLabel(active.type))}
- ${escapeHtml(getStartModeLabel(active.startMode))}
- ${t("table.laps")}: ${leadRow?.laps || 0}
- ${t("timing.total_passings")}: ${visiblePassings.length || 0}
-
-
-
- ${obsConfig.showClock ? `
-
- ${elapsedOrRemaining}
- ${escapeHtml(sessionTiming?.followUpActive ? t("timing.follow_up_active") : getStatusLabel(active.status))}
-
- ` : ""}
-
-
-
-
${showStartGrid ? t("events.start_grid") : t("overlay.leaderboard_live")}
- ${t("overlay.mode_obs")}
-
- ${showStartGrid ? renderPositionGrid(active) : renderOverlayLeaderboard(compactRows)}
-
-
- `;
- }
-
- if (obsConfig.layout === "lowerthird") {
- return `
-
-
-
-
${escapeHtml(getEventName(active.eventId))}
-
${escapeHtml(active.name)}
-
- ${obsConfig.showClock ? `
${elapsedOrRemaining}` : ""}
-
-
- ${compactRows.slice(0, 6).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)}
- ${obsConfig.showLaps ? `${t("table.laps")}: ${row.laps ?? 0}` : ""}
- ${obsConfig.showResult ? `${t("table.result")}: ${escapeHtml(row.resultDisplay)}` : ""}
- ${obsConfig.showGap ? `${t("table.gap")}: ${escapeHtml(row.gapDisplay || row.gapAhead || "-")}` : ""}
-
- `;
- }).join("")}
-
-
- `;
- }
-
- const towerRowsHtml = showStartGrid
- ? getSessionGridEntries(active)
- .slice(0, obsConfig.rows)
- .map((entry, idx) => {
- const posClass = idx === 0 ? "pos-1" : idx === 1 ? "pos-2" : idx === 2 ? "pos-3" : "";
- return `
-
- ${entry.slot}
-
- ${escapeHtml(entry.name)}
- ${escapeHtml(entry.meta || "-")}
-
-
- `;
- })
- .join("")
- : compactRows
- .map((row, idx) => {
- const posClass = idx === 0 ? "pos-1" : idx === 1 ? "pos-2" : idx === 2 ? "pos-3" : "";
- const trail = [
- obsConfig.showLaps ? `${row.laps ?? 0}L` : "",
- obsConfig.showResult ? `${escapeHtml(row.resultDisplay)}` : "",
- obsConfig.showBest ? `${formatLap(row.bestLapMs)}` : "",
- obsConfig.showGap ? `${escapeHtml(row.gapDisplay || row.gapAhead || "-")}` : "",
- ]
- .filter(Boolean)
- .join("");
- return `
-
- ${idx + 1}
-
- ${escapeHtml(row.displayName || row.driverName)}
- ${escapeHtml(row.teamId ? formatTeamActiveMemberLabel(row) : row.subLabel || row.transponder || "-")}
-
- ${trail ? `${trail}
` : ""}
-
- `;
- })
- .join("");
-
- return `
-
-
-
- ${branding.logoDataUrl ? `
})
` : ""}
-
-
${escapeHtml(getEventName(active.eventId))}
-
${escapeHtml(active.name)}
-
-
- ${obsConfig.showClock ? `
${elapsedOrRemaining}
` : ""}
-
-
- ${escapeHtml(getSessionTypeLabel(active.type))}
- ${escapeHtml(getStartModeLabel(active.startMode))}
- ${showStartGrid ? `${t("events.start_grid")}` : ""}
- ${!showStartGrid && obsConfig.showFastest ? `${t("overlay.fastest_lap")}: ${formatLap(fastestRow?.bestLapMs)}` : ""}
-
-
- ${towerRowsHtml || `${t("timing.no_laps")}
`}
-
-
- `;
-}
-
-function buildOverlayPanels(active, recent) {
- return [
- {
- title: t("overlay.last_passings"),
- content: recent.length
- ? recent
- .map(
- (passing) => `
-
- ${escapeHtml(passing.displayName || passing.teamName || passing.driverName || passing.transponder || t("common.unknown_driver"))}
- ${formatLap(passing.lapMs)}
-
- `
- )
- .join("")
- : `${t("timing.no_passings")}
`,
- },
- {
- title: t("overlay.event_markers"),
- content: overlayEvents.length
- ? overlayEvents
- .map(
- (item) => `
-
- ${escapeHtml(item.label)}
- ${new Date(item.ts).toLocaleTimeString()}
-
- `
- )
- .join("")
- : `${t("timing.no_passings")}
`,
- },
- {
- title: t("events.position_grid"),
- content:
- active && normalizeStartMode(active.startMode) === "position" ? renderPositionGrid(active) : `${t("events.na")}
`,
- },
- ];
-}
-
-function renderOverlaySidePanel(panel) {
- return `
-
-
-
${escapeHtml(panel.title)}
- ${t("overlay.rotating_panel")}
-
- ${panel.content}
-
- `;
-}
-
function getQuickAddState(transponder) {
const normalized = String(transponder || "").trim();
const driver = state.drivers.find((item) => String(item.transponder || "").trim() === normalized) || null;
@@ -7160,124 +6982,6 @@ function renderLeaderboard(rows) {
);
}
-function renderOverlayLeaderboard(rows) {
- if (!rows.length) {
- return `${t("timing.no_laps")}
`;
- }
-
- return `
-
- ${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)}
-
${escapeHtml(row.teamId ? `${t("overlay.active_member")}: ${formatTeamActiveMemberLabel(row)}` : row.subLabel || row.transponder || "-")}
- ${row.invalidPending ? `
${escapeHtml(row.invalidLabel)}${row.invalidLapMs ? ` • ${formatLap(row.invalidLapMs)}` : ""}` : ""}
-
-
-
- ${formatPredictedLapDelta(row.predictedRemainingMs)}
-
-
-
-
-
-
- ${row.laps ?? 0}
-
-
-
- ${escapeHtml(row.resultDisplay)}
-
-
-
- ${escapeHtml(row.gapDisplay || row.gapAhead || "-")}
-
-
-
- ${escapeHtml(row.gapAhead || "-")}
-
-
-
- ${escapeHtml(row.lapDelta || "-")}
-
-
-
- ${formatLap(row.bestLapMs)}
-
-
- `;
- })
- .join("")}
-
- `;
-}
-
-function renderTeamOverlay(rows, result, sessionTiming) {
- const topThree = rows.slice(0, 3);
- return `
-
-
-
-
${t("overlay.top_three")}
- ${t("overlay.team_battle")}
-
-
- ${topThree
- .map(
- (row, index) => `
-
- ${index + 1}
- ${escapeHtml(row.displayName || row.driverName)}
- ${escapeHtml(row.resultDisplay || "-")}
- ${t("overlay.active_member")}: ${escapeHtml(formatTeamActiveMemberLabel(row))}
-
- `
- )
- .join("")}
-
-
-
-
-
-
- ${t("table.laps")}
- ${rows[0]?.laps || 0}
- ${escapeHtml(rows[0]?.displayName || rows[0]?.driverName || "-")}
-
-
- ${t("timing.total_passings")}
- ${getVisiblePassings(result).length || 0}
- ${sessionTiming?.untimed ? t("timing.elapsed") : t("timing.remaining")}
-
-
- ${t("overlay.fastest_lap")}
- ${formatLap([...rows].filter((row) => Number.isFinite(row.bestLapMs)).sort((a, b) => a.bestLapMs - b.bestLapMs)[0]?.bestLapMs)}
- ${escapeHtml(
- [...rows].filter((row) => Number.isFinite(row.bestLapMs)).sort((a, b) => a.bestLapMs - b.bestLapMs)[0]?.displayName || "-"
- )}
-
-
-
-
-
${t("events.team_standings")}
-
- ${renderOverlayLeaderboard(rows)}
-
-
-
-
- `;
-}
-
function renderRecentPassings(session) {
if (!session) {
return `${t("timing.no_session_selected")}
`;
@@ -7813,10 +7517,6 @@ function getSessionTypeLabel(type) {
return translated === key ? String(type || "") : translated;
}
-function getOverlayModeLabel(mode) {
- return t(`overlay.mode_${String(mode || "leaderboard").toLowerCase()}`);
-}
-
function normalizeStartMode(mode) {
return ["mass", "position", "staggered"].includes(String(mode || "").toLowerCase()) ? String(mode).toLowerCase() : "mass";
}
diff --git a/src/overlays.js b/src/overlays.js
new file mode 100644
index 0000000..ba6fd53
--- /dev/null
+++ b/src/overlays.js
@@ -0,0 +1,373 @@
+export function getOverlayModeLabel(mode, { t }) {
+ return t(`overlay.mode_${String(mode || "leaderboard").toLowerCase()}`);
+}
+
+export function buildOverlayPanels(active, recent, { t, overlayEvents, normalizeStartMode, renderPositionGrid }) {
+ return [
+ {
+ title: t("overlay.last_passings"),
+ content: recent.length
+ ? recent
+ .map(
+ (passing) => `
+
+ ${escapeHtmlLocal(passing.displayName || passing.teamName || passing.driverName || passing.transponder || t("common.unknown_driver"))}
+ ${passing.lapMs == null ? "-" : formatLapLocal(passing.lapMs)}
+
+ `
+ )
+ .join("")
+ : `${t("timing.no_passings")}
`,
+ },
+ {
+ title: t("overlay.event_markers"),
+ content: overlayEvents.length
+ ? overlayEvents
+ .map(
+ (item) => `
+
+ ${escapeHtmlLocal(item.label)}
+ ${new Date(item.ts).toLocaleTimeString()}
+
+ `
+ )
+ .join("")
+ : `${t("timing.no_passings")}
`,
+ },
+ {
+ title: t("events.position_grid"),
+ content:
+ active && normalizeStartMode(active.startMode) === "position" ? renderPositionGrid(active) : `${t("events.na")}
`,
+ },
+ ];
+}
+
+export function renderOverlaySidePanel(panel, { t, escapeHtml }) {
+ return `
+
+
+
${escapeHtml(panel.title)}
+ ${t("overlay.rotating_panel")}
+
+ ${panel.content}
+
+ `;
+}
+
+export function renderOverlayLeaderboard(rows, { t, escapeHtml, formatTeamActiveMemberLabel, formatLap, formatPredictedLapDelta }) {
+ if (!rows.length) {
+ return `${t("timing.no_laps")}
`;
+ }
+
+ return `
+
+ ${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)}
+
${escapeHtml(row.teamId ? `${t("overlay.active_member")}: ${formatTeamActiveMemberLabel(row)}` : row.subLabel || row.transponder || "-")}
+ ${row.invalidPending ? `
${escapeHtml(row.invalidLabel)}${row.invalidLapMs ? ` • ${formatLap(row.invalidLapMs)}` : ""}` : ""}
+
+
+
+ ${formatPredictedLapDelta(row.predictedRemainingMs)}
+
+
+
+
+
+
+ ${row.laps ?? 0}
+
+
+
+ ${escapeHtml(row.resultDisplay)}
+
+
+
+ ${escapeHtml(row.gapDisplay || row.gapAhead || "-")}
+
+
+
+ ${escapeHtml(row.gapAhead || "-")}
+
+
+
+ ${escapeHtml(row.lapDelta || "-")}
+
+
+
+ ${formatLap(row.bestLapMs)}
+
+
+ `;
+ })
+ .join("")}
+
+ `;
+}
+
+export function renderTeamOverlay(rows, result, sessionTiming, deps) {
+ const { t, escapeHtml, formatLap, formatTeamActiveMemberLabel, getVisiblePassings } = deps;
+ const topThree = rows.slice(0, 3);
+ return `
+
+
+
+
${t("overlay.top_three")}
+ ${t("overlay.team_battle")}
+
+
+ ${topThree
+ .map(
+ (row, index) => `
+
+ ${index + 1}
+ ${escapeHtml(row.displayName || row.driverName)}
+ ${escapeHtml(row.resultDisplay || "-")}
+ ${t("overlay.active_member")}: ${escapeHtml(formatTeamActiveMemberLabel(row))}
+
+ `
+ )
+ .join("")}
+
+
+
+
+
+
+ ${t("table.laps")}
+ ${rows[0]?.laps || 0}
+ ${escapeHtml(rows[0]?.displayName || rows[0]?.driverName || "-")}
+
+
+ ${t("timing.total_passings")}
+ ${getVisiblePassings(result).length || 0}
+ ${sessionTiming?.untimed ? t("timing.elapsed") : t("timing.remaining")}
+
+
+ ${t("overlay.fastest_lap")}
+ ${formatLap([...rows].filter((row) => Number.isFinite(row.bestLapMs)).sort((a, b) => a.bestLapMs - b.bestLapMs)[0]?.bestLapMs)}
+ ${escapeHtml(
+ [...rows].filter((row) => Number.isFinite(row.bestLapMs)).sort((a, b) => a.bestLapMs - b.bestLapMs)[0]?.displayName || "-"
+ )}
+
+
+
+
+
${t("events.team_standings")}
+
+ ${renderOverlayLeaderboard(rows, deps)}
+
+
+
+
+ `;
+}
+
+export function renderObsOverlay(active, leaderboard, result, sessionTiming, branding, deps) {
+ const {
+ t,
+ escapeHtml,
+ formatLap,
+ getVisiblePassings,
+ getSessionTypeLabel,
+ getStartModeLabel,
+ getStatusLabel,
+ getEventName,
+ getObsOverlayConfig,
+ normalizeStartMode,
+ renderPositionGrid,
+ getSessionGridEntries,
+ formatTeamActiveMemberLabel,
+ } = deps;
+ const obsConfig = getObsOverlayConfig();
+ const compactRows = leaderboard.slice(0, obsConfig.rows);
+ const readyPositionGrid = active && active.status === "ready" && normalizeStartMode(active.startMode) === "position";
+ const showStartGrid = obsConfig.showGrid && readyPositionGrid;
+ const leadRow = compactRows[0] || null;
+ const fastestRow =
+ [...compactRows].filter((row) => Number.isFinite(row.bestLapMs)).sort((left, right) => left.bestLapMs - right.bestLapMs)[0] || null;
+ const visiblePassings = getVisiblePassings(result);
+ const elapsedOrRemaining = sessionTiming?.untimed
+ ? formatElapsedClockLocal(sessionTiming?.elapsedMs ?? 0)
+ : sessionTiming?.followUpActive
+ ? formatCountdownLocal(sessionTiming?.followUpRemainingMs ?? 0)
+ : formatCountdownLocal(sessionTiming?.remainingMs ?? 0);
+
+ if (obsConfig.layout === "grid") {
+ return `
+
+
+
+ ${branding.logoDataUrl ? `
})
` : ""}
+
+
${escapeHtml(getEventName(active.eventId))}
+
${escapeHtml(active.name)}
+
+ ${escapeHtml(getSessionTypeLabel(active.type))}
+ ${escapeHtml(getStartModeLabel(active.startMode))}
+ ${t("table.laps")}: ${leadRow?.laps || 0}
+ ${t("timing.total_passings")}: ${visiblePassings.length || 0}
+
+
+
+ ${obsConfig.showClock ? `
+
+ ${elapsedOrRemaining}
+ ${escapeHtml(sessionTiming?.followUpActive ? t("timing.follow_up_active") : getStatusLabel(active.status))}
+
+ ` : ""}
+
+
+
+
${showStartGrid ? t("events.start_grid") : t("overlay.leaderboard_live")}
+ ${t("overlay.mode_obs")}
+
+ ${showStartGrid ? renderPositionGrid(active) : renderOverlayLeaderboard(compactRows, deps)}
+
+
+ `;
+ }
+
+ if (obsConfig.layout === "lowerthird") {
+ return `
+
+
+
+
${escapeHtml(getEventName(active.eventId))}
+
${escapeHtml(active.name)}
+
+ ${obsConfig.showClock ? `
${elapsedOrRemaining}` : ""}
+
+
+ ${compactRows.slice(0, 6).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)}
+ ${obsConfig.showLaps ? `${t("table.laps")}: ${row.laps ?? 0}` : ""}
+ ${obsConfig.showResult ? `${t("table.result")}: ${escapeHtml(row.resultDisplay)}` : ""}
+ ${obsConfig.showGap ? `${t("table.gap")}: ${escapeHtml(row.gapDisplay || row.gapAhead || "-")}` : ""}
+
+ `;
+ }).join("")}
+
+
+ `;
+ }
+
+ const towerRowsHtml = showStartGrid
+ ? getSessionGridEntries(active)
+ .slice(0, obsConfig.rows)
+ .map((entry, idx) => {
+ const posClass = idx === 0 ? "pos-1" : idx === 1 ? "pos-2" : idx === 2 ? "pos-3" : "";
+ return `
+
+ ${entry.slot}
+
+ ${escapeHtml(entry.name)}
+ ${escapeHtml(entry.meta || "-")}
+
+
+ `;
+ })
+ .join("")
+ : compactRows
+ .map((row, idx) => {
+ const posClass = idx === 0 ? "pos-1" : idx === 1 ? "pos-2" : idx === 2 ? "pos-3" : "";
+ const trail = [
+ obsConfig.showLaps ? `${row.laps ?? 0}L` : "",
+ obsConfig.showResult ? `${escapeHtml(row.resultDisplay)}` : "",
+ obsConfig.showBest ? `${formatLap(row.bestLapMs)}` : "",
+ obsConfig.showGap ? `${escapeHtml(row.gapDisplay || row.gapAhead || "-")}` : "",
+ ]
+ .filter(Boolean)
+ .join("");
+ return `
+
+ ${idx + 1}
+
+ ${escapeHtml(row.displayName || row.driverName)}
+ ${escapeHtml(row.teamId ? formatTeamActiveMemberLabel(row) : row.subLabel || row.transponder || "-")}
+
+ ${trail ? `${trail}
` : ""}
+
+ `;
+ })
+ .join("");
+
+ return `
+
+
+
+ ${branding.logoDataUrl ? `
})
` : ""}
+
+
${escapeHtml(getEventName(active.eventId))}
+
${escapeHtml(active.name)}
+
+
+ ${obsConfig.showClock ? `
${elapsedOrRemaining}
` : ""}
+
+
+ ${escapeHtml(getSessionTypeLabel(active.type))}
+ ${escapeHtml(getStartModeLabel(active.startMode))}
+ ${showStartGrid ? `${t("events.start_grid")}` : ""}
+ ${!showStartGrid && obsConfig.showFastest ? `${t("overlay.fastest_lap")}: ${formatLap(fastestRow?.bestLapMs)}` : ""}
+
+
+ ${towerRowsHtml || `${t("timing.no_laps")}
`}
+
+
+ `;
+}
+
+function escapeHtmlLocal(value) {
+ return String(value ?? "")
+ .replaceAll("&", "&")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">")
+ .replaceAll('"', """)
+ .replaceAll("'", "'");
+}
+
+function padClock(value) {
+ return String(value).padStart(2, "0");
+}
+
+function formatCountdownLocal(ms) {
+ const totalMs = Math.max(0, Number(ms) || 0);
+ const totalSeconds = Math.floor(totalMs / 1000);
+ const minutes = Math.floor(totalSeconds / 60);
+ const seconds = totalSeconds % 60;
+ return `${padClock(minutes)}:${padClock(seconds)}`;
+}
+
+function formatElapsedClockLocal(ms) {
+ const totalMs = Math.max(0, Number(ms) || 0);
+ const totalSeconds = Math.floor(totalMs / 1000);
+ const hours = Math.floor(totalSeconds / 3600);
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
+ const seconds = totalSeconds % 60;
+ return `${hours}:${padClock(minutes)}:${padClock(seconds)}`;
+}
+
+function formatLapLocal(ms) {
+ if (!Number.isFinite(ms)) {
+ return "-";
+ }
+ const totalMs = Math.max(0, Math.round(ms));
+ const minutes = Math.floor(totalMs / 60000);
+ const seconds = Math.floor((totalMs % 60000) / 1000);
+ const millis = totalMs % 1000;
+ return `${minutes}:${String(seconds).padStart(2, "0")}.${String(millis).padStart(3, "0")}`;
+}