Audited all 10 active plan docs against the codebase (not just commit titles). 5 were fully shipped and stale-tagged "Planned"/"In Progress" — moved to done/ with verification notes. The other 4 got corrected Planned→In Progress status plus concrete remaining-gap notes so the next pass doesn't re-derive what's already done. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
11 KiB
2026-07-08 — Control room web UI
Status: In Progress (audited 2026-07-11 — still accurate; remaining gaps:
signal.acked/signal.resolved/signal.muted and relationship.created/
relationship.ended API calls don't emit observability.Event, and
trusted-proxy header auth for Authentik was never added to combinedAuth).
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 handuPlot— ~45 KB canvas time-series, ideal for/metricsrollupsopenapi-typescript— dev-only, generatesapi-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):
GET /api/v1/dashboard/summary(newinternal/httpapi/dashboard.go) — one round-trip for the overview: entity counts by type/state, health rollup fromentity_status, open signals by severity, pending approvals, executions by state (24h), event-rate sparkline (count/5min from theeventshypertable).GET /graph?include=status— joinentity_statusso graph nodes can be colored by health (preferred over a separate bulk-status endpoint).
Auth & deployment
- Serve
/ui/*assets withoutcombinedAuth— Caddy/Authentik already gates theoikos.hubris.networkvhost, 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 viaforward_authand strip inboundX-Authentik-*from clients. Result: the UI needs no token handling, and — critically —EventSourceworks 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):
- 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. - Overview — summary cards from
/dashboard/summary, event-rate sparkline, live event ticker, top degraded entities. - 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.changedevents.
- blast radius; live mutation from
- Entity detail — attributes, relations mini-graph, blast radius, uPlot
charts (
/metrics,/trends), entity-scoped events, knowledge, open signals, executions. - Entities table — filter/sort by type/state/health, live badges.
- Operations ledger — executions + approvals panes with approve/deny
buttons (
POST /approvals/{id}/decision); live viaexecution.*/approval.*events; grouped bycorrelation_id. The "watch Hermes work" page. - Signals — tabs by state, severity filter, ack/resolve/mute actions.
- Live event feed — full stream tail with filters, pause,
correlation-id clustering (one agent action = one cluster), scroll-back via
GET /events. - Agent activity — polled
/agent-activitytimeline joined to executions/approvals by correlation_id. - 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 apilocally, open/ui/, confirm Overview + Entities render from live API;POST /api/v1/entitiesfrom curl and watch it appear in the event feed without refresh. - M2: drive
request_executionthrough 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).