# Story 2.1: Municipality-to-Cycle Kanban Entry Point Status: review ## Story 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. ## Acceptance Criteria 1. **Given** a client services user navigates to the election cycle workspace **When** the kanban loads **Then** municipalities with active cycle jobs appear as cards in their respective cycle lane columns, and municipalities with no active jobs appear in an "Unassigned" lane 2. **Given** a municipality has jobs in multiple concurrent election cycles **When** the kanban renders **Then** the municipality card appears in each relevant cycle lane independently (UX-DR16 multi-lane support) 3. **Given** a user views a municipality card **When** displayed in a cycle lane **Then** the card shows municipality name, jurisdiction code, cycle job status badge, and a quick-open action 4. **Given** a cycle lane contains many municipality cards **When** the user scrolls within the lane **Then** the lane column header remains visible and performance is maintained 5. **Given** a user interacts with the kanban via keyboard **When** navigating cards and lanes **Then** all card actions are reachable without a mouse and focus indicators are visible (UX-DR9) ## Tasks / Subtasks - [x] Backend: expose election-cycle kanban data (AC: #1, #2, #3) - [x] Add an extension-layer read model that returns municipalities grouped by active cycle assignments, including an "Unassigned" bucket for municipalities without an active cycle job - [x] Project per-card fields: municipality name, jurisdiction code (`JCode`), cycle name, cycle job status, legacy join key - [x] Support multi-lane rendering by returning one row per (municipality, active cycle) pair without mutating legacy Access tables - [x] Authorize endpoint for client services role using existing RBAC patterns from Epic 1 - [x] Frontend: build kanban entry view (AC: #1, #2, #3, #4) - [x] Add kanban route to the workspace shell with cycle lane columns rendered from the read model and an Unassigned lane always present - [x] Render a municipality card component showing name, jurisdiction code, status badge, and a quick-open action that navigates to the cycle job detail (route stub acceptable until Story 2.2 lands) - [x] Keep lane headers sticky during column scroll and virtualize/window long lane lists to maintain interaction performance - [x] Accessibility & keyboard support (AC: #5) - [x] Provide keyboard navigation across lanes and cards with visible focus indicators per UX-DR9 - [x] Ensure card actions (quick-open, future cycle assignment) are reachable via keyboard - [x] Tests & evidence (AC: #1–#5) - [x] Backend tests cover Unassigned bucket, multi-lane projection, RBAC, and confirm no writes hit legacy Access tables - [x] Frontend tests cover lane rendering, card content, sticky headers under scroll, and keyboard navigation - [x] Capture changed files and any config notes for the dev record ## Dev Notes - Follow Epic 1 architecture constraints: ASP.NET Core + React separation, RBAC-aware patterns, and immutable legacy tables. Lane data must come from extension tables joined to legacy entities (read-only). - Reuse the workspace shell, Ant Design tokens/components, and existing client services authorization patterns established in Epic 1; avoid bespoke styling or auth shims. - This story is the entry point for Epic 2 — the quick-open / assignment actions are stubbed here and fully wired by Story 2.2 (Create Election-Cycle Job). Do not pull Story 2.2 behavior forward. - Multi-lane support is a hard requirement (UX-DR16): a municipality with N concurrent cycle jobs must render N independent cards. - Treat the lane data shape as the contract for Stories 2.2–2.5; keep field names stable. ### Project Structure Notes - Backend: `Campaign_Tracker.Server/` — add cycle kanban read model under an election-cycle feature folder consistent with existing module conventions - Frontend: `campaign-tracker-client/` — add kanban view under the workspace, sharing layout primitives with existing municipality views - Story artifacts: `_bmad-output/implementation-artifacts/` ### References - Story source: `_bmad-output/planning-artifacts/epics.md` (Epic 2 / Story 2.1) - Architecture constraints: `_bmad-output/planning-artifacts/architecture.md` (extension-table write path, legacy read-only) - UX patterns: `_bmad-output/planning-artifacts/ux-design-specification.md` (UX-DR9 keyboard/focus, UX-DR16 multi-lane kanban) - Prior reference data: Story 1.13 prior-cycle defaults view (read-only municipality cycle history) ## Dev Agent Record ### Agent Model Used GPT-5 Codex ### Debug Log References - Story generated from epic source and architecture/UX planning artifacts. - 2026-05-07: Targeted backend red test initially failed because `Campaign_Tracker.Server.ElectionCycles` did not exist; after implementation, `dotnet test campaign-tracker.sln --filter ElectionCycleKanbanReadModelTests` passed 5/5. - 2026-05-07: Targeted frontend red test initially failed because `electionCycleKanbanContracts` did not exist; after implementation, `npm test -- electionCycleKanbanContracts.test.tsx` passed 4/4. - 2026-05-07: Full validation passed: `dotnet test campaign-tracker.sln` (162/162), `npm test` (49/49), `npm run lint`, and `npm run build`. ### Implementation Plan - Implement the kanban as an extension-layer read model over municipality profiles and cycle job assignments, returning stable lane/card DTOs for frontend and future Story 2.2 wiring. - Keep legacy Access usage read-only by resolving municipality identity through existing profile/legacy repository contracts and storing cycle jobs in the extension-layer repository. ### Completion Notes List - Story context created and marked ready-for-dev. - Backend kanban endpoint added at `GET /api/election-cycles/kanban`, protected by the client-services policy. - Backend read model returns active cycle lanes plus an always-present Unassigned lane, including multi-lane cards for concurrent active cycle jobs. - Frontend Election Cycles workspace now renders the kanban board from the read model with sticky scroll lanes, card windowing, quick-open and assign-cycle route stubs, and arrow-key card navigation. - Story 2.1 validation completed with no failing tests or lint errors; Vite build completed with the existing large-chunk warning. ### File List - Campaign_Tracker.Server/Controllers/ElectionCycleKanbanController.cs - Campaign_Tracker.Server/ElectionCycles/ElectionCycleJobAssignment.cs - Campaign_Tracker.Server/ElectionCycles/ElectionCycleKanbanReadModel.cs - Campaign_Tracker.Server/ElectionCycles/IElectionCycleJobRepository.cs - Campaign_Tracker.Server/ElectionCycles/InMemoryElectionCycleJobRepository.cs - Campaign_Tracker.Server/Program.cs - Campaign_Tracker.Server.Tests/ElectionCycleKanbanReadModelTests.cs - campaign-tracker-client/src/electionCycles/electionCycleKanban.css - campaign-tracker-client/src/electionCycles/electionCycleKanbanContracts.test.tsx - campaign-tracker-client/src/electionCycles/electionCycleKanbanContracts.ts - campaign-tracker-client/src/electionCycles/electionCycleKanbanView.tsx - campaign-tracker-client/src/workspace/WorkspaceShell.tsx ### Change Log - 2026-05-07: Added backend election-cycle kanban read model, client-services endpoint, DI registration, and backend tests. - 2026-05-07: Added frontend election-cycle kanban view, workspace route wiring, keyboard support, sticky/windowed lane styling, and frontend tests. - 2026-05-07: Completed validation and moved story to review. ### Review Findings ### Review Findings (Continuation 2026-05-07) - [x] [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] - [x] [Review][Decision→Defer] JCode normalization mismatch across profiles vs cycle-job assignments — deferred, pre-existing data-quality issue carried forward from Story 1-10 - [x] [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") - [x] [Review][Decision→Defer] Audit-on-controller vs read-model boundary — deferred, revisit when more read endpoints land - [x] [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] - [x] [Review][Defer] Test coverage gaps for non-happy-path lane permutations — deferred, pre-existing pattern across stories - [x] [Review][Defer] Vite build large-chunk warning — deferred, pre-existing