diff --git a/docs/superpowers/plans/2026-06-20-cyclone-claim-drawer.md b/docs/superpowers/plans/2026-06-20-cyclone-claim-drawer.md new file mode 100644 index 0000000..8ee4dfd --- /dev/null +++ b/docs/superpowers/plans/2026-06-20-cyclone-claim-drawer.md @@ -0,0 +1,712 @@ +# Sub-project 4 — Per-Claim Detail Drawer: Implementation Plan + +**Date:** 2026-06-20 +**Status:** Approved +**Branch:** `claim-drawer` +**Spec:** `docs/superpowers/specs/2026-06-20-cyclone-claim-drawer-design.md` + +24 tasks across 5 phases. Follows the same task pattern as the SP3 plan (`[ ] Step 1 → Step N → commit`). TDD throughout. + +## File structure + +**Backend additions (2 files):** +- `backend/src/cyclone/store.py` — `get_claim_detail(claim_id) -> dict | None` method on `CycloneStore` +- `backend/src/cyclone/api.py` — new `GET /api/claims/{claim_id}` endpoint +- `backend/tests/test_api_claim_detail.py` — endpoint tests + +**Frontend additions (15 files):** +``` +src/types/index.ts # +ClaimDetail interface +src/lib/api.ts # +api.getClaimDetail(id) +src/hooks/useClaimDetail.ts # fetch + cache +src/hooks/useDrawerKeyboard.ts # j/k/esc handlers +src/hooks/useDrawerUrlState.ts # ?claim= sync +src/components/ClaimDrawer/ + ├── ClaimDrawer.tsx # root, wires everything + ├── ClaimDrawerHeader.tsx + ├── ValidationPanel.tsx + ├── ServiceLinesTable.tsx + ├── DiagnosesList.tsx + ├── PartiesGrid.tsx + ├── RawSegmentsPanel.tsx + ├── MatchedRemitCard.tsx + ├── StateHistoryTimeline.tsx + ├── ClaimDrawerSkeleton.tsx + ├── ClaimDrawerError.tsx + └── index.ts # barrel +src/components/KeyboardCheatsheet/ # new + ├── KeyboardCheatsheet.tsx + └── index.ts +src/pages/Claims.tsx # click handler + drawer render +src/pages/Claims.test.tsx # +new tests +src/index.css # +CSS variables for Modern palette +``` + +## Phase 1 — Backend: `GET /api/claims/{id}` + +### Task 1: `CycloneStore.get_claim_detail` method + +**Files:** +- Modify: `backend/src/cyclone/store.py` +- Create: `backend/tests/test_store_claim_detail.py` + +**Implementation notes:** +- Returns `None` when claim not found +- Reads `claims.raw_json` and deserializes to `ClaimOutput` (use the same `_to_ui_claim_from_orm` helpers that already exist; build a new mapper `to_ui_claim_detail` that returns the richer shape) +- Queries `activity_events` for `claim_id == X`, ordered by `ts DESC`, limit 50, maps to `{kind, ts, batchId, remittanceId}` +- If `claims.matched_remittance_id` is set, queries the remittance and embeds `{id, totalPaid, status, receivedAt}` +- Use lazy `from cyclone import db as _db` import inside the function (established pattern) + +**Tests (≥4):** +- `test_get_claim_detail_returns_none_for_missing` +- `test_get_claim_detail_happy_path_returns_full_shape` +- `test_get_claim_detail_includes_state_history` +- `test_get_claim_detail_includes_matched_remittance_summary` +- `test_get_claim_detail_no_matched_remittance_returns_null_field` + +TDD discipline: write tests first, confirm red, implement, confirm green. + +- [ ] **Step 1: Add failing tests** in `backend/tests/test_store_claim_detail.py` +- [ ] **Step 2: Run tests to verify they fail** +- [ ] **Step 3: Implement `to_ui_claim_detail` mapper + `get_claim_detail` method** +- [ ] **Step 4: Run tests to verify they pass** +- [ ] **Step 5: Commit** + +```bash +git add backend/src/cyclone/store.py backend/tests/test_store_claim_detail.py +git commit -m "feat(store): get_claim_detail returns full claim context with history + matched remit" +``` + +### Task 2: `GET /api/claims/{claim_id}` endpoint + +**Files:** +- Modify: `backend/src/cyclone/api.py` +- Create: `backend/tests/test_api_claim_detail.py` + +**Endpoint behavior:** +- `GET /api/claims/{claim_id}` — returns 200 with `ClaimDetail` JSON or 404 with `{"error": "Not found", "detail": "..."}` +- Path param is `claim_id` (not `id`) — avoids shadowing FastAPI internals and matches `/api/acks/{ack_id}` convention from SP3 + +**Tests (≥4):** +- `test_get_claim_detail_happy_path` — 200 + shape matches spec +- `test_get_claim_detail_404_for_missing` — 404, not 500 +- `test_get_claim_detail_state_history_includes_manual_match` — manual_match event appears in history +- `test_get_claim_detail_matched_remit_inlines_summary` — when matched, summary fields are populated + +- [ ] **Step 1: Add failing tests** in `backend/tests/test_api_claim_detail.py` +- [ ] **Step 2: Run tests to verify they fail** +- [ ] **Step 3: Add the endpoint** (mirror `parse_999` endpoint pattern; use existing `_claim_to_ui` helpers) +- [ ] **Step 4: Run tests to verify they pass** +- [ ] **Step 5: Commit** + +```bash +git add backend/src/cyclone/api.py backend/tests/test_api_claim_detail.py +git commit -m "feat(api): GET /api/claims/{claim_id} returns full claim context" +``` + +### Task 3: Phase 1 commit + full suite + +```bash +.venv/bin/pytest -q +``` + +Expected: 409 + ~8 new = ~417 pass + 1 skip. All green. + +- [ ] **Step 1: Run the full backend suite, confirm green** +- [ ] **Step 2: Tag phase complete** (no commit unless changes; otherwise commit "test(sp4): phase 1 backend complete") + +## Phase 2 — Frontend foundation + +### Task 4: `ClaimDetail` TypeScript type + `api.getClaimDetail` + +**Files:** +- Modify: `src/types/index.ts` +- Modify: `src/lib/api.ts` +- Modify: `src/lib/api.test.ts` + +The `ClaimDetail` type matches the JSON response shape from Task 2. Nested types: `Diagnosis`, `ServiceLine`, `Parties`, `Validation`, `MatchedRemittance`, `StateHistoryEvent`. + +- [ ] **Step 1: Add `ClaimDetail` interface** to `src/types/index.ts` (in the "backend-aligned" block, snake_case to match API) +- [ ] **Step 2: Add `api.getClaimDetail(id)` method** to `src/lib/api.ts` (uses `apiFetch` helper if present) +- [ ] **Step 3: Add test** to `src/lib/api.test.ts` — happy path + 404 +- [ ] **Step 4: Run frontend tests + typecheck, confirm green** +- [ ] **Step 5: Commit** + +```bash +git add src/types/index.ts src/lib/api.ts src/lib/api.test.ts +git commit -m "feat(frontend): ClaimDetail type + api.getClaimDetail" +``` + +### Task 5: `useClaimDetail` hook + +**Files:** +- Create: `src/hooks/useClaimDetail.ts` +- Create: `src/hooks/useClaimDetail.test.ts` + +Hook signature: +```ts +export function useClaimDetail(id: string | null): { + data: ClaimDetail | null; + isLoading: boolean; + isError: boolean; + error: Error | null; + refetch: () => void; +}; +``` + +Behavior: +- When `id === null`, returns `{ data: null, isLoading: false, ... }` (drawer closed state) +- Otherwise, fetches via `api.getClaimDetail(id)` using TanStack Query (check `package.json` — if not present, use plain `useState` + `useEffect`) +- Caches for the drawer session so j/k navigation reuses prior fetches within ~30s +- Returns 404 state as `{ isError: true, error: }` + +- [ ] **Step 1: Add failing tests** — null id (no fetch), happy path, 404 +- [ ] **Step 2: Run tests, confirm red** +- [ ] **Step 3: Implement the hook** +- [ ] **Step 4: Run tests, confirm green** +- [ ] **Step 5: Commit** + +```bash +git add src/hooks/useClaimDetail.ts src/hooks/useClaimDetail.test.ts +git commit -m "feat(frontend): useClaimDetail hook with null-id short-circuit" +``` + +### Task 6: `useDrawerKeyboard` hook + +**Files:** +- Create: `src/hooks/useDrawerKeyboard.ts` +- Create: `src/hooks/useDrawerKeyboard.test.ts` + +Hook signature: +```ts +export function useDrawerKeyboard(opts: { + enabled: boolean; + onNext: () => void; + onPrev: () => void; + onClose: () => void; + onToggleHelp: () => void; +}): void; +``` + +Behavior: +- Attaches `keydown` listener to `window` (or `document`) when `enabled === true` +- Maps `j` / `ArrowDown` → `onNext`, `k` / `ArrowUp` → `onPrev`, `Escape` → `onClose`, `?` (Shift+/) → `onToggleHelp` +- Ignores keys when an input/textarea/contentEditable has focus +- Cleans up listener on unmount or `enabled === false` + +- [ ] **Step 1: Add failing tests** — fires onNext for "j", ignores when input focused, cleanup on unmount +- [ ] **Step 2: Run tests, confirm red** +- [ ] **Step 3: Implement the hook** +- [ ] **Step 4: Run tests, confirm green** +- [ ] **Step 5: Commit** + +```bash +git add src/hooks/useDrawerKeyboard.ts src/hooks/useDrawerKeyboard.test.ts +git commit -m "feat(frontend): useDrawerKeyboard hook for j/k/esc/?" +``` + +### Task 7: `useDrawerUrlState` hook + +**Files:** +- Create: `src/hooks/useDrawerUrlState.ts` +- Create: `src/hooks/useDrawerUrlState.test.ts` + +Hook signature: +```ts +export function useDrawerUrlState(): { + claimId: string | null; + open: (id: string) => void; + close: () => void; + setClaimId: (id: string) => void; // for j/k nav +}; +``` + +Behavior: +- Reads `?claim=` from `window.location.search` on mount; returns it as `claimId` +- `open(id)` calls `history.pushState` with `?claim={id}` (new history entry) +- `setClaimId(id)` calls `history.replaceState` (no new history entry, but URL updates) +- `close()` strips the param via `history.pushState` +- Returns null `claimId` when no param is set + +- [ ] **Step 1: Add failing tests** — reads URL on mount, open adds param, setClaimId replaces, close removes +- [ ] **Step 2: Run tests, confirm red** +- [ ] **Step 3: Implement the hook** (mock `window.history` in tests via vitest's `vi.stubGlobal`) +- [ ] **Step 4: Run tests, confirm green** +- [ ] **Step 5: Commit** + +```bash +git add src/hooks/useDrawerUrlState.ts src/hooks/useDrawerUrlState.test.ts +git commit -m "feat(frontend): useDrawerUrlState hook for ?claim= sync" +``` + +### Task 8: Phase 2 commit + typecheck + +```bash +npm run typecheck +npm test -- --run +``` + +Expected: 10 + 5 new = 15 frontend pass. + +- [ ] **Step 1: Run typecheck + frontend tests, confirm green** +- [ ] **Step 2: Commit if any changes** + +## Phase 3 — Frontend leaf components + +> **Conventions for all components in this phase:** +> - Use existing shadcn/ui primitives: `Sheet`, `Button`, `Badge`, `Skeleton` (already in project) +> - Props are explicit, no context drilling +> - Each component has its own test file +> - Use Tailwind utility classes directly; add design tokens to `src/index.css` only when shadcn tokens don't fit + +### Task 9: Add Modern palette CSS variables to `src/index.css` + +**Files:** +- Modify: `src/index.css` + +Add the CSS variables from spec §3.3 to `:root`: + +```css +:root { + /* Modern palette (SP4) */ + --surface: #FAFAF7; + --ink-primary: #0A0A0A; + --ink-secondary: #6B6B6B; + --ink-tertiary: #A0A0A0; + --border-heavy: #1A1A1A; + --accent: #1D4ED8; + --accent-hover: #1E40AF; + --error: #DC2626; + --error-bg: #FEF2F2; + --success: #15803D; + --success-bg: #F0FDF4; + --warning: #B45309; + --warning-bg: #FFFBEB; +} + +@media (prefers-color-scheme: dark) { + /* Optional dark override — keep parity with existing token system. If + existing shadcn dark tokens already cover dark mode, omit this block. */ +} +``` + +Also add the Inter Tight + JetBrains Mono font imports (Google Fonts CDN). + +- [ ] **Step 1: Append the CSS variables + font import** +- [ ] **Step 2: Commit** + +```bash +git add src/index.css +git commit -m "feat(frontend): Modern palette CSS variables + Inter Tight/JetBrains Mono fonts" +``` + +### Task 10: `ClaimDrawerSkeleton` + `ClaimDrawerError` (loading + error states) + +**Files:** +- Create: `src/components/ClaimDrawer/ClaimDrawerSkeleton.tsx` +- Create: `src/components/ClaimDrawer/ClaimDrawerError.tsx` +- Create: `src/components/ClaimDrawer/ClaimDrawerSkeleton.test.tsx` +- Create: `src/components/ClaimDrawer/ClaimDrawerError.test.tsx` + +**Skeleton:** Renders 8-12 `Skeleton` primitives matching section heights (header, validation, lines, dx, parties, history). No props. + +**Error:** Takes `{ kind: "not_found" | "network"; onRetry?: () => void; onClose: () => void }`. Renders the appropriate message + buttons. + +- [ ] **Step 1: Add failing tests** for both components +- [ ] **Step 2: Implement** (keep simple — these are placeholder UIs) +- [ ] **Step 3: Run tests, confirm green** +- [ ] **Step 4: Commit** + +```bash +git add src/components/ClaimDrawer/ClaimDrawerSkeleton.tsx src/components/ClaimDrawer/ClaimDrawerSkeleton.test.tsx src/components/ClaimDrawer/ClaimDrawerError.tsx src/components/ClaimDrawer/ClaimDrawerError.test.tsx +git commit -m "feat(frontend): ClaimDrawer skeleton + error states" +``` + +### Task 11: `ClaimDrawerHeader` component + +**Files:** +- Create: `src/components/ClaimDrawer/ClaimDrawerHeader.tsx` +- Create: `src/components/ClaimDrawer/ClaimDrawerHeader.test.tsx` + +Props: `{ claim: ClaimDetail; onClose: () => void }` + +Renders: +- Top-left: `Claim {id}` label (10px uppercase), then `{id}` (24px bold, mono) +- State badge (right): `Badge` component with color per state (submitted=neutral, matched=success, denied=error, etc.) +- Total amount (right of badge): big number, mono +- Close button (top-right): `Button` with X icon + +- [ ] **Step 1: Add failing tests** — renders ID, state badge with right color, total, close callback +- [ ] **Step 2: Implement** +- [ ] **Step 3: Run tests, confirm green** +- [ ] **Step 4: Commit** + +```bash +git add src/components/ClaimDrawer/ClaimDrawerHeader.tsx src/components/ClaimDrawer/ClaimDrawerHeader.test.tsx +git commit -m "feat(frontend): ClaimDrawerHeader with state badge + close button" +``` + +### Task 12: `ValidationPanel` component + +**Files:** +- Create: `src/components/ClaimDrawer/ValidationPanel.tsx` +- Create: `src/components/ClaimDrawer/ValidationPanel.test.tsx` + +Props: `{ validation: { passed: boolean; errors: ValidationIssue[]; warnings: ValidationIssue[] } }` + +Renders: +- Section label `VALIDATION` +- If passed and no warnings: small green checkmark + "All checks passed" +- Otherwise: two sub-sections — Errors (red border-left) and Warnings (amber border-left) +- Group errors by `rule` field with count (e.g., `R050_diagnosis_present (2)`) +- List messages under each rule group + +- [ ] **Step 1: Add failing tests** — empty state, errors only, warnings only, both, grouping +- [ ] **Step 2: Implement** +- [ ] **Step 3: Run tests, confirm green** +- [ ] **Step 4: Commit** + +```bash +git add src/components/ClaimDrawer/ValidationPanel.tsx src/components/ClaimDrawer/ValidationPanel.test.tsx +git commit -m "feat(frontend): ValidationPanel with rule-grouped errors + warnings" +``` + +### Task 13: `ServiceLinesTable` component + +**Files:** +- Create: `src/components/ClaimDrawer/ServiceLinesTable.tsx` +- Create: `src/components/ClaimDrawer/ServiceLinesTable.test.tsx` + +Props: `{ serviceLines: ServiceLine[] }` + +Renders: +- Section label `SERVICE LINES ({n})` +- Table: line #, procedure (qualifier + code + modifiers), charge (mono, big), units, date +- Empty state: "No service lines" muted text + +- [ ] **Step 1: Add failing tests** — empty, single line, multiple lines; procedure formatting +- [ ] **Step 2: Implement** +- [ ] **Step 3: Run tests, confirm green** +- [ ] **Step 4: Commit** + +```bash +git add src/components/ClaimDrawer/ServiceLinesTable.tsx src/components/ClaimDrawer/ServiceLinesTable.test.tsx +git commit -m "feat(frontend): ServiceLinesTable with procedure + charge columns" +``` + +### Task 14: `DiagnosesList` component + +**Files:** +- Create: `src/components/ClaimDrawer/DiagnosesList.tsx` +- Create: `src/components/ClaimDrawer/DiagnosesList.test.tsx` + +Props: `{ diagnoses: Diagnosis[] }` + +Renders: +- Section label `DIAGNOSES ({n})` +- Inline list of code (mono) + qualifier (when present) + description (when available via inline dict) +- Empty state: "No diagnoses" muted text + +- [ ] **Step 1: Add failing tests** — empty, single, multiple; qualifier shown when present +- [ ] **Step 2: Implement** +- [ ] **Step 3: Run tests, confirm green** +- [ ] **Step 4: Commit** + +```bash +git add src/components/ClaimDrawer/DiagnosesList.tsx src/components/ClaimDrawer/DiagnosesList.test.tsx +git commit -m "feat(frontend): DiagnosesList with qualifier + code formatting" +``` + +### Task 15: `PartiesGrid` component + +**Files:** +- Create: `src/components/ClaimDrawer/PartiesGrid.tsx` +- Create: `src/components/ClaimDrawer/PartiesGrid.test.tsx` + +Props: `{ parties: { billingProvider: ..., subscriber: ..., payer: ... } }` + +Renders: +- Section label `PARTIES` +- 3 stacked cards (vertical on mobile, 3-col grid on desktop ≥768px) +- Each card: label (10px uppercase), name (16px), NPI/member ID (mono), address when present + +- [ ] **Step 1: Add failing tests** — all 3 cards; missing fields gracefully omitted +- [ ] **Step 2: Implement** +- [ ] **Step 3: Run tests, confirm green** +- [ ] **Step 4: Commit** + +```bash +git add src/components/ClaimDrawer/PartiesGrid.tsx src/components/ClaimDrawer/PartiesGrid.test.tsx +git commit -m "feat(frontend): PartiesGrid with billing provider/subscriber/payer cards" +``` + +### Task 16: `RawSegmentsPanel` component + +**Files:** +- Create: `src/components/ClaimDrawer/RawSegmentsPanel.tsx` +- Create: `src/components/ClaimDrawer/RawSegmentsPanel.test.tsx` + +Props: `{ rawSegments: string[][] }` + +Renders: +- Section label `RAW SEGMENTS ({n})` +- Native `
` element (collapsed by default) +- When expanded: monospace pre-formatted list with segments on separate lines, elements joined by `*` + +- [ ] **Step 1: Add failing tests** — collapsed by default, toggle expands, segment rendering +- [ ] **Step 2: Implement** +- [ ] **Step 3: Run tests, confirm green** +- [ ] **Step 4: Commit** + +```bash +git add src/components/ClaimDrawer/RawSegmentsPanel.tsx src/components/ClaimDrawer/RawSegmentsPanel.test.tsx +git commit -m "feat(frontend): RawSegmentsPanel collapsible debug aid" +``` + +### Task 17: `MatchedRemitCard` component + +**Files:** +- Create: `src/components/ClaimDrawer/MatchedRemitCard.tsx` +- Create: `src/components/ClaimDrawer/MatchedRemitCard.test.tsx` + +Props: `{ matchedRemittance: MatchedRemittance | null }` + +Renders: +- If null: returns null (no section) +- Section label `MATCHED REMITTANCE` +- Card with: remit ID, total paid (big mono), status badge, received date +- "View remittance →" link button that opens the remittance detail (use `window.location` or a navigate handler — keep simple for v1: `window.location.href = \`/remittances?id=${matchedRemittance.id}\`` or trigger an event) + +- [ ] **Step 1: Add failing tests** — null returns nothing, populated renders all fields +- [ ] **Step 2: Implement** +- [ ] **Step 3: Run tests, confirm green** +- [ ] **Step 4: Commit** + +```bash +git add src/components/ClaimDrawer/MatchedRemitCard.tsx src/components/ClaimDrawer/MatchedRemitCard.test.tsx +git commit -m "feat(frontend): MatchedRemitCard with view-remittance link" +``` + +### Task 18: `StateHistoryTimeline` component + +**Files:** +- Create: `src/components/ClaimDrawer/StateHistoryTimeline.tsx` +- Create: `src/components/ClaimDrawer/StateHistoryTimeline.test.tsx` + +Props: `{ history: StateHistoryEvent[] }` + +Renders: +- Section label `STATE HISTORY ({n})` +- Vertical timeline with a 2px left rule and colored dots per event kind: + - `claim_submitted` → ink-secondary + - `claim_accepted`, `claim_paid` → success green + - `claim_denied` → error red + - `manual_match`, `manual_unmatch` → warning amber + - `remit_received` → accent blue + - others → muted +- Each row: kind (10px uppercase), ts (mono), optional remittance ID when present +- Empty state: "No history events" muted + +- [ ] **Step 1: Add failing tests** — empty, single, multiple; kind-color mapping +- [ ] **Step 2: Implement** +- [ ] **Step 3: Run tests, confirm green** +- [ ] **Step 4: Commit** + +```bash +git add src/components/ClaimDrawer/StateHistoryTimeline.tsx src/components/ClaimDrawer/StateHistoryTimeline.test.tsx +git commit -m "feat(frontend): StateHistoryTimeline with kind-colored dots" +``` + +### Task 19: Barrel export + phase 3 commit + +**Files:** +- Create: `src/components/ClaimDrawer/index.ts` + +```ts +export { ClaimDrawer } from "./ClaimDrawer"; +export { ClaimDrawerHeader } from "./ClaimDrawerHeader"; +export { ValidationPanel } from "./ValidationPanel"; +export { ServiceLinesTable } from "./ServiceLinesTable"; +export { DiagnosesList } from "./DiagnosesList"; +export { PartiesGrid } from "./PartiesGrid"; +export { RawSegmentsPanel } from "./RawSegmentsPanel"; +export { MatchedRemitCard } from "./MatchedRemitCard"; +export { StateHistoryTimeline } from "./StateHistoryTimeline"; +export { ClaimDrawerSkeleton } from "./ClaimDrawerSkeleton"; +export { ClaimDrawerError } from "./ClaimDrawerError"; +``` + +- [ ] **Step 1: Create the barrel** +- [ ] **Step 2: Run typecheck + tests, confirm green** +- [ ] **Step 3: Commit** + +```bash +git add src/components/ClaimDrawer/index.ts +git commit -m "feat(frontend): ClaimDrawer barrel export" +``` + +## Phase 4 — Drawer root + page wiring + +### Task 20: `ClaimDrawer` root component + +**Files:** +- Create: `src/components/ClaimDrawer/ClaimDrawer.tsx` +- Create: `src/components/ClaimDrawer/ClaimDrawer.test.tsx` + +Props: +```ts +{ + claimId: string | null; + claims: { id: string }[]; // full list, used for j/k navigation + onClose: () => void; + onNavigate: (newId: string) => void; // called when user moves via j/k + onToggleHelp: () => void; // called when ? pressed +} +``` + +Uses shadcn `Sheet` (or `Dialog`) primitive. Behavior: +- Renders nothing (or null) when `claimId === null` +- Uses `useClaimDetail(claimId)` for data +- Wires `useDrawerKeyboard({ enabled: !!claimId, onNext, onPrev, onClose, onToggleHelp })` +- Renders `ClaimDrawerSkeleton` while loading, `ClaimDrawerError` on error, the section components on success +- `onNext` / `onPrev`: derives next/prev claim IDs from the `claims` prop (find current index, +1 or -1, wrap), then calls `onNavigate(newId)` +- `onToggleHelp`: passed through to the keyboard hook + +- [ ] **Step 1: Add failing tests** — renders null when no claimId, skeleton on loading, error state on error, sections on success, j/k calls onNavigate with the correct next/prev id (including wrap), `?` calls onToggleHelp, ESC calls onClose +- [ ] **Step 2: Implement** — keep this component focused on orchestration; defer visual polish to leaf components +- [ ] **Step 3: Run tests, confirm green** +- [ ] **Step 4: Commit** + +```bash +git add src/components/ClaimDrawer/ClaimDrawer.tsx src/components/ClaimDrawer/ClaimDrawer.test.tsx +git commit -m "feat(frontend): ClaimDrawer root orchestrator with skeleton/error/data states" +``` + +### Task 21: `Claims.tsx` page wiring + +**Files:** +- Modify: `src/pages/Claims.tsx` +- Modify: `src/pages/Claims.test.tsx` + +Wire-up: +- Read `?claim=` via `useDrawerUrlState()` +- Render `` when `claimId` is non-null +- Pass the full claim list as a sibling prop so j/k can navigate +- Clicking a row sets the URL (via `open(id)` from the URL hook) +- Clicking the dimmed overlay or ESC closes (via `close()`) +- Background list dims to 60% opacity when drawer is open (use a `useState` for `drawerOpen` or derive from `claimId`) + +- [ ] **Step 1: Add failing tests** — click row opens drawer; deep link opens on mount; ESC closes; URL clears +- [ ] **Step 2: Wire up the page** +- [ ] **Step 3: Run tests, confirm green** +- [ ] **Step 4: Commit** + +```bash +git add src/pages/Claims.tsx src/pages/Claims.test.tsx +git commit -m "feat(frontend): Claims page wires click-to-open drawer with URL sync" +``` + +### Task 22: `KeyboardCheatsheet` overlay + +**Files:** +- Create: `src/components/KeyboardCheatsheet/KeyboardCheatsheet.tsx` +- Create: `src/components/KeyboardCheatsheet/KeyboardCheatsheet.test.tsx` +- Create: `src/components/KeyboardCheatsheet/index.ts` + +Props: `{ open: boolean; onClose: () => void }` + +Renders: +- Centered modal-style overlay when open +- Lists shortcuts: `j` next, `k` prev, `↑`/`↓` same, `esc` close, `?` toggle help +- Dismisses on any other key or click outside + +- [ ] **Step 1: Add failing tests** — renders when open, hidden otherwise, dismiss on close +- [ ] **Step 2: Implement** +- [ ] **Step 3: Run tests, confirm green** +- [ ] **Step 4: Commit** + +```bash +git add src/components/KeyboardCheatsheet/ +git commit -m "feat(frontend): KeyboardCheatsheet overlay" +``` + +### Task 23: Phase 4 commit + full suite + +```bash +npm test -- --run +npm run typecheck +.venv/bin/pytest -q +``` + +Expected: ~15 frontend, ~417 backend + 1 skip. All green. + +- [ ] **Step 1: Run all checks** +- [ ] **Step 2: Commit if any changes** + +## Phase 5 — Smoke + merge + +### Task 24: End-to-end smoke + +Per spec §3.8: + +1. Start backend (`backend/.venv/bin/python -m cyclone serve &`) +2. Start frontend (`npm run dev &`) +3. `curl -s http://127.0.0.1:8000/api/health` → 200 +4. Upload co_medicaid_837p.txt via `curl -F file=@...` to `/api/parse-837` +5. `curl -s http://127.0.0.1:8000/api/claims/{first-id}` → 200 + detail +6. Visit `http://localhost:5173/claims?claim={id}` in a Playwright/Puppeteer-style headless browser, verify: + - Drawer is open + - All 8 sections render (header, validation, lines, dx, parties, raw, matched-remit if applicable, history) + - Press `k` → URL updates, drawer updates + - Press `esc` → URL clears, drawer closes +7. Kill servers +8. Commit empty: `git commit --allow-empty -m "smoke: end-to-end SP4 (claim drawer) flow passes"` + +- [ ] **Step 1: Run the smoke** +- [ ] **Step 2: Commit** + +### Task 25: README + plan doc updates + +**Files:** +- Modify: `README.md` — mark "Sub-project 4" partially shipped (only per-claim drawer); describe the new feature +- Modify: `docs/superpowers/plans/2026-06-20-cyclone-claim-drawer.md` — tick all `[ ]` to `[x]` + +- [ ] **Step 1: Update README + plan doc** +- [ ] **Step 2: Commit** + +```bash +git add README.md docs/superpowers/plans/2026-06-20-cyclone-claim-drawer.md +git commit -m "docs: mark SP4 partial shipped (claim drawer); tick all plan checkboxes" +``` + +### Task 26: Cleanup + merge to main + +1. `git status` — must be clean +2. `cd /Users/openclaw/dev/cyclone && git status` — discard stale uncommitted changes if any +3. `git merge --ff-only claim-drawer` +4. Run `cd backend && .venv/bin/pytest -q` from main — must show ~417 + 1 skip +5. `git worktree remove /Users/openclaw/dev/cyclone/.worktrees/claim-drawer` +6. `git branch -d claim-drawer` + +- [ ] **Step 1: Verify worktree clean** +- [ ] **Step 2: Discard main's stale changes (if any)** +- [ ] **Step 3: Fast-forward merge to main** +- [ ] **Step 4: Remove worktree + delete branch** + +## Test count targets + +| Phase | New backend | New frontend | Running total | +|-------|-------------|--------------|---------------| +| Start (post-SP3) | 409 + 1 skip | 10 | 419 / 10 | +| P1 end | +8 | 0 | 417 / 10 | +| P2 end | 0 | +5 | 417 / 15 | +| P3 end | 0 | +10 (one per leaf) | 417 / 25 | +| P4 end | 0 | +5 (drawer + page + cheatsheet) | 417 / 30 | + +## Risk reminders + +- Backend payload weight — cache aggressively in `useClaimDetail`. Hook returns a stable ref while data loads. +- ActivityEvent volume — default limit 50; `?limit=N` parameter documented. +- Keyboard nav collisions — `j`/`k` with browser find-in-page; documented in the cheatsheet. +- Drawer + mobile — full-screen sheet on mobile; acceptable for desktop-first operator tool. +- URL sync race — `useDrawerUrlState` reads the URL on mount and triggers the fetch before caching can interfere.