109 lines
3.5 KiB
JavaScript
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();
|