From 3069878167454b7d40086053bb25cb31ecb3aa38 Mon Sep 17 00:00:00 2001 From: larssand Date: Sat, 14 Mar 2026 19:36:59 +0100 Subject: [PATCH] =?UTF-8?q?add=20predict=20bar=20f=C3=B6r=20overlay?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.js | 23 +++++++++++++++++++++++ src/styles.css | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/src/app.js b/src/app.js index ff69e47..7bc0ab6 100644 --- a/src/app.js +++ b/src/app.js @@ -442,6 +442,7 @@ const TRANSLATIONS = { "overlay.fullscreen": "Fullscreen", "overlay.leaderboard_live": "Live leaderboard", "overlay.rotating_panel": "Displaypanel", + "overlay.next_predicted_lap": "Nästa varv", "overlay.event_markers": "Eventmarkörer", "guide.host_title": "Hur Managed AMMC körs", "guide.host_1": "1. AMMC körs alltid på samma maskin som `npm start` eller `node server.js` körs på.", @@ -902,6 +903,7 @@ const TRANSLATIONS = { "overlay.fullscreen": "Fullscreen", "overlay.leaderboard_live": "Live leaderboard", "overlay.rotating_panel": "Display panel", + "overlay.next_predicted_lap": "Next lap", "overlay.event_markers": "Event markers", "guide.host_title": "How Managed AMMC Runs", "guide.host_1": "1. AMMC always runs on the same machine where `npm start` or `node server.js` is running.", @@ -3796,6 +3798,15 @@ function renderOverlayLeaderboard(rows) {
${escapeHtml(row.driverName)} ${escapeHtml(row.transponder || "-")} +
+
+ + ${row.predictedRemainingMs !== null ? formatLap(row.predictedRemainingMs) : "-"} +
+
+
+
+
@@ -4511,6 +4522,7 @@ function buildLeaderboard(session) { const isFreePractice = sessionType === "free_practice"; const isOpenPractice = sessionType === "open_practice"; const isRollingPractice = isFreePractice || isOpenPractice; + const nowTs = Date.now(); const rows = Object.values(result.competitors).map((row) => { const totalElapsedMs = getCompetitorElapsedMs(session, row); const distanceToTargetMs = Math.abs(targetMs - totalElapsedMs); @@ -4519,6 +4531,15 @@ function buildLeaderboard(session) { const previousLapMs = passings.length >= 2 ? Number(passings[passings.length - 2].lapMs || 0) : null; const lapDeltaMs = row.lastLapMs && previousLapMs && row.lastLapMs > 0 && previousLapMs > 0 ? row.lastLapMs - previousLapMs : null; + const predictionBaseMs = + Number(row.lastLapMs || 0) > 0 + ? Number(row.lastLapMs) + : Number(row.bestLapMs || 0) > 0 + ? Number(row.bestLapMs) + : null; + const currentLapElapsedMs = row.lastTimestamp ? Math.max(0, nowTs - row.lastTimestamp) : 0; + const predictedRemainingMs = predictionBaseMs ? Math.max(0, predictionBaseMs - currentLapElapsedMs) : null; + const predictedProgress = predictionBaseMs ? Math.min(1.25, currentLapElapsedMs / predictionBaseMs) : 0; return { ...row, totalElapsedMs, @@ -4526,6 +4547,8 @@ function buildLeaderboard(session) { seedMetric, previousLapMs, lapDeltaMs, + predictedRemainingMs, + predictedProgress, comparisonMs: isRollingPractice ? row.bestLapMs || row.lastLapMs || Number.MAX_SAFE_INTEGER diff --git a/src/styles.css b/src/styles.css index f6d13fa..e2082d3 100644 --- a/src/styles.css +++ b/src/styles.css @@ -852,6 +852,38 @@ select:focus { letter-spacing: 0.08em; } +.overlay-prediction { + margin-top: 10px; +} + +.overlay-prediction-meta { + display: flex; + justify-content: space-between; + gap: 10px; + margin-bottom: 6px; +} + +.overlay-prediction-meta label, +.overlay-prediction-meta span { + color: var(--muted); + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.overlay-prediction-track { + height: 4px; + border-radius: 999px; + overflow: hidden; + background: rgba(255, 255, 255, 0.08); +} + +.overlay-prediction-fill { + height: 100%; + border-radius: 999px; + background: linear-gradient(90deg, #ffffff, #e10600); +} + .overlay-race-metric strong, .overlay-race-best strong { font-family: Orbitron, sans-serif;