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;