Status: review
As a client services staff member, I want a kanban board showing municipalities as cards organized by election cycle lanes, so that I can see at a glance which municipalities are assigned to which cycles and initiate new cycle jobs from a familiar planning view.
JCode), cycle name, cycle job status, legacy join keyCampaign_Tracker.Server/ — add cycle kanban read model under an election-cycle feature folder consistent with existing module conventionscampaign-tracker-client/ — add kanban view under the workspace, sharing layout primitives with existing municipality views_bmad-output/implementation-artifacts/_bmad-output/planning-artifacts/epics.md (Epic 2 / Story 2.1)_bmad-output/planning-artifacts/architecture.md (extension-table write path, legacy read-only)_bmad-output/planning-artifacts/ux-design-specification.md (UX-DR9 keyboard/focus, UX-DR16 multi-lane kanban)GPT-5 Codex
Campaign_Tracker.Server.ElectionCycles did not exist; after implementation, dotnet test campaign-tracker.sln --filter ElectionCycleKanbanReadModelTests passed 5/5.electionCycleKanbanContracts did not exist; after implementation, npm test -- electionCycleKanbanContracts.test.tsx passed 4/4.dotnet test campaign-tracker.sln (162/162), npm test (49/49), npm run lint, and npm run build.GET /api/election-cycles/kanban, protected by the client-services policy.[Review][Decision->Patch] Story 2.2 job creation is implemented inside Story 2.1 scope - decision resolved by Daniel: keep the create-job path in scope now and patch the review findings against it.
[Review][Patch] Backend regression suite is not green because integration tests still use shared file-backed seed storage [Campaign_Tracker.Server.Tests/AuthEndpointTests.cs:30]
[Review][Patch] Frontend lint is not green because the kanban view calls setFocus synchronously inside an effect [campaign-tracker-client/src/electionCycles/electionCycleKanbanView.tsx:125]
[Review][Patch] Long lanes are not actually windowed/virtualized in the rendered kanban view, so AC4/AC5 fail for large lanes [campaign-tracker-client/src/electionCycles/electionCycleKanbanView.tsx:168]
[Review][Patch] Create-job accepts arbitrary JCodes without legacy/profile link validation [Campaign_Tracker.Server/ElectionCycles/InMemoryElectionCycleJobRepository.cs:129]
[Review][Patch] Create-job idempotency ignores default seed assignments and can create duplicate logical jobs [Campaign_Tracker.Server/ElectionCycles/InMemoryElectionCycleJobRepository.cs:88]
[Review][Patch] Create-job job IDs are built before trimming/normalizing inputs and can diverge for equivalent requests [Campaign_Tracker.Server/ElectionCycles/InMemoryElectionCycleJobRepository.cs:109]
[Review][Patch] Create-job IDs can contain route-breaking characters that make CreatedAtAction links unretrievable [Campaign_Tracker.Server/ElectionCycles/InMemoryElectionCycleJobRepository.cs:110]
[Review][Patch] Job detail endpoint returns synthetic CreatedBy/CreatedAt metadata instead of persisted job metadata [Campaign_Tracker.Server/Controllers/ElectionCycleJobsController.cs:87]
[Review][Patch] Reload after create success is fire-and-forget and can leave stale board state with an unhandled rejection [campaign-tracker-client/src/electionCycles/electionCycleKanbanView.tsx:69]
[Review][Patch] Create-job modal defaults to an impossible existing-cycle form when there are no existing cycles [campaign-tracker-client/src/electionCycles/CreateJobModal.tsx:32]
[Review][Patch] Election-cycle frontend API helpers lack success and non-OK contract tests [campaign-tracker-client/src/electionCycles/electionCycleKanbanContracts.ts:29]
[Review][Decision→Defer] JCode normalization mismatch across profiles vs cycle-job assignments — deferred, pre-existing data-quality issue carried forward from Story 1-10
[Review][Decision→Defer] Quick-open uses raw window.history.pushState — deferred to Story 2.2 per spec (“route stub acceptable until Story 2.2 lands”)
[Review][Decision→Defer] Audit-on-controller vs read-model boundary — deferred, revisit when more read endpoints land
[Review][Decision→Resolved] Lane view gated on canCreateElectionCycle — intentional, the kanban is the create entry point; documented and accepted
[Review][Patch] Read model silently drops cycle jobs with unmatched JCode — log + surface in Unassigned rather than discard [Campaign_Tracker.Server/ElectionCycles/ElectionCycleKanbanReadModel.cs:277-295]
[Review][Patch] fetchElectionCycleKanban default fetcher loses auth headers — use the shared authenticated fetch wrapper [campaign-tracker-client/src/electionCycles/electionCycleKanbanContracts.ts]
[Review][Patch] slice(0, 50) is a hard cap, not virtualization — breaks AC4 (performance under many cards) and AC5 (cards 51+ unreachable by keyboard) [campaign-tracker-client/src/electionCycles/electionCycleKanbanView.tsx]
[Review][Patch] moveKanbanFocus crashes when active lane index exceeds lane count after re-render — clamp index before access [campaign-tracker-client/src/electionCycles/electionCycleKanbanView.tsx]
[Review][Patch] Initial keyboard focus unreachable when first lane is empty — seek to first non-empty lane on mount [campaign-tracker-client/src/electionCycles/electionCycleKanbanView.tsx]
[Review][Patch] Sentinel unassigned lane id can collide with a real cycle named “unassigned” — use a non-string-collidable sentinel (e.g., null id with explicit isUnassigned flag) [Campaign_Tracker.Server/ElectionCycles/ElectionCycleKanbanReadModel.cs]
[Review][Patch] Lane display name disagreement between backend (Unassigned) and frontend label — single source of truth or constant [campaign-tracker-client/src/electionCycles/electionCycleKanbanView.tsx]
[Review][Defer] Test coverage gaps for non-happy-path lane permutations — deferred, pre-existing pattern across stories
[Review][Defer] Vite build large-chunk warning — deferred, pre-existing
Powered by TurnKey Linux.