package store import ( "context" "fmt" "time" ) // FleetSummary is the aggregated view that powers the dashboard's // summary tiles. All numbers are point-in-time snapshots; the // caller polls. type FleetSummary struct { TotalHosts int HostsOnline int HostsDegraded int HostsOffline int RepoBytesTotal int64 SnapshotsTotal int OpenAlerts int // Last-24h job rollup. JobsTotal includes every status; the // breakdown sums to it (modulo any in-flight queued/running). JobsLast24h int JobsLast24hSucceeded int JobsLast24hFailed int JobsLast24hCancelled int } // FleetSummary aggregates host + job stats in two queries. Cheap on // SQLite at Phase 1 scale (12 hosts, a few hundred jobs/day) — no // caching layer; the tile is regenerated on every dashboard render. func (s *Store) FleetSummary(ctx context.Context) (FleetSummary, error) { var fs FleetSummary row := s.db.QueryRowContext(ctx, ` SELECT COUNT(*), COALESCE(SUM(CASE WHEN status = 'online' THEN 1 ELSE 0 END), 0), COALESCE(SUM(CASE WHEN status = 'degraded' THEN 1 ELSE 0 END), 0), COALESCE(SUM(CASE WHEN status = 'offline' THEN 1 ELSE 0 END), 0), COALESCE(SUM(repo_size_bytes), 0), COALESCE(SUM(snapshot_count), 0), COALESCE(SUM(open_alert_count), 0) FROM hosts`) if err := row.Scan( &fs.TotalHosts, &fs.HostsOnline, &fs.HostsDegraded, &fs.HostsOffline, &fs.RepoBytesTotal, &fs.SnapshotsTotal, &fs.OpenAlerts, ); err != nil { return FleetSummary{}, fmt.Errorf("store: fleet summary hosts: %w", err) } cutoff := time.Now().Add(-24 * time.Hour).UTC().Format(time.RFC3339Nano) rows, err := s.db.QueryContext(ctx, `SELECT status, COUNT(*) FROM jobs WHERE created_at > ? GROUP BY status`, cutoff) if err != nil { return FleetSummary{}, fmt.Errorf("store: fleet summary jobs: %w", err) } defer func() { _ = rows.Close() }() for rows.Next() { var status string var n int if err := rows.Scan(&status, &n); err != nil { return FleetSummary{}, fmt.Errorf("store: fleet summary scan: %w", err) } fs.JobsLast24h += n switch status { case "succeeded": fs.JobsLast24hSucceeded = n case "failed": fs.JobsLast24hFailed = n case "cancelled": //nolint:misspell // matches the DB CHECK constraint and api.JobCancelled wire value fs.JobsLast24hCancelled = n } } return fs, rows.Err() }