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 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
+2
-1
@@ -11,7 +11,8 @@
|
||||
<nav id="legal">
|
||||
<a href="/static/docs/impressum.html">Impressum</a> -
|
||||
<a href="/static/docs/datenschutz.html">Datenschutz</a> -
|
||||
<a href="/static/docs/beitragsordnung.html">Beitragsordnung</a>
|
||||
<a href="/static/docs/beitragsordnung.html">Beitragsordnung</a> -
|
||||
<a href="/stats.html">Statistik</a>
|
||||
</nav>
|
||||
<a href="/"><h1>Kontrollverlust</h1></a>
|
||||
<div id="headerbar"><a class="button" href="/">zurück zum Feed</a></div>
|
||||
|
||||
+2
-1
@@ -11,7 +11,8 @@
|
||||
<nav id="legal">
|
||||
<a href="/static/docs/impressum.html">Impressum</a> -
|
||||
<a href="/static/docs/datenschutz.html">Datenschutz</a> -
|
||||
<a href="/static/docs/beitragsordnung.html">Beitragsordnung</a>
|
||||
<a href="/static/docs/beitragsordnung.html">Beitragsordnung</a> -
|
||||
<a href="/stats.html">Statistik</a>
|
||||
</nav>
|
||||
<a href="/"><h1>Kontrollverlust</h1></a>
|
||||
<p id="stats" class="tagline"></p>
|
||||
|
||||
+102
@@ -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();
|
||||
@@ -0,0 +1,51 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Statistik – Kontrollverlust</title>
|
||||
<link rel="stylesheet" href="/css/app.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav id="legal">
|
||||
<a href="/static/docs/impressum.html">Impressum</a> -
|
||||
<a href="/static/docs/datenschutz.html">Datenschutz</a> -
|
||||
<a href="/static/docs/beitragsordnung.html">Beitragsordnung</a>
|
||||
</nav>
|
||||
<a href="/"><h1>Kontrollverlust</h1></a>
|
||||
<div id="headerbar"><a class="button" href="/">zurück zum Feed</a></div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section>
|
||||
<h2>Statistik</h2>
|
||||
|
||||
<h3>Auf einen Blick</h3>
|
||||
<div id="stat-overview" class="statgrid"></div>
|
||||
|
||||
<h3>Reichweite</h3>
|
||||
<p class="muted">
|
||||
Seitenaufrufe werden mit anonymisierten Netzen gezählt (IPv4 /16, IPv6 /32).
|
||||
„Netze" ist eine grobe Annäherung an Besucher, keine Personenzählung.
|
||||
</p>
|
||||
<div id="stat-reach" class="statgrid"></div>
|
||||
|
||||
<h3>Netze nach Betreiber</h3>
|
||||
<p class="muted">
|
||||
Aufrufe je autonomem System (Netzbetreiber), aufgelöst über GeoLite2-ASN.
|
||||
</p>
|
||||
<div id="stat-asns" class="bars"></div>
|
||||
|
||||
<h3>Aufrufe pro Tag</h3>
|
||||
<div id="stat-daily" class="bars"></div>
|
||||
|
||||
<h3>Beliebteste Seiten</h3>
|
||||
<div id="stat-pages" class="bars"></div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="/js/feed.js"></script>
|
||||
<script src="/js/stats.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+2
-1
@@ -11,7 +11,8 @@
|
||||
<nav id="legal">
|
||||
<a href="/static/docs/impressum.html">Impressum</a> -
|
||||
<a href="/static/docs/datenschutz.html">Datenschutz</a> -
|
||||
<a href="/static/docs/beitragsordnung.html">Beitragsordnung</a>
|
||||
<a href="/static/docs/beitragsordnung.html">Beitragsordnung</a> -
|
||||
<a href="/stats.html">Statistik</a>
|
||||
</nav>
|
||||
<a href="/"><h1>Kontrollverlust</h1></a>
|
||||
<div id="headerbar"><a class="button" href="/">zurück zum Feed</a></div>
|
||||
|
||||
Reference in New Issue
Block a user