P8(1): restore homepage overview curves on a memory-only sampling ring

Backend: new overviewMetrics store — a single-tier (2s x 360 = 12min)
in-memory ring fed by ServerService.RefreshStatus for exactly the eight
overview series (cpu/mem/swap/diskUsage/netUp/netDown/tcpCount/udpCount).
No persistence, no gob snapshot, no system-history page: the only surface
is GET /panel/api/server/history/:metric/:bucket restricted to those
keys, which seeds the frontend curves. ~46KB bounded by construction;
the ring starts empty on every panel start.

Frontend: restore useOverviewHistory + the VitalTile/ThroughputCard/
ConnectionsCard sparklines and the IndexPage wiring exactly as they
were before P7 removed them along with the history module.
This commit is contained in:
Muse
2026-10-21 13:49:43 +00:00
parent 60ce5eab98
commit 261a1f6787
9 changed files with 483 additions and 26 deletions
+41 -3
View File
@@ -177,8 +177,8 @@ var allowedHistoryBuckets = map[int]bool{
}
// IsAllowedHistoryBucket reports whether a bucket-seconds value is in the
// whitelist used by /server/history, /server/cpuHistory, /server/xrayMetricsHistory,
// /server/xrayObservatoryHistory, and /nodes/history.
// whitelist used by the time-series aggregation endpoints
// (/server/history for the homepage overview, /nodes/history).
func IsAllowedHistoryBucket(bucketSeconds int) bool {
return allowedHistoryBuckets[bucketSeconds]
}
@@ -242,7 +242,7 @@ func (s *ServerService) isFail2banInstalled() bool {
// Returns the new snapshot (may be nil if collection failed). Called by the
// background ticker; the caller is responsible for any side effects
// (websocket broadcast). The realtime status served to the UI comes from
// this snapshot — the removed system-history series used to be fed here too.
// this snapshot; the homepage overview ring is fed here too.
func (s *ServerService) RefreshStatus() *Status {
next := s.GetStatus(s.LastStatus())
if next == nil {
@@ -251,9 +251,47 @@ func (s *ServerService) RefreshStatus() *Status {
s.lastStatusMu.Lock()
s.lastStatus = next
s.lastStatusMu.Unlock()
s.AppendOverviewSample(time.Now(), next)
return next
}
// AppendOverviewSample writes one tick of the homepage overview metrics —
// CPU, memory/swap/disk percents, network throughput (bytes/s) and TCP/UDP
// connection counts — into the memory-only overview ring. Called by
// RefreshStatus on the @2s status cadence, so all series stay aligned.
// Only the overview keys are kept; this is not the removed system-history
// sampler (no extra series, no persistence).
func (s *ServerService) AppendOverviewSample(t time.Time, status *Status) {
if status == nil {
return
}
overviewMetrics.append("cpu", t, status.Cpu)
if status.Mem.Total > 0 {
overviewMetrics.append("mem", t, float64(status.Mem.Current)*100.0/float64(status.Mem.Total))
}
if status.Swap.Total > 0 {
overviewMetrics.append("swap", t, float64(status.Swap.Current)*100.0/float64(status.Swap.Total))
} else {
overviewMetrics.append("swap", t, 0)
}
overviewMetrics.append("netUp", t, float64(status.NetIO.Up))
overviewMetrics.append("netDown", t, float64(status.NetIO.Down))
if status.Disk.Total > 0 {
overviewMetrics.append("diskUsage", t, float64(status.Disk.Current)*100.0/float64(status.Disk.Total))
}
overviewMetrics.append("tcpCount", t, float64(status.TcpCount))
overviewMetrics.append("udpCount", t, float64(status.UdpCount))
}
// AggregateOverviewMetric returns up to maxPoints averaged buckets of size
// bucketSeconds for one overview metric (see OverviewMetricKeys). Output
// points have keys {"t": unixSec, "v": value}. It backs the seed fetch of
// the homepage overview curves; the ring behind it is memory-only, so
// shortly after a panel restart it legitimately returns few or no points.
func (s *ServerService) AggregateOverviewMetric(metric string, bucketSeconds int, maxPoints int) []map[string]any {
return overviewMetrics.aggregate(metric, bucketSeconds, maxPoints)
}
func getPublicIP(url string) string {
client := &http.Client{
Timeout: 3 * time.Second,