feat: adopt operator-console redesign + modular adapter system - #128
Merged
Merged
Conversation
End-to-end demo of the Shipwright pattern (customer pastes a GitHub
issue → agent reads repo, plans, edits, tests, opens PR). Built on
standard shadcn components with a stub agent so iteration on the UX
is fast; swap in the real SWE-AF library by replacing one file.
## Backend (examples/02-shipwright/)
- `migrations/00001_shipwright.sql` — `shipwright_tasks` +
`shipwright_steps` with RLS policies; auto-touch trigger on
`updated_at`.
- `agents/shipwright/` — AgentField agent.
- `Agent(node_id="shipwright-v2")` + `@app.reasoner execute_task`
- Stub simulates 7 steps × asyncio.sleep totaling ~10s with
deterministic output (status / summary / diff_preview / steps).
- `__capabilities__` reasoner with harness + MCP runner probe.
- Dockerfile mirrors the sample-agent pattern (NODE_ID + PORT +
AGENT_CALLBACK_URL env).
- `handlers/` — Python FastAPI workload-module sidecar.
- Routes: POST /tasks, GET /tasks, GET /tasks/{id},
POST /tasks/{id}/cancel, GET /stats.
- Tenant-bound DB connection via `SET LOCAL app.tenant_id`.
- `_drive_task` runs in `asyncio.create_task` so POST returns
immediately; persists task row + 7 step rows on completion.
- `docker-compose.yml` — compose overlay with shipwright-migrate,
shipwright-agent, shipwright-api services.
## Frontend (apps/customer-app/)
- `(app)/shipwright/page.tsx` — list page with queue table +
new-task form. shadcn Card / Table / Badge / Field / Input /
Textarea / Button. Polls /tasks every 1.5s; status badges
color-coded (queued / running / completed / failed / cancelled);
durations + relative timestamps render live.
- `(app)/shipwright/[id]/page.tsx` — detail with step timeline,
summary card (duration / steps / status), monospace diff preview.
Live-polls every 1s until terminal state.
- `api/customer/shipwright/[...path]/route.ts` — proxy to the
workload-module sidecar; reads session via `auth.api.getSession`,
resolves `lookupCustomerContext(email)` → forwards
`x-af-stack-tenant-id` + `x-af-stack-user-id` headers.
- `components/layout/customer-sidebar.tsx` — replaced "Code Helper"
nav entry with "Shipwright" (Hammer icon).
## Verified E2E
Captured screenshots in `dashboard-screenshots/`:
- shipwright-with-sidebar.png — sidebar nav, Shipwright active
- shipwright-form-filled.png — new-task form filled
- shipwright-queue-mixed.png — list showing Running + Completed
- shipwright-detail-completed.png — full task detail with steps,
summary, diff preview
- shipwright-agent-logs.txt — agent log lines from the 3 runs
## Gotchas baked into the README + code comments
1. Don't name a reasoner `run` — collides with `Agent.run()` (the
decorator overrides the method; agent exits immediately).
2. AgentField caches reasoner metadata keyed on node_id. After
renaming a reasoner, bump the node_id to force fresh registration.
3. Reasoner payload shape: `{"input": {"payload": {...}}}` for
signatures like `async def f(payload: dict)`.
4. Result shape: runtime returns `{"result": {...}}`, not `output`.
## Swapping in real SWE-AF
Replace `agents/shipwright/main.py` with the real SWE-AF agent.
Keep the node_id + reasoner name + RunResult schema. Everything
else (UI, workload module, DB, polling) is unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Shipwright workload module now routes to the actual SWE-AF library
at /Users/santoshkumarradha/Documents/agentfield/code/examples/af-swe by
default. The iteration stub remains available behind an env flag.
## Compose
- swe-af-agent service added — builds from the real af-swe Dockerfile
(gh CLI + OpenCode + Claude Agent SDK + git). Registers as node_id
`swe-planner` with 34 reasoners (build, plan, execute, resolve,
resume_build, plus 29 specialist reasoners: run_coder, run_qa,
run_code_reviewer, run_github_pr, run_ci_fixer, run_replanner, etc.).
- shipwright-agent (the stub) stays running for fast UI iteration.
- swe-af-workspaces named volume persists build artifacts.
## handler.py — sync + async modes
- `SHIPWRIGHT_AGENT` env var selects the agent (default
`swe-planner.build`; `shipwright-v2.execute_task` for the stub).
- `SHIPWRIGHT_MODE=async` posts to `/api/v1/agents/async/<name>` and
polls `/api/v1/runs/{run_id}/agentfield` every 5s for up to 1 hour.
This is the only path that works for real SWE-AF builds (which take
minutes-to-hours).
- `SHIPWRIGHT_MODE=sync` keeps the original inline pattern for the
stub.
- `_build_payload()` translates the customer-app form into either
shape: the stub's `{payload: {...}}` or SWE-AF's `{goal, repo_url,
artifacts_dir, ...}`.
- `_persist_result()` handles both response shapes — coerces SWE-AF's
BuildResult schema (pr_url, repos, plan, diff) to the canonical
{status, summary, diff_preview, steps} the UI expects.
## .env additions
- SHIPWRIGHT_AGENT=swe-planner.build (default — the real SWE-AF)
- SHIPWRIGHT_MODE=async
## Verified E2E
Submitted "[real SWE-AF] hello docs" via the customer-app UI. The
workload module posted /api/v1/agents/async/swe-planner.build with
goal + repo_url. The runtime routed to the SWE-AF agent. The build
reasoner started, attempted git clone of the target repo, and failed
with `could not read Username for 'https://github.com'` — expected
because GH_TOKEN isn't set on this dev box.
Screenshots captured:
- operator-home.png — operator dashboard after first signup
- operator-agents-with-sweaf.png — Build > Agents tab
- agentfield-with-swe-planner.png — AgentField shows 4 build runs
(real SWE-AF) + execute_task runs (stub) side by side
- agentfield-swe-af-run-detail.png — full run detail in AgentField's
UI showing the actual git clone error from the SWE-AF pipeline
## Operator credentials
- Customer: demo@shipwright.test / DemoPassword123!
- Operator: admin@af-stack.test / AdminPassword123!
## To actually complete a SWE-AF build
Set these in .env then `docker compose up -d swe-af-agent`:
- ANTHROPIC_API_KEY (or CLAUDE_CODE_OAUTH_TOKEN) — for Claude Code
- GH_TOKEN — for git clone + PR creation
- OPENROUTER_API_KEY — for the harness model fallback
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Remove obsolete planning notes under development/discussions - Archive prior discussion drafts under development/_archive - Add UX journey notes under development/ux - Stage codex run screenshots under .codex-screenshots
Strip the existing admin shells and prepare for a backend-driven UI rebuild. Preserve API clients, auth, runtime proxy, and middleware. Dashboard: - Remove (admin)/, old/, components/new-admin/, components/layout/, guided-tour, dashboard-plugins-list, lib/new-admin/, lib/nav.ts, and the cost-explorer example plugin - Add shadcn sidebar-08 block (app-sidebar + nav-* + ui primitives) - Promote the generated dashboard page to src/app/page.tsx so the existing middleware auth gate covers it Customer-app: - Remove (app)/, components/layout/, guided-tour - Add shadcn sidebar-08 block at src/app/dashboard/page.tsx (middleware already redirects "/" -> "/dashboard") Both apps: - Drop unused driver.js dependency and CSS import - Update shadcn primitives that the block ships fresh versions of Pages that will rebuild on top of lib/api.ts are not in this commit.
Frontend (apps/dashboard): - Theme module: globals.css extended with semantic CSS vars (spacing-page-x/y/section/stack/inline/tile, radius-tile/pill, text-eyebrow/meta/body/kpi-value, success/warning) wired through @theme inline so JSX consumes Tailwind utilities only. lib/theme.ts mirrors the non-className values (motion timings, chart sizing, polling cadence, layout constants). - Home shell built from standard shadcn primitives only (Card, Badge, Tooltip, Skeleton, Separator, ScrollArea, Alert, Chart). Monochrome zinc; status colour only on KPI dots, severity chips, service pills. - Sections: KPI strip (8 tiles with sparklines), unified Activity feed, 4 Quick actions, Backing services strip with admin_url deep links. - Welcome block stubbed (deferred per home.md §20). - Server-component data loader does Promise.allSettled over 6 endpoints; partial failure renders degraded sections rather than crashing the page. Backend (services/runtime): - home/overview extended with live_runs, failed_runs_last_24h, budgets_aggregate (tenants_at_risk, avg_consumed_pct, tenant_count). - queue_depth field is now populated (was declared but unused). - recent_webhook_deliveries populated from suite_webhook_deliveries with direction/status enum mapping. - New GET /api/v1/admin/events endpoint: server-side merge of runs, webhook deliveries, system alerts, activity log into a typed union sorted DESC by occurred_at, with limit + kind filter. Schemas (zod): HomeOverviewSchema extended, BudgetsAggregateSchema + AdminEventSchema + AdminEventListSchema added, api.admin.events.list method added. Tests: TestHomeOverviewEmptyDeps extended with new field checks; TestAdminEventsEmptyDeps / RespectsLimitParam / KindFilterEmpty added. All 265 server tests green; pnpm tsc --noEmit clean; Next.js production build succeeds. Docs: development/ux/implementation-status.md created (running ledger). required-backend-gaps.md updated with closed status per gap. Closes gaps 1, 2, 3, 4, 5, 7 from required-backend-gaps.md. Gaps 6 and 8-11 belong to layout / Inbox briefs and stay deferred.
A bash CLI that signs in once, caches the session cookie + an LLM API key, and exposes subcommands that fire one real request each at the runtime so Home tiles tick in response. Designed to be reusable across every page brief we build. Subcommands: status, events, services — read-only diagnostics setup, login, reset — auth lifecycle llm, fail, job, webhook, activity, budget — drive one tile per call all, loop — combined / continuous traffic Implementation notes: - Uses /usr/bin/curl explicitly to bypass the RTK shell wrapper. - Talks to the runtime directly for read-only calls (no auth needed for most admin/* reads); routes write calls through the dashboard's same-origin /api/v1/[...path] proxy so the better-auth session cookie flows to the runtime as the operator credential. - LLM calls use a Bearer api key issued through /api/v1/admin/keys and cached at /tmp/backai-demo-apikey.txt.
Addresses the hard critique end to end: the page no longer reads like a
starter template. Single-row dense KPI strip with deltas + status-aware
sparklines, persistent top bar with live anchors, hairline-divided
activity feed, flat quick-action list, monochrome service pills, and a
welcome block surfacing the dev tenant key + try-it snippet.
Backend (close gaps 6 + new demo seeder):
- New GET /api/v1/admin/anchors — unified Inbox / Cost / Health values
the top bar polls on every page (closes Gap 6).
- New POST /api/v1/admin/demo/seed — one-shot seeder that inserts ~200
gateway requests, ~20 cost events, ~6 webhook deliveries, ~30 activity
rows spread across the last 24h. Idempotent (reset=true wipes prior
demo-flagged rows). No mocking — real rows in real tables, marked so
follow-up runs stay clean.
- home/overview gains request_delta_pct, error_delta_pct, cost_delta_pct
(server-side, avg-of-last-4 vs avg-of-prior-4 buckets) so KPI tiles
render real trend arrows instead of decorative sparklines.
Frontend — app shell:
- (dashboard) route group with its own layout — SidebarProvider, real
sidebar nav grouped by IA pillars (Anchor / Activity / People / Build
/ Platform), and a persistent TopBar.
- TopBar: SidebarTrigger · wordmark · tenant switcher · anchors
(Inbox / Cost / Health pills with live dots) · ⌘K trigger · theme
toggle · notifications bell · profile menu. Anchors poll every 5s.
Frontend — Home rebuild:
- KPI strip: 8 dense tiles in one row, hairline-divided, no card-per-tile.
Each tile shows [dot] LABEL · VALUE · delta-with-arrow · sparkline on
a single line. Status colours strictly: zero is idle (not green-go),
thresholds match page-design-framework §8. Per-tile good-direction
drives delta arrow tone (RPM up = success, errors up = destructive).
- Sparklines: status-coloured stroke, no decorative gradient. Empty
series renders a dashed baseline so layout never shifts between tiles.
- Activity feed: severity-bubbled list (critical → warning → info, then
newest first), one row per event with dot · mono timestamp · source
icon · kind · message · drill arrow on hover. No subhead.
- Quick actions: flat dense link list with shortcut hints. No big
illustrated icons; no marketing copy.
- Backing services strip: compact pills with trimmed versions ("v16",
not "v16.14 (Debian 16.14-1.pgdg12+1)"). Status dots distinguish
healthy (ok) from configured-but-not-probed (idle). Arrow only when
admin_url is set.
- Welcome block: dev tenant key reveal + curl snippet, dismissible.
- Live ticks: 5s polling of home/overview + admin/events; the layout
polls anchors. KPI value transitions use motion.tick.
Theme expansion:
- Denser default spacing across the board (row-y, row-x, strip, page-x/y).
- Added size-icon-inline and size-icon-dot tokens so JSX never hardcodes
px values for icons.
- text-kpi-value sized down to 18px so eight tiles fit one row at xl.
Sidebar:
- Replaced sidebar-08 placeholder nav (Playground/Models/etc.) with
real IA derived from journeys-v1: Home/Inbox/Cost/Health/Playground
at top, then Activity/People/Build/Platform groups.
Scripts:
- ./scripts/demo-traffic.sh gains `seed` and `anchors` subcommands.
`setup → seed → status` is the canonical first-run path.
Verification:
- pnpm exec tsc --noEmit clean.
- Next.js production build clean.
- 5 Home + AdminEvents server tests still passing.
- Live verified: 11% error rate, $0.07 cost today, 16 failed-24h, 20-row
activity feed, healthy anchors after `./demo-traffic.sh seed`.
Deferred (still): Cmd+K palette content, notifications panel content,
profile menu beyond sign-out, WebSocket upgrade, activity row drill
drawers (per-page briefs to come).
Three concrete fixes against the visual review: 1. KPI tiles overflow into neighbour cells Root cause: prior tile was a horizontal flex with the value-text competing with a fixed 80px sparkline. On narrow widths Recharts' intrinsic width pushed the sparkline past the cell boundary; combined with `divide-x` it visually leaked across the divider. Fix: vertical-stack tile (dot/label, value, delta, sparkline, sublabel). Sparkline now fills the tile's full width via Recharts ResponsiveContainer + `overflow:hidden` on the row. Strip also wraps the whole ul in `overflow-hidden` and replaces `divide-x/y` with explicit per-cell borders so wrap behaviour is unambiguous across breakpoints (2/4/8 cols). 2. Activity feed needs fixed height + internal scroll Was: section grew with content, pushing the right column down. Fix: section is `height: 480px` with the row list wrapped in the shadcn ScrollArea primitive. Header stays pinned; only the list scrolls. 3. Top bar wasn't sticky and items felt scattered Fix: `sticky top-0 z-30 bg-background/95 backdrop-blur`. Left/right are now explicit grouped flex containers with internal `gap-inline`, joined by visible vertical `Separator`s, so the wordmark, anchors, and chrome icons sit in distinct visual lanes. Empty tenant list now renders a quiet "default tenant" badge instead of a missing slot. Also bumped the home shell to refresh the backing services strip on the same 5s polling cadence as overview + events. Verification: pnpm exec tsc --noEmit clean, next build clean, demo seed re-fires correctly (17 failed runs / $0.066 cost / 1.9% budget after re-seed).
Three targeted fixes from the third visual review:
1. Top bar
- Switch Sidebar variant from "inset" to "sidebar" so the inset is
flush with the viewport edges (no 8px top margin from
md:peer-data-[variant=inset]:m-2).
- Drop both vertical Separators. The left/right groups speak for
themselves through spacing; the dividers were creating visual
noise rather than structure.
- Remove the profile menu entirely per request (no "BA" avatar). Sign
out moves to /api/auth/sign-out and will be reachable from the
settings page when that brief ships.
- Tighten the bar from h-14 to h-12 — it was a hair too tall for the
dense console aesthetic.
2. Right-column height
- Activity feed exposes ACTIVITY_FEED_HEIGHT_PX as a named constant.
- Home shell sets the right-column container to that exact height.
- Quick actions stays `shrink-0` (natural height — 4 rows + header);
backing services becomes `flex-1 min-h-0` and wraps its pills
inside a shadcn ScrollArea so it fills the remaining space without
overflow.
- Net effect: left and right columns end on the same baseline; no
ragged bottom edge.
Verification: tsc clean, next build clean, dashboard rebuilt.
rotate-key button The previous reveal swallowed errors in a bare try/catch so any 401 / network blip looked like "click does nothing". Rewritten with explicit state: - Reveal click issues a fresh key via /api/v1/admin/keys with credentials: "include" so the better-auth session cookie always flows. - Any failure (HTTP non-2xx, missing `value` field, clipboard blocked) is surfaced as an inline destructive banner with a dismiss control, not silently dropped. - Loader2 spinner while the issue request is in flight. - Separate copied-state booleans for the key copy button and the snippet copy button — both work independently with their own check feedback. - New rotate button (RefreshCw) appears once a key is revealed; clicking it issues a new key, swapping the displayed one. The old key stays valid on the runtime until manually revoked. - Snippet now embeds the actual revealed key (not the $BACKAI_API_KEY placeholder) once Reveal has run.
Root cause of the OPERATOR_AUTH_REQUIRED 401 on Reveal: the dashboard middleware (middleware.ts) only checks that the better-auth session cookie EXISTS, not that it's valid against the DB. After a dashboard restart or a session-table wipe the browser still holds the cookie and gets past middleware, but the runtime's resolveOperatorPrincipal can't match it to a session row and rejects. Fix: - Recognise 401 specifically and raise a SESSION_STALE: prefixed error rather than dumping the raw HTTP body. - Error banner detects the SESSION_STALE prefix and renders a "Sign out & back in" link instead of the dismiss button. The link calls /api/auth/sign-out with redirect_to=/login?next=/ so the operator lands back on Home with a fresh session. No backend change — the runtime's behaviour is correct; the dashboard just needed to handle the case gracefully.
Root cause of the recurring OPERATOR_AUTH_REQUIRED: the middleware only
checks the better-auth session COOKIE exists, never validates the token
against the session table. After any dashboard restart (or session
table wipe) the browser still holds the old cookie and gets past
middleware, but every runtime call returns 401.
The right fix lives in the layout, not the middleware (which runs on
Edge and can't cheaply touch the DB):
app/(dashboard)/layout.tsx now calls requireOperator() which:
- reads the session via auth.api.getSession({ headers })
- confirms the user exists in suite_operators
- redirects to /login when either check fails
Both checks happen once per page navigation on the server, which is
where DB calls belong. The welcome block's SESSION_STALE handling
remains as a belt-and-suspenders fallback for client-side calls that
race with a server-side expiry.
Net effect: stale cookies trigger a clean redirect to /login without
any manual sign-out. Operator lands back on Home the moment they
re-authenticate.
Decision queue per development/ux/pages/inbox.md. v1 scope: HITL approvals + AgentField/DB unhealthy probes, client-side merged. Gaps 8/9/10/11 deferred per brief's partial-scope plan; mitigations recorded in required-backend-gaps.md. Decisions locked: - 30s polling, no WebSocket - Filter chips on severity + kind, URL-persistent - Badge = total pending count; red when any item is critical - Approval detail in right-side shadcn Sheet drawer (URL state ?item=) - Severity-tiered ordering (critical → warning → info), newest first - Affirmative "All clear" empty state Backend (admin/anchors): - Added inbox_has_critical for badge colour - inbox_pending now counts system alerts too - Pending-approvals query uses app.bypass_rls=on; the prior store call was tenant-scoped and silently returned 0 (badge never moved)
The active filter chip rendered white-on-white in dark mode. Root cause: I composed shadcn's Button(variant=default) with a nested Badge(variant=secondary) to fake a chip. shadcn variants are tuned per-component, not per-composition — an inverted-surface Button with a nested Badge is not a tested pattern, and class-merge order left the label colour unreliable across themes. Fix: - New primitive at components/ui/filter-chip.tsx owns BOTH halves of the active state (chip surface + count pill) in one place. All colour comes from semantic tokens — no raw hex, no variant-default magic, no cross-component inheritance. - Active state is an intentional, auditable inversion: bg-foreground + text-background. The count pill uses bg-background/20 + text-background so it reads against the inverted chip background by construction. - Inactive state is border + bg-card + muted text, with the label brightening on hover. - inbox/filter-chips.tsx becomes a thin domain wrapper that maps inbox-specific options onto the primitive without re-styling. Rule going forward: any selectable / inverted-surface UI gets its own primitive. Don't reuse a Button variant when the semantics differ — make the new primitive and own the contrast explicitly.
Previous chip used h-7 + text-meta font-medium + h-4 count pill, which read as a button-sized control. Filter chips are navigation, not the primary affordance — they should be visually subordinate to the list rows below. - Chip height: h-7 -> h-6 (28px -> 24px); add leading-none so the meta-size font doesn't introduce extra vertical air. - Chip weight: drop font-medium so the label has the same weight as body text (the active inversion already provides emphasis). - Count pill: h-4 -> h-3.5, text-meta -> text-eyebrow, px-1.5 -> px-1. The count is metadata; it shouldn't compete with the label. - Active count halo: bg-background/20 -> bg-background/25 for a touch more contrast on the inverted surface.
4-zone page per development/ux/pages/cost.md, sized to v1 scope. Gaps 12-19 deferred per brief §24; each v1 mitigation recorded in the gaps ledger. Zones shipped: - Zone 1: Period total + forecast tiles with sparkline/forecast bar; client-side anomaly heuristic (top-share spike + forecast overrun) - Zone 2: Tenant → Model hierarchy (2 levels); agent + reasoner depth surfaces a v0.2 note inline so the partial scope is intentional - Zone 3: Stacked area + top-N tenant list; per-tenant fan-out (top 5 /cost?tenant=X calls, shared with Zone 2 to avoid duplicate work) - Zone 4: Cache donut + cost-by-model share bars + budgets table with edit Dialog (PUT /admin/budgets) + Sonner toast Decisions locked: - 10s polling (theme.polling.services) — cost moves slower than KPIs - Range chips Today/7d/30d/90d, URL-persistent (?range=) - All chips use the FilterChip primitive established for Inbox — same contrast + density - Adapter footer surfaces the LiteLLM pill + dropdown (Open admin, Change adapter) per framework Part 9 New tokenised primitives in components/ui/: - Sparkline, DeltaIndicator, ForecastBar, GaugeBar All use semantic CSS variables — no raw hex, no per-component overrides. Backend touched: - api.cost(params) extended with optional tenant filter (was already backend-supported; client wrapper just didn't expose it) - top-bar Cost AnchorPill now links to /cost (Inbox already linked) No new runtime endpoints required. 266 server tests still green.
Page-design-framework principle #2 — "should feel dense even at zero data" — drove every change here. The page now reads as a real platform that's waiting for traffic, not a half-built skeleton. Top bar: - Cost anchor renders a DeltaIndicator (today vs same window yesterday). Required a new backend scalar — cost_yesterday_same_window_usd added to /api/v1/admin/anchors so the delta lands without a second round trip. Demo seed widened from 24h to 48h so the anchor delta has data to compute. - Tenant switcher truncates auto-generated slugs to a friendly prefix. Full slug + display name still appear in the dropdown row. Sparkline primitive: - Auto-switches to a BarChart fallback when data has fewer than 4 points; the dashed-underline "empty" frame became a real chart frame with a baseline + dots so it reads as "shape coming" instead of "nothing here". Zone 1 (At a glance): - AllClear card replaces the "Nothing unusual." footnote: positive card with check icon + spend-shape sparkline. - TodayTile now uses the new Sparkline so a 1-2 point series renders as visible bars. Zone 2 (Explain spend): - Neutral muted-foreground/30 share bar. Green was reserved for status semantics; a hierarchy proportion isn't a success signal. - Bar hidden entirely when N=1 (a full bar at 100% read as alarm). - Single-tenant row auto-expands so the model breakdown is the actual content; first-row auto-expand at N>1. - Model names formatted (last segment of the slash path, friendly casing per provider). Zone 3 (Explore): - Group-by chips visible with Tenant active + Agent/Model/Day disabled (v0.2, gated on Gap 16). Operators see the surface, not blank space. - "Other" stack suppressed when no remainder or when N=1 to avoid the monochrome muddy-blend. - Per-series fill opacity ramps so stacked tenants stay distinct in monochrome dark mode. Zone 4 (Inference economics): - New BreakEvenPlaceholder card with a stylised diagonal so the Cost÷MRR frame reads as a chart, not a void. Copy directs operators to the billing adapter (Gap 17). - Cost-by-model bars thickened to h-3, with friendly model name + provider sub-label. - Cache donut renders even with zero traffic — placeholder slice + "—" numbers so the frame doesn't collapse. Zone wrapping: - New ZoneCard / ZoneCardHeader primitive. Every zone is now a clearly bordered card with a heading row, so section hierarchy reads at a glance instead of relying on uppercase eyebrow text on flat black. AdapterFooter: - Promoted from text-meta footer line to a bordered strip with a dot+border LiteLLM pill. Modularity signal can no longer be missed. Sidebar: - Playground removed from pinned anchors (per-agent surface; lives in Build). - Activity gains Cache; Webhooks renamed to Webhook flow. - People gains Sessions / Budgets / Activity log / OAuth connections. - Build gains Reasoners / Tools / Harnesses / Crons / Playground / Feature flags / API explorer / Shipwright. - Platform gains Containers; API explorer demoted. - New v0.2 stub mechanism: items without a groomed page render dimmed with a v0.2 chip badge. Sidebar communicates the platform's surface area instead of hiding it.
…me at /health 5-zone page per development/ux/pages/health.md, sized to v1 scope. Gaps 20-24 deferred per brief, each v1 mitigation recorded in the gaps ledger. Zones shipped: - Zone 0: Incident banner with affirmative All-clear / degraded variants — same height across both so the page doesn't jump on transitions - Zone A: LLM providers grid (3/2/1 responsive cols) with status dot + uptime + p95 + sparkline + Switch-fallback link when degraded; EmptyProvidersCard when none configured - Zone B: Connected services grouped by kind (Runtime / Data / Intelligence / Storage / Queue / Delivery / Observability), each group its own sub-card; rows are click-out to OSS admin UI when admin_url is present (Block 0 decision #4 — Health is THE hub) - Zone C: Database — 5 sub-cards (Connections gauge / Cache donut / Slow queries table / Largest tables bars / Vacuum). Every sub-card renders structure at zero data — no blank rectangles - Zone D: Runtime self — Version / Uptime / Memory / Goroutines tiles + HTTP summary + Top routes table Decisions locked: - 10s polling (theme.polling.services) — DB queries are expensive - Provider window chips 24h / 7d / 30d, URL-persistent (?window=) - Manual Refresh button in the header, toasts on success - Overall summary computed client-side; drives the Incident banner. v0.2 swaps for backend-emitted incident events (Gap 20) Reused primitives: - ZoneCard / ZoneCardHeader promoted from components/cost/ to components/ui/ (Cost re-exports the shim for the old import path) - FilterChip / FilterChipGroup for window selection - Sparkline + GaugeBar from prior pages - Sonner toast on refresh Backend touched: - Top-bar Health AnchorPill points at /health - Sidebar Health removed from the v0.2 coming-soon set - No new endpoints — /admin/services, /admin/llm/provider-health, /admin/db/health, and /metrics/summary were all in place 266 server tests still green.
Addresses critical (A1–A5), layout (B1–B3), component (C1–C6), and sidebar (D1) items from the brief's "v0.1 → v1 Corrections" appendix. A1 — Zone A now shows upstream providers, not the adapter - health_poller now parses LiteLLM's /health response and emits one row per upstream provider per poll cycle (openrouter / anthropic / openai / google / deepseek / qwen / ...) instead of a single "litellm" aggregate. Falls back to the aggregate row only when the response can't be parsed. A2 — Status threshold logic (semantic correctness) - deriveProviderStatus() applies the brief's threshold table client-side: healthy = uptime ≥ 99% AND p95 ≤ 1s; degraded if either signal slips; down if both fail. ProviderHealthCard now uses the derived status so a green pill never sits next to red latency. A3 — AgentField belongs in INTELLIGENCE - mapKindToGroup() collapses llm-gateway, llm, reasoning, agent-runtime into the INTELLIGENCE bucket. Billing + Notifications now route into DELIVERY (per brief taxonomy). A4 — Recovery actions on provider cards - Provider card always renders "Open LiteLLM ↗"; "Switch fallback" shows when status ≠ healthy. Operator never sees a degraded provider without inline recovery affordance. A5 — [Open ↗] on every service row with admin_url - ServiceHealthRow renders an outline Button "Open ↗" right-aligned for any service that ships an admin URL. Per Block 0 decision #4 (Health is the central OSS-link hub). B1 — Provider card layout at N=1 - ProviderHealthCard gained a `layout="wide"` mode used by the ZoneAProviders grid when providers.length === 1. Three metrics (Uptime / p95 / Median) row + taller sparkline + footer actions. B2 — Three-tier section header weight - ZoneCardHeader bumped to text-lg font-semibold so zone titles dominate; sub-card titles remain text-body font-medium; tertiary labels stay text-eyebrow uppercase. Cost + Health both benefit. B3 — DB sub-sections in their own sub-cards - SubCard primitive already in place; padding tightened so the gap between Connections / Cache / Slow queries / etc. is visible. C1 — Connections stacked segment bar - ConnectionsCard now renders a single h-3 bar with three segments (active / idle / free) plus 4 stats above (Active / Idle / Free / Max). Operator sees pool saturation + warm/cold ratio at a glance. C2 — Cache donut sample threshold - CacheCard treats hit_ratio = 0 or = 1 as "small sample" and renders "—" with a caveat instead of a perfect-looking donut on a cold cache. C3 — Slow query truncate + DDL filter + HoverCard - 60-char truncate with full-query title tooltip. DDL statements (CREATE / ALTER / DROP / TRUNCATE / GRANT / REINDEX / VACUUM / ...) are filtered by default with an "Include DDL (N)" toggle. C4 — Consistent status indicators - Provider + service rows now follow the same pattern: dot on the left, status text on the right with semantic colour (muted-foreground when healthy, warning/destructive when not). C5 — Single timestamp convention - formatRelative() reserves "now" for genuinely sub-second age; everything else uses "Ns / Nm / Nh ago". C6 — Health anchor quiet when healthy - Top-bar Health pill hides the word "healthy" when status === healthy; text only appears when something needs attention. D1 — Sidebar v0.2 badge cleanup - The dim + tooltip + route-to-home behaviour stays so operators see the platform's surface area, but the visible "v0.2" chip is dropped. Operators no longer read the badge as "feature missing".
List + drawer composition per development/ux/pages/runs.md, sized to v1 scope. Gaps 27-31 deferred per brief, each mitigation recorded in the gaps ledger. Shipped: - Sticky filter bar: status + time chips (FilterChip primitive), debounced-style search input. Status counts on each chip. - Table: sticky header always rendered (even at zero data), skeleton loader rows, empty + degraded copy. Failed/running rows pick up a 4-px left border accent (destructive/warning). - Row: status dot + time + agent + error/id + tenant prefix + duration + cost + status text. Chevron on hover. - Drawer: shadcn Sheet with status dot in title, meta tiles (status/duration/cost/tenant/started/id), error block, Input + Output JSON, Cancel/Pause/Resume actions, "Open in AgentField" link. - URL state: ?status=, ?time=, ?search=, ?drawer=<runId>. - Polling: 5s tick (theme.polling.home) — closest the dashboard gets to a live feed without WS. Client-side fallbacks for unshipped backend filters: - Time range (Gap 27) — applyClientFilters() in lib/runs/derive.ts - Search (Gap 28) — substring match across id/agent/tenant/error - Trigger column intentionally absent until Gap 31 lands Deferred to v0.2 (per brief): - Group-by tenant/agent/status/hour - Bulk cancel / saved views / compare runs - WebSocket subscription - Trigger pill (depends on Gap 31) Sidebar: Runs item removed from the v0.2 dim set so it routes to the real page.
Addresses MUST FIX items from the v0.1 review of /activity/runs. Backend: - runs handler joins suite_tenants so each row carries tenant_name in addition to tenant_id. Default tenant now renders as "Default". - run_agentfield response gains ui_url alongside the existing details_url. The previous DetailsURL pointed at the JSON API endpoint, so "Open in AgentField" opened raw JSON in a browser instead of the UI page. Operators land on /executions/<id> now. Frontend (Runs row): - Tenant column shows tenant_name as primary text with the truncated id as muted mono below. Full id visible on hover via the row title tooltip. Frontend (RunDrawer rewrite): - shadcn Tabs (Input / Output / Errors / Audit). Errors tab only renders when the run has an error. Steps + Tools tabs deferred — AgentField timeline data isn't surfaced in v1. - Quick facts grid now uses formatRunAge for STARTED (with absolute ISO in the title tooltip). ID + Tenant fields ship with the new CopyButton primitive. - Related links section above the actions footer — Tenant detail + Agent detail. Parent-run link deferred until backend exposes the relation. - Keyboard nav: ← / → move between rows in the current filtered view; Esc closes (Sheet default). Hint bar at the bottom shows the available shortcuts plus Pause / Cancel keys when applicable. - Open in AgentField uses ui_url when the backend returns one; falls back to NEXT_PUBLIC_AGENTFIELD_URL + execution id. - Empty payload now shows "No input/output payload recorded for this run" inside a dashed-border block instead of a bare em-dash. New primitive: - components/ui/copy-button.tsx — single-click clipboard control with the standard "Copy" / "Copied" affordance. Reusable across future drawers.
Composition page per development/ux/pages/tenant-detail.md, sized to v1 scope. Gaps 33-35 deferred per brief, each mitigation recorded in the gaps ledger. Shipped: - Sticky header: identity + at-risk callouts + 5 KPI tiles (Cost 30d / Requests 30d / Members / Keys / Storage) + Suspend / Reactivate actions. KPI tiles reuse the Sparkline primitive. - 4 tabs: Overview / Members / Keys / Settings. URL-persistent via ?tab=. Tab triggers carry live count badges where useful. - Overview tab: client-side merged activity feed (recent_runs + recent_webhooks chronological — Gap 35 mitigation) + recent-runs list with status accents, both linking to /activity/runs filtered to the tenant. - Members tab: list with role badge (owner / admin / member tones) + email + last-active. Mutations (Invite / Remove) deferred. - Keys tab: card per key with status dot + masked prefix + CopyButton + rate limits + GaugeBar against budget cap + Revoke action. - Settings tab: Identity form (Save name) + Danger zone (type-to-confirm Delete tenant). Suspend / Reactivate live in the header so the operator can act without first navigating in. - At-risk computation client-side (Gap 33 mitigation): recent-run failure rate + per-key budget overrun + trial expiry. Each signal becomes an AtRiskAlert card; the page status flips to warning / destructive when warranted. - Minimal /people/tenants list hub linking every tenant to its detail page. New primitive: - AtRiskAlert (slim Alert with warning/critical accent — reusable on future User detail / Agent detail surfaces). Backend: no new endpoints — api.tenantDrilldown(id) hydrates header + Overview + Members + Keys in a single fetch. Tenant + key mutations reuse existing PATCH/DELETE endpoints. Sidebar: Tenants lifted from the v0.2 dim set.
This was referenced Jun 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
Adopts
feat/ui-redesignontomain: the operator-console redesign and themodular adapter / plugin-based connection system. This one branch contains the
full chain
supportdesk-first-dx (#118)→feat/modular-adapter-system→ theUI redesign, authored by @santoshkumarradha. It merges conflict-free (current
mainis fully contained as an ancestor).Scope (788 files, +68k / −20k)
Activity/Runs · People/Tenants. Replaces the old Build/Infra/Operate nav.
user-authorable, plugin-based connection system (swap providers behind an
interface).
telemetry write hooks and history endpoints.
Validation
CI is currently
workflow_dispatch-only, so this was validated locally:go build ./...✅ ·go vet ./...✅go test ./...✅ except two pre-existing, environment-dependent tests inservices/runtime/internal/server(TestRunAgentFieldAFUnreachable,TestHandleRunsSubscribeAFUnreachable). They fail identically onmainina sandboxed network (dialing
127.0.0.1:1returns 504 instead of 502 becausethe port doesn't RST instantly) — not introduced by this branch; they pass
under normal CI networking.
pnpm --filter dashboard build✅ — every redesign route compiles.Consolidation
nav, so restoring it is moot.
Planned follow-ups after merge (separate PRs): CLI-first binary fix, CLI
initscaffold + anonymous opt-out telemetry, flip CI to
on: [push, pull_request],positioning/docs truth pass, and regenerate the dependabot set against the new
main.🤖 Generated with Claude Code