Statistik: tägliche SVG-Diagramme statt CSS-Balken
Der Server rendert einmal täglich (0:01 Uhr deutscher Zeit) Liniendiagramme für Aufrufe, Netze und Beiträge pro Tag sowie gestapelte Säulen (Threads/ Antworten) für den Ø pro Wochentag und pro Tageszeit, je schmal (600) und breit (1200), ausgeliefert über /api/stats/charts. Hovern zeigt den Datenpunkt in einer Infozeile und markiert ihn mit einer senkrechten Linie. Die Statistik-Seite wird auf breiten Bildschirmen bis 1200 px breit. "Netze nach Betreiber" entfällt; neu ist die Kennzahl Ø Beiträge/Tag der letzten 30 Tage. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TiXsPUqw7oeomZ8wZrQW5q
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
047ad392d9
commit
07a2b64eff
@@ -236,7 +236,8 @@ func handleStats(w http.ResponseWriter, r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleStatsDetail liefert die Kennzahlen für die Statistik-Seite. Anders als
|
||||
// handleStatsDetail liefert die Kennzahlen für die Statistik-Seite (die
|
||||
// Diagramme kommen getrennt aus handleStatsCharts, charts.go). Anders als
|
||||
// /stats (Tagline, bei jedem Aufruf) darf das hier ruhig mehrere Aggregate
|
||||
// rechnen. Öffentlich, kein Auth.
|
||||
func handleStatsDetail(w http.ResponseWriter, r *http.Request) error {
|
||||
@@ -257,67 +258,25 @@ func handleStatsDetail(w http.ResponseWriter, r *http.Request) error {
|
||||
return Internal(err)
|
||||
}
|
||||
|
||||
// Tägliche Impressions + grobe Unique-Netze, jüngste 30 Tage zuerst. Das
|
||||
// Frontend dreht für die Achse auf aufsteigend.
|
||||
daily := []map[string]any{}
|
||||
rows, err := db.Query(
|
||||
`SELECT day, SUM(hits)::bigint, COUNT(DISTINCT anon_ip)
|
||||
FROM impression
|
||||
GROUP BY day
|
||||
ORDER BY day DESC
|
||||
LIMIT 30`)
|
||||
if err != nil {
|
||||
return Internal(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var day string
|
||||
var hits, visitors int64
|
||||
if err := rows.Scan(&day, &hits, &visitors); err != nil {
|
||||
return Internal(err)
|
||||
}
|
||||
daily = append(daily, map[string]any{"day": day, "impressions": hits, "visitors": visitors})
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return Internal(err)
|
||||
}
|
||||
|
||||
// Netze nach Betreiber (autonomes System). Leere asn ('') -> "(unbekannt)".
|
||||
topASNs := []map[string]any{}
|
||||
rows, err = db.Query(
|
||||
`SELECT CASE WHEN asn = '' THEN '(unbekannt)' ELSE asn END AS a,
|
||||
SUM(hits)::bigint AS h,
|
||||
COUNT(DISTINCT anon_ip)
|
||||
FROM impression
|
||||
GROUP BY a
|
||||
ORDER BY h DESC
|
||||
LIMIT 10`)
|
||||
if err != nil {
|
||||
return Internal(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var asn string
|
||||
var hits, networks int64
|
||||
if err := rows.Scan(&asn, &hits, &networks); err != nil {
|
||||
return Internal(err)
|
||||
}
|
||||
topASNs = append(topASNs, map[string]any{"asn": asn, "hits": hits, "networks": networks})
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
// Laufender Durchschnitt: Beiträge (wie in den Diagrammen einschließlich
|
||||
// gelöschter) der letzten 30 vollen Tage deutscher Zeit, ohne heute.
|
||||
now := time.Now().In(berlin)
|
||||
midnight := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, berlin)
|
||||
var recent int64
|
||||
if err := db.QueryRow(`SELECT COUNT(*) FROM entry WHERE created_at >= $1 AND created_at < $2`,
|
||||
midnight.AddDate(0, 0, -30).Unix(), midnight.Unix()).Scan(&recent); err != nil {
|
||||
return Internal(err)
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"users": users,
|
||||
"entries": entries,
|
||||
"toplevel": toplevel,
|
||||
"replies": entries - toplevel,
|
||||
"votes": map[string]any{"yes": votesYes, "no": votesNo},
|
||||
"impressions_total": imprTotal,
|
||||
"visitors_total": visitorsTotal,
|
||||
"daily": daily,
|
||||
"top_asns": topASNs,
|
||||
"users": users,
|
||||
"entries": entries,
|
||||
"toplevel": toplevel,
|
||||
"replies": entries - toplevel,
|
||||
"votes": map[string]any{"yes": votesYes, "no": votesNo},
|
||||
"impressions_total": imprTotal,
|
||||
"visitors_total": visitorsTotal,
|
||||
"entries_per_day_30": float64(recent) / 30,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user