Archaeo's picture
Upload 38 files
15d848a verified
(() => {
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();
})();