// Package stats computes dashboard/API statistics from request logs and // the live runtime snapshot. package stats import ( "database/sql" "time" "github.com/owen/vibeproxy/internal/database" "github.com/owen/vibeproxy/internal/models" "github.com/owen/vibeproxy/internal/runtime" ) type Dashboard struct { ActiveProxyHosts int `json:"active_proxy_hosts"` ActiveTCPStreams int `json:"active_tcp_streams"` ActiveUDPStreams int `json:"active_udp_streams"` RequestsLastHour int `json:"requests_last_hour"` RequestsPerSecond float64 `json:"requests_per_second"` TotalRequestsToday int `json:"total_requests_today"` StatusBreakdown []database.StatusCount `json:"status_breakdown"` BackendHealth []BackendHealthEntry `json:"backend_health"` CertsExpiringSoon []models.Certificate `json:"certs_expiring_soon"` TopHosts []database.HostCount `json:"top_hosts"` TopClients []database.HostCount `json:"top_clients"` RecentErrors []models.RequestLog `json:"recent_errors"` } type BackendHealthEntry struct { ProxyHost string `json:"proxy_host"` Backend string `json:"backend"` Healthy bool `json:"healthy"` } func Compute(db *sql.DB, snap *runtime.Snapshot) (*Dashboard, error) { d := &Dashboard{} seen := map[int64]bool{} for _, rt := range snap.HostsByDomain { if seen[rt.Model.ID] { continue } seen[rt.Model.ID] = true if rt.Model.Type == models.ProxyHostTypeProxy { d.ActiveProxyHosts++ } if rt.Pool != nil { for _, b := range rt.Pool.Backends { d.BackendHealth = append(d.BackendHealth, BackendHealthEntry{ ProxyHost: rt.Model.Name, Backend: b.Model.Host, Healthy: b.Healthy(), }) } } } streams, err := database.ListStreams(db) if err != nil { return nil, err } for _, s := range streams { if !s.Enabled { continue } if s.Protocol == models.StreamTCP { d.ActiveTCPStreams++ } else { d.ActiveUDPStreams++ } } requestsLastHour, err := database.CountRequestLogsSince(db, "-1 hours") if err != nil { return nil, err } d.RequestsLastHour = requestsLastHour d.RequestsPerSecond = float64(requestsLastHour) / 3600.0 totalToday, err := database.CountRequestLogsSince(db, "-24 hours") if err != nil { return nil, err } d.TotalRequestsToday = totalToday statuses, err := database.StatusBreakdownSince(db, "-24 hours") if err != nil { return nil, err } d.StatusBreakdown = statuses topHosts, err := database.TopHostsSince(db, "-24 hours", 10) if err != nil { return nil, err } d.TopHosts = topHosts topClients, err := database.TopClientsSince(db, "-24 hours", 10) if err != nil { return nil, err } d.TopClients = topClients recentErrors, err := database.RecentErrorsSince(db, "-24 hours", 20) if err != nil { return nil, err } d.RecentErrors = recentErrors certs, err := database.ListCertificates(db) if err != nil { return nil, err } for _, c := range certs { if c.NotAfter == nil { continue } days := int(c.NotAfter.Sub(time.Now()).Hours() / 24) if days <= 30 { d.CertsExpiringSoon = append(d.CertsExpiringSoon, c) } } return d, nil }