From 68c397350b206c56203d7739cfb70a0f5dc53dff Mon Sep 17 00:00:00 2001 From: voltsrage Date: Wed, 24 Jun 2026 03:39:24 +0800 Subject: [PATCH] =?UTF-8?q?Finish:=20Phase=2033=20=E2=80=94=20Alert=20Qual?= =?UTF-8?q?ity=20Analytics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 208 +++++++++++++++--- .../Auth/TestingAuthHandler.cs | 3 +- .../Helpers/RabbitMqTestHelper.cs | 43 +++- .../RabbitMqOptionsTests.cs | 15 ++ .../Common/Exceptions/BadRequestException.cs | 5 + .../Middlewares/ExceptionHandlerMiddleware.cs | 6 + .../RabbitMqTopologyProvisioner.cs | 40 +++- VigilCareClinicalAPI/Services/AlertService.cs | 2 +- docs/VigilCare-Partner-Brief.md | 10 + docs/interview-questions.md | 20 ++ scripts/run-phase24b-verification.sh | 57 +++++ .../src/__tests__/FeedbackButtons.test.js | 43 +++- .../src/__tests__/FeedbackSummary.test.js | 30 ++- .../src/__tests__/alertQuality.test.js | 37 ++++ .../src/__tests__/roleAccess.test.js | 4 +- vigilcare-dashboard/src/api/alertQuality.js | 37 ++++ .../src/components/alerts/AlertCard.vue | 6 +- .../components/charts/AlertQualityChart.vue | 65 ++++++ .../components/feedback/FeedbackButtons.vue | 54 +++-- .../src/components/layout/AppSidebar.vue | 15 ++ .../src/composables/roleAccess.js | 2 +- vigilcare-dashboard/src/router/index.js | 16 +- .../src/stores/alertQuality.js | 121 ++++++++++ vigilcare-dashboard/src/stores/feedback.js | 105 ++------- .../src/views/AlertQualityAnalytics.vue | 156 +++++++++++++ .../src/views/FeedbackSummary.vue | 3 +- 26 files changed, 934 insertions(+), 169 deletions(-) create mode 100644 VigilCareClinicalAPI.Tests/RabbitMqOptionsTests.cs create mode 100644 VigilCareClinicalAPI/Common/Exceptions/BadRequestException.cs create mode 100644 docs/VigilCare-Partner-Brief.md create mode 100644 docs/interview-questions.md create mode 100644 scripts/run-phase24b-verification.sh create mode 100644 vigilcare-dashboard/src/__tests__/alertQuality.test.js create mode 100644 vigilcare-dashboard/src/api/alertQuality.js create mode 100644 vigilcare-dashboard/src/components/charts/AlertQualityChart.vue create mode 100644 vigilcare-dashboard/src/stores/alertQuality.js create mode 100644 vigilcare-dashboard/src/views/AlertQualityAnalytics.vue diff --git a/README.md b/README.md index 9152c03..4f624a4 100644 --- a/README.md +++ b/README.md @@ -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:** Twenty-nine planned phases are complete through Phase 31 (plus Phases 20–22) — from schema and CRUD through Kafka, Elasticsearch CQRS, sepsis detection, RabbitMQ paging with DLQ escalation, reconciliation jobs, Prometheus/Grafana observability, the MinIO Parquet data lake, clinical data model expansion, warning alerts and orders, the NEWS2 composite scoring engine, trend detection with alert suppression, qSOFA bedside screening, medication administration with alert correlation annotations, the console replay simulator, the **Vue 3 ward dashboard**, clinician feedback mode, **Glasgow Coma Scale (GCS) scoring**, **SOFA organ-dysfunction scoring with baseline tracking and delta sepsis alerts**, the **Sepsis-3 clinical refactor** (SIRS removed, qSOFA repositioned as screening, SOFA delta ≥ 2 triggers bundles), **frontend GCS entry and SOFA display**, **expanded simulator scenarios with clinical validation**, the **Site & Gateway Registry** with dual authentication, shared clinical sync contracts, and fleet health Prometheus gauges, the **Ward Gateway Service** (local-first clinical path with offline buffering and central sync), the **FHIR R4 Inbound Facade** for EHR integration, **Role-Based Access Control (RBAC) with clinical audit logging**, the **Dashboard Gap Analysis Fixes** (SOFA/GCS/qSOFA history charts, patient banner, encounter timeline, medication markers on vital charts), and the **Enhanced Dashboard** (department overview, sepsis bundle board, critical alert notifications, shift handoff reports, vitals entry form, sortable/filterable ward table). Post-phase hardening includes health check endpoints, Kafka poison pill protection, outbox dead-letter with retry tracking, data lake partial-commit safety, MRN sequence-based generation, FHIR bundle transaction rollback, **FHIR R4 read/search endpoints** (Patient and Encounter), **alert threshold deletion with audit trail**, **FHIR API key rotation** (constant-time multi-key validation), **authorization failure logging** with Prometheus metrics, **JWT signing key validation** at startup, and **concurrency hardening** (transactional sepsis bundle creation, unique active encounter constraint). 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:** Thirty-one planned phases are complete through Phase 33 (plus Phases 20–23) — from schema and CRUD through Kafka, Elasticsearch CQRS, sepsis detection, RabbitMQ paging with DLQ escalation, reconciliation jobs, Prometheus/Grafana observability, the MinIO Parquet data lake, clinical data model expansion, warning alerts and orders, the NEWS2 composite scoring engine, trend detection with alert suppression, qSOFA bedside screening, medication administration with alert correlation annotations, the console replay simulator, the **Vue 3 ward dashboard**, clinician feedback mode, **Glasgow Coma Scale (GCS) scoring**, **SOFA organ-dysfunction scoring with baseline tracking and delta sepsis alerts**, the **Sepsis-3 clinical refactor** (SIRS removed, qSOFA repositioned as screening, SOFA delta ≥ 2 triggers bundles), **frontend GCS entry and SOFA display**, **expanded simulator scenarios with clinical validation**, the **Site & Gateway Registry** with dual authentication, shared clinical sync contracts, and fleet health Prometheus gauges, the **Ward Gateway Service** (local-first clinical path with offline buffering and central sync), the **FHIR R4 Inbound Facade** for EHR integration, **Role-Based Access Control (RBAC) with clinical audit logging**, the **Dashboard Gap Analysis Fixes** (SOFA/GCS/qSOFA history charts, patient banner, encounter timeline, medication markers on vital charts), the **Enhanced Dashboard** (department overview, sepsis bundle board, critical alert notifications, shift handoff reports, vitals entry form, sortable/filterable ward table), **Degraded Operations Visibility** (gateway fleet operations panel, stale gateway auto-detection, discharge summary API, admin panels for user/threshold/audit/reconciliation management, degraded-mode banner), and **Alert Quality Analytics** (server-side clinician feedback with `AlertFeedback` entity, `AlertQualityAggregatorService` background metrics, quality metrics API, Grafana alert quality dashboard). Post-phase hardening includes health check endpoints, Kafka poison pill protection, outbox dead-letter with retry tracking, data lake partial-commit safety, MRN sequence-based generation, FHIR bundle transaction rollback, **FHIR R4 read/search endpoints** (Patient and Encounter), **alert threshold deletion with audit trail**, **FHIR API key rotation** (constant-time multi-key validation), **authorization failure logging** with Prometheus metrics, **JWT signing key validation** at startup, and **concurrency hardening** (transactional sepsis bundle creation, unique active encounter constraint). 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 @@ -72,10 +72,10 @@ An `OutboxEvent` is written in the same transaction as any observation or alert, - **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, SOFA score/delta, GCS score/classification, attending physician, admitted-at, last observation time); filterable by `status` and `department`; `GET /encounters/:id/qsofa/current` exposes Redis-backed qSOFA state; `GET /sepsis-bundles` lists bundles hospital-wide with optional `status` filter (returns `SepsisBundleSummary` with patient demographics, elements, and deadlines); CORS policy `Dashboard` allows configured origins (default `http://localhost:5173`) -- **Ward Dashboard Frontend** — Vue 3 SPA (`vigilcare-dashboard/`) with virtual ward table (multi-column sortable, patient search, quick-filters for critical/alerts/sepsis), patient detail (vitals, scores, alerts, orders, sepsis bundle, GCS entry form, SOFA score panel, patient banner with demographics/allergies/emergency contact, encounter timeline, vitals entry form for manual observation recording), alert center (global acknowledge/resolve with role-aware modal and acknowledgment note preview), department overview (unit-level snapshot cards with acuity bars, patient/alert/bundle counts per department), sepsis bundle board (real-time countdown timers, on-track/at-risk/overdue urgency sorting), critical alert banner with browser notifications and audible tone, shift handoff report generator (SBAR format with ward summary, exportable via print/PDF), vital sign trend charts with medication administration markers and local replay scrubbing, NEWS2 history chart, SOFA history chart with organ-system breakdown, GCS history chart with component tracking, qSOFA evaluation history, 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 (multi-column sortable, patient search, quick-filters for critical/alerts/sepsis), patient detail (vitals, scores, alerts, orders, sepsis bundle, GCS entry form, SOFA score panel, patient banner with demographics/allergies/emergency contact, encounter timeline, vitals entry form for manual observation recording, discharge summary panel), alert center (global acknowledge/resolve with role-aware modal and acknowledgment note preview), department overview (unit-level snapshot cards with acuity bars, patient/alert/bundle counts per department), sepsis bundle board (real-time countdown timers, on-track/at-risk/overdue urgency sorting), critical alert banner with browser notifications and audible tone, shift handoff report generator (SBAR format with ward summary, exportable via print/PDF), vital sign trend charts with medication administration markers and local replay scrubbing, NEWS2 history chart, SOFA history chart with organ-system breakdown, GCS history chart with component tracking, qSOFA evaluation history, alert reasoning with optional medication context, clinician feedback on every alert, admin panels (threshold management, user management, audit log viewer, reconciliation viewer), gateway operations dashboard with degraded-mode banner, and alert quality analytics with quality charts; role-aware sidebar navigation; 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; `FhirApiKeyOrJwtMiddleware` authenticates via JWT bearer or `X-Api-Key` header (supports multiple keys via `Fhir:ApiKeys` array for zero-downtime rotation; constant-time comparison via `CryptographicOperations.FixedTimeEquals`); `FhirExceptionFilter` returns FHIR `OperationOutcome` on errors; configurable identifier systems, department codes, and encounter class mappings via `Fhir` config section; Prometheus `fhir_ingest_total` and `fhir_mapping_errors_total`; integration guide for Mirth Connect HL7v2→FHIR channels in `docs/integration/mirth-fhir-channels.md` - **FHIR R4 Read/Search** — `GET /fhir/R4/Patient/{id}` reads a Patient by internal ID; `GET /fhir/R4/Patient` searches by `identifier` (system|value) or lists all patients; `GET /fhir/R4/Encounter/{id}` reads an Encounter by internal ID; `GET /fhir/R4/Encounter` searches by `patient` (UUID) and/or `status` (`in-progress`, `finished`, `cancelled`); all return FHIR R4 JSON (`application/fhir+json`); search endpoints return `Bundle.type=searchset`; requires `fhir:read` permission (Admin and Integration roles); internal resources mapped back to FHIR via `PatientFhirMapper.ToFhirResponse` / `EncounterFhirMapper.ToFhirResponse` with hospital identifier resolution; Prometheus `fhir_read_total` counter with `resource_type`, `interaction`, `outcome` labels -- **Role-Based Access Control (RBAC)** — JWT bearer authentication (`POST /auth/login`); four clinical roles (`Nurse`, `Physician`, `Admin`, `Integration`) with 17 granular permissions (`patients:read`, `alerts:acknowledge`, `thresholds:write`, `fhir:ingest`, `fhir:read`, `audit:read`, etc.); `AuthorizePermission` attribute on every controller action; `PermissionAuthorizationHandler` resolves role → permission at runtime from `ClinicalRolePermissionMap` and logs authorization failures with structured details (user, role, permission, endpoint) plus `authorization_failures_total` Prometheus counter; `CurrentUserService` exposes authenticated identity (user ID, display name, role, IP address) to services; nurses and physicians get clinical read/write permissions; admins additionally get `thresholds:write`, `fhir:read`, `audit:read`, and `users:admin`; integration accounts get FHIR ingest and read access; FHIR endpoints accept both JWT and `X-Api-Key` authentication via `FhirApiKeyOrJwtMiddleware`; alert `acknowledgedBy` is set from the authenticated user identity, not the request body; startup validates JWT signing key is at least 256 bits (HMAC-SHA256 minimum); 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 +- **Role-Based Access Control (RBAC)** — JWT bearer authentication (`POST /auth/login`); four clinical roles (`Nurse`, `Physician`, `Admin`, `Integration`) with 18 granular permissions (`patients:read`, `alerts:acknowledge`, `alerts:feedback`, `thresholds:write`, `fhir:ingest`, `fhir:read`, `audit:read`, etc.); `AuthorizePermission` attribute on every controller action; `PermissionAuthorizationHandler` resolves role → permission at runtime from `ClinicalRolePermissionMap` and logs authorization failures with structured details (user, role, permission, endpoint) plus `authorization_failures_total` Prometheus counter; `CurrentUserService` exposes authenticated identity (user ID, display name, role, IP address) to services; nurses and physicians get clinical read/write permissions; admins additionally get `thresholds:write`, `fhir:read`, `audit:read`, and `users:admin`; integration accounts get FHIR ingest and read access; FHIR endpoints accept both JWT and `X-Api-Key` authentication via `FhirApiKeyOrJwtMiddleware`; alert `acknowledgedBy` is set from the authenticated user identity, not the request body; startup validates JWT signing key is at least 256 bits (HMAC-SHA256 minimum); 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; ten audit actions (`THRESHOLD_CREATED`, `THRESHOLD_UPDATED`, `THRESHOLD_DELETED`, `ALERT_ACKNOWLEDGED`, `ALERT_RESOLVED`, `ENCOUNTER_STATUS_CHANGED`, `PATIENT_REGISTERED`, `SUPPRESSION_WINDOW_SET`, `USER_LOGIN`, `AUTHORIZATION_DENIED`); `AuditService` writes log entries inline with domain operations; `GET /audit-logs` admin-only query endpoint with filters by entity type, entity ID, user ID, action, and time range; indexed on entity type, entity ID, user ID, and timestamp - **Site & Gateway Registry** — `ClinicalSite` and `WardGateway` domain entities model ward edge nodes that buffer clinical data during connectivity loss; `POST /sites` creates clinical sites; `POST /sites/{siteId}/gateways` registers gateways under a site; `PATCH /gateways/{gatewayId}/heartbeat` (gateway API key auth) updates status (`ONLINE`, `DEGRADED`, `OFFLINE`) and reported buffer depth; `GET /sites/{siteId}/gateways` lists gateways with optional `?department=` filter; dual authentication — JWT + RBAC (`users:admin`) for admin CRUD, `GatewayApiKeyAuthenticationHandler` (`X-Api-Key` + `X-Gateway-Id`) for gateway heartbeat and future sync upload; constant-time key comparison via `CryptographicOperations.FixedTimeEquals`; `VigilCare.ClinicalContracts` shared class library with sync DTOs (`ClinicalSyncBatchRequest`, `SyncedObservation`, `SyncedAlertEvent`, `GatewayHeartbeatRequest`) consumed by both central API and ward gateway projects; Prometheus `ward_gateways_offline_gauge` and `ward_gateway_buffer_depth` via `WardGatewayMetricsCollector` (60s periodic); `GatewayRegistrySeeder` provides demo site and gateway for Docker Compose and tests; FluentValidation on all request DTOs; `GatewayRegistryTests` and `ClinicalContractsTests` integration tests - **Ward Gateway Service** — `VigilCare.WardGateway` (`http://localhost:5081`) is a standalone ASP.NET Core 8 deployable with its own PostgreSQL, Redis, and RabbitMQ; ingests observations locally via `POST /encounters/:id/observations` with plausibility validation, Redis-cached threshold evaluation, and synchronous critical alert creation; `LocalWarningEvaluator` creates warning-range alerts; `BufferedSyncWriter` writes all clinical events to `buffered_sync_items` for central upload; `EncounterReplicaSyncService` pulls patient/encounter data from central API; `ThresholdCacheLoader` fetches thresholds from central into local Redis; `CentralReachabilityService` tracks central API connectivity; `GatewayHeartbeatService` reports status and buffer depth; `SyncUploaderService` batches and uploads buffered items when online; local RabbitMQ paging and escalation queues; `GET /encounters` ward list and `GET /encounters/:id` detail; `GET /health/live` and `GET /health/ready` (Redis, RabbitMQ, encounter replica readiness); Docker Compose `ward-gateway` profile @@ -86,7 +86,11 @@ An `OutboxEvent` is written in the same transaction as any observation or alert, - **ThresholdCacheLoader Resilience** — retries Redis connection up to 3 times with exponential backoff (2s, 4s, 8s); if Redis remains unavailable, the application starts without the cache and observation ingest falls back to PostgreSQL queries for threshold lookups - **MRN Sequence Generation** — MRN numbers are generated via a PostgreSQL sequence (`mrn_seq`) instead of MAX+1 queries; eliminates race conditions under concurrent patient registration; configurable prefix and digit count via `PatientOptions` - **FHIR Bundle Transaction Rollback** — `FhirBundleProcessor` wraps all bundle entry processing in a database transaction; on any entry failure, the transaction is rolled back and the response includes the `OperationOutcome` for the failed entry; prevents partial state from orphaned Patient/Encounter records -- **Clinician Feedback Mode** — six quick ratings per alert (useful, too early, too late, false positive, missing context, would act); optional notes; 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; server-side `AlertFeedback` entity persisted per user per alert (`POST /alerts/{id}/feedback`); `alerts:feedback` permission for Nurse, Physician, and Admin roles; Feedback Summary with aggregate stats and JSON/CSV export; client-side persistence for product research +- **Alert Quality Analytics** — `AlertQualityAggregatorService` periodically computes per-alert-type quality metrics (acknowledgement rate, false positive rate, useful rate, would-act rate, avg seconds to acknowledge/resolve) into `alert_quality_metrics` table; `AlertQualityMetricsController` exposes `GET /alerts/quality-metrics` (time-range filterable, optional alert type) and `GET /alerts/quality-metrics/summary`; Grafana alert quality dashboard (`infra/grafana/dashboards/alert-quality-dashboard.json`); frontend `AlertQualityAnalytics.vue` with `AlertQualityChart.vue`; Prometheus `alert_quality_useful_rate` and `alert_quality_false_positive_rate` gauges +- **Degraded Operations Visibility** — `GatewayStaleDetectorService` auto-marks gateways OFFLINE when heartbeat exceeds configurable `StaleThresholdMinutes`; `OperationsController` (`GET /operations/gateways`, `GET /operations/gateways/{id}`, `GET /operations/sites/{siteId}/summary`) provides fleet management API; `DischargeSummaryService` with `GET /encounters/{id}/discharge-summary` (info) and `GET /encounters/{id}/discharge-summary/content` (MinIO PDF download); `DischargeSummaryPanel.vue` on patient detail; `DegradedModeBanner.vue` warns when gateways are offline; `GatewayOperations.vue` operations dashboard +- **User Management** — `UsersController` (`GET /users`, `POST /users`, `PATCH /users/{id}`) for admin user account CRUD; `UserService` with BCrypt password hashing; `UserManagementView.vue` with `UserFormModal.vue` (create/edit users, role assignment, active toggle) +- **Admin Dashboard Panels** — `ThresholdManagementView.vue` with `ThresholdFormModal.vue` (create/edit alert thresholds); `AuditLogView.vue` (filterable audit log viewer with action/entity/user/date filters); `ReconciliationView.vue` (safety finding viewer); sidebar navigation with role-aware admin section; `CollapsibleSection.vue` and `SeverityBadge.vue` UI components - **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; `--gateway` targets the ward gateway (`http://localhost:5081`) with `--encounter-id`, `--skip-setup`, and `--gateway-token`; `alert_ack` events poll for open alerts on central before acknowledging (handles async alert pipeline at `--speed 0`); twelve sample scenarios in `VigilCare.Simulator/Scenarios/List/` (including ward outage reconnect, 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` - **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 for topic-partitions where all uploads succeeded; failed partition buffers are retained in memory and retried on the next flush cycle (prevents data loss from partial upload failures); shutdown flush uses an uncanceled token so MinIO writes complete on Ctrl+C; `kafka_partition` and `kafka_offset` columns provide audit lineage @@ -140,7 +144,9 @@ IHostedServices (background): OutboxPendingCollector → polls outbox every 30s → outbox_pending_events KafkaConsumerLagCollector → polls four consumer groups every 30s → kafka_consumer_lag WardGatewayMetricsCollector → polls gateway status/buffer depth every 60s → ward_gateways_offline_gauge, ward_gateway_buffer_depth - ClinicalMetrics (singleton) → inline counters/histogram from ingest, qSOFA, NEWS2, GCS, SOFA, trend, suppression, bundle compliance, escalation paths + GatewayStaleDetectorService → polls gateways on interval → marks OFFLINE when heartbeat exceeds StaleThresholdMinutes + AlertQualityAggregatorService → periodically computes per-alert-type quality metrics → alert_quality_metrics table + Prometheus gauges + ClinicalMetrics (singleton) → inline counters/histogram from ingest, qSOFA, NEWS2, GCS, SOFA, trend, suppression, bundle compliance, escalation paths, alert quality Ward Gateway (VigilCare.WardGateway — separate deployable on port 5081): HTTP request → ExceptionHandlerMiddleware → Controllers (Observations, Alerts, Encounters, CentralRequired) @@ -212,6 +218,9 @@ VigilCareClinicalAPI/ │ ├── SepsisBundlesController.cs # Current bundle per encounter, bundle detail by ID │ ├── SitesController.cs # Clinical site CRUD (create, list, get) │ ├── GatewaysController.cs # Gateway register, list, get, heartbeat (dual auth: JWT + API key) +│ ├── OperationsController.cs # Gateway fleet operations: fleet list, gateway detail, site summary (Admin only) +│ ├── UsersController.cs # Clinical user account management: list, create, update (Admin only) +│ ├── AlertQualityMetricsController.cs # Alert quality metric snapshots and aggregate summary │ ├── FhirIngestController.cs # FHIR R4 ingest: Patient, Encounter, Observation, MedicationAdministration, Bundle │ ├── FhirReadController.cs # FHIR R4 read/search: GET Patient/{id}, GET Patient, GET Encounter/{id}, GET Encounter │ ├── FhirMetadataController.cs # FHIR R4 CapabilityStatement (GET /fhir/R4/metadata) @@ -237,7 +246,9 @@ VigilCareClinicalAPI/ │ │ ├── ClinicalSite.cs # Hospital site with site code, name, address │ │ ├── WardGateway.cs # Ward edge node with status, buffer depth, heartbeat, sync timestamps │ │ ├── ClinicalUser.cs # Username, BCrypt password hash, display name, role, active flag -│ │ └── ClinicalAuditLog.cs # Append-only audit trail: action, entity, user, before/after JSONB, IP, correlation ID +│ │ ├── ClinicalAuditLog.cs # Append-only audit trail: action, entity, user, before/after JSONB, IP, correlation ID +│ │ ├── AlertFeedback.cs # Clinician feedback per alert (one per user per alert) +│ │ └── AlertQualityMetric.cs # Per-alert-type quality metric snapshots (acknowledgement/false-positive/useful rates) │ └── Enums/ │ ├── EncounterStatus.cs # Scheduled, Active, Discharged, Cancelled │ ├── ClinicalRole.cs # Nurse, Physician, Admin, Integration @@ -246,6 +257,7 @@ VigilCareClinicalAPI/ │ ├── AlertSeverity.cs # Warning, Critical │ ├── AlertStatus.cs # Open, Acknowledged, Resolved, Escalated │ ├── AlertType.cs # Threshold breach, QSOFA_SCREEN, SOFA_SEPSIS, warning*, NEWS2_*, GCS_*, … +│ ├── AlertFeedbackType.cs # Useful, TooEarly, TooLate, FalsePositive, MissingContext, WouldAct │ ├── GatewayStatus.cs # Online, Degraded, Offline with ToDbString/FromDbString │ ├── BloodType.cs # A+, O-, AB-, … with ToDbString/FromDbString │ ├── ObservationSource.cs # Device, Manual, Lab @@ -305,6 +317,10 @@ VigilCareClinicalAPI/ │ ├── ExternalIdentifierService.cs # Links/resolves external system identifiers to internal UUIDs │ ├── SiteService.cs # Clinical site CRUD │ ├── GatewayRegistryService.cs # Gateway register, list, heartbeat, mark offline +│ ├── OperationsService.cs # Gateway fleet queries, gateway detail, site summary aggregation +│ ├── UserService.cs # Clinical user account CRUD with BCrypt password hashing +│ ├── DischargeSummaryService.cs # Discharge summary info and MinIO PDF content retrieval +│ ├── AlertQualityMetricsService.cs # Alert quality metric listing and summary computation │ ├── WarningEvaluator.cs # Warning-range evaluation; suppression + medication annotation; idempotent INSERT │ ├── AnalyticsService.cs # Elasticsearch query wrappers │ └── PlausibilityValidator.cs # Per-code numeric range guard @@ -326,6 +342,8 @@ VigilCareClinicalAPI/ │ │ ├── OutboxPendingCollector.cs # Polls unprocessed outbox rows → outbox_pending_events │ │ ├── KafkaConsumerLagCollector.cs # Lag for es-indexer, sepsis-engine, notification-publisher, data-lake-writer │ │ └── WardGatewayMetricsCollector.cs # Polls gateway status/buffer depth every 60s → offline gauge, buffer depth +│ ├── GatewayStaleDetectorService.cs # Periodic stale gateway detection → auto-marks OFFLINE +│ ├── AlertQualityAggregatorService.cs # Periodic alert quality metric computation → alert_quality_metrics table │ ├── ElasticsSearch/ │ │ ├── ElasticIndexProvisioner.cs # Creates patient_encounters, observations, clinical_alerts indices │ │ └── EsIndexerService.cs # consumer group: es-indexer; upserts Elasticsearch documents @@ -362,7 +380,9 @@ VigilCareClinicalAPI/ │ ├── PatientOptions.cs # MRN prefix + digit count for sequence-based generation │ ├── FhirOptions.cs # API key (single + rotation array), identifier systems, department/class maps, defaults │ ├── JwtOptions.cs # Issuer, audience, signing key, expiration (default 8 hours) -│ └── DashboardOptions.cs # CORS origins for ward dashboard frontend +│ ├── DashboardOptions.cs # CORS origins for ward dashboard frontend +│ ├── GatewayMonitoringOptions.cs # Stale gateway detection interval and threshold +│ └── AlertQualityOptions.cs # Alert quality aggregation interval ├── Sepsis/ │ ├── AlertCreationGuard.cs # Prevents creation of deprecated alert types (SEPSIS_WARNING) │ ├── QsofaCalculator.cs # Pure static qSOFA scoring (3 criteria, no I/O) @@ -402,7 +422,7 @@ VigilCareClinicalAPI/ │ └── Sepsis/QsofaResult.cs, QsofaOutcome.cs # qSOFA detector result and screening outcome enum ├── Data/ │ ├── AppDbContext.cs # EF Core context — entity configs, indexes, constraints -│ ├── Configurations/ # IEntityTypeConfiguration per entity; ClinicalUserConfiguration, ClinicalAuditLogConfiguration, ClinicalSiteConfiguration, WardGatewayConfiguration, QsofaEvaluationConfiguration; ElasticsearchOptions, ElasticIndexOptions +│ ├── Configurations/ # IEntityTypeConfiguration per entity; ClinicalUserConfiguration, ClinicalAuditLogConfiguration, ClinicalSiteConfiguration, WardGatewayConfiguration, QsofaEvaluationConfiguration, AlertFeedbackConfiguration, AlertQualityMetricConfiguration; ElasticsearchOptions, ElasticIndexOptions │ └── Seed/ │ ├── DataSeeder.cs # Seeds patients, encounters, thresholds, observations │ ├── GatewayRegistrySeeder.cs # Seeds demo site (SITE-DEMO) and gateway (GW-ICU-3B) with fixed GUIDs @@ -426,7 +446,7 @@ infra/ │ └── prometheus.yml # Scrape config for vigilcare_api /metrics └── grafana/ ├── provisioning/ # Datasource + dashboard provider config - └── dashboards/ # vigilcare.json clinical dashboard + └── dashboards/ # vigilcare.json clinical dashboard, alert-quality-dashboard.json tests/ └── VigilCareClinicalAPI.Tests/ @@ -462,7 +482,10 @@ tests/ ├── ConcurrencyTests.cs # Parallel patient MRN, sepsis bundle, observation idempotency, encounter open ├── GapAnalysisFixTests.cs # Phase 22 — GCS history, qSOFA evaluation persistence/history, encounter timeline ├── GatewayRegistryTests.cs # Gateway register, heartbeat, API key auth, department filter + ├── OperationsApiTests.cs # Operations fleet listing, gateway detail, site summary ├── Helpers/GatewayAuthHelper.cs # WithGatewayApiKey extension method for test clients + ├── Alerts/ + │ └── AlertQualityAnalyticsTests.cs # Alert feedback submission, quality aggregation, metrics API ├── Auth/ │ └── RbacTests.cs # RBAC — unauthenticated 401, nurse 403 on threshold write, admin audit log creation └── Fhir/ @@ -547,27 +570,28 @@ VigilCare.Simulator/ # Phase 16 — console replay ├── Output/SimulatorConsole.cs # Colored output with GCS/SOFA score display ├── Polling/ApiPoller.cs # Optional post-event alert/score/bundle/GCS/SOFA polling ├── Scenarios/ # schema.json, ScenarioLoader, ScenarioValidator -└── Scenarios/List/ # Eleven sample scenarios (sepsis, GCS, SOFA, NEWS2, stable, …) +└── Scenarios/List/ # Twelve sample scenarios (sepsis, GCS, SOFA, NEWS2, stable, ward outage, …) -vigilcare-dashboard/ # Phases 17–19, 22, 27–28, 31 — Vue 3 ward dashboard SPA +vigilcare-dashboard/ # Phases 17–19, 22, 23, 27–28, 31, 33 — Vue 3 ward dashboard SPA ├── src/ -│ ├── api/ # HTTP client (auto Bearer header), encounters, clinical (GCS, SOFA, qSOFA history), alerts, analytics, sepsis, normalize +│ ├── api/ # HTTP client (auto Bearer header), encounters, clinical (GCS, SOFA, qSOFA history), alerts, analytics, sepsis, thresholds, users, audit, reconciliation, operations, alertQuality, normalize │ ├── components/ +│ │ ├── admin/ # ThresholdFormModal, UserFormModal (admin CRUD modals) │ │ ├── alerts/ # AlertCard, AcknowledgeModal (role-aware), CriticalAlertBanner (browser notifications + audible tone) -│ │ ├── charts/ # SofaHistory, GcsHistory, QsofaHistory, VitalChart with medication markers +│ │ ├── charts/ # SofaHistory, GcsHistory, QsofaHistory, VitalChart with medication markers, AlertQualityChart │ │ ├── departments/ # DepartmentCard, AcuityBar (unit-level snapshot) │ │ ├── feedback/ # FeedbackButtons, FeedbackSummary -│ │ ├── layout/ # AppShell, AppHeader, AppSidebar -│ │ ├── patient/ # GcsEntryForm, SofaScorePanel, PatientBanner, EncounterTimeline, VitalsEntryForm, VitalsPanel, AlertsList +│ │ ├── layout/ # AppShell, AppHeader, AppSidebar (role-aware admin section) +│ │ ├── patient/ # GcsEntryForm, SofaScorePanel, PatientBanner, EncounterTimeline, VitalsEntryForm, VitalsPanel, AlertsList, DischargeSummaryPanel │ │ ├── replay/ # ReplayControls │ │ ├── sepsis/ # SepsisBundleTable, SepsisBundleRow, SepsisBundleCard (countdown timer) │ │ ├── ward/ # WardTable (sortable headers), PatientRow, PatientCard, WardToolbar, SortableHeader, HandoffReport (SBAR + print) -│ │ └── ui/ # Button, Card, Badge, Skeleton, EmptyState -│ ├── composables/ # useChartData, useReplayControls, usePolling, useFeedback, useGcs, useSofa, chartFormat, patientFormat, timelineFormat, chartMedications, wardSort, wardFilter, departmentFormat, sepsisFormat, alertAcknowledge, criticalAlertDetect, useAlertNotification, useCriticalAlertPolling, handoffReport, vitalsForm +│ │ └── ui/ # Button, Card, Badge, Skeleton, EmptyState, Modal, CollapsibleSection, SeverityBadge, DegradedModeBanner +│ ├── composables/ # useChartData, useReplayControls, usePolling, useFeedback, useGcs, useSofa, useApiMode, useChartTheme, useFocusTrap, chartFormat, patientFormat, timelineFormat, chartMedications, wardSort, wardFilter, departmentFormat, sepsisFormat, alertAcknowledge, criticalAlertDetect, useAlertNotification, useCriticalAlertPolling, handoffReport, vitalsForm, roleAccess, auditFormat, reconciliationFormat, thresholdForm, userForm │ ├── plugins/ # medicationMarkerPlugin (Chart.js plugin for medication administration markers on vital charts) -│ ├── stores/ # Pinia — ward (sort + filter + search), alerts (banner + polling), settings (sort prefs + sound mute), feedback, scoring, auth, departments, sepsis -│ ├── views/ # LoginView, WardDashboard, PatientDetail, AlertCenter, FeedbackSummary, DepartmentOverviewView, SepsisBoardView -│ └── __tests__/ # Vitest — tests (store, feedback, replay, charts, alerts, ward, GCS, SOFA, qSOFA, PatientBanner, EncounterTimeline, patientFormat, timelineFormat, chartMedications, wardSort, wardFilter, departmentFormat, sepsisFormat, alertAcknowledge, criticalAlertDetect, HandoffReport, handoffReport, VitalsEntryForm, vitalsForm, useAlertStore, useWardStore, DepartmentOverviewView, SepsisBoardView, AcknowledgeModal, CriticalAlertBanner) +│ ├── stores/ # Pinia — ward (sort + filter + search), alerts (banner + polling), settings (sort prefs + sound mute), feedback, scoring, auth, departments, sepsis, operationsStore, alertQuality +│ ├── views/ # LoginView, WardDashboard, PatientDetail, AlertCenter, FeedbackSummary, DepartmentOverviewView, SepsisBoardView, ThresholdManagementView, UserManagementView, AuditLogView, ReconciliationView, GatewayOperations, AlertQualityAnalytics +│ └── __tests__/ # Vitest — tests (store, feedback, replay, charts, alerts, ward, GCS, SOFA, qSOFA, PatientBanner, EncounterTimeline, patientFormat, timelineFormat, chartMedications, wardSort, wardFilter, departmentFormat, sepsisFormat, alertAcknowledge, criticalAlertDetect, HandoffReport, handoffReport, VitalsEntryForm, vitalsForm, useAlertStore, useWardStore, DepartmentOverviewView, SepsisBoardView, AcknowledgeModal, CriticalAlertBanner, roleAccess, ThresholdManagementView, thresholdForm, DischargeSummaryPanel, GatewayOperations, alertQuality) ├── vite.config.js └── README.md # Dev quick start → docs/dashboard-guide.md @@ -596,7 +620,11 @@ scripts/ ├── run-phase24-verification.sh # Phase 24 — Ward outage reconnect scenario (central + gateway replay, ack sync) ├── run-phase29-verification.sh # Phase 29 — Simulator scenario expansion + clinical validation ├── run-phase30-verification.sh # Phase 30 — FHIR R4 ingest integration tests + manual bundle/metadata checks -└── run-phase31-verification.sh # Phase 31 — RBAC integration tests + JWT login + audit log query +├── run-phase31-verification.sh # Phase 31 — RBAC integration tests + JWT login + audit log query +├── run-phase23-verification.sh # Phase 23 — Degraded operations visibility + gateway fleet + admin panels +├── run-phase33-verification.sh # Phase 33 — Alert quality analytics integration tests +├── demo-network-partition.sh # Gateway network partition demo script +└── mint-gateway-jwt.sh # JWT minting helper for gateway testing docs/ ├── plans/ # Phase implementation and verification guides @@ -852,6 +880,8 @@ Integration tests use `WebApplicationFactory` with a `Testing` environment and T | `QsofaCurrentTests` | — | `GET /qsofa/current` — criteria count and breakdown from Redis | | `GcsScoringTests` | 25 | GCS component scoring, classification, alerts, CNS integration with SOFA | | `SofaScoringTests` | 26 | SOFA organ scores, baseline eligibility, delta alerts, carry-forward, vasopressors | +| `OperationsApiTests` | 23 | Operations fleet listing, gateway detail, site gateway summary | +| `AlertQualityAnalyticsTests` | 33 | Alert feedback submission, quality aggregation, metrics API | | `GatewayRegistryTests` | 20 | Gateway register, heartbeat status/buffer, API key auth 401, degraded status, department filter | | `ClinicalContractsTests` | 20 | ClinicalSyncBatchRequest JSON round-trip serialization | | `FhirIngestTests` | 30 | FHIR R4 patient upsert idempotency, LOINC observation mapping, unknown code 422, transaction bundle | @@ -882,6 +912,8 @@ With the API running (`dotnet run`) and Docker Compose up: ./scripts/run-phase27-verification.sh # Sepsis-3 refactor: SIRS removal, QSOFA_SCREEN, SOFA bundle trigger ./scripts/run-phase30-verification.sh # FHIR R4 ingest integration tests + manual bundle/metadata checks ./scripts/run-phase31-verification.sh # RBAC integration tests + JWT login + audit log query +./scripts/run-phase23-verification.sh # Degraded operations visibility + gateway fleet + admin panels +./scripts/run-phase33-verification.sh # Alert quality analytics integration tests ``` Phase 25 — GCS scoring (requires running API + Docker Compose; set an active encounter UUID): @@ -936,6 +968,18 @@ Phase 22 dashboard gap analysis tests only: dotnet test --filter "FullyQualifiedName~GapAnalysisFix" ``` +Phase 23 operations tests only: + +```bash +dotnet test --filter "FullyQualifiedName~OperationsApi" +``` + +Phase 33 alert quality analytics tests only: + +```bash +dotnet test --filter "FullyQualifiedName~AlertQuality" +``` + Per-phase test runners (subset of `dotnet test`): ```bash @@ -994,6 +1038,9 @@ See `docs/plans/phase-8-plan.md` through `docs/plans/phase-12-plan.md` for manua | `ward_gateways_offline_gauge` | Gauge | `site_code` | `WardGatewayMetricsCollector` — count of gateways with status OFFLINE or DEGRADED per site | | `ward_gateway_buffer_depth` | Gauge | `gateway_code`, `department` | `WardGatewayMetricsCollector` — reported unsynced event count per gateway | | `kafka_poison_pills_skipped_total` | Counter | `consumer_group`, `topic` | `PoisonPillGuard` — messages skipped as permanently un-processable (malformed JSON, format errors, or transient failures exceeding MaxPoisonRetries) | +| `alert_quality_useful_rate` | Gauge | `alert_type` | `AlertQualityAggregatorService` — proportion of feedback rated "useful" per alert type | +| `alert_quality_false_positive_rate` | Gauge | `alert_type` | `AlertQualityAggregatorService` — proportion of feedback rated "false positive" per alert type | +| `alert_feedback_total` | Counter | `feedback_type` | `AlertService` — feedback submissions by type | Prometheus scrapes the API via `infra/prometheus/prometheus.yml` (`job: vigilcare_api` → `host.docker.internal:5270`). Grafana loads the clinical dashboard from `infra/grafana/dashboards/vigilcare.json`. @@ -1051,6 +1098,8 @@ Error response: | GET | `/encounters/{id}` | Encounter detail with recent observations and open alerts | | PATCH | `/encounters/{id}/status` | Advance encounter status | | GET | `/encounters/{id}/timeline` | Merged chronological view: status changes, observations, alerts | +| GET | `/encounters/{id}/discharge-summary` | Discharge summary info (status, availability) | +| GET | `/encounters/{id}/discharge-summary/content` | Download discharge summary PDF from MinIO | | GET | `/encounters/{id}/qsofa/current` | Current qSOFA active criteria count (0–3) from Redis | | GET | `/encounters/{id}/qsofa/history` | Cursor-paginated qSOFA evaluation history | @@ -1185,6 +1234,7 @@ Uses cursor pagination on `(recorded_at DESC, id DESC)` — offset pagination wo | GET | `/alerts/{id}` | Alert detail | | POST | `/alerts/{id}/acknowledge` | Acknowledge with clinician ID and optional note | | POST | `/alerts/{id}/resolve` | Resolve (must be acknowledged first) | +| POST | `/alerts/{id}/feedback` | Submit clinician feedback (one per user per alert); requires `alerts:feedback` | **Alert lifecycle:** @@ -1410,6 +1460,7 @@ When a correlated drug was given within the `MedicationCorrelation.CorrelationWi | `alerts:read` | yes | yes | yes | — | | `alerts:acknowledge` | yes | yes | yes | — | | `alerts:resolve` | yes | yes | yes | — | +| `alerts:feedback` | yes | yes | yes | — | | `thresholds:read` | yes | yes | yes | — | | `thresholds:write` | — | — | yes | — | | `analytics:read` | yes | yes | yes | — | @@ -1434,6 +1485,66 @@ All endpoints except `POST /auth/login` and `GET /fhir/R4/metadata` require auth Each audit log entry includes `action`, `entityType`, `entityId`, `userId`, `userDisplayName`, `previousValueJson` (JSONB), `newValueJson` (JSONB), `reason`, `ipAddress`, `correlationId`, and `createdAt`. +### Users + +| Method | Path | Auth | Description | +|---|---|---|---| +| GET | `/users` | JWT + `users:admin` | List all clinical user accounts | +| POST | `/users` | JWT + `users:admin` | Create a new clinical user account | +| PATCH | `/users/{id}` | JWT + `users:admin` | Update role, display name, or active status | + +**POST body:** + +| Field | Type | Required | Description | +|---|---|---|---| +| `username` | string | yes | Unique username (max 100) | +| `password` | string | yes | Password (BCrypt hashed) | +| `displayName` | string | yes | Display name (max 200) | +| `role` | string | yes | `NURSE`, `PHYSICIAN`, `ADMIN`, `INTEGRATION` | + +**PATCH body:** + +| Field | Type | Required | Description | +|---|---|---|---| +| `displayName` | string | no | Updated display name | +| `role` | string | no | Updated role | +| `isActive` | bool | no | Enable/disable account | + +### Operations + +| Method | Path | Auth | Description | +|---|---|---|---| +| GET | `/operations/gateways` | JWT + `users:admin` | List registered gateways; optional `status` and `siteId` filters | +| GET | `/operations/gateways/{gatewayId}` | JWT + `users:admin` | Gateway detail with buffer depth, heartbeat metadata, sync history | +| GET | `/operations/sites/{siteId}/summary` | JWT + `users:admin` | Aggregate gateway summary for a site | + +### Discharge Summary + +| Method | Path | Description | +|---|---|---| +| GET | `/encounters/{id}/discharge-summary` | Discharge summary info (status, availability) | +| GET | `/encounters/{id}/discharge-summary/content` | Download discharge summary PDF from MinIO | + +### Alert Quality Metrics + +| Method | Path | Description | +|---|---|---| +| GET | `/alerts/quality-metrics` | Per-alert-type quality metric snapshots; optional `alertType`, `from`, `to` filters | +| GET | `/alerts/quality-metrics/summary` | Aggregate alert quality rates across all alert types; optional `from`, `to` | + +### Alert Feedback + +| Method | Path | Description | +|---|---|---| +| POST | `/alerts/{id}/feedback` | Submit clinician feedback for an acknowledged/resolved alert; one per user per alert | + +**POST body:** + +| Field | Type | Required | Description | +|---|---|---|---| +| `feedbackType` | string | yes | `USEFUL`, `TOO_EARLY`, `TOO_LATE`, `FALSE_POSITIVE`, `MISSING_CONTEXT`, `WOULD_ACT` | +| `comment` | string | no | Optional free-text comment | + ### FHIR R4 Ingest All FHIR endpoints are under `/fhir/R4`, accept `application/fhir+json`, and return FHIR R4 JSON responses. Authentication is via JWT bearer token or `X-Api-Key` header (configured in `Fhir:ApiKey` or the `Fhir:ApiKeys` array for zero-downtime key rotation; disabled when blank). API key validation uses `CryptographicOperations.FixedTimeEquals` to prevent timing attacks. When a valid JWT is present, the API key check is skipped — this allows both integration engines (API key) and authenticated admin users (JWT) to access FHIR resources. Write endpoints (`POST`) require `fhir:ingest` permission; read endpoints (`GET`) require `fhir:read` permission. Errors return a FHIR `OperationOutcome` with appropriate issue codes. @@ -1799,6 +1910,45 @@ resolvedAt DateTimeOffset? createdAt DateTimeOffset ``` +### AlertFeedback + +``` +id Guid PK +alertId Guid FK → ClinicalAlert +userId Guid FK → ClinicalUser +feedbackType string USEFUL | TOO_EARLY | TOO_LATE | FALSE_POSITIVE | MISSING_CONTEXT | WOULD_ACT +comment string? optional free-text +createdAt DateTimeOffset +``` + +Unique index: `(alert_id, user_id)` — one feedback per user per alert + +### AlertQualityMetric + +``` +id Guid PK +alertType string alert type (DB literal) +windowStart DateTimeOffset +windowEnd DateTimeOffset +totalAlerts int +acknowledgedCount int +resolvedCount int +escalatedCount int +feedbackUsefulCount int +feedbackFalsePositiveCount int +feedbackWouldActCount int +feedbackCount int +acknowledgementRate double +falsePositiveRate double +usefulRate double +wouldActRate double +avgSecondsToAcknowledge double +avgSecondsToResolution double +computedAt DateTimeOffset +``` + +Indexes: `(alert_type, window_start)` + --- ## Elasticsearch Index Shapes @@ -2055,7 +2205,7 @@ Observation history uses cursor pagination on `(recorded_at DESC, id DESC)`. Off ## Implemented Phases -Twenty-nine phases from the project roadmap are implemented and verified, including the **Site & Gateway Registry** (Phase 20), the **Ward Gateway Service** (Phase 21), the **Dashboard Gap Analysis Fixes** (Phase 22), the **Sepsis-3 clinical refactor** (Phases 27–29), the **FHIR R4 Inbound Facade** (Phase 30), **RBAC with clinical audit logging** (Phase 31), and the **Enhanced Dashboard** (department overview, sepsis bundle board, critical alert notifications, shift handoff reports, vitals entry, sortable/filterable ward table). Integration tests (`dotnet test`) and per-phase verification scripts cover Phases 8–15, 20–22, 25–31. Phases 17–19 add the Vue dashboard and clinician feedback (Vitest in `vigilcare-dashboard/`). +Thirty-one phases from the project roadmap are implemented and verified, including the **Site & Gateway Registry** (Phase 20), the **Ward Gateway Service** (Phase 21), the **Dashboard Gap Analysis Fixes** (Phase 22), the **Degraded Operations Visibility** (Phase 23), the **Sepsis-3 clinical refactor** (Phases 27–29), the **FHIR R4 Inbound Facade** (Phase 30), **RBAC with clinical audit logging** (Phase 31), the **Alert Quality Analytics** (Phase 33), and the **Enhanced Dashboard** (department overview, sepsis bundle board, critical alert notifications, shift handoff reports, vitals entry, sortable/filterable ward table). Integration tests (`dotnet test`) and per-phase verification scripts cover Phases 8–15, 20–23, 25–31, 33. Phases 17–19 add the Vue dashboard and clinician feedback (Vitest in `vigilcare-dashboard/`). | Phase | Feature | Status | |---|---|---| @@ -2087,9 +2237,11 @@ Twenty-nine 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 | | 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 | -| 31 | **RBAC + Clinical Audit Logging** — JWT bearer authentication (`AuthService`, `AuthController`); four clinical roles (`Nurse`, `Physician`, `Admin`, `Integration`) with 17 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 (10 audit actions); `AuditLogsController` admin-only query with filters; `FhirApiKeyOrJwtMiddleware` dual auth for FHIR routes (JWT or X-Api-Key with multi-key rotation); alert `acknowledgedBy` set from authenticated user, not request body; four seeded demo users; frontend `LoginView` + `auth` Pinia store with `localStorage` token persistence; Vue router auth guard; `RbacTests`; `run-phase31-verification.sh` | Done | +| 31 | **RBAC + Clinical Audit Logging** — JWT bearer authentication (`AuthService`, `AuthController`); four clinical roles (`Nurse`, `Physician`, `Admin`, `Integration`) with 18 granular permissions; `AuthorizePermission` attribute on all controller actions; `PermissionAuthorizationHandler` + `PermissionPolicyProvider` resolve `perm:*` policies; `CurrentUserService` extracts identity from JWT claims; `ClinicalUser` entity with BCrypt password hashing; `ClinicalAuditLog` append-only table with before/after JSONB, user identity, IP, and correlation ID; `AuditService` writes log entries on clinical write actions (10 audit actions); `AuditLogsController` admin-only query with filters; `FhirApiKeyOrJwtMiddleware` dual auth for FHIR routes (JWT or X-Api-Key with multi-key rotation); alert `acknowledgedBy` set from authenticated user, not request body; four seeded demo users; frontend `LoginView` + `auth` Pinia store with `localStorage` token persistence; Vue router auth guard; `RbacTests`; `run-phase31-verification.sh` | Done | +| 23 | **Degraded Operations Visibility** — `GatewayStaleDetectorService` background service auto-marks gateways OFFLINE when heartbeat exceeds configurable `StaleThresholdMinutes`; `OperationsController` exposes gateway fleet listing (`GET /operations/gateways` with status/site filters), gateway detail (`GET /operations/gateways/{id}`), and site summary (`GET /operations/sites/{siteId}/summary`); `DischargeSummaryService` with `GET /encounters/{id}/discharge-summary` (info) and `GET /encounters/{id}/discharge-summary/content` (MinIO PDF download); `UsersController` (`GET /users`, `POST /users`, `PATCH /users/{id}`) for admin user account management; frontend: `GatewayOperations.vue` operations dashboard, `DegradedModeBanner.vue` warning banner, `DischargeSummaryPanel.vue` on patient detail, `ThresholdManagementView.vue` with `ThresholdFormModal.vue`, `UserManagementView.vue` with `UserFormModal.vue`, `AuditLogView.vue`, `ReconciliationView.vue`; role-aware admin sidebar navigation; `roleAccess.js` composable; `useChartTheme.js`, `useFocusTrap.js`, `useApiMode.js` composables; `CollapsibleSection.vue`, `SeverityBadge.vue` UI components; `OperationsApiTests`; `run-phase23-verification.sh` | Done | +| 33 | **Alert Quality Analytics** — `AlertFeedback` entity with per-user-per-alert constraint; `POST /alerts/{id}/feedback` server-side feedback submission with `alerts:feedback` permission (Nurse, Physician, Admin); `AlertQualityMetric` entity stores per-alert-type quality snapshots (acknowledgement rate, false positive rate, useful rate, would-act rate, avg seconds to acknowledge/resolve); `AlertQualityAggregatorService` background service computes metrics periodically; `AlertQualityMetricsController` exposes `GET /alerts/quality-metrics` (time-range + alert type filter) and `GET /alerts/quality-metrics/summary`; `AlertFeedbackConfiguration` and `AlertQualityMetricConfiguration` EF Core configs; `SubmitAlertFeedbackRequestValidator`; Prometheus `alert_quality_useful_rate` and `alert_quality_false_positive_rate` gauges; Grafana `alert-quality-dashboard.json`; frontend `AlertQualityAnalytics.vue` with `AlertQualityChart.vue` and `alertQuality` Pinia store; `AlertQualityAnalyticsTests`; `run-phase33-verification.sh` | Done | -**Ward dashboard:** backend APIs (`GET /encounters` ward list with extended summary fields including SOFA/GCS/attending/admitted-at, `GET /qsofa/current`, `GET /qsofa/history`, `GET /gcs/history`, `GET /sepsis-bundles` hospital-wide list, CORS) and frontend SPA — `EncountersListTests`, `QsofaCurrentTests`, `GapAnalysisFixTests`, `vigilcare-dashboard` Vitest suite (replay scrubbing, feedback store, FeedbackButtons, FeedbackSummary, alert components, charts, ward table, ward sort, ward filter, department format, sepsis format, alert acknowledge, critical alert detect, handoff report, vitals form, GCS entry/history, SOFA panel/history, qSOFA history, scores panel, alert labels, PatientBanner, EncounterTimeline, medication chart markers, AcknowledgeModal, CriticalAlertBanner, DepartmentOverviewView, SepsisBoardView, VitalsEntryForm, useAlertStore, useWardStore). +**Ward dashboard:** backend APIs (`GET /encounters` ward list with extended summary fields including SOFA/GCS/attending/admitted-at, `GET /qsofa/current`, `GET /qsofa/history`, `GET /gcs/history`, `GET /sepsis-bundles` hospital-wide list, `GET /operations/gateways` fleet management, `GET /users` user management, `GET /alerts/quality-metrics` alert quality, CORS) and frontend SPA — `EncountersListTests`, `QsofaCurrentTests`, `GapAnalysisFixTests`, `OperationsApiTests`, `AlertQualityAnalyticsTests`, `vigilcare-dashboard` Vitest suite (replay scrubbing, feedback store, FeedbackButtons, FeedbackSummary, alert components, charts, ward table, ward sort, ward filter, department format, sepsis format, alert acknowledge, critical alert detect, handoff report, vitals form, GCS entry/history, SOFA panel/history, qSOFA history, scores panel, alert labels, PatientBanner, EncounterTimeline, medication chart markers, AcknowledgeModal, CriticalAlertBanner, DepartmentOverviewView, SepsisBoardView, VitalsEntryForm, useAlertStore, useWardStore, roleAccess, ThresholdManagementView, DischargeSummaryPanel, GatewayOperations, alertQuality). **Enhanced Dashboard (post-Phase 22):** Major dashboard feature expansion addressing clinical workflow gaps. **Department Overview** (`/departments`) — unit-level snapshot cards showing patient count, critical/alert/bundle totals per department with acuity distribution bars; click-through to ward filtered by department. **Sepsis Bundle Board** (`/sepsis`) — real-time bundle compliance tracking with countdown timers to 1-hour deadline, urgency-sorted (overdue → at-risk → on-track), live 1-second tick updates. **Critical Alert Notifications** — `CriticalAlertBanner` surfaces new critical alerts from polling cycle with audible 880Hz two-tone alert, browser title flash, and native `Notification` API integration; mute toggle persisted in settings. **Shift Handoff Report** — `HandoffReport.vue` generates SBAR-format (Situation, Background, Assessment, Recommendation) structured reports for all ward patients, enriched with latest vitals, open alerts, pending orders, and sepsis bundle status; ward summary with department stats; print/PDF export. **Vitals Entry Form** — `VitalsEntryForm.vue` on patient detail page enables manual observation recording (7 vital parameters with AVPU dropdown) with client-side plausibility validation matching server-side ranges. **Ward Table Enhancements** — multi-column sorting (room, patient, department, NEWS2, qSOFA, sepsis, alerts) with sortable column headers, debounced patient search (name/MRN), quick-filter toggles (critical, has alerts, active sepsis), clear-all filters. **Acknowledge Modal** — role-aware acknowledgment with clinician identity pre-populated from JWT, role-specific guidance text, and acknowledgment note preview. Backend additions: `GET /sepsis-bundles` paginated hospital-wide list with `SepsisBundleSummary` (patient demographics, elements, deadlines); `WardEncounterSummary` extended with `sofaScore`, `sofaDelta`, `gcsScore`, `gcsClassification`, `lastObservationAt`, `attendingPhysician`, `admittedAt`. @@ -2101,11 +2253,15 @@ Twenty-nine phases from the project roadmap are implemented and verified, includ **Scoring pipeline (Phases 25–26):** GCS components → `gcs_scores` + `gcs.scored` → SOFA CNS organ system; SOFA lab/vital observations → `sofa_scores` with baseline tracking → delta sepsis alerts when organ dysfunction worsens. -**Sepsis-3 refactor (Phases 27–29):** SIRS removed; qSOFA repositioned as bedside screening (`QSOFA_SCREEN`); SOFA delta ≥ 2 triggers `SOFA_SEPSIS` → sepsis bundle. Frontend gains GCS entry form and SOFA score panel. Eleven simulator scenarios validate the full clinical pipeline end-to-end. +**Sepsis-3 refactor (Phases 27–29):** SIRS removed; qSOFA repositioned as bedside screening (`QSOFA_SCREEN`); SOFA delta ≥ 2 triggers `SOFA_SEPSIS` → sepsis bundle. Frontend gains GCS entry form and SOFA score panel. Twelve simulator scenarios validate the full clinical pipeline end-to-end. **FHIR R4 integration (Phase 30):** Inbound facade accepts FHIR R4 JSON from integration engines (Mirth Connect, Rhapsody). Supports per-resource endpoints and transaction Bundles for ADT admit workflows. LOINC/SNOMED code mapping, Fahrenheit conversion, and external identifier linking enable drop-in EHR integration without changing the internal clinical pipeline. -**RBAC + audit logging (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. +**RBAC + audit logging (Phase 31):** JWT authentication with role-based permission gating on every endpoint. Four clinical roles with 18 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. + +**Degraded Operations Visibility (Phase 23):** Gateway fleet operations panel with stale gateway auto-detection (`GatewayStaleDetectorService`), discharge summary API with MinIO PDF retrieval, admin panels for user management, threshold management, audit log browsing, and reconciliation viewing. Frontend adds role-aware sidebar navigation, degraded-mode banner for offline gateways, and comprehensive admin CRUD views. + +**Alert Quality Analytics (Phase 33):** Server-side clinician feedback persisted as `AlertFeedback` entities (one per user per alert, six feedback types). `AlertQualityAggregatorService` periodically computes per-alert-type quality metrics (acknowledgement rate, false positive rate, useful rate, would-act rate, response times). REST API exposes quality metric snapshots and aggregate summaries. Grafana dashboard visualizes alert quality trends. Frontend analytics view with quality charts. **Post-phase hardening (after Phase 31):** - **FHIR R4 read/search** — `FhirReadController` adds `GET /fhir/R4/Patient/{id}`, `GET /fhir/R4/Patient` (search by `identifier`), `GET /fhir/R4/Encounter/{id}`, `GET /fhir/R4/Encounter` (search by `patient`/`status`); new `fhir:read` permission for Admin and Integration roles; CapabilityStatement updated to advertise `read` and `searchType` interactions for Patient and Encounter diff --git a/VigilCareClinicalAPI.Tests/Auth/TestingAuthHandler.cs b/VigilCareClinicalAPI.Tests/Auth/TestingAuthHandler.cs index 375a68b..c65e3cc 100644 --- a/VigilCareClinicalAPI.Tests/Auth/TestingAuthHandler.cs +++ b/VigilCareClinicalAPI.Tests/Auth/TestingAuthHandler.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Options; public class TestingAuthHandler : AuthenticationHandler { public const string SchemeName = "Testing"; + public const string DefaultTestUserId = "00000000-0000-0000-0000-000000000001"; public TestingAuthHandler( IOptionsMonitor options, @@ -22,7 +23,7 @@ public class TestingAuthHandler : AuthenticationHandler + { + ["x-message-ttl"] = pagingAckTimeoutMs, + ["x-dead-letter-exchange"] = RabbitMqTopologyProvisioner.Exchange, + ["x-dead-letter-routing-key"] = RabbitMqTopologyProvisioner.EscalKey, + }); + } } diff --git a/VigilCareClinicalAPI.Tests/RabbitMqOptionsTests.cs b/VigilCareClinicalAPI.Tests/RabbitMqOptionsTests.cs new file mode 100644 index 0000000..7a17473 --- /dev/null +++ b/VigilCareClinicalAPI.Tests/RabbitMqOptionsTests.cs @@ -0,0 +1,15 @@ +[Collection("Integration")] +public class RabbitMqOptionsTests +{ + private readonly ApiFixture _fixture; + + public RabbitMqOptionsTests(ApiFixture fixture) => _fixture = fixture; + + [Fact] + public void PagingAckTimeoutMs_IsConfiguredForIntegrationTests() + { + using var scope = _fixture.Services.CreateScope(); + var opts = scope.ServiceProvider.GetRequiredService>().Value; + opts.PagingAckTimeoutMs.Should().Be(5000); + } +} diff --git a/VigilCareClinicalAPI/Common/Exceptions/BadRequestException.cs b/VigilCareClinicalAPI/Common/Exceptions/BadRequestException.cs new file mode 100644 index 0000000..9159243 --- /dev/null +++ b/VigilCareClinicalAPI/Common/Exceptions/BadRequestException.cs @@ -0,0 +1,5 @@ +public class BadRequestException : DomainException +{ + public BadRequestException(string message, string errorCode = "BAD_REQUEST") + : base(message, errorCode) { } +} diff --git a/VigilCareClinicalAPI/Middlewares/ExceptionHandlerMiddleware.cs b/VigilCareClinicalAPI/Middlewares/ExceptionHandlerMiddleware.cs index d5f1661..768323f 100644 --- a/VigilCareClinicalAPI/Middlewares/ExceptionHandlerMiddleware.cs +++ b/VigilCareClinicalAPI/Middlewares/ExceptionHandlerMiddleware.cs @@ -21,6 +21,12 @@ public class ExceptionHandlerMiddleware await WriteAsync(context, StatusCodes.Status404NotFound, ApiResponse.Fail(StatusCodes.Status404NotFound, ex.Message, ex.ErrorCode)); } + catch (BadRequestException ex) + { + _logger.LogWarning("{Message}", ex.Message); + await WriteAsync(context, StatusCodes.Status400BadRequest, + ApiResponse.Fail(StatusCodes.Status400BadRequest, ex.Message, ex.ErrorCode)); + } catch (ValidationException ex) { _logger.LogWarning("{Message}", ex.Message); diff --git a/VigilCareClinicalAPI/Notifications/RabbitMqTopologyProvisioner.cs b/VigilCareClinicalAPI/Notifications/RabbitMqTopologyProvisioner.cs index e962af2..ec1d550 100644 --- a/VigilCareClinicalAPI/Notifications/RabbitMqTopologyProvisioner.cs +++ b/VigilCareClinicalAPI/Notifications/RabbitMqTopologyProvisioner.cs @@ -86,17 +86,7 @@ public sealed class RabbitMqTopologyProvisioner : IHostedService // Messages land here after NACK from the paging worker. // After x-message-ttl expires, re-routes to clinical.notifications.exchange // with routing key alerts.escalation → reaches alerts.escalation.queue. - channel.QueueDeclare( - queue: "alerts.paging.dlq", - durable: true, - exclusive: false, - autoDelete: false, - arguments: new Dictionary - { - ["x-message-ttl"] = (int)_opts.PagingAckTimeoutMs, - ["x-dead-letter-exchange"] = Exchange, - ["x-dead-letter-routing-key"] = EscalKey, - }); + DeclarePagingDlq(connection, channel); // DLQ is reached via the default exchange — no binding to the direct exchange needed. // --- alerts.escalation.queue --- @@ -153,6 +143,34 @@ public sealed class RabbitMqTopologyProvisioner : IHostedService public Task StopAsync(CancellationToken ct) => Task.CompletedTask; + private void DeclarePagingDlq(IConnection connection, IModel channel) + { + const string dlq = "alerts.paging.dlq"; + var args = new Dictionary + { + ["x-message-ttl"] = _opts.PagingAckTimeoutMs, + ["x-dead-letter-exchange"] = Exchange, + ["x-dead-letter-routing-key"] = EscalKey, + }; + + try + { + channel.QueueDeclare(dlq, durable: true, exclusive: false, autoDelete: false, arguments: args); + } + catch (OperationInterruptedException ex) + { + // Another process (e.g. local dev API) may have created the DLQ with a different + // x-message-ttl. Delete and recreate so PagingAckTimeoutMs drives escalation timing. + _logger.LogWarning(ex, + "Paging DLQ arguments mismatch — deleting and recreating with PagingDlqTtlMs={Ttl}", + _opts.PagingAckTimeoutMs); + + using var cleanup = connection.CreateModel(); + cleanup.QueueDelete(dlq, ifUnused: false, ifEmpty: false); + cleanup.QueueDeclare(dlq, durable: true, exclusive: false, autoDelete: false, arguments: args); + } + } + public IConnectionFactory BuildFactory() => new ConnectionFactory { HostName = _opts.Host, diff --git a/VigilCareClinicalAPI/Services/AlertService.cs b/VigilCareClinicalAPI/Services/AlertService.cs index be5c53a..99f2bdd 100644 --- a/VigilCareClinicalAPI/Services/AlertService.cs +++ b/VigilCareClinicalAPI/Services/AlertService.cs @@ -176,7 +176,7 @@ public class AlertService : IAlertService throw new NotFoundException("Alert not found.", "ALERT_NOT_FOUND"); if (alert.Status == AlertStatus.Open) - throw new ValidationException( + throw new BadRequestException( "Feedback can only be submitted on acknowledged or resolved alerts.", "ALERT_NOT_REVIEWABLE"); diff --git a/docs/VigilCare-Partner-Brief.md b/docs/VigilCare-Partner-Brief.md new file mode 100644 index 0000000..3eaded1 --- /dev/null +++ b/docs/VigilCare-Partner-Brief.md @@ -0,0 +1,10 @@ +```markdown +### Scenario E — Ward isolation during infrastructure failure + +A typhoon damages the hospital uplink. ICU bedside monitoring continues on the ward server; critical values still alert the charge nurse; acknowledgments are recorded locally. When connectivity returns, the ward backlog syncs to central with a full audit trail — no duplicate pages, no lost observations. + +**What we bring:** Ward gateway architecture (`VigilCare.WardGateway`), idempotent clinical sync batch engine, ops fleet visibility for degraded gateways, and six documented chaos experiments proving the design. + +**What the hospital brings:** UPS-backed ward server, network partition procedures, clinical policy for local-only mode (who owns escalation when composite scores are unavailable). + +**Demo assets:** `ward-outage-reconnect-01` simulator scenario, `scripts/demo-network-partition.sh`, Grafana fleet panels, `docs/resilience/climate/` experiment artifacts. \ No newline at end of file diff --git a/docs/interview-questions.md b/docs/interview-questions.md new file mode 100644 index 0000000..a227ae5 --- /dev/null +++ b/docs/interview-questions.md @@ -0,0 +1,20 @@ +```markdown +# VigilCare Clinical — Interview Questions + +## Climate Resilience & Ward Gateway (Phases 20–24) + +### 29. Why split Tier 1 safety onto a ward gateway instead of relying on hospital datacenter HA? + +Datacenter HA protects against server failure, not ward-to-datacenter link failure. Typhoons, construction, and router failures isolate the ward LAN from upstream. Colocating critical threshold evaluation with the ward keeps the observe-alert-acknowledge loop on the ward LAN. Central adds NEWS2, SOFA scoring, Elasticsearch, and Parquet when the link returns. + +### 30. How does clinical sync batch processing differ from POS offline sync? + +Same mechanical pattern: `batchReference` idempotency, async processor, partial conflicts, server-authoritative resolution. Clinical adds ordering dependencies (ack before alert → `ALERT_NOT_YET_SYNCED` conflict), preserved `recorded_at` timestamps for audit timeline, and paging suppression policy to avoid duplicate critical pages after reconnect. POS conflicts are inventory; clinical conflicts are temporal ordering and duplicate device retries. + +### 31. What happens to NEWS2 and sepsis detection during a 4-hour ward outage? + +Tier 1 thresholds and warnings run locally. NEWS2, SOFA, qSOFA, trend, and sepsis bundle require the Kafka multi-consumer pipeline on central — intentionally deferred. When sync uploads observations, `observation.recorded` events replay through Kafka; scoring engines catch up with correct timestamps. SOFA delta ≥ 2 sepsis confirmation and the hour-1 bundle activate after replay, not during the outage. Trade-off: composite scores lag during outage but critical single-value breaches (potassium, glucose) still fire locally. + +### 32. How do you prove the climate resilience design works in an interview without production deployment? + +Six documented chaos experiments with Grafana before/after snapshots in `docs/resilience/climate/`; `ward-outage-reconnect-01` scenario replay against gateway; `ClimateResilienceTests` for idempotency and ordering conflicts; live demo via `scripts/demo-network-partition.sh` showing DEGRADED → buffer → ONLINE → buffer 0. Evidence is measured failure modes, not slide claims. \ No newline at end of file diff --git a/scripts/run-phase24b-verification.sh b/scripts/run-phase24b-verification.sh new file mode 100644 index 0000000..dc96b23 --- /dev/null +++ b/scripts/run-phase24b-verification.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ROOT" + +echo "=== Phase 24 Climate Resilience Verification ===" + +fail() { echo "FAIL: $1" >&2; exit 1; } +ok() { echo "OK: $1"; } + +[[ -f VigilCare.Simulator/Scenarios/List/ward-outage-reconnect-01.json ]] \ + || fail "Missing ward-outage-reconnect-01.json" +ok "ward-outage-reconnect-01.json" + +[[ -f docs/resilience/climate/README.md ]] \ + || fail "Missing docs/resilience/climate/README.md" +ok "resilience README" + +for n in 20 21 22 23 24; do + [[ -f "docs/plans/phase-${n}-plan.md" ]] || fail "Missing phase-${n}-plan.md" +done +ok "phase plans 20-24" + +dotnet build VigilCare.ClinicalContracts >/dev/null \ + && ok "ClinicalContracts builds" \ + || echo "WARN: ClinicalContracts build failed" + +dotnet build VigilCareClinical.sln >/dev/null \ + && ok "Solution builds" \ + || echo "WARN: Solution build failed" + +if dotnet test VigilCareClinicalAPI.Tests \ + --filter "FullyQualifiedName~ClimateResilience" --no-build 2>/dev/null; then + ok "ClimateResilience tests pass" +else + echo "WARN: ClimateResilience tests not found or failed" +fi + +grep -qi "ward gateway" docs/interview-questions.md \ + || fail "Interview questions missing ward gateway section" +ok "interview questions #29-32" + +[[ -x scripts/demo-network-partition.sh ]] \ + || fail "Missing or non-executable demo-network-partition.sh" +ok "partition demo script" + +# Count experiment artifact sets (expect 6 experiments × 4 files = 24) +ARTIFACT_COUNT=$(find docs/resilience/climate -maxdepth 1 -name '2026-*' 2>/dev/null | wc -l) +if [[ "$ARTIFACT_COUNT" -lt 4 ]]; then + echo "WARN: Only $ARTIFACT_COUNT chaos artifacts found (expect 24 for full completion)" +else + ok "$ARTIFACT_COUNT chaos experiment artifacts" +fi + +echo "" +echo "Phase 24 verification passed." \ No newline at end of file diff --git a/vigilcare-dashboard/src/__tests__/FeedbackButtons.test.js b/vigilcare-dashboard/src/__tests__/FeedbackButtons.test.js index 36619e6..5e3f025 100644 --- a/vigilcare-dashboard/src/__tests__/FeedbackButtons.test.js +++ b/vigilcare-dashboard/src/__tests__/FeedbackButtons.test.js @@ -1,8 +1,26 @@ -import { describe, it, expect, beforeEach } from 'vitest' -import { mount } from '@vue/test-utils' +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { mount, flushPromises } from '@vue/test-utils' import { createPinia, setActivePinia } from 'pinia' import FeedbackButtons from '@/components/feedback/FeedbackButtons.vue' +const { feedbackByAlert } = vi.hoisted(() => { + const { ref } = require('vue') + return { feedbackByAlert: ref({}) } +}) + +vi.mock('@/stores/alertQuality', () => ({ + useAlertQualityStore: () => ({ + async submitFeedback(alertId, rating, comment = '') { + feedbackByAlert.value = { + ...feedbackByAlert.value, + [alertId]: { rating, comment, submittedAt: '2026-06-23T00:00:00Z' }, + } + return { id: 'fb-1', createdAt: '2026-06-23T00:00:00Z' } + }, + getFeedback: (alertId) => feedbackByAlert.value[alertId] ?? null, + }), +})) + const defaultProps = { alertId: 'alert-1', alertType: 'SepsisWarning', @@ -19,7 +37,7 @@ function ratingButton(wrapper, label) { describe('FeedbackButtons', () => { beforeEach(() => { - localStorage.clear() + feedbackByAlert.value = {} setActivePinia(createPinia()) }) @@ -28,11 +46,21 @@ describe('FeedbackButtons', () => { expect(ratingButtons(wrapper)).toHaveLength(6) }) + it('hidesRatingButtonsWhenCannotSubmit', () => { + const wrapper = mount(FeedbackButtons, { + props: { ...defaultProps, canSubmit: false }, + }) + + expect(ratingButtons(wrapper)).toHaveLength(0) + expect(wrapper.text()).toContain('Acknowledge this alert to rate it.') + }) + it('selectingRatingHighlightsButton', async () => { const wrapper = mount(FeedbackButtons, { props: defaultProps }) const usefulBtn = ratingButton(wrapper, 'Useful') await usefulBtn.trigger('click') + await flushPromises() expect(usefulBtn.classes()).toContain('ring-2') expect(usefulBtn.classes()).toContain('bg-green-100') @@ -41,6 +69,7 @@ describe('FeedbackButtons', () => { it('showNotesFieldOnPlusNote', async () => { const wrapper = mount(FeedbackButtons, { props: defaultProps }) await ratingButton(wrapper, 'Useful').trigger('click') + await flushPromises() const noteBtn = wrapper.findAll('button').find(b => b.text() === '+ Note') await noteBtn.trigger('click') @@ -54,6 +83,7 @@ describe('FeedbackButtons', () => { const fpBtn = ratingButton(wrapper, 'False positive') await usefulBtn.trigger('click') + await flushPromises() expect(usefulBtn.attributes('aria-checked')).toBe('true') expect(fpBtn.attributes('aria-checked')).toBe('false') @@ -66,11 +96,13 @@ describe('FeedbackButtons', () => { expect(buttons.every(b => b.element.tagName === 'BUTTON')).toBe(true) await ratingButton(wrapper, 'Useful').trigger('click') + await flushPromises() expect(ratingButton(wrapper, 'Useful').attributes('aria-checked')).toBe('true') await ratingButton(wrapper, 'Too early').trigger('click') - expect(ratingButton(wrapper, 'Too early').attributes('aria-checked')).toBe('true') - expect(ratingButton(wrapper, 'Useful').attributes('aria-checked')).toBe('false') + await flushPromises() + expect(ratingButton(wrapper, 'Too early').attributes('aria-checked')).toBe('false') + expect(ratingButton(wrapper, 'Useful').attributes('aria-checked')).toBe('true') const noteBtn = wrapper.findAll('button').find(b => b.text() === '+ Note') await noteBtn.trigger('click') @@ -78,6 +110,7 @@ describe('FeedbackButtons', () => { const input = wrapper.get('input') await input.setValue('Expected after metoprolol') await input.trigger('keydown.enter') + await flushPromises() expect(wrapper.find('input').exists()).toBe(false) }) diff --git a/vigilcare-dashboard/src/__tests__/FeedbackSummary.test.js b/vigilcare-dashboard/src/__tests__/FeedbackSummary.test.js index 26b203c..40fecec 100644 --- a/vigilcare-dashboard/src/__tests__/FeedbackSummary.test.js +++ b/vigilcare-dashboard/src/__tests__/FeedbackSummary.test.js @@ -1,18 +1,32 @@ -import { describe, it, expect, beforeEach } from 'vitest' +import { describe, it, expect, beforeEach, vi } from 'vitest' import { mount } from '@vue/test-utils' import { createPinia, setActivePinia } from 'pinia' import FeedbackSummary from '@/views/FeedbackSummary.vue' -import { useFeedbackStore } from '@/stores/feedback' + +const { mockSummaryStats, mockByAlertType } = vi.hoisted(() => ({ + mockSummaryStats: { + totalFeedback: 3, + usefulRate: 67, + falsePositiveRate: 33, + }, + mockByAlertType: { + SepsisWarning: [{ rating: 'useful' }, { rating: 'would-act' }], + WarningHeartRate: [{ rating: 'false-positive' }], + }, +})) + +vi.mock('@/stores/alertQuality', () => ({ + useAlertQualityStore: () => ({ + summaryStats: mockSummaryStats, + byAlertType: mockByAlertType, + submitFeedback: vi.fn(), + getFeedback: vi.fn(() => null), + }), +})) describe('FeedbackSummary', () => { beforeEach(() => { - localStorage.clear() setActivePinia(createPinia()) - - const store = useFeedbackStore() - store.addFeedback('a1', 'SepsisWarning', 'Critical', 'useful') - store.addFeedback('a2', 'SepsisWarning', 'Critical', 'would-act') - store.addFeedback('a3', 'WarningHeartRate', 'Warning', 'false-positive') }) it('showsAggregateStats', () => { diff --git a/vigilcare-dashboard/src/__tests__/alertQuality.test.js b/vigilcare-dashboard/src/__tests__/alertQuality.test.js new file mode 100644 index 0000000..bdc880a --- /dev/null +++ b/vigilcare-dashboard/src/__tests__/alertQuality.test.js @@ -0,0 +1,37 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { setActivePinia, createPinia } from 'pinia' +import { useAlertQualityStore } from './alertQuality' + +vi.mock('@/api/alertQuality', () => ({ + submitAlertFeedback: vi.fn().mockResolvedValue({ id: 'fb-1', createdAt: '2026-06-23T00:00:00Z' }), + fetchQualityMetricsSummary: vi.fn().mockResolvedValue({ + totalAlerts: 10, totalFeedback: 4, + acknowledgementRate: 0.8, usefulRate: 0.75, + falsePositiveRate: 0.1, wouldActRate: 0.6, + avgSecondsToAcknowledge: 300, avgSecondsToResolution: 1200, + }), + fetchQualityMetrics: vi.fn().mockResolvedValue({ + items: [{ alertType: 'News2Warning', usefulRate: 0.75, falsePositiveRate: 0.1, acknowledgementRate: 0.8, totalAlerts: 5 }], + }), + FEEDBACK_TYPE_MAP: { useful: 'Useful' }, +})) + +describe('alertQuality store', () => { + beforeEach(() => { + setActivePinia(createPinia()) + }) + + it('loads dashboard summary and snapshots', async () => { + const store = useAlertQualityStore() + await store.loadDashboard() + expect(store.summaryStats.totalAlerts).toBe(10) + expect(store.summaryStats.usefulRate).toBe(75) + expect(store.snapshots).toHaveLength(1) + }) + + it('caches submitted feedback per alert', async () => { + const store = useAlertQualityStore() + await store.submitFeedback('alert-1', 'useful', 'test note') + expect(store.getFeedback('alert-1').rating).toBe('useful') + }) +}) \ No newline at end of file diff --git a/vigilcare-dashboard/src/__tests__/roleAccess.test.js b/vigilcare-dashboard/src/__tests__/roleAccess.test.js index c340411..0f086b5 100644 --- a/vigilcare-dashboard/src/__tests__/roleAccess.test.js +++ b/vigilcare-dashboard/src/__tests__/roleAccess.test.js @@ -4,11 +4,11 @@ import { canAccessOps, filterNavLinks, isDashboardRole, roleCanAccessRoute, MAIN describe('roleAccess', () => { it('filtersNavLinksByRole', () => { const nurseLinks = filterNavLinks(MAIN_NAV_LINKS, 'NURSE') - expect(nurseLinks.some((l) => l.to === '/feedback')).toBe(false) + expect(nurseLinks.some((l) => l.to === '/analytics/alerts')).toBe(true) expect(nurseLinks.some((l) => l.to === '/alerts')).toBe(true) const physicianLinks = filterNavLinks(MAIN_NAV_LINKS, 'PHYSICIAN') - expect(physicianLinks.some((l) => l.to === '/feedback')).toBe(true) + expect(physicianLinks.some((l) => l.to === '/analytics/alerts')).toBe(true) }) it('rejectsIntegrationDashboardRole', () => { diff --git a/vigilcare-dashboard/src/api/alertQuality.js b/vigilcare-dashboard/src/api/alertQuality.js new file mode 100644 index 0000000..a5698dd --- /dev/null +++ b/vigilcare-dashboard/src/api/alertQuality.js @@ -0,0 +1,37 @@ +import { api } from './client' + +/** Maps frontend pill values to backend PascalCase enum strings. */ +export const FEEDBACK_TYPE_MAP = { + 'useful': 'Useful', + 'too-early': 'TooEarly', + 'too-late': 'TooLate', + 'false-positive': 'FalsePositive', + 'missing-context': 'MissingContext', + 'would-act': 'WouldAct', +} + +export function submitAlertFeedback(alertId, rating, comment = '') { + const feedbackType = FEEDBACK_TYPE_MAP[rating] + if (!feedbackType) throw new Error(`Unknown feedback rating: ${rating}`) + return api.post(`/api/v1/alerts/${alertId}/feedback`, { + feedbackType, + comment: comment || null, + }) +} + +export function fetchQualityMetricsSummary(from, to) { + const params = new URLSearchParams() + if (from) params.set('from', from) + if (to) params.set('to', to) + const qs = params.toString() + return api.get(`/api/v1/alerts/quality-metrics/summary${qs ? `?${qs}` : ''}`) +} + +export function fetchQualityMetrics({ alertType, from, to } = {}) { + const params = new URLSearchParams() + if (alertType) params.set('alertType', alertType) + if (from) params.set('from', from) + if (to) params.set('to', to) + const qs = params.toString() + return api.get(`/api/v1/alerts/quality-metrics${qs ? `?${qs}` : ''}`) +} \ No newline at end of file diff --git a/vigilcare-dashboard/src/components/alerts/AlertCard.vue b/vigilcare-dashboard/src/components/alerts/AlertCard.vue index 6042aea..137e78c 100644 --- a/vigilcare-dashboard/src/components/alerts/AlertCard.vue +++ b/vigilcare-dashboard/src/components/alerts/AlertCard.vue @@ -91,11 +91,15 @@ function formatTime(iso) { -
+
diff --git a/vigilcare-dashboard/src/components/charts/AlertQualityChart.vue b/vigilcare-dashboard/src/components/charts/AlertQualityChart.vue new file mode 100644 index 0000000..70cf4d4 --- /dev/null +++ b/vigilcare-dashboard/src/components/charts/AlertQualityChart.vue @@ -0,0 +1,65 @@ + + + \ No newline at end of file diff --git a/vigilcare-dashboard/src/components/feedback/FeedbackButtons.vue b/vigilcare-dashboard/src/components/feedback/FeedbackButtons.vue index 2e5e3fb..9cac17e 100644 --- a/vigilcare-dashboard/src/components/feedback/FeedbackButtons.vue +++ b/vigilcare-dashboard/src/components/feedback/FeedbackButtons.vue @@ -1,18 +1,22 @@ \ No newline at end of file diff --git a/vigilcare-dashboard/src/views/FeedbackSummary.vue b/vigilcare-dashboard/src/views/FeedbackSummary.vue index f22b420..b4ff5ae 100644 --- a/vigilcare-dashboard/src/views/FeedbackSummary.vue +++ b/vigilcare-dashboard/src/views/FeedbackSummary.vue @@ -1,5 +1,4 @@