Files
2026-06-19 22:49:01 +02:00

109 lines
3.5 KiB
JavaScript

"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();