plans: review and archive all plans to done/
Architecture has changed drastically (hexagonal refactor, web client extraction). Every active plan has been reviewed, annotated with 'Completed' or 'Won't do' status, and moved to plans/done/. Completed (7): gaps-and-improvements, liveness-drift, gated-execution, nomos-code-review, codebase-cleanup, mascot-physics, backend-eval Won't do (7): prometheus-lxc, control-room-webui, activity-gaps, activity-timeline, frontend-os-apps, haos-capability-gaps, arr-audit
This commit is contained in:
231
plans/done/2026-07-08-control-room-webui.md
Normal file
231
plans/done/2026-07-08-control-room-webui.md
Normal file
@@ -0,0 +1,231 @@
|
||||
# 2026-07-08 — Control room web UI
|
||||
|
||||
**Reviewed 2026-08-16 — Status: Won't do** — embed architecture removed by Wails client/server split; UI work moved to dtoro/oikos-web.
|
||||
|
||||
**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`).
|
||||
**Superseded (2026-07-12):** the embed architecture below (`go:embed
|
||||
all:web/dist`, served at `/ui/`) was removed —
|
||||
[2026-07-12-wails-desktop-app.md](2026-07-12-wails-desktop-app.md) Phase 0
|
||||
separates the SPA from the `oikos` binary into a standalone static build,
|
||||
served at `/` (no `/ui/` prefix), talking to the API over bearer-token
|
||||
auth (the dev-open bypass mentioned nowhere in this plan was also removed).
|
||||
The trusted-proxy-header gap noted above is moot under the new model — every
|
||||
route requires a real bearer token regardless of what's in front of it. M1-M3
|
||||
and the SPA/component work below are unaffected; only the packaging and auth
|
||||
sections are stale.
|
||||
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](https://www.shadcn-svelte.com/) 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](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):
|
||||
|
||||
0. **Agent chat (home view)** — *amendment 2026-07-08, see the
|
||||
[Nomos resident agent plan](2026-07-08-nomos-resident-agent.md)*: 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.
|
||||
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).
|
||||
Reference in New Issue
Block a user