"use strict"; // Statistik-Seite: holt die Aggregate von /stats/detail und rendert Zahlen + // einfache CSS-Balken (keine Chart-Library, CSP-konform). Balkenbreiten werden // per CSSOM (el.style.width) gesetzt -> kein Inline-style-Attribut, daher von // der strikten CSP nicht betroffen. function plural(n, one, many) { return `${n} ${n === 1 ? one : many}`; } // numCard baut eine Kennzahl-Kachel: große Zahl + Beschriftung darunter. function numCard(value, label) { const cell = document.createElement("div"); const num = document.createElement("span"); num.className = "num"; num.textContent = value; const cap = document.createElement("span"); cap.className = "muted"; cap.textContent = label; cell.append(num, cap); return cell; } // renderBars zeichnet eine Liste {label, value} als horizontale Balken. Die // Breite ist relativ zum größten Wert. function renderBars(container, items, formatLabel) { container.textContent = ""; if (items.length === 0) { const p = document.createElement("p"); p.className = "muted"; p.textContent = "Noch keine Daten."; container.append(p); return; } const max = Math.max(...items.map((it) => it.value), 1); // Wertespalte für alle Zeilen gleich breit: an der längsten Zahl ausrichten // (in ch), damit die Tracks bündig sind und nicht je nach Stellenzahl wandern. const valWidth = Math.max(...items.map((it) => String(it.value).length)); for (const it of items) { const row = document.createElement("div"); row.className = "bar-row"; const label = document.createElement("span"); label.className = "bar-label"; label.textContent = formatLabel ? formatLabel(it.label) : it.label; const track = document.createElement("div"); track.className = "bar-track"; const fill = document.createElement("div"); fill.className = "bar-fill"; fill.style.width = `${(it.value / max) * 100}%`; track.append(fill); const val = document.createElement("span"); val.className = "bar-value"; val.style.width = `${valWidth}ch`; val.textContent = it.value; row.append(label, track, val); container.append(row); } } async function render() { const s = await api.get("/stats/detail"); // --- Auf einen Blick --- const overview = document.getElementById("stat-overview"); overview.append( numCard(s.users ?? 0, s.users === 1 ? "Profil" : "Profile"), numCard(s.entries ?? 0, "Beiträge"), numCard(s.toplevel ?? 0, "davon Threads"), numCard(s.replies ?? 0, "davon Antworten"), numCard(s.votes?.left ?? 0, "Stimmen Links"), numCard(s.votes?.right ?? 0, "Stimmen Rechts"), ); // --- Reichweite --- document.getElementById("stat-reach").append( numCard(s.impressions_total ?? 0, "Aufrufe gesamt"), numCard(s.visitors_total ?? 0, "Netze gesamt"), ); // --- Netze nach Betreiber (ASN) --- renderBars( document.getElementById("stat-asns"), (s.top_asns ?? []).map((a) => ({ label: a.asn, value: a.hits })), ); // --- Aufrufe pro Tag (Backend liefert absteigend -> für die Achse drehen) --- const daily = (s.daily ?? []).slice().reverse(); const dayAxis = (day) => day.slice(5); // "MM-DD" reicht auf der Achse renderBars( document.getElementById("stat-daily"), daily.map((d) => ({ label: d.day, value: d.impressions })), dayAxis, ); // --- Verschiedene Netze pro Tag --- renderBars( document.getElementById("stat-networks"), daily.map((d) => ({ label: d.day, value: d.visitors })), dayAxis, ); } render();