Files
oikos/plans/2026-07-08-control-room-webui.md
dtoro 9d9cbb63c4
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
plans: rename resident agent Hermes -> Nomos, add implementable rename phase N0
Nomos (from oikonomos, steward of the oikos) avoids the name collision
with Nous Research's Hermes Agent. N0 enumerates the full rename scope:
cmd/, hermes/ dir, env vars, config fields, compose service, Caddy vhost,
identity-preserving DB slug migration + seed update, persona docs.
History, the Matrix bot user, and legacy bin/hermes stay untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 13:47:24 +02:00

8.9 KiB

2026-07-08 — Control room web UI

Status: Planned

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), no component framework; hash router; hand-rolled dark-theme CSS.

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).