27 KiB
Sub-project 21 — Universal Drill-Down: Design Spec
Date: 2026-06-21
Status: Draft (awaiting user review)
Branch: universal-drilldown
Aesthetic direction: Reuse the existing dashboard / drawer aesthetic (Radix Dialog + dim overlay, accent-tinted hover affordance). No new visual tokens.
1. Scope
Every interactive surface in the Cyclone UI becomes drillable. Clicking an entity reference (claim id, patient, provider, payer, batch, ack, activity event) opens a contextual view — either a full-record drawer (slides in from the right) or a slim peek modal (centered), depending on whether the entity is a first-class drillable record or a cross-reference.
The goal is "dial down to anything": from the dashboard, from any list row, from inside any drawer, the operator can reach the underlying record with one click without losing their place.
In scope:
- New
PeekModalprimitive (centered modal) for cross-reference drills - Reuse / refactor of the existing right-side
DrillDrawerpattern for full-entity records - New
DrillStackProvidercontext to manage a max-2-level stack (one drawer + one peek) - Hover-reveal affordance (pointer + accent tint + trailing chevron) on every clickable cell
- Per-surface drill map (every click target → its destination)
- 1 new backend read endpoint (
/api/payers/{payer_id}/summary) plus an extended response shape on the existing/api/providers/{npi}(addsrecent_claims[]andrecent_activity[]arrays). The rest of the work is frontend wiring.
Out of scope (deferred):
- Patient peek modal — patient names are PHI; a peek would surface sensitive fields. Patient-name clicks instead navigate to
/claims?patient=…filtered list (no new endpoint needed; uses existing/api/claims?patient_name=…). - Write actions from peeks — peeks are read-only. Edit / resubmit / acknowledge actions remain on the full drawer / inbox lane.
- Mobile bottom-sheet variant — desktop-only for v1. Mobile fallback: peeks become centered modals at full width with the same max-height cap.
- Customizing peek fields — peek content is fixed per surface. The existing drawer already covers "I want everything."
- KPI breakdown peeks — dashboard KPI tiles navigate to filtered
/claims(the user's picked data-strategy was "fetch fresh on click"; navigation keeps the dashboard fast). - Auto-suggest next drill — no "you might also want to see…" hints.
2. Locked decisions
2.1 Component decomposition
| Component | Role |
|---|---|
DrillDrawer.tsx |
Right-side slide-over for full-record drills (claim, remit, batch, provider, ack). Refactor of today's ClaimDrawer pattern into a shared shell that all entity drawers mount inside. |
DrillDrawerHeader.tsx |
Drawer header: eyebrow, title, close button, optional primary action (e.g. "Download 837" on the ClaimDrawer). |
PeekModal.tsx |
Centered Radix Dialog for cross-reference drills (payer, activity event, validation issue, single-row tables like Inbox rows, Batch diff rows). Smaller (≤ 480px), no keyboard j/k nav (single record), closes on Esc + click-outside. |
PeekModalHeader.tsx |
Eyebrow + title + close button. |
DrillStackProvider.tsx |
React context + zustand-backed store. Owns the stack of `[{ kind: 'drawer' |
DrillableCell.tsx |
Tiny wrapper that adds the hover-reveal affordance (pointer + accent tint + trailing chevron) and the click handler. Used inside tables, KPI tiles, activity feeds. |
ProviderDrawer.tsx |
New full-entity drawer for /api/providers/{npi}. Tabs: Overview (NPI + address + counts) / Claims (top 10 recent claims + "View all →" link) / Activity (top 10 recent activity + "View all →" link). |
AckDrawer.tsx |
New full-entity drawer for /api/acks/{id} (reuses the existing GET /api/acks/{id} endpoint). |
RemitDrawer.tsx |
New full-entity drawer for /api/remittances/{id}. Already documented as "deferred to a follow-up" in the SP4 claim-drawer spec; SP21 ships it. |
useDrillStack.ts |
Hook wrapper around DrillStackProvider's store, with selector helpers for "is this drawer open?", "open this drawer", "the active peek payload". |
PayerPeekContent.tsx |
Payer peek body (backend-fed): name, claim count, billed, received, denial rate, "view all claims →" link. |
ValidationRulePeekContent.tsx |
Validation-rule peek body (static lookup, no backend call): rule code, severity, full description, X12 spec reference if any. Opened from ClaimDrawer validation issue rows. |
All live under src/components/drill/ (new shared folder) plus src/components/ProviderDrawer/, src/components/AckDrawer/, src/components/RemitDrawer/.
2.2 Drill map (per surface)
The complete click → destination table. Each row is one click target on one surface. "Stack position" describes where the destination lives in the drill stack at the moment of the click.
| Surface | Click target | Destination | Stack position |
|---|---|---|---|
| Dashboard · KPI: Claims | tile | navigate → /claims |
— |
| Dashboard · KPI: Billed | tile | navigate → /claims?sort=-billedAmount |
— |
| Dashboard · KPI: Received | tile | navigate → /claims?sort=-receivedAmount |
— |
| Dashboard · KPI: Pending AR | tile | navigate → /claims?status=submitted,pending |
— |
| Dashboard · KPI: Denial rate | tile | navigate → /claims?status=denied |
— |
| Dashboard · Top providers | any row | ProviderDrawer(?provider=NPI) |
top drawer |
| Dashboard · Recent denials | any row | DrillDrawer(claim, ?claim=ID) |
top drawer |
| Dashboard · Recent activity | any event | routed by event kind: claim_* → claim drawer, remit_received → remit drawer, provider_added → provider drawer |
top drawer |
| Claims · table row | row body | DrillDrawer(claim, ?claim=ID) |
top drawer |
| Claims · claim id cell | id | DrillDrawer(claim, ?claim=ID) |
top drawer |
| Claims · patient cell | name | navigate → /claims?patient=NAME |
— |
| Claims · provider cell | provider name + NPI | ProviderDrawer(?provider=NPI) |
top drawer |
| Claims · payer cell | payer name | PeekModal(payer) |
top peek |
| Remittances · table row | row body | RemitDrawer(?remit=ID) (replaces the inline CAS expand) |
top drawer |
| Remittances · claim id cell | id | DrillDrawer(claim, ?claim=ID) |
top drawer |
| Remittances · payer cell | payer name | PeekModal(payer) |
top peek |
| Batches · table row | row body | BatchDetail drawer (?batch=ID) |
top drawer |
| Providers · directory card | card body | ProviderDrawer(?provider=NPI) |
top drawer |
| Activity log · event row | row body | routed by event kind (same as Dashboard · Recent activity) | top drawer |
| 999 ACKs · table row | row body | AckDrawer(?ack=ID) |
top drawer |
| 999 ACKs · Download 999 button | button | (unchanged — file download) | — |
| Inbox · Rejected row | row body | DrillDrawer(claim, ?claim=ID) |
top drawer |
| Inbox · Payer-Rejected row | row body | DrillDrawer(claim, ?claim=ID) |
top drawer |
| Inbox · Candidates row | row body | RemitDrawer(?remit=ID) |
top drawer |
| Inbox · Unmatched claim row | row body | DrillDrawer(claim, ?claim=ID) |
top drawer |
| Inbox · Unmatched remit row | row body | RemitDrawer(?remit=ID) |
top drawer |
| Inbox · Done today row | row body | routed by row kind (claim vs remit) | top drawer |
| Reconciliation · claim button | button | (unchanged — select for match) | — |
| Reconciliation · claim card body | body | DrillDrawer(claim, ?claim=ID) |
top drawer (navigates to /claims?claim=ID) |
| Reconciliation · remit card body | body | RemitDrawer(?remit=ID) |
top drawer (navigates to /remittances?remit=ID) |
| Batch diff · claim id (added) | id | DrillDrawer(claim, ?claim=ID) |
top drawer |
| Batch diff · claim id (removed) | id | DrillDrawer(claim, ?claim=ID) (will 404 if removed — show distinct not-found state) |
top drawer |
| Batch diff · claim id (changed) | id | DrillDrawer(claim, ?claim=ID) |
top drawer |
| Upload · streamed claim card | header / chevron | (unchanged — inline expand) | — |
| Upload · streamed claim card | "See claim in detail →" link (new) | DrillDrawer(claim, ?claim=ID) (only enabled if the claim id is in the persisted batch list, else disabled with tooltip) |
top drawer |
| ClaimDrawer · payer name (inside drawer) | payer name | PeekModal(payer) |
peek on top of drawer |
| ClaimDrawer · provider name (inside drawer) | provider name | ProviderDrawer(?provider=NPI) (replaces drawer) |
top drawer (replaces) |
| ClaimDrawer · matched-remit id | id | RemitDrawer(?remit=ID) (replaces drawer) |
top drawer (replaces) |
| ClaimDrawer · validation issue row | rule code | PeekModal(validation issue) |
peek on top of drawer |
Nesting rules (enforced by DrillStackProvider):
- Max 2 levels: one drawer at the bottom + at most one peek on top. Opening a third drill replaces the top peek (same kind) or pops the peek (different kind).
- Clicking inside a peek to open a drawer: closes the peek first, opens the drawer (no drawer-over-peek).
- Esc closes the top entry (peek or drawer). Browser back closes the top drawer (URL pops). Browser back while only peeks are open: no-op (peek stack isn't URL-persisted).
2.3 Backend endpoints
New endpoint:
| Method | Path | Purpose |
|---|---|---|
GET |
/api/payers/{payer_id}/summary |
Aggregate snapshot for a payer: { payer_id, name, claim_count, billed_total, received_total, denial_rate, top_providers: [{npi, count}] }. Computed on read from the existing claims + remittances tables. Cached for 60s; invalidated on claim_written / remittance_written pubsub events. |
Extended endpoint (existing, response shape widened):
| Method | Path | Change |
|---|---|---|
GET |
/api/providers/{npi} |
Response gains recent_claims: ClaimSummary[] (top 10 by submission_date DESC) and recent_activity: ActivityEvent[] (top 10 by ts DESC). Backwards-compatible: existing clients ignore the new arrays. |
Reused as-is:
GET /api/claims/{id}— already exists (SP4). Used byDrillDrawer(claim).GET /api/remittances/{id}— already exists (SP3). Used byRemitDrawer.GET /api/batches/{id}— already exists. Used byBatchDetail.GET /api/providers/{npi}— already exists (SP9). Used byProviderDrawerOverview tab AND Claims / Activity tabs (via the newly extended arrays).GET /api/config/payers/{payer_id}— already exists (SP9). Used by the payer peek header (the payer name).GET /api/acks/{id}— already exists (SP3). Used byAckDrawer.
Implementation notes:
- Add the one new handler in
backend/src/cyclone/api_routers/payers.py. - Payer summary cache lives in a module-level
lru_cachewith a TTL guard (60s); invalidated on aclaim_written/remittance_writtenpubsub event. - The new endpoint inherits the existing security middleware stack (SP19):
127.0.0.1bind, body limit, rate limit, security headers. - The extended
/api/providers/{npi}response is bounded — only 10 + 10 entries per category — so the existing payload cap (per SP19 body limit, default 50 MB) is comfortably respected.
2.4 Visual design tokens
No new tokens. Reuse:
--surface/--surface-dimfor drawer + dim overlay--accentfor hover tint (the same--accentused by today's selected-row indicator + nav-active)border-border/60for the peek modal border- Existing
display mono/eyebrowtypography for peek headers animate-fade-in/animate-fade-in-upfor peek/drawer enter
Peek modal sizing:
max-width: 480px (or 90vw on narrow viewports)
padding: 20px
border: 1px solid var(--border)
shadow: shadow-2xl
border-radius: rounded-xl (matches today's Radix Dialog)
Hover affordance CSS:
.drillable {
cursor: pointer;
transition: background-color 120ms ease;
}
.drillable:hover {
background-color: hsl(var(--accent) / 0.08);
}
.drillable:hover::after {
content: "›";
margin-left: 6px;
color: hsl(var(--accent));
font-weight: 600;
}
The trailing chevron is injected via ::after so DrillableCell doesn't need to compose a child element. Disabled cells (e.g. the "See claim in detail" link in Upload when the claim isn't persisted) drop the affordance and the click handler.
2.5 Interaction patterns
Open (drawer):
- Click target → URL updates to
?{entity}={id}viahistory.pushState(orreplaceStateif same entity type, to avoid history pollution). - Drawer slides in from right, 240ms ease-out (same as today's ClaimDrawer).
- Background page dims to 60% opacity.
Open (peek):
- Click target → zustand stack updated; peek fades in centered, 180ms.
- Background dims to 75% (more aggressive than the drawer's 60%, to differentiate).
- No URL change (peek stack is ephemeral).
Open (peek on top of drawer):
- Click target inside drawer body → zustand stack gains one entry.
- Peek renders above drawer; drawer stays in place, dims to 75%.
- Drawer's existing keyboard nav (j/k, ?) is suspended while a peek is on top.
Close:
- Esc → pops the top stack entry. If peek was top, peek closes (drawer underneath is interactive again). If only a drawer, URL strips
?{entity}=. - Click on dim overlay → same as Esc.
- Browser back → pops URL drawer only (no peek history).
Loading:
- Drawer: existing
Skeleton+ErrorStatepatterns reused. - Peek: a 6-line shimmer skeleton, max 280px height. Fetch errors collapse the peek and show an inline error toast (peek is small enough that an inline state would look broken).
Errors:
- 404 on a drawer → existing
ErrorState"not found" branch (per drawer component). - 404 on a peek → toast: "Couldn't find that record. It may have been removed." and peek auto-closes after 1.5s.
- Network error → toast with Retry button.
Keyboard:
- Drawer nav: j/k, ?, esc (unchanged from today).
- Peek nav: esc, Tab cycles focusable elements inside the peek body. No j/k (single record, no list).
- Global
?cheatsheet: extended to list peek keyboard hints (currently lists only drawer hints).
2.6 URL state contract
/claims— list, no drawer./claims?claim=CLM-114— list with drawer for CLM-114./remittances?remit=R-9876— list with remit drawer./providers?provider=1881068062— directory with provider drawer./batches?batch=BATCH-…— list with batch drawer./acks?ack=42— list with ack drawer./activity?event=evt-uuid— list with activity event drawer.
Each entity-drawer type has its own URL state hook following the existing per-page pattern (useDrawerUrlState for Claims, useBatchDrawerUrlState for Batches, plus a new useProviderDrawerUrlState for Providers, useRemitDrawerUrlState for Remittances, useAckDrawerUrlState for Acks). All hooks read & write the same ?{entity}={id} convention. Each drawer is mounted by the page that owns it — clicking a cross-page entity from inside another page navigates to that entity's owning page with ?{entity}={id} set (soft fade, no full reload). The opening source's filter state (e.g. /remittances?status=posted) is preserved when navigating back via browser-back. Peeks never trigger navigation.
2.7 Frontend file layout
src/
├── components/
│ ├── drill/ # NEW shared folder
│ │ ├── DrillDrawer.tsx
│ │ ├── DrillDrawerHeader.tsx
│ │ ├── PeekModal.tsx
│ │ ├── PeekModalHeader.tsx
│ │ ├── DrillStackProvider.tsx
│ │ ├── DrillStackMount.tsx # the actual portal mount + z-index mgmt
│ │ ├── DrillableCell.tsx
│ │ ├── PayerPeekContent.tsx
│ │ ├── ValidationRulePeekContent.tsx
│ │ ├── DrillStackProvider.test.tsx
│ │ ├── DrillableCell.test.tsx
│ │ ├── PeekModal.test.tsx
│ │ └── index.ts # barrel export
│ ├── ProviderDrawer/ # NEW
│ │ ├── ProviderDrawer.tsx
│ │ ├── ProviderOverview.tsx
│ │ ├── ProviderRecentClaims.tsx # top-10 mini-table (data from extended /providers/{npi})
│ │ ├── ProviderRecentActivity.tsx # top-10 mini-feed (data from extended /providers/{npi})
│ │ └── index.ts
│ ├── RemitDrawer/ # NEW
│ │ ├── RemitDrawer.tsx
│ │ ├── RemitHeader.tsx
│ │ ├── ClaimPaymentsTable.tsx
│ │ ├── CasAdjustmentsPanel.tsx
│ │ ├── RemitPartiesGrid.tsx
│ │ └── index.ts
│ ├── AckDrawer/ # NEW
│ │ ├── AckDrawer.tsx
│ │ ├── AckHeader.tsx
│ │ ├── SegmentStatusList.tsx
│ │ └── index.ts
│ └── ClaimDrawer/ # existing — refactor header to use DrillDrawerHeader
├── hooks/
│ ├── useDrillStack.ts # NEW (zustand-backed ephemeral peek stack)
│ ├── usePayerSummary.ts # NEW
│ ├── useProviderDrawerUrlState.ts # NEW (?provider=)
│ ├── useRemitDrawerUrlState.ts # NEW (?remit=)
│ └── useAckDrawerUrlState.ts # NEW (?ack=)
├── lib/
│ └── api.ts # +api.getPayerSummary(id); /api/providers/{npi} response widened in backend
└── pages/ # wire drills per the drill map; refactor existing useDrawerUrlState usages to follow the per-drawer convention
├── Claims.tsx
├── Remittances.tsx
├── Dashboard.tsx
├── Batches.tsx
├── Providers.tsx
├── Acks.tsx
├── ActivityLog.tsx
├── Reconciliation.tsx
├── BatchDiff.tsx
├── Upload.tsx
└── Inbox.tsx
2.8 Testing targets
Frontend (Vitest + React Testing Library):
DrillStackProvider.test.tsx— max-2-level rule, peek-over-drawer, drawer-over-peek prevention, esc closes top, closeAll resets stackDrillableCell.test.tsx— renders chevron on hover, fires onClick, disabled state hides chevron + blocks clickPeekModal.test.tsx— opens via context, closes on esc, closes on click-outside, 404 → toast + auto-close, network error → retry toastProviderDrawer.test.tsx— Overview tab shows NPI + address + counts; Claims tab paginates; Activity tab paginates; j/k nav (if claim ids in scope)RemitDrawer.test.tsx— header summary, claim payments table, CAS panel, payer cell opens payer peekAckDrawer.test.tsx— header (source batch, ack code), per-segment status list, download button (unchanged)PayerPeekContent.test.tsx— loading skeleton, populated peek, "view all claims" link hrefValidationRulePeekContent.test.tsx— known rule (R050_diagnosis_present) renders full description + X12 reference; unknown rule shows fallback copy- Page tests (per-page drill map coverage): one test per drill target verifying the click routes correctly
Backend (pytest):
test_payer_summary_endpoint— happy path, cache hit/miss, invalidation onclaim_writtentest_provider_detail_extended_response— existing fields unchanged; newrecent_claims+recent_activityarrays populated (10 each, sorted); empty arrays when the provider has no claims/activitytest_provider_detail_extended_response_backwards_compat— clients that ignore the new arrays don't break; old fields identical to SP9 response shape
Target counts: ~25 new frontend tests, ~3 new backend tests. All 249 existing frontend tests + 409 existing backend tests must stay green.
2.9 Smoke test (end-of-SP21)
- Start backend (
backend/.venv/bin/python -m cyclone serve). - Start frontend (
npm run dev). - Open
http://localhost:5173/. - Dashboard: click a KPI tile → page navigates to
/claimswith the right filter. - Dashboard: click a top-providers row → provider drawer opens from the right.
- Dashboard: click a recent-denials row → claim drawer opens.
- Dashboard: click a recent-activity event of kind
claim_paid→ claim drawer opens for the underlying claim. - Claims: click a claim row → claim drawer opens (already worked, regression check).
- Claims: hover the patient name cell → cursor + tint + chevron appear. Click → navigates to
/claims?patient=NAME. - Claims: hover the provider cell → affordance. Click → provider drawer opens.
- Claims: hover the payer cell → affordance. Click → payer peek modal opens (centered).
- Claims: with the claim drawer open, click the payer name inside the drawer → peek opens on top of the drawer. Esc → peek closes; drawer still there. Esc again → drawer closes.
- Remittances: click a remit row → remit drawer opens (was: inline expand only).
- Remittances: click the claim id cell → claim drawer opens.
- Providers: click a card → provider drawer opens.
- Acks: click a row → ack drawer opens (was: only the Download button worked).
- Activity log: click a
remit_receivedevent → remit drawer opens. - Inbox: click a rejected row → claim drawer opens (was: no-op).
- Inbox: click a candidates row → remit drawer opens (was: no-op).
- Reconciliation: click the body of an unmatched claim (not the select button) → navigates to
/claims?claim=IDwith the claim drawer open. - Batch diff: click an added claim id → claim drawer opens.
- Upload: click a streamed claim's "See claim in detail" link → claim drawer opens (if persisted).
- Browser back from any drawer state → drawer closes.
- Commit empty
smoke: end-to-end SP21 (universal drilldown) flow passes.
3. Risk areas
- Stack explosion. Without an enforcement point, it's easy to write code that opens a drawer from inside a peek from inside a drawer. The
DrillStackProviderAPI only exposesopen(which respects the max-2 rule) andcloseTop. There's noopenOnTopOf— the stack is implicit from context. Risk: future contributors might bypass the provider and mount<PeekModal>ad-hoc. Mitigation: the<DrillStackMount>portal at the root ofApp.tsxis the only place peeks render; mounting<PeekModal>outside it renders nothing. - URL vs ephemeral state confusion. Drawers persist in URL; peeks don't. If a user opens a peek, copies the URL, and pastes it later, the peek is gone. Acceptable trade-off — peeks are exploratory; drawers are shareable.
- Cross-page drawer navigation. Clicking a claim id on
/remittancesnavigates to/claims?claim=…. The user lands on the claims page, not on the remittances page with the claim drawer overlaid. This is intentional — the claim drawer lives on/claims. Side effect: the user's remittances filter context is lost on nav. Mitigation: the nav is a soft fade + the URL preserves?status=etc. on/claimsso the user can navigate back with browser back. - Drawer nav state across page navigation. When the user clicks a claim id on
/remittances, lands on/claims?claim=…, and presses browser back, they go back to/remittances(the remits filter is restored). The claim drawer closes. Tested in smoke step 23. - Inbox row click vs checkbox click. Inbox rows have multi-select checkboxes. Row body click opens the drawer; checkbox click toggles selection. The
DrillableCellwrapper goes around the row body, NOT the checkbox. Tested inInbox.test.tsx. - Payer summary cache staleness. The 60s cache + pubsub invalidation is best-effort. Worst case: a payer summary is up to 60s stale after a
claim_written. Acceptable for v1. - Patient name in URL. Navigating to
/claims?patient=John%20Doeputs the patient name in the URL bar. Cyclone is local-only (127.0.0.1) per project context, so this is acceptable for v1. Flagged for SP22 if multi-operator / networked expansion happens. The existing PII scrubber (SP18) only scrubs log lines, not URL bars. - Drawer dim overlay z-index. With a peek over a drawer, three z-index layers: page (z=0), drawer overlay (z=40), peek overlay (z=50). The
<DrillStackMount>portal renders peeks in a dedicated DOM node atz-50; the existing drawer overlays atz-40continue to work. Tested inPeekModal.test.tsx(asserts peek is rendered above drawer overlay).
4. Migration / rollout
- Single feature branch
universal-drilldowncut frommain. - Worktree at
.worktrees/universal-drilldown/, following SP1-3 pattern. - DB migrations: none (uses existing tables:
claims,remittances,batches,activity_events,providers,payers,acks). - No new dependencies (Radix Dialog + zustand already in
package.json). - Phasing within the branch (each PR is shippable independently, each closes its slice of smoke steps):
- PR1 — Foundation primitives + first 3 surfaces:
DrillStackProvider+DrillableCell+PeekModalprimitives, the App-level<DrillStackMount>portal, the hover affordance CSS, the extended/api/providers/{npi}response. Wire Dashboard KPI navigation + Dashboard Top providers drill + Dashboard Recent denials drill. Backend:/api/payers/{payer_id}/summary(the actual provider-overview data lives in the extended/api/providers/{npi}response — no separate provider endpoints needed). (Smoke steps 4-6.) - PR2 —
ProviderDrawer+ activity event routing: Build the ProviderDrawer (Overview tab only in this PR; Claims/Activity tabs come in PR3 with the recent_claims/recent_activity payload rendering). Wire Dashboard Recent activity forclaim_*events (the activity → entity mapping lives in a smalleventKindToEntity()helper introduced here). Wire Providers directory page + Claims provider cell. (Smoke steps 7, 10, 15.) - PR3 —
ProviderDrawertabs + remaining event routing: Add the Claims and Activity tabs to ProviderDrawer, using the extended/api/providers/{npi}payload. Wire Dashboard Recent activity forremit_receivedandprovider_addedevent kinds (still routed via the PR2eventKindToEntity()helper;provider_addedroutes to ProviderDrawer,remit_receivedwill route to the PR4 RemitDrawer — until PR4 lands it shows a graceful "Remit drawer coming in PR4" toast). (Smoke step 7 finishes — all 4 event kinds route correctly by PR4 merge.) - PR4 —
RemitDrawer+ 4 surfaces: Build the RemitDrawer. Wire Remittances row click (replaces inline CAS expand), Inbox candidates + unmatched remit rows, Activity logremit_receivedevent click, Remittances claim-id cell, ClaimDrawer matched-remit link. NoweventKindToEntity()'sremit_receivedbranch resolves to a real drawer. (Smoke steps 13, 14, 17, 19.) - PR5 —
AckDrawer+ final 8 surfaces: Build the AckDrawer. Wire Acks row, Inbox rejected + payer_rejected + done_today rows, Reconciliation body click, Batch diff claim id, Upload "See claim in detail" link, ClaimDrawer payer peek (PayerPeekContent), ClaimDrawer validation-rule peek (ValidationRulePeekContent), refactor existingClaimDrawerto use theDrillDrawershell +DrillDrawerHeader. (Smoke steps 8, 9, 11, 12, 16, 18, 20, 21, 22, 23.)
- PR1 — Foundation primitives + first 3 surfaces:
- Merge to main after final PR + smoke pass; cleanup worktree + branch.