Add three new pages completing the control-room web UI: - Agent activity: polls /agent-activity every 5s, filterable by type/agent - Knowledge search: FTS over /knowledge/search with snippet + entity links - Audit trail: browseable audit log with actor/action/entity filters Enhanced live events page with correlation-id clustering (Groups toggle). Added fetchAgentActivity/searchKnowledge/fetchAudit to the API client. 11 nav items now cover all planned control-room views.
161 lines
3.6 KiB
Go
161 lines
3.6 KiB
Go
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
|
|
}
|