diff --git a/data/index.css b/data/index.css index b2170e5..373b547 100644 --- a/data/index.css +++ b/data/index.css @@ -27,10 +27,12 @@ body { .logo { position: fixed; - top: 20px; + /* Vertikal zentriert im 60px-Header-Bereich (top:20px, height:60px → Mitte 50px) */ + top: 50px; left: 20px; width: auto; height: auto; + transform: translateY(-50%); z-index: 1000; border-radius: 10px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); @@ -43,7 +45,7 @@ body { } .logo:hover { - transform: scale(1.1); + transform: translateY(-50%) scale(1.1); } .logo img { @@ -105,17 +107,43 @@ body { transform: scale(1.1); } +.live-clock { + position: fixed; + top: 20px; + left: 25%; + transform: translateX(-50%); + height: 60px; + min-width: 150px; + display: flex; + align-items: center; + justify-content: center; + padding: 0 24px; + z-index: 1000; + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 30px; + font-family: "Consolas", "Menlo", "Courier New", monospace; + font-size: 1.6rem; + font-weight: 600; + letter-spacing: 2px; + color: rgba(255, 255, 255, 0.95); + font-variant-numeric: tabular-nums; +} + .heartbeat-indicators { position: fixed; top: 20px; right: 160px; + height: 60px; display: flex; - gap: 15px; + align-items: flex-end; + gap: 18px; z-index: 1000; background: rgba(255, 255, 255, 0.1); backdrop-filter: blur(10px); - border-radius: 25px; - padding: 10px 20px; + border-radius: 30px; + padding: 0 24px 10px 24px; border: 1px solid rgba(255, 255, 255, 0.2); } @@ -123,7 +151,8 @@ body { .logo { width: 40px; height: 40px; - top: 15px; + /* Mobile: Header-Band top:15px height:60px → Mitte 45px */ + top: 45px; left: 15px; padding: 3px; } @@ -142,22 +171,34 @@ body { font-size: 1.2rem; } + .live-clock { + top: 15px; + height: 40px; + min-width: 100px; + padding: 0 14px; + font-size: 1rem; + letter-spacing: 1px; + border-radius: 20px; + } + .heartbeat-indicators { top: 15px; right: 90px; - gap: 8px; - padding: 8px 12px; + height: 60px; + gap: 12px; + padding: 0 16px 10px 16px; font-size: 0.8rem; + border-radius: 30px; } - + .heartbeat-indicator { - width: 12px; - height: 12px; + width: 18px; + height: 18px; } - + .heartbeat-indicator::before { font-size: 8px; - top: -20px; + top: -14px; } .header h1 { @@ -170,8 +211,8 @@ body { } .heartbeat-indicator { - width: 20px; - height: 20px; + width: 26px; + height: 26px; border-radius: 50%; background: #f50f0f; transition: all 0.3s ease; @@ -181,7 +222,7 @@ body { .heartbeat-indicator::before { content: attr(data-label); position: absolute; - top: -25px; + top: -18px; left: 50%; transform: translateX(-50%); font-size: 10px; diff --git a/data/index.html b/data/index.html index 61f44e3..a70d0d8 100644 --- a/data/index.html +++ b/data/index.html @@ -24,6 +24,7 @@ +
--:--:--
🏆 ⚙️ @@ -167,6 +168,23 @@ document.getElementById(indicatorId).classList.remove("active"); } } + // Start-/Stopp-Event → Status kann jetzt wechseln (ready↔running). + // Bei Stop-Events den Display-Wert lokal einfrieren, damit der + // Timer nicht bis zum nächsten Sync weiterzählt und dann sichtbar + // zurückspringt. Anschließend sofort syncen, damit der Scheduler + // zwischen schneller (50 ms) und langsamer (500 ms) Taktung wechselt. + if (data.active === true) { + const now = Date.now(); + if (data.button === "stop1" && status1 === "running") { + timer1 += (now - lastSync) / 1000; + status1 = "stopped"; + } else if (data.button === "stop2" && status2 === "running") { + timer2 += (now - lastSync) / 1000; + status2 = "stopped"; + } + kickDisplayScheduler(); + syncFromBackend(); + } } try { @@ -468,13 +486,28 @@ } } + function formatEndTime(epochSeconds) { + if (!epochSeconds || epochSeconds < 1577836800) return ""; // < 2020 = kein NTP-Sync + const d = new Date(epochSeconds * 1000); + const hh = String(d.getHours()).padStart(2, "0"); + const mm = String(d.getMinutes()).padStart(2, "0"); + const ss = String(d.getSeconds()).padStart(2, "0"); + return `${hh}:${mm}:${ss}`; + } + function createEntryElement(entry) { const div = document.createElement("div"); div.className = "leaderboard-entry"; const nameSpan = document.createElement("span"); nameSpan.className = "name"; - nameSpan.textContent = entry.name || "Unbekannt"; + let label = entry.name || "Unbekannt"; + // Bei "Lauf N"-Einträgen die Endzeit in Klammern anhängen + if (/^Lauf\s+\d+$/.test(label)) { + const endTime = formatEndTime(entry.endEpoch); + if (endTime) label += ` (${endTime})`; + } + nameSpan.textContent = label; const timeSpan = document.createElement("span"); timeSpan.className = "time"; @@ -800,7 +833,7 @@ learningMode = data.learningMode; learningButton = data.learningButton || ""; lastSync = Date.now(); - updateDisplay(); + kickDisplayScheduler(); }) .catch((error) => console.error("Fehler beim Laden deiner Daten:", error) @@ -849,8 +882,24 @@ // Sync with backend every 1 second setInterval(syncFromBackend, 1000); - // Smooth update every 50ms - setInterval(updateDisplay, 50); + // Adaptive Update-Rate: 50 ms wenn mindestens eine Bahn läuft, + // sonst 500 ms. Über kickDisplayScheduler() kann der Zyklus sofort + // neu gestartet werden (WebSocket-Start-Event, frische Sync-Daten), + // damit beim Übergang Stand→Lauf nichts springt. + let displayTimer = null; + function scheduleDisplayUpdate() { + updateDisplay(); + const anyRunning = status1 === "running" || status2 === "running"; + displayTimer = setTimeout(scheduleDisplayUpdate, anyRunning ? 50 : 500); + } + function kickDisplayScheduler() { + if (displayTimer !== null) { + clearTimeout(displayTimer); + displayTimer = null; + } + scheduleDisplayUpdate(); + } + scheduleDisplayUpdate(); // Heartbeat timeout check (every second) setInterval(() => { @@ -882,6 +931,18 @@ // Leaderboard alle 5 Sekunden aktualisieren setInterval(loadLeaderboard, 5000); + + // Live-Uhr im Header (HH:mm:ss, Browser-Lokalzeit) + function updateLiveClock() { + const now = new Date(); + const hh = String(now.getHours()).padStart(2, "0"); + const mm = String(now.getMinutes()).padStart(2, "0"); + const ss = String(now.getSeconds()).padStart(2, "0"); + const el = document.getElementById("live-clock"); + if (el) el.textContent = `${hh}:${mm}:${ss}`; + } + updateLiveClock(); + setInterval(updateLiveClock, 1000); diff --git a/src/databasebackend.h b/src/databasebackend.h index 8de8a72..3f1019e 100644 --- a/src/databasebackend.h +++ b/src/databasebackend.h @@ -382,6 +382,7 @@ void setupBackendRoutes(AsyncWebServer &server) { entry["lane"] = t.lane; entry["time"] = t.timeMs / 1000.0; entry["timeFormatted"] = formatTime(t.timeMs); + entry["endEpoch"] = t.timestamp; }; DynamicJsonDocument doc(2048); @@ -557,7 +558,10 @@ void addLocalTime(String uid, String name, unsigned long timeMs, int lane) { newTime.uid = uid; newTime.name = name; newTime.timeMs = timeMs; - newTime.timestamp = millis(); + // Epoch-Sekunden zum Zeitpunkt des Laufendes (via NTP). Fällt auf 0 zurück, + // wenn noch keine Zeit synchronisiert wurde — Frontend blendet die Uhrzeit + // in dem Fall aus. + newTime.timestamp = (unsigned long)time(nullptr); newTime.lane = lane; localTimes.push_back(newTime);