From 8be8ecedc97468c347b4c325fef43c1ebe9f6921 Mon Sep 17 00:00:00 2001 From: irrlicht Date: Wed, 17 Jun 2026 23:21:17 +0200 Subject: [PATCH] Statistik-Seite: Kennzahlen, Reichweite, ASN und Verlaeufe Neue Seite /stats.html: Basiszahlen, Reichweite, Netze nach Betreiber (ASN), Aufrufe pro Tag und beliebteste Seiten. Diagramme als CSS-Balken (Breite per CSSOM, kein Inline-style) statt Chart-Library -- bleibt CSP-konform. Verlinkt in der Legal-Nav aller Seiten. Co-Authored-By: Claude Opus 4.8 --- web/css/app.css | 54 +++++++++++++++++++++++++ web/entry.html | 3 +- web/index.html | 3 +- web/js/stats.js | 102 ++++++++++++++++++++++++++++++++++++++++++++++++ web/stats.html | 51 ++++++++++++++++++++++++ web/user.html | 3 +- 6 files changed, 213 insertions(+), 3 deletions(-) create mode 100644 web/js/stats.js create mode 100644 web/stats.html diff --git a/web/css/app.css b/web/css/app.css index e510f0e..d46aef6 100644 --- a/web/css/app.css +++ b/web/css/app.css @@ -299,3 +299,57 @@ hr { border: none; border-top: 1px solid #ccc; } + +/* --- Statistik-Seite --- */ +.statgrid { + display: flex; + flex-wrap: wrap; + gap: 0.6rem 1.8rem; + padding: 0.3rem 0.5rem 0.6rem; +} +.statgrid > div { + display: flex; + flex-direction: column; +} +.statgrid .num { + font-size: 1.8rem; + line-height: 1.1; +} + +/* Horizontale Balken (Aufrufe/Tag, beliebteste Seiten). Breite wird per JS + gesetzt (CSSOM, kein Inline-style -> CSP-konform). */ +.bars { + padding: 0.3rem 0.5rem 0.6rem; +} +.bar-row { + display: flex; + align-items: center; + gap: 0.5rem; + margin: 0.15rem 0; +} +.bar-label { + flex: 0 0 7rem; + text-align: right; + font-size: 0.95rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.bar-track { + flex: 1; + background: #eee; +} +.bar-fill { + height: 1.1rem; + background: gray; +} +.bar-value { + flex: 0 0 auto; + font-size: 0.95rem; +} + +/* ASN-Namen sind länger als Datum/Pfad -> mehr Platz, Umbruch statt Ellipse. */ +#stat-asns .bar-label { + flex-basis: 13rem; + white-space: normal; +} diff --git a/web/entry.html b/web/entry.html index e810689..3bf939d 100644 --- a/web/entry.html +++ b/web/entry.html @@ -11,7 +11,8 @@

Kontrollverlust

diff --git a/web/index.html b/web/index.html index b429d4e..44853bf 100644 --- a/web/index.html +++ b/web/index.html @@ -11,7 +11,8 @@

Kontrollverlust

diff --git a/web/js/stats.js b/web/js/stats.js new file mode 100644 index 0000000..a887592 --- /dev/null +++ b/web/js/stats.js @@ -0,0 +1,102 @@ +"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); + 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.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(); + renderBars( + document.getElementById("stat-daily"), + daily.map((d) => ({ label: d.day, value: d.impressions })), + (day) => day.slice(5), // "MM-DD" reicht auf der Achse + ); + + // --- Beliebteste Seiten --- + renderBars( + document.getElementById("stat-pages"), + (s.top_pages ?? []).map((p) => ({ label: p.path, value: p.hits })), + ); +} + +render(); diff --git a/web/stats.html b/web/stats.html new file mode 100644 index 0000000..95516df --- /dev/null +++ b/web/stats.html @@ -0,0 +1,51 @@ + + + + + + Statistik – Kontrollverlust + + + +
+ +

Kontrollverlust

+ +
+ +
+
+

Statistik

+ +

Auf einen Blick

+
+ +

Reichweite

+

+ Seitenaufrufe werden mit anonymisierten Netzen gezählt (IPv4 /16, IPv6 /32). + „Netze" ist eine grobe Annäherung an Besucher, keine Personenzählung. +

+
+ +

Netze nach Betreiber

+

+ Aufrufe je autonomem System (Netzbetreiber), aufgelöst über GeoLite2-ASN. +

+
+ +

Aufrufe pro Tag

+
+ +

Beliebteste Seiten

+
+
+
+ + + + + diff --git a/web/user.html b/web/user.html index 8d6d4c0..0a1ce19 100644 --- a/web/user.html +++ b/web/user.html @@ -11,7 +11,8 @@

Kontrollverlust