update readme and create gap document
This commit is contained in:
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
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.
|
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:** Twenty-five planned phases are complete through Phase 30 — 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**, and the **FHIR R4 Inbound Facade** for EHR integration. See [Implemented Phases](#implemented-phases) for the full breakdown. Guides: [dashboard-guide.md](docs/dashboard-guide.md) (technical), [clinical-testing-guide.md](docs/clinical-testing-guide.md) (doctors & nurses).
|
**Implementation status:** Twenty-six planned phases are complete through Phase 31 — 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 **FHIR R4 Inbound Facade** for EHR integration, and **Role-Based Access Control (RBAC) with clinical audit logging**. See [Implemented Phases](#implemented-phases) for the full breakdown. Guides: [dashboard-guide.md](docs/dashboard-guide.md) (technical), [clinical-testing-guide.md](docs/clinical-testing-guide.md) (doctors & nurses).
|
||||||
|
|
||||||
## Domain Model — How It Maps to a Real Clinical System
|
## 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 or through the FHIR R4 inbound facade that maps HL7 FHIR resources from integration engines (Mirth Connect, Rhapsody) into the internal domain. 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 acknowledge and resolve alerts. If a critical alert goes unacknowledged for five minutes, the system escalates to the on-call backup. All events flow through Kafka so the Elasticsearch dashboard, scoring engines, and data lake writer consume the same stream independently.
|
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 or through the FHIR R4 inbound facade that maps HL7 FHIR resources from integration engines (Mirth Connect, Rhapsody) into the internal domain. 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, 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
|
Patient ─────────────────────────── one patient = one MRN, many lifetime encounters
|
||||||
@@ -74,6 +74,8 @@ An `OutboxEvent` is written in the same transaction as any observation or alert,
|
|||||||
- **Ward Dashboard APIs** — `GET /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); filterable by `status` and `department`; `GET /encounters/:id/qsofa/current` exposes Redis-backed qSOFA state; CORS policy `Dashboard` allows configured origins (default `http://localhost:5173`)
|
- **Ward Dashboard APIs** — `GET /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); filterable by `status` and `department`; `GET /encounters/:id/qsofa/current` exposes Redis-backed qSOFA state; CORS policy `Dashboard` allows configured origins (default `http://localhost:5173`)
|
||||||
- **Ward Dashboard Frontend** — Vue 3 SPA (`vigilcare-dashboard/`) with virtual ward table (NEWS2-sorted), patient detail (vitals, scores, alerts, orders, sepsis bundle, GCS entry form, SOFA score panel), alert center (global acknowledge/resolve), vital sign trend charts with local replay scrubbing, NEWS2 history chart, alert reasoning with optional medication context, and clinician feedback on every alert; polls API every 5–10 s; guides in `docs/dashboard-guide.md` and `docs/clinical-testing-guide.md`
|
- **Ward Dashboard Frontend** — Vue 3 SPA (`vigilcare-dashboard/`) with virtual ward table (NEWS2-sorted), patient detail (vitals, scores, alerts, orders, sepsis bundle, GCS entry form, SOFA score panel), alert center (global acknowledge/resolve), vital sign trend charts with local replay scrubbing, NEWS2 history chart, alert reasoning with optional medication context, and clinician feedback on every alert; polls API every 5–10 s; guides in `docs/dashboard-guide.md` and `docs/clinical-testing-guide.md`
|
||||||
- **FHIR R4 Inbound Facade** — `POST /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; `FhirApiKeyMiddleware` authenticates via `X-Api-Key` header; `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 Inbound Facade** — `POST /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; `FhirApiKeyMiddleware` authenticates via `X-Api-Key` header; `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`
|
||||||
|
- **Role-Based Access Control (RBAC)** — JWT bearer authentication (`POST /auth/login`); four clinical roles (`Nurse`, `Physician`, `Admin`, `Integration`) with 16 granular permissions (`patients:read`, `alerts:acknowledge`, `thresholds:write`, `fhir:ingest`, `audit:read`, etc.); `AuthorizePermission` attribute on every controller action; `PermissionAuthorizationHandler` resolves role → permission at runtime from `ClinicalRolePermissionMap`; `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`, `audit:read`, and `users:admin`; integration accounts get write-only access for FHIR ingest; 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; four seeded demo users (`nurse.demo`, `physician.demo`, `admin.demo`, `integration.mirth`); frontend login page with `localStorage` token persistence and automatic `Authorization: Bearer` header injection
|
||||||
|
- **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; eight audit actions (`THRESHOLD_CREATED`, `THRESHOLD_UPDATED`, `ALERT_ACKNOWLEDGED`, `ALERT_RESOLVED`, `ENCOUNTER_STATUS_CHANGED`, `PATIENT_REGISTERED`, `SUPPRESSION_WINDOW_SET`, `USER_LOGIN`); `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
|
||||||
- **Clinician Feedback Mode** — six quick ratings per alert (useful, too early, too late, false positive, missing context, would act); optional notes; Feedback Summary with aggregate stats and JSON/CSV export; client-side persistence for product research
|
- **Clinician Feedback Mode** — six quick ratings per alert (useful, too early, too late, false positive, missing context, would act); optional notes; Feedback Summary with aggregate stats and JSON/CSV export; client-side persistence for product research
|
||||||
- **Console Replay Simulator** — standalone `VigilCare.Simulator` .NET console app replays JSON scenario files against the live API with configurable speed (`--speed 0` instant, `60` = 60× faster); commands: `replay`, `replay-all`, `validate`, `dry-run`; optional `--poll` shows alerts, NEWS2, GCS, SOFA, and sepsis bundle state during replay; eleven sample scenarios in `VigilCare.Simulator/Scenarios/List/` (including GCS neurological decline, SOFA sepsis progression, and SpO₂/FiO₂ fallback); user guide in `docs/simulator-guide.md`
|
- **Console Replay Simulator** — standalone `VigilCare.Simulator` .NET console app replays JSON scenario files against the live API with configurable speed (`--speed 0` instant, `60` = 60× faster); commands: `replay`, `replay-all`, `validate`, `dry-run`; optional `--poll` shows alerts, NEWS2, GCS, SOFA, and sepsis bundle state during replay; eleven sample scenarios in `VigilCare.Simulator/Scenarios/List/` (including GCS neurological decline, SOFA sepsis progression, and SpO₂/FiO₂ fallback); user guide in `docs/simulator-guide.md`
|
||||||
- **RabbitMQ Notification Workers** — `NotificationPublisherService` 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`
|
- **RabbitMQ Notification Workers** — `NotificationPublisherService` 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`
|
||||||
@@ -90,11 +92,14 @@ An `OutboxEvent` is written in the same transaction as any observation or alert,
|
|||||||
|
|
||||||
```
|
```
|
||||||
HTTP request
|
HTTP request
|
||||||
→ FhirApiKeyMiddleware (X-Api-Key guard for /fhir/* routes)
|
→ FhirApiKeyOrJwtMiddleware (X-Api-Key or JWT bearer for /fhir/* routes)
|
||||||
→ CorrelationIdMiddleware
|
→ CorrelationIdMiddleware
|
||||||
→ ExceptionHandlerMiddleware
|
→ ExceptionHandlerMiddleware
|
||||||
|
→ JWT Authentication + RBAC (PermissionAuthorizationHandler)
|
||||||
→ Controllers (REST API + FHIR R4 ingest)
|
→ Controllers (REST API + FHIR R4 ingest)
|
||||||
→ Services
|
→ Services
|
||||||
|
├── CurrentUserService (authenticated user identity from JWT claims)
|
||||||
|
├── AuditService (append-only clinical_audit_logs on write actions)
|
||||||
├── PostgreSQL (EF Core — writes, keyed reads)
|
├── PostgreSQL (EF Core — writes, keyed reads)
|
||||||
├── Redis (threshold cache, qSOFA state, NEWS2 parameter state, GCS state, SOFA lab cache, trend history, alert suppression keys)
|
├── Redis (threshold cache, qSOFA state, NEWS2 parameter state, GCS state, SOFA lab cache, trend history, alert suppression keys)
|
||||||
└── OutboxEvent (same transaction as domain write)
|
└── OutboxEvent (same transaction as domain write)
|
||||||
@@ -145,6 +150,8 @@ IHostedServices (background):
|
|||||||
| Metrics | prometheus-net.AspNetCore (`GET /metrics`) |
|
| Metrics | prometheus-net.AspNetCore (`GET /metrics`) |
|
||||||
| Dashboards | Prometheus 2.52 + Grafana 10.4 |
|
| Dashboards | Prometheus 2.52 + Grafana 10.4 |
|
||||||
| Data lake format | Parquet.Net 4.x |
|
| 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) |
|
| FHIR | Hl7.Fhir.R4 (Firely SDK — parsing, serialization, model) |
|
||||||
| Docs | Swagger / OpenAPI (Swashbuckle) |
|
| Docs | Swagger / OpenAPI (Swashbuckle) |
|
||||||
| Validation | FluentValidation.AspNetCore |
|
| Validation | FluentValidation.AspNetCore |
|
||||||
@@ -160,6 +167,8 @@ VigilCareClinicalAPI/
|
|||||||
├── Program.cs # Service registration, middleware, seed on startup
|
├── Program.cs # Service registration, middleware, seed on startup
|
||||||
├── appsettings.json # Connection strings, Kafka, Elasticsearch, RabbitMQ, MinIO, Serilog, ReconciliationJobs
|
├── appsettings.json # Connection strings, Kafka, Elasticsearch, RabbitMQ, MinIO, Serilog, ReconciliationJobs
|
||||||
├── Controllers/
|
├── Controllers/
|
||||||
|
│ ├── AuthController.cs # JWT login + authenticated user profile (GET /auth/me)
|
||||||
|
│ ├── AuditLogsController.cs # Clinical audit log query (Admin only)
|
||||||
│ ├── PatientsController.cs # Patient CRUD, search by name/MRN
|
│ ├── PatientsController.cs # Patient CRUD, search by name/MRN
|
||||||
│ ├── EncountersController.cs # Encounter list (ward summary), get, status PATCH, timeline
|
│ ├── EncountersController.cs # Encounter list (ward summary), get, status PATCH, timeline
|
||||||
│ ├── MedicationsController.cs # Medication administration create, list, get
|
│ ├── MedicationsController.cs # Medication administration create, list, get
|
||||||
@@ -191,9 +200,13 @@ VigilCareClinicalAPI/
|
|||||||
│ │ ├── SepsisBundle.cs # Four-element treatment bundle with 1-hour compliance deadline
|
│ │ ├── SepsisBundle.cs # Four-element treatment bundle with 1-hour compliance deadline
|
||||||
│ │ ├── SepsisBundleElement.cs # Individual bundle element linked to a clinical order
|
│ │ ├── SepsisBundleElement.cs # Individual bundle element linked to a clinical order
|
||||||
│ │ ├── MedicationAdministration.cs # Drug administration record per encounter
|
│ │ ├── MedicationAdministration.cs # Drug administration record per encounter
|
||||||
│ │ └── ExternalResourceIdentifier.cs # Links external system identifiers (MRN, visit#) to internal UUIDs
|
│ │ ├── ExternalResourceIdentifier.cs # Links external system identifiers (MRN, visit#) to internal UUIDs
|
||||||
|
│ │ ├── ClinicalUser.cs # Username, BCrypt password hash, display name, role, active flag
|
||||||
|
│ │ └── ClinicalAuditLog.cs # Append-only audit trail: action, entity, user, before/after JSONB, IP, correlation ID
|
||||||
│ └── Enums/
|
│ └── Enums/
|
||||||
│ ├── EncounterStatus.cs # Scheduled, Active, Discharged, Cancelled
|
│ ├── EncounterStatus.cs # Scheduled, Active, Discharged, Cancelled
|
||||||
|
│ ├── ClinicalRole.cs # Nurse, Physician, Admin, Integration
|
||||||
|
│ ├── AuditAction.cs # ThresholdCreated/Updated, AlertAcknowledged/Resolved, EncounterStatusChanged, PatientRegistered, SuppressionWindowSet, UserLogin
|
||||||
│ ├── EncounterType.cs # Inpatient, Outpatient, Emergency
|
│ ├── EncounterType.cs # Inpatient, Outpatient, Emergency
|
||||||
│ ├── AlertSeverity.cs # Warning, Critical
|
│ ├── AlertSeverity.cs # Warning, Critical
|
||||||
│ ├── AlertStatus.cs # Open, Acknowledged, Resolved, Escalated
|
│ ├── AlertStatus.cs # Open, Acknowledged, Resolved, Escalated
|
||||||
@@ -225,8 +238,18 @@ VigilCareClinicalAPI/
|
|||||||
│ ├── FhirExceptionFilter.cs # Converts exceptions to FHIR OperationOutcome responses
|
│ ├── FhirExceptionFilter.cs # Converts exceptions to FHIR OperationOutcome responses
|
||||||
│ ├── FhirMappingException.cs # Typed exception for FHIR mapping failures
|
│ ├── FhirMappingException.cs # Typed exception for FHIR mapping failures
|
||||||
│ └── FhirOperationOutcomeBuilder.cs # Builds FHIR OperationOutcome from exceptions and error codes
|
│ └── FhirOperationOutcomeBuilder.cs # Builds FHIR OperationOutcome from exceptions and error codes
|
||||||
|
├── Authorization/
|
||||||
|
│ ├── AuthorizePermissionAttribute.cs # [AuthorizePermission("patients:read")] attribute
|
||||||
|
│ ├── ClinicalPermissions.cs # 16 permission constants (patients:read, thresholds:write, 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
|
||||||
├── Services/
|
├── Services/
|
||||||
│ ├── Interfaces/ # IPatientService, IEncounterService, …
|
│ ├── Interfaces/ # IPatientService, IEncounterService, IAuditService, IAuthService, ICurrentUserService, …
|
||||||
|
│ ├── AuthService.cs # Login (BCrypt verify), JWT generation, login audit log
|
||||||
|
│ ├── 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
|
│ ├── PatientService.cs
|
||||||
│ ├── EncounterService.cs # Status state machine + ConflictException on invalid transitions
|
│ ├── EncounterService.cs # Status state machine + ConflictException on invalid transitions
|
||||||
│ ├── AlertThresholdService.cs # CRUD + Redis write-through invalidation
|
│ ├── AlertThresholdService.cs # CRUD + Redis write-through invalidation
|
||||||
@@ -289,6 +312,7 @@ VigilCareClinicalAPI/
|
|||||||
│ ├── ReconciliationJobOptions.cs
|
│ ├── ReconciliationJobOptions.cs
|
||||||
│ ├── MedicationCorrelationOptions.cs # Drug-vital mappings + correlation window
|
│ ├── MedicationCorrelationOptions.cs # Drug-vital mappings + correlation window
|
||||||
│ ├── FhirOptions.cs # API key, identifier systems, department/class maps, defaults
|
│ ├── FhirOptions.cs # API key, identifier systems, department/class maps, defaults
|
||||||
|
│ ├── JwtOptions.cs # Issuer, audience, signing key, expiration (default 8 hours)
|
||||||
│ └── DashboardOptions.cs # CORS origins for ward dashboard frontend
|
│ └── DashboardOptions.cs # CORS origins for ward dashboard frontend
|
||||||
├── Sepsis/
|
├── Sepsis/
|
||||||
│ ├── AlertCreationGuard.cs # Prevents creation of deprecated alert types (SEPSIS_WARNING)
|
│ ├── AlertCreationGuard.cs # Prevents creation of deprecated alert types (SEPSIS_WARNING)
|
||||||
@@ -329,8 +353,10 @@ VigilCareClinicalAPI/
|
|||||||
│ └── Sepsis/QsofaResult.cs, QsofaOutcome.cs # qSOFA detector result and screening outcome enum
|
│ └── Sepsis/QsofaResult.cs, QsofaOutcome.cs # qSOFA detector result and screening outcome enum
|
||||||
├── Data/
|
├── Data/
|
||||||
│ ├── AppDbContext.cs # EF Core context — entity configs, indexes, constraints
|
│ ├── AppDbContext.cs # EF Core context — entity configs, indexes, constraints
|
||||||
│ ├── Configurations/ # IEntityTypeConfiguration per entity; ElasticsearchOptions, ElasticIndexOptions
|
│ ├── Configurations/ # IEntityTypeConfiguration per entity; ClinicalUserConfiguration, ClinicalAuditLogConfiguration; ElasticsearchOptions, ElasticIndexOptions
|
||||||
│ └── Seed/DataSeeder.cs # Seeds patients, encounters, thresholds, observations
|
│ └── Seed/
|
||||||
|
│ ├── DataSeeder.cs # Seeds patients, encounters, thresholds, observations
|
||||||
|
│ └── UserSeeder.cs # Seeds four demo users (nurse, physician, admin, integration)
|
||||||
├── Common/
|
├── Common/
|
||||||
│ ├── ApiResponse.cs # { success, statusCode, data, error } envelope
|
│ ├── ApiResponse.cs # { success, statusCode, data, error } envelope
|
||||||
│ ├── PagedResult.cs / CursorPage.cs
|
│ ├── PagedResult.cs / CursorPage.cs
|
||||||
@@ -340,7 +366,7 @@ VigilCareClinicalAPI/
|
|||||||
│ ├── DomainException.cs
|
│ ├── DomainException.cs
|
||||||
│ └── ValidationException.cs
|
│ └── ValidationException.cs
|
||||||
├── Middlewares/
|
├── Middlewares/
|
||||||
│ ├── FhirApiKeyMiddleware.cs # X-Api-Key guard for /fhir/* routes; returns OperationOutcome on 401
|
│ ├── FhirApiKeyOrJwtMiddleware.cs # Dual auth for /fhir/* routes: JWT bearer or X-Api-Key → Integration identity
|
||||||
│ ├── CorrelationIdMiddleware.cs
|
│ ├── CorrelationIdMiddleware.cs
|
||||||
│ └── ExceptionHandlerMiddleware.cs
|
│ └── ExceptionHandlerMiddleware.cs
|
||||||
└── Migrations/
|
└── Migrations/
|
||||||
@@ -382,6 +408,8 @@ tests/
|
|||||||
├── QsofaCurrentTests.cs # qSOFA current API — Redis state, criteria breakdown
|
├── QsofaCurrentTests.cs # qSOFA current API — Redis state, criteria breakdown
|
||||||
├── GcsScoringTests.cs # GCS component scoring, alerts, NEWS2/qSOFA integration paths
|
├── GcsScoringTests.cs # GCS component scoring, alerts, NEWS2/qSOFA integration paths
|
||||||
├── SofaScoringTests.cs # SOFA organ scores, baseline, delta alerts, carry-forward, vasopressors
|
├── SofaScoringTests.cs # SOFA organ scores, baseline, delta alerts, carry-forward, vasopressors
|
||||||
|
├── Auth/
|
||||||
|
│ └── RbacTests.cs # RBAC — unauthenticated 401, nurse 403 on threshold write, admin audit log creation
|
||||||
└── Fhir/
|
└── Fhir/
|
||||||
└── FhirIngestTests.cs # FHIR R4 patient upsert idempotency, observation LOINC mapping, unknown code 422, transaction bundle
|
└── FhirIngestTests.cs # FHIR R4 patient upsert idempotency, observation LOINC mapping, unknown code 422, transaction bundle
|
||||||
|
|
||||||
@@ -395,13 +423,13 @@ VigilCare.Simulator/ # Phase 16 — console replay
|
|||||||
├── Scenarios/ # schema.json, ScenarioLoader, ScenarioValidator
|
├── Scenarios/ # schema.json, ScenarioLoader, ScenarioValidator
|
||||||
└── Scenarios/List/ # Eleven sample scenarios (sepsis, GCS, SOFA, NEWS2, stable, …)
|
└── Scenarios/List/ # Eleven sample scenarios (sepsis, GCS, SOFA, NEWS2, stable, …)
|
||||||
|
|
||||||
vigilcare-dashboard/ # Phases 17–19, 27–28 — Vue 3 ward dashboard SPA
|
vigilcare-dashboard/ # Phases 17–19, 27–28, 31 — Vue 3 ward dashboard SPA
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── api/ # HTTP client, encounters, clinical (GCS, SOFA), alerts, normalize
|
│ ├── api/ # HTTP client (auto Bearer header), encounters, clinical (GCS, SOFA), alerts, normalize
|
||||||
│ ├── components/ # charts, replay, alerts, feedback, patient (GcsEntryForm, SofaScorePanel), ward, layout, ui
|
│ ├── components/ # charts, replay, alerts, feedback, patient (GcsEntryForm, SofaScorePanel), ward, layout, ui
|
||||||
│ ├── composables/ # useChartData, useReplayControls, usePolling, useFeedback, useGcs, useSofa, chartFormat
|
│ ├── composables/ # useChartData, useReplayControls, usePolling, useFeedback, useGcs, useSofa, chartFormat
|
||||||
│ ├── stores/ # Pinia — ward, alerts, settings, feedback, scoring (localStorage)
|
│ ├── stores/ # Pinia — ward, alerts, settings, feedback, scoring, auth (localStorage token + user)
|
||||||
│ ├── views/ # WardDashboard, PatientDetail, AlertCenter, FeedbackSummary
|
│ ├── views/ # LoginView, WardDashboard, PatientDetail, AlertCenter, FeedbackSummary
|
||||||
│ └── __tests__/ # Vitest — tests (store, feedback, replay, charts, alerts, ward, GCS, SOFA)
|
│ └── __tests__/ # Vitest — tests (store, feedback, replay, charts, alerts, ward, GCS, SOFA)
|
||||||
├── vite.config.js
|
├── vite.config.js
|
||||||
└── README.md # Dev quick start → docs/dashboard-guide.md
|
└── README.md # Dev quick start → docs/dashboard-guide.md
|
||||||
@@ -426,7 +454,8 @@ scripts/
|
|||||||
├── run-phase27-verification.sh # Phase 27 — Sepsis-3 refactor: SIRS removal, QSOFA_SCREEN, SOFA bundle trigger
|
├── 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-phase28-verification.sh # Phase 28 — Frontend GCS entry + SOFA display + sepsis UI refactor
|
||||||
├── run-phase29-verification.sh # Phase 29 — Simulator scenario expansion + clinical validation
|
├── 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-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
|
||||||
|
|
||||||
docs/
|
docs/
|
||||||
├── plans/ # Phase implementation and verification guides
|
├── plans/ # Phase implementation and verification guides
|
||||||
@@ -571,7 +600,7 @@ dotnet run
|
|||||||
|
|
||||||
On startup the application:
|
On startup the application:
|
||||||
1. Runs EF Core migrations
|
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, and sample observations
|
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
|
3. Pre-loads all thresholds into Redis
|
||||||
4. Provisions Kafka topics and Elasticsearch indices
|
4. Provisions Kafka topics and Elasticsearch indices
|
||||||
5. Declares the RabbitMQ exchange and queue topology
|
5. Declares the RabbitMQ exchange and queue topology
|
||||||
@@ -602,7 +631,7 @@ npm install
|
|||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Open `http://localhost:5173` — **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** (`/feedback`).
|
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** (`/feedback`).
|
||||||
|
|
||||||
Replay a simulator scenario in another terminal to watch charts and alerts populate in real time. Run dashboard tests with `cd vigilcare-dashboard && npm test`. See `docs/dashboard-guide.md` for technical documentation and `docs/clinical-testing-guide.md` for structured clinician evaluation sessions.
|
Replay a simulator scenario in another terminal to watch charts and alerts populate in real time. Run dashboard tests with `cd vigilcare-dashboard && npm test`. See `docs/dashboard-guide.md` for technical documentation and `docs/clinical-testing-guide.md` for structured clinician evaluation sessions.
|
||||||
|
|
||||||
@@ -645,6 +674,7 @@ Integration tests use `WebApplicationFactory` with a `Testing` environment and T
|
|||||||
| `GcsScoringTests` | 25 | GCS component scoring, classification, alerts, CNS integration with SOFA |
|
| `GcsScoringTests` | 25 | GCS component scoring, classification, alerts, CNS integration with SOFA |
|
||||||
| `SofaScoringTests` | 26 | SOFA organ scores, baseline eligibility, delta alerts, carry-forward, vasopressors |
|
| `SofaScoringTests` | 26 | SOFA organ scores, baseline eligibility, delta alerts, carry-forward, vasopressors |
|
||||||
| `FhirIngestTests` | 30 | FHIR R4 patient upsert idempotency, LOINC observation mapping, unknown code 422, transaction bundle |
|
| `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 |
|
||||||
|
|
||||||
### Verification Scripts
|
### Verification Scripts
|
||||||
|
|
||||||
@@ -661,6 +691,7 @@ With the API running (`dotnet run`) and Docker Compose up:
|
|||||||
./scripts/run-phase15-verification.sh # Medication administration + correlation annotation pipeline
|
./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-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-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
|
||||||
```
|
```
|
||||||
|
|
||||||
Phase 25 — GCS scoring (requires running API + Docker Compose; set an active encounter UUID):
|
Phase 25 — GCS scoring (requires running API + Docker Compose; set an active encounter UUID):
|
||||||
@@ -691,6 +722,12 @@ Phase 15 unit/integration tests only:
|
|||||||
dotnet test --filter "FullyQualifiedName~Medication"
|
dotnet test --filter "FullyQualifiedName~Medication"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Phase 31 RBAC tests only:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet test --filter "FullyQualifiedName~Rbac"
|
||||||
|
```
|
||||||
|
|
||||||
Per-phase test runners (subset of `dotnet test`):
|
Per-phase test runners (subset of `dotnet test`):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -1066,9 +1103,78 @@ Bundles are created automatically by `SepsisAlertHandler` when a `SOFA_SEPSIS` a
|
|||||||
|
|
||||||
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`.
|
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`.
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
| Method | Path | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| POST | `/auth/login` | Authenticate with username/password; returns JWT bearer token |
|
||||||
|
| GET | `/auth/me` | 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 |
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `accessToken` | string | JWT bearer token |
|
||||||
|
| `expiresAt` | DateTimeOffset | Token expiration (default 8 hours) |
|
||||||
|
| `userId` | Guid | User ID |
|
||||||
|
| `username` | string | Username |
|
||||||
|
| `displayName` | string | Display name |
|
||||||
|
| `role` | string | `NURSE`, `PHYSICIAN`, `ADMIN`, `INTEGRATION` |
|
||||||
|
|
||||||
|
**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 | — |
|
||||||
|
| `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 |
|
||||||
|
| `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`, `ALERT_ACKNOWLEDGED`, `ALERT_RESOLVED`, `ENCOUNTER_STATUS_CHANGED`, `PATIENT_REGISTERED`, `SUPPRESSION_WINDOW_SET`, `USER_LOGIN`
|
||||||
|
|
||||||
|
Each audit log entry includes `action`, `entityType`, `entityId`, `userId`, `userDisplayName`, `previousValueJson` (JSONB), `newValueJson` (JSONB), `reason`, `ipAddress`, `correlationId`, and `createdAt`.
|
||||||
|
|
||||||
### FHIR R4 Ingest
|
### FHIR R4 Ingest
|
||||||
|
|
||||||
All FHIR endpoints are under `/fhir/R4`, accept `application/fhir+json`, and return FHIR R4 JSON responses. Authentication is via `X-Api-Key` header (configured in `Fhir:ApiKey`; disabled when blank). Errors return a FHIR `OperationOutcome` with appropriate issue codes.
|
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`; disabled when blank). 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 ingest FHIR resources. Errors return a FHIR `OperationOutcome` with appropriate issue codes.
|
||||||
|
|
||||||
| Method | Path | Description |
|
| Method | Path | Description |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -1323,6 +1429,40 @@ createdAt DateTimeOffset
|
|||||||
|
|
||||||
Unique index: `(resource_type, system, value)` — one mapping per external identifier
|
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?
|
||||||
|
```
|
||||||
|
|
||||||
|
### ClinicalAuditLog
|
||||||
|
|
||||||
|
```
|
||||||
|
id Guid PK
|
||||||
|
action string required (max 50) — THRESHOLD_CREATED | THRESHOLD_UPDATED | ALERT_ACKNOWLEDGED | ALERT_RESOLVED | ENCOUNTER_STATUS_CHANGED | PATIENT_REGISTERED | SUPPRESSION_WINDOW_SET | USER_LOGIN
|
||||||
|
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)`
|
||||||
|
|
||||||
### ReconciliationAlert
|
### ReconciliationAlert
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -1591,7 +1731,7 @@ Observation history uses cursor pagination on `(recorded_at DESC, id DESC)`. Off
|
|||||||
|
|
||||||
## Implemented Phases
|
## Implemented Phases
|
||||||
|
|
||||||
Twenty-five phases from the project roadmap are implemented and verified, including the **Sepsis-3 clinical refactor** (Phases 27–29) and the **FHIR R4 Inbound Facade** (Phase 30). Integration tests (`dotnet test`) and per-phase verification scripts cover Phases 8–15, 25–30. Phases 17–19 add the Vue dashboard and clinician feedback (Vitest in `vigilcare-dashboard/`).
|
Twenty-six phases from the project roadmap are implemented and verified, including the **Sepsis-3 clinical refactor** (Phases 27–29), the **FHIR R4 Inbound Facade** (Phase 30), and **RBAC with clinical audit logging** (Phase 31). Integration tests (`dotnet test`) and per-phase verification scripts cover Phases 8–15, 25–31. Phases 17–19 add the Vue dashboard and clinician feedback (Vitest in `vigilcare-dashboard/`).
|
||||||
|
|
||||||
| Phase | Feature | Status |
|
| Phase | Feature | Status |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -1620,6 +1760,7 @@ Twenty-five phases from the project roadmap are implemented and verified, includ
|
|||||||
| 28 | **Frontend GCS + SOFA + sepsis UI refactor** — `GcsEntryForm.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 |
|
| 28 | **Frontend GCS + SOFA + sepsis UI refactor** — `GcsEntryForm.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 |
|
| 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 Facade** — `FhirIngestController` (`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 |
|
| 30 | **FHIR R4 Inbound Facade** — `FhirIngestController` (`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** — JWT bearer authentication (`AuthService`, `AuthController`); four clinical roles (`Nurse`, `Physician`, `Admin`, `Integration`) with 16 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; `ClinicalAuditLog` append-only table with before/after JSONB, user identity, IP, and correlation ID; `AuditService` writes log entries on clinical write actions (8 audit actions); `AuditLogsController` admin-only query with filters; `FhirApiKeyOrJwtMiddleware` dual auth for FHIR routes (JWT or X-Api-Key); alert `acknowledgedBy` set from authenticated user, not request body; four seeded demo users; frontend `LoginView` + `auth` Pinia store with `localStorage` token persistence; Vue router auth guard; `RbacTests`; `run-phase31-verification.sh` | Done |
|
||||||
|
|
||||||
**Ward dashboard:** backend APIs (`GET /encounters` ward list, `GET /qsofa/current`, CORS) and frontend SPA — `EncountersListTests`, `QsofaCurrentTests`, `vigilcare-dashboard` Vitest suite (replay scrubbing, feedback store, FeedbackButtons, FeedbackSummary, alert components, charts, ward table, GCS entry, SOFA panel, scores panel, alert labels).
|
**Ward dashboard:** backend APIs (`GET /encounters` ward list, `GET /qsofa/current`, CORS) and frontend SPA — `EncountersListTests`, `QsofaCurrentTests`, `vigilcare-dashboard` Vitest suite (replay scrubbing, feedback store, FeedbackButtons, FeedbackSummary, alert components, charts, ward table, GCS entry, SOFA panel, scores panel, alert labels).
|
||||||
|
|
||||||
@@ -1629,4 +1770,6 @@ Twenty-five phases from the project roadmap are implemented and verified, includ
|
|||||||
|
|
||||||
**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.
|
**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 (Phase 31):** JWT authentication with role-based permission gating on every endpoint. Four clinical roles with granular permissions. Append-only audit logging records who did what, when, and why — with before/after state snapshots for compliance and incident review. Frontend login page with token-based session management.
|
||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|||||||
@@ -0,0 +1,876 @@
|
|||||||
|
# VigilCare Clinical Platform — Gap Analysis
|
||||||
|
|
||||||
|
Comprehensive gap analysis of the VigilCareClinical system covering data integrity, API surface, infrastructure reliability, security posture, observability, and test coverage. Items are ordered by **impact on correctness and patient safety first**, then **operational reliability**, then **API completeness**, then **observability and polish**.
|
||||||
|
|
||||||
|
Each item includes **why** it matters and **how** to fix it at an implementation-ready level.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Priority legend
|
||||||
|
|
||||||
|
| Tier | Meaning |
|
||||||
|
|------|---------|
|
||||||
|
| **P0** | Data integrity or clinical correctness bug; fix before expanding clinical workflows |
|
||||||
|
| **P1** | Silent misconfiguration, transaction safety hole, or data loss risk; fix soon after P0 |
|
||||||
|
| **P2** | Blocks common admin/integration workflows or degrades operational reliability |
|
||||||
|
| **P3** | Security hardening, compliance, or defense-in-depth; no immediate patient-safety impact |
|
||||||
|
| **P4** | API completeness, consistency, and developer experience |
|
||||||
|
| **P5** | Observability and test coverage; does not change clinical outcomes but makes incidents diagnosable |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Part A — Data Integrity & Correctness
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P0 — MRN generation race condition
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
|
||||||
|
`PatientService.RegisterAsync` generates MRNs via `MRN-{count+1:D6}` where `count` is a `SELECT COUNT(*)`. Two concurrent registrations can read the same count and generate duplicate MRNs. The unique index on `Patient.Mrn` catches this at the database level, but the exception surfaces as an unhandled `DbUpdateException`, not a controlled retry or user-friendly error.
|
||||||
|
|
||||||
|
### Why fix
|
||||||
|
|
||||||
|
MRN is the primary patient identifier across clinical systems. Duplicate MRN attempts that surface as 500 errors during FHIR bulk-import or concurrent admissions will halt ingest pipelines and require manual intervention.
|
||||||
|
|
||||||
|
### How to fix
|
||||||
|
|
||||||
|
1. **Replace count-based generation** with a PostgreSQL sequence: `CREATE SEQUENCE mrn_seq START WITH 1 INCREMENT BY 1`.
|
||||||
|
2. In `PatientService.RegisterAsync`, call `SELECT nextval('mrn_seq')` to get the next MRN atomically.
|
||||||
|
3. Format as `MRN-{sequence:D6}`.
|
||||||
|
4. Extract MRN prefix/format to `PatientOptions` for configurability.
|
||||||
|
5. Handle `DbUpdateException` with unique violation check as a fallback (retry once with next sequence value).
|
||||||
|
|
||||||
|
**Files:** `PatientService.cs:197-200`, new migration for `mrn_seq`, optional `PatientOptions.cs`.
|
||||||
|
|
||||||
|
**Dependency:** None.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P0 — Sepsis bundle creation race condition (TOCTOU)
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
|
||||||
|
`SepsisBundleService.CreateAsync` checks `AnyAsync(b => b.EncounterId == encounterId && b.ComplianceStatus == InProgress)` before inserting a new bundle. Two SOFA_SEPSIS alerts arriving simultaneously for the same encounter can both pass this check and create duplicate bundles, resulting in duplicate sepsis bundle elements and compliance tracking.
|
||||||
|
|
||||||
|
### Why fix
|
||||||
|
|
||||||
|
Duplicate bundles for the same sepsis episode create conflicting compliance timelines, confuse clinician dashboards, and may trigger duplicate paging/escalation workflows. In a clinical setting this means duplicate nurse pages for the same patient.
|
||||||
|
|
||||||
|
### How to fix
|
||||||
|
|
||||||
|
1. Replace `AnyAsync` check with an **idempotent INSERT** pattern matching the approach used for alert creation:
|
||||||
|
```sql
|
||||||
|
INSERT INTO sepsis_bundles (...)
|
||||||
|
SELECT ... WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM sepsis_bundles
|
||||||
|
WHERE encounter_id = @encounterId AND compliance_status = 'IN_PROGRESS'
|
||||||
|
)
|
||||||
|
```
|
||||||
|
2. Check `rowsAffected == 0` to detect concurrent creation; return existing bundle instead of creating a new one.
|
||||||
|
3. Wrap bundle + elements creation in a single transaction with `SERIALIZABLE` isolation or use `FOR UPDATE` on the encounter row.
|
||||||
|
|
||||||
|
**Files:** `SepsisBundleService.cs:24-29`, `SepsisBundleConfiguration.cs` (add unique filtered index on `(encounter_id) WHERE compliance_status = 'IN_PROGRESS'`).
|
||||||
|
|
||||||
|
**Dependency:** None.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P0 — Trend alert matching uses fragile LIKE pattern
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
|
||||||
|
`TrendDetector.TryCreateAlertAsync` uses `LIKE '%{observationCode}%'` to check for existing open trend alerts. The pattern `%HEART_RATE%` could match a hypothetical `HEART_RATE_VARIABILITY` alert, and `%TEMP%` could match `TEMP_C` and `TEMP_F`. This bypasses deduplication and creates spurious alerts, or worse, suppresses alerts for the wrong vital sign.
|
||||||
|
|
||||||
|
### Why fix
|
||||||
|
|
||||||
|
Trend alerts fire for the 5 most critical vitals (HR, RR, SBP, Temp, SpO2). False suppression means a rapid deterioration goes unnotified; false creation means alert fatigue on a clinical floor.
|
||||||
|
|
||||||
|
### How to fix
|
||||||
|
|
||||||
|
1. Change the deduplication query to use **exact match** on a structured field rather than LIKE on the `Details` text column.
|
||||||
|
2. Option A: Add an `ObservationCode` column to `ClinicalAlert` (nullable, indexed) and match on it directly.
|
||||||
|
3. Option B: Use `Details LIKE 'Rapid deterioration: {observationCode} %'` with a prefix match instead of substring.
|
||||||
|
4. Prefer **Option A** — it also benefits analytics queries that currently parse alert details text.
|
||||||
|
|
||||||
|
**Files:** `TrendDetector.cs:102-113`, `ClinicalAlert.cs` (optional new column), `ClinicalAlertConfiguration.cs`, migration.
|
||||||
|
|
||||||
|
**Dependency:** None.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P1 — Order result → sepsis bundle update lacks spanning transaction
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
|
||||||
|
`OrderService.RecordResultAsync` updates the order status to `Resulted`, then calls `SepsisBundleService.OnOrderResultedAsync` as a separate operation. If the bundle update fails (e.g., database timeout), the order is marked as resulted but the bundle element remains `Pending`. The bundle may then be incorrectly marked `NonCompliant` by `SepsisBundleMonitorService` even though the order was completed on time.
|
||||||
|
|
||||||
|
### Why fix
|
||||||
|
|
||||||
|
Sepsis bundle compliance is a CMS/Joint Commission quality metric. A false `NonCompliant` due to a transient failure triggers incorrect escalation and skews compliance reporting.
|
||||||
|
|
||||||
|
### How to fix
|
||||||
|
|
||||||
|
1. Wrap both operations in a single `IDbContextTransaction`:
|
||||||
|
```csharp
|
||||||
|
using var tx = await _db.Database.BeginTransactionAsync();
|
||||||
|
// update order status
|
||||||
|
// call bundle service
|
||||||
|
await tx.CommitAsync();
|
||||||
|
```
|
||||||
|
2. If `OnOrderResultedAsync` fails, the entire transaction rolls back — order stays in previous state for retry.
|
||||||
|
3. Add explicit error logging when bundle element is not found for an order (currently silent no-op at `SepsisBundleService:120`).
|
||||||
|
|
||||||
|
**Files:** `OrderService.cs:100-120`, `SepsisBundleService.cs:114-162`.
|
||||||
|
|
||||||
|
**Dependency:** None.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P1 — FHIR bundle processing has no rollback on partial failure
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
|
||||||
|
`FhirBundleProcessor` processes transaction bundles by iterating entries and calling individual service methods (patient upsert, encounter upsert, observation ingest). If entry 3 of 5 fails, entries 1-2 are already persisted. FHIR R4 transaction semantics require **all-or-nothing**: either all entries succeed or none do.
|
||||||
|
|
||||||
|
### Why fix
|
||||||
|
|
||||||
|
EHR integration engines (Mirth, Rhapsody) send transaction bundles expecting atomic semantics. Partial writes create orphaned records — an encounter without its patient, observations without their encounter — that break referential integrity assumptions downstream.
|
||||||
|
|
||||||
|
### How to fix
|
||||||
|
|
||||||
|
1. Wrap the entire bundle processing loop in a single `IDbContextTransaction`.
|
||||||
|
2. On any entry failure, roll back the transaction and return a FHIR `OperationOutcome` with per-entry diagnostics.
|
||||||
|
3. Collect outbox events during processing but only write them after successful commit.
|
||||||
|
4. Add a `batch` mode (non-atomic, per-entry results) as a separate code path if needed.
|
||||||
|
|
||||||
|
**Files:** `FhirBundleProcessor.cs:60-80`, `FhirIngestController.cs:186-192`.
|
||||||
|
|
||||||
|
**Dependency:** None.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Part B — Infrastructure & Reliability
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P1 — Kafka replication factor hardcoded to 1
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
|
||||||
|
`KafkaTopicProvisioner` creates all topics with `ReplicationFactor = 1`. A single broker failure loses all unconsumed messages on those topics — including `alert.generated`, `observation.recorded`, and `sepsis.bundle.created`.
|
||||||
|
|
||||||
|
### Why fix
|
||||||
|
|
||||||
|
Clinical alert delivery is safety-critical. Losing `alert.generated` messages means nurses are not paged for critical vitals. Losing `observation.recorded` means scoring services miss data points, potentially delaying sepsis detection.
|
||||||
|
|
||||||
|
### How to fix
|
||||||
|
|
||||||
|
1. Make replication factor configurable via `KafkaTopicOptions.ReplicationFactor` (default 3 for production, 1 for dev/test).
|
||||||
|
2. Add `MinInSyncReplicas` to topic config (recommended: 2 with RF=3).
|
||||||
|
3. Validate on startup: if `ReplicationFactor > broker count`, log a warning and fall back to broker count.
|
||||||
|
4. Update docker-compose with a comment noting RF=1 is dev-only.
|
||||||
|
|
||||||
|
**Files:** `KafkaTopicProvisioner.cs:38`, `KafkaTopicOptions.cs`, `appsettings.json`, `appsettings.Development.json`.
|
||||||
|
|
||||||
|
**Dependency:** None.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P2 — No health check endpoints
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
|
||||||
|
The API has no `/health` or `/ready` endpoints. There is no startup probe, no liveness check, and no readiness check for any dependency (PostgreSQL, Redis, Kafka, RabbitMQ, Elasticsearch, MinIO).
|
||||||
|
|
||||||
|
### Why fix
|
||||||
|
|
||||||
|
Without health checks: Kubernetes/container orchestrators cannot detect unhealthy instances and route traffic away. Load balancers send requests to instances with dead database connections. Monitoring systems cannot distinguish "service down" from "service unhealthy."
|
||||||
|
|
||||||
|
### How to fix
|
||||||
|
|
||||||
|
1. Add `Microsoft.Extensions.Diagnostics.HealthChecks` and provider packages:
|
||||||
|
- `AspNetCore.HealthChecks.NpgSql` (PostgreSQL)
|
||||||
|
- `AspNetCore.HealthChecks.Redis` (Redis)
|
||||||
|
- `AspNetCore.HealthChecks.Kafka` (Kafka)
|
||||||
|
- `AspNetCore.HealthChecks.RabbitMQ` (RabbitMQ)
|
||||||
|
- `AspNetCore.HealthChecks.Elasticsearch` (Elasticsearch)
|
||||||
|
2. Register health checks in `Program.cs` with tags: `startup`, `liveness`, `readiness`.
|
||||||
|
3. Map endpoints:
|
||||||
|
- `GET /health/live` — liveness (is the process alive?)
|
||||||
|
- `GET /health/ready` — readiness (are all dependencies reachable?)
|
||||||
|
- `GET /health/startup` — startup (has initial provisioning completed?)
|
||||||
|
4. Expose health check results to Prometheus via `AspNetCore.HealthChecks.Publisher.Prometheus`.
|
||||||
|
|
||||||
|
**Files:** `Program.cs`, `VigilCareClinicalAPI.csproj` (new packages), optional `HealthChecksConfiguration.cs`.
|
||||||
|
|
||||||
|
**Dependency:** None.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P2 — Kafka consumer poison pill causes infinite retry
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
|
||||||
|
All 7 Kafka consumer services (SepsisEngine, News2Scoring, GcsScoring, TrendAnalyzer, WarningAlert, SofaScoring, EsIndexer) share the same error handling pattern: on exception, log error, delay 2000ms, retry. A malformed message (corrupt JSON, unknown observation code causing unhandled exception) will block the consumer indefinitely — no other messages on that partition are processed.
|
||||||
|
|
||||||
|
### Why fix
|
||||||
|
|
||||||
|
A single bad observation record from a misconfigured device or FHIR integration halts all downstream scoring for that partition. NEWS2, SOFA, qSOFA, and trend alerts stop computing for all patients whose observations land on the blocked partition.
|
||||||
|
|
||||||
|
### How to fix
|
||||||
|
|
||||||
|
1. Add a **retry counter** per message (track in memory or via Kafka headers).
|
||||||
|
2. After `MaxRetries` (configurable, default 3), log at Error level with full message payload and **commit the offset** to skip the poison pill.
|
||||||
|
3. Optionally publish to a dead-letter topic (`{topic}.dlq`) for manual replay.
|
||||||
|
4. Add a Prometheus counter `kafka_consumer_poison_pills_total{consumer_group, topic}`.
|
||||||
|
|
||||||
|
**Files:** All consumer services in `BackgroundServices/`: `SepsisEngineService.cs`, `News2ScoringService.cs`, `GcsScoringService.cs`, `TrendAnalyzerService.cs`, `WarningAlertService.cs`, `SofaScoringService.cs`, `EsIndexerService.cs`. Extract shared retry logic to a `KafkaConsumerBase<T>` helper.
|
||||||
|
|
||||||
|
**Dependency:** None.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P2 — Outbox relay has no dead-letter or max retry limit
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
|
||||||
|
`OutboxRelayService` retries failed publishes every 1000ms with no maximum retry count and no dead-letter mechanism. If Kafka is down for an extended period, the outbox table grows unbounded. When Kafka recovers, a flood of stale events may overwhelm consumers.
|
||||||
|
|
||||||
|
### Why fix
|
||||||
|
|
||||||
|
Extended Kafka outages are common during upgrades or broker failures. Unbounded outbox growth degrades PostgreSQL query performance (the unprocessed-events index grows). Stale clinical alerts published hours late may trigger incorrect escalations.
|
||||||
|
|
||||||
|
### How to fix
|
||||||
|
|
||||||
|
1. Add `MaxRetryCount` and `RetryBackoffMs` to outbox configuration.
|
||||||
|
2. Add a `retry_count` and `last_error` column to `OutboxEvent`.
|
||||||
|
3. After `MaxRetryCount` exceeded, mark event as `FAILED` (new status column or nullable `FailedAt` timestamp).
|
||||||
|
4. Add backoff: `delay = min(RetryBackoffMs * 2^retryCount, MaxBackoffMs)`.
|
||||||
|
5. Add `GET /api/v1/ops/outbox?status=failed` admin endpoint for manual inspection/replay.
|
||||||
|
6. Prometheus metrics: `outbox_pending_total`, `outbox_failed_total`.
|
||||||
|
|
||||||
|
**Files:** `OutboxRelayService.cs:58-139`, `OutboxEvent.cs`, `OutboxEventConfiguration.cs`, migration, `appsettings.json`.
|
||||||
|
|
||||||
|
**Dependency:** None.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P2 — ThresholdCacheLoader crashes startup on Redis failure
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
|
||||||
|
`ThresholdCacheLoader` runs once at startup and loads all alert thresholds into Redis. If Redis is unavailable, the service throws an unhandled exception, which may crash the entire application depending on host configuration. There is no retry logic.
|
||||||
|
|
||||||
|
### Why fix
|
||||||
|
|
||||||
|
Redis restarts during deployment are common. A transient Redis blip at exactly the wrong moment prevents the entire clinical API from starting, even though Redis will be available seconds later.
|
||||||
|
|
||||||
|
### How to fix
|
||||||
|
|
||||||
|
1. Wrap the Redis write loop in a retry with exponential backoff (3 attempts, 2s/4s/8s).
|
||||||
|
2. On final failure, log at Error level but **allow the application to start** — the observation ingest pipeline already has a Redis-miss fallback that loads thresholds from PostgreSQL.
|
||||||
|
3. Optionally add a background retry that re-attempts cache population after 30 seconds.
|
||||||
|
|
||||||
|
**Files:** `ThresholdCacheLoader.cs`.
|
||||||
|
|
||||||
|
**Dependency:** None.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P2 — DataLake writer partial commit inconsistency
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
|
||||||
|
`DataLakeWriterService` flushes Parquet files per partition. If 5 of 6 partitions flush successfully but one fails, the service commits Kafka offsets for the 5 successful partitions and clears their buffers. The failed partition's buffer is also cleared (line 176) even though its data was not written to MinIO. Those events are lost — they won't be re-consumed because the surrounding offsets advanced.
|
||||||
|
|
||||||
|
### Why fix
|
||||||
|
|
||||||
|
Data lake completeness is essential for clinical analytics, research datasets, and regulatory reporting. Silently dropped observations create gaps in longitudinal patient records.
|
||||||
|
|
||||||
|
### How to fix
|
||||||
|
|
||||||
|
1. **Do not clear buffers on flush failure**: only clear the buffer for partitions that flushed successfully.
|
||||||
|
2. **Do not commit offsets for failed partitions**: track per-partition flush success and only commit offsets for successful ones.
|
||||||
|
3. Add a retry counter per partition buffer; after `MaxFlushRetries`, log at Error with partition/offset range and clear (accept data loss with explicit audit trail) or halt the consumer for that partition.
|
||||||
|
4. Prometheus metric: `datalake_flush_failures_total{topic, partition}`.
|
||||||
|
|
||||||
|
**Files:** `DataLakeWriterService.cs:144-176`, `DataLakeOptions.cs`.
|
||||||
|
|
||||||
|
**Dependency:** None.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Part C — API Completeness & Consistency
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P2 — Missing input validators for 4 request types
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
|
||||||
|
Four request types used by controllers have no FluentValidation validator:
|
||||||
|
1. `TransitionStatusRequest` (encounter status changes) — no validation of `DischargeDiagnosis` length.
|
||||||
|
2. `RecordOrderResultRequest` (order results) — no validation of `ResultSummary` length or content.
|
||||||
|
3. `FhirPatientUpsertRequest` — no validation of FHIR-mapped fields before database write.
|
||||||
|
4. `FhirEncounterUpsertRequest` — no validation of department/type enum mappings.
|
||||||
|
|
||||||
|
The existing 9 validators cover other request types thoroughly.
|
||||||
|
|
||||||
|
### Why fix
|
||||||
|
|
||||||
|
Unvalidated inputs can cause database constraint violations that surface as 500 errors instead of 422s. FHIR upsert requests from integration engines may contain malformed data that is difficult to debug without validation error messages.
|
||||||
|
|
||||||
|
### How to fix
|
||||||
|
|
||||||
|
1. Create `TransitionStatusRequestValidator`: validate `DischargeDiagnosis` max length (500), `NewStatus` is valid enum.
|
||||||
|
2. Create `RecordOrderResultRequestValidator`: validate `ResultSummary` max length, non-empty.
|
||||||
|
3. Create `FhirPatientUpsertRequestValidator`: validate identifier system/value presence, gender mapping.
|
||||||
|
4. Create `FhirEncounterUpsertRequestValidator`: validate class mapping, department code mapping, period dates.
|
||||||
|
5. Register all in DI (auto-registration via `FluentValidation.DependencyInjectionExtensions` if not already configured).
|
||||||
|
|
||||||
|
**Files:** New files in `Validators/`, `Program.cs` (DI registration if needed).
|
||||||
|
|
||||||
|
**Dependency:** None.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P4 — No patient update endpoint
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
|
||||||
|
`PatientsController` has `POST` (register) but no `PUT`/`PATCH`. Patient demographics (blood type, allergies, emergency contact, name corrections) cannot be updated without direct database access.
|
||||||
|
|
||||||
|
### Why fix
|
||||||
|
|
||||||
|
Patient data corrections are a daily workflow. Allergies discovered during an encounter, emergency contact changes, and name typos all require update capability. FHIR upsert handles external system updates, but internal admin workflows have no path.
|
||||||
|
|
||||||
|
### How to fix
|
||||||
|
|
||||||
|
1. Add `UpdatePatientRequest` record with optional fields: `firstName`, `lastName`, `dateOfBirth`, `gender`, `bloodType`, `allergies`, `emergencyContactName`, `emergencyContactPhone`.
|
||||||
|
2. Add `UpdatePatientRequestValidator` (same rules as registration, all fields optional).
|
||||||
|
3. Add `PatientService.UpdateAsync(Guid id, UpdatePatientRequest)` — load, apply non-null fields, save.
|
||||||
|
4. Add `PATCH /api/v1/patients/{id}` with `[AuthorizePermission(PatientsWrite)]`.
|
||||||
|
5. Emit `ClinicalAuditLog` entry with before/after JSON.
|
||||||
|
|
||||||
|
**Files:** `PatientsController.cs`, `PatientService.cs`, new `UpdatePatientRequest.cs`, new `UpdatePatientRequestValidator.cs`.
|
||||||
|
|
||||||
|
**Dependency:** None.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P4 — Pagination inconsistencies across list endpoints
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
|
||||||
|
List endpoints use three different pagination strategies:
|
||||||
|
- **1-based page/pageSize** (most controllers): `page=1, pageSize=20`
|
||||||
|
- **0-based page** (AnalyticsController.PatientSearch): `page=0`
|
||||||
|
- **Cursor-based** (SOFA, NEWS2, Observations): varying default limits (20, 20, 50)
|
||||||
|
|
||||||
|
`AlertThresholdsController.List()` has **no pagination at all** — returns every threshold in one response. No endpoint supports sorting parameters.
|
||||||
|
|
||||||
|
### Why fix
|
||||||
|
|
||||||
|
Inconsistent pagination confuses integrators and dashboard developers. Missing pagination on thresholds is fine today (small dataset) but will break if observation codes expand. Missing sort parameters force client-side sorting.
|
||||||
|
|
||||||
|
### How to fix
|
||||||
|
|
||||||
|
1. **Standardize page-based endpoints** to 1-based pagination with consistent defaults (`page=1, pageSize=20, maxPageSize=100`).
|
||||||
|
2. Fix AnalyticsController.PatientSearch to use 1-based pagination (breaking change — document in release notes).
|
||||||
|
3. **Standardize cursor-based endpoints** to a consistent default limit (20).
|
||||||
|
4. Add pagination to `AlertThresholdsController.List()` (or document that the dataset is bounded and pagination is unnecessary).
|
||||||
|
5. Add optional `sortBy` and `sortDirection` query parameters to list endpoints where ordering matters (alerts, observations, encounters).
|
||||||
|
|
||||||
|
**Files:** `AnalyticsController.cs:103`, `AlertThresholdsController.cs:37-44`, `ObservationsController.cs:80`, all list endpoints for sort params.
|
||||||
|
|
||||||
|
**Dependency:** None.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P4 — Missing list/get-by-id endpoints
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
|
||||||
|
Several resources lack expected REST endpoints:
|
||||||
|
1. **SepsisBundles**: No list endpoint — only get-by-encounter. No way to query all active bundles across the hospital.
|
||||||
|
2. **qSOFA**: Only "current" endpoint — no history, unlike NEWS2/SOFA/GCS which all have history endpoints.
|
||||||
|
3. **AlertThresholds**: No get-by-id — only list-all and get-by-code.
|
||||||
|
4. **ReconciliationAlerts**: No API surface at all — backend-only data quality checks.
|
||||||
|
|
||||||
|
### Why fix
|
||||||
|
|
||||||
|
Clinical dashboards need a hospital-wide view of active sepsis bundles for charge nurse/supervisor workflows. qSOFA history is needed for trend visualization. ReconciliationAlerts are invisible to operators without SQL access.
|
||||||
|
|
||||||
|
### How to fix
|
||||||
|
|
||||||
|
1. Add `GET /api/v1/sepsis-bundles?status=IN_PROGRESS&page=1&pageSize=20` — list with status filter.
|
||||||
|
2. Add `GET /api/v1/encounters/{encounterId}/qsofa/history` — mirror NEWS2/SOFA history pattern with cursor pagination.
|
||||||
|
3. Add `GET /api/v1/alert-thresholds/{id}` for admin detail views.
|
||||||
|
4. Add `GET /api/v1/reconciliation-alerts?resolved=false&page=1&pageSize=20` with `checkType` filter.
|
||||||
|
|
||||||
|
**Files:** `SepsisBundlesController.cs`, `QsofaController.cs`, `AlertThresholdsController.cs`, new `ReconciliationAlertsController.cs`, corresponding service methods.
|
||||||
|
|
||||||
|
**Dependency:** None.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P4 — No delete operations across entire API
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
|
||||||
|
The API has zero DELETE endpoints. The system is entirely append-only/immutable. While this is appropriate for clinical records (observations, alerts, scores), it's problematic for configuration entities like alert thresholds and for test/dev workflows.
|
||||||
|
|
||||||
|
### Why fix
|
||||||
|
|
||||||
|
Administrators who create test thresholds or misconfigured entries cannot remove them. Draft/test patients created during onboarding clutter the production database. This is acceptable for clinical records but not for configuration data.
|
||||||
|
|
||||||
|
### How to fix
|
||||||
|
|
||||||
|
1. Add `DELETE /api/v1/alert-thresholds/{id}` with `[AuthorizePermission(ThresholdsWrite)]` — hard delete for configuration data.
|
||||||
|
2. Document explicitly that clinical entities (patients, encounters, observations, alerts, scores) are **immutable by design** and do not support deletion (regulatory compliance).
|
||||||
|
3. Optionally add a `Patient.Status = "inactive"` transition endpoint for marking test patients without deletion.
|
||||||
|
|
||||||
|
**Files:** `AlertThresholdsController.cs`, `AlertThresholdService.cs`.
|
||||||
|
|
||||||
|
**Dependency:** None.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P4 — FHIR R4 compliance limited to inbound-only facade
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
|
||||||
|
The FHIR implementation supports only `Create` interactions (POST). The `CapabilityStatement` correctly declares this, but there are no `Read`, `Search`, or `Update` operations. Only 4 resource types are supported (Patient, Encounter, Observation, MedicationAdministration). There is no FHIR search, no `_include`/`_revinclude`, no resource versioning (ETag/If-Match), and no batch bundle mode.
|
||||||
|
|
||||||
|
### Why fix
|
||||||
|
|
||||||
|
EHR integrations commonly need bidirectional data flow. Care coordination systems need to read patient data back in FHIR format. Audit systems query for encounters. Without read operations, downstream systems must use the proprietary REST API instead of standard FHIR.
|
||||||
|
|
||||||
|
### How to fix (phased)
|
||||||
|
|
||||||
|
**Phase 1 — Read operations:**
|
||||||
|
1. Add `GET /fhir/Patient/{id}` and `GET /fhir/Patient?identifier={system}|{value}`.
|
||||||
|
2. Add `GET /fhir/Encounter/{id}` and `GET /fhir/Encounter?patient={patientId}`.
|
||||||
|
3. Map internal entities back to FHIR R4 resources using reverse mappers.
|
||||||
|
4. Update `CapabilityStatement` to include `Read` and `SearchType` interactions.
|
||||||
|
|
||||||
|
**Phase 2 — Search and versioning:**
|
||||||
|
1. Add search parameters: `_lastUpdated`, `_count`, `_offset`.
|
||||||
|
2. Add `ETag` headers based on `UpdatedAt` or row version.
|
||||||
|
|
||||||
|
**Files:** `FhirIngestController.cs`, new `FhirReadController.cs`, `FhirMetadataController.cs`, new reverse mapper classes.
|
||||||
|
|
||||||
|
**Dependency:** Product decision on FHIR read scope.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Part D — Security & Hardening
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P3 — FHIR API key not rotatable and timing-attack vulnerable
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
|
||||||
|
`FhirApiKeyOrJwtMiddleware` compares the `X-Api-Key` header against a config value using standard string equality (`== config["Fhir:ApiKey"]`). This is vulnerable to timing attacks. The API key is stored in `appsettings.json` in plaintext and cannot be rotated without redeploying the service.
|
||||||
|
|
||||||
|
### Why fix
|
||||||
|
|
||||||
|
FHIR endpoints receive PHI (Protected Health Information). A compromised API key grants full integration-role access to patient data. Timing attacks are low-probability but easily prevented.
|
||||||
|
|
||||||
|
### How to fix
|
||||||
|
|
||||||
|
1. Replace string equality with `CryptographicOperations.FixedTimeEquals()` for constant-time comparison.
|
||||||
|
2. Support multiple active API keys (array in config) for zero-downtime rotation.
|
||||||
|
3. Move API keys to environment variables or a secrets manager (Azure Key Vault, AWS Secrets Manager, HashiCorp Vault).
|
||||||
|
4. Add `X-Api-Key` rotation documentation to the ops runbook.
|
||||||
|
5. Optionally add per-key audit logging (which key was used).
|
||||||
|
|
||||||
|
**Files:** `FhirApiKeyOrJwtMiddleware.cs:46`, `FhirOptions.cs`, `appsettings.json`.
|
||||||
|
|
||||||
|
**Dependency:** None.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P3 — JWT signing key not validated on startup
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
|
||||||
|
`JwtOptions.SigningKey` is read from configuration and used to create a `SymmetricSecurityKey`. There is no validation that the key meets minimum length requirements (256 bits for HMAC-SHA256). A short or empty key causes a runtime exception on the first authentication attempt, not at startup.
|
||||||
|
|
||||||
|
### Why fix
|
||||||
|
|
||||||
|
Fail-fast on misconfiguration prevents deploying a service that accepts no requests. In development, a missing or weak key wastes debugging time on cryptic `SecurityTokenInvalidSignatureException` errors.
|
||||||
|
|
||||||
|
### How to fix
|
||||||
|
|
||||||
|
1. Add a startup validation check in `Program.cs` after binding `JwtOptions`:
|
||||||
|
```csharp
|
||||||
|
if (string.IsNullOrEmpty(jwtOptions.SigningKey) ||
|
||||||
|
Encoding.UTF8.GetByteCount(jwtOptions.SigningKey) < 32)
|
||||||
|
throw new InvalidOperationException("JWT SigningKey must be at least 256 bits");
|
||||||
|
```
|
||||||
|
2. Optionally add `IValidateOptions<JwtOptions>` implementation for structured validation.
|
||||||
|
|
||||||
|
**Files:** `Program.cs`, optionally `JwtOptions.cs`.
|
||||||
|
|
||||||
|
**Dependency:** None.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P3 — No audit of authorization failures
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
|
||||||
|
`PermissionAuthorizationHandler` returns `context.Fail()` when a user lacks the required permission, but does not log the attempt or write a `ClinicalAuditLog` entry. Failed authorization attempts are invisible in both application logs and the audit trail.
|
||||||
|
|
||||||
|
### Why fix
|
||||||
|
|
||||||
|
Security audits and compliance reviews (HIPAA, SOC2) require evidence that unauthorized access attempts are logged. Without this, there is no way to detect credential compromise or privilege escalation attempts.
|
||||||
|
|
||||||
|
### How to fix
|
||||||
|
|
||||||
|
1. Inject `ILogger<PermissionAuthorizationHandler>` and log at Warning level on failure: `user={username}, role={role}, requiredPermission={permission}, endpoint={resource}`.
|
||||||
|
2. Optionally write a `ClinicalAuditLog` entry with action `AuthorizationDenied` (new enum value) for persistent audit trail.
|
||||||
|
3. Add a Prometheus counter: `authorization_failures_total{permission, role}`.
|
||||||
|
|
||||||
|
**Files:** `PermissionAuthorizationHandler.cs`, `AuditAction.cs` (new enum value), `AuditService.cs`.
|
||||||
|
|
||||||
|
**Dependency:** None.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P3 — Elasticsearch security disabled in deployment
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
|
||||||
|
`docker-compose.yml` sets `xpack.security.enabled=false` and `xpack.security.http.ssl.enabled=false` on the Elasticsearch container. The ES instance accepts unauthenticated requests from any container on the network. The `patient_encounters` index contains PHI (patient names, MRNs, encounter details).
|
||||||
|
|
||||||
|
### Why fix
|
||||||
|
|
||||||
|
Any compromised container on the Docker network can read/write/delete clinical data in Elasticsearch. Even in development, this creates a risk of accidental data exposure if the Docker network is bridged to a shared network.
|
||||||
|
|
||||||
|
### How to fix
|
||||||
|
|
||||||
|
1. Enable `xpack.security.enabled=true` in docker-compose.
|
||||||
|
2. Set `ELASTIC_PASSWORD` via Docker secrets or `.env` file.
|
||||||
|
3. Update `ElasticsearchOptions` to include `Username`, `Password`, and `UseTls` fields.
|
||||||
|
4. Configure the .NET `ElasticClient` with basic auth credentials.
|
||||||
|
5. Document that production deployments must use TLS + authentication.
|
||||||
|
|
||||||
|
**Files:** `docker-compose.yml:67`, new `ElasticsearchOptions.cs` fields, `EsIndexerService.cs`, `ElasticIndexProvisioner.cs`.
|
||||||
|
|
||||||
|
**Dependency:** None.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P3 — No token refresh or revocation mechanism
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
|
||||||
|
JWT tokens are issued with a configurable expiration but there is no refresh token flow and no token revocation/blacklist. A compromised token remains valid until natural expiration. There is no `POST /auth/refresh` or `POST /auth/revoke` endpoint.
|
||||||
|
|
||||||
|
### Why fix
|
||||||
|
|
||||||
|
Clinical sessions may last entire shifts (8-12 hours). Short token lifetimes require frequent re-authentication, disrupting clinical workflows. Long lifetimes without revocation mean a stolen token grants extended access. Compromised accounts cannot be locked out until the token expires.
|
||||||
|
|
||||||
|
### How to fix
|
||||||
|
|
||||||
|
1. Add refresh token support: issue a long-lived opaque refresh token stored in the database alongside the access token.
|
||||||
|
2. Add `POST /api/v1/auth/refresh` — validate refresh token, issue new access token.
|
||||||
|
3. Add `POST /api/v1/auth/revoke` — invalidate refresh token and optionally blacklist the access token (via Redis TTL set matching remaining token lifetime).
|
||||||
|
4. Add `LastLoginAt` update on token refresh (already exists on `ClinicalUser`).
|
||||||
|
|
||||||
|
**Files:** `AuthController.cs`, `AuthService.cs`, `ClinicalUser.cs` (add `RefreshToken`, `RefreshTokenExpiresAt`), migration.
|
||||||
|
|
||||||
|
**Dependency:** None.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Part E — Observability & Operations
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P5 — No request/response timing metrics
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
|
||||||
|
The API has Prometheus metrics for clinical events (alerts, bundles, consumer lag) but no HTTP request timing histograms. There is no way to measure API latency, identify slow endpoints, or set SLOs.
|
||||||
|
|
||||||
|
### Why fix
|
||||||
|
|
||||||
|
Clinical dashboards and FHIR integrations depend on API responsiveness. Without latency metrics, there is no baseline for alerting on degradation, and performance regressions go undetected until users report them.
|
||||||
|
|
||||||
|
### How to fix
|
||||||
|
|
||||||
|
1. Add `prometheus-net.AspNetCore` middleware: `app.UseHttpMetrics()` in `Program.cs`.
|
||||||
|
2. This automatically provides `http_request_duration_seconds` histogram with labels: `method`, `controller`, `action`, `status_code`.
|
||||||
|
3. Add Grafana dashboard panels for p50/p95/p99 latency per endpoint.
|
||||||
|
4. Set initial SLO targets (e.g., observation ingest p95 < 200ms).
|
||||||
|
|
||||||
|
**Files:** `Program.cs:276` (add `app.UseHttpMetrics()` before `app.MapMetrics()`), `VigilCareClinicalAPI.csproj` (package).
|
||||||
|
|
||||||
|
**Dependency:** None.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P5 — Background service errors not metricked
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
|
||||||
|
Kafka consumer services, outbox relay, bundle monitor, and RabbitMQ workers log errors but do not increment Prometheus counters on failure. The only background service metrics are `sepsis_bundle_compliance_total` and `kafka_consumer_lag`. There are no failure-rate metrics for any background service.
|
||||||
|
|
||||||
|
### Why fix
|
||||||
|
|
||||||
|
Log-based alerting requires parsing structured logs. Metrics-based alerting (Prometheus + Alertmanager) is standard in production Kubernetes deployments and enables rate-of-change alerts ("consumer errors spiking") that are impossible with log grep.
|
||||||
|
|
||||||
|
### How to fix
|
||||||
|
|
||||||
|
1. Add counters per background service:
|
||||||
|
- `kafka_consumer_errors_total{consumer_group, topic, error_type}`
|
||||||
|
- `outbox_relay_failures_total{reason}`
|
||||||
|
- `rabbitmq_worker_errors_total{queue, error_type}`
|
||||||
|
- `datalake_flush_failures_total{topic, partition}`
|
||||||
|
2. Add processing duration histograms:
|
||||||
|
- `kafka_consumer_processing_seconds{consumer_group}`
|
||||||
|
- `outbox_relay_batch_seconds`
|
||||||
|
3. Increment counters in existing catch blocks (minimal code change).
|
||||||
|
|
||||||
|
**Files:** All background services, new `BackgroundServiceMetrics.cs` static class for metric definitions.
|
||||||
|
|
||||||
|
**Dependency:** None.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P5 — Thin test coverage for concurrent operations and background services
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
|
||||||
|
Test coverage analysis reveals:
|
||||||
|
- **No concurrent operation tests**: No tests for simultaneous alert creation, parallel observation ingest, or race conditions in deduplication logic.
|
||||||
|
- **Thin background service tests**: Kafka consumer behavior, outbox relay failure recovery, and RabbitMQ worker retry logic are not directly tested.
|
||||||
|
- **No performance tests**: No benchmarks for observation ingest throughput, scoring latency, or alert pipeline end-to-end timing.
|
||||||
|
- **No chaos tests**: No fault injection for database/Redis/Kafka/RabbitMQ failures.
|
||||||
|
|
||||||
|
Well-tested areas include: clinical scoring (qSOFA, SOFA, NEWS2, GCS), alert lifecycle, FHIR ingest, medication correlation, sepsis bundle tracking, and end-to-end scenarios.
|
||||||
|
|
||||||
|
### Why fix
|
||||||
|
|
||||||
|
The concurrent operation gaps directly correspond to P0 race conditions identified in this document (MRN generation, sepsis bundle creation). Without concurrent tests, fixes cannot be verified. Background service resilience is untested, meaning the Kafka poison pill and outbox retry gaps have no regression safety net.
|
||||||
|
|
||||||
|
### How to fix
|
||||||
|
|
||||||
|
1. **Concurrent operation tests** (priority — validates P0 fixes):
|
||||||
|
- Parallel patient registration with same demographics → verify unique MRN.
|
||||||
|
- Parallel SOFA_SEPSIS alerts for same encounter → verify single bundle.
|
||||||
|
- Parallel observation ingest with same idempotency key → verify single record.
|
||||||
|
|
||||||
|
2. **Background service tests**:
|
||||||
|
- Test Kafka consumer with malformed message → verify skip after max retries.
|
||||||
|
- Test outbox relay with simulated Kafka failure → verify retry and eventual dead-letter.
|
||||||
|
- Test PagingWorker with acknowledged alert → verify no escalation.
|
||||||
|
|
||||||
|
3. **Performance benchmarks** (optional, lower priority):
|
||||||
|
- Observation ingest throughput (target: 1000/sec per instance).
|
||||||
|
- Alert pipeline latency (observation → alert → page: target < 5s p95).
|
||||||
|
|
||||||
|
**Files:** New test files in `VigilCareClinicalAPI.Tests/`: `ConcurrencyTests.cs`, `BackgroundServiceTests.cs`, optional `BenchmarkTests.cs`.
|
||||||
|
|
||||||
|
**Dependency:** P0 fixes (concurrent tests validate the fixes).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Part F — Hardcoded Values & Configuration Gaps
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P4 — Clinical parameters hardcoded instead of configurable
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
|
||||||
|
Several clinically significant parameters are hardcoded:
|
||||||
|
| Value | Location | Current |
|
||||||
|
|-------|----------|---------|
|
||||||
|
| Sepsis bundle deadline | `SepsisBundleService.cs:39` | 1 hour |
|
||||||
|
| Bundle monitor scan interval | `SepsisBundleMonitorService.cs:5` | 5 minutes |
|
||||||
|
| qSOFA criterion TTL | `QsofaDetector.cs:8` | 1800 seconds |
|
||||||
|
| GCS/NEWS2 scoring TTL | `GcsDetector.cs:8`, `News2Detector.cs:8` | 14400 seconds |
|
||||||
|
| MRN format pattern | `PatientService.cs:200` | `MRN-{count:D6}` |
|
||||||
|
| Paging worker poll interval | `PagingWorkerService.cs` | 2 seconds |
|
||||||
|
|
||||||
|
### Why fix
|
||||||
|
|
||||||
|
Different hospitals and clinical settings have different protocols. CMS Sepsis SEP-1 requires a 3-hour bundle, not 1-hour. Facilities operating under different guidelines need to adjust these parameters without code changes.
|
||||||
|
|
||||||
|
### How to fix
|
||||||
|
|
||||||
|
1. Move sepsis bundle deadline to `SepsisOptions.BundleDeadlineHours` (default 1, CMS standard 3).
|
||||||
|
2. Move bundle monitor scan interval to `SepsisOptions.MonitorScanIntervalMinutes`.
|
||||||
|
3. Move qSOFA TTL to `QsofaOptions.CriterionTtlSeconds`.
|
||||||
|
4. Move GCS/NEWS2 TTL to a shared `ScoringOptions.CalculationTtlSeconds`.
|
||||||
|
5. Move MRN format to `PatientOptions.MrnPrefix` and `MrnDigits`.
|
||||||
|
6. All via `IOptions<T>` pattern already established in the codebase.
|
||||||
|
|
||||||
|
**Files:** Respective service files, new/updated options classes, `appsettings.json`.
|
||||||
|
|
||||||
|
**Dependency:** None.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Summary matrix
|
||||||
|
|
||||||
|
| # | Issue | Priority | Part | Status |
|
||||||
|
|---|-------|----------|------|--------|
|
||||||
|
| 1 | MRN generation race condition | P0 | A | Open |
|
||||||
|
| 2 | Sepsis bundle creation TOCTOU | P0 | A | Open |
|
||||||
|
| 3 | Trend alert LIKE pattern | P0 | A | Open |
|
||||||
|
| 4 | Order→Bundle transaction gap | P1 | A | Open |
|
||||||
|
| 5 | FHIR bundle no rollback | P1 | A | Open |
|
||||||
|
| 6 | Kafka replication factor = 1 | P1 | B | Open |
|
||||||
|
| 7 | No health check endpoints | P2 | B | Open |
|
||||||
|
| 8 | Kafka consumer poison pill | P2 | B | Open |
|
||||||
|
| 9 | Outbox relay no dead-letter | P2 | B | Open |
|
||||||
|
| 10 | ThresholdCacheLoader crash on Redis | P2 | B | Open |
|
||||||
|
| 11 | DataLake partial commit | P2 | B | Open |
|
||||||
|
| 12 | Missing input validators | P2 | C | Open |
|
||||||
|
| 13 | No patient update endpoint | P4 | C | Open |
|
||||||
|
| 14 | Pagination inconsistencies | P4 | C | Open |
|
||||||
|
| 15 | Missing list/get endpoints | P4 | C | Open |
|
||||||
|
| 16 | No delete operations | P4 | C | Open |
|
||||||
|
| 17 | FHIR R4 read-only facade | P4 | C | Open |
|
||||||
|
| 18 | API key timing attack + rotation | P3 | D | Open |
|
||||||
|
| 19 | JWT key not validated on startup | P3 | D | Open |
|
||||||
|
| 20 | No authorization failure audit | P3 | D | Open |
|
||||||
|
| 21 | Elasticsearch security disabled | P3 | D | Open |
|
||||||
|
| 22 | No token refresh/revocation | P3 | D | Open |
|
||||||
|
| 23 | No request timing metrics | P5 | E | Open |
|
||||||
|
| 24 | Background service error metrics | P5 | E | Open |
|
||||||
|
| 25 | Thin concurrent/resilience tests | P5 | E | Open |
|
||||||
|
| 26 | Clinical params hardcoded | P4 | F | Open |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Suggested implementation sequence
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
subgraph correctness [Part A — Correctness]
|
||||||
|
P0A[P0: MRN sequence]
|
||||||
|
P0B[P0: Bundle idempotent INSERT]
|
||||||
|
P0C[P0: Trend exact match]
|
||||||
|
P1A[P1: Order→Bundle transaction]
|
||||||
|
P1B[P1: FHIR bundle rollback]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph infra [Part B — Infrastructure]
|
||||||
|
P1K[P1: Kafka replication factor]
|
||||||
|
P2H[P2: Health checks]
|
||||||
|
P2P[P2: Poison pill handling]
|
||||||
|
P2O[P2: Outbox dead-letter]
|
||||||
|
P2T[P2: ThresholdCacheLoader retry]
|
||||||
|
P2D[P2: DataLake partial commit]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph security [Part D — Security]
|
||||||
|
P3K[P3: API key hardening]
|
||||||
|
P3J[P3: JWT validation]
|
||||||
|
P3A[P3: Auth failure audit]
|
||||||
|
P3E[P3: ES security]
|
||||||
|
P3R[P3: Token refresh]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph api [Part C — API]
|
||||||
|
P2V[P2: Missing validators]
|
||||||
|
P4P[P4: Patient update]
|
||||||
|
P4G[P4: Pagination/sorting]
|
||||||
|
P4L[P4: Missing endpoints]
|
||||||
|
P4F[P4: FHIR read ops]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph obs [Part E — Observability]
|
||||||
|
P5M[P5: Request metrics]
|
||||||
|
P5B[P5: Background metrics]
|
||||||
|
P5T[P5: Concurrent tests]
|
||||||
|
end
|
||||||
|
|
||||||
|
P0A --> P5T
|
||||||
|
P0B --> P5T
|
||||||
|
P0C --> P5T
|
||||||
|
P2P --> P5B
|
||||||
|
P2O --> P5B
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sprint-sized batches
|
||||||
|
|
||||||
|
| Batch | Items | Outcome |
|
||||||
|
|-------|-------|---------|
|
||||||
|
| **1 — Correctness** | P0 MRN sequence, P0 bundle idempotent INSERT, P0 trend exact match, P1 order→bundle tx, P1 FHIR rollback | Race conditions eliminated; clinical data integrity guaranteed |
|
||||||
|
| **2 — Infrastructure resilience** | P1 Kafka RF, P2 health checks, P2 poison pill, P2 outbox dead-letter, P2 ThresholdCacheLoader, P2 DataLake commit | Production-ready infrastructure; no silent data loss |
|
||||||
|
| **3 — Security hardening** | P3 API key, P3 JWT validation, P3 auth audit, P3 ES security, P3 token refresh | HIPAA/compliance baseline; audit trail for access |
|
||||||
|
| **4 — API completeness** | P2 validators, P4 patient update, P4 pagination, P4 missing endpoints, P4 delete ops, P4 config extraction | Admin UI and integration teams unblocked |
|
||||||
|
| **5 — Observability & testing** | P5 request metrics, P5 background metrics, P5 concurrent tests, P4 FHIR read | Incidents diagnosable; regression safety net for Batch 1 fixes |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing strategy (cross-cutting)
|
||||||
|
|
||||||
|
For each fix, add or extend tests in `VigilCareClinicalAPI.Tests/`:
|
||||||
|
|
||||||
|
- **Concurrency tests** (Batch 1): Parallel patient registration, parallel bundle creation, parallel observation ingest with same idempotency key.
|
||||||
|
- **Transaction rollback tests** (Batch 1): Order result failure rolls back bundle update; FHIR bundle entry failure rolls back all entries.
|
||||||
|
- **Infrastructure resilience tests** (Batch 2): Consumer with poison pill message, outbox with simulated Kafka failure, startup with Redis unavailable.
|
||||||
|
- **Security tests** (Batch 3): Timing-safe API key comparison, expired/revoked token rejection, authorization failure audit log entry.
|
||||||
|
- **API contract tests** (Batch 4): New validators return 422 with correct error shapes, pagination parameters respected, new endpoints return expected status codes.
|
||||||
|
- **Metrics verification tests** (Batch 5): Prometheus counter increments on consumer error, request histogram populated after API call.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Out of scope (unless explicitly requested)
|
||||||
|
|
||||||
|
- Full OpenTelemetry distributed tracing (P5 covers Prometheus metrics as interim).
|
||||||
|
- Multi-tenancy or organization-scoped data isolation.
|
||||||
|
- FHIR Subscription or WebSocket push for real-time updates.
|
||||||
|
- HL7v2 ADT message support (current integration is FHIR-only).
|
||||||
|
- Rate limiting on public-facing endpoints (API is internal-only today).
|
||||||
|
- Database read replicas or CQRS pattern.
|
||||||
|
- Kubernetes manifests, Helm charts, or CI/CD pipeline definitions.
|
||||||
|
- SMART on FHIR authorization (OAuth2 scopes for EHR launch context).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Success criteria
|
||||||
|
|
||||||
|
When complete, the system should support:
|
||||||
|
|
||||||
|
**Data Integrity (Part A)**
|
||||||
|
- Concurrent patient registrations produce unique MRNs without 500 errors.
|
||||||
|
- Concurrent SOFA_SEPSIS alerts for the same encounter create exactly one bundle.
|
||||||
|
- Trend alerts match on exact observation code, not substring.
|
||||||
|
- Order results and bundle compliance update atomically.
|
||||||
|
- FHIR transaction bundles are all-or-nothing.
|
||||||
|
|
||||||
|
**Infrastructure (Part B)**
|
||||||
|
- Kafka topic loss requires losing 2+ brokers (RF=3).
|
||||||
|
- Health checks report dependency status; orchestrators route around failures.
|
||||||
|
- A malformed Kafka message is dead-lettered after 3 retries, not retried forever.
|
||||||
|
- Outbox events have bounded retry with backoff and dead-letter.
|
||||||
|
- Startup survives transient Redis outage.
|
||||||
|
- Data lake writes are complete or explicitly failed — never silently dropped.
|
||||||
|
|
||||||
|
**Security (Part D)**
|
||||||
|
- FHIR API keys can be rotated without downtime.
|
||||||
|
- JWT misconfiguration fails at startup, not at first request.
|
||||||
|
- Authorization failures are logged and auditable.
|
||||||
|
- Elasticsearch requires authentication.
|
||||||
|
|
||||||
|
**API (Part C)**
|
||||||
|
- All request types have input validation with 422 error responses.
|
||||||
|
- Patient demographics are updatable via API.
|
||||||
|
- Pagination is consistent (1-based, sortable) across all list endpoints.
|
||||||
|
- Clinical dashboards have API access to sepsis bundles, qSOFA history, and reconciliation alerts.
|
||||||
|
|
||||||
|
**Observability (Part E)**
|
||||||
|
- HTTP request latency is measurable via Prometheus histograms.
|
||||||
|
- Background service failures are countable and alertable.
|
||||||
|
- Concurrent operation tests provide regression safety for P0 fixes.
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user