Fixed /stats layout
This commit is contained in:
@@ -160,6 +160,10 @@ Image=localhost/kver:latest
|
||||
PublishPort=127.0.0.1:7777:8080
|
||||
Volume=kver-data:/app/data
|
||||
Volume=kver-media:/app/static/media
|
||||
# Optional: ASN-Auflösung der Impressions. Pfad zeigt ins kver-data-Volume,
|
||||
# daher zur Laufzeit setzen (nicht ins Image backen). Zeile weglassen, wenn
|
||||
# keine GeoLite2-ASN.mmdb vorhanden ist -> ASN-Auflösung bleibt still aus.
|
||||
Environment=KVER_GEOIP_ASN=/app/data/GeoLite2-ASN.mmdb
|
||||
# Healthcheck hier statt im Containerfile: Podman baut OCI-Images,
|
||||
# die kennen kein eingebettetes HEALTHCHECK.
|
||||
HealthCmd=wget -qO /dev/null http://localhost:8080/entry/feed/0
|
||||
|
||||
@@ -219,23 +219,6 @@ func handleStatsDetail(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// Beliebteste Seiten nach Impressions.
|
||||
topPages := []map[string]any{}
|
||||
if rows, err := db.Query(
|
||||
`SELECT path, SUM(hits) AS h
|
||||
FROM impression
|
||||
GROUP BY path
|
||||
ORDER BY h DESC
|
||||
LIMIT 10`); err == nil {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var path string
|
||||
var hits int64
|
||||
rows.Scan(&path, &hits)
|
||||
topPages = append(topPages, map[string]any{"path": path, "hits": hits})
|
||||
}
|
||||
}
|
||||
|
||||
// Netze nach Betreiber (autonomes System). Leere asn ('') -> "(unbekannt)".
|
||||
topASNs := []map[string]any{}
|
||||
if rows, err := db.Query(
|
||||
@@ -264,7 +247,6 @@ func handleStatsDetail(w http.ResponseWriter, r *http.Request) {
|
||||
"impressions_total": imprTotal,
|
||||
"visitors_total": visitorsTotal,
|
||||
"daily": daily,
|
||||
"top_pages": topPages,
|
||||
"top_asns": topASNs,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -345,6 +345,7 @@ hr {
|
||||
}
|
||||
.bar-value {
|
||||
flex: 0 0 auto;
|
||||
text-align: right;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
|
||||
+10
-4
@@ -34,6 +34,9 @@ function renderBars(container, items, formatLabel) {
|
||||
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";
|
||||
@@ -51,6 +54,7 @@ function renderBars(container, items, formatLabel) {
|
||||
|
||||
const val = document.createElement("span");
|
||||
val.className = "bar-value";
|
||||
val.style.width = `${valWidth}ch`;
|
||||
val.textContent = it.value;
|
||||
|
||||
row.append(label, track, val);
|
||||
@@ -86,16 +90,18 @@ async function render() {
|
||||
|
||||
// --- 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 })),
|
||||
(day) => day.slice(5), // "MM-DD" reicht auf der Achse
|
||||
dayAxis,
|
||||
);
|
||||
|
||||
// --- Beliebteste Seiten ---
|
||||
// --- Verschiedene Netze pro Tag ---
|
||||
renderBars(
|
||||
document.getElementById("stat-pages"),
|
||||
(s.top_pages ?? []).map((p) => ({ label: p.path, value: p.hits })),
|
||||
document.getElementById("stat-networks"),
|
||||
daily.map((d) => ({ label: d.day, value: d.visitors })),
|
||||
dayAxis,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -41,8 +41,8 @@
|
||||
<h3>Aufrufe pro Tag</h3>
|
||||
<div id="stat-daily" class="bars"></div>
|
||||
|
||||
<h3>Beliebteste Seiten</h3>
|
||||
<div id="stat-pages" class="bars"></div>
|
||||
<h3>Verschiedene Netze pro Tag</h3>
|
||||
<div id="stat-networks" class="bars"></div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user