Trent d90fc13955
CI / frontend (push) Canceled after 0s
CI / backend (push) Canceled after 5s
Docs for ci/cd setup
2026-08-11 08:06:00 +08:00
2026-06-19 15:22:14 +08:00
2026-08-11 04:32:48 +08:00
2026-08-11 08:06:00 +08:00
2026-08-05 19:22:54 +08:00
2026-08-05 00:26:20 +08:00
2026-06-25 15:30:21 +08:00
2026-08-05 00:26:20 +08:00
2026-08-05 21:49:23 +08:00
2026-06-25 15:30:21 +08:00
2026-08-07 19:36:57 +08:00

VigilCare Clinical API

A production-quality clinical backend built with ASP.NET Core 8, PostgreSQL, Apache Kafka, RabbitMQ, Elasticsearch, Redis, and MinIO. The domain models the observe-alert-acknowledge lifecycle at the center of any clinical monitoring system: patient encounters, continuous vital sign and lab result ingest, real-time sepsis and NEWS2 scoring, and clinician notification with automatic escalation.

Implementation status: Phases 138 are complete — from schema and CRUD through Kafka, Elasticsearch CQRS, sepsis detection, RabbitMQ paging with DLQ escalation, reconciliation jobs, Prometheus/Grafana observability, the MinIO Parquet data lake, clinical data model expansion, warning alerts and orders, the NEWS2 composite scoring engine, trend detection with alert suppression, qSOFA bedside screening, medication administration with alert correlation annotations, the console replay simulator, the Vue 3 ward dashboard, clinician feedback mode, Glasgow Coma Scale (GCS) scoring, SOFA organ-dysfunction scoring with baseline tracking and delta sepsis alerts, the Sepsis-3 clinical refactor (SIRS removed, qSOFA repositioned as screening, SOFA delta ≥ 2 triggers bundles), frontend GCS entry and SOFA display, expanded simulator scenarios with clinical validation, the Site & Gateway Registry with dual authentication, shared clinical sync contracts, and fleet health Prometheus gauges, the Ward Gateway Service (local-first clinical path with offline buffering and central sync), the FHIR R4 Inbound Facade for EHR integration, Role-Based Access Control (RBAC) with clinical audit logging, the Dashboard Gap Analysis Fixes (SOFA/GCS/qSOFA history charts, patient banner, encounter timeline, medication markers on vital charts), the Enhanced Dashboard (department overview, sepsis bundle board, critical alert notifications, shift handoff reports, vitals entry form, sortable/filterable ward table), Degraded Operations Visibility (gateway fleet operations panel, stale gateway auto-detection, discharge summary API, admin panels for user/threshold/audit/reconciliation management, degraded-mode banner), Alert Quality Analytics (server-side clinician feedback with AlertFeedback entity, AlertQualityAggregatorService background metrics, quality metrics API, Grafana alert quality dashboard, scenario-attributed feedback for simulated alerts), Explainable Alerts (immutable JSONB explanation on composite alerts with score contributors, trend context, structured medication context, and bedside NarrativeSummary; AlertResponse DTO on GET/list/acknowledge/resolve; dashboard AlertReasoning.vue; ES indexer and data lake propagation; ward gateway sync), MIMIC-IV Replay Scenario Generator (offline CLI tool converting real de-identified ICU data from MIT PhysioNet into VigilCare scenario JSONs; streaming CSV parser for 668K-row chartevents; 17 chart + 8 lab item ID mappings to VigilCare observation codes; GCS text-to-numeric conversion; Fahrenheit-to-Celsius; blood pressure deduplication preferring non-invasive over arterial; 10-observation cluster limit enforcement; mimic-list and mimic-generate CLI commands with Spectre.Console output; 100 patients / 140 ICU stays available for replay through NEWS2, SOFA, GCS, qSOFA, trend detection, and alerting), and self-service clinical simulation (Phases 3638: default-off in-app runner, Simulation control UI, session presets AD, ward purge/reset, and a rewritten clinical testing guide so clinicians complete sessions with no terminal). Post-phase hardening includes health check endpoints, Kafka poison pill protection, outbox dead-letter with retry tracking, data lake partial-commit safety, MRN sequence-based generation, FHIR bundle transaction rollback, FHIR R4 read/search endpoints (Patient and Encounter), alert threshold deletion with audit trail, FHIR API key rotation (constant-time multi-key validation), authorization failure logging with Prometheus metrics, JWT signing key validation at startup, token refresh and revocation (short-lived access tokens with rotating refresh tokens, server-side logout, proactive frontend refresh), and concurrency hardening (transactional sepsis bundle creation, unique active encounter constraint). See Implemented Phases for the full breakdown. Guides: dashboard-guide.md (technical), clinical-testing-guide.md (doctors & nurses — self-service Simulation sessions), simulator-guide.md (CLI / CI), vigilcare-clinical-roadmap.md (Phases 3638 order).

Domain Model — How It Maps to a Real Clinical System

In a hospital, a patient presents for care and an encounter is opened. Bedside monitors and lab systems post observations continuously against that encounter — either directly via the REST API, through the FHIR R4 facade that maps HL7 FHIR resources from integration engines (Mirth Connect, Rhapsody), or via ward gateway edge nodes that buffer observations locally during connectivity loss and sync to the central API when the link recovers. The FHIR facade also exposes read and search interactions so EHR systems can query patient and encounter data back in standard FHIR R4 format. A rules engine evaluates each observation against configured thresholds and flags abnormal values as clinical alerts. Composite scoring engines (NEWS2, GCS, SOFA, qSOFA) aggregate multiple vitals and labs into acuity scores. The sepsis pathway follows Sepsis-3 consensus: qSOFA ≥ 2 creates a bedside screening alert recommending SOFA labs; when SOFA delta ≥ 2 from baseline confirms organ dysfunction, a SOFA_SEPSIS alert triggers the treatment bundle. Clinicians authenticate via JWT with short-lived access tokens (15 min) and rotating refresh tokens (7 days), and role-based access control (RBAC) gates every endpoint by clinical role (Nurse, Physician, Admin, Integration). Clinicians acknowledge and resolve alerts. If a critical alert goes unacknowledged for five minutes, the system escalates to the on-call backup. All clinical write actions — patient registration, alert acknowledgment, threshold changes, encounter transitions — are recorded in an append-only audit log with user identity, IP address, correlation ID, and before/after state. All events flow through Kafka so the Elasticsearch dashboard, scoring engines, and data lake writer consume the same stream independently.

Patient ─────────────────────────── one patient = one MRN, many lifetime encounters
 └── Encounter                       one clinical episode (inpatient, outpatient, ED)
      ├── Observation                 one measurement: vital sign, lab value, SpO₂
      │    └── OutboxEvent            written in the same transaction → relayed to Kafka
      └── ClinicalAlert              generated on threshold breach, qSOFA screen, SOFA delta, or NEWS2 composite score
           ├── OutboxEvent            → Kafka → RabbitMQ → clinician page → escalation
           └── SepsisBundle           auto-created on SOFA_SEPSIS alert → four treatment orders → compliance tracking

Patient

A Patient is registered with demographic information and assigned a Medical Record Number (MRN) — a stable identifier that never changes across encounters. Optional clinical fields include blood type (A+, O-, etc.), known allergies, and emergency contact name/phone. Patient search supports both MRN exact match and name partial match (ILIKE).

Encounter

An Encounter is a single clinical episode. Status follows a controlled machine: scheduled → active → discharged (or cancelled from any pre-discharged state). Optional roomBed and admissionReason fields support ward assignment and clinical context; dischargeDiagnosis is set on discharge. Observations, alerts, and orders belong to an encounter, not directly to a patient — this bounds queries naturally and mirrors real clinical data ownership. Discharge triggers a RabbitMQ job to generate a discharge summary.

AlertThreshold

Alert thresholds define the numeric boundaries that trigger a clinical alert for a given observation code. Each threshold has four optional bounds: criticalLow, warningLow, warningHigh, criticalHigh. Thresholds are pre-loaded into Redis on startup and invalidated on write — they are read on every observation ingest and must not add database latency to the hot path.

Observation

An Observation is a single recorded measurement: a vital sign, lab value, or pulse oximetry reading. Observations are append-only — never updated or deleted. Each observation is evaluated against the Redis-cached threshold immediately on ingest. A CRITICAL breach synchronously creates a ClinicalAlert within the same transaction before the API returns. A WARNING breach is deferred to the Kafka consumer. This split is a deliberate patient safety decision.

An idempotencyKey (partial unique index) prevents duplicate observations when medical devices retry on network failure.

ClinicalAlert

A ClinicalAlert is generated when an observation breaches a threshold, when the qSOFA engine detects two or more organ-dysfunction criteria (screening), when SOFA delta ≥ 2 from baseline confirms sepsis, or when the NEWS2 engine computes a medium/high-risk composite score (or a single-parameter score of 3). Lifecycle: open → acknowledged → resolved (or escalated after a five-minute NACK cycle through the RabbitMQ dead-letter queue). Alerts carry an audit trail: who acknowledged, when, and with what note. Composite alerts from NEWS2, SOFA, GCS, and trend detection also carry an immutable JSONB explanation snapshot — score contributors, trend context, medication context, and a bedside narrative — frozen at alert creation time. The human-readable details string remains for backward compatibility. Only SOFA_SEPSIS alerts trigger automatic sepsis bundle creation.

SepsisBundle

A SepsisBundle is created automatically when the SOFA scoring engine detects a delta ≥ 2 from baseline (SOFA_SEPSIS alert). Each bundle contains four mandatory treatment elements (blood cultures, serum lactate, broad-spectrum antibiotics, IV fluid resuscitation) mapped to clinical orders that are created simultaneously. A one-hour compliance deadline is set from the recognition time. As linked orders are resulted, bundle elements transition to COMPLETED; when all four are done, the bundle is marked COMPLIANT (within deadline) or NON_COMPLIANT (past deadline). Only one in-progress bundle can exist per encounter at a time.

OutboxEvent

An OutboxEvent is written in the same transaction as any observation or alert, then relayed to Kafka by a background worker. This decouples Kafka availability from the ingest transaction — observations commit to PostgreSQL while Kafka is down, and the relay catches up on recovery.


Features

  • Patient Registration — register patients with MRN generation; optional blood type, allergies, and emergency contact; paginated list with name (ILIKE) and MRN (exact) search; patient detail with active encounter summary
  • Encounter Management — open encounters against a patient with optional room/bed and admission reason; encounter status state machine (scheduled → active → discharged / cancelled) with 409 on illegal transitions; unique active encounter per type per patient enforced by partial unique index (concurrent duplicate attempts return 409); optional discharge diagnosis on discharge; encounter timeline as a merged chronological view across status changes, observation summaries, and alerts
  • Alert Threshold Management — configure per-observation-code numeric bounds (criticalLow, warningLow, warningHigh, criticalHigh) for 12 observation codes; thresholds pre-loaded into Redis on startup; write-through cache invalidation on update and delete; DELETE /alert-thresholds/{id} removes a threshold with THRESHOLD_DELETED audit trail and Redis cache invalidation
  • Observation IngestPOST /encounters/:id/observations accepts single or small batch (up to 10); idempotency via Idempotency-Key header (partial unique index); plausibility validation per observation code; synchronous critical alert creation within the ingest transaction; warning-range breaches evaluated asynchronously by WarningAlertService (Kafka consumer group warning-evaluator); outbox event written in the same commit; cursor-paginated history on (encounter_id, observation_code, recorded_at DESC)
  • Warning Threshold AlertsWarningEvaluator reads thresholds from Redis; creates WARNING-severity alerts for values above warningHigh or below warningLow that are not also critical breaches; idempotent INSERT WHERE NOT EXISTS per encounter and alert type while status is OPEN or ACKNOWLEDGED; warning alerts are indexed in Elasticsearch but not published to the RabbitMQ paging queue
  • Clinical Order ManagementPOST /encounters/:id/orders create; GET /encounters/:id/orders list with optional status filter; GET /orders/:id detail; PATCH /orders/:id/status status transitions; PATCH /orders/:id/result record result and transition to Resulted; status machine enforces Pending → InProgress → Resulted and terminal Cancelled
  • Clinical Alert Lifecycle — paginated alert list per encounter and globally; acknowledge with clinician ID and optional note; resolve (must be acknowledged first); global list filterable by status, severity, and department
  • Outbox RelayIHostedService polling every 500ms; reads unprocessed outbox rows with FOR UPDATE SKIP LOCKED (safe for concurrent instances), publishes to Kafka via idempotent producer (EnableIdempotence = true), marks processed; per-event retry tracking (RetryCount, LastError); events exceeding OutboxMaxRetries (default 10) are marked permanently failed (FailedAt) and excluded from future polls; partitioned by encounterId for per-encounter ordering
  • Kafka Pipeline — core topics (observation.recorded, alert.generated, encounter.status.changed, gcs.scored, sepsis bundle topics) with six partitions each; configurable ReplicationFactor (default 3); KRaft mode, no Zookeeper; KAFKA_AUTO_CREATE_TOPICS_ENABLE=false — topics are provisioned explicitly by KafkaTopicProvisioner (including gcs.scored for SOFA CNS re-scoring and sofa.scored for downstream consumers); all consumers protected by PoisonPillGuard (permanent errors skipped, transient errors retried up to MaxPoisonRetries)
  • Elasticsearch CQRS ProjectionEsIndexerService consumer group upserts patient_encounters documents, appends to the observations index, increments openAlertCount on alert events, stamps news2Score / news2RiskLevel when a NEWS2 alert is generated, and projects sepsisBundleStatus / sepsisBundleElementsCompleted / sepsisBundleDeadlineAt from SOFA-triggered sepsis bundle events; patient/encounter search; per-encounter observation trend (hourly avg/min/max); alert volume summary by department and severity; population query (numeric range aggregation across all patients)
  • Sepsis Screening EngineSepsisEngineService Kafka consumer evaluates qSOFA criteria per encounter using Redis keys with a 30-minute TTL sliding window; qSOFA evaluates respiratory rate ≥ 22, systolic BP ≤ 100, and altered mentation (GCS < 15 or AVPU ≥ 1); on ≥ 2 active criteria and no open screening alert, inserts a QSOFA_SCREEN (WARNING-level) alert idempotently (INSERT WHERE NOT EXISTS); every evaluation is persisted to qsofa_evaluations with criteria values and screen-alert-fired flag; GET /encounters/:id/qsofa/history provides cursor-paginated evaluation history; qSOFA screening recommends ordering SOFA labs — definitive sepsis detection and bundle triggering are handled by SofaScoringService via SOFA delta ≥ 2
  • Sepsis Bundle ComplianceSepsisBundleService creates a four-element treatment bundle (blood cultures, serum lactate, broad-spectrum antibiotics, IV fluid resuscitation) when a SOFA_SEPSIS alert fires (delta ≥ 2 from baseline); each element maps to an auto-created clinical order (orderedBy: sepsis-bundle-engine); one-hour compliance deadline from recognition; OrderService.RecordResult calls back to OnOrderResultedAsync to mark elements complete; final element completion sets bundle to COMPLIANT or NON_COMPLIANT; SepsisBundleMonitorService scans every 5 minutes for overdue in-progress bundles past their deadline and marks them NON_COMPLIANT; idempotent — only one in-progress bundle per encounter; GET /encounters/:id/sepsis-bundle/current and GET /sepsis-bundles/:id expose bundle state; Kafka topics sepsis.bundle.created / sepsis.bundle.updated; Prometheus qsofa_detections_total and sepsis_bundle_compliance_total
  • NEWS2 Composite Scoring EngineNews2ScoringService Kafka consumer (news2-scoring) evaluates seven vital parameters per encounter (RESP_RATE, SPO2, SYSTOLIC_BP, HEART_RATE, AVPU, TEMP_C, SUPPLEMENTAL_O2) using Redis keys with a 4-hour TTL; when all seven are present, computes the official NEWS2 aggregate score, persists to news2_scores, and creates NEWS2_WARNING (score 56 or single param = 3) or NEWS2_EMERGENCY (score ≥ 7) alerts idempotently; consciousness resolves GCS-first with AVPU fallback; GET /encounters/:id/news2/current and /history expose score history; Prometheus news2_scores_total and news2_scoring_duration_seconds
  • Glasgow Coma Scale (GCS) ScoringGcsScoringService Kafka consumer (gcs-scoring) tracks three components (GCS_EYE, GCS_VERBAL, GCS_MOTOR) in Redis; when all three are present, computes total score and classification (MILD / MODERATE / SEVERE), persists to gcs_scores, creates GCS_CRITICAL (total ≤ 8) or GCS_WARNING (912) alerts idempotently, and publishes gcs.scored via outbox for downstream SOFA CNS re-scoring; feeds NEWS2 consciousness and qSOFA altered mentation; GET /encounters/:id/gcs exposes the latest score; GET /encounters/:id/gcs/history provides cursor-paginated score history with component breakdown; Prometheus gcs_scores_total
  • SOFA Organ-Dysfunction ScoringSofaScoringService Kafka consumer (sofa-scoring) subscribes to observation.recorded and gcs.scored; scores six organ systems (respiratory, coagulation, liver, cardiovascular, CNS, renal) from Redis lab cache with carry-forward staleness, MAP derivation, SpO₂/FiO₂ fallback, and vasopressor detection from MedicationAdministration; persists to sofa_scores with baseline tracking (≥ 4 populated organ systems) and delta-from-baseline; delta ≥ 2 creates SOFA_SEPSIS (CRITICAL), delta = 1 creates SOFA_WARNING; skips stale Kafka events when the encounter row no longer exists; GET /encounters/:id/sofa and /sofa/history expose scores; Prometheus sofa_scores_total and sofa_scoring_duration_seconds
  • Trend Detection EngineTrendAnalyzerService Kafka consumer (trend-analyzer) tracks rate-of-change for five vital parameters (HEART_RATE, RESP_RATE, SYSTOLIC_BP, TEMP_C, SPO2) using Redis sliding-window history; when velocity exceeds configured thresholds (e.g. 72→95 bpm in 30 min), creates a RAPID_DETERIORATION alert even if the current value is below warning thresholds; Prometheus trend_alerts_total and trend_analysis_duration_seconds
  • Alert Suppression Windows — acknowledging a suppressible alert (WARNING_*, NEWS2_WARNING) sets a Redis key suppress:{encounterId}:{alertType} with a configurable TTL (default 30 min from AlertSuppression config; optional per-code override via alert_thresholds.suppression_window_minutes); WarningEvaluator and News2Detector check suppression before creating new warning alerts; critical alerts (CRITICAL_*, NEWS2_EMERGENCY, SEPSIS_WARNING, RAPID_DETERIORATION) are never suppressed; observations and NEWS2 scores continue to persist during suppression; Prometheus alert_suppressions_total
  • Medication AdministrationPOST /encounters/:id/medications records drug administrations (name, dose, route, timestamp, administered-by); GET /encounters/:id/medications lists with optional since filter; GET /medications/:id detail; active-encounter guard; FluentValidation on request DTOs
  • Medication Correlation AnnotationsMedicationCorrelationHelper appends medication context to warning alert details when a mapped drug was administered within the correlation window (default 90 min); explainable alerts (NEWS2, SOFA, GCS, rapid deterioration) receive structured MedicationContext in the JSONB explanation via TryGetContextAsync(); drug-to-vital mappings in MedicationCorrelation config (appsettings.json); annotates rather than suppresses — alerts still fire; sepsis, trend, and critical sync-path alerts are never annotated; design rationale in docs/decisions/medication-correlation-design.md
  • Ward Dashboard APIsGET /encounters returns paginated WardEncounterSummary rows (patient name/MRN, room/bed, department, status, latest NEWS2 score, live qSOFA criteria count from Redis, sepsis bundle status, open alert count, SOFA score/delta, GCS score/classification, attending physician, admitted-at, last observation time); filterable by status and department; GET /encounters/:id/qsofa/current exposes Redis-backed qSOFA state; GET /sepsis-bundles lists bundles hospital-wide with optional status filter (returns SepsisBundleSummary with patient demographics, elements, and deadlines); CORS policy Dashboard allows configured origins (default http://localhost:5173)
  • Ward Dashboard Frontend — Vue 3 SPA (vigilcare-dashboard/) with virtual ward table (multi-column sortable, patient search, quick-filters for critical/alerts/sepsis, SIM badge on simulated patients), patient detail (vitals, scores, alerts, orders, sepsis bundle, GCS entry form, SOFA score panel, patient banner with demographics/allergies/emergency contact, encounter timeline, vitals entry form for manual observation recording, discharge summary panel), alert center (global acknowledge/resolve with role-aware modal and acknowledgment note preview), department overview (unit-level snapshot cards with acuity bars, patient/alert/bundle counts per department), sepsis bundle board (real-time countdown timers, on-track/at-risk/overdue urgency sorting), critical alert banner with browser notifications and audible tone, shift handoff report generator (SBAR format with ward summary, exportable via print/PDF), vital sign trend charts with medication administration markers and local replay scrubbing, NEWS2 history chart, SOFA history chart with organ-system breakdown, GCS history chart with component tracking, qSOFA evaluation history, structured alert reasoning (AlertReasoning.vue — score contributors, trend context, medication context, narrative summary from explanation), clinician feedback on every alert, Simulation control (SimulationControlView — Sessions / Scenarios tabs, speed control, run progress, typed ward reset), admin panels (threshold management, user management, audit log viewer, reconciliation viewer), gateway operations dashboard with degraded-mode banner, and alert quality analytics with quality charts and by-scenario breakdown; role-aware sidebar navigation; polls API every 510 s; guides in docs/dashboard-guide.md and docs/clinical-testing-guide.md
  • FHIR R4 Inbound FacadePOST /fhir/R4/{Patient,Encounter,Observation,MedicationAdministration} accepts FHIR R4 JSON resources (application/fhir+json); POST /fhir/R4 processes transaction Bundles (Patient → Encounter → Observation in dependency order); GET /fhir/R4/metadata returns a CapabilityStatement; LOINC-to-internal code mapping (19 observation codes + SNOMED CT fallbacks); Fahrenheit-to-Celsius unit conversion; ExternalResourceIdentifier table links hospital MRNs and visit numbers to internal UUIDs for idempotent upserts; FhirApiKeyOrJwtMiddleware authenticates via JWT bearer or X-Api-Key header (supports multiple keys via Fhir:ApiKeys array for zero-downtime rotation; constant-time comparison via CryptographicOperations.FixedTimeEquals); FhirExceptionFilter returns FHIR OperationOutcome on errors; configurable identifier systems, department codes, and encounter class mappings via Fhir config section; Prometheus fhir_ingest_total and fhir_mapping_errors_total; integration guide for Mirth Connect HL7v2→FHIR channels in docs/integration/mirth-fhir-channels.md
  • FHIR R4 Read/SearchGET /fhir/R4/Patient/{id} reads a Patient by internal ID; GET /fhir/R4/Patient searches by identifier (system|value) or lists all patients; GET /fhir/R4/Encounter/{id} reads an Encounter by internal ID; GET /fhir/R4/Encounter searches by patient (UUID) and/or status (in-progress, finished, cancelled); all return FHIR R4 JSON (application/fhir+json); search endpoints return Bundle.type=searchset; requires fhir:read permission (Admin and Integration roles); internal resources mapped back to FHIR via PatientFhirMapper.ToFhirResponse / EncounterFhirMapper.ToFhirResponse with hospital identifier resolution; Prometheus fhir_read_total counter with resource_type, interaction, outcome labels
  • Role-Based Access Control (RBAC) — JWT bearer authentication (POST /auth/login); four clinical roles (Nurse, Physician, Admin, Integration) with 18 granular permissions (patients:read, alerts:acknowledge, alerts:feedback, thresholds:write, fhir:ingest, fhir:read, audit:read, etc.); AuthorizePermission attribute on every controller action; PermissionAuthorizationHandler resolves role → permission at runtime from ClinicalRolePermissionMap and logs authorization failures with structured details (user, role, permission, endpoint) plus authorization_failures_total Prometheus counter; CurrentUserService exposes authenticated identity (user ID, display name, role, IP address) to services; nurses and physicians get clinical read/write permissions; admins additionally get thresholds:write, fhir:read, audit:read, and users:admin; integration accounts get FHIR ingest and read access; FHIR endpoints accept both JWT and X-Api-Key authentication via FhirApiKeyOrJwtMiddleware; alert acknowledgedBy is set from the authenticated user identity, not the request body; startup validates JWT signing key is at least 256 bits (HMAC-SHA256 minimum); token refresh and revocation — short-lived access tokens (15 min) paired with rotating opaque refresh tokens (7 days) stored in the refresh_tokens table; POST /auth/refresh exchanges a valid refresh token for a new access + refresh token pair (rotation on every use revokes the previous token); POST /auth/logout revokes the refresh token server-side with USER_LOGOUT audit log; frontend auto-refreshes 1 minute before expiry, retries on 401, and redirects to login when the refresh token is exhausted; logout button in header, sidebar, and mobile nav; four seeded demo users (nurse.demo, physician.demo, admin.demo, integration.mirth)
  • Clinical Audit Logging — append-only clinical_audit_logs table records clinical write actions with user identity, entity type/ID, before/after state (JSONB), reason, IP address, and correlation ID; twelve audit actions (THRESHOLD_CREATED, THRESHOLD_UPDATED, THRESHOLD_DELETED, ALERT_ACKNOWLEDGED, ALERT_RESOLVED, ENCOUNTER_STATUS_CHANGED, PATIENT_REGISTERED, SUPPRESSION_WINDOW_SET, USER_LOGIN, AUTHORIZATION_DENIED, USER_LOGOUT, TOKEN_REFRESHED); AuditService writes log entries inline with domain operations; GET /audit-logs admin-only query endpoint with filters by entity type, entity ID, user ID, action, and time range; indexed on entity type, entity ID, user ID, and timestamp
  • Site & Gateway RegistryClinicalSite and WardGateway domain entities model ward edge nodes that buffer clinical data during connectivity loss; POST /sites creates clinical sites; POST /sites/{siteId}/gateways registers gateways under a site; PATCH /gateways/{gatewayId}/heartbeat (gateway API key auth) updates status (ONLINE, DEGRADED, OFFLINE) and reported buffer depth; GET /sites/{siteId}/gateways lists gateways with optional ?department= filter; dual authentication — JWT + RBAC (users:admin) for admin CRUD, GatewayApiKeyAuthenticationHandler (X-Api-Key + X-Gateway-Id) for gateway heartbeat and future sync upload; constant-time key comparison via CryptographicOperations.FixedTimeEquals; VigilCare.ClinicalContracts shared class library with sync DTOs (ClinicalSyncBatchRequest, SyncedObservation, SyncedAlertEvent, GatewayHeartbeatRequest) consumed by both central API and ward gateway projects; Prometheus ward_gateways_offline_gauge and ward_gateway_buffer_depth via WardGatewayMetricsCollector (60s periodic); GatewayRegistrySeeder provides demo site and gateway for Docker Compose and tests; FluentValidation on all request DTOs; GatewayRegistryTests and ClinicalContractsTests integration tests
  • Ward Gateway ServiceVigilCare.WardGateway (http://localhost:5081) is a standalone ASP.NET Core 8 deployable with its own PostgreSQL, Redis, and RabbitMQ; ingests observations locally via POST /encounters/:id/observations with plausibility validation, Redis-cached threshold evaluation, and synchronous critical alert creation; LocalWarningEvaluator creates warning-range alerts; BufferedSyncWriter writes all clinical events to buffered_sync_items for central upload; EncounterReplicaSyncService pulls patient/encounter data from central API; ThresholdCacheLoader fetches thresholds from central into local Redis; CentralReachabilityService tracks central API connectivity; GatewayHeartbeatService reports status and buffer depth; SyncUploaderService batches and uploads buffered items when online; local RabbitMQ paging and escalation queues; GET /encounters ward list and GET /encounters/:id detail; GET /health/live and GET /health/ready (Redis, RabbitMQ, encounter replica readiness); Docker Compose ward-gateway profile
  • Health Check EndpointsGET /health/live (liveness — always returns 200 if the process is running) and GET /health/ready (readiness — checks PostgreSQL, Redis, Kafka, RabbitMQ, and Elasticsearch connectivity); both return structured JSON with per-check status and duration; anonymous access; suitable for Kubernetes probes and load balancer health checks
  • Kafka Poison Pill ProtectionPoisonPillGuard prevents a single un-processable message from blocking a consumer partition forever; permanent errors (malformed JSON, bad format) are skipped immediately; transient errors are retried up to MaxPoisonRetries (default 5) before the offset is committed and the message is abandoned; all eight Kafka consumers use the guard; skipped messages are logged at CRITICAL with full payload and tracked by Prometheus kafka_poison_pills_skipped_total (labeled by consumer group and topic)
  • Outbox Dead-Letter with Retry TrackingOutboxRelayService tracks RetryCount, LastError, and FailedAt per event; events that fail OutboxMaxRetries (default 10) Kafka produce attempts are marked permanently failed (FailedAt set) and excluded from future relay polls; uses FOR UPDATE SKIP LOCKED for safe concurrent relay instances; idempotent Kafka producer (EnableIdempotence = true) prevents duplicate messages from network-level retries
  • Data Lake Partial-Commit SafetyDataLakeWriterService commits Kafka offsets only for topic-partitions where all MinIO uploads succeeded; failed partition buffers are retained in memory and retried on the next flush cycle; prevents data loss from partial upload failures
  • ThresholdCacheLoader Resilience — retries Redis connection up to 3 times with exponential backoff (2s, 4s, 8s); if Redis remains unavailable, the application starts without the cache and observation ingest falls back to PostgreSQL queries for threshold lookups
  • MRN Sequence Generation — MRN numbers are generated via a PostgreSQL sequence (mrn_seq) instead of MAX+1 queries; eliminates race conditions under concurrent patient registration; configurable prefix and digit count via PatientOptions
  • FHIR Bundle Transaction RollbackFhirBundleProcessor wraps all bundle entry processing in a database transaction; on any entry failure, the transaction is rolled back and the response includes the OperationOutcome for the failed entry; prevents partial state from orphaned Patient/Encounter records
  • Clinician Feedback Mode — six quick ratings per alert (useful, too early, too late, false positive, missing context, would act); optional notes; server-side AlertFeedback entity persisted per user per alert (POST /alerts/{id}/feedback); alerts:feedback permission for Nurse, Physician, and Admin roles; Feedback Summary with aggregate stats and JSON/CSV export; client-side persistence for product research
  • Alert Quality AnalyticsAlertQualityAggregatorService periodically computes per-alert-type quality metrics (acknowledgement rate, false positive rate, useful rate, would-act rate, avg seconds to acknowledge/resolve) into alert_quality_metrics table; AlertQualityMetricsController exposes GET /alerts/quality-metrics (time-range filterable, optional alert type), GET /alerts/quality-metrics/summary, and GET /alerts/quality-metrics/feedback (per-alert feedback rows with optional ?scenarioId= filter and scenarioId/sessionId attribution when simulation is enabled); Grafana alert quality dashboard (infra/grafana/dashboards/alert-quality-dashboard.json); frontend AlertQualityAnalytics.vue with AlertQualityChart.vue, scenario filter, by-scenario breakdown, and CSV export including attribution columns; Prometheus alert_quality_useful_rate and alert_quality_false_positive_rate gauges
  • Explainable AlertsAlertExplanation value object (ScoreContributor, TrendContext, MedicationContext, NarrativeSummary) serialized as JSONB on ClinicalAlert.Explanation at creation time; AlertExplanationBuilder and contributor builders (NEWS2, SOFA, GCS, trend) assemble explanation from scoring outputs; ClinicalAlertFactory idempotent INSERT with explanation; NEWS2, SOFA, GCS, and TrendDetector wire explanation and include explanation in alert.generated outbox payloads; AlertResponse DTO exposes optional Explanation on GET/list/acknowledge/resolve; Elasticsearch indexes NarrativeSummary; data lake Parquet includes explanation_json; ward gateway LocalClinicalAlert.ExplanationJson synced via ClinicalSyncBatchProcessor; dashboard AlertReasoning.vue + alertExplanation.js composable render structured reasoning; simulator ExpectedOutcomeValidator supports narrativeContains on key scenarios; ExplainableAlertsTests (10 tests) + run-phase34-verification.sh
  • Degraded Operations VisibilityGatewayStaleDetectorService auto-marks gateways OFFLINE when heartbeat exceeds configurable StaleThresholdMinutes; OperationsController (GET /operations/gateways, GET /operations/gateways/{id}, GET /operations/sites/{siteId}/summary) provides fleet management API; DischargeSummaryService with GET /encounters/{id}/discharge-summary (info) and GET /encounters/{id}/discharge-summary/content (MinIO PDF download); DischargeSummaryPanel.vue on patient detail; DegradedModeBanner.vue warns when gateways are offline; GatewayOperations.vue operations dashboard
  • User ManagementUsersController (GET /users, POST /users, PATCH /users/{id}) for admin user account CRUD; UserService with BCrypt password hashing; UserManagementView.vue with UserFormModal.vue (create/edit users, role assignment, active toggle)
  • Admin Dashboard PanelsThresholdManagementView.vue with ThresholdFormModal.vue (create/edit alert thresholds); AuditLogView.vue (filterable audit log viewer with action/entity/user/date filters); ReconciliationView.vue (safety finding viewer); sidebar navigation with role-aware admin section; CollapsibleSection.vue and SeverityBadge.vue UI components
  • In-App Clinical Simulation (Phases 3638) — default-off Simulation:Enabled gate; ScenarioCatalog / SessionCatalog load scenario JSON and sessions.json presets; SimulationRunner hosted service replays via loopback API client; patients marked IsSimulated; simulation_runs with optional SessionId; REST under /api/v1/simulation (config, scenarios, sessions start, runs, data summary, purge); dashboard Simulation page (Sessions default tab, Scenarios catalogue, speed control, run panel, typed Reset ward); all-or-nothing multi-run session start with staggered launch; purge deletes only simulated patients and dependents and audits SIMULATION_DATA_PURGED; clinicians follow docs/clinical-testing-guide.md with no terminal
  • Console Replay Simulator — standalone VigilCare.Simulator .NET console app (developer/CI tool — clinicians use the in-app Simulation page) replays JSON scenario files against the live API with configurable speed (--speed 0 instant, 60 = 60× faster); commands: replay, replay-all, validate, dry-run, mimic-list, mimic-generate; optional --poll shows alerts, NEWS2, GCS, SOFA, and sepsis bundle state during replay; --gateway targets the ward gateway (http://localhost:5081) with --encounter-id, --skip-setup, and --gateway-token; alert_ack events poll for open alerts on central before acknowledging (handles async alert pipeline at --speed 0); ExpectedOutcomeValidator validates alert narrativeContains on key scenarios; twelve sample scenarios in VigilCare.Simulator/Scenarios/List/ (including ward outage reconnect, GCS neurological decline, SOFA sepsis progression, and SpO₂/FiO₂ fallback); session presets in VigilCare.Simulator/Scenarios/sessions.json; MIMIC-IV scenario generation from real ICU data; user guide in docs/simulator-guide.md
  • MIMIC-IV Scenario Generator — offline CLI tool that reads MIMIC-IV CSV files (docs/MIMIC-IV/, 100 patients, 140 ICU stays, 668K chart events, 107K lab events) and generates VigilCare scenario JSONs; mimic-list <dir> displays a Spectre.Console table of available stays with demographics, care unit, LOS, and outcome; mimic-generate <dir> --stay-id <id> produces a scenario with options for --max-hours, --no-medications, --no-labs, --validate; streaming CSV parser (MimicCsvReader) filters 668K-row chartevents by stay_id and item ID set at the string level before allocating records; MimicItemMap maps 17 chart event items (vitals, GCS, FiO₂, PaO₂, labs) and 8 lab event items to VigilCare observation codes; GCS text labels ("Obeys Commands" → 6, "To Speech" → 3) resolved from valuenum with text-to-numeric fallback dictionary; Fahrenheit temperature converted to Celsius; blood pressure deduplication prefers non-invasive (NBP) over arterial (ABP); 10-observation-per-cluster limit enforced by priority-based splitting (vitals first, labs spill to next offset); medications from prescriptions with dose parsing; generated scenarios pass ScenarioValidator and replay through the standard replay command
  • RabbitMQ Notification WorkersNotificationPublisherService reads alert.generated from Kafka and publishes paging jobs to alerts.paging.queue; PagingWorkerService sends the page and waits for acknowledgment; if no ack arrives before timeout it NACKs to alerts.paging.dlq with x-message-ttl = 300000ms; if the host is stopping, in-flight paging messages are NACKed with requeue=true so they are retried after restart and do not false-escalate; EscalationWorkerService pages the on-call backup and sets alert status to escalated; DischargeSummaryWorkerService reads encounter.status.changed, generates a discharge summary, and stores it in MinIO under /discharge-summaries/{encounterId}/summary.pdf
  • Data Lake WriterDataLakeWriterService (consumer group data-lake-writer) buffers observation.recorded, alert.generated, and encounter.status.changed events, flushes date-partitioned Parquet files to MinIO (/observations/, /alerts/, /encounters/), and commits Kafka offsets only for topic-partitions where all uploads succeeded; failed partition buffers are retained in memory and retried on the next flush cycle (prevents data loss from partial upload failures); shutdown flush uses an uncanceled token so MinIO writes complete on Ctrl+C; kafka_partition and kafka_offset columns provide audit lineage
  • Reconciliation Jobs — three scheduled checks: (1) unacknowledged CRITICAL alerts older than 30 minutes, (2) pending orders without results after 4 hours, (3) active inpatients with no observation in 2 hours; each finding creates a reconciliation_alerts row and publishes to RabbitMQ
  • Standard Envelope — all responses use a consistent { success, statusCode, data, error } wrapper; validation errors use the same shape; ApiBehaviorOptions overridden so model validation also produces the standard envelope with field-level details
  • Input Validation — FluentValidation validators on all request DTOs (patient registration, encounter open, observation ingest, alert acknowledge, alert thresholds, orders); invalid requests return 400 before reaching the service layer
  • Observability — Serilog structured logging enriched with correlationId, encounterId, patientId on alert paths; Seq sink (http://localhost:5345); Prometheus (http://localhost:9101) scrapes GET /metrics; application metric families via ClinicalMetrics (including GCS and SOFA scoring) and three background collectors (AlertsUnacknowledgedCollector, OutboxPendingCollector, KafkaConsumerLagCollector); Grafana clinical dashboard (http://localhost:3101, admin/admin) with alerts_unacknowledged_gauge as the primary safety panel; per-request correlation IDs in request logs and X-Correlation-Id response headers
  • Swagger UI — OpenAPI spec via Swashbuckle (Development only)

Architecture

HTTP request
  → FhirApiKeyOrJwtMiddleware (X-Api-Key or JWT bearer for /fhir/* routes)
  → CorrelationIdMiddleware
  → ExceptionHandlerMiddleware
  → JWT Authentication + RBAC (PermissionAuthorizationHandler) / GatewayApiKeyAuthenticationHandler (X-Api-Key for gateway routes)
  → Controllers (REST API + FHIR R4 ingest + Site/Gateway registry)
  → Services
      ├── CurrentUserService (authenticated user identity from JWT claims)
      ├── AuthService (login, refresh token rotation, logout with revocation)
      ├── AuditService (append-only clinical_audit_logs on write actions)
      ├── PostgreSQL (EF Core — writes, keyed reads)
      ├── Redis (threshold cache, qSOFA state, NEWS2 parameter state, GCS state, SOFA lab cache, trend history, alert suppression keys)
      └── OutboxEvent (same transaction as domain write)

IHostedServices (background):
  ThresholdCacheLoader   → pre-loads Redis on startup
  KafkaTopicProvisioner  → creates topics with correct partition count
  RabbitMqTopologyProvisioner → declares exchange, queues, DLQ bindings
  ElasticIndexProvisioner → creates index mappings
  OutboxRelayService     → PostgreSQL outbox → Kafka (every 500ms)
  EsIndexerService       → Kafka → Elasticsearch (consumer group: es-indexer)
  SepsisEngineService    → Kafka → Redis qSOFA state → PostgreSQL QSOFA_SCREEN alert (consumer group: sepsis-engine)
  WarningAlertService    → Kafka → WarningEvaluator (+ MedicationCorrelationHelper) → PostgreSQL WARNING alert (consumer group: warning-evaluator)
  News2ScoringService    → Kafka → News2Detector (+ MedicationCorrelationHelper) → Redis NEWS2 state → PostgreSQL score + alert (consumer group: news2-scoring)
  GcsScoringService      → Kafka → GcsDetector → Redis GCS components → PostgreSQL gcs_scores + gcs.scored outbox (consumer group: gcs-scoring)
  SofaScoringService     → Kafka (observation.recorded + gcs.scored) → SofaDetector → Redis SOFA lab cache → PostgreSQL sofa_scores + delta alerts → SepsisAlertHandler → SepsisBundleService on SOFA_SEPSIS (consumer group: sofa-scoring)
  TrendAnalyzerService   → Kafka → TrendDetector → Redis trend history → PostgreSQL RAPID_DETERIORATION alert (consumer group: trend-analyzer)
  AlertSuppressionService → Redis suppress:{enc}:{type} keys set on acknowledge; read by WarningEvaluator, News2Detector, QsofaDetector
  NotificationPublisherService → Kafka → RabbitMQ paging.queue (consumer group: notification-publisher)
  PagingWorkerService    → RabbitMQ paging.queue → log page → NACK on timeout (or requeue on shutdown)
  EscalationWorkerService → RabbitMQ escalation.queue → update alert status
  DischargeSummaryWorkerService → RabbitMQ discharge.queue → MinIO PDF
  DataLakeWriterService  → Kafka (data-lake-writer) → Parquet files in MinIO
  ReconciliationScheduler → three scheduled safety checks → reconciliation_alerts + RabbitMQ
  SepsisBundleMonitorService → polls overdue bundles every 5 min → marks NON_COMPLIANT
  PoisonPillGuard (per consumer) → skips permanently malformed messages after MaxPoisonRetries
  AlertsUnacknowledgedCollector → polls PostgreSQL every 30s → alerts_unacknowledged_gauge
  OutboxPendingCollector     → polls outbox every 30s → outbox_pending_events
  KafkaConsumerLagCollector  → polls four consumer groups every 30s → kafka_consumer_lag
  WardGatewayMetricsCollector → polls gateway status/buffer depth every 60s → ward_gateways_offline_gauge, ward_gateway_buffer_depth
  GatewayStaleDetectorService → polls gateways on interval → marks OFFLINE when heartbeat exceeds StaleThresholdMinutes
  AlertQualityAggregatorService → periodically computes per-alert-type quality metrics → alert_quality_metrics table + Prometheus gauges
  SimulationRunner (when Simulation:Enabled) → in-app scenario/session replay via loopback API; marks patients IsSimulated
  ClinicalMetrics (singleton) → inline counters/histogram from ingest, qSOFA, NEWS2, GCS, SOFA, trend, suppression, bundle compliance, escalation paths, alert quality

Ward Gateway (VigilCare.WardGateway — separate deployable on port 5081):
  HTTP request → ExceptionHandlerMiddleware → Controllers (Observations, Alerts, Encounters, CentralRequired)
  Services:
    LocalObservationService → PostgreSQL (ward) + Redis (ward) threshold cache → critical alert + outbox
    LocalWarningEvaluator   → Redis threshold lookup → WARNING alert creation
    LocalAlertService       → Acknowledge/resolve local alerts
    BufferedSyncWriter      → Writes sync items to buffered_sync_items table
    EncounterReadService    → Ward encounter list and detail views
  BackgroundServices:
    EncounterReplicaSyncService → pulls patient/encounter data from central API
    ThresholdCacheLoader        → fetches thresholds from central → local Redis
    CentralReachabilityService  → polls central /health/ready every 30s
    GatewayHeartbeatService     → reports status + buffer depth to central registry
    SyncUploaderService         → batches buffered_sync_items → central API upload
    LocalPagingWorkerService    → local RabbitMQ paging queue → clinician page
    LocalEscalationWorkerService → local RabbitMQ escalation queue → on-call backup

Why Kafka and RabbitMQ coexist: Kafka is an append-only log — the same observation event reaches the Elasticsearch indexer, the sepsis engine, and the data lake independently without coordination. Each consumer holds its own offset and can replay from the beginning. RabbitMQ handles the action side: one message, one worker, one page. A duplicate page at 3am is a patient safety concern, not a minor inconvenience — RabbitMQ's acknowledgment-then-delete model is correct here. The DLQ TTL-based escalation has no equivalent in Kafka.


Tech Stack

Layer Technology
Server ASP.NET Core 8 (.NET 8.0)
Database PostgreSQL 16 with EF Core 8 (code-first migrations)
Cache / scoring state Redis 7
Message log Apache Kafka 3.7 (KRaft, 6 partitions per topic)
Task queue RabbitMQ 3.13 (direct exchange, DLQ escalation)
Search / analytics Elasticsearch 8.13 (CQRS read projection)
Data lake MinIO (Parquet, S3-compatible)
Logging Serilog + Seq sink
Metrics prometheus-net.AspNetCore (GET /metrics)
Dashboards Prometheus 2.52 + Grafana 10.4
Data lake format Parquet.Net 4.x
Authentication JWT Bearer (Microsoft.AspNetCore.Authentication.JwtBearer)
Password hashing BCrypt.Net-Next
FHIR Hl7.Fhir.R4 (Firely SDK — parsing, serialization, model)
Docs Swagger / OpenAPI (Swashbuckle)
Validation FluentValidation.AspNetCore
Testing xUnit + Testcontainers + WebApplicationFactory
Ward dashboard Vue 3 + Vite + Pinia + Tailwind CSS v4 + Chart.js (vigilcare-dashboard/)

Project Structure

VigilCareClinicalAPI/
├── Program.cs                                  # Service registration, middleware, seed on startup
├── appsettings.json                            # Connection strings, Kafka, Elasticsearch, RabbitMQ, MinIO, Serilog, ReconciliationJobs
├── Controllers/
│   ├── AuthController.cs                       # JWT login, token refresh, logout, authenticated user profile
│   ├── AuditLogsController.cs                  # Clinical audit log query (Admin only)
│   ├── PatientsController.cs                   # Patient CRUD, search by name/MRN
│   ├── EncountersController.cs                 # Encounter list (ward summary), get, status PATCH, timeline
│   ├── MedicationsController.cs                # Medication administration create, list, get
│   ├── QsofaController.cs                      # Current qSOFA criteria count (Redis-backed) + cursor-paginated evaluation history
│   ├── ObservationsController.cs               # Ingest POST, cursor-paginated GET
│   ├── AlertThresholdsController.cs            # Threshold CRUD + cache invalidation
│   ├── AlertsController.cs                     # Alert list (global + per-encounter), get by ID, acknowledge, resolve → AlertResponse
│   ├── OrdersController.cs                     # Order create, list, get, status transition, record result
│   ├── News2Controller.cs                      # Current NEWS2 score and cursor-paginated history
│   ├── GcsController.cs                        # Latest GCS score and cursor-paginated history per encounter
│   ├── SofaController.cs                       # Current SOFA score and cursor-paginated history
│   ├── SepsisBundlesController.cs              # Current bundle per encounter, bundle detail by ID
│   ├── SitesController.cs                      # Clinical site CRUD (create, list, get)
│   ├── GatewaysController.cs                   # Gateway register, list, get, heartbeat (dual auth: JWT + API key)
│   ├── OperationsController.cs                 # Gateway fleet operations: fleet list, gateway detail, site summary (Admin only)
│   ├── UsersController.cs                      # Clinical user account management: list, create, update (Admin only)
│   ├── AlertQualityMetricsController.cs        # Alert quality metric snapshots and aggregate summary
│   ├── FhirIngestController.cs                 # FHIR R4 ingest: Patient, Encounter, Observation, MedicationAdministration, Bundle
│   ├── FhirReadController.cs                   # FHIR R4 read/search: GET Patient/{id}, GET Patient, GET Encounter/{id}, GET Encounter
│   ├── FhirMetadataController.cs               # FHIR R4 CapabilityStatement (GET /fhir/R4/metadata)
│   └── AnalyticsController.cs                  # Elasticsearch-backed patient search, trend, alert summary, population
├── Domains/
│   ├── Entities/
│   │   ├── Patient.cs
│   │   ├── Encounter.cs                        # Status machine; SetStatus() enforces transition matrix
│   │   ├── AlertThreshold.cs
│   │   ├── Observation.cs                      # Append-only; IdempotencyKey; partial unique index
│   │   ├── ClinicalAlert.cs                    # open → acknowledged → resolved / escalated; JSONB Explanation snapshot
│   │   ├── Order.cs
│   │   ├── News2Score.cs                       # Composite score with seven component scores + risk level
│   │   ├── GcsScore.cs                         # Eye/verbal/motor components, total, classification
│   │   ├── SofaScore.cs                        # Six organ-system scores, baseline flag, delta, staleness JSON
│   │   ├── QsofaEvaluation.cs                  # Per-evaluation qSOFA record: criteria count, values, screen alert fired
│   │   ├── OutboxEvent.cs                      # topic + payload JSONB + processed_at
│   │   ├── ReconciliationAlert.cs
│   │   ├── SepsisBundle.cs                     # Four-element treatment bundle with 1-hour compliance deadline
│   │   ├── SepsisBundleElement.cs              # Individual bundle element linked to a clinical order
│   │   ├── MedicationAdministration.cs         # Drug administration record per encounter
│   │   ├── ExternalResourceIdentifier.cs       # Links external system identifiers (MRN, visit#) to internal UUIDs
│   │   ├── ClinicalSite.cs                     # Hospital site with site code, name, address
│   │   ├── WardGateway.cs                      # Ward edge node with status, buffer depth, heartbeat, sync timestamps
│   │   ├── ClinicalUser.cs                     # Username, BCrypt password hash, display name, role, active flag
│   │   ├── RefreshToken.cs                     # Opaque refresh token with user FK, expiry, revocation timestamp
│   │   ├── ClinicalAuditLog.cs                 # Append-only audit trail: action, entity, user, before/after JSONB, IP, correlation ID
│   │   ├── AlertFeedback.cs                    # Clinician feedback per alert (one per user per alert)
│   │   └── AlertQualityMetric.cs               # Per-alert-type quality metric snapshots (acknowledgement/false-positive/useful rates)
│   ├── ValueObjects/
│   │   └── AlertExplanation.cs                 # ScoreContributor, TrendContext, MedicationContext, NarrativeSummary
│   └── Enums/
│       ├── EncounterStatus.cs                  # Scheduled, Active, Discharged, Cancelled
│       ├── ClinicalRole.cs                     # Nurse, Physician, Admin, Integration
│       ├── AuditAction.cs                      # ThresholdCreated/Updated/Deleted, AlertAcknowledged/Resolved, EncounterStatusChanged, PatientRegistered, SuppressionWindowSet, UserLogin, AuthorizationDenied, UserLogout, TokenRefreshed
│       ├── EncounterType.cs                    # Inpatient, Outpatient, Emergency
│       ├── AlertSeverity.cs                    # Warning, Critical
│       ├── AlertStatus.cs                      # Open, Acknowledged, Resolved, Escalated
│       ├── AlertType.cs                        # Threshold breach, QSOFA_SCREEN, SOFA_SEPSIS, warning*, NEWS2_*, GCS_*, …
│       ├── AlertFeedbackType.cs                 # Useful, TooEarly, TooLate, FalsePositive, MissingContext, WouldAct
│       ├── GatewayStatus.cs                    # Online, Degraded, Offline with ToDbString/FromDbString
│       ├── BloodType.cs                        # A+, O-, AB-, … with ToDbString/FromDbString
│       ├── ObservationSource.cs                # Device, Manual, Lab
│       ├── SepsisBundleComplianceStatus.cs     # InProgress, Compliant, NonCompliant
│       ├── SepsisBundleElementStatus.cs        # Pending, Completed
│       ├── SepsisBundleElementType.cs          # BloodCultures, SerumLactate, BroadSpectrumAntibiotics, IvFluidResuscitation
│       ├── ExternalResourceType.cs              # Patient, Encounter — for external identifier linking
│       └── OrderType.cs / ReconciliationCheckType.cs / Department.cs / OrderStatus.cs
│   └── Json/
│       ├── ObservationSourceJsonConverter.cs
│       ├── DepartmentJsonConverter.cs
│       └── BloodTypeJsonConverter.cs           # Clinical notation (A+, AB-) in JSON API
├── Fhir/
│   ├── Codes/
│   │   ├── LoincCodeMapper.cs                  # LOINC → internal observation code (19 codes + SNOMED CT fallbacks)
│   │   ├── LoincMapping.cs                     # Code mapping record (InternalCode, ExpectedUnit, AllowFahrenheit)
│   │   └── FhirUnitConverter.cs                # Fahrenheit→Celsius conversion for temperature observations
│   ├── Mapping/
│   │   ├── PatientFhirMapper.cs                # FHIR Patient ↔ internal Patient upsert
│   │   ├── EncounterFhirMapper.cs              # FHIR Encounter ↔ internal Encounter upsert (ACT class, department, status)
│   │   ├── ObservationFhirMapper.cs            # FHIR Observation → IngestObservationRequest (single + component)
│   │   ├── MedicationAdministrationFhirMapper.cs # FHIR MedicationAdministration → CreateMedicationAdministrationRequest
│   │   ├── FhirReferenceResolver.cs            # Resolves FHIR references (identifier or UUID) to internal IDs
│   │   └── FhirMappingHelpers.cs               # DateTimeOffset extraction, reference parsing utilities
│   ├── FhirBundleProcessor.cs                  # Transaction Bundle processing in dependency order (Patient→Encounter→Obs)
│   ├── FhirExceptionFilter.cs                  # Converts exceptions to FHIR OperationOutcome responses
│   ├── FhirMappingException.cs                 # Typed exception for FHIR mapping failures
│   └── FhirOperationOutcomeBuilder.cs          # Builds FHIR OperationOutcome from exceptions and error codes
├── Authorization/
│   ├── AuthorizePermissionAttribute.cs         # [AuthorizePermission("patients:read")] attribute
│   ├── ClinicalPermissions.cs                  # 17 permission constants (patients:read, thresholds:write, fhir:read, audit:read, …)
│   ├── ClinicalRolePermissionMap.cs            # Role → permission set (Nurse, Physician, Admin, Integration)
│   ├── PermissionAuthorizationHandler.cs       # ASP.NET Core authorization handler resolving role claims
│   ├── PermissionPolicyProvider.cs             # Dynamic policy provider for perm:* policies
│   └── PermissionRequirement.cs                # IAuthorizationRequirement for a single permission string
├── Authentication/
│   └── GatewayApiKeyAuthenticationHandler.cs   # X-Api-Key + X-Gateway-Id auth for gateway heartbeat/sync routes
├── Services/
│   ├── Interfaces/                             # IPatientService, IEncounterService, IAuditService, IAuthService, ICurrentUserService, ISiteService, IGatewayRegistryService, …
│   ├── AuthService.cs                          # Login (BCrypt verify), JWT generation, refresh token rotation, logout revocation, audit logging
│   ├── AuditService.cs                         # Append-only clinical audit log writer (user, entity, before/after, IP, correlation ID)
│   ├── CurrentUserService.cs                   # Extracts authenticated user identity from JWT claims (HttpContext)
│   ├── PatientService.cs
│   ├── EncounterService.cs                     # Status state machine + ConflictException on invalid transitions
│   ├── AlertThresholdService.cs                # CRUD + Redis write-through invalidation
│   ├── ObservationService.cs                   # Ingest transaction: idempotency → plausibility → threshold → alert → outbox; emits Prometheus counters
│   ├── ObservationQueryService.cs              # Cursor-paginated history
│   ├── AlertService.cs                         # Acknowledge (sets suppression), resolve, list → AlertResponse with optional Explanation
│   ├── AlertSuppressionService.cs              # Redis suppress:{enc}:{type} TTL keys
│   ├── OrderService.cs                         # Order lifecycle; status machine; calls SepsisBundleService.OnOrderResultedAsync on result
│   ├── News2Service.cs                         # Current score + cursor-paginated history from PostgreSQL
│   ├── GcsService.cs                           # Latest GCS score from PostgreSQL
│   ├── SofaService.cs                          # Current, baseline, and history SOFA scores
│   ├── SepsisBundleService.cs                  # Bundle creation, element completion, compliance evaluation
│   ├── MedicationService.cs                    # Medication CRUD; GetRecentForEncounterAsync for correlation
│   ├── QsofaService.cs                         # Redis-backed qSOFA criteria count for API/dashboard
│   ├── ExternalIdentifierService.cs            # Links/resolves external system identifiers to internal UUIDs
│   ├── SiteService.cs                          # Clinical site CRUD
│   ├── GatewayRegistryService.cs               # Gateway register, list, heartbeat, mark offline
│   ├── OperationsService.cs                    # Gateway fleet queries, gateway detail, site summary aggregation
│   ├── UserService.cs                          # Clinical user account CRUD with BCrypt password hashing
│   ├── DischargeSummaryService.cs              # Discharge summary info and MinIO PDF content retrieval
│   ├── AlertQualityMetricsService.cs           # Alert quality metric listing and summary computation
│   ├── WarningEvaluator.cs                     # Warning-range evaluation; suppression + medication annotation; idempotent INSERT
│   ├── AnalyticsService.cs                     # Elasticsearch query wrappers
│   └── PlausibilityValidator.cs                # Per-code numeric range guard
├── Alerts/
│   ├── ClinicalAlertFactory.cs                 # Idempotent alert INSERT with explanation JSON; outbox payload serialization
│   └── AlertExplanationBuilder.cs              # Assembles explanation from contributors, trend, medication context
├── Trend/
│   ├── TrendCalculator.cs                      # Pure static rate-of-change logic
│   └── TrendDetector.cs                        # Redis history + RAPID_DETERIORATION alert creation
├── Medication/
│   └── MedicationCorrelationHelper.cs          # String annotation on warning details; structured MedicationContext for explanations
├── Validators/                                 # FluentValidation — RegisterPatient, OpenEncounter, IngestObservation, CreateMedicationAdministration, CreateSiteRequest, RegisterGatewayRequest, GatewayHeartbeatRequest, …
├── Observability/
│   └── Metrics/
│       └── ClinicalMetrics.cs                  # Prometheus metric families (counters, histograms, gauges); includes FHIR read and authorization failure metrics
├── BackgroundServices/
│   ├── ThresholdCacheLoader.cs                 # Pre-loads all thresholds into Redis on startup
│   ├── KafkaTopicProvisioner.cs                # Creates topics with NumPartitions from config
│   ├── OutboxRelayService.cs                   # Polls outbox every 500ms; publishes to Kafka; marks processed
│   ├── Metrics/
│   │   ├── AlertsUnacknowledgedCollector.cs    # Polls open CRITICAL alerts > 5 min → alerts_unacknowledged_gauge
│   │   ├── OutboxPendingCollector.cs           # Polls unprocessed outbox rows → outbox_pending_events
│   │   ├── KafkaConsumerLagCollector.cs        # Lag for es-indexer, sepsis-engine, notification-publisher, data-lake-writer
│   │   └── WardGatewayMetricsCollector.cs      # Polls gateway status/buffer depth every 60s → offline gauge, buffer depth
│   ├── GatewayStaleDetectorService.cs          # Periodic stale gateway detection → auto-marks OFFLINE
│   ├── AlertQualityAggregatorService.cs        # Periodic alert quality metric computation → alert_quality_metrics table
│   ├── ElasticsSearch/
│   │   ├── ElasticIndexProvisioner.cs          # Creates patient_encounters, observations, clinical_alerts indices
│   │   └── EsIndexerService.cs                 # consumer group: es-indexer; upserts Elasticsearch documents
│   ├── SepsisEngineService.cs                  # consumer group: sepsis-engine; qSOFA screening via Redis TTL keys
│   ├── WarningAlertService.cs                  # consumer group: warning-evaluator; observation.recorded → WARNING alerts
│   ├── News2ScoringService.cs                  # consumer group: news2-scoring; observation.recorded → NEWS2 score + alert
│   ├── GcsScoringService.cs                    # consumer group: gcs-scoring; observation.recorded → GCS score + alert
│   ├── SofaScoringService.cs                   # consumer group: sofa-scoring; observation.recorded + gcs.scored → SOFA score + delta alerts
│   ├── TrendAnalyzerService.cs                 # consumer group: trend-analyzer; observation.recorded → RAPID_DETERIORATION alert
│   ├── SepsisBundleMonitorService.cs           # Polls every 5 min; marks overdue in-progress bundles NON_COMPLIANT
│   ├── Notifications/
│   │   ├── NotificationPublisherService.cs     # consumer group: notification-publisher; alert.generated → RabbitMQ paging.queue
│   │   ├── PagingWorkerService.cs              # RabbitMQ consumer; logs page; NACK on ack timeout → DLQ, requeue on graceful shutdown
│   │   ├── EscalationWorkerService.cs          # RabbitMQ escalation.queue; logs escalation; sets alert.status = escalated
│   │   └── DischargeSummaryWorkerService.cs    # RabbitMQ discharge.queue; generates summary PDF; uploads to MinIO
│   └── Reconciliation/
│       ├── ReconciliationScheduler.cs          # Runs three safety checks on a configurable interval
│       ├── UnacknowledgedAlertsCheck.cs        # CRITICAL alerts unacknowledged > 30 min
│       ├── PendingOrdersCheck.cs               # Pending orders without results > 4 hours
│       ├── DisconnectedMonitorsCheck.cs      # Active inpatients with no observation > 2 hours
│       └── ReconciliationPublisher.cs          # Publishes findings to notifications.reconciliation.queue
├── Infrastructure/
│   ├── PoisonPillGuard.cs                      # Kafka consumer protection — skips permanent errors, retries transient up to MaxPoisonRetries
│   ├── ElasticsearchHealthCheck.cs             # Readiness check for Elasticsearch connectivity
│   ├── HealthCheckResponseWriter.cs            # Structured JSON response for /health/* endpoints
│   ├── KafkaHealthCheck.cs                     # Readiness check for Kafka broker connectivity
│   ├── RabbitMqHealthCheck.cs                  # Readiness check for RabbitMQ connectivity
│   └── RedisHealthCheck.cs                     # Readiness check for Redis connectivity
├── Configuration/
│   ├── KafkaOptions.cs / KafkaTopicOptions.cs  # Includes ReplicationFactor, OutboxMaxRetries, MaxPoisonRetries
│   ├── RabbitMqOptions.cs / MinioOptions.cs
│   ├── ReconciliationJobOptions.cs
│   ├── MedicationCorrelationOptions.cs         # Drug-vital mappings + correlation window
│   ├── PatientOptions.cs                       # MRN prefix + digit count for sequence-based generation
│   ├── FhirOptions.cs                          # API key (single + rotation array), identifier systems, department/class maps, defaults
│   ├── JwtOptions.cs                           # Issuer, audience, signing key, access token expiration (15 min), refresh token expiration (7 days)
│   ├── DashboardOptions.cs                     # CORS origins for ward dashboard frontend
│   ├── GatewayMonitoringOptions.cs             # Stale gateway detection interval and threshold
│   └── AlertQualityOptions.cs                  # Alert quality aggregation interval
├── Sepsis/
│   ├── AlertCreationGuard.cs                   # Prevents creation of deprecated alert types (SEPSIS_WARNING)
│   ├── QsofaCalculator.cs                      # Pure static qSOFA scoring (3 criteria, no I/O)
│   ├── QsofaDetector.cs                        # Redis qSOFA state, QSOFA_SCREEN alert creation, evaluation persistence to qsofa_evaluations
│   └── SepsisAlertHandler.cs                   # Bridges SOFA_SEPSIS alert → SepsisBundleService
├── News2/
│   ├── News2Calculator.cs                      # Pure static NEWS2 scoring tables (no I/O)
│   └── News2Detector.cs                        # Redis parameter state, score persistence, alert creation
├── Gcs/
│   ├── GcsCalculator.cs                        # GCS total, classification, NEWS2/qSOFA/SOFA mappings
│   └── GcsDetector.cs                          # Redis component state, score persistence, gcs.scored outbox
├── Sofa/
│   ├── SofaCalculator.cs                       # Six organ-system SOFA scoring (04 each)
│   ├── SofaDetector.cs                         # Lab cache compose, baseline/delta, alert creation
│   ├── SofaLabCache.cs                         # Redis carry-forward with staleness classification
│   └── SofaVasopressorResolver.cs              # Vasopressor dose from MedicationAdministration + Redis cache
├── Services/MapCalculator.cs                   # MAP from systolic + diastolic BP
├── Elasticsearch/Documents/
│   ├── PatientEncounterDocument.cs
│   ├── ObservationDocument.cs
│   └── ClinicalAlertDocument.cs
├── Notifications/
│   └── RabbitMqTopologyProvisioner.cs          # Declares exchange, queues, DLQ bindings on startup
├── Storage/
│   └── MinioClientFactory.cs
├── DataLake/
│   ├── DataLakeOptions.cs                      # Flush thresholds and bucket settings
│   ├── DataLakeWriterService.cs                # consumer group: data-lake-writer; Kafka → Parquet → MinIO; graceful shutdown flush
│   └── ParquetFileBuilder.cs                   # Topic row models → Parquet byte arrays
├── Models/Records/
│   ├── Observation/ObservationRow.cs           # Parquet row contract for observation events
│   ├── Alert/AlertRow.cs                       # Parquet row contract for alert events
│   ├── Encounter/EncounterStatusRow.cs         # Parquet row contract for encounter status events
│   ├── Encounter/WardEncounterSummary.cs       # Denormalized row for ward encounter list
│   ├── Medication/CreateMedicationAdministrationRequest.cs
│   ├── Qsofa/QsofaCurrentResponse.cs
│   └── Sepsis/QsofaResult.cs, QsofaOutcome.cs  # qSOFA detector result and screening outcome enum
├── Data/
│   ├── AppDbContext.cs                         # EF Core context — entity configs, indexes, constraints
│   ├── Configurations/                         # IEntityTypeConfiguration per entity; ClinicalUserConfiguration, RefreshTokenConfiguration, ClinicalAuditLogConfiguration, ClinicalSiteConfiguration, WardGatewayConfiguration, QsofaEvaluationConfiguration, AlertFeedbackConfiguration, AlertQualityMetricConfiguration; ElasticsearchOptions, ElasticIndexOptions
│   └── Seed/
│       ├── DataSeeder.cs                       # Seeds patients, encounters, thresholds, observations
│       ├── GatewayRegistrySeeder.cs            # Seeds demo site (SITE-DEMO) and gateway (GW-ICU-3B) with fixed GUIDs
│       └── UserSeeder.cs                       # Seeds four demo users (nurse, physician, admin, integration)
├── Common/
│   ├── ApiResponse.cs                          # { success, statusCode, data, error } envelope
│   ├── PagedResult.cs / CursorPage.cs
│   └── Exceptions/
│       ├── NotFoundException.cs
│       ├── ConflictException.cs                # Thrown by encounter status machine
│       ├── DomainException.cs
│       └── ValidationException.cs
├── Middlewares/
│   ├── FhirApiKeyOrJwtMiddleware.cs            # Dual auth for /fhir/* routes: JWT bearer or X-Api-Key (multi-key rotation, constant-time compare) → Integration identity
│   ├── CorrelationIdMiddleware.cs
│   └── ExceptionHandlerMiddleware.cs
└── Migrations/

infra/
├── prometheus/
│   └── prometheus.yml                          # Scrape config for vigilcare_api /metrics
└── grafana/
    ├── provisioning/                           # Datasource + dashboard provider config
    └── dashboards/                             # vigilcare.json clinical dashboard, alert-quality-dashboard.json

tests/
└── VigilCareClinicalAPI.Tests/
    ├── ObservationIngestTests.cs               # Ingest happy path, critical alert creation, discharged encounter rejection, idempotency
    ├── AlertLifecycleTests.cs                  # Acknowledge, resolve, escalation guard
    ├── NotificationPipelineTests.cs            # RabbitMQ topology, DLQ routing
    ├── ReconciliationTests.cs                  # Three reconciliation checks, deduplication, RabbitMQ publish
    ├── ObservabilityPhase8Tests.cs             # /metrics families and correlation header behavior
    ├── DataLakePhase9Tests.cs                  # Kafka → MinIO Parquet flow and schema checks
    ├── ClinicalDemographicsAndObservationTests.cs  # Patient/encounter enrichment, expanded observation alerts
    ├── WarningAlertTests.cs                    # WarningEvaluator — warning created, normal/critical skipped, idempotent
    ├── OrderLifecycleTests.cs                  # Orders API — create, list, record result, illegal transition 409
    ├── ValidationTests.cs                      # FluentValidation — empty fields, threshold ordering, order description
    ├── News2CalculatorTests.cs                 # Boundary tests for all seven NEWS2 scoring tables
    ├── News2DetectorTests.cs                   # NEWS2 detector — score tiers, alerts, idempotency, incomplete set
    ├── TrendCalculatorTests.cs                 # Pure unit tests — rate-of-change, threshold direction, describe
    ├── TrendDetectorTests.cs                   # Trend detector — rapid climb, stable, idempotent, non-trend code
    ├── AlertSuppressionTests.cs                # Suppression on acknowledge, read-side skip, TTL expiry
    ├── QsofaCalculatorTests.cs                 # Boundary tests for three qSOFA criteria
    ├── QsofaDetectorTests.cs                   # qSOFA detector — two-criteria QSOFA_SCREEN alert, normalization, idempotency
    ├── SepsisBundleTests.cs                    # Bundle creation from SOFA_SEPSIS, element completion, compliance outcomes
    ├── SepsisRefactorTests.cs                  # Sepsis-3 refactor — SIRS removal, qSOFA screen workflow, SOFA bundle trigger
    ├── AlertCreationGuardTests.cs              # Guard prevents deprecated SEPSIS_WARNING creation
    ├── ClinicalRefactorEndToEndTests.cs        # End-to-end scenario replay: qSOFA screen → SOFA labs → bundle
    ├── MedicationServiceTests.cs               # Medication CRUD, discharged encounter rejection, pagination
    ├── MedicationCorrelationTests.cs           # End-to-end warning/NEWS2 annotation with medication context
    ├── MedicationValidationTests.cs            # FluentValidation 400 on invalid medication requests
    ├── EncountersListTests.cs                  # Ward encounter list filters and summary fields
    ├── QsofaCurrentTests.cs                    # qSOFA current API — Redis state, criteria breakdown
    ├── GcsScoringTests.cs                      # GCS component scoring, alerts, NEWS2/qSOFA integration paths
    ├── SofaScoringTests.cs                     # SOFA organ scores, baseline, delta alerts, carry-forward, vasopressors
    ├── BackgroundServiceTests.cs                # Outbox relay, Kafka consumer, sepsis bundle monitor, reconciliation
    ├── ConcurrencyTests.cs                      # Parallel patient MRN, sepsis bundle, observation idempotency, encounter open
    ├── GapAnalysisFixTests.cs                  # Phase 22 — GCS history, qSOFA evaluation persistence/history, encounter timeline
    ├── GatewayRegistryTests.cs                 # Gateway register, heartbeat, API key auth, department filter
    ├── OperationsApiTests.cs                   # Operations fleet listing, gateway detail, site summary
    ├── Helpers/GatewayAuthHelper.cs             # WithGatewayApiKey extension method for test clients
    ├── Alerts/
    │   ├── AlertQualityAnalyticsTests.cs       # Alert feedback submission, quality aggregation, metrics API
    │   └── ExplainableAlertsTests.cs           # Explanation JSONB, AlertResponse mapping, medication context, legacy null
    ├── Auth/
    │   └── RbacTests.cs                        # RBAC — unauthenticated 401, nurse 403 on threshold write, admin audit log creation
    └── Fhir/
        └── FhirIngestTests.cs                  # FHIR R4 patient upsert idempotency, observation LOINC mapping, unknown code 422, transaction bundle

VigilCare.ClinicalContracts/                      # Phase 20 — shared sync DTOs (no ASP.NET dependency)
├── VigilCare.ClinicalContracts.csproj            # net8.0 class library, no NuGet packages
└── Sync/
    ├── ClinicalSyncBatchRequest.cs               # Batch upload envelope (batchReference, gatewayId, siteId, items)
    ├── SyncedObservation.cs                      # Per-observation sync item with idempotency key
    ├── SyncedAlertEvent.cs                       # Client-generated alert event
    ├── SyncedAlertAcknowledgment.cs              # Alert acknowledgment from ward
    ├── SyncedAlertResolution.cs                  # Alert resolution from ward
    └── GatewayHeartbeatRequest.cs                # Status + buffer depth + timestamp

VigilCare.ClinicalContracts.Tests/                # Contracts round-trip serialization tests
└── ClinicalContractsTests.cs

VigilCare.WardGateway/                            # Phase 21 — local-first ward edge API (separate DB/Redis/RabbitMQ)
├── Dockerfile
├── Program.cs
├── Common/                                        # ApiResponse envelope, CursorPage, PagedResult, domain exceptions
├── Data/
│   ├── GatewayDbContext.cs
│   └── Configurations/                           # snake_case mappings mirroring central API patterns
├── Domain/
│   ├── Entities/                                 # ReplicaPatient, ReplicaEncounter, LocalObservation, LocalClinicalAlert, BufferedSyncItem, SyncOutboxEntry, GatewaySyncState, ReplicaAlertThreshold
│   └── Enums/                                    # AlertType, AlertSeverity, AlertStatus, EncounterStatus, EncounterType, BufferedSyncItemType, SyncOutboxStatus, ObservationSource, Department
├── Models/Records/ThresholdCacheEntry.cs
├── Models/Records/Observation/IngestObservationRequest.cs
├── Models/Records/Alert/AcknowledgeAlertRequest.cs
├── Models/Records/Encounter/                      # EncounterDetail, WardEncounterSummary
├── Models/Records/LocalIngestResult.cs
├── Models/Central/CentralSyncDtos.cs              # DTOs for central API sync responses
├── Models/Central/CentralApiJson.cs
├── Validators/IngestObservationRequestValidator.cs
├── Validators/AcknowledgeAlertRequestValidator.cs
├── Services/
│   ├── PlausibilityValidator.cs
│   ├── LocalObservationService.cs
│   ├── LocalWarningEvaluator.cs
│   ├── LocalAlertService.cs
│   ├── LocalPagingPublisher.cs
│   ├── ObservationQueryService.cs
│   ├── EncounterReadService.cs
│   └── BufferedSyncWriter.cs
├── Controllers/
│   ├── ObservationsController.cs
│   ├── AlertsController.cs
│   ├── EncountersController.cs
│   └── CentralRequiredController.cs
├── Notifications/RabbitMqTopologyProvisioner.cs
├── Middlewares/ExceptionHandlerMiddleware.cs
├── BackgroundService/
│   ├── EncounterReplicaSyncService.cs             # Syncs patient/encounter data from central API on startup
│   ├── ThresholdCacheLoader.cs                    # Pre-loads alert thresholds from central API into Redis
│   ├── CentralReachabilityService.cs              # Periodic central API health check (sets online/offline state)
│   ├── GatewayHeartbeatService.cs                 # Reports gateway status and buffer depth to central API
│   ├── SyncUploaderService.cs                     # Uploads buffered observations/alerts to central API when online
│   ├── LocalPagingWorkerService.cs
│   └── LocalEscalationWorkerService.cs
├── Configurations/                               # GatewayOptions, CentralApiOptions, RabbitMqOptions, SuppressionOptions
├── Infrastructure/                               # Health checks (Redis, RabbitMQ, encounter replica ready)
└── Migrations/                                   # InitialGatewaySchema

VigilCare.WardGateway.Tests/                       # Phase 21 — ward gateway integration tests
├── VigilCare.WardGateway.Tests.csproj
├── Fixtures/
│   ├── GatewayApiFixture.cs                       # WebApplicationFactory with Testcontainers (PostgreSQL, Redis, RabbitMQ)
│   └── GatewayIntegrationCollection.cs
├── Helpers/
│   ├── GatewayDbResetHelper.cs                    # Database reset between tests
│   └── GatewayTestSeeder.cs                       # Seeds test patients, encounters, thresholds
├── WardGatewayLocalPathTests.cs                   # Local observation ingest, warning alerts, buffered sync items
└── WardGatewayPartitionTests.cs                   # Network partition simulation — offline buffering and sync upload

VigilCare.Simulator/                              # Phase 16, 29, 34, 35 — console replay simulator (HTTP-only, no direct DB/Kafka)
├── Program.cs                                    # CLI: replay, replay-all, validate, dry-run, mimic-list, mimic-generate
├── Commands/                                     # System.CommandLine command handlers
├── Client/
│   ├── VigilCareApiClient.cs                   # Typed HTTP client for all API endpoints (incl. GCS, SOFA)
│   └── Models/AlertExplanation.cs              # Explanation DTO for poll/validation
├── Engine/ReplayEngine.cs                        # Scenario replay with speed multiplier + event logging
├── Output/SimulatorConsole.cs                    # Colored output with GCS/SOFA score + explanation display
├── Polling/ApiPoller.cs                          # Optional post-event alert/score/bundle/GCS/SOFA polling
├── Mimic/                                        # Phase 35 — MIMIC-IV scenario generator
│   ├── MimicCsvReader.cs                       # Streaming CSV parser with header-index lookup and filter predicate
│   ├── MimicItemMap.cs                         # MIMIC item ID → VigilCare observation code mappings (17 chart + 8 lab)
│   ├── MimicCareUnitMap.cs                     # ICU care unit → department + tag mapping
│   ├── MimicDataLoader.cs                      # Data access layer with streaming filters for chartevents/labevents
│   ├── MimicScenarioBuilder.cs                 # Core algorithm: BP dedup, cluster limits, offset conversion
│   ├── MimicListCommand.cs                     # CLI: mimic-list — Spectre.Console table of available stays
│   └── MimicGenerateCommand.cs                 # CLI: mimic-generate — produces scenario JSON from a stay ID
├── Scenarios/                                    # schema.json, ScenarioLoader, ScenarioValidator, ExpectedOutcomeValidator
└── Scenarios/List/                             # Twelve sample scenarios + MIMIC-generated scenarios

vigilcare-dashboard/                              # Phases 1719, 22, 23, 2728, 31, 3334 — Vue 3 ward dashboard SPA
├── src/
│   ├── api/                                      # HTTP client (auto Bearer header, 401 auto-refresh), encounters, clinical (GCS, SOFA, qSOFA history), alerts, analytics, sepsis, thresholds, users, audit, reconciliation, operations, alertQuality, normalize
│   ├── components/
│   │   ├── admin/                                # ThresholdFormModal, UserFormModal (admin CRUD modals)
│   │   ├── alerts/                               # AlertCard, AlertReasoning (structured explanation), AcknowledgeModal (role-aware), CriticalAlertBanner (browser notifications + audible tone)
│   │   ├── charts/                               # SofaHistory, GcsHistory, QsofaHistory, VitalChart with medication markers, AlertQualityChart
│   │   ├── departments/                          # DepartmentCard, AcuityBar (unit-level snapshot)
│   │   ├── feedback/                             # FeedbackButtons, FeedbackSummary
│   │   ├── layout/                               # AppShell, AppHeader (user + logout), AppSidebar (user + logout + role-aware admin), MobileNav (logout)
│   │   ├── patient/                              # GcsEntryForm, SofaScorePanel, PatientBanner, EncounterTimeline, VitalsEntryForm, VitalsPanel, AlertsList, DischargeSummaryPanel
│   │   ├── replay/                               # ReplayControls
│   │   ├── sepsis/                               # SepsisBundleTable, SepsisBundleRow, SepsisBundleCard (countdown timer)
│   │   ├── ward/                                 # WardTable (sortable headers), PatientRow, PatientCard, WardToolbar, SortableHeader, HandoffReport (SBAR + print)
│   │   └── ui/                                   # Button, Card, Badge, Skeleton, EmptyState, Modal, CollapsibleSection, SeverityBadge, DegradedModeBanner
│   ├── composables/                              # useChartData, useReplayControls, usePolling, useFeedback, useGcs, useSofa, useApiMode, useChartTheme, useFocusTrap, chartFormat, patientFormat, timelineFormat, chartMedications, wardSort, wardFilter, departmentFormat, sepsisFormat, alertAcknowledge, alertExplanation, criticalAlertDetect, useAlertNotification, useCriticalAlertPolling, handoffReport, vitalsForm, roleAccess, auditFormat, reconciliationFormat, thresholdForm, userForm
│   ├── plugins/                                  # medicationMarkerPlugin (Chart.js plugin for medication administration markers on vital charts)
│   ├── stores/                                   # Pinia — ward (sort + filter + search), alerts (banner + polling), settings (sort prefs + sound mute), feedback, scoring, auth (login + refresh token rotation + logout + expiry redirect), departments, sepsis, operationsStore, alertQuality
│   ├── views/                                    # LoginView, WardDashboard, PatientDetail, AlertCenter, FeedbackSummary, DepartmentOverviewView, SepsisBoardView, SimulationControlView, ThresholdManagementView, UserManagementView, AuditLogView, ReconciliationView, GatewayOperations, AlertQualityAnalytics
│   └── __tests__/                                # Vitest — tests (store, feedback, replay, charts, alerts, ward, GCS, SOFA, qSOFA, PatientBanner, EncounterTimeline, patientFormat, timelineFormat, chartMedications, wardSort, wardFilter, departmentFormat, sepsisFormat, alertAcknowledge, criticalAlertDetect, HandoffReport, handoffReport, VitalsEntryForm, vitalsForm, useAlertStore, useWardStore, DepartmentOverviewView, SepsisBoardView, AcknowledgeModal, CriticalAlertBanner, roleAccess, ThresholdManagementView, thresholdForm, DischargeSummaryPanel, GatewayOperations, alertQuality)
├── vite.config.js
└── README.md                                     # Dev quick start → docs/dashboard-guide.md

scripts/
├── run-api-redis-tests.sh                      # Phase 1 — patient/encounter/threshold + Redis cache
├── run-kafka-outbox-tests.sh                   # Phase 3 — outbox relay and Kafka topics
├── run-elasticsearch-analytics-tests.sh        # Phase 4 — Elasticsearch CQRS projection
├── run-sepsis-sirs-tests.sh                    # Phase 5 — legacy SIRS tests (now qSOFA-only)
├── run-notification-pipeline-tests.sh            # Phase 6 — RabbitMQ paging, DLQ, discharge summary
├── run-reconciliation-tests.sh                 # Phase 7 — reconciliation scheduler checks
├── run-phase8-verification.sh                  # Phase 8 — Prometheus metrics, alerts_unacknowledged_gauge, correlation headers
├── run-phase9-verification.sh                  # Phase 9 — data lake tests, Kafka offsets, MinIO Parquet, DuckDB schema
├── run-phase10-verification.sh                 # Phase 10 — 12 Redis thresholds, clinical enrichment, ES pipeline, integration tests
├── run-phase11-verification.sh                 # Phase 11 — warning alerts, orders API, validation, integration tests
├── run-phase12-verification.sh                 # Phase 12 — NEWS2 end-to-end pipeline, API, ES, Prometheus, integration tests
├── run-phase13-verification.sh                 # Phase 13 — trend detection, alert suppression, consumer lag, integration tests
├── run-phase14-verification.sh                 # Phase 14 — qSOFA, sepsis bundle compliance, integration tests
├── run-phase15-verification.sh                 # Phase 15 — medication administration + correlation annotations
├── run-phase25-verification.sh                 # Phase 25 — GCS scoring integration tests + manual API checks
├── run-phase26-verification.sh                 # Phase 26 — SOFA scoring integration tests + baseline/delta API checks
├── run-phase27-verification.sh                 # Phase 27 — Sepsis-3 refactor: SIRS removal, QSOFA_SCREEN, SOFA bundle trigger
├── run-phase28-verification.sh                 # Phase 28 — Frontend GCS entry + SOFA display + sepsis UI refactor
├── run-phase20-verification.sh                 # Phase 20 — Gateway registry tests + site/gateway/heartbeat curl checks
├── run-phase21-verification.sh                 # Phase 21 — Ward gateway local-first path, partition tests, sync upload
├── run-phase22-verification.sh                 # Phase 22 — Dashboard gap analysis fixes (SOFA/GCS/qSOFA history, patient banner, timeline, medication markers)
├── run-phase24-verification.sh                 # Phase 24 — Ward outage reconnect scenario (central + gateway replay, ack sync)
├── run-phase29-verification.sh                 # Phase 29 — Simulator scenario expansion + clinical validation
├── run-phase30-verification.sh                 # Phase 30 — FHIR R4 ingest integration tests + manual bundle/metadata checks
├── run-phase31-verification.sh                 # Phase 31 — RBAC integration tests + JWT login + audit log query
├── run-phase23-verification.sh                 # Phase 23 — Degraded operations visibility + gateway fleet + admin panels
├── run-phase33-verification.sh                 # Phase 33 — Alert quality analytics integration tests
├── run-phase34-verification.sh                 # Phase 34 — Explainable alerts integration tests
├── demo-network-partition.sh                   # Gateway network partition demo script
└── mint-gateway-jwt.sh                         # JWT minting helper for gateway testing

docs/
├── plans/                                      # Phase implementation and verification guides
│   ├── vigilcare-clinical-roadmap.md           # Phases 3638 implementation order
│   └── phase-*-plan.md                         # Per-phase plans (incl. phase-36/37/38 simulation)
├── clinical-testing-guide.md                   # Doctor/nurse guide — self-service Simulation sessions AD
├── dashboard-guide.md                          # VigilCare Dashboard user guide (ward, patient detail, charts)
├── dashboard-gap-analysis.md                   # Comprehensive gap analysis — P0P5 clinical usefulness assessment
├── patient-encounter-api-lifecycle.md          # Full API walkthrough: registration → active stay → discharge
├── simulator-guide.md                          # VigilCare.Simulator CLI / CI user guide (clinicians use dashboard Simulation)
├── integration/
│   └── mirth-fhir-channels.md                  # Mirth Connect HL7v2→FHIR channel mapping (ADT A01/A03/A08, ORU R01)
├── decisions/
│   ├── data-lake-design.md                     # Parquet vs JSON, partitioning, replay rationale
│   ├── sepsis-engine-design.md                 # Sepsis-3 qSOFA screening and idempotent alert design
│   ├── medication-correlation-design.md        # Drug-vital mapping and annotation rationale
│   └── clinical-refactor-sofa-gcs.md           # Interview Q&A: why SIRS→SOFA, carry-forward, GCS dependency chain
├── docker-compose-usage-and-troubleshooting.md
└── vigilcare-clinical-api-prd.md               # Product requirements and phase roadmap

Architecture Decisions

Synchronous vs Asynchronous Alert Detection — The Split

Critical threshold breaches are detected synchronously within the ingest transaction. A critical potassium of 2.1 mEq/L is immediately life-threatening. If the API returns 201 Created before generating the alert and the Kafka consumer lags by 30 seconds, a patient could deteriorate during that window. The synchronous check costs one additional Redis read per observation on the hot path — acceptable for correctness.

A warning heart rate of 95 bpm warrants attention but is not an emergency. The additional latency of Kafka consumer processing is clinically acceptable for a warning.

This is the architectural decision that separates thinking about healthcare systems from thinking about financial systems. The tradeoff is latency for correctness, and the correctness definition is clinical, not technical.

Encounter as the Aggregate Root (Not Patient)

Observations, alerts, and orders belong to an encounter, not directly to a patient. A patient's blood pressure taken during a 2022 admission belongs to that admission. This bounds queries naturally: "show me all observations for this encounter" is a bounded query. "Show me all observations ever recorded for this patient" is a cross-encounter aggregation that belongs in the data lake.

Redis for Seven Distinct Purposes

Redis serves seven independent roles with different semantics:

  1. Threshold cache: write-through invalidation on every threshold update. Staleness here has clinical consequences — a stale threshold could suppress a critical alert. TTL expiry is not sufficient; invalidation must be immediate on write.

  2. qSOFA sliding window: SET qsofa:{encounterId}:{code} EX 1800. The 30-minute TTL is a clinical parameter — a respiratory rate that was abnormal 31 minutes ago stops contributing to the qSOFA count without any cleanup job. When a value normalizes, the key is explicitly deleted rather than waiting for TTL — a systolic BP that recovers from 90 to 120 should immediately reduce the qSOFA count.

  3. NEWS2 parameter state: SET news2:{encounterId}:{code} with a 4-hour TTL. Each of the seven NEWS2 parameters is scored individually and stored in Redis; MGET across all seven keys determines completeness. An incomplete set (fewer than seven present keys) does not produce a score — expired parameters must be re-recorded before the aggregate is computed.

  4. GCS component state: SET gcs:{encounterId}:{component} tracking Eye, Verbal, and Motor components. All three must be present before a total score is computed. Completion triggers gcs.scored for SOFA CNS re-scoring.

  5. SOFA lab cache: SET sofa:{encounterId}:{code} with carry-forward semantics (configurable 24-hour TTL). Labs are classified as CURRENT (< 12h), STALE (1224h), or EXPIRED (> 24h). Stale values are still used for scoring but flagged in staleness metadata. This enables SOFA scoring on wards where labs are drawn every 612 hours, not continuously.

  6. Trend history: sliding-window observation history for rate-of-change detection. Five vital parameters tracked for velocity thresholds.

  7. Alert suppression windows: SET suppress:{encounterId}:{alertType} with a configurable TTL (default 30 min). Set on acknowledge of suppressible alerts; checked by WarningEvaluator and News2Detector before creating new warning alerts. Critical alerts are never suppressed.

Outbox Pattern

Observation and alert writes use the transactional outbox: the outbox_events row is inserted in the same transaction as the domain record. The relay publishes to Kafka asynchronously using an idempotent producer (EnableIdempotence = true) — the broker deduplicates in-flight retries using producer ID and sequence number. This prevents message loss when Kafka is temporarily unavailable and prevents phantom messages when the transaction rolls back. Events that repeatedly fail Kafka delivery (network partition, broker-level rejection) are retried up to OutboxMaxRetries (default 10) before being marked permanently failed — acting as a dead-letter mechanism that prevents a single poisoned event from blocking the entire relay. The relay uses FOR UPDATE SKIP LOCKED so multiple instances can run concurrently without contention.

Kafka Partition Key: encounterId

All events for the same encounter land on the same partition. The sepsis engine requires this: if observations from the same patient arrive on different partitions, they may be consumed out of order and simultaneous qSOFA criteria could be missed. Six partitions balance parallelism against per-encounter ordering guarantees.

Elasticsearch as a CQRS Read Projection

PostgreSQL is always the write side and the source of truth. Elasticsearch is a denormalized, queryable projection optimized for the queries clinicians actually run. The population endpoint — "how many active patients have a heart rate above 100 in the last hour" — is a numeric range aggregation across potentially millions of observation rows. Running this against PostgreSQL on the operational database would compete with ingest writes. Elasticsearch's aggregation engine is purpose-built for this pattern.

The replay: stop EsIndexerService → delete both indices → reset consumer group offset to 0 → restart → wait for rebuild → verify document count matches PostgreSQL row count. This is the proof that Elasticsearch is a projection and not a source of truth, and the clearest demonstration of why Kafka retains events after consumption.

DLQ as a Clinical Escalation Protocol

The five-minute escalation is not a retry — it is a clinical workflow. When an alert is created, the paging worker sends a page to the attending physician. If no acknowledgment arrives within five minutes, the message NACKs to alerts.paging.dlq with x-message-ttl = 300000ms. After TTL expires, the DLQ re-routes to alerts.escalation.queue and the on-call backup is paged. During graceful shutdown, cancellation of the in-flight wait loop is treated as non-failure and the message is NACKed with requeue=true, preventing false escalation during deploy/restart windows. This pattern has no equivalent in Kafka — Kafka has no concept of per-message TTL or conditional re-routing based on consumer acknowledgment.

Why Not a Time-Series Database for Observations?

A medium hospital with 200 concurrent inpatients at five observations per patient per minute produces approximately 17 observations per second at steady state. PostgreSQL with the composite index (encounter_id, observation_code, recorded_at DESC) handles this volume with headroom. TimescaleDB would be the correct next step at 10,000+ observations/second — it is PostgreSQL with automatic time partitioning, meaning the query layer would not change. The Parquet data lake handles the analytics workload that would otherwise stress the operational database over a 10-year horizon.

Two-Tier Sepsis Detection (Sepsis-3: qSOFA Screen → SOFA Confirmation)

The sepsis pathway follows the Sepsis-3 consensus (2016), replacing the older SIRS-based approach:

  1. Screening (qSOFA): SepsisEngineService evaluates three bedside criteria (respiratory rate ≥ 22, systolic BP ≤ 100, altered mentation via GCS < 15). When ≥ 2 are active, a QSOFA_SCREEN (WARNING-level) alert is created, recommending SOFA lab orders.

  2. Confirmation (SOFA): SofaScoringService scores six organ systems from labs and vitals. When SOFA delta ≥ 2 from baseline, a SOFA_SEPSIS (CRITICAL) alert fires and triggers the sepsis bundle via SepsisAlertHandler.

This two-tier design prevents false-positive bundle activations — SIRS criteria (temperature, heart rate, respiratory rate, WBC) were too non-specific, triggering bundles for post-surgical inflammation, anxiety, and viral infections. SOFA measures actual organ dysfunction, making the bundle trigger clinically meaningful. The legacy SEPSIS_WARNING and QSOFA_WARNING alert types are retained (marked [Obsolete]) for historical queries but can no longer be created.


Getting Started

Prerequisites

  • .NET 8 SDK
  • Docker and Docker Compose

Start Infrastructure

Central stack only (default — no Compose profile):

docker compose up -d

Ward gateway stack (separate PostgreSQL, Redis, RabbitMQ, and gateway API on port 5081):

docker compose --profile ward-gateway up -d

Everything (central + ward):

docker compose --profile full up -d

Ward services use Compose profiles and do not start with a plain docker compose up. See docs/docker-compose-usage-and-troubleshooting.md §10.

All services join the vigilcare_net bridge network so containers can reach each other by service name (e.g. Grafana → http://prometheus:9090). Connection strings in appsettings.json use host ports when running dotnet run on your machine.

Service Host Port Notes
PostgreSQL 16 5436 Database: vigilcare, user: postgres, password: password
Redis 7 6382 No auth
Seq 5345 UI at http://localhost:5345 — login: admin / admin
Kafka 3.7 9092 KRaft mode, no Zookeeper
Elasticsearch 8.13 9200 Security disabled for development
RabbitMQ 3.13 5674 (AMQP), 15674 (UI) login: guest / guest
MinIO 9005 (S3 API), 9006 (console) login: minioadmin / minioadmin
Prometheus 2.52 9101 UI at http://localhost:9101 — scrapes GET /metrics on the API
Grafana 10.4 3101 UI at http://localhost:3101 — login: admin / admin

Ward gateway stack (--profile ward-gateway or full):

Service Host Port Notes
Ward PostgreSQL 16 5437 Database: vigilcare_ward, user: postgres, password: password
Ward Redis 7 6383 No auth
Ward RabbitMQ 3.13 5675 (AMQP), 15675 (UI) login: guest / guest
Ward Gateway API 5081 VigilCare.WardGateway — local-first clinical path (Phase 21)

Seq first-run: SEQ_FIRSTRUN_ADMINPASSWORD=admin is set in docker-compose.yml. This password is only applied on the very first container start (when the /data volume is empty). After initialization, the password is stored in the volume and this env var is ignored.

Docker notes for Linux

Prometheus scrapes the API using host.docker.internal:5270. On Linux, two things are required:

  1. In docker-compose.yml under prometheus:
    extra_hosts:
      - "host.docker.internal:host-gateway"
    
  2. Run the API bound to all interfaces (not only loopback), so containers can reach it:
    • Use http://0.0.0.0:5270 (or ASPNETCORE_URLS=http://0.0.0.0:5270)

Without this, Prometheus may show target errors like:

  • lookup host.docker.internal ... no such host (DNS mapping missing), or
  • dial tcp 172.17.0.1:5270: connect: connection refused (API bound only to 127.0.0.1)

For full Docker troubleshooting and recovery steps, see:

  • docs/docker-compose-usage-and-troubleshooting.md

Simulation mode (default off)

In-app scenario replay for clinical testing is controlled by the Simulation section in appsettings.json (or environment variables). Simulation:Enabled defaults to false. When enabled, the API registers session/scenario/purge endpoints and a loopback runner; clinicians use the dashboard Simulation page (session presets AD, individual scenarios, speed control, ward reset — see docs/clinical-testing-guide.md). Never enable simulation against a database with real patient data — purge and replay only touch rows marked IsSimulated, but the feature is intended for evaluation environments only.

Key settings: Simulation:ScenarioDirectory (flat folder of scenario JSON; sessions.json may sit in that directory or its parent), Simulation:MaxConcurrentRuns (default 8 — enough for Session Cs seven concurrent patients), Simulation:LoopbackBaseUrl, and the simulation.runner service account password.

Install and Run

cd VigilCareClinicalAPI
dotnet restore
dotnet run

On startup the application:

  1. Runs EF Core migrations
  2. Seeds two patients (with blood type, allergies, emergency contact), one active inpatient encounter each (with room/bed and admission reason), twelve alert thresholds, sample observations, and four clinical users (nurse, physician, admin, integration)
  3. Pre-loads all thresholds into Redis
  4. Provisions Kafka topics and Elasticsearch indices
  5. Declares the RabbitMQ exchange and queue topology
  6. Starts all background consumers (outbox relay, ES indexer, sepsis engine with qSOFA screening, warning evaluator, NEWS2 scoring, GCS scoring, SOFA scoring, trend analyzer, notification workers, data lake writer, reconciliation scheduler, sepsis bundle monitor)
  7. Starts Prometheus metric collectors (unacknowledged alerts, outbox pending, Kafka consumer lag, ward gateway fleet health)

Swagger UI is available at http://localhost:5270/swagger in Development (API binds to 0.0.0.0:5270 per launchSettings.json).

Health checks (anonymous, no JWT required):

  • GET /health/live — liveness probe (always 200 if process is running)
  • GET /health/ready — readiness probe (checks PostgreSQL, Redis, Kafka, RabbitMQ, Elasticsearch)

Run the Simulator (CLI / CI)

Clinicians: prefer the dashboard Simulation page (Sessions / Scenarios / Reset ward). The CLI is for developers, CI, validate / dry-run / replay-all / mimic-generate, and scripts/run-phase*-verification.sh. See docs/simulator-guide.md §12.

With the API running and (for in-app use) Simulation:Enabled=true, replay a scenario from the repository root via CLI:

dotnet run --project VigilCare.Simulator -- replay \
  VigilCare.Simulator/Scenarios/List/uti-sepsis-elderly-01.json \
  --speed 60 --poll

Other commands: validate <file>, dry-run <file>, replay-all <directory>. See docs/simulator-guide.md for the full user guide.

MIMIC-IV real patient data (Phase 35): generate and replay scenarios from de-identified ICU records:

# List 140 available ICU stays across 100 patients
dotnet run --project VigilCare.Simulator -- mimic-list docs/MIMIC-IV/

# Generate a 24-hour scenario from a CVICU patient
dotnet run --project VigilCare.Simulator -- mimic-generate docs/MIMIC-IV/ \
  --stay-id 32604416 --max-hours 24 --validate

# Replay the generated scenario
dotnet run --project VigilCare.Simulator -- replay \
  VigilCare.Simulator/Scenarios/List/mimic-s32604416.json --speed 0 --poll

Ward outage reconnect (Phase 24): scenario ward-outage-reconnect-01.json exercises critical hyperkalemia alerting and nurse acknowledgment during a central outage, then sync back to central when connectivity returns. Manual procedure in docs/simulator-guide.md §10; automated end-to-end check:

./scripts/run-phase24-verification.sh

Requires Docker Compose (central API, ward gateway stack) and a running central API on http://localhost:5270. Use SKIP_DOCKER=1 when the stack is already up. Notes from verification runs: docs/resilience/phase-24-verification-notes.md.

Run the Dashboard

With the API running, start the Vue frontend:

cd vigilcare-dashboard
npm install
npm run dev

Open http://localhost:5173 — log in with a demo account (e.g. nurse.demo / DemoNurse1!). Virtual Ward lists active patients sorted by NEWS2 score. Click a row for patient detail (vitals, alerts, charts, replay scrubbing, alert reasoning). Use Alert Center for hospital-wide triage. After reviewing alerts, rate them with the six feedback buttons and export results from Feedback Summary or Alert Quality.

With Simulation:Enabled=true, open Simulation in the sidebar: start a Sessions preset (AD) or an individual scenario, watch runs on the progress panel, then Reset ward (type RESET) between testers. See docs/clinical-testing-guide.md.

Run dashboard tests with cd vigilcare-dashboard && npm test. See docs/dashboard-guide.md for technical documentation.

Run Tests

dotnet test

Integration tests use WebApplicationFactory with a Testing environment and Testcontainers where needed (PostgreSQL, Redis, Kafka, RabbitMQ, Elasticsearch, MinIO). No manual infrastructure setup is required for dotnet test.

Test class Phase Coverage
ObservationIngestTests 2 Ingest happy path, critical alert creation, discharged encounter rejection, idempotency
AlertLifecycleTests 2 Acknowledge, resolve, escalation guard
SepsisRefactorTests 27 Sepsis-3 refactor — SIRS removed, qSOFA creates QSOFA_SCREEN, SOFA delta triggers bundle
AlertCreationGuardTests 27 Guard prevents creation of deprecated SEPSIS_WARNING alerts
ClinicalRefactorEndToEndTests 29 End-to-end scenario replay: qSOFA screen → SOFA labs → SOFA_SEPSIS → bundle
NotificationPipelineTests 6 RabbitMQ topology, DLQ routing, paging
ReconciliationTests 7 Three reconciliation checks, deduplication, RabbitMQ publish
ObservabilityPhase8Tests 8 All ten /metrics families, correlation headers, ingest counter increment
DataLakePhase9Tests 9 Kafka → MinIO Parquet flow and schema checks
ClinicalDemographicsAndObservationTests 10 Patient clinical fields, encounter enrichment, expanded observation codes, critical glucose alert
WarningAlertTests 11 WarningEvaluator — warning HR alert, normal/critical skipped, duplicate idempotent
OrderLifecycleTests 11 Orders API create, list, record result, cancel-resulted 409
ValidationTests 11 FluentValidation 400 on empty first name, invalid threshold order, empty order description
News2CalculatorTests 12 Boundary tests for all seven NEWS2 scoring tables and risk-level determination
News2DetectorTests 12 NEWS2 detector — score tiers, alert creation, incomplete parameters, idempotency
TrendCalculatorTests 13 Pure unit tests — delta/time rate, SPO2/BP decline direction, describe formatting
TrendDetectorTests 13 Trend detector — rapid HR climb alert, stable high HR, idempotency, non-trend code
AlertSuppressionTests 13 Acknowledge sets Redis key, suppressed warning skipped, critical/NEWS2 emergency never suppressed, TTL expiry
QsofaCalculatorTests 14 Boundary tests for three qSOFA criteria (RESP_RATE, SYSTOLIC_BP, AVPU)
QsofaDetectorTests 14 qSOFA detector — two-criteria QSOFA_SCREEN alert, normalization key delete, idempotent duplicate, non-qSOFA code ignored
SepsisBundleTests 14 Bundle creation from SOFA_SEPSIS, four auto-orders, element completion, compliant/non-compliant outcomes, monitor marks overdue bundles, idempotency
MedicationServiceTests 15 Medication create/list on active encounter, discharged encounter 409, pagination, since filter
MedicationCorrelationTests 15 Warning and NEWS2 alert details annotated when correlated drug administered
MedicationValidationTests 15 FluentValidation 400 on empty drug name, zero dose, future administeredAt
EncountersListTests Ward encounter list — status/department filters, summary fields
QsofaCurrentTests GET /qsofa/current — criteria count and breakdown from Redis
GcsScoringTests 25 GCS component scoring, classification, alerts, CNS integration with SOFA
SofaScoringTests 26 SOFA organ scores, baseline eligibility, delta alerts, carry-forward, vasopressors
OperationsApiTests 23 Operations fleet listing, gateway detail, site gateway summary
AlertQualityAnalyticsTests 33 Alert feedback submission, quality aggregation, metrics API
GatewayRegistryTests 20 Gateway register, heartbeat status/buffer, API key auth 401, degraded status, department filter
ClinicalContractsTests 20 ClinicalSyncBatchRequest JSON round-trip serialization
FhirIngestTests 30 FHIR R4 patient upsert idempotency, LOINC observation mapping, unknown code 422, transaction bundle
RbacTests 31 Unauthenticated 401, nurse 403 on threshold write, admin threshold update with audit log creation, alert acknowledge uses authenticated user
BackgroundServiceTests Outbox relay, Kafka consumer, sepsis bundle monitor, reconciliation
ConcurrencyTests Parallel patient registration unique MRNs, parallel sepsis alerts single bundle, parallel observation idempotency, parallel encounter open duplicate rejection
GapAnalysisFixTests 22 GCS history API, qSOFA evaluation persistence and history API, encounter timeline with patient data
WardGatewayLocalPathTests 21 Local observation ingest, critical/warning alert creation, buffered sync item generation
WardGatewayPartitionTests 21 Network partition simulation — offline buffering, sync upload to central API, encounter replica sync

Verification Scripts

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

./scripts/run-phase22-verification.sh  # Dashboard gap analysis fixes — SOFA/GCS/qSOFA history, patient banner, timeline
./scripts/run-phase21-verification.sh  # Ward gateway local-first path, partition tests, sync upload verification
./scripts/run-phase24-verification.sh  # Ward outage reconnect — gateway replay, local ack, central sync after restart
./scripts/run-phase20-verification.sh  # Gateway registry tests, site/gateway/heartbeat API verification
./scripts/run-phase8-verification.sh   # Prometheus target UP, ten metrics, alerts_unacknowledged_gauge live update
./scripts/run-phase9-verification.sh   # DataLakePhase9Tests, Kafka consumer group, MinIO Parquet, DuckDB schema
./scripts/run-phase10-verification.sh  # 12 Redis thresholds, clinical enrichment, ES pipeline, Phase 10 integration tests
./scripts/run-phase11-verification.sh  # Warning alert pipeline, orders API, FluentValidation, Phase 11 integration tests
./scripts/run-phase12-verification.sh  # NEWS2 end-to-end pipeline, API, Elasticsearch, Prometheus, Phase 12 integration tests
./scripts/run-phase13-verification.sh  # Trend detection, alert suppression, consumer lag, Phase 13 integration tests
./scripts/run-phase14-verification.sh  # qSOFA, sepsis bundle compliance, Phase 14 integration tests
./scripts/run-phase15-verification.sh  # Medication administration + correlation annotation pipeline
./scripts/run-phase27-verification.sh  # Sepsis-3 refactor: SIRS removal, QSOFA_SCREEN, SOFA bundle trigger
./scripts/run-phase30-verification.sh  # FHIR R4 ingest integration tests + manual bundle/metadata checks
./scripts/run-phase31-verification.sh  # RBAC integration tests + JWT login + audit log query
./scripts/run-phase23-verification.sh  # Degraded operations visibility + gateway fleet + admin panels
./scripts/run-phase33-verification.sh  # Alert quality analytics integration tests
./scripts/run-phase34-verification.sh  # Explainable alerts integration tests

Phase 25 — GCS scoring (requires running API + Docker Compose; set an active encounter UUID):

export ENCOUNTER_ID=$(docker compose exec -T postgres psql -U postgres -d vigilcare -t -A \
  -c "SELECT id FROM encounters WHERE status = 'ACTIVE' LIMIT 1;")
./scripts/run-phase25-verification.sh

Phase 26 — SOFA scoring (same prerequisites; script polls for async Kafka scoring):

export ENCOUNTER_ID=$(docker compose exec -T postgres psql -U postgres -d vigilcare -t -A \
  -c "SELECT id FROM encounters WHERE status = 'ACTIVE' LIMIT 1;")
./scripts/run-phase26-verification.sh

Phase 13 unit/integration tests only:

dotnet test --filter "FullyQualifiedName~Trend|FullyQualifiedName~Suppression"

Phase 15 unit/integration tests only:

dotnet test --filter "FullyQualifiedName~Medication"

Phase 21 ward gateway tests only:

dotnet test --filter "FullyQualifiedName~WardGateway"

Phase 20 gateway tests only:

dotnet test --filter "FullyQualifiedName~GatewayRegistry|FullyQualifiedName~ClinicalContracts"

Phase 31 RBAC tests only:

dotnet test --filter "FullyQualifiedName~Rbac"

Phase 22 dashboard gap analysis tests only:

dotnet test --filter "FullyQualifiedName~GapAnalysisFix"

Phase 23 operations tests only:

dotnet test --filter "FullyQualifiedName~OperationsApi"

Phase 33 alert quality analytics tests only:

dotnet test --filter "FullyQualifiedName~AlertQuality"

Phase 34 explainable alerts tests only:

dotnet test --filter "FullyQualifiedName~ExplainableAlerts"

Per-phase test runners (subset of dotnet test):

./scripts/run-api-redis-tests.sh
./scripts/run-kafka-outbox-tests.sh
./scripts/run-elasticsearch-analytics-tests.sh
./scripts/run-sepsis-sirs-tests.sh
./scripts/run-notification-pipeline-tests.sh
./scripts/run-reconciliation-tests.sh

Phase 9 optional tools (install without sudo):

# MinIO client — object listing and download
mkdir -p ~/.local/bin
curl -fsSL https://dl.min.io/client/mc/release/linux-amd64/mc -o ~/.local/bin/mc
chmod +x ~/.local/bin/mc

# DuckDB — query Parquet files locally
curl https://install.duckdb.org | sh
export PATH="$HOME/.duckdb/cli/latest:$HOME/.local/bin:$PATH"

See docs/plans/phase-8-plan.md through docs/plans/phase-12-plan.md for manual Grafana, Seq, Kafka replay, and DuckDB query examples.


Prometheus Metrics

GET /metrics exposes application metric families registered in ClinicalMetrics. Three background collectors poll PostgreSQL and Kafka every 30 seconds; counters and histograms are updated inline during request handling and background processing.

Metric Type Labels Source
observations_ingested_total Counter observation_code, source ObservationService on each committed observation
observation_ingest_duration_seconds Histogram ObservationService — full ingest transaction to COMMIT
clinical_alerts_total Counter alert_type, severity ObservationService, QsofaDetector, News2Detector, TrendDetector, SofaDetector, WarningEvaluator
qsofa_detections_total Counter QsofaDetector — only on successful idempotent QSOFA_SCREEN insert
sepsis_bundle_compliance_total Counter status SepsisBundleService — on bundle completion (COMPLIANT, NON_COMPLIANT)
news2_scores_total Counter risk_level News2Detector — on each persisted score (LOW, MEDIUM, HIGH, …)
news2_scoring_duration_seconds Histogram News2Detector — Redis update through score persistence
gcs_scores_total Counter classification GcsDetector — on each persisted score (MILD, MODERATE, SEVERE)
sofa_scores_total Counter has_delta_alert SofaDetector — on each persisted score (true / false)
sofa_scoring_duration_seconds Histogram SofaDetector — full SOFA compose + persist
trend_alerts_total Counter observation_code TrendDetector — on each RAPID_DETERIORATION alert created
trend_analysis_duration_seconds Histogram TrendDetector — per-observation trend evaluation
alert_suppressions_total Counter alert_type AlertSuppressionService — on each suppression window set after acknowledge
escalations_total Counter EscalationWorkerService on DLQ escalation
alerts_unacknowledged_gauge Gauge AlertsUnacknowledgedCollector — open CRITICAL alerts older than 5 minutes
outbox_pending_events Gauge OutboxPendingCollector — unprocessed outbox rows
kafka_consumer_lag Gauge consumer_group KafkaConsumerLagCollectores-indexer, sepsis-engine, notification-publisher, data-lake-writer
fhir_ingest_total Counter resource_type, outcome FhirIngestController — per resource type (Patient, Encounter, Observation, MedicationAdministration, Bundle) with success / error outcome
fhir_read_total Counter resource_type, interaction, outcome FhirReadController — per resource type (Patient, Encounter) with interaction (read, search) and success outcome
authorization_failures_total Counter permission, role PermissionAuthorizationHandler — authorization denials by required permission and user role
fhir_mapping_errors_total Counter resource_type FhirExceptionFilter — mapping/validation failures by resource type
ward_gateways_offline_gauge Gauge site_code WardGatewayMetricsCollector — count of gateways with status OFFLINE or DEGRADED per site
ward_gateway_buffer_depth Gauge gateway_code, department WardGatewayMetricsCollector — reported unsynced event count per gateway
kafka_poison_pills_skipped_total Counter consumer_group, topic PoisonPillGuard — messages skipped as permanently un-processable (malformed JSON, format errors, or transient failures exceeding MaxPoisonRetries)
alert_quality_useful_rate Gauge alert_type AlertQualityAggregatorService — proportion of feedback rated "useful" per alert type
alert_quality_false_positive_rate Gauge alert_type AlertQualityAggregatorService — proportion of feedback rated "false positive" per alert type
alert_feedback_total Counter feedback_type AlertService — feedback submissions by type

Prometheus scrapes the API via infra/prometheus/prometheus.yml (job: vigilcare_apihost.docker.internal:5270). Grafana loads the clinical dashboard from infra/grafana/dashboards/vigilcare.json.


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": 422,
  "data": null,
  "error": {
    "message": "Observation value exceeds plausible range for this code.",
    "code": "OBSERVATION_OUT_OF_PLAUSIBLE_RANGE"
  }
}

Patients

Method Path Description
POST /patients Register a patient; generates MRN
GET /patients Paginated list; optional q search by name (ILIKE) or MRN (exact)
GET /patients/{id} Patient detail with active encounter summary

POST body:

Field Type Required Description
firstName string yes
lastName string yes
dateOfBirth date yes
gender string yes
bloodType string no Clinical notation: A+, O-, AB-, etc.
allergies string no Free-text allergy list
emergencyContactName string no
emergencyContactPhone string no

Encounters

Method Path Description
GET /encounters Paginated ward list with clinical summaries; optional status, department filters
POST /patients/{id}/encounters Open an encounter
GET /encounters/{id} Encounter detail with recent observations and open alerts
PATCH /encounters/{id}/status Advance encounter status
GET /encounters/{id}/timeline Merged chronological view: status changes, observations, alerts
GET /encounters/{id}/discharge-summary Discharge summary info (status, availability)
GET /encounters/{id}/discharge-summary/content Download discharge summary PDF from MinIO
GET /encounters/{id}/qsofa/current Current qSOFA active criteria count (03) from Redis
GET /encounters/{id}/qsofa/history Cursor-paginated qSOFA evaluation history

GET /encounters query params: status (DB literal, e.g. ACTIVE), department (DB literal, e.g. ICU), page, pageSize

Ward summary fields: encounterId, patientId, mrn, firstName, lastName, roomBed, department, status, news2Score, news2RiskLevel, qsofaScore, sepsisActive, sepsisBundleStatus, openAlertCount, sofaScore, sofaDelta, gcsScore, gcsClassification, lastObservationAt, attendingPhysician, admittedAt

Encounter status machine:

scheduled → active → discharged
          → cancelled

PATCH /encounters/{id}/status returns 409 on illegal transitions.

POST body:

Field Type Required Description
encounterType string yes INPATIENT, OUTPATIENT, EMERGENCY
department string yes
attendingPhysician string yes
roomBed string no Ward and bed assignment (e.g. ICU-1A)
admissionReason string no Clinical reason for admission

PATCH /encounters/{id}/status body — optional dischargeDiagnosis when transitioning to DISCHARGED.

Alert Thresholds

Method Path Description
POST /alert-thresholds Create a threshold
GET /alert-thresholds List all thresholds (paginated)
GET /alert-thresholds/{id} Get a threshold by ID
PUT /alert-thresholds/{id} Update a threshold; invalidates Redis cache
DELETE /alert-thresholds/{id} Delete a threshold; invalidates Redis cache; audit logged

Body:

Field Type Required Description
observationCode string yes e.g. HEART_RATE, SYSTOLIC_BP, AVPU, GLUCOSE_MG_DL
displayName string yes Human-readable label
unit string yes e.g. bpm, °C, mEq/L
criticalLow decimal no
warningLow decimal no
warningHigh decimal no
criticalHigh decimal no

Seeded thresholds (12 codes):

Code Display Unit Critical Low Warning Low Warning High Critical High
HEART_RATE Heart Rate bpm 30 50 100 150
TEMP_C Temperature °C 35.0 36.0 38.3 40.0
POTASSIUM_MEQ_L Serum Potassium mEq/L 2.5 3.5 5.0 6.5
SPO2 Oxygen Saturation % 88 92
RESP_RATE Respiratory Rate breaths/min 12 20 30
WBC_K_UL White Blood Cell Count k/µL 2.0 4.0 12.0 20.0
SYSTOLIC_BP Systolic Blood Pressure mmHg 70 90 160 180
DIASTOLIC_BP Diastolic Blood Pressure mmHg 40 60 90 110
LACTATE_MMOL_L Serum Lactate mmol/L 2.0 4.0
AVPU AVPU Consciousness score 2
SUPPLEMENTAL_O2 Supplemental Oxygen flag
GLUCOSE_MG_DL Blood Glucose mg/dL 40 70 180 400

Observations

Method Path Description
POST /encounters/{id}/observations Ingest one or more observations (max 10 per call)
GET /encounters/{id}/observations Cursor-paginated observation history

POST body:

Field Type Required Description
observations array yes One to ten observation objects

Observation object:

Field Type Required Description
observationCode string yes Must match a configured alert threshold
value decimal yes Numeric measurement
unit string yes Unit of measure
source string no DEVICE (default), MANUAL, LAB
recordedAt DateTimeOffset yes When the measurement was taken

Idempotency: Pass an Idempotency-Key header. Same key → original 201 response, no duplicate row.

Ingest transaction sequence:

  1. Validate encounter is active
  2. Check idempotency key
  3. Validate observation value within plausible range
  4. Insert observation row
  5. Load alert threshold from Redis cache (→ PostgreSQL on miss)
  6. If value breaches CRITICAL threshold: insert clinical_alert + outbox_event (topic: alert.generated)
  7. Insert outbox_event (topic: observation.recorded)
  8. COMMIT

Status codes:

Code Meaning
201 Observation(s) recorded
200 Idempotency-Key matched existing observation
404 Encounter not found
409 Encounter is not active (discharged or cancelled)
422 Value outside plausible range or body invalid

GET query params:

Param Description
code Filter by observation code
from Inclusive start (DateTimeOffset)
to Inclusive end (DateTimeOffset)
limit Page size (default 20)
cursor Opaque cursor from previous response for next page

Uses cursor pagination on (recorded_at DESC, id DESC) — offset pagination would shift results as new observations arrive in a continuously growing table.

Clinical Alerts

Method Path Description
GET /encounters/{id}/alerts Paginated alert list for an encounter
GET /alerts Global alert list; optional status, severity, department filter
GET /alerts/{id} Alert detail (AlertResponse with optional explanation)
POST /alerts/{id}/acknowledge Acknowledge with clinician ID and optional note; returns AlertResponse
POST /alerts/{id}/resolve Resolve (must be acknowledged first); returns AlertResponse
POST /alerts/{id}/feedback Submit clinician feedback (one per user per alert); requires alerts:feedback

Alert response shape: list, get, acknowledge, and resolve endpoints return AlertResponse — alert fields plus optional explanation (scoreContributors, trend, medicationContext, narrativeSummary). Omitted on legacy and threshold-only alerts.

Alert lifecycle:

open → acknowledged → resolved
     → escalated (RabbitMQ DLQ after 5 min unacknowledged)

POST /alerts/{id}/acknowledge body:

Field Type Required Description
clinicianId string yes Clinician identifier
note string no Optional acknowledgment note

Orders

Method Path Description
POST /encounters/{id}/orders Create a clinical order for an active encounter
GET /encounters/{id}/orders List orders for an encounter; optional status, page, pageSize
GET /orders/{id} Order detail with encounter
PATCH /orders/{id}/status Transition order status
PATCH /orders/{id}/result Record a result; transitions to Resulted

Order status machine:

pending → in_progress → resulted
        → cancelled

PATCH /orders/{id}/status and PATCH /orders/{id}/result return 409 on illegal transitions (e.g. cancelling a resulted order).

POST body:

Field Type Required Description
orderType string yes Lab, Imaging, Medication, Procedure
description string yes Order description
orderedBy string yes Ordering clinician

PATCH /orders/{id}/result body:

Field Type Required Description
resultSummary string no Free-text result summary

Analytics (Elasticsearch)

Method Path Description
GET /analytics/patients Patient/encounter search across MRN, name, department
GET /analytics/observations/trend Time-series aggregation (hourly avg/min/max) for a specific observation code per encounter
GET /analytics/alerts/summary Alert volume by department and severity over a time window
GET /analytics/population Count of patients with a value above or below a threshold in a time window

GET /analytics/patients query params: q (free text), department, status

GET /analytics/observations/trend query params: encounterId (required), code (required), from, to

GET /analytics/alerts/summary query params: severity, from, to, department

GET /analytics/population query params: code (required), threshold (required), from, to

The population query uses Elasticsearch's numeric range aggregation engine — no full-text search. Running this against PostgreSQL on the operational database would compete with ingest writes under load.

NEWS2 (National Early Warning Score 2)

Method Path Description
GET /encounters/{id}/news2/current Latest NEWS2 score for an encounter (404 if none computed)
GET /encounters/{id}/news2/history Cursor-paginated score history

GET /news2/current response includes totalScore, riskLevel (LOW, LOW_MEDIUM, MEDIUM, HIGH), all seven component scores (respRateScore, spo2Score, …), hasSingleParamThree, and calculatedAt.

GET /news2/history query params: limit (default 20), cursor (opaque token from previous response).

Scores are computed asynchronously by News2ScoringService after observations are ingested — allow a few seconds for the Kafka consumer to process all seven parameters before querying.

GCS (Glasgow Coma Scale)

Method Path Description
GET /encounters/{id}/gcs Latest GCS score for an encounter (null data if none computed)
GET /encounters/{id}/gcs/history Cursor-paginated GCS score history

GET /gcs response includes eyeScore, verbalScore, motorScore, totalScore (315), classification (MILD, MODERATE, SEVERE), and calculatedAt.

GET /gcs/history query params: limit (default 20), cursor (opaque token from previous response).

All three components (GCS_EYE, GCS_VERBAL, GCS_MOTOR) must be recorded before a score is computed. Scores are asynchronous via GcsScoringService. A completed GCS score publishes gcs.scored to Kafka (via outbox) for SOFA CNS re-scoring.

SOFA (Sequential Organ Failure Assessment)

Method Path Description
GET /encounters/{id}/sofa Latest SOFA score for an encounter (404 if none computed)
GET /encounters/{id}/sofa/history Cursor-paginated score history

Response includes totalScore, six component scores (respiratoryScorerenalScore), isBaseline, deltaFromBaseline, optional staleness metadata, and calculatedAt.

SOFA scores are computed asynchronously by SofaScoringService from SOFA-related observation codes (PAO2_MMHG, FIO2_PCT, PLATELET_K_UL, BILIRUBIN_MG_DL, CREATININE_MG_DL, URINE_OUTPUT_ML_H, vitals, SPO2, vasopressors) and from gcs.scored events (CNS organ system). Baseline is established when ≥ 4 of 6 organ systems have available data. Delta ≥ 2 from baseline creates a SOFA_SEPSIS alert; delta = 1 creates SOFA_WARNING. Poll /sofa/history to confirm baseline — the latest score may have isBaseline: false after subsequent observations.

Sepsis Bundles

Method Path Description
GET /sepsis-bundles Paginated list of bundles hospital-wide; optional status filter (IN_PROGRESS, COMPLIANT, NON_COMPLIANT); returns SepsisBundleSummary with patient demographics, elements, and deadlines
GET /encounters/{id}/sepsis-bundle/current Current (most recent) sepsis bundle for an encounter (404 if none)
GET /sepsis-bundles/{id} Bundle detail with all elements and linked orders

GET /sepsis-bundle/current response includes encounterId, triggeringAlertId, triggeringAlertType (SOFA_SEPSIS), recognizedAt, deadlineAt, complianceStatus (IN_PROGRESS, COMPLIANT, NON_COMPLIANT), completedAt, and elements[] — each with elementType, status, orderId, and completedAt.

Bundles are created automatically by SepsisAlertHandler when a SOFA_SEPSIS alert fires (delta ≥ 2 from baseline). Four clinical orders (blood cultures, serum lactate, broad-spectrum antibiotics, IV fluid resuscitation) are auto-created with orderedBy: sepsis-bundle-engine. As orders are resulted via PATCH /orders/{id}/result, the corresponding bundle element is marked complete. When all four elements are done, the bundle transitions to COMPLIANT (within the 1-hour deadline) or NON_COMPLIANT.

Medications

Method Path Description
POST /encounters/{id}/medications Record a medication administration for an active encounter
GET /encounters/{id}/medications List administrations; optional since ISO 8601 filter; paginated
GET /medications/{id} Medication administration detail

POST body:

Field Type Required Description
drugName string yes Drug name (case-insensitive for correlation lookups)
dose decimal yes Must be > 0
doseUnit string yes e.g. mg, g, mcg
route string yes e.g. PO, IV
administeredAt DateTimeOffset no Defaults to server time if omitted
administeredBy string yes Clinician or nurse identifier

When a correlated drug was given within the MedicationCorrelation.CorrelationWindowMinutes window (default 90), subsequent warning and NEWS2 alerts for affected vitals include an annotation in details — e.g. — note: metoprolol 25mg (PO) administered 45 min ago. See docs/decisions/medication-correlation-design.md.

Sites

Method Path Auth Description
POST /sites JWT + users:admin Create a clinical site
GET /sites JWT + users:admin List all sites
GET /sites/{siteId} JWT + users:admin Site detail

POST body:

Field Type Required Description
siteCode string yes Uppercase alphanumeric with hyphens (max 32)
name string yes Hospital/site name (max 200)
address string no Site address (max 500)

Gateways

Method Path Auth Description
POST /sites/{siteId}/gateways JWT + users:admin Register a gateway under a site
GET /sites/{siteId}/gateways JWT + users:admin List gateways; optional ?department= filter
GET /gateways/{gatewayId} JWT + users:admin Gateway detail
PATCH /gateways/{gatewayId}/heartbeat Gateway API key Update status and buffer depth

POST /sites/{siteId}/gateways body:

Field Type Required Description
gatewayCode string yes Unique per site (max 64)
department string yes Department assignment (max 100)

PATCH /gateways/{gatewayId}/heartbeat body:

Field Type Required Description
status string yes ONLINE, DEGRADED, or OFFLINE
bufferDepth int yes Unsynced event count (≥ 0)
reportedAtUtc DateTimeOffset yes Gateway-reported timestamp

Gateway API key auth: Set X-Api-Key header (configured in ApiKey:Gateway) and X-Gateway-Id header. The handler validates the key with constant-time comparison and confirms the gateway ID in the URL matches the header.

Authentication

Method Path Auth Description
POST /auth/login Anonymous Authenticate with username/password; returns access + refresh tokens
POST /auth/refresh Anonymous Exchange a valid refresh token for a new access + refresh token pair
POST /auth/logout JWT Revoke the refresh token and end the session
GET /auth/me JWT Returns the authenticated user's profile (user ID, username, display name, role)

POST /auth/login body:

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

Login response:

Field Type Description
accessToken string JWT bearer token (default 15 min)
refreshToken string Opaque refresh token (default 7 days)
expiresAt DateTimeOffset Access token expiration
userId Guid User ID
username string Username
displayName string Display name
role string NURSE, PHYSICIAN, ADMIN, INTEGRATION

POST /auth/refresh body:

Field Type Required Description
refreshToken string yes The current refresh token

Refresh response:

Field Type Description
accessToken string New JWT bearer token
refreshToken string New refresh token (previous one is revoked)
expiresAt DateTimeOffset New access token expiration

Refresh tokens rotate on every use — each call revokes the previous refresh token and issues a new one. If the refresh token is expired, revoked, or the user account is deactivated, the endpoint returns 422 and the client must re-authenticate via login.

POST /auth/logout body:

Field Type Required Description
refreshToken string yes The refresh token to revoke

Returns 204 No Content. Revokes the refresh token server-side and creates a USER_LOGOUT audit log entry. The access token remains valid until its natural expiration (15 min max).

Seeded demo users:

Username Password Role
nurse.demo DemoNurse1! Nurse
physician.demo DemoPhysician1! Physician
admin.demo DemoAdmin1! Admin
integration.mirth MirthIntegration1! Integration

RBAC permission matrix:

Permission Nurse Physician Admin Integration
patients:read yes yes yes
patients:write yes yes yes yes
encounters:read yes yes yes
encounters:write yes yes yes yes
observations:ingest yes yes yes yes
alerts:read yes yes yes
alerts:acknowledge yes yes yes
alerts:resolve yes yes yes
alerts:feedback yes yes yes
thresholds:read yes yes yes
thresholds:write yes
analytics:read yes yes yes
orders:write yes yes yes
medications:write yes yes yes yes
fhir:ingest yes yes
fhir:read yes yes
audit:read yes
users:admin yes

All endpoints except POST /auth/login and GET /fhir/R4/metadata require authentication. Unauthenticated requests receive 401. Authenticated requests without the required permission receive 403.

Audit Logs

Method Path Description
GET /audit-logs Query clinical audit logs (Admin only — requires audit:read permission)

GET /audit-logs query params: entityType, entityId, userId, action, from, to, page, pageSize

Audit actions: THRESHOLD_CREATED, THRESHOLD_UPDATED, THRESHOLD_DELETED, ALERT_ACKNOWLEDGED, ALERT_RESOLVED, ENCOUNTER_STATUS_CHANGED, PATIENT_REGISTERED, SUPPRESSION_WINDOW_SET, USER_LOGIN, AUTHORIZATION_DENIED, USER_LOGOUT, TOKEN_REFRESHED

Each audit log entry includes action, entityType, entityId, userId, userDisplayName, previousValueJson (JSONB), newValueJson (JSONB), reason, ipAddress, correlationId, and createdAt.

Users

Method Path Auth Description
GET /users JWT + users:admin List all clinical user accounts
POST /users JWT + users:admin Create a new clinical user account
PATCH /users/{id} JWT + users:admin Update role, display name, or active status

POST body:

Field Type Required Description
username string yes Unique username (max 100)
password string yes Password (BCrypt hashed)
displayName string yes Display name (max 200)
role string yes NURSE, PHYSICIAN, ADMIN, INTEGRATION

PATCH body:

Field Type Required Description
displayName string no Updated display name
role string no Updated role
isActive bool no Enable/disable account

Operations

Method Path Auth Description
GET /operations/gateways JWT + users:admin List registered gateways; optional status and siteId filters
GET /operations/gateways/{gatewayId} JWT + users:admin Gateway detail with buffer depth, heartbeat metadata, sync history
GET /operations/sites/{siteId}/summary JWT + users:admin Aggregate gateway summary for a site

Discharge Summary

Method Path Description
GET /encounters/{id}/discharge-summary Discharge summary info (status, availability)
GET /encounters/{id}/discharge-summary/content Download discharge summary PDF from MinIO

Alert Quality Metrics

Method Path Description
GET /alerts/quality-metrics Per-alert-type quality metric snapshots; optional alertType, from, to filters
GET /alerts/quality-metrics/summary Aggregate alert quality rates across all alert types; optional from, to
GET /alerts/quality-metrics/feedback Per-alert feedback rows; optional scenarioId, from, to; includes scenarioId/sessionId when Simulation:Enabled

Simulation (requires Simulation:Enabled=true)

Method Path Description
GET /simulation/config Feature probe — { enabled } (always 200; enabled=false when off)
GET /simulation/scenarios Scenario catalogue
GET /simulation/sessions Session presets with resolved scenario summaries
POST /simulation/sessions/{sessionId}/start Start all scenarios in a preset (optional { speed }); all-or-nothing capacity check
POST /simulation/runs Start a single scenario run
GET /simulation/runs Active and recent runs
GET /simulation/runs/{runId} One run
POST /simulation/runs/{runId}/stop Cancel a run
GET /simulation/data/summary Simulated patient / observation / alert counts
DELETE /simulation/data Purge simulated patients and dependents (409 if runs active)

Requires simulation:run (except GET /simulation/config, which needs alerts:read). Endpoints return 404 when simulation is disabled.

Alert Feedback

Method Path Description
POST /alerts/{id}/feedback Submit clinician feedback for an acknowledged/resolved alert; one per user per alert

POST body:

Field Type Required Description
feedbackType string yes USEFUL, TOO_EARLY, TOO_LATE, FALSE_POSITIVE, MISSING_CONTEXT, WOULD_ACT
comment string no Optional free-text comment

FHIR R4 Ingest

All FHIR endpoints are under /fhir/R4, accept application/fhir+json, and return FHIR R4 JSON responses. Authentication is via JWT bearer token or X-Api-Key header (configured in Fhir:ApiKey or the Fhir:ApiKeys array for zero-downtime key rotation; disabled when blank). API key validation uses CryptographicOperations.FixedTimeEquals to prevent timing attacks. When a valid JWT is present, the API key check is skipped — this allows both integration engines (API key) and authenticated admin users (JWT) to access FHIR resources. Write endpoints (POST) require fhir:ingest permission; read endpoints (GET) require fhir:read permission. Errors return a FHIR OperationOutcome with appropriate issue codes.

Method Path Description
GET /fhir/R4/metadata CapabilityStatement — supported resource types and interactions
POST /fhir/R4/Patient Upsert a Patient by hospital identifier (MRN); idempotent
GET /fhir/R4/Patient/{id} Read a Patient by internal ID; returns FHIR R4 Patient resource
GET /fhir/R4/Patient Search Patients by identifier (system|value) or list all; returns Bundle (searchset)
POST /fhir/R4/Encounter Upsert an Encounter by visit identifier; resolves patient by identifier
GET /fhir/R4/Encounter/{id} Read an Encounter by internal ID; returns FHIR R4 Encounter resource
GET /fhir/R4/Encounter Search Encounters by patient (UUID), status (in-progress/finished/cancelled); returns Bundle (searchset)
POST /fhir/R4/Observation Ingest an Observation; maps LOINC/SNOMED codes to internal codes; supports component observations
POST /fhir/R4/MedicationAdministration Record a medication administration; resolves encounter by identifier
POST /fhir/R4 Process a transaction Bundle (Patient → Encounter → Observation/MedicationAdministration in dependency order)

CapabilityStatement: GET /fhir/R4/metadata advertises create, read, and searchType interactions for Patient and Encounter; create only for Observation and MedicationAdministration; and Bundle transaction support.

Identifier resolution: FHIR resources reference each other by hospital identifiers (e.g. MRN in Patient.identifier, visit number in Encounter.identifier). The ExternalResourceIdentifier table maps these to internal UUIDs. On first ingest, a new internal record is created and the identifier is linked. Subsequent requests with the same identifier update the existing record (idempotent upsert).

LOINC code mapping: 19 LOINC codes and 3 SNOMED CT fallback codes map to internal observation codes (see LoincCodeMapper). Unsupported codes return 422 with an OperationOutcome. Temperature observations in Fahrenheit ([degF]) are automatically converted to Celsius.

Transaction Bundles: POST /fhir/R4 accepts Bundle.type=transaction. Entries are processed in dependency order (Patient first, then Encounter, then Observation/MedicationAdministration). On first failure, processing stops (transaction semantics) and the response includes the OperationOutcome.

Read interactions: GET /fhir/R4/Patient/{id} and GET /fhir/R4/Encounter/{id} return the internal resource mapped back to a FHIR R4 resource with hospital identifiers resolved from ExternalResourceIdentifier. Returns 404 with an OperationOutcome if the resource is not found.

Search interactions: GET /fhir/R4/Patient?identifier=system|value searches by external identifier; omitting identifier lists all patients (up to _count, default 20). GET /fhir/R4/Encounter?patient={uuid}&status=in-progress filters encounters by patient and/or FHIR status (in-progress, finished, cancelled). Both return a Bundle with type=searchset.

Integration with Mirth Connect: HL7v2 ADT messages (A01 admit, A03 discharge, A08 update) and ORU R01 lab results can be mapped to FHIR Bundles via Mirth channels. See docs/integration/mirth-fhir-channels.md.


Data Models

Patient

id                    Guid    PK
mrn                   string  required, unique — auto-generated on registration (e.g. MRN-000001)
firstName             string  required (max 100)
lastName              string  required (max 100)
dateOfBirth           Date    required
gender                string  required (max 10)
bloodType             string? A+ | A- | B+ | B- | AB+ | AB- | O+ | O-
allergies             string? free text
emergencyContactName  string? (max 200)
emergencyContactPhone string? (max 30)
status                string  active | inactive  (default: active)
createdAt             DateTimeOffset

Encounter

id                  Guid    PK
patientId           Guid    FK → Patient
encounterType       string  INPATIENT | OUTPATIENT | EMERGENCY
status              string  scheduled | active | discharged | cancelled  (default: scheduled)
department          string  required (max 100)
attendingPhysician  string  required (max 200)
roomBed             string? ward/bed assignment (max 50)
admissionReason     string? clinical reason for admission
dischargeDiagnosis  string? set on discharge
admittedAt          DateTimeOffset
dischargedAt        DateTimeOffset?
createdAt           DateTimeOffset

Indexes: (patient_id, admitted_at DESC), partial (status, admitted_at DESC) WHERE status = 'active', partial unique (patient_id, encounter_type) WHERE status = 'ACTIVE' (prevents duplicate active encounters of the same type per patient)

AlertThreshold

id               Guid    PK
observationCode  string  required, unique (max 50)
displayName      string  required (max 200)
unit             string  required (max 20)
criticalLow      decimal(10,3)?
warningLow       decimal(10,3)?
warningHigh      decimal(10,3)?
criticalHigh     decimal(10,3)?
createdAt        DateTimeOffset

Observation

id               Guid    PK
encounterId      UUID    FK → Encounter
observationCode  string  required (max 50)
value            decimal(10,3) required
unit             string  required (max 20)
source           string  DEVICE | MANUAL | LAB  (default: DEVICE)
idempotencyKey   string? optional, partial unique index
recordedAt       DateTimeOffset required
createdAt        DateTimeOffset

Indexes: partial unique (idempotency_key) WHERE idempotency_key IS NOT NULL, (encounter_id, observation_code, recorded_at DESC)

ClinicalAlert

id              Guid    PK
encounterId     Guid    FK → Encounter
patientId       Guid    FK → Patient
observationId   Guid?   FK → Observation (null for NEWS2, GCS, SOFA composite alerts)
alertType       string  e.g. CRITICAL_HEART_RATE, QSOFA_SCREEN, SOFA_SEPSIS, NEWS2_WARNING, NEWS2_EMERGENCY, GCS_CRITICAL
severity        string  WARNING | CRITICAL
details         text    required
explanation     jsonb?  immutable structured explanation snapshot (score contributors, trend, medication context, narrative); null on legacy/threshold-only alerts
observationCode string? observation code that triggered this alert (e.g. HEART_RATE) — enables direct lookups without LIKE pattern matching
status          string  open | acknowledged | resolved | escalated  (default: open)
acknowledgedAt  DateTimeOffset?
acknowledgedBy  string?
resolvedAt      DateTimeOffset?
triggeredAt     DateTimeOffset

Indexes: (encounter_id, triggered_at DESC), (patient_id, triggered_at DESC), partial (severity, triggered_at DESC) WHERE status = 'open', (encounter_id, observation_code, status)

News2Score

id                    Guid    PK
encounterId           Guid    FK → Encounter
patientId             Guid    FK → Patient
totalScore            int     aggregate 020+
riskLevel             string  LOW | LOW_MEDIUM | MEDIUM | HIGH
respRateScore         int     component 03
spo2Score             int
systolicBpScore       int
heartRateScore        int
consciousnessScore    int
temperatureScore      int
supplementalO2Score   int
hasSingleParamThree   bool    true when any single parameter scored 3
calculatedAt          DateTimeOffset

Indexes: (encounter_id, calculated_at DESC), (patient_id, calculated_at DESC)

GcsScore

id              Guid    PK
encounterId     Guid    FK → Encounter
patientId       Guid    FK → Patient
eyeScore        int     14
verbalScore     int     15
motorScore      int     16
totalScore      int     315
classification  string  MILD | MODERATE | SEVERE
calculatedAt    DateTimeOffset

Indexes: (encounter_id, calculated_at DESC)

SofaScore

id                  Guid    PK
encounterId         Guid    FK → Encounter
patientId           Guid    FK → Patient
totalScore          int     sum of six components (024)
respiratoryScore    int     04
coagulationScore    int     04
liverScore          int     04
cardiovascularScore int     04
cnsScore            int     04
renalScore          int     04
isBaseline          bool    true for admission baseline row
deltaFromBaseline   int?    current total minus baseline total
stalenessFlags      jsonb?  stale/missing components, SpO2 fallback flag
calculatedAt        DateTimeOffset

Indexes: (encounter_id, calculated_at DESC), partial (encounter_id) WHERE is_baseline = true

QsofaEvaluation

id                Guid    PK
encounterId       Guid    FK → Encounter
patientId         Guid    FK → Patient (via Encounter)
activeCriteria    int     03 (check constraint)
respRate          decimal? respiratory rate value at evaluation time
systolicBp        decimal? systolic BP value at evaluation time
avpu              decimal? AVPU/GCS value at evaluation time
screenAlertFired  bool    true if this evaluation triggered a QSOFA_SCREEN alert
evaluatedAt       DateTimeOffset
createdAt         DateTimeOffset

Indexes: (encounter_id, evaluated_at)

Order

id            Guid    PK
encounterId   Guid    FK → Encounter
orderType     string  LAB | MEDICATION | IMAGING
description   string  required (max 500)
orderedBy     string  required (max 200)
status        string  pending | in_progress | resulted | cancelled  (default: pending)
orderedAt     DateTimeOffset
resultedAt    DateTimeOffset?
resultSummary string? Free-text result summary (set on record result)

Indexes: (encounter_id, ordered_at DESC), partial (status, ordered_at) WHERE status IN ('pending', 'in_progress')

SepsisBundle

id                    Guid    PK
encounterId           Guid    FK → Encounter
triggeringAlertId     Guid    FK → ClinicalAlert
triggeringAlertType   string  required (max 50) — SOFA_SEPSIS
recognizedAt          DateTimeOffset
deadlineAt            DateTimeOffset — recognizedAt + 1 hour
complianceStatus      string  IN_PROGRESS | COMPLIANT | NON_COMPLIANT  (default: IN_PROGRESS)
completedAt           DateTimeOffset?

Indexes: (encounter_id, recognized_at DESC), (compliance_status), (triggering_alert_id), partial unique (encounter_id) WHERE compliance_status = 'IN_PROGRESS' (prevents TOCTOU race on concurrent bundle creation)

Check constraint: compliance_status IN ('IN_PROGRESS', 'COMPLIANT', 'NON_COMPLIANT')

SepsisBundleElement

id            Guid    PK
bundleId      Guid    FK → SepsisBundle (CASCADE)
elementType   string  required (max 40) — BLOOD_CULTURES | SERUM_LACTATE | BROAD_SPECTRUM_ANTIBIOTICS | IV_FLUID_RESUSCITATION
status        string  PENDING | COMPLETED  (default: PENDING)
orderId       Guid?   FK → Order (SET NULL)
completedAt   DateTimeOffset?

Indexes: unique (bundle_id, element_type), (order_id)

Check constraints: element_type IN (...), status IN ('PENDING', 'COMPLETED')

MedicationAdministration

id              Guid    PK
encounterId     Guid    FK → Encounter (Restrict on delete)
drugName        string  required (max 200)
dose            decimal(10,4) required
doseUnit        string  required (max 20)
route           string  required (max 20)
administeredAt  DateTimeOffset required
administeredBy  string  required (max 200)

Indexes: (encounter_id, administered_at), (encounter_id, drug_name)

OutboxEvent

id           Guid    PK
topic        string  required (max 200)
partitionKey string? — encounterId for per-encounter ordering
payload      JSONB   required
createdAt    DateTimeOffset
processedAt  DateTimeOffset?
retryCount   int     default 0 — Kafka produce attempt counter
lastError    string? — last Kafka produce failure reason
failedAt     DateTimeOffset? — set when retryCount exceeds OutboxMaxRetries (permanently failed)

Partial index: (created_at) WHERE processed_at IS NULL AND failed_at IS NULL

ExternalResourceIdentifier

id              Guid    PK
resourceType    string  PATIENT | ENCOUNTER
internalId      Guid    FK → Patient or Encounter (logical, not enforced)
system          string  required — identifier system URI (e.g. http://hospital.example/mrn)
value           string  required — identifier value (e.g. MRN-001)
createdAt       DateTimeOffset

Unique index: (resource_type, system, value) — one mapping per external identifier

ClinicalUser

id              Guid    PK
username        string  required, unique (max 100)
passwordHash    string  required (BCrypt)
displayName     string  required (max 200)
role            string  NURSE | PHYSICIAN | ADMIN | INTEGRATION
isActive        bool    default true
createdAt       DateTimeOffset
lastLoginAt     DateTimeOffset?

RefreshToken

id          Guid    PK
token       string  required, unique (max 256) — opaque base64 token (64 random bytes)
userId      Guid    FK → ClinicalUser (CASCADE)
expiresAt   DateTimeOffset required
createdAt   DateTimeOffset
revokedAt   DateTimeOffset? — set on refresh rotation or explicit logout

Indexes: unique (token), (user_id)

ClinicalAuditLog

id                Guid    PK
action            string  required (max 50) — THRESHOLD_CREATED | THRESHOLD_UPDATED | THRESHOLD_DELETED | ALERT_ACKNOWLEDGED | ALERT_RESOLVED | ENCOUNTER_STATUS_CHANGED | PATIENT_REGISTERED | SUPPRESSION_WINDOW_SET | USER_LOGIN | AUTHORIZATION_DENIED | USER_LOGOUT | TOKEN_REFRESHED
entityType        string  required (max 100) — e.g. AlertThreshold, ClinicalAlert, Encounter, Patient, ClinicalUser
entityId          Guid    required
userId            Guid?   FK → ClinicalUser (null for system-initiated actions)
userDisplayName   string? (max 200)
previousValueJson jsonb?  state before the action
newValueJson      jsonb?  state after the action
reason            string? optional clinician-provided reason
ipAddress         string? (max 45) — IPv4 or IPv6
correlationId     string? (max 100) — links to request correlation header
createdAt         DateTimeOffset

Append-only — no UPDATE or DELETE from application code.

Indexes: (entity_type), (entity_id), (user_id), (created_at)

ClinicalSite

id          Guid    PK
siteCode    string  required, unique (max 32) — uppercase alphanumeric with hyphens
name        string  required (max 200)
address     string? free text
active      bool    default true
createdAt   DateTimeOffset

Indexes: unique (site_code)

WardGateway

id                    Guid    PK
siteId                Guid    FK → ClinicalSite (RESTRICT)
gatewayCode           string  required (max 64)
department            string  required (max 100)
status                string  ONLINE | DEGRADED | OFFLINE  (default: OFFLINE)
reportedBufferDepth   int     default 0
lastHeartbeatAt       DateTimeOffset?
lastSyncAt            DateTimeOffset?
createdAt             DateTimeOffset

Indexes: unique (site_id, gateway_code), (site_id, department), partial (status) WHERE status != 'ONLINE'

Check constraint: status IN ('ONLINE', 'DEGRADED', 'OFFLINE')

ReconciliationAlert

id            Guid    PK
checkType     string  UNACKNOWLEDGED_CRITICAL_ALERT | PENDING_ORDER_NO_RESULT | ACTIVE_INPATIENT_NO_OBSERVATION
encounterId   Guid?   FK → Encounter
patientId     Guid?   FK → Patient
details       text    required
resolvedAt    DateTimeOffset?
createdAt     DateTimeOffset

AlertFeedback

id            Guid    PK
alertId       Guid    FK → ClinicalAlert
userId        Guid    FK → ClinicalUser
feedbackType  string  USEFUL | TOO_EARLY | TOO_LATE | FALSE_POSITIVE | MISSING_CONTEXT | WOULD_ACT
comment       string? optional free-text
createdAt     DateTimeOffset

Unique index: (alert_id, user_id) — one feedback per user per alert

AlertQualityMetric

id                      Guid    PK
alertType               string  alert type (DB literal)
windowStart             DateTimeOffset
windowEnd               DateTimeOffset
totalAlerts             int
acknowledgedCount       int
resolvedCount           int
escalatedCount          int
feedbackUsefulCount     int
feedbackFalsePositiveCount int
feedbackWouldActCount   int
feedbackCount           int
acknowledgementRate     double
falsePositiveRate       double
usefulRate              double
wouldActRate            double
avgSecondsToAcknowledge double
avgSecondsToResolution  double
computedAt              DateTimeOffset

Indexes: (alert_type, window_start)


Elasticsearch Index Shapes

patient_encounters

{
  "encounterId": "uuid",
  "patientId": "uuid",
  "mrn": "MRN-000001",
  "patientName": "Jane Smith",
  "department": "ICU",
  "status": "active",
  "attendingPhysician": "Dr. Osei",
  "roomBed": "ICU-4B",
  "admissionReason": "Chest pain, rule out MI",
  "admittedAt": "2025-01-01T08:00:00Z",
  "openAlertCount": 2,
  "news2Score": 6,
  "news2RiskLevel": "MEDIUM",
  "sepsisBundleStatus": "IN_PROGRESS",
  "sepsisBundleElementsCompleted": 2,
  "sepsisBundleDeadlineAt": "2025-01-01T09:00:00Z",
  "lastObservationAt": "2025-01-01T09:45:00Z"
}

news2Score and news2RiskLevel are optional — populated by EsIndexerService when a NEWS2_WARNING or NEWS2_EMERGENCY alert is indexed (payload fields news2Score / news2RiskLevel from News2Detector). LOW-risk scores with no alert are not projected to Elasticsearch.

sepsisBundleStatus, sepsisBundleElementsCompleted, and sepsisBundleDeadlineAt are optional — populated by EsIndexerService from sepsis.bundle.created and sepsis.bundle.updated Kafka events. Enables department dashboards to filter/sort encounters by sepsis bundle compliance in real time.

observations

{
  "observationId": "uuid",
  "encounterId": "uuid",
  "patientId": "uuid",
  "mrn": "MRN-000001",
  "observationCode": "HEART_RATE",
  "value": 118.0,
  "unit": "bpm",
  "source": "DEVICE",
  "recordedAt": "2025-01-01T09:45:00Z"
}

clinical_alerts

{
  "alertId": "uuid",
  "encounterId": "uuid",
  "patientId": "uuid",
  "department": "ICU",
  "alertType": "THRESHOLD_BREACH",
  "severity": "CRITICAL",
  "status": "open",
  "triggeredAt": "2025-01-01T09:45:00Z"
}

RabbitMQ Exchange Topology

Exchange: clinical.notifications.exchange (direct)

Queue Purpose DLQ
alerts.paging.queue Physician paging jobs; prefetch=3 alerts.paging.dlq on NACK
alerts.paging.dlq Dead-letter queue; x-message-ttl = 300000ms alerts.escalation.queue on TTL expiry
alerts.escalation.queue On-call backup paging
notifications.discharge.queue Discharge summary PDF generation + MinIO upload
notifications.reconciliation.queue Reconciliation safety findings from scheduled checks
notifications.appointment.queue Appointment reminder SMS

Kafka Topics

Topic Partition key Consumer groups
observation.recorded encounterId es-indexer, sepsis-engine (qSOFA), warning-evaluator, news2-scoring, gcs-scoring, sofa-scoring, trend-analyzer, data-lake-writer
alert.generated encounterId es-indexer, notification-publisher, data-lake-writer
encounter.status.changed encounterId es-indexer, data-lake-writer
gcs.scored encounterId sofa-scoring
sepsis.bundle.created encounterId es-indexer
sepsis.bundle.updated encounterId es-indexer

All topics use 6 partitions. KAFKA_AUTO_CREATE_TOPICS_ENABLE=false — topics (including gcs.scored) are provisioned explicitly by KafkaTopicProvisioner on API startup.

alert.generated payload (minimum fields for downstream consumers):

Field Required by Notes
alertId, encounterId, patientId ES indexer, data lake, paging UUIDs
alertType, severity, triggeredAt All consumers DB string literals for type/severity
details Data lake Parquet Human-readable breach summary; always set on new alerts
department ES indexer Optional; critical ingest alerts include it
news2Score, news2RiskLevel ES indexer Optional; set on NEWS2_WARNING / NEWS2_EMERGENCY alerts for encounter document projection
explanation Dashboard, analytics Optional; structured score contributors, trend, medication context, and narrative summary on explainable alerts (NEWS2, SOFA, GCS, rapid deterioration). Omitted on legacy and threshold-only alerts. Existing consumers that ignore unknown fields remain compatible.
partitionKey Outbox relay Same as encounterId


qSOFA Screening (Sepsis-3 Bedside Tool)

The qSOFA (quick Sequential Organ Failure Assessment) engine evaluates three organ-dysfunction criteria per encounter within the SepsisEngineService Kafka consumer. Redis keys use a 30-minute TTL sliding window:

Criterion Observation Code Trigger
Tachypnea RESP_RATE ≥ 22 breaths/min
Hypotension SYSTOLIC_BP ≤ 100 mmHg
Altered mentation GCS / AVPU GCS < 15 or AVPU ≥ 1 (any non-Alert state)

Redis key pattern: qsofa:{encounterId}:{code} with 30-minute TTL. When a criterion normalizes, the key is deleted immediately. When ≥ 2 of 3 criteria are active simultaneously and no open QSOFA_SCREEN alert exists, the engine inserts a WARNING-level screening alert with details formatted as "qSOFA score 2/3: RESP_RATE=24, SYSTOLIC_BP=95 — recommend SOFA lab panel".

Clinical role: qSOFA is a bedside screening tool — it identifies patients who should have SOFA labs ordered. It does not trigger the sepsis bundle directly. Only SOFA_SEPSIS (delta ≥ 2 from baseline) triggers bundle creation. This matches the Sepsis-3 two-tier workflow: screen → confirm → treat.

API: GET /encounters/{id}/qsofa/current returns activeCriteria (03) and per-criterion values from Redis via QsofaService — used by ward dashboards and the simulator poll loop. GET /encounters/{id}/qsofa/history returns cursor-paginated evaluation history from the qsofa_evaluations table, including criteria values and whether a screen alert was fired — used by the QsofaHistory.vue dashboard chart.


Sepsis Bundle Compliance (SEP-1)

When a SOFA_SEPSIS alert fires (delta ≥ 2 from baseline), SepsisAlertHandler calls SepsisBundleService.TryCreateBundleAsync() to initiate a four-element treatment bundle with a one-hour compliance deadline:

Bundle Element Order Type Auto-Created Order Description
BLOOD_CULTURES Lab SEP-1: Blood cultures
SERUM_LACTATE Lab SEP-1: Serum lactate
BROAD_SPECTRUM_ANTIBIOTICS Medication SEP-1: Broad-spectrum antibiotics
IV_FLUID_RESUSCITATION Procedure SEP-1: IV fluid bolus

Lifecycle:

  1. SOFA_SEPSIS alert fires → SepsisAlertHandlerSepsisBundleService.TryCreateBundleAsync()
  2. Bundle created with complianceStatus = IN_PROGRESS, deadlineAt = recognizedAt + 1 hour
  3. Four orders created (orderedBy: sepsis-bundle-engine), each linked to a bundle element
  4. Outbox event → Kafka topic sepsis.bundle.created → ES indexer projects sepsisBundleStatus on encounter
  5. Clinicians work through orders; PATCH /orders/{id}/resultOrderServiceSepsisBundleService.OnOrderResultedAsync()
  6. Each resulted order marks its bundle element COMPLETED; outbox event → sepsis.bundle.updated → ES update
  7. When all four elements are complete: complianceStatus = COMPLIANT (within deadline) or NON_COMPLIANT (past deadline); Prometheus sepsis_bundle_compliance_total{status} incremented
  8. If the deadline passes with incomplete elements, SepsisBundleMonitorService (polling every 5 min) marks the bundle NON_COMPLIANT — elements remain PENDING but the bundle status reflects the missed deadline

Idempotency: Only one in-progress bundle can exist per encounter, enforced by a partial unique index (encounter_id) WHERE compliance_status = 'IN_PROGRESS'. Bundle creation wraps order and bundle inserts in a single database transaction — if the unique constraint rejects a concurrent duplicate, the transaction rolls back all associated orders, preventing orphaned order records. A second alert for the same encounter returns early without side effects.


NEWS2 Scoring

The NEWS2 engine evaluates seven observation codes that overlap with the expanded vital-sign vocabulary from Phase 10:

Parameter Observation Code Score range
Respiratory rate RESP_RATE 03
Oxygen saturation (Scale 1) SPO2 03
Systolic blood pressure SYSTOLIC_BP 03
Heart rate HEART_RATE 03
Consciousness (AVPU) AVPU 0 or 3
Temperature TEMP_C 03
Supplemental oxygen SUPPLEMENTAL_O2 0 or 2

Risk levels (aggregate score):

Total score Risk level Alert
04 (no single param = 3) LOW None
Any single param = 3 (total under 5) LOW_MEDIUM NEWS2_WARNING
56 MEDIUM NEWS2_WARNING
≥ 7 HIGH NEWS2_EMERGENCY (CRITICAL)

All seven parameters must be present in Redis (4-hour TTL per key) before a score is computed. Each new observation after completeness triggers a new news2_scores row; alert creation is idempotent per encounter and alert type while an alert remains open.


Elasticsearch Index Replay

If the Elasticsearch indices need to be rebuilt (e.g., after a mapping change or data loss):

# 1. Stop the indexer consumer group (set consumer group to a known-good offset, or reset to beginning)
# 2. Delete existing indices
curl -X DELETE http://localhost:9200/patient_encounters
curl -X DELETE http://localhost:9200/observations
curl -X DELETE http://localhost:9200/clinical_alerts

# 3. Reset the es-indexer consumer group offset to the beginning
docker exec -it <kafka-container> kafka-consumer-groups.sh \
  --bootstrap-server localhost:9092 \
  --group es-indexer \
  --topic observation.recorded \
  --reset-offsets --to-earliest --execute

# 4. Restart the application — EsIndexerService will replay all events from offset 0
dotnet run

The indices rebuild from the full Kafka history. Document count should match PostgreSQL row count when complete. This is only possible because Kafka retains events after consumption.


Data Lake Replay

The MinIO Parquet archive is a pure Kafka projection — rebuildable without touching PostgreSQL. See docs/plans/phase-9-plan.md for the full procedure. Summary:

# Reset data-lake-writer offsets to earliest
docker compose exec -T kafka /opt/kafka/bin/kafka-consumer-groups.sh \
  --bootstrap-server localhost:9092 \
  --group data-lake-writer \
  --reset-offsets --to-earliest --all-topics --execute

# Clear Parquet prefixes in MinIO (mc alias localvc http://localhost:9005 minioadmin minioadmin)
mc rm --recursive --force localvc/vigilcare/observations/
mc rm --recursive --force localvc/vigilcare/alerts/
mc rm --recursive --force localvc/vigilcare/encounters/

# Restart the API — DataLakeWriterService replays from offset 0
dotnet run

Verify with ./scripts/run-phase9-verification.sh.


Pagination

List endpoints use offset pagination:

Param Default Description
page 1 Page number (1-based)
pageSize 20 Items per page (max 100)

Response shape:

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

Observation history uses cursor pagination on (recorded_at DESC, id DESC). Offset pagination would shift results as new observations arrive continuously. The cursor is opaque and returned in the response; pass it as ?cursor= on the next request.


Implemented Phases

Phases 138 from the project roadmap are implemented and verified, including the Site & Gateway Registry (Phase 20), the Ward Gateway Service (Phase 21), the Dashboard Gap Analysis Fixes (Phase 22), the Degraded Operations Visibility (Phase 23), the Sepsis-3 clinical refactor (Phases 2729), the FHIR R4 Inbound Facade (Phase 30), RBAC with clinical audit logging (Phase 31), the Alert Quality Analytics (Phase 33), the Explainable Alerts (Phase 34), the MIMIC-IV Replay Scenario Generator (Phase 35), the Enhanced Dashboard (department overview, sepsis bundle board, critical alert notifications, shift handoff reports, vitals entry, sortable/filterable ward table), and self-service clinical simulation (Phases 3638 — in-app runner, Simulation UI, session presets, ward purge, scenario-attributed feedback). Integration tests (dotnet test) and per-phase verification scripts cover Phases 815, 2023, 2531, 3338. Phases 1719 add the Vue dashboard and clinician feedback (Vitest in vigilcare-dashboard/). See also docs/plans/vigilcare-clinical-roadmap.md.

Phase Feature Status
1 Schema, EF Core migrations, patient/encounter CRUD, alert threshold CRUD, encounter status machine, Redis threshold pre-load, seed data Done
2 Observation ingest — idempotency, plausibility validation, synchronous critical alert creation, outbox event, cursor-paginated history; alert lifecycle (acknowledge, resolve); integration tests Done
3 Outbox relay (IHostedService, 500ms poll); Kafka topics with 6 partitions; encounterId partition key; relay survives Kafka restart Done
4 Elasticsearch CQRS projection (EsIndexerService); patient search; observation trend; alert summary; population aggregation; replay procedure Done
5 Sepsis engine (SepsisEngineService); Redis-based qSOFA screening (SIRS removed in Phase 27); idempotent alert creation; integration tests Done
6 RabbitMQ exchange and queue topology; NotificationPublisherService; PagingWorkerService; DLQ escalation (EscalationWorkerService); discharge summary (DischargeSummaryWorkerService → MinIO); integration tests Done
7 Reconciliation scheduler — unacknowledged critical alerts, stale pending orders, disconnected monitors; reconciliation_alerts table; RabbitMQ publish; integration tests Done
8 Prometheus metrics (GET /metrics); ten metric families and three collectors; Grafana clinical dashboard; ObservabilityPhase8Tests; run-phase8-verification.sh Done
9 Data lake writer — data-lake-writer consumer group; date-partitioned Parquet flush to MinIO; DataLakePhase9Tests; run-phase9-verification.sh; design doc in docs/decisions/data-lake-design.md Done
10 Clinical data model expansion — BloodType, patient allergies/emergency contact, encounter room/bed/admission/discharge fields; five new observation codes (SYSTOLIC_BP, DIASTOLIC_BP, LACTATE_MMOL_L, AVPU, SUPPLEMENTAL_O2); GLUCOSE_MG_DL threshold fix; 12 seeded thresholds; ClinicalDemographicsAndObservationTests; run-phase10-verification.sh Done
11 Warning alert consumer (WarningAlertService / warning-evaluator); 10 Warning* alert types; Orders API (OrdersController, OrderService); FluentValidation on all request DTOs; WarningAlertTests, OrderLifecycleTests, ValidationTests; run-phase11-verification.sh Done
12 NEWS2 composite scoring (News2Calculator, News2Detector, News2ScoringService); news2_scores table; NEWS2_WARNING / NEWS2_EMERGENCY alert types; News2Controller (current + history); ES news2Score / news2RiskLevel projection; Prometheus NEWS2 metrics; News2CalculatorTests, News2DetectorTests; run-phase12-verification.sh Done
13 Trend detection (TrendCalculator, TrendDetector, TrendAnalyzerService); RAPID_DETERIORATION alert type; alert suppression windows (AlertSuppressionService, Redis suppress:{enc}:{type}); TrendCalculatorTests, TrendDetectorTests, AlertSuppressionTests; run-phase13-verification.sh Done
14 qSOFA scoring engine (QsofaCalculator, QsofaDetector); sepsis bundle compliance (SepsisBundle, SepsisBundleElement, SepsisBundleService); auto-created treatment orders with 1-hour deadline; SepsisAlertHandler bridge; SepsisBundleMonitorService (5-min overdue scan); SepsisBundlesController API; ES projection of bundle status; Kafka topics sepsis.bundle.created / sepsis.bundle.updated; Prometheus qsofa_detections_total and sepsis_bundle_compliance_total; QsofaCalculatorTests, QsofaDetectorTests, SepsisBundleTests (bundle trigger updated to SOFA_SEPSIS in Phase 27) Done
15 Medication administration (MedicationAdministration, MedicationsController, MedicationService); drug-vital correlation config (MedicationCorrelationOptions); MedicationCorrelationHelper annotates WarningEvaluator and News2Detector alert details; medication_administrations table + migration; MedicationServiceTests, MedicationCorrelationTests, MedicationValidationTests; run-phase15-verification.sh; design doc in docs/decisions/medication-correlation-design.md Done
16 Console replay simulator (VigilCare.Simulator); scenario JSON schema; CLI commands replay, replay-all, validate, dry-run; speed multiplier and optional API polling; eight sample scenarios; docs/simulator-guide.md Done
20 Site & Gateway Registry + Clinical Sync ContractsVigilCare.ClinicalContracts shared class library with sync DTOs (ClinicalSyncBatchRequest, SyncedObservation, SyncedAlertEvent, GatewayHeartbeatRequest); ClinicalSite and WardGateway domain entities with EF Core configurations and migrations; GatewayApiKeyAuthenticationHandler (constant-time X-Api-Key validation + X-Gateway-Id claim) registered alongside JWT bearer; SitesController (create, list, get) and GatewaysController (register, list, get, heartbeat) with dual auth — JWT + users:admin for admin CRUD, gateway API key for heartbeat; SiteService, GatewayRegistryService; FluentValidation on CreateSiteRequest, RegisterGatewayRequest, GatewayHeartbeatRequest; GatewayRegistrySeeder with fixed GUIDs for demo site and gateway; WardGatewayMetricsCollector (60s periodic) exports ward_gateways_offline_gauge and ward_gateway_buffer_depth; GatewayRegistryTests (register, heartbeat, API key 401, degraded status, department filter); ClinicalContractsTests (JSON round-trip); run-phase20-verification.sh Done
21 Ward Gateway Service (Local-First Clinical Path)VigilCare.WardGateway standalone ASP.NET Core 8 deployable with its own PostgreSQL (vigilcare_ward), Redis, and RabbitMQ; domain entities mirror central API (ReplicaPatient, ReplicaEncounter, LocalObservation, LocalClinicalAlert, ReplicaAlertThreshold); LocalObservationService ingests observations locally with plausibility validation and synchronous critical alert creation; LocalWarningEvaluator creates warning-range alerts from Redis-cached thresholds; BufferedSyncWriter writes observation and alert events to buffered_sync_items table for upload when online; EncounterReplicaSyncService pulls patient/encounter data from central API on startup; ThresholdCacheLoader fetches alert thresholds from central API into local Redis; CentralReachabilityService polls central API health every 30s; GatewayHeartbeatService reports gateway status and buffer depth to central registry; SyncUploaderService batches buffered sync items and uploads to central API using ClinicalSyncBatchRequest contracts when online; local RabbitMQ paging (LocalPagingWorkerService) and escalation (LocalEscalationWorkerService) for ward-level clinician notification; EncounterReadService provides ward encounter list and detail views; Docker Compose ward-gateway profile with separate PostgreSQL, Redis, and RabbitMQ; health checks (Redis, RabbitMQ, encounter replica readiness); WardGatewayLocalPathTests and WardGatewayPartitionTests integration tests with Testcontainers; run-phase21-verification.sh Done
22 Dashboard Gap Analysis Fixes — SOFA history chart (SofaHistory.vue) with organ-system breakdown; GCS history chart (GcsHistory.vue) with component tracking; qSOFA evaluation history (QsofaHistory.vue) backed by new qsofa_evaluations table and GET /qsofa/history API; patient banner (PatientBanner.vue) with demographics, age, blood type, allergies, emergency contact; encounter timeline (EncounterTimeline.vue) with merged chronological status/observation/alert events; medication administration markers on vital trend charts (medicationMarkerPlugin.js); composables patientFormat.js, timelineFormat.js, chartMedications.js; GET /encounters/{id}/gcs/history cursor-paginated GCS history endpoint; QsofaDetector now persists every evaluation; GapAnalysisFixTests integration tests; Vitest tests for all new components; run-phase22-verification.sh; docs/dashboard-gap-analysis.md Done
17 Ward dashboard shell — Vue 3 + Vite + Pinia + Tailwind; virtual ward table (NEWS2-sorted, department filter); patient detail (vitals, scores, alerts, orders, sepsis bundle); alert center (global acknowledge/resolve); API polling; CORS-backed GET /encounters ward list Done
18 Clinical review mode — Chart.js vital sign trends (5 charts), NEWS2 history chart, local replay controls, alert reasoning panel, medication context on alerts; fetchNews2History / fetchMedications; Vitest composable and component tests; docs/dashboard-guide.md Done
19 Clinician feedback mode — six rating buttons per alert, optional notes, Feedback Summary with aggregate stats, JSON/CSV export, localStorage persistence; docs/clinical-testing-guide.md for doctor/nurse evaluation sessions Done
25 Glasgow Coma Scale — GcsCalculator, GcsDetector, GcsScoringService; gcs_scores table; GCS_CRITICAL / GCS_WARNING alerts; gcs.scored outbox topic; NEWS2 consciousness GCS-first; qSOFA altered mentation sync; GcsController; Prometheus gcs_scores_total; GcsScoringTests; run-phase25-verification.sh Done
26 SOFA scoring — SofaCalculator, SofaDetector, SofaLabCache, SofaVasopressorResolver, SofaScoringService; sofa_scores table with baseline + delta; SOFA_SEPSIS / SOFA_WARNING alerts; six new observation codes; SofaController; Kafka topic gcs.scored provisioned for CNS re-score; stale-encounter guard for Kafka replay; Prometheus sofa_scores_total; SofaScoringTests; run-phase26-verification.sh Done
27 Sepsis-3 clinical refactor — SIRS removed (SirsDetector, SirsEvaluator deleted); qSOFA repositioned as bedside screening (QSOFA_SCREEN replaces QSOFA_WARNING); sepsis bundle now triggered only by SOFA_SEPSIS (delta ≥ 2) via SepsisAlertHandler; AlertCreationGuard prevents deprecated SEPSIS_WARNING creation; legacy alert types retained [Obsolete] for historical queries; migration AddQsofaScreenAlertType; SepsisRefactorTests, AlertCreationGuardTests; run-phase27-verification.sh Done
28 Frontend GCS + SOFA + sepsis UI refactorGcsEntryForm.vue (bedside GCS component entry); SofaScorePanel.vue (organ-system breakdown with staleness indicators); useGcs / useSofa composables; scoring Pinia store; ScoresPanel updated with GCS/SOFA display; SepsisBundlePanel and AlertReasoning refactored for Sepsis-3 alert types; Vitest tests for GCS entry, SOFA panel, scores panel, alert labels; run-phase28-verification.sh Done
29 Simulator scenario expansion + clinical validation — three new scenarios (neurological-decline-gcs-01, sepsis-sofa-progression-01, sofa-partial-spo2-fallback-01); existing scenarios enriched with GCS/SOFA observations; ScenarioReplayHelper for end-to-end test replay; ClinicalRefactorEndToEndTests validates qSOFA screen → SOFA labs → bundle workflow; simulator polls GCS/SOFA scores; run-phase29-verification.sh Done
30 FHIR R4 Inbound FacadeFhirIngestController (POST /fhir/R4/{Patient,Encounter,Observation,MedicationAdministration}); FhirMetadataController (CapabilityStatement); FhirBundleProcessor (transaction Bundles in dependency order); LoincCodeMapper (19 LOINC + 3 SNOMED CT → internal codes); FhirUnitConverter (°F→°C); ExternalResourceIdentifier table + ExternalIdentifierService for hospital MRN/visit number ↔ internal UUID linking; FhirApiKeyMiddleware (X-Api-Key auth); FhirExceptionFilter (→ OperationOutcome); PatientFhirMapper, EncounterFhirMapper, ObservationFhirMapper, MedicationAdministrationFhirMapper, FhirReferenceResolver; idempotent patient/encounter upserts (RegisterOrUpdateByIdentifierAsync, OpenOrUpdateByIdentifierAsync); configurable identifier systems, department codes, encounter class maps (FhirOptions); Prometheus fhir_ingest_total, fhir_mapping_errors_total; Mirth Connect integration guide; FhirIngestTests; run-phase30-verification.sh Done
31 RBAC + Clinical Audit Logging + Token Refresh — JWT bearer authentication (AuthService, AuthController); four clinical roles (Nurse, Physician, Admin, Integration) with 18 granular permissions; AuthorizePermission attribute on all controller actions; PermissionAuthorizationHandler + PermissionPolicyProvider resolve perm:* policies; CurrentUserService extracts identity from JWT claims; ClinicalUser entity with BCrypt password hashing; RefreshToken entity with DB-backed opaque token storage, rotation on use, and server-side revocation; short-lived access tokens (15 min) paired with long-lived refresh tokens (7 days); POST /auth/refresh and POST /auth/logout endpoints; ClinicalAuditLog append-only table with before/after JSONB, user identity, IP, and correlation ID; AuditService writes log entries on clinical write actions (12 audit actions including USER_LOGOUT and TOKEN_REFRESHED); AuditLogsController admin-only query with filters; FhirApiKeyOrJwtMiddleware dual auth for FHIR routes (JWT or X-Api-Key with multi-key rotation); alert acknowledgedBy set from authenticated user, not request body; four seeded demo users; frontend LoginView + auth Pinia store with proactive token refresh, 401 auto-retry, session expiry redirect, and logout button in header/sidebar/mobile nav; Vue router auth guard; RbacTests; run-phase31-verification.sh Done
23 Degraded Operations VisibilityGatewayStaleDetectorService background service auto-marks gateways OFFLINE when heartbeat exceeds configurable StaleThresholdMinutes; OperationsController exposes gateway fleet listing (GET /operations/gateways with status/site filters), gateway detail (GET /operations/gateways/{id}), and site summary (GET /operations/sites/{siteId}/summary); DischargeSummaryService with GET /encounters/{id}/discharge-summary (info) and GET /encounters/{id}/discharge-summary/content (MinIO PDF download); UsersController (GET /users, POST /users, PATCH /users/{id}) for admin user account management; frontend: GatewayOperations.vue operations dashboard, DegradedModeBanner.vue warning banner, DischargeSummaryPanel.vue on patient detail, ThresholdManagementView.vue with ThresholdFormModal.vue, UserManagementView.vue with UserFormModal.vue, AuditLogView.vue, ReconciliationView.vue; role-aware admin sidebar navigation; roleAccess.js composable; useChartTheme.js, useFocusTrap.js, useApiMode.js composables; CollapsibleSection.vue, SeverityBadge.vue UI components; OperationsApiTests; run-phase23-verification.sh Done
33 Alert Quality AnalyticsAlertFeedback entity with per-user-per-alert constraint; POST /alerts/{id}/feedback server-side feedback submission with alerts:feedback permission (Nurse, Physician, Admin); AlertQualityMetric entity stores per-alert-type quality snapshots (acknowledgement rate, false positive rate, useful rate, would-act rate, avg seconds to acknowledge/resolve); AlertQualityAggregatorService background service computes metrics periodically; AlertQualityMetricsController exposes GET /alerts/quality-metrics (time-range + alert type filter) and GET /alerts/quality-metrics/summary; AlertFeedbackConfiguration and AlertQualityMetricConfiguration EF Core configs; SubmitAlertFeedbackRequestValidator; Prometheus alert_quality_useful_rate and alert_quality_false_positive_rate gauges; Grafana alert-quality-dashboard.json; frontend AlertQualityAnalytics.vue with AlertQualityChart.vue and alertQuality Pinia store; AlertQualityAnalyticsTests; run-phase33-verification.sh Done
34 Explainable AlertsAlertExplanation value object (ScoreContributor, TrendContext, MedicationContext, NarrativeSummary); JSONB ClinicalAlert.Explanation column (immutable at creation); contributor builders for NEWS2, SOFA, GCS; TrendContextBuilder; AlertExplanationBuilder + ClinicalAlertFactory; NEWS2, SOFA, GCS, and TrendDetector wire explanation and include explanation in alert.generated outbox; MedicationCorrelationHelper.TryGetContextAsync() for structured medication context; AlertResponse DTO + AlertResponseMapper; GET/list/acknowledge/resolve return AlertResponse; ES indexer projects NarrativeSummary; data lake Parquet explanation_json; ward gateway LocalClinicalAlert.ExplanationJson + sync; dashboard AlertReasoning.vue + alertExplanation.js; simulator ExpectedOutcomeValidator with narrativeContains; ExplainableAlertsTests; run-phase34-verification.sh Done
35 MIMIC-IV Replay Scenario Generator — offline CLI tool in VigilCare.Simulator/Mimic/ that reads MIMIC-IV CSV files (100 patients, 140 ICU stays, 668K chart events, 107K lab events from docs/MIMIC-IV/) and generates standard VigilCare scenario JSONs; MimicCsvReader streaming CSV parser with header-index lookup and filter predicate (memory-efficient for large files); MimicItemMap maps 17 chart event items (vitals, GCS, FiO₂, PaO₂, labs) and 8 lab event items to VigilCare observation codes with GCS text-to-numeric fallback dictionary, Fahrenheit-to-Celsius conversion, and blood pressure priority (non-invasive preferred over arterial); MimicCareUnitMap maps care units to departments and generates tags; MimicDataLoader streams chartevents/labevents/prescriptions filtered by stay_id + item ID set; MimicScenarioBuilder deduplicates BP, enforces 10-observation-per-cluster limit with priority-based splitting, computes offsetMinutes, parses medication doses; MimicListCommand (mimic-list) displays Spectre.Console table of available stays; MimicGenerateCommand (mimic-generate) produces scenario JSON with --max-hours, --no-medications, --no-labs, --validate options; generated scenarios pass ScenarioValidator and replay through the standard replay command Done
36 In-app simulation runnerSimulation:Enabled gate; ScenarioCatalog + SimulationRunner hosted service; loopback API client; IsSimulated patient flag + filtered index; simulation_runs table; GET/POST /api/v1/simulation/* (config, scenarios, runs); simulation:run permission Done
37 Simulation control UI — dashboard Simulation page with scenario catalogue, speed control, run progress panel, SIM badge / simulation mode banner Done
38 Self-service clinical testing sessionssessions.json presets; multi-run session start (all-or-nothing, staggered); simulated-data purge + typed reset UI; scenario attribution on alert-quality feedback/CSV; clinical testing guide rewritten for self-service Done

Ward dashboard: backend APIs (GET /encounters ward list with extended summary fields including SOFA/GCS/attending/admitted-at/isSimulated, GET /qsofa/current, GET /qsofa/history, GET /gcs/history, GET /sepsis-bundles hospital-wide list, GET /operations/gateways fleet management, GET /users user management, GET /alerts/quality-metrics alert quality (+ /feedback with scenario attribution), GET /alerts/{id} with structured explanation, /api/v1/simulation/* when enabled, CORS) and frontend SPA — EncountersListTests, QsofaCurrentTests, GapAnalysisFixTests, OperationsApiTests, AlertQualityAnalyticsTests, ExplainableAlertsTests, simulation suite (SimulationRunnerTests, SimulationSessionTests, SimulationPurgeTests, SessionCatalogTests, AlertQualityScenarioAttributionTests), vigilcare-dashboard Vitest suite (replay scrubbing, feedback store, FeedbackButtons, FeedbackSummary, alert components, AlertReasoning, charts, ward table, ward sort, ward filter, department format, sepsis format, alert acknowledge, critical alert detect, handoff report, vitals form, GCS entry/history, SOFA panel/history, qSOFA history, scores panel, alert labels, PatientBanner, EncounterTimeline, medication chart markers, AcknowledgeModal, CriticalAlertBanner, DepartmentOverviewView, SepsisBoardView, VitalsEntryForm, useAlertStore, useWardStore, roleAccess, ThresholdManagementView, DischargeSummaryPanel, GatewayOperations, alertQuality, SimulationControlView, SessionCard, ResetWardPanel, ScenarioCard, SimulationRunPanel, SimulationModeBanner, useSimulationStore).

Enhanced Dashboard (post-Phase 22): Major dashboard feature expansion addressing clinical workflow gaps. Department Overview (/departments) — unit-level snapshot cards showing patient count, critical/alert/bundle totals per department with acuity distribution bars; click-through to ward filtered by department. Sepsis Bundle Board (/sepsis) — real-time bundle compliance tracking with countdown timers to 1-hour deadline, urgency-sorted (overdue → at-risk → on-track), live 1-second tick updates. Critical Alert NotificationsCriticalAlertBanner surfaces new critical alerts from polling cycle with audible 880Hz two-tone alert, browser title flash, and native Notification API integration; mute toggle persisted in settings. Shift Handoff ReportHandoffReport.vue generates SBAR-format (Situation, Background, Assessment, Recommendation) structured reports for all ward patients, enriched with latest vitals, open alerts, pending orders, and sepsis bundle status; ward summary with department stats; print/PDF export. Vitals Entry FormVitalsEntryForm.vue on patient detail page enables manual observation recording (7 vital parameters with AVPU dropdown) with client-side plausibility validation matching server-side ranges. Ward Table Enhancements — multi-column sorting (room, patient, department, NEWS2, qSOFA, sepsis, alerts) with sortable column headers, debounced patient search (name/MRN), quick-filter toggles (critical, has alerts, active sepsis), clear-all filters. Acknowledge Modal — role-aware acknowledgment with clinician identity pre-populated from JWT, role-specific guidance text, and acknowledgment note preview. Backend additions: GET /sepsis-bundles paginated hospital-wide list with SepsisBundleSummary (patient demographics, elements, deadlines); WardEncounterSummary extended with sofaScore, sofaDelta, gcsScore, gcsClassification, lastObservationAt, attendingPhysician, admittedAt.

Site & Gateway Registry (Phase 20): Central API manages clinical sites and ward edge nodes (gateways). Gateways authenticate via API key for heartbeat and sync upload. Shared VigilCare.ClinicalContracts class library defines sync DTOs consumed by both central API and ward gateway projects. Prometheus fleet health gauges track offline gateways and buffer depth per site.

Ward Gateway (Phase 21): VigilCare.WardGateway is a separate ASP.NET deployable with its own PostgreSQL (vigilcare_ward), Redis, and RabbitMQ — a local-first clinical path for ward edge nodes. Docker Compose services use the ward-gateway profile — start with docker compose --profile ward-gateway up -d. Observations are ingested locally with threshold evaluation and critical alert creation, then buffered for upload to the central API when the network link is available. Background services replicate encounter/patient data and alert thresholds from central on startup, report heartbeat status, and batch-upload buffered sync items. Local RabbitMQ provides ward-level paging and escalation independent of central connectivity. Integration tests (WardGatewayLocalPathTests, WardGatewayPartitionTests) validate the local ingest path and network partition/recovery workflow with Testcontainers.

Dashboard gap analysis (Phase 22): Addresses P0P1 clinical gaps identified in docs/dashboard-gap-analysis.md. SOFA, GCS, and qSOFA now have history charts matching the existing NEWS2 history pattern. Patient detail gains a demographic banner (age, blood type, allergies, emergency contact) and an encounter timeline merging status changes, observation summaries, and alerts into a single chronological view. Vital trend charts overlay medication administration markers so clinicians can correlate drug timing with vital changes. Backend additions: GET /gcs/history cursor-paginated endpoint, qsofa_evaluations table with GET /qsofa/history for evaluation persistence.

Scoring pipeline (Phases 2526): GCS components → gcs_scores + gcs.scored → SOFA CNS organ system; SOFA lab/vital observations → sofa_scores with baseline tracking → delta sepsis alerts when organ dysfunction worsens.

Sepsis-3 refactor (Phases 2729): SIRS removed; qSOFA repositioned as bedside screening (QSOFA_SCREEN); SOFA delta ≥ 2 triggers SOFA_SEPSIS → sepsis bundle. Frontend gains GCS entry form and SOFA score panel. Twelve simulator scenarios validate the full clinical pipeline end-to-end.

FHIR R4 integration (Phase 30): Inbound facade accepts FHIR R4 JSON from integration engines (Mirth Connect, Rhapsody). Supports per-resource endpoints and transaction Bundles for ADT admit workflows. LOINC/SNOMED code mapping, Fahrenheit conversion, and external identifier linking enable drop-in EHR integration without changing the internal clinical pipeline.

RBAC + audit logging + token refresh (Phase 31): JWT authentication with role-based permission gating on every endpoint. Four clinical roles with 18 granular permissions. Short-lived access tokens (15 min) paired with rotating opaque refresh tokens (7 days) stored in PostgreSQL — POST /auth/refresh rotates tokens, POST /auth/logout revokes server-side. Frontend auto-refreshes before expiry, retries on 401, and redirects to login on refresh failure; logout button in header, sidebar, and mobile nav. Append-only audit logging records who did what, when, and why — with before/after state snapshots for compliance and incident review, including USER_LOGOUT and TOKEN_REFRESHED actions.

Degraded Operations Visibility (Phase 23): Gateway fleet operations panel with stale gateway auto-detection (GatewayStaleDetectorService), discharge summary API with MinIO PDF retrieval, admin panels for user management, threshold management, audit log browsing, and reconciliation viewing. Frontend adds role-aware sidebar navigation, degraded-mode banner for offline gateways, and comprehensive admin CRUD views.

Alert Quality Analytics (Phase 33): Server-side clinician feedback persisted as AlertFeedback entities (one per user per alert, six feedback types). AlertQualityAggregatorService periodically computes per-alert-type quality metrics (acknowledgement rate, false positive rate, useful rate, would-act rate, response times). REST API exposes quality metric snapshots and aggregate summaries. Grafana dashboard visualizes alert quality trends. Frontend analytics view with quality charts. Phase 38 extends this with GET /alerts/quality-metrics/feedback scenario attribution (scenarioId / sessionId via SimulationRun.EncounterId join), scenario filter, by-scenario breakdown, and CSV columns.

Explainable Alerts (Phase 34): Composite alerts (NEWS2, SOFA, GCS, rapid deterioration) carry an immutable JSONB explanation snapshot at creation — score contributors with raw values and normal ranges, trend context (percent change, duration, direction), structured medication context, and a bedside NarrativeSummary. AlertResponse exposes explanation on GET/list/acknowledge/resolve. Downstream consumers (Elasticsearch indexer, data lake Parquet, ward gateway sync, Kafka alert.generated) propagate explanation without breaking legacy consumers. Dashboard AlertReasoning.vue renders structured reasoning. Simulator validates narrativeContains on key scenarios.

MIMIC-IV Replay Scenario Generator (Phase 35): Offline CLI tool that converts real de-identified ICU data from MIT's MIMIC-IV dataset into VigilCare scenario JSONs. mimic-list browses 140 ICU stays across 100 patients with demographics, care unit, LOS, and outcome. mimic-generate produces a scenario from a specific stay ID with options for duration capping, medication/lab exclusion, and inline validation. The streaming CSV parser handles 668K-row chartevents efficiently by filtering at the string level before allocating records. Item mappings cover 17 chart event items (vitals, GCS text-to-numeric, FiO₂, PaO₂, ICU labs) and 8 lab event items (creatinine, platelets, bilirubin, lactate, WBC, potassium, glucose, PaO₂). Blood pressure deduplication prefers non-invasive over arterial readings. The 10-observation-per-cluster limit is enforced by priority-based splitting (vitals first, labs spill to the next offset). Generated scenarios are structurally identical to hand-crafted ones and replay through the existing replay command (or in-app Simulation), driving NEWS2, SOFA, GCS, qSOFA, trend detection, and alerting on real patient trajectories.

Self-service clinical simulation (Phases 3638): Default-off in-app runner (Simulation:Enabled) loads scenario JSON and sessions.json presets, replays via loopback as simulation.runner, and marks patients IsSimulated. Dashboard Simulation page offers Sessions (AD presets with all-or-nothing staggered multi-run start), Scenarios catalogue, speed control, run progress, and typed ward reset that purges only simulated data (SIMULATION_DATA_PURGED audit). Alert Quality feedback joins to SimulationRun for scenario/session attribution. Clinical testing guide rewritten so doctors and nurses complete Sessions AD with no terminal; CLI remains the developer/CI tool (docs/simulator-guide.md §12). Roadmap: docs/plans/vigilcare-clinical-roadmap.md.

Post-phase hardening (after Phase 31):

  • FHIR R4 read/searchFhirReadController adds GET /fhir/R4/Patient/{id}, GET /fhir/R4/Patient (search by identifier), GET /fhir/R4/Encounter/{id}, GET /fhir/R4/Encounter (search by patient/status); new fhir:read permission for Admin and Integration roles; CapabilityStatement updated to advertise read and searchType interactions for Patient and Encounter
  • Alert threshold deletionDELETE /alert-thresholds/{id} with THRESHOLD_DELETED audit action and Redis cache invalidation
  • FHIR API key rotationFhir:ApiKeys array alongside existing Fhir:ApiKey for zero-downtime key rotation; constant-time comparison via CryptographicOperations.FixedTimeEquals prevents timing attacks
  • Authorization failure loggingPermissionAuthorizationHandler logs denied requests with structured details (username, user ID, role, required permission, endpoint); Prometheus authorization_failures_total counter with permission and role labels
  • JWT signing key validation — startup guard rejects keys shorter than 256 bits (HMAC-SHA256 minimum); prevents silent misconfiguration that would weaken token verification
  • Token refresh and revocationRefreshToken entity with DB-backed opaque token storage; POST /auth/refresh rotates access + refresh tokens (previous refresh token revoked on each use); POST /auth/logout revokes refresh token server-side; access token reduced from 8 hours to 15 minutes; refresh token valid for 7 days; frontend auto-refreshes 1 minute before expiry with 401 retry fallback; logout button in header, sidebar, and mobile nav with session redirect; USER_LOGOUT and TOKEN_REFRESHED audit actions
  • Concurrency hardeningSepsisBundleService.TryCreateBundleAsync wraps order + bundle creation in a single database transaction so the unique constraint rollback also reverts orphaned orders; PatientService.OpenEncounterAsync enforced by new partial unique index ix_encounters_patient_active_type on (patient_id, encounter_type) WHERE status = 'ACTIVE' with constraint-violation catch returning 409 Conflict; ConcurrencyTests validates parallel patient registration, sepsis bundle creation, observation idempotency, and encounter open race conditions
  • New Prometheus metricsfhir_read_total (resource_type, interaction, outcome), authorization_failures_total (permission, role)
  • New audit actionsTHRESHOLD_DELETED, AUTHORIZATION_DENIED, USER_LOGOUT, TOKEN_REFRESHED

Optional follow-up: execute and document the Kafka replay demonstration for the data lake (reset data-lake-writer offsets, clear MinIO prefixes, restart API, confirm Parquet rebuild). See docs/plans/phase-9-plan.md § Replay demonstration.

S
Description
No description provided
Readme
35 MiB
Languages
C# 69.3%
Vue 11.4%
JavaScript 9.7%
Shell 9.2%
Dockerfile 0.2%
Other 0.1%