feat(planner): assignment board (Kanban, drag-to-assign) — M18-7 #197
No reviewers
Labels
No labels
area:agents
area:dashboard
area:database
area:design
area:design-review
area:flows
area:infra
area:meta
area:security
area:sessions
area:webhook
area:workdir
security
type:bug
type:chore
type:meta
type:user-story
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
charles/claude-hooks!197
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "boss/168"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #168
Summary
/app/planner/boardsurface — Kanban-style view of the fleet's workload. One column per webhook-dispatchable agent type (boss / dev / reviewer / designer / design-reviewer), plus a synthetic Unassigned gutter fortype:user-storyissues with no assignee.busy/capacitysaturation (e.g.dev · 1/2 busy).POST /board/assign(service-held Forgejo token; the UI never touches Forgejo directly). Forgejo then fires the normalissues.assignedwebhook so dispatch routing stays in one place — no new dispatch code.Open in Forgejo,Task eventsdeep link, andCancelfor running tasks./eventstask-lifecycle envelopes invalidate the TanStack Query cache; 30 s polling backstops idle tabs.Architecture
packages/shared/src/board.tsownsBoardResponse,BoardColumn,BoardCard,BoardAssignRequest/Response.apps/server/src/board.ts::buildBoardderives the snapshot from the worker registry + livelistRepoIssuescalls across every configured repo. 5 s cache, Forgejo dep-injected for tests.main.tsregistersGET /board(open) andPOST /board/assign(operator-gated), injecting aprobeBoardWorker(name)closure over theworkersmap.components/board/*tree (board.tsxorchestrator,board-column.tsx,board-card.tsx,board-filters.tsx,board-side-panel.tsx, purefilter-logic.ts). Route lives atapps/web/src/routes/planner.board.tsx; the oldplanner.tsxis split intoplanner.tsx(pathless layout) +planner.index.tsx(existing architect chat UI) so/planner/boardnests cleanly — mirrors the/monitorsplit.Boardnav item added toAppShell;/plannerlink getsexact: trueso it doesn't light up for/planner/board.Optimistic + rollback contract (acceptance criterion)
planner.board.tsxwires a TanStack Query mutation with:onMutate: snapshot the cache, move the card into the target column immediately.onError: restore the snapshot + fire a toast.onSettled: invalidate to reconcile with the next poll.The rollback contract is directly exercised in
components/board/assign-rollback.test.tsx.Tests
apps/server/src/board.test.ts(7 tests): unassigned bucket narrows totype:user-storywith no assignee; idle-assigned lands in the right column; running/queued cards surface withtask_id+agent_instance; same-issue dedup between running + idle-assigned; 5 s cache hit; response carriesrepos[]. PlusGET /board+POST /board/assignsmoke tests inmain.test.ts.components/board/board.test.tsx(6 tests, incl. the drag-and-drop between columns case required by the ACs),components/board/assign-rollback.test.tsx(2 tests for optimistic + rollback),components/board/filter-logic.test.ts(3 tests).e2e/board.spec.tsdrags a card to a sibling column and assertsPOST /board/assignfires.Test plan
just qapasses locally (server typecheck + tests + lint + format; web typecheck + vitest + lint)bun x vite buildsucceeds and the new route appears under/app/planner/boardissues.assignedwebhook triggers dispatchUnassignedcolumn is a no-op (read-only gutter for now)POST /board/assign, the card snaps back into its source column and a toast surfaces🤖 Generated with Claude Code
Review — M18-7 Assignment Board (Kanban, drag-to-assign)
CI: ✅ green (run #1809, sha
c4a114b, 3m32s)Acceptance criteria
busy/capacitysaturationPOST /board/assign→ Forgejo PATCH →issues.assignedwebhook dispatchboard.test.tsxassign-rollback.test.tsxSecurity
POST /board/assignvalidatesassigneeagainst the knownforgejo_userset — arbitrary login injection is blocked.repoagainstcfg.repos— cross-repo mutations refused.guardMutatingapplied to write endpoint;GET /boardis appropriately open.Advisory findings (neither blocks merge)
1.
apps/web/src/routes/planner.board.tsx—mutationFnreadsforgejo_userfrom the cache rather than mutation variablesTanStack Query guarantees
onMutatecompletes beforemutationFnstarts, andonMutateonly modifiescardsarrays (not column-level metadata), soforgejo_useris always present in the optimistic cache state whenmutationFnruns. Correct in practice. The cleaner pattern is to resolveforgejyUserinhandleAssign(which already hasqueryClient) and pass it as a mutation variable, makingmutationFna pure function of its inputs. Low-priority refactor, not a correctness issue.2.
apps/server/src/main.test.ts—/board/assignsmoke tests accept503as valid, masking the repo allowlist checkIn CI (no config loaded for
main.test.ts), this test consistently returns503and never reaches the allowlist check. The production code is correct — the check is inhandleBoardAssign. Consider adding a variant that loads a minimal config and verifies the400rejection for an unwatched repo. Minor test coverage gap, not a production issue.Everything else is solid: the
board.tsderivation logic is clean and well-tested (unassigned bucket narrows totype:user-story, dedup of running+idle-assigned works correctly, 5s cache hit verified), the drag-and-drop HTML5 implementation is testable under RTL'sfireEvent.drop, theUnassignedcolumn correctly rejects drops (isDropTarget = false), thecolumnTypeForCardnull-for-non-unassigned behaviour is documented and intentionally idempotent via Forgejo PATCH semantics, and the planner routing split (planner.tsx→ pathless layout,planner.index.tsx+planner.board.tsx) is correct.c4a114be1e355d7ca19f