From d09ac0844a1c09f21f19d7692ae05237eeb8c588 Mon Sep 17 00:00:00 2001 From: Evanfeenstra Date: Tue, 15 Sep 2026 13:00:56 -0700 Subject: [PATCH] =?UTF-8?q?gateway:=20plan=20housekeeping=20=E2=80=94=20ti?= =?UTF-8?q?ck=20phase=208/9=20checklists,=20renumber=20phase=209,=20drop?= =?UTF-8?q?=20dead=20useSpendByAgent=20param?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docs reflect what actually shipped, judged from code rather than the tickboxes: - phase-8 and phase-9 wire-up checklists ticked; compound lines split so unshipped items (Config page + /_plugin/config/*, Sessions, Runs list, BudgetEditor, Bucket/DimensionPicker) stay unticked. DataTable noted as sortable-not-paginated: the run call log truncates at the server's 50-row default with no pager. - phase-9 doc was titled and self-referenced as "Phase 8" from before the numbering shift; renumbered, with a status note at the top. References to the cookie/session layer stay "phase 8" because that layer shipped there. - llm-governance-v2 spec table now points at phases 8 and 9; phase-3 notes the admin pair also signs into /_plugin/ui; phase-12 records the unsigned /_plugin/tlog/status route the status card uses. - README page list updated (Canvas, People, UserDetail, catalog tabs, kill switches) with what phase 9 still owes. - Sidebar comment no longer promises Sessions/Config as present. - useSpendByAgent's userID param removed: no caller passed it and UserDetail reads agents_used from /users/:id instead. --- gateway/README.md | 9 +- .../internal/adminapi/ui/src/api/queries.ts | 13 +- .../ui/src/components/layout/Sidebar.tsx | 5 +- gateway/plans/llm-governance-v2.md | 2 + .../plans/phases/phase-12-transparency-log.md | 11 +- gateway/plans/phases/phase-3-swarm-handoff.md | 5 +- .../phases/phase-8-observability-dashboard.md | 79 ++++++------ gateway/plans/phases/phase-9-operator-ui.md | 114 +++++++++++------- 8 files changed, 140 insertions(+), 98 deletions(-) diff --git a/gateway/README.md b/gateway/README.md index 59d360198..192a0fdac 100644 --- a/gateway/README.md +++ b/gateway/README.md @@ -141,9 +141,12 @@ runs the operator dashboard. Source lives under (in CI / docker) the `plugin-ui-builder` stage in the Dockerfile, then embedded into the plugin .so via `//go:embed all:ui/dist`. -Pages in v1: Login, Dashboard, Agents, AgentDetail, RunDetail. All -read-only; phase 9 adds the kill switches, budget editors, and live -Redis-blended panels. +Pages: Login, Canvas (landing), Dashboard, People, UserDetail, Agents, +AgentDetail (Overview / Prompts / Tools / Skills / Evals tabs), and +RunDetail (call log, cap meters, live Redis state, kill). Agent kill +switches are live too. Still open from +[phase 9](./plans/phases/phase-9-operator-ui.md): budget editing, the +Config page, Sessions, and a `/runs` list. Local UI dev with HMR (proxies `/_plugin/*` to a running plugin on `localhost:8181`): diff --git a/gateway/internal/adminapi/ui/src/api/queries.ts b/gateway/internal/adminapi/ui/src/api/queries.ts index 00324b941..6723eaf63 100644 --- a/gateway/internal/adminapi/ui/src/api/queries.ts +++ b/gateway/internal/adminapi/ui/src/api/queries.ts @@ -60,16 +60,13 @@ export function useMe() { // Polled at 30s on the dashboard's rankings tables and the Agents // page. Polling pauses automatically when the tab is hidden. -export function useSpendByAgent(window: Window, userID?: string) { - // Optional userID scopes the rollup to one person's calls — - // used by the UserDetail page to render "agents this user - // invoked". Server-side filter so we don't fan out per row. - const params = new URLSearchParams({ window }); - if (userID) params.set("user_id", userID); +export function useSpendByAgent(window: Window) { return useQuery({ - queryKey: ["spend", "by-agent", window, userID ?? ""], + queryKey: ["spend", "by-agent", window], queryFn: () => - apiFetch(`/spend/by-agent?${params.toString()}`), + apiFetch( + `/spend/by-agent?window=${encodeURIComponent(window)}` + ), refetchInterval: 30_000, staleTime: 10_000, }); diff --git a/gateway/internal/adminapi/ui/src/components/layout/Sidebar.tsx b/gateway/internal/adminapi/ui/src/components/layout/Sidebar.tsx index 00ede6813..ae2669f25 100644 --- a/gateway/internal/adminapi/ui/src/components/layout/Sidebar.tsx +++ b/gateway/internal/adminapi/ui/src/components/layout/Sidebar.tsx @@ -2,8 +2,9 @@ import { Link, useLocation } from "wouter-preact"; import { ChevronLeftIcon } from "../icons"; -// Sidebar nav. Phase 8 has Dashboard + People + Agents; phase 9 -// grows Sessions and Config — each is just another row here. +// Sidebar nav. Canvas is the landing page; Dashboard, People and +// Agents are the phase-8/9 views. Phase 9's Sessions and Config +// pages are still unbuilt — when they land, each is one more row. // // People above Agents reflects the organising principle of the // governance plan: "every LLM call traces to a specific human." diff --git a/gateway/plans/llm-governance-v2.md b/gateway/plans/llm-governance-v2.md index 8791004f1..a9bafbd23 100644 --- a/gateway/plans/llm-governance-v2.md +++ b/gateway/plans/llm-governance-v2.md @@ -16,6 +16,8 @@ > | Trust registry storage, admin API, env-var seed | [`phases/phase-5-trust-registry.md`](./phases/phase-5-trust-registry.md) | > | Plugin Redis schema, per-hook ops, TTL policy, failure modes | [`phases/phase-6-plugin-enforcement.md`](./phases/phase-6-plugin-enforcement.md) | > | `/_plugin/*` observability over `logs.db` (per-dim analytics) | [`phases/phase-7-observability.md`](./phases/phase-7-observability.md) | +> | Read-only operator dashboard (embedded SPA, sessions, tygo) | [`phases/phase-8-observability-dashboard.md`](./phases/phase-8-observability-dashboard.md) | +> | Operator console: kill switches, cap meters, budget/config edits | [`phases/phase-9-operator-ui.md`](./phases/phase-9-operator-ui.md) | > | Symmetric narrowing rule, per-realm budgets, cross-swarm spawning | [`phases/phase-11-symmetric-recursive-authorization.md`](./phases/phase-11-symmetric-recursive-authorization.md) | > | Transparency log: Merkle leaves + Hive witness (Part 1); receipts + client enforcement (Part 2) | [`phases/phase-12-transparency-log.md`](./phases/phase-12-transparency-log.md) | > | Agent defaults / registry | [`agent-registry.md`](./agent-registry.md) | diff --git a/gateway/plans/phases/phase-12-transparency-log.md b/gateway/plans/phases/phase-12-transparency-log.md index 63d83b513..af2384ee8 100644 --- a/gateway/plans/phases/phase-12-transparency-log.md +++ b/gateway/plans/phases/phase-12-transparency-log.md @@ -258,6 +258,13 @@ Registered exactly like `/_plugin/admin-credentials`: `cookieOrBearer`: a dashboard cookie must not be able to pull material Hive then org-signs. +A second, dashboard-facing route, `GET /_plugin/tlog/status` +(`cookieOrBearer`), landed with the status card in PR #1690. It +returns local facts only — enabled flag, tree size, root hash, the +per-boot `log_pubkey`, leaf path, last-leaf time — never a signed STH +or leaves, so a dashboard cookie still cannot pull anything Hive would +org-sign. + `since` is a base-10 integer in `[0, tree_size]`. Anything else → 400. `since > tree_size` → 409 with the current size in the body (see "Failure modes"). The handler `Sync()`s the leaf file, then responds: @@ -667,6 +674,8 @@ builds receives the wrapper. - **Redis-backed receipt map** (after Part 2), so a restart mid-stream does not 404 the poll. - **Dashboard.** A provenance badge on `/runs/:id` ("witnessed at - head N") and, after Part 2, a per-run receipt list. + head N") and, after Part 2, a per-run receipt list. (The `TlogCard` + on the Dashboard is not this: it renders the unsigned + `/_plugin/tlog/status` facts, not witness state.) - **Actions the gateway never sees** (git push, file writes). The leaf format generalizes; the submit path does not exist yet. diff --git a/gateway/plans/phases/phase-3-swarm-handoff.md b/gateway/plans/phases/phase-3-swarm-handoff.md index 2495f8896..22eb6bd62 100644 --- a/gateway/plans/phases/phase-3-swarm-handoff.md +++ b/gateway/plans/phases/phase-3-swarm-handoff.md @@ -29,7 +29,10 @@ gateway and own every Bifrost in the system. The `bifrost_admin_password` is meant to be user-facing — that's the "copy this to log in to your dashboard" experience. It's per-gateway -and only grants control over one workspace's Bifrost. +and only grants control over one workspace's Bifrost. The same pair +also signs into the plugin's own operator UI at `/_plugin/ui/` +(`POST /_plugin/login`, phases 8–9), so one credential drives both +`/api/*` and the dashboard. ## Status diff --git a/gateway/plans/phases/phase-8-observability-dashboard.md b/gateway/plans/phases/phase-8-observability-dashboard.md index f53741329..cdc3a4131 100644 --- a/gateway/plans/phases/phase-8-observability-dashboard.md +++ b/gateway/plans/phases/phase-8-observability-dashboard.md @@ -774,75 +774,80 @@ served by the same SPA handler (wouter handles it client-side). **Backend — sessions (`gateway/internal/sessions/`):** -- [ ] `store.go`: `SessionStore` interface; Redis implementation. -- [ ] `store_test.go`: miniredis-backed tests covering create, get, +- [x] `store.go`: `SessionStore` interface; Redis implementation. +- [x] `store_test.go`: miniredis-backed tests covering create, get, refresh, delete, kick-all-for-user, TTL expiry. **Backend — adminapi (`gateway/internal/adminapi/`):** -- [ ] `session.go`: middleware combining session-cookie and +- [x] `session.go`: middleware combining session-cookie and bearer-token auth; `allowAnon` list. -- [ ] `login.go`: `POST /_plugin/login` (Basic-in, cookie-out), +- [x] `login.go`: `POST /_plugin/login` (Basic-in, cookie-out), `POST /_plugin/logout`, `GET /_plugin/me`. -- [ ] `ratelimit.go`: per-IP login attempt counter. -- [ ] `logstore_client.go`: HTTP client to +- [x] `ratelimit.go`: per-IP login attempt counter. +- [x] `logstore_client.go`: HTTP client to `http://127.0.0.1:8080/api/logs` (and rankings / histogram sub-paths). Owns `SearchFilters` serialization, retry, timeout. Reusable across all handlers below and any phase 9 additions. -- [ ] `spend.go`: `/_plugin/spend/by-{agent,user}` handlers. -- [ ] `histogram.go`: `/_plugin/histogram/cost` handler calling +- [x] `spend.go`: `/_plugin/spend/by-{agent,user}` handlers. +- [x] `histogram.go`: `/_plugin/histogram/cost` handler calling `GetDimensionCostHistogram` through the client. -- [ ] `runs.go`: `/_plugin/runs/:run_id` drill-down (read-only; - phase 9 extends with `/state` and `/kill`). -- [ ] `ui.go`: `//go:embed ui/dist` + SPA fallback handler. -- [ ] Route registration in `server.go`, with `/_plugin/ui/*` and +- [x] `/_plugin/runs/:run_id` drill-down — lives in `observability.go`, + not a separate `runs.go`; phase 9 added `/state` and `/kill` in + `hotstate.go`. +- [x] `ui.go`: `//go:embed ui/dist` + SPA fallback handler. +- [x] Route registration in `server.go`, with `/_plugin/ui/*` and every `/_plugin/spend/*`, `/_plugin/histogram/*`, `/_plugin/runs/:id` route behind the session middleware, and `/_plugin/health`, `/_plugin/login` in `allowAnon`. **Backend — type codegen (`gateway/`):** -- [ ] `tygo.yaml` covering `internal/adminapi`. -- [ ] `make tygo` target invoking the codegen. -- [ ] CI step: `make tygo && git diff --exit-code internal/adminapi/ui/src/api/types.ts`. +- [x] `tygo.yaml` covering `internal/adminapi`. +- [x] `make tygo` target invoking the codegen. +- [x] CI step: `make tygo-check` in `.github/workflows/gateway-check.yml`. **Frontend (`gateway/internal/adminapi/ui/`):** -- [ ] Vite + Preact + TS scaffold; `package.json`, `vite.config.ts`, +- [x] Vite + Preact + TS scaffold; `package.json`, `vite.config.ts`, `tsconfig.json`, `index.html`. -- [ ] `src/api/client.ts`: typed fetch wrapper, 401 redirect. -- [ ] `src/api/queries.ts`: one hook per endpoint. -- [ ] `src/app.tsx`: wouter routes, QueryClient provider, global +- [x] `src/api/client.ts`: typed fetch wrapper, 401 redirect. +- [x] `src/api/queries.ts`: one hook per endpoint. +- [x] `src/app.tsx`: wouter routes, QueryClient provider, global auth-error handler. -- [ ] `src/components/layout/Shell.tsx` + `Sidebar` + `Topbar`. -- [ ] `src/components/charts/UplotChart.tsx`: generic wrapper. -- [ ] `src/components/charts/CostHistogram.tsx`: stacked-area +- [x] `src/components/layout/Shell.tsx` + `Sidebar` + `Topbar`. +- [x] `src/components/charts/UplotChart.tsx`: generic wrapper. +- [x] `src/components/charts/CostHistogram.tsx`: stacked-area cost-by-time-by-dimension chart. -- [ ] `src/components/tables/DataTable.tsx`: sortable, paginated. -- [ ] `src/components/controls/WindowPicker.tsx`. -- [ ] `src/components/EmptyState.tsx`, `ErrorBoundary.tsx`. -- [ ] `src/pages/Login.tsx`, `Dashboard.tsx`, `Agents.tsx`, - `AgentDetail.tsx`, `RunDetail.tsx`, `NotFound.tsx`. -- [ ] `src/styles/base.css` + `components.css`. +- [x] `src/components/tables/DataTable.tsx`: sortable. **Not paginated**: + it caps rows client-side, and `useRunDetail` sends no + `limit`/`offset`, so a run with more calls than the server's + 50-row default is silently truncated — still open. +- [x] `src/components/controls/WindowPicker.tsx`. +- [x] `src/components/EmptyState.tsx`, `ErrorBoundary.tsx`. +- [x] `src/pages/Login.tsx`, `Dashboard.tsx`, `Agents.tsx`, + `AgentDetail.tsx`, `RunDetail.tsx`, `NotFound.tsx` — plus + `Canvas.tsx` (the landing page), `People.tsx`, `UserDetail.tsx` + and the `EvalsView` tab, all beyond this phase's scope. +- [x] `src/styles/base.css` + `components.css`. **Dockerfile:** -- [ ] `ui-builder` stage running `npm ci && npm run build`. -- [ ] `COPY --from=ui-builder /ui/dist` into the Go build context +- [x] `plugin-ui-builder` stage running `npm ci && npm run build`. +- [x] `COPY --from=plugin-ui-builder /pui/dist` into the Go build context before `go build`. **Docs:** -- [ ] Update `gateway/README.md` with the observability-dashboard +- [x] Update `gateway/README.md` with the observability-dashboard bullet (URL, default creds in dev, link to this phase). -- [ ] Update `phase-3-swarm-handoff.md` to note that admin creds +- [x] Update `phase-3-swarm-handoff.md` to note that admin creds now drive both `/api/*` and the observability dashboard. -- [ ] Update `phase-7-observability.md` to note that the four - endpoints used by phase 8 ship in phase 8; the remainder - ships with phase 9. -- [ ] Forward-pointer from `llm-governance-v2.md` §"Plugin" to this - phase. +- [x] Update `phase-7-observability.md` — moot: every phase-7 + endpoint has since shipped (its own checklist is ticked). +- [x] Forward-pointer from `llm-governance-v2.md` to this phase (in + its authoritative-spec table). **Gate:** phase 8 ships when: diff --git a/gateway/plans/phases/phase-9-operator-ui.md b/gateway/plans/phases/phase-9-operator-ui.md index 551627738..e6bd2a817 100644 --- a/gateway/plans/phases/phase-9-operator-ui.md +++ b/gateway/plans/phases/phase-9-operator-ui.md @@ -1,5 +1,15 @@ -# Phase 8 — Operator UI: Embedded Console for Kill, Budgets, and Analytics - +# Phase 9 — Operator UI: Embedded Console for Kill, Budgets, and Analytics + +> **Status (2026-09-15):** slices 1–4 have shipped — session auth, +> dashboard, run kill + live state + cap meters (PRs #1685, #1686), +> agent kill + the kill-state column. Still open: the `/runs` list, +> `Sessions` / `SessionDetail`, the `/users/:id/quota` panel, the +> inline budget editor, `BucketPicker` / `DimensionPicker`, and the +> whole `/_plugin/config/*` override layer with its `Config` page +> (`agent_budgets`, `hard_ceiling`, `tool_loop` are still YAML-only, +> so gate 2 at the bottom is unmet). The wire-up checklist reflects +> this. +> > Operator-facing single-page app served by the plugin at > `/_plugin/ui/*`. Companion to `phase-6-plugin-enforcement.md` (which > defines the kill/state/budget mutation endpoints the UI drives) and @@ -32,7 +42,7 @@ But: scheme, one binary, no cross-origin headache), and the wrapper already proxies it. -Phase 8 decides: +Phase 9 decides: - The UI's framework stack, build pipeline, and embed mechanism. - The auth model (basic-auth handoff to plugin-issued session cookies @@ -65,14 +75,14 @@ After this: | Phase 5 | Which orgs the swarm trusts | | Phase 6 | How the plugin enforces caveats against Redis state | | Phase 7 | Read-only per-dim analytics over `logs.db` | -| **Phase 8** | **Operator UI on top of phases 6 + 7** | +| **Phase 9** | **Operator UI on top of phases 6 + 7** | -Phase 8 has a hard dependency on phases 6 and 7 because every UI +Phase 9 has a hard dependency on phases 6 and 7 because every UI view it ships reads or writes one of their endpoints. If phase 6 or 7 changes a response shape, the typed UI client breaks at compile time — by design. -Phase 8 does **not** depend on Hive shipping anything; the swarm +Phase 9 does **not** depend on Hive shipping anything; the swarm operator UI is fully self-contained inside the plugin. Hive integration (single-sign-on from Hive's dashboard, cross-workspace fan-out) is a follow-on phase. @@ -93,12 +103,12 @@ fan-out) is a follow-on phase. Hive-side job that fans out to each plugin's `/_plugin/spend/*`. - **Macaroon inspector / debugger view.** Speculative; not in v1. The HTTP endpoints exist (`/api/logs` carries the raw `x-macaroon` - header in metadata if logged) but the UI for it is not phase 8. + header in metadata if logged) but the UI for it is not phase 9. - **WebSocket live tail.** Phase 7 explicitly excluded this; Hive can connect to Bifrost's `ws://...:8080/ws` directly if it wants real - log streams. Phase 8 polls at the cadences specified in + log streams. Phase 9 polls at the cadences specified in "Data-fetching cadence" below. -- **Mobile / responsive layout.** Phase 8 targets desktop browsers +- **Mobile / responsive layout.** Phase 9 targets desktop browsers ≥1280px wide. Operators use this from a workstation, not a phone. ## Framework stack @@ -374,7 +384,7 @@ The client IP is read from `X-Forwarded-For` (the swarm ingress sets it). Without an X-Forwarded-For header, the remote-addr from the wrapper is used, which in production is always `127.0.0.1` and makes the limiter degenerate — operators running without an ingress -proxy in front are explicitly out of phase 8's threat model. +proxy in front are explicitly out of phase 9's threat model. ## Endpoint surface (additions in phase 8) @@ -412,7 +422,7 @@ The full operator-relevant surface, by page: The Config page edits `agent_budgets`, `hard_ceiling`, and `tool_loop` from `plugin.yaml`. Phase 6 specified these as YAML-only -configuration. Phase 8 adds HTTP equivalents: +configuration. Phase 9 adds HTTP equivalents: ``` GET /_plugin/config → full config (read-only fields redacted) @@ -422,7 +432,7 @@ PUT /_plugin/config/hard_ceiling { per_invocation_cost_usd, per_i PUT /_plugin/config/tool_loop { window, threshold } → 204 ``` -**Overrides are persisted in Redis, not in a new SQL table.** Phase 8 +**Overrides are persisted in Redis, not in a new SQL table.** Phase 9 introduces no new SQL storage. The plugin already owns Redis (phase 6 hot state, phase 8 sessions); config overrides live there too: @@ -434,7 +444,7 @@ bifrost:config:tool_loop HASH { window, threshold } no TTL These three key shapes extend phase 6's schema (the canonical schema list in `phase-6-plugin-enforcement.md` "Redis schema" should be -updated to include them when phase 8 lands). +updated to include them when phase 9 lands). **Precedence at runtime:** @@ -465,7 +475,7 @@ Listing this here rather than in phase 6 because phase 6 has been operational on YAML-only for as long as the plugin has existed. Adding HTTP mutability is a UI-driven need; the Redis key shapes above belong logically in phase 6's schema and should be folded -back in when phase 8 lands. +back in when phase 9 lands. ## View inventory @@ -754,69 +764,81 @@ authenticated calls after submission. **Backend — sessions (`gateway/internal/sessions/`):** -- [ ] `store.go`: `SessionStore` interface; Redis implementation. -- [ ] `store_test.go`: miniredis-backed tests covering create, get, +- [x] `store.go`: `SessionStore` interface; Redis implementation. +- [x] `store_test.go`: miniredis-backed tests covering create, get, refresh, delete, kick-all-for-user, TTL expiry. **Backend — adminapi (`gateway/internal/adminapi/`):** -- [ ] `session.go`: middleware combining session-cookie and +- [x] `session.go`: middleware combining session-cookie and bearer-token auth; `allowAnon` list. -- [ ] `login.go`: `POST /_plugin/login` (Basic-in, cookie-out), +- [x] `login.go`: `POST /_plugin/login` (Basic-in, cookie-out), `POST /_plugin/logout`, `GET /_plugin/me`. -- [ ] `ratelimit.go`: per-IP login attempt counter. +- [x] `ratelimit.go`: per-IP login attempt counter. - [ ] `config.go`: `GET /_plugin/config` and the PUT/DELETE endpoints for `agent_budgets`, `hard_ceiling`, `tool_loop`. Persists overrides to Redis under `bifrost:config:*` (no new SQL table). Composes YAML baseline + Redis overrides into an in-memory effective config that PreLLMHook reads. -- [ ] `ui.go`: `//go:embed ui/dist` + SPA fallback handler. -- [ ] Route registration in `server.go`, with `/_plugin/ui/*` +- [x] `ui.go`: `//go:embed ui/dist` + SPA fallback handler. +- [x] Route registration in `server.go`, with `/_plugin/ui/*` behind the session middleware and `/_plugin/health`, `/_plugin/login` in `allowAnon`. **Backend — type codegen (`gateway/`):** -- [ ] `tygo.yaml` covering `internal/adminapi` and any response - types in `internal/auth`. -- [ ] `make tygo` target invoking the codegen. -- [ ] CI step: `make tygo && git diff --exit-code internal/adminapi/ui/src/api/types.ts`. +- [x] `tygo.yaml` covering `internal/adminapi` (every type the SPA + decodes is declared there; nothing in `internal/auth` is + decoded directly). +- [x] `make tygo` target invoking the codegen. +- [x] CI step: `make tygo-check` in `.github/workflows/gateway-check.yml`. **Frontend (`gateway/internal/adminapi/ui/`):** -- [ ] Vite + Preact + TS scaffold; `package.json`, `vite.config.ts`, +- [x] Vite + Preact + TS scaffold; `package.json`, `vite.config.ts`, `tsconfig.json`, `index.html`. -- [ ] `src/api/client.ts`: typed fetch wrapper, 401 redirect. -- [ ] `src/api/queries.ts` + `mutations.ts`: one hook per endpoint. -- [ ] `src/app.tsx`: wouter routes, QueryClient provider, global +- [x] `src/api/client.ts`: typed fetch wrapper, 401 redirect. +- [x] `src/api/queries.ts`: one hook per endpoint, mutations included + (no separate `mutations.ts`). +- [x] `src/app.tsx`: wouter routes, QueryClient provider, global auth-error handler. -- [ ] `src/components/layout/Shell.tsx` + `Sidebar` + `Topbar`. -- [ ] `src/components/charts/UplotChart.tsx`: generic wrapper. -- [ ] `src/components/tables/DataTable.tsx`: sortable, paginated. -- [ ] `src/components/controls/*`: WindowPicker, BucketPicker, - DimensionPicker, KillButton, BudgetEditor. -- [ ] `src/pages/Login.tsx`, `Dashboard.tsx`, `Runs.tsx`, - `RunDetail.tsx`, `Agents.tsx`, `AgentDetail.tsx`, - `Users.tsx`, `UserDetail.tsx`, `Sessions.tsx`, - `SessionDetail.tsx`, `Config.tsx`, `NotFound.tsx`. -- [ ] `src/styles/base.css` + `components.css`. +- [x] `src/components/layout/Shell.tsx` + `Sidebar` + `Topbar`. +- [x] `src/components/charts/UplotChart.tsx`: generic wrapper. +- [x] `src/components/tables/DataTable.tsx`: sortable. **Not paginated** + (see the phase-8 checklist: the run call log truncates at the + server's 50-row default with no pager). +- [x] `src/components/controls/WindowPicker.tsx`; kill triggers are + inlined in `RunDetail` / `AgentDetail` behind + `components/KillConfirmModal.tsx` (no standalone `KillButton`). +- [ ] `BucketPicker`, `DimensionPicker` (bucket is derived from the + window; dimension is hardcoded to `agent-name`). +- [ ] `BudgetEditor` (blocked on the `/_plugin/config/*` layer above). +- [x] `src/pages/Login.tsx`, `Dashboard.tsx`, `RunDetail.tsx`, + `Agents.tsx`, `AgentDetail.tsx`, `People.tsx` (the plan's + `Users.tsx`), `UserDetail.tsx`, `NotFound.tsx` — plus + `Canvas.tsx` (landing page) and `EvalsView.tsx`, both beyond + this phase's scope. +- [ ] `Runs.tsx` (no `/runs` list; runs are reached from agent / user + detail), `Sessions.tsx`, `SessionDetail.tsx` (backend routes + exist, nothing queries them), `Config.tsx`. +- [x] `src/styles/base.css` + `components.css`. **Dockerfile:** -- [ ] `ui-builder` stage running `npm ci && npm run build`. -- [ ] `COPY --from=ui-builder /ui/dist` into the Go build context +- [x] `plugin-ui-builder` stage running `npm ci && npm run build`. +- [x] `COPY --from=plugin-ui-builder /pui/dist` into the Go build context before `go build`. **Docs:** -- [ ] Update `gateway/README.md` with the operator-UI bullet (URL, +- [x] Update `gateway/README.md` with the operator-UI bullet (URL, default creds in dev). -- [ ] Update `phase-3-swarm-handoff.md` to note that admin creds now +- [x] Update `phase-3-swarm-handoff.md` to note that admin creds now drive both `/api/*` and the operator UI. -- [ ] Forward-pointer from `llm-governance-v2.md` §"Plugin" to this - phase. +- [x] Forward-pointer from `llm-governance-v2.md` to this phase (in + its authoritative-spec table). -**Gate:** phase 8 ships when: +**Gate:** phase 9 ships when: 1. Login → dashboard → kill a test run → see the run state freeze end-to-end against a real Bifrost+plugin+Redis stack.