update docs and prep for frontend

This commit is contained in:
voltsrage
2026-06-19 15:44:58 +08:00
parent abc781c9c0
commit 49973271e5
20 changed files with 1071 additions and 26 deletions
+120 -13
View File
@@ -2,7 +2,7 @@
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:** All fourteen planned phases are complete — from schema and CRUD through Kafka, Elasticsearch CQRS, sepsis detection, RabbitMQ paging with DLQ escalation, reconciliation jobs, Prometheus/Grafana observability, the MinIO Parquet data lake, clinical data model expansion (patient demographics, encounter enrichment, 12 observation codes), warning alerts and orders, the NEWS2 composite scoring engine, trend detection with alert suppression, and qSOFA scoring with sepsis bundle compliance tracking. See [Implemented Phases](#implemented-phases) for the full breakdown.
**Implementation status:** Sixteen planned phases are complete — from schema and CRUD through Kafka, Elasticsearch CQRS, sepsis detection, RabbitMQ paging with DLQ escalation, reconciliation jobs, Prometheus/Grafana observability, the MinIO Parquet data lake, clinical data model expansion (patient demographics, encounter enrichment, 12 observation codes), warning alerts and orders, the NEWS2 composite scoring engine, trend detection with alert suppression, qSOFA scoring with sepsis bundle compliance tracking, medication administration with alert correlation annotations, and the console replay simulator. Ward-dashboard backend APIs (`GET /encounters` list with clinical summaries, `GET /qsofa/current`, CORS for a frontend on port 5173) are also in place. See [Implemented Phases](#implemented-phases) for the full breakdown.
## Domain Model — How It Maps to a Real Clinical System
@@ -67,6 +67,10 @@ An `OutboxEvent` is written in the same transaction as any observation or alert,
- **NEWS2 Composite Scoring Engine** — `News2ScoringService` Kafka consumer (`news2-scoring`) evaluates seven vital parameters per encounter (`RESP_RATE`, `SPO2`, `SYSTOLIC_BP`, `HEART_RATE`, `AVPU`, `TEMP_C`, `SUPPLEMENTAL_O2`) using Redis keys with a 4-hour TTL; when all seven are present, computes the official NEWS2 aggregate score, persists to `news2_scores`, and creates `NEWS2_WARNING` (score 56 or single param = 3) or `NEWS2_EMERGENCY` (score ≥ 7) alerts idempotently; `GET /encounters/:id/news2/current` and `/history` expose score history; Prometheus `news2_scores_total` and `news2_scoring_duration_seconds`
- **Trend Detection Engine** — `TrendAnalyzerService` Kafka consumer (`trend-analyzer`) tracks rate-of-change for five vital parameters (`HEART_RATE`, `RESP_RATE`, `SYSTOLIC_BP`, `TEMP_C`, `SPO2`) using Redis sliding-window history; when velocity exceeds configured thresholds (e.g. 72→95 bpm in 30 min), creates a `RAPID_DETERIORATION` alert even if the current value is below warning thresholds; Prometheus `trend_alerts_total` and `trend_analysis_duration_seconds`
- **Alert Suppression Windows** — acknowledging a suppressible alert (`WARNING_*`, `NEWS2_WARNING`) sets a Redis key `suppress:{encounterId}:{alertType}` with a configurable TTL (default 30 min from `AlertSuppression` config; optional per-code override via `alert_thresholds.suppression_window_minutes`); `WarningEvaluator` and `News2Detector` check suppression before creating new warning alerts; critical alerts (`CRITICAL_*`, `NEWS2_EMERGENCY`, `SEPSIS_WARNING`, `RAPID_DETERIORATION`) are never suppressed; observations and NEWS2 scores continue to persist during suppression; Prometheus `alert_suppressions_total`
- **Medication Administration** — `POST /encounters/:id/medications` records drug administrations (name, dose, route, timestamp, administered-by); `GET /encounters/:id/medications` lists with optional `since` filter; `GET /medications/:id` detail; active-encounter guard; FluentValidation on request DTOs
- **Medication Correlation Annotations** — `MedicationCorrelationHelper` appends medication context to warning and NEWS2 alert details when a mapped drug was administered within the correlation window (default 90 min); drug-to-vital mappings in `MedicationCorrelation` config (`appsettings.json`); annotates rather than suppresses — alerts still fire; sepsis, trend, and critical sync-path alerts are never annotated; design rationale in `docs/decisions/medication-correlation-design.md`
- **Ward Dashboard 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`)
- **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, and sepsis bundle state during replay; eight sample scenarios in `VigilCare.Simulator/Scenarios/List/`; 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`
- **Data Lake Writer** — `DataLakeWriterService` (consumer group `data-lake-writer`) buffers `observation.recorded`, `alert.generated`, and `encounter.status.changed` events, flushes date-partitioned Parquet files to MinIO (`/observations/`, `/alerts/`, `/encounters/`), and commits Kafka offsets only after at least one successful upload; shutdown flush uses an uncanceled token so MinIO writes complete on Ctrl+C; `kafka_partition` and `kafka_offset` columns provide audit lineage
- **Reconciliation Jobs** — three scheduled checks: (1) unacknowledged CRITICAL alerts older than 30 minutes, (2) pending orders without results after 4 hours, (3) active inpatients with no observation in 2 hours; each finding creates a `reconciliation_alerts` row and publishes to RabbitMQ
@@ -97,8 +101,8 @@ IHostedServices (background):
OutboxRelayService → PostgreSQL outbox → Kafka (every 500ms)
EsIndexerService → Kafka → Elasticsearch (consumer group: es-indexer)
SepsisEngineService → Kafka → Redis SIRS + qSOFA state → PostgreSQL alert → SepsisAlertHandler → SepsisBundleService (consumer group: sepsis-engine)
WarningAlertService → Kafka → WarningEvaluator → PostgreSQL WARNING alert (consumer group: warning-evaluator)
News2ScoringService → Kafka → News2Detector → Redis NEWS2 state → PostgreSQL score + alert (consumer group: news2-scoring)
WarningAlertService → Kafka → WarningEvaluator (+ MedicationCorrelationHelper) → PostgreSQL WARNING alert (consumer group: warning-evaluator)
News2ScoringService → Kafka → News2Detector (+ MedicationCorrelationHelper) → Redis NEWS2 state → PostgreSQL score + alert (consumer group: news2-scoring)
TrendAnalyzerService → Kafka → TrendDetector → Redis trend history → PostgreSQL RAPID_DETERIORATION alert (consumer group: trend-analyzer)
AlertSuppressionService → Redis suppress:{enc}:{type} keys set on acknowledge; read by WarningEvaluator + News2Detector
NotificationPublisherService → Kafka → RabbitMQ paging.queue (consumer group: notification-publisher)
@@ -147,7 +151,9 @@ VigilCareClinicalAPI/
├── appsettings.json # Connection strings, Kafka, Elasticsearch, RabbitMQ, MinIO, Serilog, ReconciliationJobs
├── Controllers/
│ ├── PatientsController.cs # Patient CRUD, search by name/MRN
│ ├── EncountersController.cs # Encounter open, status PATCH, timeline
│ ├── EncountersController.cs # Encounter list (ward summary), get, status PATCH, timeline
│ ├── MedicationsController.cs # Medication administration create, list, get
│ ├── QsofaController.cs # Current qSOFA criteria count (Redis-backed)
│ ├── ObservationsController.cs # Ingest POST, cursor-paginated GET
│ ├── AlertThresholdsController.cs # Threshold CRUD + cache invalidation
│ ├── AlertsController.cs # Alert list (global + per-encounter), acknowledge, resolve
@@ -167,7 +173,8 @@ VigilCareClinicalAPI/
│ │ ├── OutboxEvent.cs # topic + payload JSONB + processed_at
│ │ ├── ReconciliationAlert.cs
│ │ ├── SepsisBundle.cs # Four-element treatment bundle with 1-hour compliance deadline
│ │ ── SepsisBundleElement.cs # Individual bundle element linked to a clinical order
│ │ ── SepsisBundleElement.cs # Individual bundle element linked to a clinical order
│ │ └── MedicationAdministration.cs # Drug administration record per encounter
│ └── Enums/
│ ├── EncounterStatus.cs # Scheduled, Active, Discharged, Cancelled
│ ├── EncounterType.cs # Inpatient, Outpatient, Emergency
@@ -196,13 +203,17 @@ VigilCareClinicalAPI/
│ ├── OrderService.cs # Order lifecycle; status machine; calls SepsisBundleService.OnOrderResultedAsync on result
│ ├── News2Service.cs # Current score + cursor-paginated history from PostgreSQL
│ ├── SepsisBundleService.cs # Bundle creation, element completion, compliance evaluation
│ ├── WarningEvaluator.cs # Warning-range evaluation; suppression check; idempotent alert INSERT
│ ├── MedicationService.cs # Medication CRUD; GetRecentForEncounterAsync for correlation
│ ├── QsofaService.cs # Redis-backed qSOFA criteria count for API/dashboard
│ ├── WarningEvaluator.cs # Warning-range evaluation; suppression + medication annotation; idempotent INSERT
│ ├── AnalyticsService.cs # Elasticsearch query wrappers
│ └── PlausibilityValidator.cs # Per-code numeric range guard
├── Trend/
│ ├── TrendCalculator.cs # Pure static rate-of-change logic
│ └── TrendDetector.cs # Redis history + RAPID_DETERIORATION alert creation
├── Validators/ # FluentValidation — RegisterPatient, OpenEncounter, IngestObservation, …
├── Medication/
│ └── MedicationCorrelationHelper.cs # Appends drug context to warning/NEWS2 alert details
├── Validators/ # FluentValidation — RegisterPatient, OpenEncounter, IngestObservation, CreateMedicationAdministration, …
├── Observability/
│ └── Metrics/
│ └── ClinicalMetrics.cs # Fifteen Prometheus metric families (counters, histograms, gauges)
@@ -236,7 +247,9 @@ VigilCareClinicalAPI/
├── Configuration/
│ ├── KafkaOptions.cs / KafkaTopicOptions.cs
│ ├── RabbitMqOptions.cs / MinioOptions.cs
── ReconciliationJobOptions.cs
── ReconciliationJobOptions.cs
│ ├── MedicationCorrelationOptions.cs # Drug-vital mappings + correlation window
│ └── DashboardOptions.cs # CORS origins for ward dashboard frontend
├── Sepsis/
│ ├── SirsDetector.cs # Redis SIRS state management (SET/DEL/MGET)
│ ├── SirsEvaluator.cs # Per-code criterion evaluation
@@ -262,6 +275,9 @@ VigilCareClinicalAPI/
│ ├── Observation/ObservationRow.cs # Parquet row contract for observation events
│ ├── Alert/AlertRow.cs # Parquet row contract for alert events
│ ├── Encounter/EncounterStatusRow.cs # Parquet row contract for encounter status events
│ ├── Encounter/WardEncounterSummary.cs # Denormalized row for ward encounter list
│ ├── Medication/CreateMedicationAdministrationRequest.cs
│ ├── Qsofa/QsofaCurrentResponse.cs
│ └── Sepsis/QsofaResult.cs, QsofaOutcome.cs # qSOFA detector result and outcome enum
├── Data/
│ ├── AppDbContext.cs # EF Core context — entity configs, indexes, constraints
@@ -308,7 +324,21 @@ tests/
├── AlertSuppressionTests.cs # Suppression on acknowledge, read-side skip, TTL expiry
├── QsofaCalculatorTests.cs # Boundary tests for three qSOFA criteria
├── QsofaDetectorTests.cs # qSOFA detector — two-criteria alert, normalization, idempotency
── SepsisBundleTests.cs # Bundle creation from SIRS/qSOFA, element completion, compliance outcomes
── SepsisBundleTests.cs # Bundle creation from SIRS/qSOFA, element completion, compliance outcomes
├── MedicationServiceTests.cs # Medication CRUD, discharged encounter rejection, pagination
├── MedicationCorrelationTests.cs # End-to-end warning/NEWS2 annotation with medication context
├── MedicationValidationTests.cs # FluentValidation 400 on invalid medication requests
├── EncountersListTests.cs # Ward encounter list filters and summary fields
└── QsofaCurrentTests.cs # qSOFA current API — Redis state, criteria breakdown
VigilCare.Simulator/ # Phase 16 — console replay simulator (HTTP-only, no direct DB/Kafka)
├── Program.cs # CLI: replay, replay-all, validate, dry-run
├── Commands/ # System.CommandLine command handlers
├── Client/VigilCareApiClient.cs # Typed HTTP client for all API endpoints
├── Engine/ReplayEngine.cs # Scenario replay with speed multiplier + event logging
├── Polling/ApiPoller.cs # Optional post-event alert/score/bundle polling
├── Scenarios/ # schema.json, ScenarioLoader, ScenarioValidator
└── Scenarios/List/ # Eight sample scenarios (sepsis, NEWS2, stable, medication, …)
scripts/
├── run-api-redis-tests.sh # Phase 1 — patient/encounter/threshold + Redis cache
@@ -322,13 +352,17 @@ scripts/
├── run-phase10-verification.sh # Phase 10 — 12 Redis thresholds, clinical enrichment, ES pipeline, integration tests
├── run-phase11-verification.sh # Phase 11 — warning alerts, orders API, validation, integration tests
├── run-phase12-verification.sh # Phase 12 — NEWS2 end-to-end pipeline, API, ES, Prometheus, integration tests
── run-phase13-verification.sh # Phase 13 — trend detection, alert suppression, consumer lag, integration tests
── run-phase13-verification.sh # Phase 13 — trend detection, alert suppression, consumer lag, integration tests
├── run-phase14-verification.sh # Phase 14 — qSOFA, sepsis bundle compliance, integration tests
└── run-phase15-verification.sh # Phase 15 — medication administration + correlation annotations
docs/
├── plans/ # Phase 113 implementation and verification guides
├── plans/ # Phase implementation and verification guides
├── simulator-guide.md # VigilCare.Simulator user guide
├── decisions/
│ ├── data-lake-design.md # Parquet vs JSON, partitioning, replay rationale
── sepsis-engine-design.md # SIRS sliding window and idempotent alert design
── sepsis-engine-design.md # SIRS sliding window and idempotent alert design
│ └── medication-correlation-design.md # Drug-vital mapping and annotation rationale
├── docker-compose-usage-and-troubleshooting.md
└── vigilcare-clinical-api-prd.md # Product requirements and phase roadmap
```
@@ -458,6 +492,18 @@ On startup the application:
Swagger UI is available at `http://localhost:5270/swagger` in Development (API binds to `0.0.0.0:5270` per `launchSettings.json`).
### Run the Simulator
With the API running, replay a scenario from the repository root:
```bash
dotnet run --project VigilCare.Simulator -- replay \
VigilCare.Simulator/Scenarios/List/uti-sepsis-elderly-01.json \
--speed 60 --poll
```
Other commands: `validate <file>`, `dry-run <file>`, `replay-all <directory>`. See `docs/simulator-guide.md` for the full user guide.
### Run Tests
```bash
@@ -487,6 +533,11 @@ Integration tests use `WebApplicationFactory` with a `Testing` environment and T
| `QsofaCalculatorTests` | 14 | Boundary tests for three qSOFA criteria (RESP_RATE, SYSTOLIC_BP, AVPU) |
| `QsofaDetectorTests` | 14 | qSOFA detector — two-criteria alert, normalization key delete, idempotent duplicate, non-qSOFA code ignored |
| `SepsisBundleTests` | 14 | Bundle creation from SIRS/qSOFA, four auto-orders, element completion, compliant/non-compliant outcomes, monitor marks overdue bundles, idempotency |
| `MedicationServiceTests` | 15 | Medication create/list on active encounter, discharged encounter 409, pagination, `since` filter |
| `MedicationCorrelationTests` | 15 | Warning and NEWS2 alert details annotated when correlated drug administered |
| `MedicationValidationTests` | 15 | FluentValidation 400 on empty drug name, zero dose, future `administeredAt` |
| `EncountersListTests` | — | Ward encounter list — status/department filters, summary fields |
| `QsofaCurrentTests` | — | `GET /qsofa/current` — criteria count and breakdown from Redis |
### Verification Scripts
@@ -499,6 +550,8 @@ With the API running (`dotnet run`) and Docker Compose up:
./scripts/run-phase11-verification.sh # Warning alert pipeline, orders API, FluentValidation, Phase 11 integration tests
./scripts/run-phase12-verification.sh # NEWS2 end-to-end pipeline, API, Elasticsearch, Prometheus, Phase 12 integration tests
./scripts/run-phase13-verification.sh # Trend detection, alert suppression, consumer lag, Phase 13 integration tests
./scripts/run-phase14-verification.sh # qSOFA, sepsis bundle compliance, Phase 14 integration tests
./scripts/run-phase15-verification.sh # Medication administration + correlation annotation pipeline
```
Phase 13 unit/integration tests only:
@@ -507,6 +560,12 @@ Phase 13 unit/integration tests only:
dotnet test --filter "FullyQualifiedName~Trend|FullyQualifiedName~Suppression"
```
Phase 15 unit/integration tests only:
```bash
dotnet test --filter "FullyQualifiedName~Medication"
```
Per-phase test runners (subset of `dotnet test`):
```bash
@@ -608,10 +667,16 @@ Error response:
| Method | Path | Description |
|---|---|---|
| GET | `/encounters` | Paginated ward list with clinical summaries; optional `status`, `department` filters |
| POST | `/patients/{id}/encounters` | Open an encounter |
| GET | `/encounters/{id}` | Encounter detail with recent observations and open alerts |
| PATCH | `/encounters/{id}/status` | Advance encounter status |
| GET | `/encounters/{id}/timeline` | Merged chronological view: status changes, observations, alerts |
| GET | `/encounters/{id}/qsofa/current` | Current qSOFA active criteria count (03) from Redis |
**GET `/encounters` query params:** `status` (DB literal, e.g. `ACTIVE`), `department` (DB literal, e.g. `ICU`), `page`, `pageSize`
**Ward summary fields:** `encounterId`, `patientId`, `mrn`, `firstName`, `lastName`, `roomBed`, `department`, `status`, `news2Score`, `news2RiskLevel`, `qsofaScore`, `sepsisActive`, `sepsisBundleStatus`, `openAlertCount`
**Encounter status machine:**
@@ -830,6 +895,27 @@ Scores are computed asynchronously by `News2ScoringService` after observations a
Bundles are created automatically by `SepsisAlertHandler` when a SIRS or qSOFA alert fires. Four clinical orders (blood cultures, serum lactate, broad-spectrum antibiotics, IV fluid resuscitation) are auto-created with `orderedBy: sepsis-bundle-engine`. As orders are resulted via `PATCH /orders/{id}/result`, the corresponding bundle element is marked complete. When all four elements are done, the bundle transitions to `COMPLIANT` (within the 1-hour deadline) or `NON_COMPLIANT`.
### Medications
| Method | Path | Description |
|---|---|---|
| POST | `/encounters/{id}/medications` | Record a medication administration for an active encounter |
| GET | `/encounters/{id}/medications` | List administrations; optional `since` ISO 8601 filter; paginated |
| GET | `/medications/{id}` | Medication administration detail |
**POST body:**
| Field | Type | Required | Description |
|---|---|---|---|
| `drugName` | string | yes | Drug name (case-insensitive for correlation lookups) |
| `dose` | decimal | yes | Must be > 0 |
| `doseUnit` | string | yes | e.g. `mg`, `g`, `mcg` |
| `route` | string | yes | e.g. `PO`, `IV` |
| `administeredAt` | DateTimeOffset | no | Defaults to server time if omitted |
| `administeredBy` | string | yes | Clinician or nurse identifier |
When a correlated drug was given within the `MedicationCorrelation.CorrelationWindowMinutes` window (default 90), subsequent warning and NEWS2 alerts for affected vitals include an annotation in `details` — e.g. `— note: metoprolol 25mg (PO) administered 45 min ago`. See `docs/decisions/medication-correlation-design.md`.
---
## Data Models
@@ -988,6 +1074,21 @@ Indexes: unique `(bundle_id, element_type)`, `(order_id)`
Check constraints: `element_type IN (...)`, `status IN ('PENDING', 'COMPLETED')`
### MedicationAdministration
```
id Guid PK
encounterId Guid FK → Encounter (Restrict on delete)
drugName string required (max 200)
dose decimal(10,4) required
doseUnit string required (max 20)
route string required (max 20)
administeredAt DateTimeOffset required
administeredBy string required (max 200)
```
Indexes: `(encounter_id, administered_at)`, `(encounter_id, drug_name)`
### OutboxEvent
```
@@ -1145,6 +1246,8 @@ The qSOFA (quick Sequential Organ Failure Assessment) engine evaluates three org
Redis key pattern: `qsofa:{encounterId}:{code}` with 30-minute TTL. When a criterion normalizes, the key is deleted immediately. When ≥ 2 of 3 criteria are active simultaneously and no open `QSOFA_WARNING` alert exists, the engine inserts a `CRITICAL` alert with details formatted as `"qSOFA score 2/3: RESP_RATE=24, SYSTOLIC_BP=95"`. A successful qSOFA alert triggers sepsis bundle creation via `SepsisAlertHandler`.
**API:** `GET /encounters/{id}/qsofa/current` returns `activeCriteria` (03) and per-criterion values from Redis via `QsofaService` — used by ward dashboards and the simulator poll loop.
**Clinical distinction:** SIRS detects systemic inflammation (infection response); qSOFA detects organ dysfunction (Sepsis-3 consensus). Both can fire independently for the same encounter. The sepsis bundle is idempotent — if one is already in progress, the second alert does not create a duplicate.
---
@@ -1279,7 +1382,7 @@ Observation history uses cursor pagination on `(recorded_at DESC, id DESC)`. Off
## Implemented Phases
Fourteen phases from the project roadmap are implemented and verified. Integration tests (`dotnet test` — 106 test methods) and per-phase verification scripts cover Phases 814.
Sixteen phases from the project roadmap are implemented and verified. Integration tests (`dotnet test` — 116 test methods) and per-phase verification scripts cover Phases 815.
| Phase | Feature | Status |
|---|---|---|
@@ -1297,5 +1400,9 @@ Fourteen phases from the project roadmap are implemented and verified. Integrati
| 12 | NEWS2 composite scoring (`News2Calculator`, `News2Detector`, `News2ScoringService`); `news2_scores` table; `NEWS2_WARNING` / `NEWS2_EMERGENCY` alert types; `News2Controller` (current + history); ES `news2Score` / `news2RiskLevel` projection; Prometheus NEWS2 metrics; `News2CalculatorTests`, `News2DetectorTests`; `run-phase12-verification.sh` | Done |
| 13 | Trend detection (`TrendCalculator`, `TrendDetector`, `TrendAnalyzerService`); `RAPID_DETERIORATION` alert type; alert suppression windows (`AlertSuppressionService`, Redis `suppress:{enc}:{type}`); `TrendCalculatorTests`, `TrendDetectorTests`, `AlertSuppressionTests`; `run-phase13-verification.sh` | Done |
| 14 | qSOFA scoring engine (`QsofaCalculator`, `QsofaDetector`); `QSOFA_WARNING` alert type; sepsis bundle compliance (`SepsisBundle`, `SepsisBundleElement`, `SepsisBundleService`); auto-created treatment orders with 1-hour deadline; `SepsisAlertHandler` bridge; `SepsisBundleMonitorService` (5-min overdue scan); `SepsisBundlesController` API; ES projection of bundle status; Kafka topics `sepsis.bundle.created` / `sepsis.bundle.updated`; Prometheus `qsofa_detections_total` and `sepsis_bundle_compliance_total`; `QsofaCalculatorTests`, `QsofaDetectorTests`, `SepsisBundleTests` | Done |
| 15 | Medication administration (`MedicationAdministration`, `MedicationsController`, `MedicationService`); drug-vital correlation config (`MedicationCorrelationOptions`); `MedicationCorrelationHelper` annotates `WarningEvaluator` and `News2Detector` alert details; `medication_administrations` table + migration; `MedicationServiceTests`, `MedicationCorrelationTests`, `MedicationValidationTests`; `run-phase15-verification.sh`; design doc in `docs/decisions/medication-correlation-design.md` | Done |
| 16 | Console replay simulator (`VigilCare.Simulator`); scenario JSON schema; CLI commands `replay`, `replay-all`, `validate`, `dry-run`; speed multiplier and optional API polling; eight sample scenarios; `docs/simulator-guide.md` | Done |
**Ward dashboard backend (in progress):** `GET /encounters` ward list with `WardEncounterSummary`, `GET /encounters/{id}/qsofa/current`, `DashboardOptions` CORS — `EncountersListTests`, `QsofaCurrentTests`.
**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.