From f93ce1e57b63f7447403076e8663cdd4e872c0d6 Mon Sep 17 00:00:00 2001 From: voltsrage Date: Sun, 21 Jun 2026 05:24:02 +0800 Subject: [PATCH] chore: update patient lifecycle --- docs/patient-encounter-api-lifecycle.md | 616 ++++++++++++++++++------ 1 file changed, 463 insertions(+), 153 deletions(-) diff --git a/docs/patient-encounter-api-lifecycle.md b/docs/patient-encounter-api-lifecycle.md index 98de420..eacdf4d 100644 --- a/docs/patient-encounter-api-lifecycle.md +++ b/docs/patient-encounter-api-lifecycle.md @@ -1,12 +1,13 @@ # Patient Encounter API Lifecycle -**Audience:** API integrators, simulator authors, dashboard developers, and anyone who needs to understand how a patient moves through VigilCare from registration to discharge — not from one subsystem's perspective, but across the full `/api/v1` surface. +**Audience:** API integrators, simulator authors, dashboard developers, and anyone who needs to understand how a patient moves through VigilCare from registration to discharge. + +**What is VigilCare?** VigilCare is a clinical monitoring system that tracks patients in a hospital. It continuously watches vital signs (like heart rate, blood pressure, and temperature), calculates safety scores, and fires alerts when something looks wrong — so doctors and nurses can respond quickly. **Companion docs:** - [API Reference](../README.md#api-reference) — authoritative endpoint contracts and request bodies - [Medication correlation design](decisions/medication-correlation-design.md) — how drug context annotates warning/NEWS2 alerts -- [Sepsis engine design](decisions/sepsis-engine-design.md) — SIRS/qSOFA Redis state and bundle creation - [Simulator guide](simulator-guide.md) — replay JSON scenarios against this lifecycle - [Dashboard guide](dashboard-guide.md) — which endpoints the Vue ward UI polls @@ -14,9 +15,34 @@ ## What this document covers -A hospital stay in VigilCare is an **encounter** scoped to a **patient**. Everything clinical — observations, alerts, orders, medications, NEWS2 scores, sepsis bundles — hangs off that encounter while it is `ACTIVE`. This guide walks through the lifecycle in chronological order: what to call, what happens synchronously in the HTTP response, what happens asynchronously via Kafka consumers, and what stops working after discharge. +A hospital stay in VigilCare is called an **encounter**, and it belongs to a **patient**. Think of it this way: a patient is a person, and an encounter is one visit to the hospital. The same patient can have many encounters over their lifetime. -There is no single "admit patient" orchestration endpoint. Integrators compose the standard REST resources below. +Everything clinical — measurements, alerts, medication records, lab orders, safety scores, sepsis bundles — is attached to an encounter while it is `ACTIVE` (meaning the patient is still in the hospital). + +This guide walks through the lifecycle in order: what API calls to make, what happens right away in the response, what happens a few seconds later in the background, and what stops working after the patient leaves. + +There is no single "admit patient" button. Instead, you combine the standard REST endpoints below. + +--- + +## Quick glossary of medical terms + +Before we dive in, here are the medical terms you will see throughout this document: + +| Term | What it means | +|---|---| +| **Vital signs** | Basic body measurements that show how a patient is doing — heart rate, blood pressure, temperature, breathing rate, oxygen level | +| **Observation** | Any measurement recorded for a patient (vitals, lab results, consciousness checks) | +| **Sepsis** | A life-threatening condition where the body's response to an infection starts damaging its own organs. It can kill quickly if not treated | +| **NEWS2** | National Early Warning Score 2 — a scoring system that combines 7 vital signs into a single number (0–20) to detect patients who are getting sicker | +| **SOFA** | Sequential Organ Failure Assessment — a score (0–24) that measures how well 6 organ systems are working. Used to detect organ failure and sepsis | +| **qSOFA** | Quick SOFA — a fast bedside check using just 3 things (breathing rate, blood pressure, consciousness). Used as an early screen: "should we look more closely for sepsis?" | +| **GCS** | Glasgow Coma Scale — measures how conscious/awake a patient is (3–15). Lower is worse. A score of 3 means completely unresponsive, 15 means fully alert | +| **AVPU** | A simpler consciousness scale: **A**lert, responds to **V**oice, responds to **P**ain, **U**nresponsive | +| **Threshold** | A boundary value. If a measurement crosses a threshold, it triggers an alert | +| **Alert** | A notification to clinical staff that something needs attention — like an alarm going off | +| **Sepsis bundle** | A checklist of 4 things that must be done within 1 hour when sepsis is suspected (blood tests, antibiotics, IV fluids) | +| **MRN** | Medical Record Number — the human-readable ID for a patient (e.g. `MRN-000042`) | --- @@ -28,7 +54,7 @@ There is no single "admit patient" orchestration endpoint. Integrators compose t | Response envelope | `{ success, statusCode, data, error }` | | Correlation | Optional `X-Correlation-Id` request header; echoed on the response | | Active encounter guard | `POST` observations, medications, and orders return **409** (`ENCOUNTER_NOT_ACTIVE`) when the encounter is discharged or cancelled | -| Async latency | Warning alerts, NEWS2 scores, SIRS/qSOFA, trend alerts, and sepsis bundles are created by Kafka consumers — allow a few seconds after observation ingest before polling read endpoints | +| Async latency | Warning alerts, NEWS2 scores, qSOFA, SOFA, trend alerts, and sepsis bundles are created by background workers — allow a few seconds after recording a measurement before polling for results | --- @@ -45,11 +71,13 @@ flowchart LR M2[POST /encounters/id/medications] M3[POST /encounters/id/orders] end - subgraph async [Async engines via Kafka] + subgraph async [Background scoring engines] K1[warning-evaluator] K2[news2-scoring] K3[sepsis-engine] K4[trend-analyzer] + K5[sofa-scoring] + K6[gcs-scoring] end subgraph triage [Clinician response] T1[GET /alerts] @@ -63,34 +91,259 @@ flowchart LR end A1 --> A2 --> M1 - M1 --> K1 & K2 & K3 & K4 - K1 & K2 & K3 & K4 --> T1 + M1 --> K1 & K2 & K3 & K4 & K5 & K6 + K1 & K2 & K3 & K4 & K5 & K6 --> T1 M2 & M3 --> T1 T1 --> T2 --> T3 M3 --> T4 T3 --> D1 --> D2 ``` -**Encounter status machine** (enforced by `PATCH /encounters/{id}/status`): +**How encounter status works** (enforced by `PATCH /encounters/{id}/status`): ``` scheduled → active → discharged → cancelled ``` -`POST /patients/{id}/encounters` creates encounters directly in `ACTIVE` (the `scheduled` state exists in the model but is not used by the open-encounter endpoint). Illegal transitions return **409** (`ILLEGAL_STATUS_TRANSITION`). +`POST /patients/{id}/encounters` creates encounters directly in `ACTIVE`. Illegal transitions (like going from discharged back to active) return **409** (`ILLEGAL_STATUS_TRANSITION`). + +--- + +## All measurements the system tracks + +VigilCare tracks measurements (called "observations") across three categories: vital signs, lab results, and consciousness assessments. Each measurement has a plausible range — values outside this range are rejected as likely errors. + +### Vital signs (measured at the bedside) + +These are the basic measurements that nurses and devices collect regularly: + +| Code | What it measures | Unit | Normal range (approx.) | Plausible range | Example | +|---|---|---|---|---|---| +| `HEART_RATE` | How fast the heart beats | bpm (beats per minute) | 60–100 | 1–300 | A resting adult is ~70 bpm. Over 150 or under 30 is critical | +| `RESP_RATE` | How many breaths per minute | breaths/min | 12–20 | 1–80 | Normal adults breathe 12–20 times/min. Over 30 is critical | +| `SYSTOLIC_BP` | Blood pressure (the top number) — pressure when the heart squeezes | mmHg | 90–140 | 40–300 | 120/80 is "normal." Under 70 systolic is critical (not enough blood reaching organs) | +| `DIASTOLIC_BP` | Blood pressure (the bottom number) — pressure when the heart relaxes | mmHg | 60–90 | 20–200 | The lower number in a blood pressure reading | +| `TEMP_C` | Body temperature | °C (Celsius) | 36.1–37.2 | 15–50 | Normal is ~37°C (98.6°F). Over 40°C or under 35°C is critical | +| `SPO2` | Oxygen saturation — how much oxygen is in the blood | % | 95–100 | 50–100 | Measured with a clip on the finger. Below 88% is critical | +| `SUPPLEMENTAL_O2` | Whether the patient is receiving extra oxygen | flag (0 or 1) | 0 (room air) | 0–1 | 0 = breathing normal air, 1 = on supplemental oxygen. Being on oxygen adds points to the NEWS2 score | + +### Lab results (from blood tests and other samples) + +These require a blood draw or lab analysis — they take longer to get but reveal what is happening inside the body: + +| Code | What it measures | Unit | Normal range (approx.) | Plausible range | Why it matters | +|---|---|---|---|---|---| +| `POTASSIUM_MEQ_L` | Potassium level in blood | mEq/L | 3.5–5.0 | 0.1–12 | Too high or too low can cause dangerous heart rhythms | +| `WBC_K_UL` | White blood cell count | k/uL (thousands per microliter) | 4.0–12.0 | 0.1–500 | White blood cells fight infection. Very high = possible infection. Very low = weak immune system | +| `GLUCOSE_MG_DL` | Blood sugar level | mg/dL | 70–100 (fasting) | 10–1000 | Too low (hypoglycemia) can cause seizures. Too high (hyperglycemia) damages organs over time | +| `LACTATE_MMOL_L` | Lactic acid in blood | mmol/L | 0.5–2.0 | 0.1–30 | High lactate means tissues are not getting enough oxygen — a key sign of sepsis or shock | +| `PAO2_MMHG` | Partial pressure of oxygen in arterial blood | mmHg | 80–100 | 20–600 | Requires an arterial blood draw. Shows how well lungs transfer oxygen. Used in SOFA respiratory scoring | +| `FIO2_PCT` | Fraction of inspired oxygen — how concentrated the oxygen the patient breathes is | % | 21 (room air) | 21–100 | Room air is 21%. A patient on a ventilator might be at 40–100%. Used with PaO2 to calculate SOFA respiratory score | +| `PLATELET_K_UL` | Platelet count | k/uL | 150–400 | 1–1500 | Platelets help blood clot. Very low platelets = bleeding risk. Used in SOFA coagulation scoring | +| `BILIRUBIN_MG_DL` | Bilirubin level | mg/dL | 0.1–1.2 | 0.1–50 | Produced when the liver breaks down old blood cells. High bilirubin = liver is struggling. Used in SOFA liver scoring | +| `CREATININE_MG_DL` | Creatinine level | mg/dL | 0.6–1.2 | 0.1–20 | A waste product filtered by the kidneys. High creatinine = kidneys are not filtering properly. Used in SOFA renal scoring | +| `URINE_OUTPUT_ML_H` | How much urine the patient produces per hour | mL/h | >50 | 0–500 | Low urine output = kidneys may be failing. Used in SOFA renal scoring | + +### Consciousness assessments + +These measure how awake and responsive the patient is: + +| Code | What it measures | Range | What the numbers mean | +|---|---|---|---| +| `AVPU` | Quick consciousness check | 0–3 | **0** = Alert (awake and talking), **1** = responds to Voice, **2** = responds to Pain only, **3** = Unresponsive. Used in NEWS2 scoring | +| `GCS_EYE` | Eye opening response | 1–4 | **4** = opens eyes on own, **3** = opens to voice, **2** = opens to pain, **1** = no eye opening | +| `GCS_VERBAL` | Verbal response | 1–5 | **5** = oriented/normal conversation, **4** = confused, **3** = inappropriate words, **2** = incomprehensible sounds, **1** = none | +| `GCS_MOTOR` | Motor (movement) response | 1–6 | **6** = obeys commands, **5** = localizes pain, **4** = withdraws from pain, **3** = abnormal flexion, **2** = extension, **1** = none | + +GCS total (3–15) is the sum of all three components. A GCS of 15 means fully alert. A GCS of 3 means completely unresponsive. + +--- + +## Alert thresholds — when do alarms go off? + +Every measurement is checked against configured thresholds. There are two levels: + +- **Critical** — something is dangerously wrong right now. The alert fires immediately (in the same HTTP response). These are never suppressed. +- **Warning** — something is concerning and needs attention. The alert fires in the background (via Kafka). Warnings can be temporarily suppressed after a clinician acknowledges them (30-minute window). + +| Measurement | Critical Low | Warning Low | Warning High | Critical High | +|---|---|---|---|---| +| Heart Rate (bpm) | 30 | 50 | 100 | 150 | +| Temperature (°C) | 35.0 | 36.0 | 38.3 | 40.0 | +| Potassium (mEq/L) | 2.5 | 3.5 | 5.0 | 6.5 | +| Oxygen Saturation (%) | 88 | 92 | — | — | +| Respiratory Rate (breaths/min) | — | 12 | 20 | 30 | +| White Blood Cells (k/uL) | 2.0 | 4.0 | 12.0 | 20.0 | +| Systolic BP (mmHg) | 70 | 90 | 160 | 180 | +| Diastolic BP (mmHg) | 40 | 60 | 90 | 110 | +| Lactate (mmol/L) | — | — | 2.0 | 4.0 | +| AVPU | — | — | — | 2 | +| Glucose (mg/dL) | 40 | 70 | 180 | 400 | +| PaO2 (mmHg) | 60 | 80 | — | — | +| Platelets (k/uL) | 20 | 50 | — | — | +| Bilirubin (mg/dL) | — | — | 2.0 | 6.0 | +| Creatinine (mg/dL) | — | — | 2.0 | 3.5 | + +**Example:** A heart rate of 155 bpm crosses the critical high threshold (150), so a `CRITICAL_HEART_RATE` alert fires immediately. A heart rate of 105 bpm crosses only the warning high threshold (100), so a `WARNING_HEART_RATE` alert fires in the background. + +A dash (—) means there is no threshold configured on that side. For example, oxygen saturation has no high threshold because high oxygen is generally not dangerous in this context. + +--- + +## Clinical scoring systems explained + +VigilCare calculates several clinical scores automatically. Each score looks at patient data from a different angle. Together, they give clinicians a complete picture of how the patient is doing. + +### NEWS2 — National Early Warning Score 2 + +**What it does:** Combines 7 vital signs into a single number (0–20) that tells staff "how sick is this patient right now?" It is the most widely used early warning system in UK hospitals. + +**Why it matters:** A rising NEWS2 score often means the patient is getting worse — even if no single measurement has crossed a critical threshold yet. It catches deterioration early. + +**The 7 required measurements:** + +All 7 must be recorded within a 4-hour window for a score to be calculated. If any are missing, no score is produced. + +| Measurement | Score 3 | Score 2 | Score 1 | Score 0 (normal) | Score 1 | Score 2 | Score 3 | +|---|---|---|---|---|---|---|---| +| Respiratory Rate | ≤8 | | 9–11 | 12–20 | | 21–24 | ≥25 | +| Oxygen Saturation (%) | ≤91 | 92–93 | 94–95 | ≥96 | | | | +| Systolic BP (mmHg) | ≤90 | 91–100 | 101–110 | 111–219 | | | ≥220 | +| Heart Rate (bpm) | ≤40 | | 41–50 | 51–90 | 91–110 | 111–130 | ≥131 | +| Consciousness (AVPU) | | | | Alert | | | Any other (V, P, or U) | +| Temperature (°C) | ≤35.0 | | 35.1–36.0 | 36.1–38.0 | 38.1–39.0 | ≥39.1 | | +| Supplemental O2 | | On oxygen (2) | | Room air (0) | | | | + +**How the total score maps to risk levels:** + +| Total Score | Risk Level | What it means | Alert type | +|---|---|---|---| +| 0–4 | **LOW** | Routine monitoring. Patient is stable | No alert | +| 0–4 but any single parameter scores 3 | **LOW-MEDIUM** | One vital sign is individually concerning, even though the total looks OK | `NEWS2_WARNING` | +| 5–6 | **MEDIUM** | Patient may be deteriorating. Increase monitoring frequency | `NEWS2_WARNING` | +| 7 or higher | **HIGH** | Urgent — patient is at significant risk. Senior clinician review needed | `NEWS2_EMERGENCY` | + +**Example:** A patient has: respiratory rate 24 (score 2), SpO2 94% (score 1), systolic BP 115 (score 0), heart rate 115 (score 2), AVPU alert (score 0), temperature 38.5°C (score 1), on room air (score 0). Total = 6 → **MEDIUM** risk → `NEWS2_WARNING` alert fires. + +--- + +### SOFA — Sequential Organ Failure Assessment + +**What it does:** Scores how well 6 organ systems are functioning, on a scale of 0–4 each (total 0–24). Higher scores mean worse organ function. It is the gold standard for detecting sepsis-related organ damage. + +**Why it matters:** Sepsis kills by damaging organs. SOFA catches this damage early. A jump of 2+ points from the patient's baseline is the clinical definition of sepsis (per Sepsis-3 guidelines). + +**The 6 organ systems scored:** + +| Organ System | What it measures | Data needed | Score 0 (normal) | Score 1 | Score 2 | Score 3 | Score 4 (worst) | +|---|---|---|---|---|---|---|---| +| **Respiratory** (lungs) | How well lungs transfer oxygen | PaO2 and FiO2 (or SpO2 if PaO2 unavailable) | PaO2/FiO2 ≥400 | 300–399 | 200–299 | 100–199 | <100 | +| **Coagulation** (blood clotting) | Platelet count | Platelet count | ≥150 k/uL | 100–149 | 50–99 | 20–49 | <20 | +| **Liver** | Liver function | Bilirubin | <1.2 mg/dL | 1.2–1.9 | 2.0–5.9 | 6.0–11.9 | ≥12.0 | +| **Cardiovascular** (heart/circulation) | Blood pressure support needed | MAP (mean arterial pressure) | MAP ≥70 | MAP <70 | Low-dose vasopressor | Moderate vasopressor | High-dose vasopressor | +| **CNS** (brain) | Consciousness level | GCS total score | 15 | 13–14 | 10–12 | 6–9 | <6 | +| **Renal** (kidneys) | Kidney function | Creatinine and/or urine output | Creatinine <1.2 | 1.2–1.9 | 2.0–3.4 | 3.5–4.9 | ≥5.0 or urine <200 mL/day | + +**How SOFA alerts work:** + +The system establishes a "baseline" SOFA score once at least 4 of the 6 organ systems have data. Then it watches for changes: + +| Change from baseline | Alert | Severity | What it means | +|---|---|---|---| +| Increase of 2+ points | `SOFA_SEPSIS` | CRITICAL | Meets the Sepsis-3 definition of sepsis. Immediate clinical response needed | +| Increase of 1 point | `SOFA_WARNING` | WARNING | Organs are trending worse. Watch closely | + +**Lab staleness:** SOFA uses lab results that may not be measured frequently. The system considers lab values stale after 12 hours and expired after 24 hours, meaning the score becomes less reliable over time without fresh data. + +**Example:** A patient has a baseline SOFA of 3. New labs come in showing worsening kidney function and lower platelets, pushing the SOFA to 6. That is a jump of 3 points → `SOFA_SEPSIS` alert fires. + +--- + +### qSOFA — Quick SOFA (bedside screening) + +**What it does:** A fast, simple check using just 3 things you can measure at the bedside — no lab work needed. It asks: "should we be worried about sepsis?" + +**Why it matters:** Lab results take time. qSOFA gives an answer in seconds using measurements that are already being collected. It is a screening tool — it does not diagnose sepsis, but it flags patients who need deeper evaluation (like a full SOFA score). + +**The 3 criteria:** + +| Criteria | Threshold | What it means | +|---|---|---| +| Respiratory rate | ≥22 breaths/min | Patient is breathing faster than normal — body may be compensating for something | +| Systolic blood pressure | ≤100 mmHg | Blood pressure is dropping — organs may not be getting enough blood | +| Altered mental status | AVPU ≥1 (not fully alert) or GCS <15 | Patient is confused or not fully conscious | + +**Scoring:** Each criterion met = 1 point. Range is 0–3. When **2 or more** criteria are met, a `QSOFA_SCREEN` alert fires. + +**Important:** qSOFA is a screen, not a diagnosis. It says "look closer" — it does not by itself trigger a sepsis bundle. The full SOFA score is used for sepsis determination. + +**Example:** A patient has a respiratory rate of 24 (meets criterion), systolic BP of 95 (meets criterion), and is alert (AVPU = 0, does not meet criterion). Score = 2 → `QSOFA_SCREEN` alert fires. + +--- + +### GCS — Glasgow Coma Scale + +**What it does:** Measures how conscious a patient is by testing three types of responses: eye opening, verbal response, and motor (movement) response. Total score ranges from 3 (completely unresponsive) to 15 (fully alert). + +**Why it matters:** Changes in consciousness can signal brain injury, stroke, medication effects, or worsening illness. A dropping GCS is an emergency. + +**The three components:** + +| Component | Best response (highest score) | Worst response (lowest score) | +|---|---|---| +| **Eye opening** | 4 — opens eyes spontaneously | 1 — no eye opening | +| **Verbal response** | 5 — oriented, normal conversation | 1 — no verbal response | +| **Motor response** | 6 — obeys commands | 1 — no movement | + +**Severity classification:** + +| GCS Total | Classification | Alert | What it means | +|---|---|---|---| +| 13–15 | **Mild** | No alert | Patient is mostly or fully alert | +| 9–12 | **Moderate** | `GCS_WARNING` | Significant impairment — needs monitoring | +| 3–8 | **Severe (coma)** | `GCS_CRITICAL` | Patient is in or near coma — urgent intervention needed | + +**How GCS connects to other scores:** +- **NEWS2:** GCS 15 maps to AVPU = "Alert" (score 0). Any GCS below 15 maps to "not alert" (score 3 in NEWS2). This is why even a small drop in consciousness adds 3 points to NEWS2. +- **qSOFA:** GCS below 15 counts as "altered mental status" — one of the 3 qSOFA criteria. +- **SOFA CNS component:** GCS maps directly to the SOFA brain score (15→0, 13–14→1, 10–12→2, 6–9→3, <6→4). + +--- + +### Trend detection — Rapid Deterioration + +**What it does:** Watches how fast certain vital signs are changing over time, not just whether they have crossed a threshold. Even if a value is still in a "normal" range, a rapid change can signal trouble. + +**Why it matters:** A heart rate going from 70 to 100 in 30 minutes is more alarming than a stable heart rate of 100. The trend tells you the patient is getting worse fast. + +**Monitored vital signs and velocity thresholds:** + +The system looks at changes over a 30-minute sliding window: + +| Vital Sign | Trigger rate | In plain terms | Alert | +|---|---|---|---| +| Heart Rate | ≥0.5 bpm/min | Rising by 15+ bpm in 30 minutes | `RAPID_DETERIORATION` | +| Respiratory Rate | ≥0.3 breaths/min | Rising by 9+ breaths/min in 30 minutes | `RAPID_DETERIORATION` | +| Systolic BP | ≥1.0 mmHg/min decline | Dropping by 30+ mmHg in 30 minutes | `RAPID_DETERIORATION` | +| Temperature | ≥0.05 °C/min | Rising by 1.5+°C in 30 minutes | `RAPID_DETERIORATION` | +| Oxygen Saturation | ≥0.2 %/min decline | Dropping by 6+% in 30 minutes | `RAPID_DETERIORATION` | + +**Example:** A patient's heart rate readings: 75 bpm at 2:00 PM, 82 bpm at 2:10 PM, 92 bpm at 2:25 PM. That is a rise of 17 bpm in 25 minutes (0.68 bpm/min) — above the 0.5 threshold → `RAPID_DETERIORATION` alert fires, even though 92 bpm is not above the warning threshold (100) yet. + +`RAPID_DETERIORATION` alerts are never suppressed — every rapid change triggers a new alert. --- ## Phase 0 — Platform prerequisites (before any patient) -These are typically seeded at deploy time, not called per patient. +These are typically set up once at deploy time, not called per patient. | What | How | Why it matters | |---|---|---| -| Alert thresholds (12 observation codes) | Seeded in PostgreSQL; loaded into Redis on startup; manageable via `POST/GET/PUT /alert-thresholds` | Every observation ingest validates against a configured code; thresholds drive critical (sync) and warning (async) alerts | +| Alert thresholds | Seeded in PostgreSQL; loaded into Redis on startup; manageable via `POST/GET/PUT /alert-thresholds` | Every measurement is validated against configured thresholds to decide if alerts fire | | Kafka topics | Provisioned by `KafkaTopicProvisioner` | `observation.recorded`, `alert.generated`, `encounter.status.changed` (+ sepsis bundle topics) | -| Drug-vital mappings | `MedicationCorrelation` section in `appsettings.json` | Medication context on warning/NEWS2 alert `details` | +| Drug-vital mappings | `MedicationCorrelation` section in `appsettings.json` | Links medications to vital signs so alerts can include context like "heart rate may be elevated due to epinephrine given 20 min ago" | --- @@ -105,15 +358,15 @@ POST /api/v1/patients | Field | Required | Notes | |---|---|---| | `firstName`, `lastName`, `dateOfBirth`, `gender` | yes | | -| `bloodType`, `allergies`, `emergencyContactName`, `emergencyContactPhone` | no | Stored on the patient record for ward context | +| `bloodType`, `allergies`, `emergencyContactName`, `emergencyContactPhone` | no | Stored for ward context | -**Response:** `201 Created` — `data` includes system-generated `mrn` (e.g. `MRN-000042`) and `id` (UUID). Save both; the MRN is the human-facing identifier, the UUID is used in all subsequent paths. +**Response:** `201 Created` — `data` includes a system-generated `mrn` (e.g. `MRN-000042`) and `id` (UUID). Save both; the MRN is what nurses see on wristbands, the UUID is used in all API paths. **Later lookups:** | Need | Endpoint | |---|---| -| Search by name or MRN | `GET /patients?q=…` | +| Search by name or MRN | `GET /patients?q=...` | | Demographics + active encounter summary | `GET /patients/{id}` | ### 1.2 Open an encounter (admission) @@ -128,22 +381,22 @@ POST /api/v1/patients/{patientId}/encounters | `department` | yes | e.g. `ICU`, `GENERAL_MEDICINE`, `SURGERY` | | `attendingPhysician` | yes | | | `roomBed` | no | Ward assignment (e.g. `ICU-1A`) | -| `admissionReason` | no | Clinical context for admission | +| `admissionReason` | no | Why the patient was admitted | **Response:** `201 Created` — encounter `id`, `status: ACTIVE`, `admittedAt`. **Side effects:** -- Outbox event `encounter.status.changed` → Kafka → Elasticsearch ward index, data lake, discharge-summary queue listener -- Only **one active encounter per patient per encounter type** — duplicate returns **409** (`DUPLICATE_ACTIVE_ENCOUNTER`) +- Event `encounter.status.changed` → Kafka → updates Elasticsearch ward index, data lake, and discharge-summary queue +- Only **one active encounter per patient per encounter type** — trying to create a duplicate returns **409** (`DUPLICATE_ACTIVE_ENCOUNTER`) -From this point, `encounterId` is the primary key for all clinical writes. +From this point, `encounterId` is the key for all clinical writes. --- ## Phase 2 — Ward visibility (who is on the floor) -While the patient is active, ward systems poll aggregated state rather than joining tables client-side. +While the patient is active, ward systems poll aggregated state. ### Virtual ward board @@ -153,16 +406,16 @@ GET /api/v1/encounters?status=ACTIVE&department=ICU&page=1&pageSize=20 Each row (`WardEncounterSummary`) includes: -| Field | Source | +| Field | What it shows | |---|---| -| `encounterId`, `patientId`, `mrn`, `firstName`, `lastName`, `roomBed`, `department`, `status` | Encounter + patient | -| `news2Score`, `news2RiskLevel` | Latest `news2_scores` row | -| `qsofaScore` | Live Redis count (`GET /encounters/{id}/qsofa/current` uses the same backing store) | -| `sepsisActive` | Whether a non-compliant sepsis bundle exists | -| `sepsisBundleStatus` | `IN_PROGRESS`, `COMPLIANT`, `NON_COMPLIANT` | -| `openAlertCount` | Open clinical alerts for the encounter | +| `encounterId`, `patientId`, `mrn`, `firstName`, `lastName`, `roomBed`, `department`, `status` | Patient and encounter info | +| `news2Score`, `news2RiskLevel` | Latest NEWS2 score and risk level (LOW, LOW_MEDIUM, MEDIUM, HIGH) | +| `qsofaScore` | Current qSOFA criteria count (0–3) | +| `sepsisActive` | Whether the patient has a sepsis investigation in progress | +| `sepsisBundleStatus` | `IN_PROGRESS`, `COMPLIANT`, or `NON_COMPLIANT` | +| `openAlertCount` | How many unresolved alerts exist for this patient | -The dashboard sorts this list by NEWS2 score for acuity-first display. +The dashboard sorts patients by NEWS2 score — sickest patients appear first. ### Single-patient chart header @@ -170,13 +423,13 @@ The dashboard sorts this list by NEWS2 score for acuity-first display. GET /api/v1/encounters/{id} ``` -Returns the encounter with nested `patient`, the **10 most recent observations**, and **open alerts**. Use this for a chart summary; use dedicated list endpoints for full history. +Returns the encounter with the patient's info, the **10 most recent observations**, and **open alerts**. Use this for a quick summary; use the dedicated list endpoints for full history. --- -## Phase 3 — Continuous monitoring (observation ingest) +## Phase 3 — Continuous monitoring (recording measurements) -Observations are the heartbeat of the system. Bedside devices, manual entry, and lab interfaces all use the same endpoint. +Observations are the heartbeat of the system. Bedside devices, manual nurse entries, and lab interfaces all use the same endpoint. ### Record measurements @@ -184,76 +437,72 @@ Observations are the heartbeat of the system. Bedside devices, manual entry, and POST /api/v1/encounters/{encounterId}/observations ``` -Body: `{ "observations": [ … ] }` — **1 to 10** objects per call. +Body: `{ "observations": [ ... ] }` — **1 to 10** measurements per call. | Observation field | Required | Notes | |---|---|---| -| `observationCode` | yes | Must match a configured alert threshold (12 seeded codes) | -| `value`, `unit` | yes | Plausibility-checked per code | -| `recordedAt` | yes | When the measurement was taken | +| `observationCode` | yes | One of the measurement codes listed above (e.g. `HEART_RATE`, `RESP_RATE`) | +| `value`, `unit` | yes | The numeric value and its unit. Checked for plausibility (e.g. a heart rate of 500 would be rejected) | +| `recordedAt` | yes | When the measurement was actually taken | | `source` | no | `DEVICE` (default), `MANUAL`, `LAB` | -Optional header: `Idempotency-Key` — retries with the same key return the original row without duplicate insert. +Optional header: `Idempotency-Key` — if you accidentally send the same measurement twice, the system returns the original record without creating a duplicate. -### What happens inside one ingest (synchronous HTTP path) +### What happens immediately (in the HTTP response) -1. Encounter must be `ACTIVE` -2. Idempotency check -3. Plausibility validation → **422** if out of range -4. Insert `observations` row -5. Load threshold from Redis -6. **If CRITICAL breach:** insert `clinical_alert` + `alert.generated` outbox event **in the same transaction** -7. Always insert `observation.recorded` outbox event -8. `COMMIT` +1. Encounter must be `ACTIVE` — otherwise **409** +2. Idempotency check — skip if already recorded +3. Plausibility validation — reject impossible values with **422** +4. Save the measurement +5. Check against critical thresholds from Redis +6. **If a critical threshold is breached:** create an alert immediately in the same database transaction +7. Queue the measurement for background processing +8. Commit -**HTTP response (`201`):** per-observation result with `observation`, `alertGenerated` (true only for **critical** sync alerts), `alertId`, `duplicate`. +**HTTP response (`201`):** for each measurement, you get `observation`, `alertGenerated` (true only for critical alerts), `alertId`, and `duplicate`. -| Status | Meaning | -|---|---| -| `201` | Recorded | -| `200` | Idempotency key matched existing row | -| `404` | Encounter not found | -| `409` | Encounter not active | -| `422` | Plausibility failure or unknown observation code | +### What happens in the background (not in the HTTP response) -### What happens after commit (asynchronous — not in the HTTP response) +Every recorded measurement is processed independently by these background engines: -Every `observation.recorded` Kafka message is consumed independently: +| Engine | What it does | Alert produced | +|---|---|---| +| **warning-evaluator** | Checks if the value crosses a warning threshold. Optionally adds medication context (e.g. "patient was given morphine 30 min ago") | `WARNING_*` alerts | +| **news2-scoring** | Recalculates NEWS2 if all 7 parameters are available within 4 hours | `NEWS2_WARNING` or `NEWS2_EMERGENCY` | +| **sepsis-engine** | Recalculates qSOFA criteria count | `QSOFA_SCREEN` (when 2+ criteria met) | +| **sofa-scoring** | Recalculates SOFA organ scores using latest vitals and labs | `SOFA_WARNING` or `SOFA_SEPSIS` | +| **gcs-scoring** | Recalculates GCS when eye/verbal/motor components are recorded | `GCS_WARNING` or `GCS_CRITICAL` | +| **trend-analyzer** | Checks if the vital sign is changing too fast (rate of change over 30-min window) | `RAPID_DETERIORATION` | +| **es-indexer** | Updates Elasticsearch for search and analytics | (no alert) | +| **data-lake-writer** | Writes Parquet files to MinIO for long-term analysis | (no alert) | -| Consumer group | Produces | -|---|---| -| `warning-evaluator` | `WARNING_*` threshold alerts (with optional medication annotation) | -| `news2-scoring` | `news2_scores` row; `NEWS2_WARNING` / `NEWS2_EMERGENCY` when all 7 parameters present | -| `sepsis-engine` | `SEPSIS_WARNING` (SIRS ≥2) or `QSOFA_WARNING` (qSOFA ≥2); triggers sepsis bundle | -| `trend-analyzer` | `RAPID_DETERIORATION` when rate-of-change exceeds configured velocity | -| `es-indexer` | Elasticsearch projection for ward search and analytics | -| `data-lake-writer` | Parquet files in MinIO | +**Important for integrators:** After posting measurements, poll `GET /encounters/{id}/alerts` or score endpoints. Warning and scoring alerts are NOT in the initial HTTP response — they arrive 1–3 seconds later. -**Integrator pattern:** after `POST …/observations`, poll `GET /encounters/{id}/alerts` or use the simulator's `--poll` flag. Do not expect warning or NEWS2 alerts in the ingest response body. - -### Read observation history +### Read measurement history ``` -GET /api/v1/encounters/{encounterId}/observations?code=HEART_RATE&from=…&to=…&limit=50&cursor=… +GET /api/v1/encounters/{encounterId}/observations?code=HEART_RATE&from=...&to=...&limit=50&cursor=... ``` -Cursor-paginated on `(recorded_at DESC, id DESC)` — preferred over offset pagination for live streams. +Cursor-paginated (newest first). Use `code` to filter by measurement type. --- ## Phase 4 — Clinical interventions (medications and orders) -These run in parallel with monitoring; they do not replace observation ingest. +These run in parallel with monitoring; they do not replace recording measurements. ### Medication administrations ``` POST /api/v1/encounters/{encounterId}/medications -GET /api/v1/encounters/{encounterId}/medications?since=…&page=1&pageSize=20 +GET /api/v1/encounters/{encounterId}/medications?since=...&page=1&pageSize=20 GET /api/v1/medications/{id} ``` -Recording a drug does **not** create alerts. When a later warning or NEWS2 alert fires, `MedicationCorrelationHelper` may append context to `details` if a mapped drug was given within the correlation window (default 90 min). See [medication-correlation-design.md](decisions/medication-correlation-design.md). +Recording a drug does **not** create alerts on its own. Instead, when a warning or NEWS2 alert fires later, the system checks: "was a relevant drug given recently?" If so, it adds context to the alert details. For example, a high heart rate warning might include: "Note: epinephrine administered 20 minutes ago." + +The correlation window is 90 minutes by default — only drugs given within the last 90 minutes are linked. ### Clinical orders @@ -265,40 +514,45 @@ PATCH /api/v1/orders/{id}/status PATCH /api/v1/orders/{id}/result ``` -**Order status machine:** +**Order status flow:** ``` pending → in_progress → resulted → cancelled ``` -`PATCH …/result` is the clinical completion path — it transitions to `Resulted` and, for sepsis-bundle-linked orders, marks the corresponding bundle element complete. +`PATCH .../result` is how clinicians record that an order is complete — for example, recording that a blood culture was collected. For orders that are part of a sepsis bundle, completing the order also marks that bundle element as done. --- ## Phase 5 — Alerts fire (detection to triage) -Alerts are created by multiple engines (sync critical threshold, async warning, NEWS2, SIRS/qSOFA, trend). All share the same read and lifecycle API. +Alerts are created by multiple engines (critical thresholds, warning thresholds, NEWS2, qSOFA, SOFA, GCS, trend detection). All share the same read and lifecycle API. ### List and inspect | Endpoint | Use | |---|---| -| `GET /encounters/{id}/alerts?status=OPEN` | Per-patient alert list | +| `GET /encounters/{id}/alerts?status=OPEN` | Alerts for one patient | | `GET /alerts?status=OPEN&severity=CRITICAL&department=ICU` | Hospital-wide alert center | -| `GET /alerts/{id}` | Alert detail with encounter | +| `GET /alerts/{id}` | Single alert with full details | -Each `ClinicalAlert` includes `alertType`, `severity`, `details`, `status`, `triggeredAt`, and optional `observationId`. +Each alert includes `alertType`, `severity`, `details` (with medication context when applicable), `status`, `triggeredAt`, and optional `observationId` (which measurement triggered it). -**Alert types you will see during an active stay:** +**All alert types:** -| Type | Engine | Severity | Medication annotation | +| Alert Type | What triggered it | Severity | Can be suppressed? | |---|---|---|---| -| `CRITICAL_*` | Sync ingest | CRITICAL | Never | -| `WARNING_*` | `warning-evaluator` | WARNING | Yes, when correlated | -| `NEWS2_WARNING`, `NEWS2_EMERGENCY` | `news2-scoring` | WARNING / CRITICAL | Yes (warnings) | -| `SEPSIS_WARNING`, `QSOFA_WARNING` | `sepsis-engine` | WARNING / CRITICAL | Never | -| `RAPID_DETERIORATION` | `trend-analyzer` | WARNING | Never | +| `CRITICAL_*` (e.g. `CRITICAL_HEART_RATE`) | A measurement crossed a critical threshold | CRITICAL | No | +| `WARNING_*` (e.g. `WARNING_HEART_RATE`) | A measurement crossed a warning threshold | WARNING | Yes (30 min) | +| `NEWS2_WARNING` | NEWS2 score is 5–6, or any single parameter scores 3 | WARNING | Yes (30 min) | +| `NEWS2_EMERGENCY` | NEWS2 score is 7 or higher | CRITICAL | No | +| `QSOFA_SCREEN` | 2 or more qSOFA criteria met | WARNING | Yes (30 min) | +| `SOFA_SEPSIS` | SOFA score increased 2+ points from baseline | CRITICAL | No | +| `SOFA_WARNING` | SOFA score increased 1 point from baseline | WARNING | Yes (30 min) | +| `GCS_CRITICAL` | GCS total is 8 or below (coma) | CRITICAL | No | +| `GCS_WARNING` | GCS total is 9–12 (moderate impairment) | WARNING | Yes (30 min) | +| `RAPID_DETERIORATION` | A vital sign is changing too fast | WARNING | No | ### Acknowledge and resolve @@ -309,64 +563,92 @@ Body: { "clinicianId": "DR-SMITH", "note": "optional" } POST /api/v1/alerts/{id}/resolve ``` -**Lifecycle:** +**Alert lifecycle:** ``` open → acknowledged → resolved - → escalated (unacknowledged CRITICAL > 5 min → RabbitMQ DLQ → on-call page) + → escalated (unacknowledged CRITICAL after 5 min → pages on-call staff) ``` -Acknowledging a suppressible warning (`WARNING_*`, `NEWS2_WARNING`) sets a Redis suppression window (default 30 min) so duplicate warnings for the same encounter/type do not fire repeatedly. Critical and sepsis alerts are never suppressed. +When a clinician acknowledges a suppressible warning, a 30-minute suppression window starts in Redis. During that window, duplicate warnings of the same type for the same patient will not fire again. Critical and SOFA-sepsis alerts are never suppressed. -### Notification path (no HTTP endpoint) +### Escalation (no HTTP endpoint) -`alert.generated` → Kafka → RabbitMQ `alerts.paging.queue` → clinician page simulation. Critical unacknowledged alerts escalate via DLQ. This is infrastructure-side; integrators observe it through alert `status` and Prometheus `escalations_total`. +Critical alerts that remain unacknowledged for 5+ minutes are automatically escalated — they are routed to an on-call paging queue. This happens at the infrastructure level; integrators see it through the alert's `status` field changing to `escalated`. --- -## Phase 6 — Composite scores (NEWS2 and qSOFA) +## Phase 6 — Composite scores (NEWS2, SOFA, qSOFA, GCS) ### NEWS2 -Requires all seven parameters in Redis within a 4-hour window: `RESP_RATE`, `SPO2`, `SYSTOLIC_BP`, `HEART_RATE`, `AVPU`, `TEMP_C`, `SUPPLEMENTAL_O2`. +Requires all 7 parameters in Redis within a 4-hour window: `RESP_RATE`, `SPO2`, `SYSTOLIC_BP`, `HEART_RATE`, `AVPU`, `TEMP_C`, `SUPPLEMENTAL_O2`. ``` GET /api/v1/encounters/{id}/news2/current → latest score (404 if not yet computed) -GET /api/v1/encounters/{id}/news2/history → cursor-paginated score history +GET /api/v1/encounters/{id}/news2/history → score history over time ``` -Scores and NEWS2 alerts are **async** — ingest the seventh parameter, wait a few seconds, then poll. +Scores and NEWS2 alerts are calculated in the background — record the 7th parameter, wait a few seconds, then poll. + +### SOFA + +Requires lab results and vitals across 6 organ systems. Not all systems need data immediately — the score is computed with whatever is available once at least 4 systems have data. + +``` +GET /api/v1/encounters/{id}/sofa/current → latest SOFA score with per-organ breakdown +GET /api/v1/encounters/{id}/sofa/history → score history +``` + +The response includes per-organ scores (respiratory, coagulation, liver, cardiovascular, CNS, renal) so clinicians can see which organ systems are struggling. ### qSOFA -No dedicated score history table. Live criteria count from Redis: +Live criteria count from Redis (no stored history — just the current state): ``` GET /api/v1/encounters/{id}/qsofa/current ``` -Returns active criteria count (0–3). When count reaches 2, `sepsis-engine` creates a `QSOFA_WARNING` alert and a sepsis bundle (same as SIRS). +Returns how many of the 3 criteria are currently met (0–3). When the count reaches 2, a `QSOFA_SCREEN` alert fires. + +### GCS + +``` +GET /api/v1/encounters/{id}/gcs/current → latest GCS with component breakdown +``` + +Returns the total GCS score, individual component scores (eye, verbal, motor), and severity classification (mild/moderate/severe). --- ## Phase 7 — Sepsis bundle (when infection is suspected) -Triggered automatically when `SEPSIS_WARNING` or `QSOFA_WARNING` fires — not by a direct API call. +A sepsis bundle is a checklist of things that **must** be done within 1 hour when sepsis is suspected. VigilCare creates bundles automatically when a `SOFA_SEPSIS` alert fires — not by a direct API call. ``` GET /api/v1/encounters/{id}/sepsis-bundle/current GET /api/v1/sepsis-bundles/{id} ``` -**Bundle contents:** four elements (blood cultures, serum lactate, broad-spectrum antibiotics, IV fluid resuscitation), each backed by an auto-created order (`orderedBy: sepsis-bundle-engine`). +**The 4 bundle elements** (each backed by an auto-created clinical order): + +| Element | What it is | Why within 1 hour | +|---|---|---| +| **Blood cultures** | Draw blood samples to identify which bacteria is causing the infection | Need to identify the infection before antibiotics potentially mask it | +| **Serum lactate** | Measure lactic acid level in blood | High lactate means organs are not getting enough oxygen — guides how aggressive treatment needs to be | +| **Broad-spectrum antibiotics** | Give antibiotics that cover many types of bacteria | Every hour of delay increases mortality risk. Start broad, then narrow down when culture results come back | +| **IV fluid resuscitation** | Give fluids through an IV to restore blood pressure and organ perfusion | Sepsis causes blood vessels to leak — fluids keep blood pressure up and organs working | **Compliance flow:** -1. Bundle created with `complianceStatus: IN_PROGRESS`, `deadlineAt` = recognition + 1 hour -2. Clinician (or simulator) results orders via `PATCH /orders/{id}/result` -3. Each result marks the matching bundle element `COMPLETED` -4. When all four complete: `COMPLIANT` (within deadline) or `NON_COMPLIANT` (past deadline) -5. `SepsisBundleMonitorService` marks overdue in-progress bundles `NON_COMPLIANT` every 5 minutes +1. Bundle is created with `complianceStatus: IN_PROGRESS` and `deadlineAt` set to 1 hour from detection +2. Clinicians complete orders via `PATCH /orders/{id}/result` +3. Each completed order marks its bundle element `COMPLETED` +4. When all 4 elements are done: + - **Within deadline** → `COMPLIANT` (good) + - **Past deadline** → `NON_COMPLIANT` (needs review — delay may have worsened outcome) +5. A background monitor checks every 5 minutes and marks overdue bundles as `NON_COMPLIANT` ``` GET /api/v1/encounters/{id}/orders @@ -384,18 +666,18 @@ Use this to see bundle-linked orders and their status during the stay. GET /api/v1/encounters/{id}/timeline ``` -Merged chronological stream of **observations** and **alerts** (not medications, orders, or status changes). Useful for audit-style review and dashboard replay alignment. +A combined chronological stream of **observations** and **alerts** — like a scrolling history of everything that has happened to the patient. Useful for reviewing events during ward rounds or shift handovers. ### Elasticsearch analytics (read-only, cross-encounter) | Endpoint | Purpose | |---|---| -| `GET /analytics/patients?q=…&department=…` | Patient/encounter search | -| `GET /analytics/observations/trend?encounterId=…&code=…` | Hourly avg/min/max time series | -| `GET /analytics/alerts/summary?severity=…&department=…` | Alert volume by department | -| `GET /analytics/population?code=…&threshold=…` | Patients above/below threshold in a window | +| `GET /analytics/patients?q=...&department=...` | Search patients and encounters | +| `GET /analytics/observations/trend?encounterId=...&code=...` | Hourly avg/min/max time series for a measurement | +| `GET /analytics/alerts/summary?severity=...&department=...` | Alert volume by department | +| `GET /analytics/population?code=...&threshold=...` | Find patients above/below a measurement threshold | -These read from the CQRS projection fed by Kafka — not from live PostgreSQL ingest tables. +These read from a search index fed by Kafka — not from the live database. There may be a few seconds of delay. --- @@ -412,22 +694,22 @@ Body: { "status": "DISCHARGED", "dischargeDiagnosis": "optional free text" } **Side effects:** -- `dischargedAt` set to server UTC time -- Outbox `encounter.status.changed` → Kafka → Elasticsearch status update, data lake, **discharge summary worker** (PDF to MinIO at `/discharge-summaries/{encounterId}/summary.pdf`) +- `dischargedAt` is set to the current time +- Event fires → Kafka → Elasticsearch status update, data lake, and a **discharge summary worker** that generates a PDF stored at `/discharge-summaries/{encounterId}/summary.pdf` ### What stops working after discharge | Action | Result | |---|---| -| `POST …/observations` | **409** `ENCOUNTER_NOT_ACTIVE` | -| `POST …/medications` | **409** `ENCOUNTER_NOT_ACTIVE` | -| `POST …/orders` | **409** `ENCOUNTER_NOT_ACTIVE` | -| `GET …/alerts`, `GET …/observations`, `GET …/news2/history` | Still work — historical read | +| `POST .../observations` | **409** `ENCOUNTER_NOT_ACTIVE` — cannot record new measurements | +| `POST .../medications` | **409** `ENCOUNTER_NOT_ACTIVE` — cannot record new medications | +| `POST .../orders` | **409** `ENCOUNTER_NOT_ACTIVE` — cannot create new orders | +| `GET .../alerts`, `GET .../observations`, `GET .../news2/history` | Still work — you can read historical data | | `GET /encounters?status=ACTIVE` | Patient drops off the active ward list | -**Cancellation path:** `PATCH …/status` with `{ "status": "CANCELLED" }` from `ACTIVE` (e.g. admission error). Same write restrictions apply afterward. +**Cancellation:** `PATCH .../status` with `{ "status": "CANCELLED" }` from `ACTIVE` (e.g. an admission was created by mistake). Same write restrictions apply afterward. -A patient may later receive a new encounter via `POST /patients/{id}/encounters` — each episode is independent; MRN persists across the lifetime. +A patient may later return and receive a new encounter via `POST /patients/{id}/encounters`. Each hospital visit is independent; the patient's MRN stays the same across their lifetime. --- @@ -448,6 +730,9 @@ A patient may later receive a new encounter via `POST /patients/{id}/encounters` | **5 — Alerts** | `GET` | `/encounters/{id}/alerts`, `/alerts`, `/alerts/{id}` | | | `POST` | `/alerts/{id}/acknowledge`, `/alerts/{id}/resolve` | | **6 — Scores** | `GET` | `/encounters/{id}/news2/current`, `/encounters/{id}/news2/history` | +| | `GET` | `/encounters/{id}/sofa/current`, `/encounters/{id}/sofa/history` | +| | `GET` | `/encounters/{id}/qsofa/current` | +| | `GET` | `/encounters/{id}/gcs/current` | | **7 — Sepsis** | `GET` | `/encounters/{id}/sepsis-bundle/current`, `/sepsis-bundles/{id}` | | **8 — Review** | `GET` | `/encounters/{id}/timeline` | | | `GET` | `/analytics/patients`, `/analytics/observations/trend`, `/analytics/alerts/summary`, `/analytics/population` | @@ -457,40 +742,43 @@ A patient may later receive a new encounter via `POST /patients/{id}/encounters` --- -## Reference walkthrough — inpatient stay (curl-style sequence) +## Reference walkthrough — inpatient stay (step by step) -Minimal happy-path sequence an integrator or simulator follows: +Here is the minimum sequence an integrator or simulator follows for a typical hospital stay: ``` -# 1. Admit -POST /api/v1/patients -POST /api/v1/patients/{patientId}/encounters +# 1. Admit the patient +POST /api/v1/patients ← register the person +POST /api/v1/patients/{patientId}/encounters ← start their hospital visit -# 2. Baseline vitals (may not trigger alerts) -POST /api/v1/encounters/{encounterId}/observations +# 2. Record baseline vitals (first set of measurements) +POST /api/v1/encounters/{encounterId}/observations ← heart rate, BP, temp, etc. -# 3. Treat -POST /api/v1/encounters/{encounterId}/medications -POST /api/v1/encounters/{encounterId}/orders +# 3. Give medications and order tests +POST /api/v1/encounters/{encounterId}/medications ← record drugs given +POST /api/v1/encounters/{encounterId}/orders ← order blood tests, cultures, etc. -# 4. Monitor & respond (repeat during stay) -POST /api/v1/encounters/{encounterId}/observations -GET /api/v1/encounters/{encounterId}/alerts -GET /api/v1/encounters/{encounterId}/news2/current -GET /api/v1/encounters/{encounterId}/sepsis-bundle/current -POST /api/v1/alerts/{alertId}/acknowledge -PATCH /api/v1/orders/{orderId}/result +# 4. Ongoing monitoring (repeat during the stay) +POST /api/v1/encounters/{encounterId}/observations ← new vitals every few hours +GET /api/v1/encounters/{encounterId}/alerts ← check for alerts (wait 1-3s after posting vitals) +GET /api/v1/encounters/{encounterId}/news2/current ← check NEWS2 score +GET /api/v1/encounters/{encounterId}/sofa/current ← check SOFA score +GET /api/v1/encounters/{encounterId}/qsofa/current ← check qSOFA screen +GET /api/v1/encounters/{encounterId}/gcs/current ← check consciousness level +GET /api/v1/encounters/{encounterId}/sepsis-bundle/current ← check sepsis bundle if active +POST /api/v1/alerts/{alertId}/acknowledge ← clinician acknowledges an alert +PATCH /api/v1/orders/{orderId}/result ← record order completion -# 5. Ward round -GET /api/v1/encounters?status=ACTIVE -GET /api/v1/encounters/{encounterId} -GET /api/v1/encounters/{encounterId}/timeline +# 5. Ward round (overview) +GET /api/v1/encounters?status=ACTIVE ← see all active patients +GET /api/v1/encounters/{encounterId} ← patient summary +GET /api/v1/encounters/{encounterId}/timeline ← full event history # 6. Discharge -PATCH /api/v1/encounters/{encounterId}/status { "status": "DISCHARGED", "dischargeDiagnosis": "…" } +PATCH /api/v1/encounters/{encounterId}/status { "status": "DISCHARGED", "dischargeDiagnosis": "..." } ``` -For a worked clinical narrative (UTI → sepsis, medication false alarm, rapid deterioration), replay the JSON scenarios in `VigilCare.Simulator/Scenarios/List/` — each scenario file is a scripted instance of this lifecycle. +For realistic clinical narratives (UTI progressing to sepsis, medication-related false alarms, rapid deterioration), replay the JSON scenarios in `VigilCare.Simulator/Scenarios/List/` — each scenario file walks through this lifecycle with real clinical data. --- @@ -501,24 +789,46 @@ The Vue ward dashboard (`vigilcare-dashboard/`) implements this lifecycle throug | UI screen | Primary API calls | |---|---| | Virtual Ward | `GET /encounters?status=ACTIVE` | -| Patient Detail | `GET /encounters/{id}`, `GET …/news2/current`, `GET …/medications`, `GET …/orders`, `GET …/sepsis-bundle/current`, `GET …/observations`, `GET …/news2/history` | +| Patient Detail | `GET /encounters/{id}`, `GET .../news2/current`, `GET .../sofa/current`, `GET .../medications`, `GET .../orders`, `GET .../sepsis-bundle/current`, `GET .../observations`, `GET .../news2/history` | | Alert Center | `GET /alerts?status=OPEN` | | Alert actions | `POST /alerts/{id}/acknowledge`, `POST /alerts/{id}/resolve` | -| Alert reasoning | `alert.details` from alert list + client-side medication window from `GET …/medications` | - -Clinician feedback ratings are stored in the browser only (no API endpoint) — see [clinical-testing-guide.md](clinical-testing-guide.md). +| Alert reasoning | `alert.details` from alert list + client-side medication window from `GET .../medications` | --- ## Sync vs async quick reference -| Event | Visible in HTTP response? | Poll these endpoints | +| Event | In the HTTP response? | Where to check afterward | |---|---|---| -| Observation saved | Yes (`201`) | `GET …/observations` | -| Critical alert | Yes (`alertGenerated: true`) | `GET …/alerts` | -| Warning alert | No — Kafka | `GET …/alerts` (after ~1–3 s) | -| NEWS2 score / alert | No — Kafka | `GET …/news2/current`, `GET …/alerts` | -| SIRS / qSOFA / sepsis bundle | No — Kafka | `GET …/alerts`, `GET …/sepsis-bundle/current`, `GET …/qsofa/current` | -| Trend alert | No — Kafka | `GET …/alerts` | -| Medication annotation on alert | No — applied at alert insert | `GET …/alerts` → read `details` | -| Discharge summary PDF | No — RabbitMQ worker | MinIO `/discharge-summaries/{encounterId}/summary.pdf` | +| Measurement saved | Yes (`201`) | `GET .../observations` | +| Critical threshold alert | Yes (`alertGenerated: true`) | `GET .../alerts` | +| Warning threshold alert | No — background (Kafka) | `GET .../alerts` (after ~1–3 s) | +| NEWS2 score / alert | No — background (Kafka) | `GET .../news2/current`, `GET .../alerts` | +| SOFA score / alert | No — background (Kafka) | `GET .../sofa/current`, `GET .../alerts` | +| qSOFA screen | No — background (Kafka) | `GET .../alerts`, `GET .../qsofa/current` | +| GCS score / alert | No — background (Kafka) | `GET .../gcs/current`, `GET .../alerts` | +| Trend alert | No — background (Kafka) | `GET .../alerts` | +| Medication annotation on alert | No — applied at alert creation | `GET .../alerts` → read `details` field | +| Discharge summary PDF | No — background (RabbitMQ) | MinIO `/discharge-summaries/{encounterId}/summary.pdf` | + +--- + +## How it all fits together — a real example + +Imagine a patient named Maria arrives at the Emergency Department with a suspected urinary tract infection (UTI): + +1. **Admission:** A clerk registers Maria (`POST /patients`) and opens an emergency encounter (`POST /patients/{id}/encounters` with `encounterType: EMERGENCY`). + +2. **First vitals:** A nurse records heart rate 95, respiratory rate 20, BP 115/75, temperature 38.8°C, SpO2 97%, AVPU = Alert, room air. No critical thresholds are crossed, so no immediate alerts. In the background, NEWS2 calculates: total = 3 (temp score 1 + heart rate score 1 + resp rate score 0 + others 0 + temperature gives 1) → LOW risk, no alert. + +3. **Labs ordered:** The doctor orders blood cultures, lactate, and a CBC (`POST /encounters/{id}/orders`). Broad-spectrum antibiotics are administered (`POST /encounters/{id}/medications`). + +4. **Two hours later — getting worse:** New vitals come in: heart rate 118, respiratory rate 24, BP 95/60, temperature 39.5°C, SpO2 94%. The `WARNING_SYSTOLIC_BP` fires (below 100). NEWS2 recalculates to 8 → HIGH risk → `NEWS2_EMERGENCY` alert fires. qSOFA sees respiratory rate ≥22 and systolic BP ≤100 → 2 criteria → `QSOFA_SCREEN` fires. + +5. **Lab results arrive:** Lactate comes back at 3.2 mmol/L (high — tissue hypoxia). Creatinine is 2.1 mg/dL (elevated — kidneys struggling). SOFA score jumps by 3 points from baseline → `SOFA_SEPSIS` alert fires. A sepsis bundle is automatically created with a 1-hour deadline. + +6. **Sepsis bundle completion:** The team draws blood cultures, gives IV fluids, and confirms antibiotics were already given. Each order is completed via `PATCH /orders/{id}/result`. All 4 elements complete within the deadline → bundle status: `COMPLIANT`. + +7. **Stabilization and discharge:** Over the next 48 hours, vitals normalize. NEWS2 drops back to 2. SOFA returns to baseline. Maria is discharged with `PATCH /encounters/{id}/status`. + +Throughout this story, the ward board showed Maria's deterioration in real time — her NEWS2 score rising, qSOFA flagging, SOFA alerting — giving the clinical team the information they needed to act fast.