Moderation entfernt, JSON-API unter /api
- Moderationsseite und Melde-Funktion (report) komplett ausgebaut; folgt als eigenständiges Projekt - alle API-Endpunkte unter /api, dort ausschließlich JSON: auch 404, 405, Rate-Limit (429) und Panics (500) - Fehler nur über HTTP-Status; stille DB-Fehler in stats, logout und Vote-Zählern liefern jetzt 500 statt Nullen - /auth/headerbar entfernt (Frontend nutzt /api/user/info) - Frontend auf /api und statusbasierte Auswertung umgestellt - notes/api.md neu als Referenz der aktuellen API 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
2714fdb1a5
commit
030a5bd943
@@ -223,53 +223,73 @@ func recordImpression(path, anonIP, asn string) {
|
||||
// handleStatsDetail liefert die Kennzahlen für die Statistik-Seite. 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) {
|
||||
func handleStatsDetail(w http.ResponseWriter, r *http.Request) error {
|
||||
var users, entries, toplevel, votesLeft, votesRight int64
|
||||
db.QueryRow(`SELECT COUNT(*) FROM account`).Scan(&users)
|
||||
db.QueryRow(`SELECT COUNT(*) FROM entry WHERE deleted = 0`).Scan(&entries)
|
||||
db.QueryRow(`SELECT COUNT(*) FROM entry WHERE deleted = 0 AND reply_to = 0`).Scan(&toplevel)
|
||||
db.QueryRow(`SELECT COUNT(*) FROM vote WHERE mode = 'left'`).Scan(&votesLeft)
|
||||
db.QueryRow(`SELECT COUNT(*) FROM vote WHERE mode = 'right'`).Scan(&votesRight)
|
||||
if err := db.QueryRow(
|
||||
`SELECT (SELECT COUNT(*) FROM account),
|
||||
(SELECT COUNT(*) FROM entry WHERE deleted = 0),
|
||||
(SELECT COUNT(*) FROM entry WHERE deleted = 0 AND reply_to = 0),
|
||||
(SELECT COUNT(*) FROM vote WHERE mode = 'left'),
|
||||
(SELECT COUNT(*) FROM vote WHERE mode = 'right')`).
|
||||
Scan(&users, &entries, &toplevel, &votesLeft, &votesRight); err != nil {
|
||||
return Internal(err)
|
||||
}
|
||||
|
||||
var imprTotal, visitorsTotal int64
|
||||
db.QueryRow(`SELECT COALESCE(SUM(hits), 0)::bigint, COUNT(DISTINCT anon_ip) FROM impression`).
|
||||
Scan(&imprTotal, &visitorsTotal)
|
||||
if err := db.QueryRow(`SELECT COALESCE(SUM(hits), 0)::bigint, COUNT(DISTINCT anon_ip) FROM impression`).
|
||||
Scan(&imprTotal, &visitorsTotal); err != nil {
|
||||
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{}
|
||||
if rows, err := db.Query(
|
||||
rows, err := db.Query(
|
||||
`SELECT day, SUM(hits)::bigint, COUNT(DISTINCT anon_ip)
|
||||
FROM impression
|
||||
GROUP BY day
|
||||
ORDER BY day DESC
|
||||
LIMIT 30`); err == nil {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var day string
|
||||
var hits, visitors int64
|
||||
rows.Scan(&day, &hits, &visitors)
|
||||
daily = append(daily, map[string]any{"day": day, "impressions": hits, "visitors": visitors})
|
||||
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{}
|
||||
if rows, err := db.Query(
|
||||
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`); err == nil {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var asn string
|
||||
var hits, networks int64
|
||||
rows.Scan(&asn, &hits, &networks)
|
||||
topASNs = append(topASNs, map[string]any{"asn": asn, "hits": hits, "networks": networks})
|
||||
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 {
|
||||
return Internal(err)
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
@@ -283,6 +303,7 @@ func handleStatsDetail(w http.ResponseWriter, r *http.Request) {
|
||||
"daily": daily,
|
||||
"top_asns": topASNs,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// serveIndex liefert die Startseite. Eigene Route (statt FileServer-Catch-All),
|
||||
|
||||
Reference in New Issue
Block a user