Commit Graph

370 Commits

Author SHA1 Message Date
Nora e158871a9a feat(auth): sessions module + permissions matrix + rate limiter
SQLite drops tzinfo on DateTime roundtrip — normalize on read/write
so callers see tz-aware datetimes.
2026-06-22 15:34:05 -06:00
Nora 1ca50e2bc0 test(auth): tighten duplicate-username test + add update_password test 2026-06-22 15:32:56 -06:00
Nora 74d7056284 feat(auth): users module with bcrypt hashing + CRUD 2026-06-22 15:32:56 -06:00
Nora 0ba91040f1 feat(migration): 0010 users + sessions, 0011 audit_log.user_id 2026-06-22 15:32:56 -06:00
Nora dc83d7bef2 feat(db): add User and Session models 2026-06-22 15:32:56 -06:00
Nora 9d4798a124 feat(deps): add passlib[bcrypt] for password hashing
passlib 1.7.4 + bcrypt >= 4.1 are incompatible (passlib probes
bcrypt.__about__ which 4.x removed). Pin bcrypt<4.1.
2026-06-22 15:32:56 -06:00
Nora d552d3d3ec docs(plan): auth implementation plan — 29 tasks, ~40 files 2026-06-22 15:32:45 -06:00
Nora 42a826cb73 docs(spec): auth (admin/user/viewer) design — sessions, RBAC, login flow 2026-06-22 15:32:45 -06:00
Tyler 05b43078b9 chore(deps): consolidate dev deps into [project.optional-dependencies] dev
Follow-up to 7c1be58. pytest-randomly was added to a PEP 735
[dependency-groups] dev block by 'uv add --dev', leaving the
project with two 'dev' groups (the legacy [project.optional-
dependencies] dev and the new [dependency-groups] dev). To get
the full dev env someone had to run 'uv sync --extra dev --group
dev', which is easy to miss.

This commit moves pytest-randomly>=4.1 into the
[project.optional-dependencies] dev list (where pytest,
pytest-cov, pytest-asyncio, and httpx already live) and drops
the [dependency-groups] block. 'uv sync --extra dev' now
installs the full dev toolchain. uv.lock updated accordingly:
the [package.dev-dependencies] and [package.metadata.requires-
dev] sections are gone, and pytest-randomly is part of the
standard dev extra.
2026-06-22 11:44:39 -06:00
Tyler 7c1be58860 chore(deps): add cryptography dep, pytest-randomly dev dep
Two follow-ups to the v0.2.0 release:

1. Add cryptography>=49.0,<50 to the [project.dependencies] list.
   cyclone.backup / cyclone.backup_service import it at module top
   level, so it has to be a hard dep — not an extra — or the test
   suite fails to collect on a fresh 'uv sync'. A clean install
   (without this commit) gets 28 collection errors with no tracked
   file changes. This was latent because the original 99M venv
   had cryptography installed out-of-band and masked the missing
   dep declaration.

2. Add pytest-randomly>=4.1.0 to the dev dep group.
   Used to characterize the test suite's order-dependence: under
   alphabetic order the suite shows 7 failures; under seed=12345
   it shows 34. That variance is evidence of test-order state
   interactions, not venv issues. Keeping pytest-randomly in
   the dev deps so this can be re-verified on demand.

Note: pytest-randomly was added with 'uv add --dev', which uses
the PEP 735 [dependency-groups] syntax. pyproject.toml now has
two 'dev' groups — the legacy [project.optional-dependencies] dev
= [pytest, pytest-cov, pytest-asyncio, httpx] and the new
[dependency-groups] dev = [pytest-randomly]. To get the full dev
env, run: uv sync --extra dev --group dev. Worth consolidating
in a follow-up.
2026-06-22 11:43:32 -06:00
Tyler 9bca4b608a feat(release): v0.2.0 — batch 837 export, ClaimCard, theme tokens
Backend:

- New POST /api/batches/{id}/export-837: regenerate X12 837 files
  for a list of claim_ids into a ZIP using HCPF file naming standards,
  with a unique interchange/group control number per export. Wire
  the clearhouse Loop 1000A (NM1*41 + PER) and per-payer receiver
  (NM1*40) blocks so the serializer no longer falls back to
  CYCLONE / RECEIVER placeholders.
- /api/parse-837 and /api/parse-835 now surface the server-side
  batch_id in both JSON and NDJSON response shapes so the frontend
  can hit batch-scoped endpoints without an extra listBatches
  round-trip.
- Filename helpers and the 837 serializer updated to match the new
  HCPF envelope; tests cover batch export, parse batch_id, and the
  serializer's control-number uniqueness guarantee.

Frontend:
- New shared components: ClaimCard, ClaimCard837, DominantKpiCard,
  EditorialNote, ExportBar, TickerTape, and a charts/ set
  (BarChart, HBarChart, SegmentedBar, AgingBars).
- New useBatchExport hook driving ExportBar's download flow against
  the new endpoint.
- ClaimDrawer, Lane, and Layout migrated from raw CSS-variable
  colors to Tailwind theme tokens (bg-card, text-foreground,
  border/60, etc.) for consistency with the rest of the instrument
  chrome; the active tab indicator gains a subtle accent glow.
- Upload, Inbox, Batches, BatchDiff, Reconciliation, and Acks pages
  reworked to compose the new shared components and consume the new
  batch-scoped API surface (notably ExportBar wired into Batches).

Tooling / Docs:
- Add audit-uiux.mjs and a docs/goodclaim.x12 sample fixture.
- Update ClaimDrawer testids and add coverage for the new
  components and the useBatchExport hook.

Rolls up into the v0.2.0 release tag.
v0.2.0
2026-06-22 11:01:58 -06:00
cyclone 35298907bc docs(spec): add Ubuntu Docker deployment design
Production deployment of Cyclone on a single Ubuntu Linux server:
- Two-container Docker Compose (backend + nginx frontend)
- App-layer auth: username/password, argon2id, session cookies
- 3 fixed roles: admin / operator / viewer with documented action matrix
- SQLCipher encryption at rest, key as Docker secret
- Existing BackupService autostarts (24h interval, 14-day retention)
- LAN-only bind, single operator, VPN for outside access
- Manual update via 'docker compose pull', env-var rollback
- SFTP stays stub (download ZIP + MFT UI); listed as v2

Builds on the parent 2026-06-19 production-readiness spec which
shipped local-only in-memory store + react-query wiring.
2026-06-22 10:43:26 -06:00
Tyler 22720168c4 test(claims): wrap page in DrillStackProvider, switch to DrillDrawerHeader testids
Phase 5 Task 5.8 (PartiesGrid) and 5.9 (ValidationPanel) added useDrillStack()
calls, so Claims.test.tsx needs a DrillStackProvider wrapper around the
page render to keep the hook happy. Task 5.10 refactored the ClaimDrawer
header onto the shared DrillDrawerHeader, so the deep-link and close-button
tests need to find the title via <h2> and the close button via
aria-label='Close drawer' instead of the now-removed header-id /
header-close testids.

Without this fix the page-level Claims tests report 3 failures
(deep link, URL clear after close, ?-mark with drawer closed) that
were not failing on the Phase 4 base (9a313d2).
sp21-complete
2026-06-21 18:10:43 -06:00
Tyler 1c0d855b8e refactor(claim-drawer): mount on shared DrillDrawerHeader shell 2026-06-21 18:10:43 -06:00
Tyler 8db5db7610 feat(claim-drawer): validation rule opens peek with rule catalog 2026-06-21 18:10:43 -06:00
Tyler 786ead8c94 feat(claim-drawer): payer name opens PeekModal on top of drawer 2026-06-21 18:10:43 -06:00
Tyler 6c773c1159 feat(upload): streamed claim cards offer drill to persisted entity 2026-06-21 18:10:43 -06:00
Tyler 5c7e9b6168 feat(batch-diff): claim ids drillable to /claims?claim=ID 2026-06-21 18:10:43 -06:00
Tyler ac87ed4908 feat(reconciliation): card body drillable, select button split 2026-06-21 18:10:43 -06:00
Tyler 4a8ce1a524 feat(inbox): rejected + payer_rejected + done_today rows drillable 2026-06-21 18:10:43 -06:00
Tyler 5053a1ea8e feat(acks): row click opens AckDrawer 2026-06-21 18:10:43 -06:00
Tyler 33fa899217 feat(drill): AckDrawer with header + segment status list 2026-06-21 18:10:43 -06:00
Tyler 6fdbceefc2 feat(drill): useAckDrawerUrlState — ?ack= URL sync 2026-06-21 18:10:43 -06:00
Tyler 7290cac643 plan+spec: add migration 0014 to relax PKs; renumber plan Task 1.3 -> 1.4
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.
2026-06-21 18:09:06 -06:00
Tyler 5e8c7b11ea plan: parse-decide workflow implementation for 837/835 upload dedup 2026-06-21 17:53:21 -06:00
Tyler 9c0cec8f0c spec: parse-then-decide workflow for 837/835 upload dedup 2026-06-21 17:47:03 -06:00
Tyler 9a313d2c1b feat(activity): remit_received events drill to RemitDrawer sp21-phase4 2026-06-21 17:19:31 -06:00
Tyler 2eb61f16ff feat(reconciliation): candidate rows drill to RemitDrawer or ClaimDrawer 2026-06-21 17:19:31 -06:00
Tyler 12c2913ba1 feat(batchdiff): remit rows drill to RemitDrawer 2026-06-21 17:19:31 -06:00
Tyler 54440da2cd feat(inbox): candidates + unmatched rows drill to RemitDrawer or ClaimDrawer 2026-06-21 17:19:31 -06:00
Tyler 7427838292 feat(remits): row click opens RemitDrawer (replaces inline CAS expand) 2026-06-21 17:19:31 -06:00
Tyler b606e8c9a2 plan: drop claims UNIQUE + 409 UX implementation plan 2026-06-21 17:02:10 -06:00
Tyler ac709c07c3 spec: self-review fixes (explicit claim_id arg, defer_foreign_keys) 2026-06-21 16:58:50 -06:00
Tyler 08e83da91d feat(drill): ProviderDrawer — Claims + Activity tabs from extended /providers/{npi} 2026-06-21 16:56:36 -06:00
Tyler 3f672d5db3 spec: drop claims UNIQUE constraint + 409 UX sp21-phase3 2026-06-21 16:54:07 -06:00
Tyler 65d98cf674 feat(dashboard): Recent activity events route to entity by kind
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.
sp21-phase2
2026-06-21 16:41:15 -06:00
Tyler d15c04d983 feat(claims): provider cell drillable to /providers?provider=NPI 2026-06-21 16:41:15 -06:00
Tyler 980627b675 feat(providers): directory cards drillable — opens ProviderDrawer 2026-06-21 16:41:15 -06:00
Tyler 1db6b8841c fix(drill): ProviderDrawer quality pass — error branching, demo fallback, flex layout
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.
2026-06-21 16:41:15 -06:00
Tyler 0678e25de7 feat(drill): ProviderDrawer with Overview tab + DrillDrawerHeader shell 2026-06-21 16:41:15 -06:00
Tyler 5b3b8619e6 feat(drill): useProviderDrawerUrlState — provider drawer URL state (mirrors remit hook) 2026-06-21 16:41:15 -06:00
Tyler b6607b2009 feat(api): harden CORS and surface 500/409 errors with proper CORS headers
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)
2026-06-21 16:29:59 -06:00
Tyler c0b7924aad docs(sp22): drop speculative uv.lock note from deviations 2026-06-21 16:28:24 -06:00
Tyler ebd2834cc4 docs(sp22): backfill plan with 10 as-built deviations
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}.
2026-06-21 16:28:07 -06:00
Tyler 888c99d848 docs: cross-link to cyclone-pipeline sibling project
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.
2026-06-21 16:12:10 -06:00
Cyclone UI 388ea6e49b Refine Batch Diff page with hybrid dark/paper treatment
- 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.
2026-06-21 14:37:36 -06:00
Cyclone UI 736bf4d333 Refine Batches page with hybrid dark/paper treatment
- 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.
2026-06-21 14:34:26 -06:00
Tyler 7db5e448cd feat(dashboard): Recent denials row drillable to /claims?claim=ID sp21-phase1 2026-06-21 14:28:49 -06:00
Tyler 5d6b5894f3 feat(dashboard): Top providers row drillable to /providers?provider=NPI 2026-06-21 14:28:49 -06:00
Tyler 88da3a6246 feat(dashboard): KPI tiles drillable — navigate to /claims with filter 2026-06-21 14:28:49 -06:00