Files
oikos/plans/2026-07-08-control-room-webui.md
dtoro 2908b0a377
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
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.
2026-07-08 17:02:09 +02:00

10 KiB

2026-07-08 — Control room web UI

Status: In Progress — N0-N3 (Nomos amendment: chat home + sessions), M1 (dashboard/summary, Overview, Entities table, live event feed, shadcn-svelte component system), M2 (Operations ledger with approve/deny + cancel, Signals page with ack/resolve/mute, live nav badges), and M3 (graph explorer with include=status health coloring, per-relationship-type edge coloring + legend/filter, node-type filter, node search/highlight, entity detail page with uPlot metric charts, #/entity/:slug route) complete 2026-07-08. Event gap-fill (M2's other half) landed earlier in commit e8e230b, and approval creation's FK bug (gaps-plan A1) was already fixed, unblocking M2. While building M3, also fixed GET /metrics to make the metric query param genuinely optional (server now reports every metric recorded for the entity in range) — the implementation previously 400'd when it was omitted, contradicting its own documented-optional spec. M4 (agent activity, knowledge search page, audit, correlation grouping, polish) remains.

Goal

A realtime "control room" web UI for inspecting the state of Oikos and all its entities — graphs, tables, and lists — that updates live as an agent (Hermes) interacts with the system: executions appearing, approvals firing, health changing, the entity graph mutating. Inspect and act: approve/deny pending approvals and ack/resolve/mute signals directly from the UI.


Stack (decided)

Svelte 5 + Vite + TypeScript SPA, compiled to static assets in web/dist, embedded into the existing oikos binary via go:embed all:web/dist in a new internal/httpapi/ui.go, served at /ui/ (redirect //ui/, SPA fallback to index.html).

Why: the hard requirements (force-directed graph, time-series charts, one SSE stream patching many widgets) are client-side-JS problems; Svelte's reactive stores map 1:1 onto "SSE event mutates shared state, every widget reacts"; the compiled runtime keeps the embed small; and openapi-typescript generates frontend types from api/openapi.yaml — the frontend twin of the repo's oapi-codegen contract-first discipline (ADR-0004). Embedding preserves the single-binary story: no new container, no CORS, no Caddy changes.

Dependencies kept minimal:

  • d3-force — graph physics only; render SVG/canvas by hand
  • uPlot — ~45 KB canvas time-series, ideal for /metrics rollups
  • openapi-typescript — dev-only, generates api-types.d.ts
  • No SvelteKit (no SSR wanted — the Go binary is the server); hash router.

Amendment 2026-07-08 (M1): component library is shadcn-svelte over Tailwind CSS v4 (@tailwindcss/vite), not hand-rolled CSS — Table, Card, Badge, Sidebar, Sheet, Select, Input, Button, Tabs, ScrollArea, Tooltip, Dialog, Dropdown-menu, Sonner installed via npx shadcn-svelte add. The existing dark GitHub-style palette (app.css) was ported into shadcn's CSS-variable theme contract (--background, --card, --primary, etc. under @theme inline) so old and new components share one palette. d3-force / uPlot remain the plan for the graph/charts milestones (M3), unaffected by this change.

Build integration: commit a placeholder web/dist/index.html so backend-only go build never breaks; make ui runs the Vite build; add a node stage to compose/oikos/Dockerfile (3-stage: node → go → runtime). Local dev: vite dev proxying /api:8090.


Realtime

Reuse the existing pipeline: migration 008's pg_notify('oikos_events') trigger → internal/httpapi/sse.go broker → GET /api/v1/events/stream (Last-Event-ID replay, 15s heartbeat). One shared EventSource in a Svelte store; pages subscribe by event type and either patch state from event.data or trigger a targeted refetch. Scheduler and actuator are separate processes but share Postgres, so their events reach the api role's LISTEN automatically.

Event emission gap-fill (required)

Today only 5 event types are emitted (entity.created, entity.updated, client.enrolled, entity.provisioned, execution.requested). Most of what the control room must show live is silent. Add observability.Event(...) calls at:

Event Site
signal.raised / signal.resolved internal/scheduler/scheduler.go (~115 / ~145)
health.changed (on transition only, to avoid flooding) scheduler entity_status writes (~104/135/156)
signal.acked / signal.muted / signal.resolved (API side) internal/httpapi/impl.go:545/582/619
approval.created internal/mcp/server.go createApproval — depends on bug A1 in gaps plan
approval.decided internal/httpapi/phase3.go:871
execution.started/completed/failed/cancelled internal/actuator/actuator.go, phase3.go:746
relationship.created / relationship.ended phase3.go:1560/1621

Agent activity: poll GET /agent-activity every ~5s rather than duplicating every tool call into events.


API surface

Existing endpoints already cover nearly everything the UI needs: /graph?root=&depth=&rel_type= (impl.go:268), /health fleet rollup (impl.go:481), /metrics?rollup=raw|1h|1d|auto&from=&to=, /trends/{id}, /events (filterable) + /events/stream (SSE), /signals + ack/resolve/ mute, /executions, /approvals + POST /approvals/{id}/decision, /agent-activity, /knowledge/search, /audit, entities CRUD, /ontology.

New endpoints (spec-first in api/openapi.yaml, regen, then implement):

  1. GET /api/v1/dashboard/summary (new internal/httpapi/dashboard.go) — one round-trip for the overview: entity counts by type/state, health rollup from entity_status, open signals by severity, pending approvals, executions by state (24h), event-rate sparkline (count/5min from the events hypertable).
  2. GET /graph?include=status — join entity_status so graph nodes can be colored by health (preferred over a separate bulk-status endpoint).

Auth & deployment

  • Serve /ui/* assets without combinedAuth — Caddy/Authentik already gates the oikos.hubris.network vhost, and the assets are public JS/CSS.
  • Trusted-proxy header auth for API calls from the browser: extend combinedAuth (internal/httpapi/server.go:157) with a config flag (OIKOS_TRUSTED_PROXY_AUTH) — when no Bearer token is present but Authentik forward-auth headers (X-Authentik-Username/-Email) are, resolve an operator actor. Caddy must inject these via forward_auth and strip inbound X-Authentik-* from clients. Result: the UI needs no token handling, and — critically — EventSource works unmodified (it cannot set Authorization headers).
  • Docker: node build stage in compose/oikos/Dockerfile; no compose or Caddy routing changes.

Pages

Persistent nav + live top status strip (health dots, open-signal badge, pending-approval badge):

  1. Agent chat (home view)amendment 2026-07-08, see the Nomos resident agent plan: the default view at /ui/#/ is a conversation with the resident Nomos agent (formerly Hermes; streamed via /agent/chat), with a live right rail showing pending approvals (decidable in place), recent events, and health. A persistent chat drawer is reachable from every other page. Lands with resident-agent milestone N3; until then, Overview is the home view.
  2. Overview — summary cards from /dashboard/summary, event-rate sparkline, live event ticker, top degraded entities.
  3. Graph explorer — d3-force over /graph; node color = health; filters by entity type / rel_type / root+depth; click → side panel with attributes
    • blast radius; live mutation from entity.* / relationship.* / health.changed events.
  4. Entity detail — attributes, relations mini-graph, blast radius, uPlot charts (/metrics, /trends), entity-scoped events, knowledge, open signals, executions.
  5. Entities table — filter/sort by type/state/health, live badges.
  6. Operations ledger — executions + approvals panes with approve/deny buttons (POST /approvals/{id}/decision); live via execution.* / approval.* events; grouped by correlation_id. The "watch Hermes work" page.
  7. Signals — tabs by state, severity filter, ack/resolve/mute actions.
  8. Live event feed — full stream tail with filters, pause, correlation-id clustering (one agent action = one cluster), scroll-back via GET /events.
  9. Agent activity — polled /agent-activity timeline joined to executions/approvals by correlation_id.
  10. Knowledge — FTS search over /knowledge/search. Audit — low priority.

Milestones

  • M1 (MVP): scaffold + embed + proxy auth; Overview, Entities table, Live event feed over SSE. Proves the pipeline end-to-end (agent creates an entity → it appears live in the browser).
  • M2: event gap-fill (all sites above); Operations ledger with live approvals + decision buttons; Signals page. The payoff milestone. Depends on gaps-plan bug A1 for approvals to exist at all.
  • M3: graph explorer with live mutation; entity detail with charts; /dashboard/summary; include=status.
  • M4: agent activity, knowledge, audit, correlation grouping, polish.

File layout

web/
  package.json  vite.config.ts
  src/
    main.ts  App.svelte  router.ts
    lib/api.ts  lib/api-types.d.ts
    lib/stores/{events.ts, summary.ts}
    pages/{Overview,Graph,Entity,Entities,Ops,Signals,Events,Agent,Knowledge}.svelte
  dist/index.html                     # committed placeholder
internal/httpapi/ui.go                # go:embed + SPA fallback at /ui
internal/httpapi/dashboard.go         # GET /dashboard/summary
internal/httpapi/server.go            # mount /ui; trusted-proxy auth
api/openapi.yaml                      # new paths
internal/{scheduler/scheduler.go, mcp/server.go, actuator/actuator.go,
  httpapi/{impl,phase3}.go}           # missing event emissions
Makefile  compose/oikos/Dockerfile    # ui build targets, node stage

Verification

  • M1: run oikos api locally, open /ui/, confirm Overview + Entities render from live API; POST /api/v1/entities from curl and watch it appear in the event feed without refresh.
  • M2: drive request_execution through Hermes and watch the approval appear on the Operations ledger, decide it from the UI, and see the execution state advance live.
  • Backend-only go build ./... succeeds without a node toolchain (placeholder dist).