Rebuild the bundled web UI as a Next.js (React + TypeScript) static export
implementing the Claude Activity Dashboard design: a left-nav "Control Center"
hub over Runs, Repositories, Agents, Approvals, Git Servers, Activity, and
Shared, styled with the Leeworks design-system tokens (flat, dark, border-led).
The dashboard is a pure client of the existing API (same contract as curl):
the browser prompts for the token once, stores it in localStorage, attaches it
to every call, and renders all API values as React text so agent-authored
strings stay inert. Control actions enqueue a command and poll it to a terminal
state, matching the worker model.
The build output is committed to src/handler/api/static/ so the wheel ships it
and FastAPI serves it same-origin. app.py now mounts the export at "/" after the
API routers (a non-shadowing fallback: unmatched paths 404, no SPA rewrite).
UI-serving tests updated for the export; frontend source lives in frontend/.
Claude-Session: https://claude.ai/code/session_01ATgVWRjFzG8nHEnwgZpJWD
Co-authored-by: Claude <noreply@anthropic.com>
Make credentials/hosts, projects, agents, and approvals manageable from the
dashboard. The API and control layer are separate containers, so the API can't
run control actions directly (no git/tmux/claude, doesn't own the tmux
sessions). Instead the API enqueues a command and a worker in the control
container executes it and writes the result back.
Data model (migration 0003):
- `commands` queue/audit table; `forge_hosts` registry; `approvals` gains a
nullable approver id + `actor` so operator verdicts are first-class.
Control worker:
- `control/worker.py` claims commands and dispatches to the existing control
functions (spawn/kill/resume/record_approval/write_skills/poller.sweep),
plus a periodic CI sweep. New `handler worker` CLI subcommand; it becomes the
control image's default command (subsumes `poll-ci --watch`).
API:
- `require_admin` gate + `ADMIN_TOKEN`; project GET/PATCH/DELETE; agent
spawn/kill/delete; resume now enqueues (fixes a cross-container bug where the
API tried to send tmux keys to a session in the control container); new
approvals/commands/hosts routes; forge-init and poll-ci enqueue endpoints.
Credentials/hosts:
- host->token-env lookup consults the `forge_hosts` registry first (built-in
map is the fallback); `resolve()` refactored to a scheme dispatch reserving
`db:` for a future encrypted store. Web input restricts credential_ref to
env:/file:/db: (cmd: stays CLI-only — it would run arbitrary commands).
Dashboard:
- New tabs for projects, agents (spawn/kill with live command-status polling),
approvals, hosts, and an activity/audit view; shared context is now writable.
Tests: +33 (queue atomicity, worker dispatch, CRUD, hosts, admin gating,
cmd: rejection, host-aware credentials, and an API->queue->worker->spawn
end-to-end). README gains a Web management section.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CrhrBToauu4L2qG6jdnuFP
Add a no-build, same-origin web frontend so an operator can open a URL,
see every agent's state, and answer a paused question with no terminal
(Phase 3 DoD). The UI is a client of the existing API — no endpoint,
schema, or auth change — so the 106 existing tests pass unchanged.
- app.py serves the bundled UI from / and /static, gated on UI_ENABLED
(default on); optional CORS_ORIGINS (default empty => no middleware)
for hosting the UI on a separate origin. Dedicated /static prefix +
explicit / route so API routes are never shadowed. Zero new runtime
deps (StaticFiles/CORSMiddleware ship with Starlette).
- static/: vanilla fetch + plain CSS + vendored alpine.min.js (v3.14.8,
no CDN). Token captured once into localStorage; all API values render
via x-text (never x-html) to block agent-authored markup injection.
Project switcher, agent list, checkmark panel, paginated log, shared
feed, and Answer / Answer & Resume. Polling scoped to the selected
agent to avoid an N+1 over the fleet.
- config.py: ui_enabled, cors_origins (+ cors_origin_list); documented
in .env.example.
- tests/test_api_ui.py: serving, unauthenticated shell, non-shadowing
401 regression, CORS toggle, UI_ENABLED=false. 114 tests, ruff clean.
The static assets ship in the wheel by default (they live inside the
packaged src/handler tree) — no force-include needed.
Phase 2 configures forge for the agents (operator only sets a credential_ref +
optional version pin) and lets them drive a junior→senior→deploy workflow:
- Credential resolution/injection (control/credentials.py): credential_ref pointers
(env:/file:/cmd:) resolved only at spawn, injected as FORGE_TOKEN + host var, with a
forge-host-scoped git credential helper reading the token from env (never on disk / in
the DB). Resolution is a fail-fast spawn gate.
- Role-based forge skills committed into the managed repo (control/skills_gen.py,
`handler forge-init`): forge-junior/senior/deploy + a workflow overview.
- Hard approval gate (hooks/gate.py, approvals table, migration 0002): merge/deploy —
and direct pushes to protected branches — are denied unless a DIFFERENT agent has an
`approved` record for the branch, pinned to the reviewed commit (approved_sha). Senior
records verdicts via `handler approve`/`reject`.
- forge/git seams (control/forge.py, control/gitops.py) matching the Phase 1 seam pattern.
- CI status poller (control/poller.py, `handler poll-ci [--watch]`) backfilling
ci_status/ci_checked_at via `forge ci list`.
- Fix: migrations/env.py commits explicitly after run_migrations — pysqlite on Py 3.12+
was rolling back the final migration's DDL + alembic_version stamp (latent in Phase 1).
Reviewed via a separate code-reviewer pass; gate-bypass and credential-scoping findings
addressed. 106 tests, ruff clean, verified end-to-end against real git + migrations.
Implements the Phase 1 MVP from the README: a stateless control layer + HTTP
API over a centralized database, with hook-enforced test/push gates.
- DB layer: SQLAlchemy Core, one schema rendering both Postgres (BIGSERIAL /
TIMESTAMPTZ / JSONB) and SQLite (INTEGER PK / TEXT / JSON) via portable types;
native ON CONFLICT DO UPDATE checkmark upsert on both dialects.
- Alembic dual-dialect migrations (render_as_batch for SQLite); tests run a real
`alembic upgrade head`.
- FastAPI: projects/agents/checkmark/log/answer/resume + shared log/context
routes, single global bearer token, higher-trust token gating shared-context
writes, project isolation on every route.
- Hooks (`python -m handler.hooks <event>`): Stop test gate (block on red),
PreToolUse AskUserQuestion defer + `git push` gate (tests then throwaway
build), Notification generic webhook (no-op without WEBHOOK_URL). Identity via
env injected at spawn; verify is the mock seam.
- Control CLI: spawn/list/attach/kill, hard `.mise.toml [tasks.test]` gate,
generated per-agent settings.json, identity + DATABASE_URL injected via tmux;
tmux is the mock seam.
- 45 tests (SQLite), ruff clean. Live claude/tmux/mise spawning deferred behind
the mocked seams.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W5ZuS5pV1NS6eKsRZHXonY