InboxHeader:
- Editorial dark hero replaces the 30px sticky header
- Massive 'Inbox.' (clamp 48-80px) with ghost amber 'TRIAGE' watermark
- Italic subtitle: 'Five lanes, one queue. N need eyes; N done today.'
- Right column: SUN JUN 21 / 14:15 / local
- Live status row: amber pulse + counts + date
Inbox page:
- Amber fold with '↘ FIVE LANES · ONE QUEUE ↙' between header and lanes
- Lanes stay dark (working surface preserved)
- New paper-toned Queue ledger panel below lanes:
- Title block: 'The eye-flow, at a glance.' with need-eyes total
- 5 paper tiles (Rejected / Payer rejected / Candidates / Unmatched / Done today)
- Each tile shows lane accent dot + count + 'Clear'/'rows' chip
- Loading and error states now also use the new InboxHeader for consistency
- SummaryTile helper with paper-toned metric cards
Closes the gap between the SP10 backend (5 lanes) and the SP6
frontend (4 lanes). The Payer-Rejected lane (277CA STC A4/A6/A7)
is now rendered alongside Rejected/Candidates/Unmatched/Done,
with an Acknowledge bulk action that drops claims from the
working surface without erasing the original 277CA rejection
event (audit log stays intact, SP11).
Backend:
* Migration 0010: add payer_rejected_acknowledged_at +
payer_rejected_acknowledged_actor columns + partial index.
* db.py: surface the two new columns on the Claim model.
* inbox_lanes.py: filter acknowledged claims out of the
payer_rejected lane; expose the new fields on the row payload
for forward-compat (e.g. a future 'Recently acknowledged' view).
* api.py:
- POST /api/inbox/payer-rejected/acknowledge
Bulk-acknowledge. Idempotent. Returns transitioned /
already_acked / not_found / not_rejected counts so the UI
can show '3 of 5 were already acknowledged' on a noop bulk.
Writes a 'claim.payer_rejected_acknowledged' event to the
SP11 hash-chained audit log.
- GET /api/inbox/export.csv: accept 'payer_rejected' lane.
* test_acks.py: bump user_version assertion to 10.
* test_lane_filter_acknowledged.py: 4 tests for the lane filter
and forward-compat row payload.
* test_payer_rejected_acknowledge.py: 6 tests for the endpoint
(happy path, idempotency, no-op on non-rejected, missing
ids, 400 on empty, audit-log wiring + chain integrity).
Frontend:
* lib/inbox-api.ts: add payer_rejected to InboxLanes, add
acknowledgePayerRejected(), update exportInboxCsvUrl union.
* hooks/useInboxLanes.ts: add payer_rejected to initial state.
* hooks/useInboxLanes.test.ts: add payer_rejected to mocks.
* components/inbox/BulkBar.tsx: add 'payer_rejected' lane with
Acknowledge action (no Resubmit, no Dismiss — payer-rejected
is not eligible for either).
* components/inbox/BulkBar.test.tsx: add payer_rejected test.
* pages/Inbox.tsx: render the 5th lane, hook up onAcknowledge,
include payer_rejected in the needEyes count.
* pages/Inbox.test.tsx: 3 new tests (5-lane render, need-eyes
count, acknowledge action hits the right endpoint).
* components/inbox/InboxHeader.tsx: doc comment now explains
why payer_rejected rolls up into need-eyes.
Pre-existing typecheck warnings in BulkBar.test.tsx / InboxRow
.test.tsx / Lane.tsx / download.test.ts are unchanged from
main — not touched here.
Test counts: backend 724 -> 734 (+10). Frontend 350 -> 354 (+4).
Three pieces:
- src/lib/download.ts: generic downloadTextFile(filename, mime, text)
helper. Mirrors csv.ts:downloadCsv but takes an explicit MIME type and
drops the BOM prepend (which would corrupt the ISA segment).
- src/lib/api.ts: serializeClaim837(id) → {text, filename}. Fetches
GET /api/claims/{id}/serialize-837, pulls the suggested filename from
Content-Disposition (falls back to claim-{id}.x12 if the header is
missing). Throws ApiError on non-2xx so callers can branch on .status.
- ClaimDrawerHeader: Download icon button between the amount and the
close button. Click → api.serializeClaim837 → downloadTextFile.
Disabled + 'Downloading 837 file' aria-label while the fetch is in
flight so the click feels responsive. Optional onError prop surfaces
fetch failures; defaults to a no-op so existing callers stay clean.
Tests: 3 download.test.ts, 3 api.test.ts, 2 header.test.ts (happy
path + error path). Frontend: 350 passing (+8 from 342).
The live-tail stream (now reachable through the new Vite /api proxy)
started delivering activity rows with kind="manual_match", a kind the
ActivityFeed kindConfig map didn't know about. The map lookup returned
undefined, and reading .icon on it threw — taking the whole Activity
Log page down.
Two fixes:
1. Add "manual_match" to the ActivityKind union in src/types/index.ts
and to the kindConfig map in src/components/ActivityFeed.tsx (with
a Wrench icon + neutral tone), so the kind is properly typed and
rendered.
2. Add a FALLBACK_KIND guard so any future backend-emitted kind that
arrives before the frontend is updated no longer crashes the page.
kindConfig stays exhaustive for known kinds, so TypeScript still
catches missing entries at compile time.
Also adds src/components/ActivityFeed.test.tsx with three regression
tests (empty state, multi-item render, unknown-kind safety).
Brings the SP5 live-tail implementation into main:
Backend
* EventBus (cyclone.pubsub): per-kind fan-out with drop-oldest overflow
* FastAPI lifespan initializes the bus + db once per process
* store.add() publishes claim_written / remittance_written /
activity_recorded events on every batch write
* GET /api/{claims,remittances,activity}/stream: NDJSON snapshot +
live subscription + 15s idle heartbeat
* EventBus.unsubscribe() lets the tail loop release its queue on
client disconnect (no queue leak per open stream)
Frontend
* src/lib/tail-stream.ts: streamTail() async-generator over fetch
* src/store/tail-store.ts: zustand with FIFO cap 10k per slice
* src/hooks/useTailStream.ts: connecting/live/reconnecting/stalled/error/closed
state machine with 1→2→4→8→16→30s backoff
* src/hooks/useMergedTail.ts: base + tail merge with filter
* src/components/TailStatusPill.tsx: badge + Reconnect button
* Claims, Remittances, ActivityLog pages wired to the tail
Tests
* 437 backend tests pass (was 418 before SP5)
* 154 frontend tests pass (was 124)
* npm run typecheck clean
* end-to-end smoke: open /api/claims/stream, POST 837, see new claims
arrive in real time without refresh
# Conflicts:
# src/pages/ActivityLog.tsx
# src/pages/Remittances.test.tsx
# src/pages/Remittances.tsx
- RawSegmentsPanel: summary now reads 'Show N raw segments' for
non-empty (with singular pluralization). The previous identical
ternary branches read as a bug.
- PartiesGrid: replace the verbose intersection cast on AddressBlock
with a direct ClaimDetailAddress cast (the union's other arm is
Record<string, never>, ruled out by isEmptyAddress above).
ClaimDrawerError.tsx already wraps its error surface with role=alert
(line 36). Apply the same pattern to the ValidationPanel errors
sub-section so assistive tech announces blocking validation failures
when the drawer opens, matching the established sibling pattern.
Warnings stay decorative (no role change) — they're advisories, not
blocks.
The parametrized variant test only exercised 4 of the 6 mapped states
(submitted/denied/paid/pending). Add accepted (-> default) and matched
(-> success) so the entire STATE_VARIANT map is verified, not just the
branches whose tokens happened to be easy to reach.