From 47e5a3dea1a03aa0fd333e21ed085947f11cc970 Mon Sep 17 00:00:00 2001 From: larssand Date: Wed, 25 Mar 2026 19:32:59 +0100 Subject: [PATCH] Extract overlay and OBS rendering into module --- src/app.js | 348 ++++---------------------------------------- src/overlays.js | 373 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 397 insertions(+), 324 deletions(-) create mode 100644 src/overlays.js 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")}`; +}