Trent 4d95108ac5
CI / frontend (push) Canceled after 0s
CI / backend (push) Canceled after 10s
Fix docker file issue
2026-08-12 00:56:55 +08:00
2026-08-12 00:56:55 +08:00
2026-08-11 20:17:53 +08:00
2026-08-11 20:54:59 +08:00
2026-08-12 00:56:55 +08:00
2026-08-11 20:17:53 +08:00
2026-08-11 20:17:53 +08:00
2026-06-26 04:20:20 +08:00
2026-08-11 20:17:53 +08:00
2026-06-28 01:59:31 +08:00
2026-06-26 05:04:39 +08:00

VigilCare Records

A clinical records digitization platform built with ASP.NET Core 8, PostgreSQL, MinIO, Redis, and a Vue 3 workstation UI. The domain models the scan-to-approved lifecycle at the center of any paper chart digitization system: scanned document upload, human data entry, dual-human verification with separation of duties, site-configurable clinical approval routing, atomic promotion to live clinical tables, and governed correction via supersession.

Implementation status: Phases 113 are complete. Phase 7 delivers the digitization workstation UI (vigilcare-records-web) with role-specific views for intake, entry, verification, clinical approval, live capture, patient history, and supervisor dashboard. Phase 8 adds Prometheus metrics, supervisor work-queue overview, cursor-paginated batch audit trails, promotion deferral with exponential backoff retry, and a Docker monitoring stack (Prometheus on 9095, Grafana on 3013). Phase 9 adds extended demo seed data (10 batches across all types, tracks, and statuses), the E2E verification script (scripts/run-vigilcare-records-verification-p9.sh), and clinical scenario documentation (digitization-workstation-guide.md). Phase 10 adds barcode/QR cover sheets for high-volume backfill: generate printable cover pages with encoded batch type, track, optional patient, and optional entry-clerk pre-assignment; barcode-assisted upload auto-creates batches and skips manual classification. Phase 11 adds an HL7 FHIR R4 read-only API for promoted clinical data (Patient, Encounter, Observation) with LOINC code mapping, search bundles with pagination links, $everything, and an administrator FHIR Explorer view at /fhir-explorer. Phase 12 makes the backend the single source of truth for batch-type field requirements — fieldRequirements metadata on batch and draft responses drives which entry/verification form sections render (allergies, medications, encounter summary, observations). Phase 13 adds optional OCR-assisted draft pre-fill (Ocr:Enabled, Azure Document Intelligence or self-hosted Tesseract), confidence scoring on draft fields, and UI confidence indicators; disabled by default. Post-phase hardening includes health check endpoints, user management APIs, auth rate limiting, document access audit events, batch cancellation, list/queue sorting (sortBy/sortDirection), unified promotion retry logic, normalized patient deduplication, assignment-time IN_ENTRY transitions, API-proxied document streaming for the scan viewer, and CORS for production frontend origins. See Implemented Phases and the gap analysis for remaining work.

Domain Model — How It Maps to a Real Clinical System

In a paper-based hospital, patient records exist as handwritten charts, ward books, and index cards. VigilCare Records converts these into structured digital data through a governed workflow: an intake clerk scans a paper chart and uploads it to MinIO. A data entry clerk reads the scan and transcribes patient demographics, encounter context, and observation values into structured draft fields. A verifier (who cannot be the entry clerk) compares the draft against the original scan and approves or rejects field-by-field. For high-stakes batch types (vitals, labs, encounter summaries, medications), a clinical approver provides a final sign-off before promotion. On approval, draft records are promoted atomically to VigilCareClinical's live tables — Patient, Encounter, Observation — where they enter the real-time alerting and scoring pipeline. Unapproved drafts never trigger alerts, scoring, or surveillance.

DigitizationBatch ────────────── one unit of work: one scan, one chart section
 ├── ScannedDocument              MinIO object — PDF/JPEG/PNG, SHA-256, never deleted
 ├── DraftPatient                 demographics, allergies, blood type, medications (structured from paper)
 ├── DraftEncounter               admission date, department, room/bed, admission reason
 ├── DraftObservation[]           one measurement per row: observation code, value, unit, recordedAt
 └── DigitizationEvent[]          append-only audit log: every status transition, every actor, every timestamp

DigitizationBatch

The unit of work for one digitization effort — typically one scanned document or one logical chart section (vitals sheet, lab report, admission face sheet). Tracks the full lifecycle from upload through promotion with actor attribution at every gate. Seven batch types (PATIENT_REGISTRATION, ENCOUNTER_SUMMARY, VITALS_SHEET, LAB_RESULTS, MEDICATION_LIST, ALLERGY_UPDATE, MIXED) drive completeness validation rules on submit. Two tracks: BACKFILL (full dual-human gate for historical charts) and LIVE_CAPTURE (clinician attestation at bedside, lighter gate).

DraftPatient

Structured patient fields extracted from paper — full name, date of birth, sex, blood type, allergies, emergency contact, medications. Stored in PostgreSQL as JSON columns (allergies_json, medications_json); the draft API exposes them as allergies and medications string arrays on read/write. On approval of a PATIENT_REGISTRATION or ALLERGY_UPDATE batch, merges into VigilCareClinical's live Patient record.

DraftEncounter

A clinical episode extracted from the chart: admission date, department, room/bed, admission reason, discharge diagnosis. Promotes to VigilCareClinical's live Encounter.

DraftObservation

A single measurable value: observation code, numeric value, unit, recordedAt (from the chart, required), optional note. Subject to the same plausibility ranges as VigilCareClinical ingest. Never written to live observations until batch approval.

ScannedDocument

Stored in MinIO. Original paper is the legal source; the scan is the working reference for entry and verification. Scanned documents are never deleted when a batch is rejected — only the draft is returned for correction.

DigitizationEvent

Append-only audit log entry for every state transition, field-level correction, and workflow action. Each event records the batch, event type, actor user ID, timestamp, and optional JSON metadata (field checks, rejection reasons, assigned-to user).


Features

  • Document Upload and Batch Creation — multipart upload (PDF, JPEG, PNG; max 25 MB) with MinIO storage (object key scans/{year}/{month}/{batchId}/{sha256}.{ext}), SHA-256 integrity hash, presigned GET URLs (15-minute expiry on batch detail); duplicate detection per patient within 24 hours by SHA-256 (409 DUPLICATE_DOCUMENT); cross-patient duplicate scans allowed; optional supersedesBatchId creates a correction batch linked to a promoted batch; batch created in UPLOADED status with DigitizationEvent audit trail; optional coverSheetCode looks up a cover sheet barcode, applies encoded batch type/track/patient, redeems the cover sheet on success (409 COVER_SHEET_ALREADY_USED on reuse), and auto-assigns the batch when the cover sheet has assignToUserId set
  • Cover Sheet SystemPOST /cover-sheets/generate creates 1100 cover sheets with unique VCR-CS-{8-hex} codes encoding batch type, track, optional patient, and optional entry-clerk pre-assignment; GET /cover-sheets/lookup/{code} resolves a barcode for intake auto-fill; GET /cover-sheets lists sheets with isUsed/patientId filters; POST /cover-sheets/{id}/pdf and POST /cover-sheets/batch-pdf produce printable PDFs with QR codes (QRCoder); cover sheets are single-use and linked to the batch they create via batchId
  • Batch AssignmentPATCH /digitization-batches/:id/assign assigns an entry clerk with a Redis lock (SET batch:assign:{id} NX EX 3600) to prevent double-assignment; transitions UPLOADED → IN_ENTRY immediately and writes an entry_started audit event; only UPLOADED batches can be assigned; 409 BATCH_ALREADY_ASSIGNED on conflict
  • Batch CancellationPOST /digitization-batches/:id/cancel (administrator only) permanently cancels batches in UPLOADED, IN_ENTRY, or REJECTED status with a mandatory reason (min 5 characters); releases the Redis assignment lock; CANCELLED is terminal
  • Draft Data Entry — full CRUD on draft payload: upsert patient demographics, upsert encounter context, add/edit/delete observation rows; plausibility validation on observation values at draft save time (catches decimal errors before verification); DraftService retains a fallback UPLOADED/REJECTED → IN_ENTRYtransition when entry begins without prior assignment; draft save requires the acting user to matchenteredByUserIdor holdAdministrator role (409 BATCH_NOT_ASSIGNED`)
  • Submit for Verification — validates completeness per batch type (e.g. vitals sheet requires linked patient, encounter context, and at least one observation with recordedAt); transitions IN_ENTRY → PENDING_VERIFICATION; returns 422 with missing fields if incomplete
  • Verification and Rejection — verifier reviews entry against the scan with field-level checks (fieldName, status: ok|warning|error, optional note); verify pass transitions to VERIFIED or AWAITING_CLINICAL_APPROVAL based on site configuration for the batch type; verify fail transitions to REJECTED with mandatory reason; separation of duties enforced: entry clerk cannot verify their own batch (409 SEPARATION_OF_DUTIES_VIOLATION)
  • Clinical Approval Routing — site-configurable per batch type (SiteConfig.ClinicalApprovalRequired); high-stakes batch types (encounter summary, vitals sheet, lab results, medication list, mixed) route to AWAITING_CLINICAL_APPROVAL after verification for physician sign-off; low-stakes types (patient registration, allergy update) go directly to VERIFIED
  • Approval and PromotionPOST /digitization-batches/:id/approve (on ApprovalController only) atomically promotes draft data to live VigilCareClinical tables (patients, encounters, observations) in a single PostgreSQL transaction via shared ExecutePromotionCoreAsync; generates MRN via PostgreSQL sequence (VCR-000001); patient deduplication by case-insensitive normalized name + DOB with fuzzy-match warnings (Levenshtein distance ≤ 3) logged when a near-duplicate exists; encounter matching by patient + department + active status; writes observation.created outbox events for downstream consumers; separation of duties enforced (approver cannot be entry clerk or verifier); Idempotency-Key header required for safe retries with 24-hour TTL; retroactive alert policy (enableRetroactiveAlerts) controls whether backfill observations emit outbox events; on transient infrastructure failure returns 202 with PROMOTION_DEFERRED — batch stays APPROVED and PromotionRetryService retries via POST /digitization-batches/:id/promote using the same core promotion path
  • Promotion Result QueryGET /digitization-batches/:id/promotion-result returns live entity IDs (patient, MRN, encounter, observations) created during promotion
  • Corrections and Supersession — approved live observations are never silently edited; a correction uploads a new batch with supersedesBatchId, goes through the full entry → verify → approve cycle, and on promotion marks the original batch's live_observations rows as is_superseded (append-only — never deleted); 422 SUPERSEDED_BATCH_NOT_PROMOTED, 409 BATCH_ALREADY_SUPERSEDED, and 404 SUPERSEDED_BATCH_NOT_FOUND guard invalid supersession targets; correction batches inherit patient context and reuse the original promotion encounter
  • Patient Digitization HistoryGET /patients/:id/digitization-history returns all batches for a patient with correction chain metadata (isCorrection, hasBeenSuperseded, supersededByBatchId), live vs superseded observation counts, summary totals, and per-batch audit trails; 404 PATIENT_HISTORY_NOT_FOUND when no batches exist for the patient
  • Patient Registry SearchGET /patients/search?q= searches live patients by MRN or full name (minimum 2 characters); used by the intake workstation to link uploads to existing patients
  • Work QueuesGET /work-queue/verification (batches in PENDING_VERIFICATION); GET /work-queue/entry (batches in UPLOADED, IN_ENTRY, or REJECTED); GET /work-queue/clinical-approval (batches in AWAITING_CLINICAL_APPROVAL); GET /work-queue/overview (aggregate status counts, average queue age, reject rate, oldest pending verification — administrator only); all queue and batch list endpoints support sortBy and sortDirection; role-restricted access
  • Batch Audit Trail APIGET /digitization-batches/:id/events returns cursor-paginated digitization events with actor username and full name; accessible by administrator, verifier, and clinical approver roles
  • User Directory and ManagementGET /users?role= lists active users for batch assignment; administrators can POST /users (create), PATCH /users/:id (update name, role, active flag), POST /users/:id/reset-password, and any authenticated user can POST /users/me/change-password with current-password verification
  • Batch-Type Field RequirementsBatchTypeFieldRequirements metadata on GET /digitization-batches/{id} and GET /digitization-batches/{id}/draft tells the workstation which form sections to show per batch type (patient demographics, encounter context, encounter summary fields, observations, allergies, medications); entry and verification forms read this metadata instead of hardcoding batch-type rules
  • Document Access AuditGET /digitization-batches/:id and GET /digitization-batches/:id/document write a document_accessed digitization event (deduplicated per user within 5 minutes) whenever a scan is retrieved
  • Digitization Workstation UI — Vue 3 SPA at vigilcare-records-web (dev server port 3028): role-based routing and nav (intake, cover sheets, entry, verification, clinical approval, live capture, patient history, supervisor dashboard, FHIR Explorer); split-pane scan viewer with zoom/pan/rotate (loads scans via authenticated GET /digitization-batches/:id/document blob URLs — avoids cross-origin MinIO iframe issues); cover sheet management view (generate, list, print PDF); barcode-assisted intake (scan/type cover sheet code to auto-fill batch type, track, patient, then upload); draft entry with auto-save and backend-driven field visibility (allergies, medications, discharge diagnosis); optional OCR confidence indicators when Phase 13 OCR is enabled; field-level verification checkboxes; clinical approval queue with scan review, approve/reject, and retroactive alert toggle; live capture form for new or existing encounters with attestation and password confirm; patient history timeline with correction chain and audit trail; JWT refresh interceptor and toast notifications
  • Optional OCR-Assisted Pre-Fill (Phase 13) — background OcrProcessingService polls uploaded batches when Ocr:Enabled is true; extracts text via Azure Document Intelligence or self-hosted Tesseract; pre-fills draft patient/encounter/observation fields with per-field confidence scores returned as ocrConfidence on the draft payload; entry clerks review and correct — OCR does not skip verification; disabled by default in appsettings.json
  • HL7 FHIR R4 Read API — read-only FHIR endpoints at /fhir for promoted clinical data: GET /fhir/metadata (anonymous CapabilityStatement); authenticated read and search for Patient, Encounter, and Observation; MRN search via Patient?identifier=; LOINC bidirectional mapping for observation codes (e.g. HEART_RATE8867-4); vital-signs category search; date filtering on recordedAt; GET /fhir/Patient/{id}/$everything composite bundle; search bundles with self/next pagination links; 404 responses as FHIR OperationOutcome; application/fhir+json content negotiation via FhirJsonOutputFormatter
  • FHIR Explorer UI — administrator-only Vue view at /fhir-explorer: browse Patient/Encounter/Observation resources, run FHIR searches, inspect raw JSON, load Patient $everything, and open the CapabilityStatement metadata link; dev proxy at /fhir → API port 5217
  • Live Capture (Track B)POST /live-capture/encounters/{encounterId}/observations and POST /live-capture/encounters for credentialed clinicians entering vitals at bedside; clinician attestation + password re-confirm replaces the dual-human verification gate; observations promote synchronously to live tables in a single transaction with source = live_capture; critical threshold evaluation runs before the response returns, with inline criticalAlert per observation, committed ClinicalAlert rows, and observation.recorded / alert.generated outbox events; 422 ATTESTATION_REQUIRED, 422 PASSWORD_CONFIRM_INVALID, 422 EMPTY_OBSERVATIONS, 409 ENCOUNTER_NOT_ACTIVE, 409 ACTIVE_ENCOUNTER_EXISTS
  • Batch Status Machine — explicit transition matrix enforced in the service layer; illegal transitions return 409; PROMOTED and CANCELLED are terminal — corrections require a new batch with supersedesBatchId
  • JWT AuthenticationPOST /auth/login returns access token (15 min) and refresh token (7 days); POST /auth/refresh rotates tokens; POST /auth/logout revokes server-side; GET /auth/me returns authenticated user profile; BCrypt password hashing; login and refresh rate-limited to 10 requests per 5 minutes per client (429)
  • Role-Based Access — six roles (INTAKE_CLERK, DATA_ENTRY_CLERK, VERIFIER, CLINICAL_APPROVER, CLINICIAN, ADMINISTRATOR) with role-based endpoint authorization; twelve seeded demo users (two per role)
  • Auth Audit Events — append-only auth_audit_events table records login, logout, token refresh, and failed login attempts with user ID, IP address, and timestamp
  • Standard Envelope — all responses use { success, statusCode, data, error } wrapper; validation errors use the same shape with stable error codes
  • Observability — Serilog structured logging with Seq sink; correlation IDs via CorrelationIdMiddleware; ExceptionHandlerMiddleware for consistent error responses; Prometheus metrics at GET /metrics (HTTP request histograms, .NET runtime stats, custom gauges for batch counts by status and queue age, promotion duration histogram, rejection counter by reason category); MetricsCollectorService refreshes DB-backed gauges every 30 seconds; Prometheus scrapes the API via prometheus.yml; Grafana available at http://localhost:3013 (dashboard panels configured manually); health probes at GET /health/live, GET /health/ready (PostgreSQL, Redis, MinIO), and GET /health/startup
  • Swagger UI — OpenAPI spec via Swashbuckle (Development only) at http://localhost:5217/swagger

Architecture

HTTP request
  → CorrelationIdMiddleware
  → ExceptionHandlerMiddleware
  → JWT Authentication (Bearer)
  → Role-based Authorization ([Authorize(Roles = "...")])
  → Controllers (REST API)
  → Services
      ├── AuthService (login, refresh token rotation, logout, BCrypt verify)
      ├── BatchService (batch CRUD, status machine, Redis assignment lock, duplicate detection)
      ├── DraftService (draft patient/encounter/observation CRUD, plausibility validation, submit-for-verification)
      ├── VerificationService (verify/reject with separation of duties, site-config approval routing)
      ├── PromotionService (approve + atomic promote to live tables, patient dedup, encounter matching, outbox events, supersession on correction promotion)
      ├── DigitizationHistoryService (patient digitization history with correction chain and audit trails)
      ├── IdempotencyService (Idempotency-Key storage + replay for safe promotion retries)
      ├── MrnGenerator (PostgreSQL sequence-backed MRN generation: VCR-000001)
      ├── WorkQueueService (verification, entry, clinical approval queues, supervisor overview metrics)
      ├── BatchEventService (cursor-paginated batch audit trail)
      ├── MetricsCollectorService (periodic DB gauge refresh for Prometheus)
      ├── PromotionRetryService (exponential backoff retry for deferred promotions)
      ├── PatientRegistryService (live patient search by MRN or name)
      ├── UserDirectoryService (active user listing, create/update/deactivate, password reset/change)
      ├── AttestationService (clinician role + password re-confirm for live capture)
      ├── LiveCaptureService (Track B synchronous promotion + critical alert evaluation)
      ├── CoverSheetService (generate, lookup, redeem, list cover sheets)
      ├── CoverSheetPdfGenerator (printable PDF with QR codes)
      ├── FhirService (FHIR R4 read/search/$everything over promoted clinical tables)
      ├── OcrProcessingService + IOcrService (optional Azure/Tesseract draft pre-fill when enabled)
      ├── DocumentStorageService (MinIO upload, SHA-256, presigned URLs, download stream)
      ├── PlausibilityValidator (per-code numeric range guard)
      ├── PostgreSQL (EF Core — writes, keyed reads, clinical schema for live tables)
      ├── Redis (batch assignment locks)
      ├── MinIO (scanned document storage)
      └── HealthChecks (PostgreSQL, Redis, MinioHealthCheck)

Relationship to VigilCareClinical: VigilCare Records is the precursor that creates the fuel — patient identity, encounters, and observations — in a governed, auditable way. Draft observations are invisible to VigilCareClinical's alert engine, Elasticsearch ward projection, and scoring consumers. Only batches that pass approval are promoted to live tables. Promotion writes directly to VigilCareClinical's patients, encounters, observations, and outbox_events tables in a single atomic transaction with idempotency protection.

┌─────────────────────────────────────────────────────────────────────┐
│  VigilCare Records (this project)                                  │
│                                                                     │
│  Scan → Entry → Verify → Approve → Promote                          │
│       ↓                                                             │
│  Draft tables (never alert)                                         │
│       ↓ on approval                                                 │
│  PromotionService (atomic txn + idempotency) ────────────────────┐  │
└──────────────────────────────────────────────────────────────────│──┘
                                                                   │
┌──────────────────────────────────────────────────────────────────▼──┐
│  VigilCareClinicalAPI                                              │
│                                                                     │
│  Patient → Encounter → Observation → Outbox → Kafka → Alerts       │
└─────────────────────────────────────────────────────────────────────┘

Tech Stack

Layer Technology
Server ASP.NET Core 8 (.NET 8.0)
Frontend Vue 3, Vite, Pinia, Vue Router, Axios, Tailwind CSS, VueUse
Database PostgreSQL 16 with EF Core 8 (code-first migrations)
Cache / locking Redis 7 (batch assignment locks, alert threshold cache for live capture)
Object storage MinIO (scanned documents — PDF, JPEG, PNG)
Authentication JWT Bearer (Microsoft.AspNetCore.Authentication.JwtBearer)
Password hashing BCrypt.Net-Next
Logging Serilog + Seq sink
Metrics Prometheus (prometheus-net) + Grafana
Docs Swagger / OpenAPI (Swashbuckle)
Barcode / PDF QRCoder (cover sheet QR codes; raw PDF generation)
FHIR Hl7.Fhir.R4 5.x (Firely SDK — Patient, Encounter, Observation, Bundle, OperationOutcome)
Testing xUnit + FluentAssertions + WebApplicationFactory

Project Structure

VigilCareRecords/
├── VigilCareRecordsAPI/
│   ├── Program.cs                                  # Service registration, middleware, seed on startup
│   ├── appsettings.json                            # Connection strings, Redis, MinIO, Seq, JWT, site config, OCR (disabled by default)
│   ├── Controllers/
│   │   ├── ApprovalController.cs                   # Batch approval, promotion deferral (202), promotion to live tables
│   │   ├── AuthController.cs                       # JWT login, token refresh, logout, authenticated user profile
│   │   ├── CoverSheetController.cs                 # Cover sheet generate, lookup, list, PDF export
│   │   ├── Fhir/                                   # FHIR R4 read/search controllers (Patient, Encounter, Observation, metadata)
│   │   ├── DigitizationBatchesController.cs        # Batch CRUD, document upload/stream, assignment, events, promote
│   │   ├── DraftController.cs                      # Draft data entry: patient, encounter, observations, submit
│   │   ├── PatientsController.cs                   # Patient search and digitization history
│   │   ├── UsersController.cs                      # User directory and admin user management
│   │   ├── LiveCaptureController.cs                # Track B live capture: record vitals, open encounter + vitals
│   │   ├── VerificationController.cs               # Batch verification and rejection with separation of duties
│   │   └── WorkQueueController.cs                  # Work queues and supervisor overview
│   ├── HealthChecks/MinioHealthCheck.cs            # MinIO bucket-exists readiness probe
│   ├── Domain/ …                                 # Entities, enums (batch, draft, clinical, user)
│   ├── Services/ …                               # Auth, batch, draft, verification, promotion, FHIR, work queue, patient registry, user directory, live capture
│   ├── Infrastructure/Fhir/ …                  # FHIR mappers, JSON formatter, OperationOutcome helpers
│   ├── Models/Records/ …                         # Request/response DTOs
│   ├── Data/ …                                   # EF Core context, configurations, migrations, seed
│   └── …                                         # Middleware, Common, Infrastructure
├── vigilcare-records-web/                          # Vue 3 digitization workstation UI
│   ├── src/
│   │   ├── api/client.ts                         # Axios client with JWT interceptor and proactive refresh
│   │   ├── api/fhirClient.ts                     # Authenticated FHIR GET client (`Accept: application/fhir+json`)
│   │   ├── stores/                               # Pinia: auth, batches, liveCapture
│   │   ├── router/index.ts                       # Role-based routes and navigation guards
│   │   ├── views/                                # Login, Intake, CoverSheets, Entry, Verification, Approval, LiveCapture, PatientHistory, QueueDashboard, FhirExplorer
│   │   ├── components/                           # ScanViewer, EntryForm, VerificationForm, BatchList, PatientSearch, AppHeader, ToastContainer, AssignClerkDialog
│   │   ├── composables/                          # usePresignedUrl (document blob load), useOcrFieldConfidence, useToast
│   │   └── types/index.ts                        # TypeScript interfaces matching API response shapes
│   ├── vite.config.ts                            # Dev server on port 3028; proxies /api and /fhir → localhost:5217
│   └── tailwind.config.js                        # Clinical color palette and layout component classes
├── VigilCareRecordsAPI.Tests/                      # Integration tests (Phases 113)
├── prometheus.yml                                # Prometheus scrape config (API on host.docker.internal:5217)
├── docker-compose.yml                            # PostgreSQL, Redis, Seq, MinIO, Prometheus, Grafana
├── scripts/
│   ├── run-vigilcare-records-verification.sh       # Phase 1
│   ├── run-vigilcare-records-phase-2-verification.sh
│   ├── run-vigilcare-records-phase-3-verification.sh
│   ├── run-vigilcare-records-phase-4-verification.sh
│   ├── run-vigilcare-records-phase-5-verification.sh
│   ├── run-vigilcare-records-phase-6-verification.sh
│   ├── run-vigilcare-records-phase-8-verification.sh  # Prometheus metrics, overview, events, retry
│   ├── run-vigilcare-records-verification-p9.sh    # E2E workflow + work queue overview smoke test
│   ├── run-vigilcare-records-phase-10-verification.sh # Cover sheets, barcode upload, PDF, auto-assign
│   ├── run-vigilcare-records-phase-11-verification.sh # FHIR metadata, read/search, $everything, LOINC mapping
│   ├── run-vigilcare-records-phase-13-verification.sh # OCR config, ocrConfidence API, optional live OCR polling
│   └── fixtures/test-scan.pdf                    # Sample PDF for upload verification scripts
└── docs/
    ├── plans/                                    # Phase 113 implementation guides
    ├── digitization-workstation-guide.md         # Clinical scenarios and clerk workflow reference
    ├── vigilcare-records-gap-analysis.md         # Known gaps and hardening backlog
    └── vigilcare-records-prd.md                  # Product requirements and phase roadmap

Batch Status State Machine

                    ┌──────────────┐
                    │   UPLOADED   │──── cancel ────┐
                    └──────┬───────┘                │
                           │ assign                 │
                           ▼                        │
                    ┌──────────────┐                │
         ┌──────────│   IN_ENTRY   │◄─────────┐    │
         │ cancel   └──────┬───────┘          │    │
         │                 │ submit           │ reject
         │                 ▼                  │    │
         │          ┌──────────────┐         │    │
         │          │   PENDING    │─────────┘    │
         │          │ VERIFICATION │              │
         │          └──────┬───────┘              │
         │                 │                      │
         │    verify fail  │ verify pass          │
         │        ─────────┤                      │
         │                 │                      │
         │   site config   │ site config          │
         │   = false       │ = true               │
         │        ┌────────┴────────┐             │
         │        ▼                 ▼             │
         │  ┌──────────────┐  ┌──────────────────────┐
         │  │   VERIFIED   │  │ AWAITING_CLINICAL    │
         │  └──────┬───────┘  │     _APPROVAL          │
         │         │          └──────────┬─────────────┘
         │         └──────────┬──────────┘
         │                    │ approve
         │                    ▼
         │             ┌──────────────┐
         └────────────►│   APPROVED   │
                       └──────┬───────┘
                              │ promotion
                              ▼
                       ┌──────────────┐
                       │  PROMOTED    │  (terminal — live records exist)
                       └──────┬───────┘
                              │ correction batch promoted
                              ▼
                       original observations marked superseded;
                       correction observations become active

                       ┌──────────────┐
                       │  CANCELLED   │  (terminal — from UPLOADED, IN_ENTRY, or REJECTED)
                       └──────────────┘

Correction flow (Phase 5): A PROMOTED batch cannot be edited in place. To fix an erroneous live value, intake uploads a new batch with supersedesBatchId pointing at the promoted batch. The correction goes through entry → verification → approval like any other batch. On promotion, the original batch's live_observations rows are soft-flagged (is_superseded = true, superseded_by_batch_id set) — never deleted.

Allowed transitions:

From To
UPLOADED IN_ENTRY, CANCELLED
IN_ENTRY PENDING_VERIFICATION, CANCELLED
PENDING_VERIFICATION VERIFIED, AWAITING_CLINICAL_APPROVAL, REJECTED
REJECTED IN_ENTRY, CANCELLED
VERIFIED APPROVED
AWAITING_CLINICAL_APPROVAL APPROVED, REJECTED
APPROVED PROMOTED
PROMOTED (none — terminal)
CANCELLED (none — terminal)

Illegal transitions return 409 with a stable error code. A PROMOTED batch cannot return to any earlier state. Corrections require a new batch referencing supersedesBatchId.

Site-configurable clinical approval routing:

batchType Clinical sign-off after verify?
PATIENT_REGISTRATION No
ALLERGY_UPDATE No
ENCOUNTER_SUMMARY Yes
VITALS_SHEET Yes
LAB_RESULTS Yes
MEDICATION_LIST Yes
MIXED Yes

Architecture Decisions

Draft Isolation — The Core Safety Invariant

Draft observations are invisible to VigilCareClinical's alert engine, Elasticsearch ward projection, and scoring consumers. Only batches that have passed approval are promoted to live tables. This is the architectural decision that separates a digitization system from a data entry form. If an entry clerk misreads a potassium of 5.2 mEq/L as 52 mEq/L, the plausibility validator catches it at draft save. If the plausibility range allows it but the value is wrong, the verifier catches it against the scan. If neither catches it, the clinical approver is the last gate for high-stakes batch types. At no point does the incorrect value enter the alerting pipeline until all human gates have cleared.

Separation of Duties — Who Cannot Do What

The person who enters data cannot verify their own entry. This is enforced in the service layer, not only in the UI. enteredByUserId === currentUserId blocks verify and approve actions with 409 SEPARATION_OF_DUTIES_VIOLATION. In a small island clinic with limited staff, the same person may hold intake and entry roles — but never entry and verifier on the same batch.

Two-Track Workflow (Backfill vs Live Capture)

Track A (backfill) is the full pipeline for historical charts: scan → entry → verification → clinical approval → promotion. Track B (live capture) is for credentialed clinicians entering vitals at bedside via POST /live-capture/encounters/{encounterId}/observations or POST /live-capture/encounters — clinician attestation + password re-confirm replaces the dual-human gate, and observations promote synchronously with critical alerting before the response returns. Both tracks create DigitizationBatch records with full audit trails, keeping metrics and coverage stats consistent.

Redis for Batch Assignment Locking and Alert Threshold Cache

Redis serves two purposes in this project: (1) preventing double-assignment of batches to entry clerks via SET batch:assign:{id} NX EX 3600, and (2) caching alert threshold definitions for synchronous critical evaluation during live capture (threshold:{observationCode}). Work-queue counters are derived from PostgreSQL queries, not Redis counters.

Integrated Database Deployment

VigilCare Records draft tables and VigilCareClinical live tables share one PostgreSQL instance (separate logical concerns). Promotion runs in a single local transaction — no distributed saga required. Split deployment with HTTP + saga retry is documented as a future deployment option.

Corrections — Append-Only Supersession

Approved live observations are never mutated or deleted. When a transcription error is discovered after promotion, a correction batch (supersedesBatchId) goes through the full human workflow. On promotion, PromotionService marks the original live_observations as superseded and inserts the corrected values as new active rows. Clinical queries filter is_superseded = false by default; audit queries retain the full chain. No API endpoint exists to PATCH live observations directly.


Getting Started

Prerequisites

  • .NET 8 SDK
  • Node.js 20+ and npm (for the workstation UI)
  • Docker and Docker Compose

Start Infrastructure

docker compose up -d
Service Host Port Notes
PostgreSQL 16 5437 Database: vigilcare_records, user: postgres, password: password
Redis 7 6383 No auth
Seq 5346 UI at http://localhost:5346, login: admin / seqadmin
MinIO 9012 (S3 API), 9013 (console) login: minioadmin / minioadmin
Prometheus 9095 Scrapes API at host.docker.internal:5217/metrics; UI at http://localhost:9095
Grafana 3013 UI at http://localhost:3013; add Prometheus data source http://prometheus:9090

Install and Run

cd VigilCareRecordsAPI
dotnet restore
dotnet run

On startup the application:

  1. Runs EF Core migrations
  2. Seeds twelve demo users (two per role) and ten demo batches spanning all batch types, both tracks, and every workflow status (including a correction batch, a deferred-promotion candidate, and live-capture vitals)

Swagger UI is available at http://localhost:5217/swagger in Development.

Run the Workstation UI

With the API running:

cd vigilcare-records-web
npm install
npm run dev

Open http://localhost:3028. The Vite dev server proxies /api and /fhir requests to the API on port 5217.

Username Password Default route
intake1 password /intake — upload scans, barcode-assisted cover sheet upload; /cover-sheets — generate and print cover sheets
entry1 password /entry — data entry queue and split-pane form
verifier1 password /verification — field-level verification
approver1 password /approval — clinical sign-off before promotion
clinician1 password /live-capture — bedside vitals with attestation
admin1 password /dashboard — supervisor queue overview; /fhir-explorer — FHIR resource browser

All roles can access /patients for patient search and digitization history.

See docs/digitization-workstation-guide.md for clinical scenarios (backfill, live capture, corrections) and the full clerk workflow.

Paper originals: The scanned document is the working reference for entry and verification. The physical chart remains the legal original until jurisdiction-specific retention rules apply. Scans are never deleted on batch rejection.

Production build:

cd vigilcare-records-web
npm run build   # output in dist/

For production deployment where the UI and API are on different origins, add the frontend URL to Cors:AllowedOrigins in appsettings.json (default: http://localhost:3028 for local dev).

To enable optional OCR pre-fill (Phase 13), set Ocr:Enabled to true in appsettings.json or via environment variable (Ocr__Enabled=true), configure Azure credentials or install Tesseract data (Ocr:Tesseract:DataPath, default /usr/share/tessdata), then restart the API. Run ./scripts/run-vigilcare-records-phase-13-verification.sh to verify; set VIGILCARE_OCR_LIVE=1 for live OCR polling tests.

Run Tests

dotnet test

Integration tests use WebApplicationFactory with PostgreSQL, Redis, and MinIO containers. No manual infrastructure setup is required for dotnet test.

Test class Phase Coverage
DraftEntryTests 2 Draft CRUD, plausibility validation, submit-for-verification completeness checks
VerificationTests 3 Verification, rejection, separation of duties enforcement, clinical approval routing
PromotionTests 4 Approval, atomic promotion to live tables, idempotency replay, separation of duties, retroactive alert policy, patient deduplication, MRN generation
CorrectionSupersessionTests 5 Correction batch supersession, validation guards (non-promoted, already superseded), patient digitization history, unknown patient 404
LiveCaptureIntegrationTests 6 Attestation and password confirm, synchronous promotion, critical low/high potassium alerts, mixed-batch alerting, open encounter + vitals, role and validation guards, audit events
UserManagementTests 9 User create/update/deactivate, admin password reset, self-service password change, duplicate username guard, weak password validation
CoverSheetBatchTests 10 Cover sheet redeem on upload, reuse prevention, auto-assign from pre-assigned cover sheet
CoverSheetPdfTests 10 Single and batch PDF generation, valid PDF structure with QR metadata
FhirIntegrationTests 11 FHIR metadata, Patient/Encounter/Observation read and search, LOINC mapping, $everything, content-type, 404 OperationOutcome, bundle pagination
BatchOperationsTests Batch cancellation (status guards, Redis lock release), list/queue sortBy/sortDirection validation

Verification Scripts

With the API running (dotnet run) and Docker Compose up:

./scripts/run-vigilcare-records-verification.sh        # Phase 1 — schema, auth, batch CRUD, MinIO, status machine
./scripts/run-vigilcare-records-phase-2-verification.sh # Phase 2 — draft entry, plausibility, submit
./scripts/run-vigilcare-records-phase-3-verification.sh # Phase 3 — verification, rejection, separation of duties
./scripts/run-vigilcare-records-phase-4-verification.sh # Phase 4 — approval, promotion, idempotency, patient dedup
./scripts/run-vigilcare-records-phase-5-verification.sh # Phase 5 — corrections, supersession, digitization history
./scripts/run-vigilcare-records-phase-6-verification.sh # Phase 6 — live capture, attestation, critical alerts
./scripts/run-vigilcare-records-phase-8-verification.sh # Phase 8 — Prometheus metrics, work-queue overview, batch events, promotion retry
./scripts/run-vigilcare-records-verification-p9.sh      # E2E workflow + work queue overview smoke test
./scripts/run-vigilcare-records-phase-10-verification.sh # Phase 10 — cover sheets, barcode upload, PDF, auto-assign
./scripts/run-vigilcare-records-phase-11-verification.sh # Phase 11 — FHIR R4 read/search, $everything, LOINC mapping, clinical fixture seed

The Phase 11 script auto-seeds a minimal clinical fixture for VCR-000001 when PostgreSQL is available (updates existing patients or inserts deterministic demo rows). Run dotnet test --filter FullyQualifiedName~FhirIntegrationTests separately for WebApplicationFactory integration coverage.


API Reference

All endpoints are prefixed /api/v1. Responses follow the standard envelope:

{ "success": true, "statusCode": 200, "data": {}, "error": null }

Error response:

{
  "success": false,
  "statusCode": 409,
  "data": null,
  "error": {
    "message": "Verifier cannot approve a batch they entered.",
    "code": "SEPARATION_OF_DUTIES_VIOLATION"
  }
}

Authentication

Method Path Auth Description
POST /auth/login Anonymous Authenticate with username/password; returns access + refresh tokens (rate-limited: 10/5 min)
POST /auth/refresh Anonymous Exchange a valid refresh token for new access + refresh token pair (rate-limited: 10/5 min)
POST /auth/logout Anonymous Revoke the refresh token server-side
GET /auth/me JWT Returns the authenticated user's profile

POST /auth/login body:

Field Type Required Description
username string yes Username
password string yes Password

Login response:

Field Type Description
token string JWT bearer token (15 min)
refreshToken string Opaque refresh token (7 days)
userId Guid User ID
username string Username
displayName string Display name
role string INTAKE_CLERK, DATA_ENTRY_CLERK, VERIFIER, CLINICAL_APPROVER, CLINICIAN, ADMINISTRATOR

Seeded demo users:

Username Password Role
intake1 password Intake Clerk
intake2 password Intake Clerk
entry1 password Data Entry Clerk
entry2 password Data Entry Clerk
verifier1 password Verifier
verifier2 password Verifier
approver1 password Clinical Approver
approver2 password Clinical Approver
clinician1 password Clinician
clinician2 password Clinician
admin1 password Administrator
admin2 password Administrator

Digitization Batches

Method Path Description
POST /digitization-batches Upload a scanned document and create a batch (multipart/form-data)
GET /digitization-batches List batches; optional status, batchType, assignedTo, track filters; paginated and sortable (sortBy, sortDirection; default createdAt desc)
GET /digitization-batches/{id} Batch detail with presigned document URL (15-minute expiry) and fieldRequirements; audits document_accessed
GET /digitization-batches/{id}/document Stream scanned document (PDF/JPEG/PNG) for in-app viewing; same auth and audit as batch detail
PATCH /digitization-batches/{id}/assign Assign batch to an entry clerk (Redis lock; transitions to IN_ENTRY)
POST /digitization-batches/{id}/cancel Cancel batch permanently (administrator only; UPLOADED, IN_ENTRY, or REJECTED)

POST body (multipart/form-data):

Field Type Required Description
file binary yes PDF, JPEG, or PNG (max 25 MB)
batchType string yes* PATIENT_REGISTRATION, ENCOUNTER_SUMMARY, VITALS_SHEET, LAB_RESULTS, MEDICATION_LIST, ALLERGY_UPDATE, MIXED — required unless coverSheetCode is provided; cover sheet values override when both are sent
track string no BACKFILL (default) or LIVE_CAPTURE — overridden by cover sheet when coverSheetCode is set
patientId Guid no Link to existing patient (enables duplicate detection); inherited from cover sheet when set
supersedesBatchId Guid no Links a correction batch to the promoted batch it will supersede on promotion
coverSheetCode string no Cover sheet barcode (e.g. VCR-CS-A3F7B2D1); auto-applies batch type, track, and patient; redeems on success; auto-assigns when cover sheet has assignToUserId

Status codes:

Code Meaning
201 Batch created
400 Empty file or invalid MIME type
404 Superseded batch not found (SUPERSEDED_BATCH_NOT_FOUND); cover sheet not found (COVER_SHEET_NOT_FOUND)
409 Duplicate document (same SHA-256 for same patient within 24 hours); batch already superseded (BATCH_ALREADY_SUPERSEDED); cover sheet already used (COVER_SHEET_ALREADY_USED)
422 Superseded batch not in PROMOTED status (SUPERSEDED_BATCH_NOT_PROMOTED)

PATCH /digitization-batches/{id}/assign body:

Field Type Required Description
entryClerkUserId Guid yes User ID of the entry clerk to assign

POST /digitization-batches/{id}/cancel body:

Field Type Required Description
reason string yes Cancellation reason (minimum 5 characters)

Status codes: 409 ILLEGAL_STATUS_TRANSITION when the batch is not in a cancellable status.

Cover Sheets

Method Path Auth Description
POST /cover-sheets/generate Intake Clerk, Administrator Generate 1100 cover sheets with unique barcode codes
GET /cover-sheets/lookup/{code} Any authenticated Look up a cover sheet by barcode for intake auto-fill
GET /cover-sheets Intake Clerk, Administrator List cover sheets; optional isUsed, patientId filters; paginated
POST /cover-sheets/{id}/pdf Intake Clerk, Administrator Download a printable PDF with QR code for one cover sheet
POST /cover-sheets/batch-pdf Intake Clerk, Administrator Download a multi-page PDF for a list of cover sheet IDs

POST /cover-sheets/generate body:

Field Type Required Description
count int yes Number of cover sheets to generate (1100)
batchType string yes Batch type encoded in the barcode
track string no BACKFILL (default) or LIVE_CAPTURE
patientId Guid no Pre-link to an existing patient
assignToUserId Guid no Pre-assign batches created with this cover sheet to an entry clerk

Cover sheet response fields: id, code (VCR-CS-{8-hex}), batchType, track, patientId, patientName, patientMrn, assignToUserId, assignToUserName, isUsed, batchId, createdAt, usedAt.

Status codes: 404 PATIENT_NOT_FOUND, 404 USER_NOT_FOUND, 404 COVER_SHEET_NOT_FOUND, 409 COVER_SHEET_ALREADY_USED.

Draft Data Entry

Method Path Description
GET /digitization-batches/{id}/draft Full draft payload: fieldRequirements, ocrConfidence, patient, encounter, observations
PUT /digitization-batches/{id}/draft/patient Upsert draft patient demographics
PUT /digitization-batches/{id}/draft/encounter Upsert draft encounter fields
POST /digitization-batches/{id}/draft/observations Add an observation row
PUT /digitization-batches/{id}/draft/observations/{obsId} Edit an observation row
DELETE /digitization-batches/{id}/draft/observations/{obsId} Remove an observation from draft
POST /digitization-batches/{id}/submit-for-verification Validate completeness and transition to PENDING_VERIFICATION

PUT /digitization-batches/{id}/draft/patient body:

Field Type Required Description
fullName string no Patient full name
dateOfBirth string (ISO date) no Date of birth; omit or send null when unknown — do not send empty string
sex string no male, female, other, or unknown
bloodType string no A+, A-, B+, B-, AB+, AB-, O+, O-
emergencyContact string no Emergency contact info
allergies string[] no Allergy list; omit or null when noKnownAllergies is true
noKnownAllergies bool no Explicit NKA flag
medications string[] no Medication list; omit or null when noActiveMedications is true
noActiveMedications bool no Explicit no-medications flag

Entry form visibility by batch type (fieldRequirements on draft/batch detail):

batchType Allergies section Medications section Observations Encounter summary fields
PATIENT_REGISTRATION
VITALS_SHEET yes
LAB_RESULTS yes
ALLERGY_UPDATE yes
ENCOUNTER_SUMMARY yes
MEDICATION_LIST yes
MIXED yes yes yes yes

Observation request body:

Field Type Required Description
observationCode string yes e.g. HEART_RATE, SYSTOLIC_BP, TEMP_C, SPO2
value decimal yes Numeric measurement
unit string yes Unit of measure
recordedAt DateTimeOffset yes When the measurement was taken (from the chart)
note string no Optional note about the reading

Required fields before submit (by batch type):

batchType Required draft content
PATIENT_REGISTRATION Full name, date of birth, sex
ENCOUNTER_SUMMARY Linked patient; encounter with admission date, department, admission reason
VITALS_SHEET Linked patient, encounter context, at least one observation with recordedAt
LAB_RESULTS Linked patient, encounter, at least one lab observation code, recordedAt (correction batches with supersedesBatchId require observations only — patient and encounter are inherited)
MEDICATION_LIST Linked patient; medications list with at least one entry or explicit noActiveMedications: true
ALLERGY_UPDATE Linked patient, allergies list (may be empty with explicit noKnownAllergies: true)
MIXED Linked patient, encounter context, and at least one of: observation with recordedAt, or complete encounter summary

Verification and Rejection

Method Path Auth Description
POST /digitization-batches/{id}/verify Verifier, Clinical Approver, Administrator Verify a batch with field-level checks
POST /digitization-batches/{id}/reject Verifier, Clinical Approver, Administrator Reject a batch with mandatory reason

POST /verify body:

Field Type Required Description
fieldChecks array yes Field-level review results
passed bool yes Overall verification pass/fail

Field check object:

Field Type Required Description
fieldName string yes e.g. observations[0].value, patient.fullName
status string yes ok, warning, or error
note string no Optional reviewer note

POST /reject body:

Field Type Required Description
reason string yes Rejection reason (minimum 10 characters)

Approval and Promotion

Method Path Auth Description
POST /digitization-batches/{id}/approve Clinical Approver, Administrator Approve and atomically promote draft data to live clinical tables
POST /digitization-batches/{id}/promote Clinical Approver, Administrator Manually promote an APPROVED batch (used after deferred promotion or by operators)
GET /digitization-batches/{id}/promotion-result Any authenticated Retrieve live entity IDs created during promotion
GET /digitization-batches/{id}/events Administrator, Verifier, Clinical Approver Cursor-paginated batch audit trail with actor username and full name

POST /approve headers:

Header Required Description
Idempotency-Key yes Unique key (max 100 chars) for safe retries; replays return the original response within 24 hours

POST /approve body:

Field Type Required Description
enableRetroactiveAlerts bool no Default false. When true, backfill observations emit outbox events for downstream alerting. Live capture batches always emit outbox events regardless of this flag.

Promotion response (PromotionResultResponse):

Field Type Description
batchId Guid The promoted batch
status string promoted
patientId Guid Live patient ID (created or matched)
mrn string Medical Record Number (e.g. VCR-000001)
encounterId Guid Live encounter ID (created or matched)
observationIds Guid[] Live observation IDs created
promotedAt DateTimeOffset Promotion timestamp
outboxEventsWritten int Number of outbox events emitted for downstream consumers

Status codes:

Code Meaning
200 Batch approved and promoted (or idempotent replay)
202 Promotion deferred — batch stays APPROVED; PromotionRetryService retries automatically (PROMOTION_DEFERRED)
400 Missing or invalid Idempotency-Key header
404 Batch not found
409 Illegal status transition or separation of duties violation
422 Missing draft patient data

Separation of duties: The approver cannot be the entry clerk (enteredByUserId) or the verifier (verifiedByUserId) of the same batch. Both checks return 409 SEPARATION_OF_DUTIES_VIOLATION.

Patient deduplication: On promotion, the service matches existing patients by case-insensitive normalized fullName + dateOfBirth. Near-matches (Levenshtein distance ≤ 3 on normalized name with same DOB) are logged as warnings. If an exact normalized match is found, the existing patient record is updated with any new fields from the draft. Otherwise, a new patient is created with a sequence-generated MRN (VCR-NNNNNN).

Encounter matching: Active encounters for the same patient and department are reused. Otherwise, a new encounter is created. Encounters with a discharge diagnosis are created with discharged status.

Work Queues

Method Path Auth Description
GET /work-queue/verification Verifier, Clinical Approver, Administrator Batches in PENDING_VERIFICATION
GET /work-queue/entry Data Entry Clerk, Administrator Batches awaiting or in entry (UPLOADED, IN_ENTRY, REJECTED)
GET /work-queue/clinical-approval Clinical Approver, Administrator Batches in AWAITING_CLINICAL_APPROVAL
GET /work-queue/overview Administrator Aggregate metrics: status counts, average queue age, 24h reject rate, oldest pending verification

All work queue endpoints support pagination via ?page=1&pageSize=20 and sorting via ?sortBy=updatedAt&sortDirection=asc (except overview). Allowed sort fields: createdAt, updatedAt, status, batchType, track. Invalid sortBy returns 422 INVALID_SORT_FIELD.

Batch Audit Trail

Method Path Auth Description
GET /digitization-batches/{id}/events Administrator, Verifier, Clinical Approver Cursor-paginated digitization events for a batch

Query parameters: after (ISO-8601 cursor from previous page's nextCursor), pageSize (default 50, max 200). Events are ordered chronologically (oldest first).

Event object (BatchEventResponse):

Field Type Description
id Guid Event ID
batchId Guid Batch ID
eventType string e.g. uploaded, verified, promoted, document_accessed, cancelled, promotion_retry_failed
actorUserId Guid User who performed the action
actorUsername string Actor username
actorFullName string Actor display name
occurredAt DateTimeOffset Event timestamp
metadataJson string? Optional JSON (field checks, rejection reason, etc.)

GET /work-queue/overview response:

Field Type Description
statusCounts object Batch count per status (all 9 statuses present)
averageTimeInQueueMinutes number Average age of batches in PENDING_VERIFICATION
rejectRate number Rejections / (rejections + verifications) over the last 24 hours (0.01.0)
oldestPendingVerificationMinutes number Age of the oldest batch in PENDING_VERIFICATION

Patient Registry

Method Path Auth Description
GET /patients/search?q= Intake Clerk, Administrator Search live patients by MRN or full name (min 2 characters); returns up to 20 matches

Search result object:

Field Type Description
id Guid Patient ID
fullName string Patient full name
mrn string Medical record number

User Directory and Management

Method Path Auth Description
GET /users?role= Intake Clerk, Administrator List active users; optional role filter (e.g. DATA_ENTRY_CLERK)
POST /users Administrator Create a new user
PATCH /users/{id} Administrator Update fullName, role, and/or isActive
POST /users/{id}/reset-password Administrator Set a new password for any user
POST /users/me/change-password Any authenticated Self-service password change (requires current password)

Used by the intake workstation assign-clerk dialog. Admin endpoints support operator provisioning without database access.

POST /users body:

Field Type Required Description
username string yes Unique username
password string yes Password (minimum strength enforced)
fullName string yes Display name
role string yes One of the six user roles

PATCH /users/{id} body: any of fullName, role, isActive (set isActive: false to deactivate).

Status codes: 409 USERNAME_TAKEN on duplicate username; 422 on weak password.

Patient Digitization History

Method Path Auth Description
GET /patients/{patientId}/digitization-history Any authenticated Full digitization history for a patient: batches, correction chain, observation counts, audit trails

Response summary fields:

Field Type Description
patientId Guid Patient ID
totalBatches int All batches linked to this patient
promotedBatches int Batches in PROMOTED status
supersededBatches int Promoted batches that have been superseded by a correction
pendingBatches int Batches not yet promoted or rejected
entries array Per-batch detail with isCorrection, hasBeenSuperseded, supersededByBatchId, live/superseded observation counts, and auditTrail

Status codes:

Code Meaning
200 History returned
404 No digitization batches for patient (PATIENT_HISTORY_NOT_FOUND)

Live Capture (Track B)

Method Path Auth Description
POST /live-capture/encounters/{encounterId}/observations Clinician Record observations against an existing active encounter; promotes synchronously with inline critical alerts
POST /live-capture/encounters Clinician Open a new encounter and record initial vitals in one request (outpatient workflow)

Request body (both endpoints):

Field Type Required Description
observations array yes One or more observation objects (see below)
clinicianAttestation bool yes Must be true — clinician attests values are accurate
passwordConfirm string yes Re-enter password to confirm identity

Open encounter only — additional fields:

Field Type Required Description
patientId Guid yes Existing patient ID
department string yes e.g. Outpatient Clinic, Internal Medicine
roomBed string no Ward/bed assignment
admissionReason string no Reason for visit or admission

Observation object:

Field Type Required Description
observationCode string yes e.g. HEART_RATE, POTASSIUM_MEQ_L, TEMP_C
value decimal yes Numeric measurement
unit string yes Unit of measure
recordedAt DateTimeOffset yes When the measurement was taken
note string no Optional note

Response (LiveCaptureResponse):

Field Type Description
batchId Guid DigitizationBatch created in PROMOTED status with track = LIVE_CAPTURE
encounterId Guid Live encounter ID
observations array Promoted observations with liveObservationId and optional inline criticalAlert
criticalAlertCount int Number of synchronous critical alerts generated
promotedAt DateTimeOffset Promotion timestamp

Inline critical alert object (criticalAlert on each observation):

Field Type Description
alertId Guid Committed ClinicalAlert row ID
severity string CRITICAL
thresholdBound string CRITICAL_LOW or CRITICAL_HIGH
thresholdValue decimal Breached threshold value
message string Human-readable breach description

Status codes:

Code Meaning
201 Observations promoted; critical alerts (if any) committed before response
403 Caller lacks CLINICIAN role
404 Patient or encounter not found
409 Encounter not active (ENCOUNTER_NOT_ACTIVE); patient already has active encounter (ACTIVE_ENCOUNTER_EXISTS)
422 Attestation false (ATTESTATION_REQUIRED); wrong password (PASSWORD_CONFIRM_INVALID); empty observations list (EMPTY_OBSERVATIONS)

Track B still creates a full audit trail: each submission writes a DigitizationBatch (status PROMOTED, documentRef = "live-capture"), draft observation rows, live_capture_attested and promoted digitization events, live Observation rows with source = live_capture, and outbox events for downstream alerting.

Health Checks

Unauthenticated probe endpoints for orchestrators and load balancers:

Method Path Description
GET /health/live Process liveness (always 200 if the app is running)
GET /health/ready Readiness — PostgreSQL, Redis, and MinIO must be reachable
GET /health/startup Startup — PostgreSQL reachable (post-migration)

Returns 503 when a required dependency is unhealthy.

HL7 FHIR R4 (Read-Only)

FHIR endpoints live at /fhir (not under /api/v1). Responses use application/fhir+json. Errors return FHIR OperationOutcome resources.

Method Path Auth Description
GET /fhir/metadata Anonymous Server CapabilityStatement (supported resources, interactions, search params)
GET /fhir/Patient/{id} JWT Read a promoted patient by logical ID
GET /fhir/Patient JWT Search patients by name, birthdate, identifier (MRN); supports _count, _offset
GET /fhir/Patient/{id}/$everything JWT Bundle of Patient + all Encounters + all Observations for the patient
GET /fhir/Encounter/{id} JWT Read a promoted encounter by logical ID
GET /fhir/Encounter JWT Search encounters by patient, status, date; supports _count, _offset
GET /fhir/Observation/{id} JWT Read a promoted observation by logical ID
GET /fhir/Observation JWT Search observations by patient, code (LOINC or VigilCare code), date, category, encounter; supports _count, _offset

Search notes:

  • Observation code=8867-4 resolves LOINC to VigilCare HEART_RATE (and reverse mapping on read).
  • category=vital-signs filters vital-sign observation codes (HEART_RATE, TEMP_C, blood pressure, etc.).
  • Date parameters use FHIR prefixes (e.g. date=ge2026-06-20) against observation recordedAt.
  • Search bundles include self and next links when paginating.

Status codes: 404 with OperationOutcome issue code not-found when a resource ID does not exist.

UI: Administrators can browse FHIR resources interactively at http://localhost:3028/fhir-explorer.


Data Models

CoverSheet

Single-use barcode label that encodes batch metadata for high-volume backfill intake. Redeemed atomically when a batch is created with coverSheetCode.

id                  Guid    PK
code                string  required, unique — VCR-CS-{8-hex} encoded in QR barcode
patientId           Guid?   optional pre-link to patient
batchType           string  encoded batch type
track               string  BACKFILL | LIVE_CAPTURE
assignToUserId      Guid?   optional entry clerk pre-assignment
generatedByUserId   Guid    FK → User who generated the cover sheet
isUsed              bool    default false — set true on batch creation
batchId             Guid?   FK → DigitizationBatch created from this cover sheet
createdAt           DateTimeOffset
usedAt              DateTimeOffset? set when redeemed

DigitizationBatch

id                      Guid    PK
status                  string  UPLOADED | IN_ENTRY | PENDING_VERIFICATION | REJECTED | VERIFIED | AWAITING_CLINICAL_APPROVAL | APPROVED | PROMOTED | CANCELLED
batchType               string  PATIENT_REGISTRATION | ENCOUNTER_SUMMARY | VITALS_SHEET | LAB_RESULTS | MEDICATION_LIST | ALLERGY_UPDATE | MIXED
track                   string  BACKFILL | LIVE_CAPTURE
patientId               Guid?   nullable until linked
encounterDraftId        Guid?   encounter context for vitals/labs
documentRef             string  MinIO object key for the scanned document
documentSha256          string  content hash for integrity verification
enableRetroactiveAlerts bool    default false
enteredByUserId         Guid?   set on first draft save
verifiedByUserId        Guid?   set on verification pass
approvedByUserId        Guid?   set on final approval
rejectionReason         string? required when status → REJECTED
promotedAt              DateTimeOffset? timestamp when live records created
promotionEncounterId    Guid?   VigilCareClinical encounter ID after promotion
supersedesBatchId       Guid?   links a correction batch to the batch it replaces
clinicianAttestation    bool    true for Track B batches attested at bedside
createdAt               DateTimeOffset
updatedAt               DateTimeOffset

DraftPatient

id                Guid    PK
batchId           Guid    FK → DigitizationBatch
fullName          string? required for patient_registration on submit
dateOfBirth       DateOnly? required for patient_registration on submit
sex               string? required for patient_registration on submit
bloodType         string? A+ | A- | B+ | B- | AB+ | AB- | O+ | O-
emergencyContact  string?
allergiesJson     string? JSON array (DB column; API read/write uses `allergies` string[])
noKnownAllergies  bool
medicationsJson   string? JSON array (DB column; API read/write uses `medications` string[])
noActiveMedications bool
createdAt         DateTimeOffset
updatedAt         DateTimeOffset

DraftEncounter

id                 Guid    PK
batchId            Guid    FK → DigitizationBatch
admissionDate      DateTimeOffset?
department         string? ICU, ED, MedSurg, etc.
roomBed            string? ward/bed assignment
admissionReason    string?
dischargeDiagnosis string?
status             string? encounter status from chart
createdAt          DateTimeOffset
updatedAt          DateTimeOffset

DraftObservation

id              Guid    PK
batchId         Guid    FK → DigitizationBatch
observationCode string  required (e.g. HEART_RATE, TEMP_C)
value           decimal required — validated against plausibility ranges
unit            string  required (e.g. bpm, °C, mmHg)
recordedAt      DateTimeOffset required — from the paper chart
note            string? optional transcription note
createdAt       DateTimeOffset

ScannedDocument

id            Guid    PK
batchId       Guid    FK → DigitizationBatch
objectKey     string  MinIO object key
sha256        string  content hash
contentType   string  application/pdf | image/jpeg | image/png
fileSizeBytes long
uploadedAt    DateTimeOffset

DigitizationEvent

id            Guid    PK
batchId       Guid    FK → DigitizationBatch
eventType     string  uploaded | entry_started | submitted_for_verification | rejected | verified | awaiting_clinical_approval | approved | promoted | live_capture_attested | correction_uploaded | correction_promoted | superseded | document_accessed | cancelled | ...
actorUserId   Guid    FK → User
occurredAt    DateTimeOffset
metadataJson  string? optional JSON (field checks, rejection reason, assigned-to)

Patient (Clinical — Live)

id                Guid    PK
mrn               string  required, unique — generated from PostgreSQL sequence (VCR-000001)
fullName          string  required
dateOfBirth       DateOnly?
sex               string?
bloodType         BloodType?
emergencyContact  string?
allergiesJson     string?
noKnownAllergies  bool
createdAt         DateTimeOffset
updatedAt         DateTimeOffset

Encounter (Clinical — Live)

id                 Guid    PK
patientId          Guid    FK → Patient
admissionDate      DateTimeOffset?
department         Department?
roomBed            string?
admissionReason    string?
dischargeDiagnosis string?
status             string  active | discharged
sourceBatchId      Guid?   FK → DigitizationBatch (traceability)
createdAt          DateTimeOffset
updatedAt          DateTimeOffset

LiveObservation

Append-only mirror of promoted observations used for supersession tracking and digitization history queries. Rows are never deleted; corrections mark prior rows as superseded.

id                    Guid    PK
encounterId           Guid    FK → LiveEncounter
patientId             Guid?   FK → Patient
sourceBatchId         Guid    FK → DigitizationBatch
observationCode       string  e.g. K, Na
value                 decimal
unit                  string
recordedAt            DateTimeOffset
note                  string?
isSuperseded          bool    default false — set true when a correction batch promotes
supersededByBatchId   Guid?   correction batch that replaced this observation
supersededAt          DateTimeOffset? when supersession occurred
createdAt             DateTimeOffset

Observation (Clinical — Live)

id                       Guid    PK
encounterId              Guid    FK → Encounter
patientId                Guid    FK → Patient
observationCode          string  required (e.g. HEART_RATE, TEMP_C)
value                    decimal required
unit                     string  required
recordedAt               DateTimeOffset required
note                     string?
source                   string  digitization_backfill | live_capture
sourceDraftObservationId Guid?   FK → DraftObservation (traceability)
sourceBatchId            Guid?   FK → DigitizationBatch (traceability)
createdAt                DateTimeOffset

OutboxEvent

id            Guid    PK
eventType     string  e.g. observation.created | observation.recorded | alert.generated
aggregateType string  e.g. Observation | ClinicalAlert
aggregateId   Guid    FK → the created entity
payloadJson   string  full event payload for downstream consumers
createdAt     DateTimeOffset
processedAt   DateTimeOffset? set when consumed
retryCount    int     default 0

AlertThreshold

id                        Guid    PK
observationCode           string  required, unique (e.g. POTASSIUM_MEQ_L)
displayName               string  required
unit                      string  required
criticalLow               decimal?
warningLow                decimal?
warningHigh               decimal?
criticalHigh              decimal?
suppressionWindowMinutes  int?
createdAt                 DateTimeOffset

ClinicalAlert

id              Guid    PK
encounterId     Guid    FK → Encounter
patientId       Guid    FK → Patient
observationId   Guid?   FK → Observation (triggering value)
alertType       string  e.g. CRITICAL_POTASSIUM_MEQ_L
severity        string  WARNING | CRITICAL
details         string  human-readable breach message
observationCode string?
status          string  OPEN | ACKNOWLEDGED | RESOLVED | ESCALATED
triggeredAt     DateTimeOffset

IdempotencyRecord

id                Guid    PK
idempotencyKey    string  required, unique (from Idempotency-Key header)
operationName     string  e.g. batch_promote
resourceId        Guid    the batch ID
httpStatusCode    int     original response code
responseBodyJson  string  serialized original response
createdAt         DateTimeOffset
expiresAt         DateTimeOffset  24-hour TTL

PromotionAttempt

Tracks each promotion attempt for deferred-retry batches. PromotionRetryService polls rows where nextRetryAt <= now and the batch is still APPROVED.

id              Guid    PK
batchId         Guid    FK → DigitizationBatch
attemptNumber   int     1-based attempt counter
succeeded       bool    whether this attempt completed promotion
errorMessage    string? failure reason when succeeded = false
attemptedAt     DateTimeOffset
nextRetryAt     DateTimeOffset? scheduled retry time (null on success)

User

id            Guid    PK
username      string  required, unique
passwordHash  string  required (BCrypt)
fullName      string  required
role          string  INTAKE_CLERK | DATA_ENTRY_CLERK | VERIFIER | CLINICAL_APPROVER | CLINICIAN | ADMINISTRATOR
isActive      bool    default true
createdAt     DateTimeOffset
lastLoginAt   DateTimeOffset?

RefreshToken

id          Guid    PK
token       string  required, unique — opaque base64 token
userId      Guid    FK → User
expiresAt   DateTimeOffset required
createdAt   DateTimeOffset
revokedAt   DateTimeOffset? — set on refresh rotation or explicit logout

AuthAuditEvent

id          Guid    PK
eventType   string  LOGIN | LOGOUT | TOKEN_REFRESHED | LOGIN_FAILED
userId      Guid?
username    string?
ipAddress   string?
occurredAt  DateTimeOffset

Pagination

Offset pagination

List endpoints (batch list, work queues) use offset pagination:

Param Default Description
page 1 Page number (1-based)
pageSize 20 Items per page
sortBy createdAt (batch list) or updatedAt (work queues) Sort field: createdAt, updatedAt, status, batchType, track
sortDirection desc (batch list) or asc (work queues) asc or desc

Invalid sortBy values return 422 with code INVALID_SORT_FIELD.

Response shape:

{
  "items": [],
  "page": 1,
  "pageSize": 20,
  "totalCount": 42,
  "totalPages": 3
}

Cursor pagination

GET /digitization-batches/{id}/events uses cursor pagination on occurredAt:

Param Default Description
after ISO-8601 timestamp cursor from the previous page's nextCursor
pageSize 50 Events per page (max 200)

Response shape:

{
  "items": [],
  "pageSize": 50,
  "nextCursor": "2026-06-27T12:00:00Z",
  "hasMore": true
}

Implemented Phases

Phases 113 are fully implemented and verified via integration tests and per-phase scripts. Post-phase hardening (health checks, user management, promotion retry unification, patient dedup normalization, assignment-time status transitions, document access audit, batch cancellation, API-proxied document streaming) is also in place. See docs/vigilcare-records-gap-analysis.md for the remaining backlog.

Phase Feature Status
1 Schema, EF Core migrations, JWT authentication with refresh tokens, six user roles, batch CRUD, MinIO upload with SHA-256 and presigned URLs, batch status machine with transition matrix, duplicate document detection, Redis batch assignment locks, twelve seeded demo users, auth audit events Done
2 Draft data entry API (patient demographics, encounter context, observations), plausibility validation on observation values, batch-type completeness validation on submit-for-verification, assignment-time and fallback UPLOADED/REJECTED → IN_ENTRY transitions, assignment guard (BATCH_NOT_ASSIGNED), DraftEntryTests` integration tests Done
3 Verification with field-level checks (ok, warning, error per field), rejection with mandatory reason (min 10 chars), separation of duties enforcement (SEPARATION_OF_DUTIES_VIOLATION), site-configurable clinical approval routing per batch type, work queue endpoints (verification, entry, clinical approval) with role-based access, VerificationTests integration tests Done
4 Approval with three-person separation of duties (entry ≠ verifier ≠ approver), atomic promotion to live patients/encounters/observations tables in a single PostgreSQL transaction, PostgreSQL sequence-backed MRN generation (VCR-NNNNNN), patient dedup by normalized name + DOB, encounter matching by patient + department + active status, transactional outbox (observation.created events), retroactive alert policy per batch, Idempotency-Key header with 24h TTL for safe retries, PromotionTests integration tests, Phase 4 verification script Done
5 Correction batches via supersedesBatchId, supersession validation on create (422/404/409), append-only live_observations supersession flags (is_superseded, superseded_by_batch_id, superseded_at), correction promotion reuses original encounter and linked patient, correction_uploaded/correction_promoted/superseded audit events, GET /patients/:id/digitization-history with correction chain and per-batch audit trails, CorrectionSupersessionTests integration tests, Phase 5 verification script Done
6 Track B live capture: LiveCaptureController with clinician-only endpoints, AttestationService (role + password re-confirm), synchronous promotion via LiveCaptureService, Redis-backed critical threshold evaluation, inline critical alerts + committed ClinicalAlert rows, observation.recorded and alert.generated outbox events, open-encounter + vitals outpatient workflow, LiveCaptureIntegrationTests, Phase 6 verification script Done
7 Digitization workstation UI (vigilcare-records-web): Vue 3 + Pinia + Tailwind, role-based routing and nav, split-pane scan viewer (API-proxied document stream), backend-driven draft entry field visibility, verification checkboxes, clinical approval view, live capture view (new/existing encounter), patient history timeline, patient search, assign-clerk dialog, supervisor dashboard, toast notifications Done
8 Prometheus metrics (GET /metrics), custom digitization gauges/histograms/counters, MetricsCollectorService, supervisor GET /work-queue/overview, cursor-paginated GET /digitization-batches/:id/events, promotion deferral (202 / PROMOTION_DEFERRED), PromotionRetryService with exponential backoff, Docker Prometheus (9095) + Grafana (3013), Phase 8 verification script Done
9 Extended seed data (10 demo batches across all types/tracks/statuses), E2E verification script (run-vigilcare-records-verification-p9.sh), clinical scenario docs (docs/digitization-workstation-guide.md), UserManagementTests Done
10 Barcode/QR cover sheet system: CoverSheet entity, generate/lookup/list/PDF APIs, coverSheetCode on batch upload with redeem and auto-assign, printable PDF with QRCoder, /cover-sheets and barcode-assisted /intake UI views, CoverSheetBatchTests, CoverSheetPdfTests, Phase 10 verification script Done
11 HL7 FHIR R4 read-only API: FhirService, Patient/Encounter/Observation mappers with LOINC mapping, read/search/$everything controllers, CapabilityStatement metadata, bundle pagination links, FhirJsonOutputFormatter, FhirIntegrationTests, administrator FHIR Explorer UI (/fhir-explorer), Phase 11 verification script Done
12 Backend-driven batch-type field requirements: BatchTypeFieldRequirements on batch/draft responses, entry and verification forms consume fieldRequirements metadata instead of hardcoded batch-type switches Done
13 Optional OCR-assisted draft pre-fill: OcrProcessingService, Azure/Tesseract providers, ocrConfidence on draft payload, UI confidence indicators, OcrResult entity, Phase 13 verification script (run-vigilcare-records-phase-13-verification.sh); disabled by default (Ocr:Enabled=false) Done
Health probes (/health/live, /health/ready, /health/startup), user management CRUD + password endpoints, auth rate limiting, document access audit, batch cancellation, list/queue sorting, unified ExecutePromotionCoreAsync for approve + retry promote, assignment-time IN_ENTRY transition, CORS policy for production frontend origins, GET /digitization-batches/:id/document scan streaming for workstation viewer Done
S
Description
No description provided
Readme
15 MiB
Languages
C# 50.5%
Vue 16.9%
Shell 16.7%
TypeScript 15.1%
CSS 0.5%
Other 0.3%