The implementer caught that claims.id is a single-column PK, which
prevents the same CLM01 from existing in multiple batches. That
makes the spec's pre-flight 409 workflow unreachable and resubmits
impossible. Migration 0014 relaxes the PKs to composite (batch_id,
id) on both claims and remittances, and updates all FKs that
referenced the old single-column PK. Task 1.3 in the plan is now
the migration; the previous Task 1.3 (helper tightening) is
renumbered to Task 1.4 and will run after 0014 lands.
SP21 Task 2.5 — the Dashboard's 'Recent activity' card now routes clicks
to the matching entity drawer / page by event kind:
- claim_* → /claims?claim=<id> (drawer in Phase 5)
- provider_added → /providers?provider=<npi> (ProviderDrawer)
- remit_received → toast 'coming in a later phase' (RemitDrawer in Phase 4)
- anything else → toast (manual_match, unknown kinds)
Implementation:
- New src/lib/event-routing.ts with the eventKindToUrl() helper,
plus a unit test covering all 6 + default branches.
- src/components/ActivityFeed.tsx gains an optional onItemClick
prop; when set, each row gets role='button', tabIndex=0, the
drillable hover affordance (chevron + tint), and an Enter/Space
keybinding. e.stopPropagation() is called before the handler so a
parent row click can't double-fire (same fix as Task 2.4).
- src/pages/Dashboard.tsx wires the handler on the 'Recent activity'
card via eventKindToUrl + sonner toast for unhandled kinds.
- Backend: CycloneStore.recent_activity() now exposes claimId and
remittanceId on each row (read from ActivityEvent.claim_id /
remittance_id) so the routing helper has the entity ids it needs.
- The frontend Activity interface gains optional claimId /
remittanceId fields; the in-memory sample data and the
addClaim store action populate them so the dashboard works in
both API-configured and sample-data modes.
Quality-fix iteration on 078c9ad. Resolves the 4 Important issues
flagged in the code review while preserving everything that's working.
Issue 1 — ProviderDrawer silently swallowed error states (infinite
skeleton on any failure). Now mirrors ClaimDrawer/RemitDrawer:
- Destructure { data, isLoading, isError, error, refetch }
- Compute errorKind = ApiError(404) → not_found, else → network
- New ProviderDrawerError.tsx renders the right copy + retry/close
- Body branches on errorKind → loading → data
Issue 2 — useProviderDetail did not match the useClaimDetail contract:
- 404 retry guard (no retries on 404, <=3 on everything else)
- Explicit typed return shape { data, isLoading, isError, error,
refetch }
- npi === null short-circuit after the useQuery call
Issue 3 — Provider drilldown was non-functional in demo mode. Added
the useSyncExternalStore fallback to useAppStore.providers (same
pattern as useProviders), gated by api.isConfigured. Unknown NPI in
demo mode surfaces via the error branch instead of an infinite
skeleton.
Issue 4 — Test was shape-only (1 case). Rewrote with vi.hoisted +
vi.fn() hook mock and 7 cases covering null/loading/404/network/
close/success/hook-call-shape.
Issue 5 — Replaced h-[calc(100%-64px)] with a flex layout
(flex h-full flex-col on DialogContent, flex h-full flex-col
overflow-y-auto on inner wrappers). Mirrors RemitDrawer's pattern;
no more coupling to DrillDrawerHeader's padding/font sizes.
Issue 7 — Removed the dead npi === null ? null : (...) wrapper
inside DialogContent; Dialog open={npi !== null} already gates it.
Issue 8 — Trailing newlines added across all touched + created files.
Self-review: tsc clean on changed files; drawer test suite 211/211
passing; full test suite 369/372 (3 pre-existing Inbox/Reconciliation
failures unchanged). ProviderDrawer now 7/7.
Three independent improvements that fix real browser-facing bugs:
1. CORS: allow 127.0.0.1:5173 in addition to localhost:5173. Both
resolve to the same Vite dev server but CORS treats them as distinct
origins, so tabs opened via the IP form silently break.
2. CORS: support CYCLONE_ALLOWED_ORIGINS env var (comma-separated) for
LAN / staging hosts. The middleware reads it at module import.
3. Catch-all exception handler: returns JSON 500 with CORS headers
instead of a bare Uvicorn text/plain response. Without this, any
unhandled exception is misreported by browsers as a CORS error
because the body can't be read without the allow-origin header.
4. IntegrityError → 409: when (batch_id, patient_control_number) is
UNIQUE-constrained and a duplicate collides, return 409 with the
batch id instead of letting the exception 500. Same problem as (3)
for the most common ingest failure mode.
Tests added:
- test_cors_headers_present_for_loopback_ip
- test_cors_extra_origins_via_env (uses importlib.reload because the
allow-list is built at module import)
The implementation shipped with 10 inline bug fixes that weren't reflected
in the original plan document. This commit updates the plan so it
matches the as-built code and adds a summary table near the top so a
future reader of the plan knows what was adjusted and why.
- Bug 1a: AckTimeoutError.__init__ signature widened from int to float
plus adds self.phase attribute for report remediation hints.
- Bug 1b: wait_for passes timeout_s through without int() round-trip.
- Bug 2: Markdown table assertion updated to match the actual format.
- Bug 3: Added ## Remediation section to write_report_md for soft/hard fails.
- Bug 4: 835 expected-by text now says 'typically the following Monday'.
- Bug 5: Removed bogus HealthSnapshot import in test_pipeline.py.
- Bug 6: All phase tests wrapped in 'async with CyclonePipeline(...) as p:'.
- Bug 7: Playwright + UploadPage imports moved to module level in pipeline.py.
- Bug 8: Added _phase_records_to_results() helper for PhaseRecord→PhaseResult.
- Bug 9: Resume test uses _make_stub factory so stubs append to completed_phases.
- Bug 10: CLI uses context_settings={'allow_interspersed_args': True}.
Add a 'Pipeline automation agent' section under '## Dev' that
points operators to the sibling project at
/Users/openclaw/dev/cyclone-pipeline/. Add a 'Sub-project 22
(shipped)' entry to the roadmap so the new project is discoverable
from the cyclone README's release history.
- Replace small header with editorial hero: massive 'Compare *batches.*'
(clamp 48-80px serif, italic for 'batches.') + ghost 'DIFF' watermark
+ GitCompareArrows pill (837P/835/first vs second) + pick-a-batch hint
- Add torn-page fold with '↘ A → B ↙' label and 48-dot perforation
- Cream paper plane (max-w-[1280px] mx-auto) with paper-grain SVG and
double-bordered title block: 'SHEET 01 · COMPARE TWO BATCHES' over
'Compare them.' (clamp 48-96px) plus A→B wire indicator (blue A,
amber B, paper-ink-3 placeholder)
- New Folio helper renders the § N + vertical-rl italic label; reused
on the picks and diff sections
- § 01 The picks: BatchPicker gets a paper-toned card with side badge
(A blue / B amber tinted square) and claim count, preserving the
data-testid='diff-picker-{a,b}' and 'diff-picker-kind-{kind}' and
'diff-picker-option-{id}' contracts
- § 02 The diff: conditional rendering (awaiting-picks dashed card,
loading skeleton, not-found, error, empty diff, full diff view)
with paper-toned wrappers
- BatchDiffView refactored to paper-toned chrome: SideMeta cards
(warm surface, surface-ink text), SummaryCards → new DiffKpiTile
helper (success/destructive/amber/ink accent rails with lucide
icons), SectionTables → tone='paper' tables with cream header band
and surface-ink text. All section/row/indicator testids preserved.
- Action footer: 'Comparing A with B.' italic note + Refresh / Clear
picks buttons; paper-toned container, sits inside the paper plane
- Bottom footer: 'End of sheet 01 / Cyclone · Batch diff / A vs B
or awaiting picks'
Round 11/11 — last page in the hybrid sweep.
- Add tone='paper' prop to shared Table/TableHeader/TableRow/TableHead/
TableCell components; paper-toned chrome swaps dark muted/20 header
for cream paper, dark hover for warm cream hover, dark selected for
paper blue tint, muted-foreground text for surface-ink. Dark tone
is the default and unchanged.
- Extend BatchesList with tone='paper' variant: cream hover, warm
surface-ink text, paper-tinted open-row (blue tint + ring). Kind
badge (text-sky-300 / text-amber-300 + data-testid) and row
data-testid='batch-row-{id}' preserved for the test contract.
- Replace PageHeader with editorial hero: massive 'Parsed *batches.*'
(clamp 48-80px serif, italic for 'batches.') + ghost 'PARSED'
watermark + Files pill (837P/835/envelope) + tap-to-open hint
- Add torn-page fold with '↘ THE REGISTER ↙' label and 48-dot perforation
- Cream paper plane (max-w-[1280px] mx-auto) with paper-grain SVG and
double-bordered title block: 'REGISTER · PARSED BATCHES' over
'The register.' (clamp 48-96px) plus on-file count column
- Folio system: § 01 Vital signs (4 paper KPI tiles via new
BatchesKpiTile: On file / 837P / 835 / Claims with ink/blue/amber/
success accent rails and Icons from lucide), § 02 The register
(paper-toned BatchesList)
- Error/Loading/Empty states wrapped in paper-toned containers
- Footer: 'End of register / Cyclone · Batches / N rows'
Round 10/11.
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
- Dark editorial hero: 'Manual pairing.' (80px serif, italic pairing)
- 'AUTO-MATCH PAUSED' amber status pill on the right
- Ghost 'PAIRED' watermark behind the hero
- Dramatic torn-page fold with shadow + perforation + 'PAIR THEM BY HAND' label
- Paper plane with title block ('RECONCILIATION · SHEET 01' / 'Pair them.' 96px)
- Folio system (§ 01 Vital signs, § 02 The match, § 03 The action)
- KPI strip with 3 paper-toned tiles (Unmatched claims / remits / in queue)
- Two-column pairing surface with paper cards, blue/amber accent rails
and a center vertical bridge that flips to green-merge when both selected
- Active row uses paper-tinted blue (claim) or amber (remit) backgrounds
- Action footer with italic 'Ready to pair' / 'Now pick a remit' states
- Empty state: 'Nothing pending.' with circle-dashed icon on cream
- Footer: 'END OF SHEET 01 / CYCLONE · RECONCILIATION / N ROWS PENDING'
- Dark editorial hero: 'Parse an X12 file.' (80px serif, italic X12)
- Ghost 'DROP' watermark behind the hero
- Dramatic torn-page fold with shadow + perforation + 'DROP & PARSE' label
- Paper plane with title block ('INGESTION · SHEET 01' / 'Drop a file.' 96px)
- Right column accepts indicator (.txt / 837P · 835 / backend status pill)
- Folio system (§ 01 The drop, § 02 The stream, § 03 The archive)
- Paper-toned drop zone, selects, and claim cards (replaces dark surface-2)
- Stream section with progress bar (gradient fill) and 2-column totals
- Recent batches list with 'batches' count in footer
- Empty state with inbox icon when no batches exist
- Footer: 'END OF SHEET 01 / CYCLONE · INGESTION / N BATCHES'
Update _get_typed in the plan to:
- raise immediately on 4xx (terminal)
- retry on 5xx and RequestError (transient)
- use BACKOFF_S = (1.0, 2.0) instead of (1.0, 2.0, 4.0)
- use RuntimeError instead of assert for the context-manager guard
Also add test_4xx_is_terminal_not_retried to the plan's test list.