# 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.

- [x] **Step 1: Add failing tests** in `backend/tests/test_store_claim_detail.py`
- [x] **Step 2: Run tests to verify they fail**
- [x] **Step 3: Implement `to_ui_claim_detail` mapper + `get_claim_detail` method**
- [x] **Step 4: Run tests to verify they pass**
- [x] **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

- [x] **Step 1: Add failing tests** in `backend/tests/test_api_claim_detail.py`
- [x] **Step 2: Run tests to verify they fail**
- [x] **Step 3: Add the endpoint** (mirror `parse_999` endpoint pattern; use existing `_claim_to_ui` helpers)
- [x] **Step 4: Run tests to verify they pass**
- [x] **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.

- [x] **Step 1: Run the full backend suite, confirm green**
- [x] **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`.

- [x] **Step 1: Add `ClaimDetail` interface** to `src/types/index.ts` (in the "backend-aligned" block, snake_case to match API)
- [x] **Step 2: Add `api.getClaimDetail(id)` method** to `src/lib/api.ts` (uses `apiFetch` helper if present)
- [x] **Step 3: Add test** to `src/lib/api.test.ts` — happy path + 404
- [x] **Step 4: Run frontend tests + typecheck, confirm green**
- [x] **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: <NotFoundError> }`

- [x] **Step 1: Add failing tests** — null id (no fetch), happy path, 404
- [x] **Step 2: Run tests, confirm red**
- [x] **Step 3: Implement the hook**
- [x] **Step 4: Run tests, confirm green**
- [x] **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`

- [x] **Step 1: Add failing tests** — fires onNext for "j", ignores when input focused, cleanup on unmount
- [x] **Step 2: Run tests, confirm red**
- [x] **Step 3: Implement the hook**
- [x] **Step 4: Run tests, confirm green**
- [x] **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

- [x] **Step 1: Add failing tests** — reads URL on mount, open adds param, setClaimId replaces, close removes
- [x] **Step 2: Run tests, confirm red**
- [x] **Step 3: Implement the hook** (mock `window.history` in tests via vitest's `vi.stubGlobal`)
- [x] **Step 4: Run tests, confirm green**
- [x] **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.

- [x] **Step 1: Run typecheck + frontend tests, confirm green**
- [x] **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).

- [x] **Step 1: Append the CSS variables + font import**
- [x] **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.

- [x] **Step 1: Add failing tests** for both components
- [x] **Step 2: Implement** (keep simple — these are placeholder UIs)
- [x] **Step 3: Run tests, confirm green**
- [x] **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

- [x] **Step 1: Add failing tests** — renders ID, state badge with right color, total, close callback
- [x] **Step 2: Implement**
- [x] **Step 3: Run tests, confirm green**
- [x] **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

- [x] **Step 1: Add failing tests** — empty state, errors only, warnings only, both, grouping
- [x] **Step 2: Implement**
- [x] **Step 3: Run tests, confirm green**
- [x] **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

- [x] **Step 1: Add failing tests** — empty, single line, multiple lines; procedure formatting
- [x] **Step 2: Implement**
- [x] **Step 3: Run tests, confirm green**
- [x] **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

- [x] **Step 1: Add failing tests** — empty, single, multiple; qualifier shown when present
- [x] **Step 2: Implement**
- [x] **Step 3: Run tests, confirm green**
- [x] **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

- [x] **Step 1: Add failing tests** — all 3 cards; missing fields gracefully omitted
- [x] **Step 2: Implement**
- [x] **Step 3: Run tests, confirm green**
- [x] **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 `<details>` element (collapsed by default)
- When expanded: monospace pre-formatted list with segments on separate lines, elements joined by `*`

- [x] **Step 1: Add failing tests** — collapsed by default, toggle expands, segment rendering
- [x] **Step 2: Implement**
- [x] **Step 3: Run tests, confirm green**
- [x] **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)

- [x] **Step 1: Add failing tests** — null returns nothing, populated renders all fields
- [x] **Step 2: Implement**
- [x] **Step 3: Run tests, confirm green**
- [x] **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

- [x] **Step 1: Add failing tests** — empty, single, multiple; kind-color mapping
- [x] **Step 2: Implement**
- [x] **Step 3: Run tests, confirm green**
- [x] **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";
```

- [x] **Step 1: Create the barrel**
- [x] **Step 2: Run typecheck + tests, confirm green**
- [x] **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

- [x] **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
- [x] **Step 2: Implement** — keep this component focused on orchestration; defer visual polish to leaf components
- [x] **Step 3: Run tests, confirm green**
- [x] **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 `<ClaimDrawer>` 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`)

- [x] **Step 1: Add failing tests** — click row opens drawer; deep link opens on mount; ESC closes; URL clears
- [x] **Step 2: Wire up the page**
- [x] **Step 3: Run tests, confirm green**
- [x] **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

- [x] **Step 1: Add failing tests** — renders when open, hidden otherwise, dismiss on close
- [x] **Step 2: Implement**
- [x] **Step 3: Run tests, confirm green**
- [x] **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.

- [x] **Step 1: Run all checks**
- [x] **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"`

- [x] **Step 1: Run the smoke**
- [x] **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]`

- [x] **Step 1: Update README + plan doc**
- [x] **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`

- [x] **Step 1: Verify worktree clean**
- [x] **Step 2: Discard main's stale changes (if any)**
- [x] **Step 3: Fast-forward merge to main**
- [x] **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 |
| **Actual final** | **417 + 1 skip** | **124** | **417 / 124** |

(Note: the frontend test count grew well past the conservative target —
every leaf component ended up with its own dedicated test file, and the
drawer root + Claims page wiring + keyboard cheatsheet each gained
their own integration suite. Net new from SP4: 114 frontend tests
across 14 files. Backend matched the +8 estimate exactly.)

## 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.
