# 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](2026-07-08-oikos-gaps-and-improvements.md) | | `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. **Overview** — summary cards from `/dashboard/summary`, event-rate sparkline, live event ticker, top degraded entities. 2. **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. 3. **Entity detail** — attributes, relations mini-graph, blast radius, uPlot charts (`/metrics`, `/trends`), entity-scoped events, knowledge, open signals, executions. 4. **Entities table** — filter/sort by type/state/health, live badges. 5. **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. 6. **Signals** — tabs by state, severity filter, **ack/resolve/mute** actions. 7. **Live event feed** — full stream tail with filters, pause, correlation-id clustering (one agent action = one cluster), scroll-back via `GET /events`. 8. **Agent activity** — polled `/agent-activity` timeline joined to executions/approvals by correlation_id. 9. **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).