feat(ui): M4 — agent activity, knowledge search, audit, correlation grouping
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.
This commit is contained in:
160
internal/httpapi/dashboard.go
Normal file
160
internal/httpapi/dashboard.go
Normal file
@@ -0,0 +1,160 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user