Build a web frontend for the Drydock workbench. The API and CLI exist (Phase 1). Now we need a visual surface.
Goal: Open a browser, see items by status, see what agents are doing, and manage work.
- Framework: Next.js 15 (App Router)
- UI: shadcn/ui + Tailwind v4 — initialise with:
bunx --bun shadcn@latest init --preset b1sSMVhlA --template next - Runtime: Bun
- Real-time: WebSocket (Bun native on the Hono API)
- Deploy: Same Docker Compose stack — new
webservice on port 3001
The Hono API runs on port 3000. Key endpoints the frontend consumes:
Items:
GET /items— list with query params:status,priority,tag,parent_id,created_by,sort(created_at|updated_at|priority),direction(asc|desc),limit,offsetGET /items/:id— detail includingtags,recent_runs,dependencies,dependents,blockedflagPOST /items— create:{ title, status?, priority?, description?, parent_id? }PATCH /items/:id— update fieldsDELETE /items/:id— soft delete (sets status to "dead")GET /items/:id/children— child itemsGET /items/:id/changelog— field-level audit trail
Tags:
GET /tags— list allPOST /tags— create:{ name, color? }POST /items/:id/tags— assign:{ tag_id }or{ tag_name }DELETE /items/:id/tags/:tagId— remove
Agent Runs:
GET /items/:id/runs— list runs for itemPOST /items/:id/runs— create:{ agent, branch?, status?, pr_url?, ci_status?, notes? }PATCH /runs/:id— update run
Dependencies:
POST /items/:id/dependencies— add:{ depends_on: <item_id> }DELETE /items/:id/dependencies/:dependsOnId— removeGET /items/:id/dependencies— items this depends onGET /items/:id/dependents— items depending on this
Enums:
- Item status:
idea,speccing,ready,building,evaluating,shipped,parked,dead - Item priority:
critical,high,medium,low,none - Run status:
running,succeeded,failed,cancelled - Run CI status:
pending,passed,failed,unknown
Actor header: Mutating requests should include X-Drydock-User header (e.g. "charl", "clawdysseus") for the changelog trigger.
- Items have a flat structure with optional
parent_idfor lineage - Items can have dependencies (item A depends on item B)
blocked: trueon item detail means at least one dependency isn'tshipped- Agent runs track coding agent execution per item (agent name, branch, PR URL, CI status)
- Changelog is automatic via DB trigger — every field mutation is logged
- Run
bunx --bun shadcn@latest init --preset b1sSMVhlA --template nextin a newweb/directory - If the preset creates a standalone project, that's fine — integrate it into the repo's
web/dir - Add
webservice tocompose.yaml(Bun runtime, port 3001) - Configure Next.js rewrites to proxy
/api/*tohttp://api:3000/*(they're on the same Docker network) - Set dark mode as default theme
- Add a basic layout with header showing "Drydock" title
Done when: docker compose up --build starts all 3 services, frontend loads at localhost:3001, /api/health returns OK through the proxy
- Default view at
/ - Fetch all items from
GET /items?limit=100(pagination later) - Group into columns by status:
idea|speccing|ready|building|evaluating|shipped parkedanddeadhidden by default (add a toggle to show them)- Each card shows:
- Title
- Priority badge (color-coded: critical=red, high=orange, medium=yellow, low=blue, none=gray — use shadcn chart colors)
- Tags as small chips
- If the item has agent runs: show latest run's agent name + status icon (spinner for running, checkmark for succeeded, x for failed)
- If
blocked: true: show a lock icon or "Blocked" badge
- Within each column, sort: unblocked before blocked, then by priority (critical first)
- Quick-create: button or input at the top that creates an item with just a title (status defaults to
idea) - Clicking a card opens the detail panel (task 2.4)
- Empty columns show a muted placeholder
Done when: Board renders with correct columns, cards show all info, quick-create works, cards are clickable
- Available at a toggle (board/list switch in the header area)
- View preference saved in localStorage
- Use shadcn's DataTable pattern (or similar)
- Columns: title, status (badge), priority (badge), tags, agent (latest run agent name), CI status (badge), last updated (relative time)
- Sortable columns (click header to toggle sort)
- Filter bar: status dropdown, priority dropdown, tag selector (multi-select)
- Clicking a row opens the same detail panel as the board view
- Pagination controls at the bottom
Done when: Table renders all items, sorting and filtering work, row click opens detail, view toggle persists
-
Slide-over panel (right side) or full modal — triggered by clicking a card/row
-
Sections:
- Header: Title (editable inline), status dropdown, priority dropdown, blocked badge if applicable
- Description: Textarea with markdown preview/toggle. Saves on blur or explicit save.
- Tags: Current tags with X to remove, input to add (autocomplete from
GET /tags) - Agent Runs: Reverse-chronological timeline:
- Agent name (e.g. "codex") with a small icon/avatar
- Run status badge (running=blue spinner, succeeded=green check, failed=red x, cancelled=gray dash)
- Branch name (monospace)
- PR link (clickable → new tab)
- CI status badge (pending=yellow, passed=green, failed=red)
- Duration (started_at → completed_at, or "running for Xm")
- Notes (expandable/collapsible)
- Dependencies: List of items this depends on (title + status badge). Add button with item search/autocomplete. Remove button per dependency.
- Dependents: List of items depending on this one (title + status badge, read-only).
- Changelog: Collapsible section. Each entry: field name, old value → new value, actor, timestamp.
- Children: List of child items if any (title + status, clickable).
-
All mutations go through the API (PATCH /items/:id, POST/DELETE for tags and dependencies)
-
Include
X-Drydock-User: charlheader on all mutating requests (hardcode for now, no auth)
Done when: All sections render and are interactive, edits save via API, dependencies manageable
API side (modify api/src/index.ts):
- Bun's
serve()supports awebsockethandler alongsidefetch. Add WebSocket upgrade handling. - Maintain a
Set<ServerWebSocket>of connected clients - After every successful mutating endpoint (POST/PATCH/DELETE), broadcast a JSON message to all WS clients:
{ "type": "item.created", "data": { ...serialized } } { "type": "item.updated", "data": { ...serialized } } { "type": "run.created", "data": { ...serialized } } { "type": "run.updated", "data": { ...serialized } } { "type": "dependency.created", "data": { "item_id": N, "depends_on_id": N } } { "type": "dependency.removed", "data": { "item_id": N, "depends_on_id": N } } - Handle client disconnect (remove from Set)
Frontend side:
- Create a
useWebSockethook that:- Connects to
ws://<api-host>:3000/ws(direct to API, not through Next.js proxy) - On message: refetch/invalidate the affected queries (React Query
queryClient.invalidateQueries) - Reconnects with exponential backoff on disconnect
- Exposes connection status
- Connects to
- Add a small connection indicator in the header (green dot = connected, amber = reconnecting, red = disconnected)
- The Docker Compose
webservice needs the API WS port accessible — expose port 3000 to the web container or use Docker network hostname
Done when: Changes made via API/CLI appear on the frontend without refresh, connection indicator works, reconnection works after API restart
2.1 Scaffold → 2.2 Board → 2.3 List → 2.4 Detail Panel → 2.5 WebSocket
Commit after each task. Push when all are complete.