package httpapi import ( "context" "time" "github.com/dtoro/oikos/internal/httpapi/gen" ) // GetDashboardSummary returns one round-trip overview for the control room // home page: entity counts, health rollup, open signals, pending approvals, // executions in the last 24h, and an event-rate sparkline. func (s *Server) GetDashboardSummary(ctx context.Context, req gen.GetDashboardSummaryRequestObject) (gen.GetDashboardSummaryResponseObject, error) { resp := gen.DashboardSummary{ EntitiesByType: map[string]int{}, EntitiesByState: map[string]int{}, SignalsBySeverity: map[string]int{}, ExecutionsByState: map[string]int{}, } rows, err := s.pool.Query(ctx, `SELECT type, count(*) FROM entities GROUP BY type`) if err != nil { return nil, err } for rows.Next() { var typ string var n int if err := rows.Scan(&typ, &n); err != nil { rows.Close() return nil, err } resp.EntitiesByType[typ] = n } rows.Close() if err := rows.Err(); err != nil { return nil, err } rows, err = s.pool.Query(ctx, `SELECT coalesce(state, 'unknown'), count(*) FROM entities GROUP BY state`) if err != nil { return nil, err } for rows.Next() { var state string var n int if err := rows.Scan(&state, &n); err != nil { rows.Close() return nil, err } resp.EntitiesByState[state] = n } rows.Close() if err := rows.Err(); err != nil { return nil, err } rows, err = s.pool.Query(ctx, `SELECT health, count(*) FROM entity_status GROUP BY health`) if err != nil { return nil, err } for rows.Next() { var health string var n int if err := rows.Scan(&health, &n); err != nil { rows.Close() return nil, err } switch health { case "healthy": resp.Health.Healthy = n case "degraded": resp.Health.Degraded = n case "down": resp.Health.Down = n default: resp.Health.Unknown = n } } rows.Close() if err := rows.Err(); err != nil { return nil, err } rows, err = s.pool.Query(ctx, ` SELECT severity, count(*) FROM signals WHERE state NOT IN ('resolved', 'failed') GROUP BY severity`) if err != nil { return nil, err } for rows.Next() { var severity string var n int if err := rows.Scan(&severity, &n); err != nil { rows.Close() return nil, err } resp.SignalsBySeverity[severity] = n } rows.Close() if err := rows.Err(); err != nil { return nil, err } if err := s.pool.QueryRow(ctx, `SELECT count(*) FROM approvals WHERE status = 'pending'`, ).Scan(&resp.ApprovalsPending); err != nil { return nil, err } rows, err = s.pool.Query(ctx, ` SELECT status, count(*) FROM executions WHERE created_at > now() - interval '24 hours' GROUP BY status`) if err != nil { return nil, err } for rows.Next() { var status string var n int if err := rows.Scan(&status, &n); err != nil { rows.Close() return nil, err } resp.ExecutionsByState[status] = n } rows.Close() if err := rows.Err(); err != nil { return nil, err } rows, err = s.pool.Query(ctx, ` SELECT date_trunc('hour', ts) + (extract(minute FROM ts)::int / 5) * interval '5 minutes' AS bucket, count(*) FROM events WHERE ts > now() - interval '6 hours' GROUP BY bucket ORDER BY bucket`) if err != nil { return nil, err } for rows.Next() { var bucket time.Time var n int if err := rows.Scan(&bucket, &n); err != nil { rows.Close() return nil, err } resp.EventRate = append(resp.EventRate, struct { Bucket time.Time `json:"bucket"` Count int `json:"count"` }{Bucket: bucket, Count: n}) } rows.Close() if err := rows.Err(); err != nil { return nil, err } return gen.GetDashboardSummary200JSONResponse(resp), nil }