Files
HexaWetter/frontend/admin.js
2026-06-18 23:59:08 +02:00

249 lines
8.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const TOKEN_KEY = "hexawetter_admin_token";
const el = (id) => document.getElementById(id);
function getToken() {
return sessionStorage.getItem(TOKEN_KEY);
}
function setToken(token) {
if (token) sessionStorage.setItem(TOKEN_KEY, token);
else sessionStorage.removeItem(TOKEN_KEY);
}
async function api(path, options = {}) {
const headers = { ...(options.headers || {}) };
const token = getToken();
if (token) headers.Authorization = `Bearer ${token}`;
const res = await fetch(path, { ...options, headers });
if (res.status === 401) {
setToken(null);
showLogin();
throw new Error("Sitzung abgelaufen");
}
if (!res.ok) {
const text = await res.text();
throw new Error(`${res.status}: ${text}`);
}
return res.json();
}
function formatBytes(bytes) {
if (!bytes) return "0 B";
const units = ["B", "KB", "MB", "GB"];
let v = bytes, u = 0;
while (v >= 1024 && u < units.length - 1) { v /= 1024; u++; }
return `${v.toFixed(u === 0 ? 0 : 1)} ${units[u]}`;
}
async function fetchBlobUrl(path) {
const token = getToken();
const res = await fetch(path, { headers: token ? { Authorization: `Bearer ${token}` } : {} });
if (!res.ok) throw new Error(`${res.status}`);
const blob = await res.blob();
return URL.createObjectURL(blob);
}
async function downloadAdminFile(path, filename) {
const url = await fetchBlobUrl(path);
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
async function loadRadarData() {
try {
const local = await api("/api/admin/radar/files");
el("radarFiles").innerHTML = local.files.length ? local.files.slice().reverse().map((file) => `
<tr>
<td>${file.name}</td>
<td>${formatBytes(file.size_bytes)}</td>
<td>${new Date(file.modified_utc).toLocaleString("de-DE")}</td>
<td><button type="button" class="btn btn-secondary" data-download="${file.download_url}" data-name="${file.name}">Download</button></td>
</tr>
`).join("") : `<tr><td colspan="4" class="muted">Noch keine lokalen Radar-Dateien.</td></tr>`;
el("radarFiles").querySelectorAll("[data-download]").forEach((btn) => {
btn.addEventListener("click", () => downloadAdminFile(btn.dataset.download, btn.dataset.name));
});
} catch {
el("radarFiles").innerHTML = `<tr><td colspan="4" class="muted">Rohdaten nicht verfügbar</td></tr>`;
}
try {
const rendered = await api("/api/admin/radar/rendered?limit=12");
el("renderedCount").textContent = `${rendered.count} Frame(s)`;
const frames = rendered.frames || [];
if (!frames.length) {
el("renderedFrames").innerHTML = `<p class="muted">Noch keine gerenderten Frames.</p>`;
return;
}
el("renderedFrames").innerHTML = frames.map((f, i) => `
<figure class="rendered-frame" id="renderedFrame${i}">
<div class="muted" style="padding:40px;text-align:center">Lade…</div>
<figcaption>${f.filename}</figcaption>
</figure>
`).join("");
await Promise.allSettled(frames.map(async (f, i) => {
const url = await fetchBlobUrl(f.image_url);
const figure = el(`renderedFrame${i}`);
if (!figure) return;
figure.innerHTML = `<img src="${url}" alt="Radar ${f.filename}" loading="lazy" /><figcaption>${f.filename}</figcaption>`;
}));
} catch {
el("renderedCount").textContent = "";
el("renderedFrames").innerHTML = `<p class="muted">Renderer nicht verfügbar.</p>`;
}
}
function showLogin() {
el("loginSection").classList.remove("admin-hidden");
el("dashboardSection").classList.add("admin-hidden");
el("logoutBtn").classList.add("admin-hidden");
}
function showDashboard() {
el("loginSection").classList.add("admin-hidden");
el("dashboardSection").classList.remove("admin-hidden");
el("logoutBtn").classList.remove("admin-hidden");
}
function renderServices(services) {
el("serviceGrid").innerHTML = (services || []).map((s) => `
<div class="source-status ${s.status}">
<strong>${s.name}</strong>
<span>${s.status}${s.latency_ms ? ` · ${s.latency_ms} ms` : ""}</span>
${s.http_status ? `<small>HTTP ${s.http_status}</small>` : ""}
${s.error ? `<small>${s.error}</small>` : ""}
</div>
`).join("");
}
function renderStorage(storage) {
el("storageGrid").innerHTML = Object.entries(storage || {}).map(([key, info]) => `
<div class="admin-stat">
<strong>${formatBytes(info.bytes || 0)}</strong>
<span>${key}: ${info.files || 0} Dateien</span>
</div>
`).join("");
}
function renderInfra(infra) {
el("infraGrid").innerHTML = Object.entries(infra || {}).map(([key, status]) => `
<div class="admin-stat">
<strong>${status}</strong>
<span>${key}</span>
</div>
`).join("");
}
async function loadDashboard() {
const data = await api("/api/admin/dashboard");
el("adminTime").textContent = `Stand: ${new Date(data.time_utc).toLocaleString("de-DE")}`;
renderInfra(data.infrastructure);
renderServices(data.services);
renderStorage(data.storage);
el("configBlock").textContent = JSON.stringify(data.config, null, 2);
const settings = await api("/api/admin/settings");
const defaults = settings.defaults;
const overrides = settings.runtime_overrides || {};
el("setRateLimit").value = overrides.rate_limit_per_minute ?? defaults.rate_limit_per_minute;
el("setCacheForecast").value = overrides.cache_ttl_forecast ?? defaults.cache_ttl.forecast;
el("setCacheObs").value = overrides.cache_ttl_observations ?? defaults.cache_ttl.observations;
el("setCacheWarn").value = overrides.cache_ttl_warnings ?? defaults.cache_ttl.warnings;
el("setRadarRetention").value = overrides.radar_retention_hours ?? defaults.radar_retention_hours;
await loadRadarData();
}
el("loginForm").addEventListener("submit", async (e) => {
e.preventDefault();
el("loginError").textContent = "";
try {
const data = await api("/api/admin/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: el("adminUser").value, password: el("adminPass").value }),
});
setToken(data.access_token);
showDashboard();
await loadDashboard();
} catch (err) {
el("loginError").textContent = err.message.includes("401") ? "Ungültige Zugangsdaten" : err.message;
}
});
el("logoutBtn").addEventListener("click", () => {
setToken(null);
showLogin();
});
el("refreshBtn").addEventListener("click", () => loadDashboard().catch((e) => alert(e.message)));
el("clearCacheBtn").addEventListener("click", async () => {
if (!confirm("Redis-Cache wirklich leeren?")) return;
await api("/api/admin/cache/clear", { method: "POST" });
alert("Cache geleert");
await loadDashboard();
});
el("syncRadarBtn").addEventListener("click", async () => {
const button = el("syncRadarBtn");
button.disabled = true;
const label = button.textContent;
button.textContent = "Sync läuft…";
try {
const result = await api("/api/admin/radar/sync?limit=6", { method: "POST" });
alert(`Sync abgeschlossen: ${result.downloaded?.length || 0} neu, ${result.skipped?.length || 0} übersprungen`);
await loadRadarData();
} catch (err) {
alert(err.message);
} finally {
button.disabled = false;
button.textContent = label;
}
});
el("settingsForm").addEventListener("submit", async (e) => {
e.preventDefault();
const payload = {
rate_limit_per_minute: Number(el("setRateLimit").value),
cache_ttl_forecast: Number(el("setCacheForecast").value),
cache_ttl_observations: Number(el("setCacheObs").value),
cache_ttl_warnings: Number(el("setCacheWarn").value),
radar_retention_hours: Number(el("setRadarRetention").value),
};
try {
await api("/api/admin/settings", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
el("settingsMsg").textContent = "Einstellungen in Redis gespeichert.";
} catch (err) {
el("settingsMsg").textContent = err.message;
}
});
async function init() {
try {
const status = await fetch("/api/admin/status").then((r) => r.json());
if (!status.admin_enabled) {
el("loginError").textContent = "Admin ist nicht konfiguriert. Bitte ADMIN_PASSWORD_HASH und ADMIN_JWT_SECRET in .env setzen.";
return;
}
} catch { /* ignore */ }
if (getToken()) {
try {
showDashboard();
await loadDashboard();
return;
} catch {
setToken(null);
}
}
showLogin();
}
init();