(() => { const script = document.currentScript; if (!script) { return; } const apiLatest = script.dataset.apiLatest; const refreshSeconds = Number.parseInt(script.dataset.refresh ?? "120", 10) || 120; const demoFlag = script.dataset.demo === "true"; const timezone = document.body?.dataset?.timezone || "Europe/Berlin"; const levelValue = document.getElementById("levelValue"); const warningBadge = document.getElementById("warningBadge"); const trendBadge = document.getElementById("trendBadge"); const trendSymbol = document.getElementById("trendSymbol"); const updatedTime = document.getElementById("updatedTime"); const relativeAge = document.getElementById("relativeAge"); const countdown = document.getElementById("countdown"); const refreshButton = document.getElementById("refreshButton"); const autoToggle = document.getElementById("autoRefreshToggle"); const sparklinePath = document.getElementById("sparklinePath"); const errorBanner = document.getElementById("errorBanner"); const demoBadge = document.getElementById("demoBadge"); const initialDataEl = document.getElementById("initial-data"); let initialPayload = { latest: null, history: [], autoRefresh: refreshSeconds, demo: demoFlag }; if (initialDataEl?.textContent) { try { initialPayload = JSON.parse(initialDataEl.textContent); } catch (err) { console.warn("Failed to parse initial payload", err); } } const state = { autoRefresh: true, secondsRemaining: refreshSeconds, lastMeasurement: initialPayload.latest ?? null, history: initialPayload.history ?? [], demoMode: demoFlag || Boolean(initialPayload.demo), timezone, }; const dateFormatter = new Intl.DateTimeFormat("de-DE", { timeZone: timezone, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", }); function showError(message) { if (!errorBanner) return; errorBanner.textContent = message; errorBanner.hidden = false; } function clearError() { if (!errorBanner) return; errorBanner.textContent = ""; errorBanner.hidden = true; } function warningFor(level) { if (level < 400) { return { label: "Normal", cls: "badge-normal" }; } if (level < 600) { return { label: "Aufmerksamkeit", cls: "badge-attention" }; } if (level < 800) { return { label: "Warnung", cls: "badge-warning" }; } return { label: "Alarm", cls: "badge-alarm" }; } function trendInfo(trendValue) { switch (trendValue) { case 1: return { label: "steigend", symbol: "↑", cls: "badge-rising" }; case -1: return { label: "fallend", symbol: "↓", cls: "badge-falling" }; default: return { label: "gleichbleibend", symbol: "→", cls: "badge-stable" }; } } function computeSparkline(history) { if (!history || history.length === 0) { return ""; } const levels = history.map((entry) => Number(entry.level_cm ?? entry.levelCm)).filter((v) => Number.isFinite(v)); if (levels.length === 0) { return ""; } const min = Math.min(...levels); const max = Math.max(...levels); const span = Math.max(max - min, 1); const step = 100 / Math.max(levels.length - 1, 1); const parts = levels.map((level, index) => { const x = index * step; const y = 40 - ((level - min) / span) * 40; return `${x.toFixed(2)},${y.toFixed(2)}`; }); return `M ${parts.join(" L ")}`; } function updateDemoBadge(isDemo) { if (!demoBadge) return; if (isDemo) { demoBadge.hidden = false; document.body?.setAttribute("data-demo", "true"); } else { demoBadge.hidden = true; document.body?.setAttribute("data-demo", "false"); } } function updateUI(measurement, history) { if (!measurement) { showError("Keine Messdaten verfügbar"); return; } state.lastMeasurement = measurement; state.history = history ?? state.history; clearError(); if (levelValue) { levelValue.textContent = measurement.level_cm ?? measurement.levelCm; } const warning = warningFor(measurement.level_cm ?? measurement.levelCm); if (warningBadge) { warningBadge.className = `badge ${warning.cls}`; warningBadge.textContent = warning.label; } const trend = trendInfo(measurement.trend); if (trendBadge) { trendBadge.className = `badge trend-badge ${trend.cls}`; trendBadge.setAttribute("aria-label", `Trend ${trend.label}`); } if (trendSymbol) { trendSymbol.textContent = trend.symbol; } if (updatedTime) { updatedTime.dataset.timestamp = measurement.timestamp; const date = new Date(measurement.timestamp); updatedTime.textContent = `Zuletzt aktualisiert: ${dateFormatter.format(date)} Uhr`; } updateRelativeAge(); updateDemoBadge(Boolean(measurement.is_demo ?? measurement.isDemo)); if (sparklinePath) { sparklinePath.setAttribute("d", computeSparkline(state.history.slice(-24))); } } function updateRelativeAge() { if (!relativeAge || !state.lastMeasurement?.timestamp) { return; } const lastDate = new Date(state.lastMeasurement.timestamp); const diffSeconds = Math.max(0, Math.floor((Date.now() - lastDate.getTime()) / 1000)); if (diffSeconds < 5) { relativeAge.textContent = "vor wenigen Sekunden"; return; } if (diffSeconds < 60) { relativeAge.textContent = `vor ${diffSeconds} Sekunden`; return; } const minutes = Math.floor(diffSeconds / 60); const seconds = diffSeconds % 60; relativeAge.textContent = `vor ${minutes} Minute${minutes === 1 ? "" : "n"} und ${seconds} Sekunde${seconds === 1 ? "" : "n"}`; } function updateCountdown() { if (!countdown) return; if (!state.autoRefresh) { countdown.textContent = "Auto-Refresh deaktiviert"; return; } const minutes = Math.floor(state.secondsRemaining / 60); const seconds = state.secondsRemaining % 60; countdown.textContent = `Nächste Aktualisierung in ${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`; } async function refreshNow() { if (!apiLatest) return; if (refreshButton) { refreshButton.disabled = true; } try { const url = new URL(apiLatest, window.location.origin); if (state.demoMode) { url.searchParams.set("demo", "1"); } const response = await fetch(url.toString(), { headers: { Accept: "application/json" } }); if (!response.ok) { throw new Error(`Serverfehler ${response.status}`); } const payload = await response.json(); if (payload.error) { showError(payload.error); } else { clearError(); } state.demoMode = Boolean(payload.demo_mode ?? payload.demoMode ?? state.demoMode); updateUI(payload.measurement, payload.history); state.secondsRemaining = refreshSeconds; updateCountdown(); } catch (error) { console.error("Aktualisierung fehlgeschlagen", error); showError("Aktualisierung fehlgeschlagen. Bitte später erneut versuchen."); } finally { if (refreshButton) { refreshButton.disabled = false; } } } if (autoToggle) { autoToggle.addEventListener("change", (event) => { state.autoRefresh = event.target.checked; autoToggle.setAttribute("aria-checked", state.autoRefresh ? "true" : "false"); if (state.autoRefresh) { state.secondsRemaining = refreshSeconds; } updateCountdown(); }); } if (refreshButton) { refreshButton.addEventListener("click", () => { refreshNow(); }); } setInterval(() => { if (!state.autoRefresh) { return; } state.secondsRemaining -= 1; if (state.secondsRemaining <= 0) { state.secondsRemaining = refreshSeconds; refreshNow(); } updateCountdown(); }, 1000); setInterval(updateRelativeAge, 1000); updateUI(state.lastMeasurement, state.history); updateCountdown(); })();