snapshot+hugo: speedtest tab with recent table + per-node aggregates

This commit is contained in:
BergaBruh
2026-05-09 19:01:24 +05:00
parent e6f1f63b8d
commit c01341c63a
5 changed files with 265 additions and 0 deletions
+71
View File
@@ -3,8 +3,19 @@ package storage
import (
"context"
"fmt"
"sort"
)
// Agg holds median aggregates for a single node over an arbitrary time
// window. Counts and medians are computed Go-side (sort + middle pick)
// because SQLite has no native percentile aggregate.
type Agg struct {
Count int64 `json:"count"`
MedianDl float64 `json:"median_dl"`
MedianUl float64 `json:"median_ul"`
MedianPing float64 `json:"median_ping"`
}
// SpeedTest is a single client-side speedtest result reported via the
// RecordSpeedTest RPC. Timestamp is unix seconds. Node is 'fl' or 'pl1'
// (DB-level CHECK). Country is ISO-3166-1 alpha-2 (validated at the
@@ -63,3 +74,63 @@ func (s *Store) DeleteSpeedTestsBefore(ctx context.Context, ts int64) (int64, er
}
return res.RowsAffected()
}
// AggregateSpeedTestsSince returns per-node median aggregates of dl/ul/ping
// for rows with timestamp >= since. The result is keyed by node ("fl"/"pl1").
// Reads only metric columns — no privacy-sensitive data (country, jitter
// not used here).
func (s *Store) AggregateSpeedTestsSince(ctx context.Context, since int64) (map[string]Agg, error) {
rows, err := s.db.QueryContext(ctx, `
SELECT node, dl_mbps, ul_mbps, ping_ms FROM speedtests
WHERE timestamp >= ? ORDER BY node`, since)
if err != nil {
return nil, err
}
defer rows.Close()
type bucket struct{ dl, ul, ping []float64 }
buckets := map[string]*bucket{}
for rows.Next() {
var node string
var dl, ul, ping float64
if err := rows.Scan(&node, &dl, &ul, &ping); err != nil {
return nil, err
}
b, ok := buckets[node]
if !ok {
b = &bucket{}
buckets[node] = b
}
b.dl = append(b.dl, dl)
b.ul = append(b.ul, ul)
b.ping = append(b.ping, ping)
}
if err := rows.Err(); err != nil {
return nil, err
}
out := map[string]Agg{}
for node, b := range buckets {
out[node] = Agg{
Count: int64(len(b.dl)),
MedianDl: median(b.dl),
MedianUl: median(b.ul),
MedianPing: median(b.ping),
}
}
return out, nil
}
// median sorts xs in place and returns the middle value (or mean of the
// two middle values if even-length). Returns 0 for empty input.
func median(xs []float64) float64 {
if len(xs) == 0 {
return 0
}
sort.Float64s(xs)
mid := len(xs) / 2
if len(xs)%2 == 0 {
return (xs[mid-1] + xs[mid]) / 2
}
return xs[mid]
}