Strictly eliminate fake and stale vitals and enforce machine-to-MRN isolation
This commit is contained in:
@@ -11,6 +11,7 @@ from fastapi import APIRouter, Depends, HTTPException, WebSocket, WebSocketDisco
|
||||
from fastapi.responses import HTMLResponse
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import or_
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from database import get_db
|
||||
from models import Device, Patient, VitalReading, TransmissionLog
|
||||
|
||||
@@ -161,74 +162,100 @@ def get_latest_readings(
|
||||
def list_patients(db: Session = Depends(get_db)):
|
||||
"""
|
||||
List all patients with their assigned device and latest vitals readings.
|
||||
Strictly enforce live data timeouts:
|
||||
- Continuous parameters (HR, SpO2, RR, Temp): max 25 seconds age.
|
||||
- Intermittent parameters (NIBP): max 15 minutes age.
|
||||
- Never backfill from stale historical readings.
|
||||
"""
|
||||
patients = db.query(Patient).filter(Patient.patient_id != "UNKNOWN", Patient.mrn != "UNKNOWN").all()
|
||||
results = []
|
||||
now = datetime.now(timezone.utc)
|
||||
live_cutoff = now - timedelta(seconds=25)
|
||||
nibp_cutoff = now - timedelta(minutes=15)
|
||||
|
||||
for p in patients:
|
||||
# Find latest non-null vitals across recent readings for this patient or device
|
||||
recent_readings = (
|
||||
db.query(VitalReading)
|
||||
.filter(
|
||||
or_(
|
||||
VitalReading.patient_id == p.mrn,
|
||||
VitalReading.patient_id == p.patient_id,
|
||||
(VitalReading.device_id == p.device_id) if p.device_id else False
|
||||
)
|
||||
)
|
||||
.order_by(VitalReading.timestamp.desc())
|
||||
.limit(100)
|
||||
.all()
|
||||
)
|
||||
|
||||
merged_vitals = None
|
||||
if recent_readings:
|
||||
merged_vitals = {
|
||||
"heart_rate": None,
|
||||
"spo2": None,
|
||||
"systolic_bp": None,
|
||||
"diastolic_bp": None,
|
||||
"map_bp": None,
|
||||
"respiratory_rate": None,
|
||||
"temperature": None,
|
||||
"temperature2": None,
|
||||
"temperature_diff": None,
|
||||
"timestamp": recent_readings[0].timestamp.isoformat() if recent_readings[0].timestamp else None,
|
||||
}
|
||||
for r in reversed(recent_readings):
|
||||
if r.heart_rate is not None: merged_vitals["heart_rate"] = r.heart_rate
|
||||
if r.spo2 is not None: merged_vitals["spo2"] = r.spo2
|
||||
if r.systolic_bp is not None: merged_vitals["systolic_bp"] = r.systolic_bp
|
||||
if r.diastolic_bp is not None: merged_vitals["diastolic_bp"] = r.diastolic_bp
|
||||
if r.map_bp is not None: merged_vitals["map_bp"] = r.map_bp
|
||||
if r.respiratory_rate is not None: merged_vitals["respiratory_rate"] = r.respiratory_rate
|
||||
if r.temperature is not None: merged_vitals["temperature"] = r.temperature
|
||||
if getattr(r, 'temperature2', None) is not None: merged_vitals["temperature2"] = r.temperature2
|
||||
if getattr(r, 'temperature_diff', None) is not None: merged_vitals["temperature_diff"] = r.temperature_diff
|
||||
|
||||
# Backfill any parameters (like SpO2 or NIBP) that were measured earlier
|
||||
# Primary source: in-memory live cache if running locally with active machine stream
|
||||
try:
|
||||
from contec_server import device_cache, device_field_timestamps
|
||||
if p.device_id and p.device_id in device_cache:
|
||||
cached = device_cache[p.device_id]
|
||||
field_ts = device_field_timestamps.get(p.device_id, {})
|
||||
cached_time = cached.timestamp
|
||||
if cached_time:
|
||||
if cached_time.tzinfo is None:
|
||||
cached_time = cached_time.replace(tzinfo=timezone.utc)
|
||||
if (now - cached_time).total_seconds() <= 25:
|
||||
merged_vitals = {
|
||||
"heart_rate": cached.heart_rate if (field_ts.get("heart_rate") and (now - field_ts["heart_rate"]).total_seconds() <= 25) else None,
|
||||
"spo2": cached.spo2 if (field_ts.get("spo2") and (now - field_ts["spo2"]).total_seconds() <= 25) else None,
|
||||
"respiratory_rate": cached.respiratory_rate if (field_ts.get("respiratory_rate") and (now - field_ts["respiratory_rate"]).total_seconds() <= 25) else None,
|
||||
"temperature": cached.temperature if (field_ts.get("temperature") and (now - field_ts["temperature"]).total_seconds() <= 25) else None,
|
||||
"temperature2": getattr(cached, 'temperature2', None) if (field_ts.get("temperature2") and (now - field_ts["temperature2"]).total_seconds() <= 25) else None,
|
||||
"temperature_diff": getattr(cached, 'temperature_diff', None) if (field_ts.get("temperature_diff") and (now - field_ts["temperature_diff"]).total_seconds() <= 25) else None,
|
||||
"systolic_bp": cached.systolic_bp if (field_ts.get("systolic_bp") and (now - field_ts["systolic_bp"]).total_seconds() <= 900) else None,
|
||||
"diastolic_bp": cached.diastolic_bp if (field_ts.get("diastolic_bp") and (now - field_ts["diastolic_bp"]).total_seconds() <= 900) else None,
|
||||
"map_bp": cached.map_bp if (field_ts.get("map_bp") and (now - field_ts["map_bp"]).total_seconds() <= 900) else None,
|
||||
"timestamp": cached_time.isoformat()
|
||||
}
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not read from device_cache: {e}")
|
||||
|
||||
# Fallback or cloud/Render mode: query DB strictly within recent time window
|
||||
if merged_vitals is None:
|
||||
patient_filters = or_(
|
||||
VitalReading.patient_id == p.mrn,
|
||||
VitalReading.patient_id == p.patient_id,
|
||||
(VitalReading.device_id == p.device_id) if p.device_id else False
|
||||
)
|
||||
if merged_vitals["spo2"] is None:
|
||||
r_spo2 = db.query(VitalReading.spo2).filter(patient_filters, VitalReading.spo2 != None).order_by(VitalReading.timestamp.desc()).first()
|
||||
if r_spo2: merged_vitals["spo2"] = r_spo2[0]
|
||||
if merged_vitals["heart_rate"] is None:
|
||||
r_hr = db.query(VitalReading.heart_rate).filter(patient_filters, VitalReading.heart_rate != None).order_by(VitalReading.timestamp.desc()).first()
|
||||
if r_hr: merged_vitals["heart_rate"] = r_hr[0]
|
||||
if merged_vitals["systolic_bp"] is None:
|
||||
r_bp = db.query(VitalReading.systolic_bp, VitalReading.diastolic_bp, VitalReading.map_bp).filter(patient_filters, VitalReading.systolic_bp != None).order_by(VitalReading.timestamp.desc()).first()
|
||||
if r_bp:
|
||||
merged_vitals["systolic_bp"] = r_bp[0]
|
||||
merged_vitals["diastolic_bp"] = r_bp[1]
|
||||
merged_vitals["map_bp"] = r_bp[2]
|
||||
if merged_vitals["respiratory_rate"] is None:
|
||||
r_rr = db.query(VitalReading.respiratory_rate).filter(patient_filters, VitalReading.respiratory_rate != None).order_by(VitalReading.timestamp.desc()).first()
|
||||
if r_rr: merged_vitals["respiratory_rate"] = r_rr[0]
|
||||
if merged_vitals["temperature"] is None:
|
||||
r_temp = db.query(VitalReading.temperature).filter(patient_filters, VitalReading.temperature != None).order_by(VitalReading.timestamp.desc()).first()
|
||||
if r_temp: merged_vitals["temperature"] = r_temp[0]
|
||||
# Find readings within the live cutoff window (last 25 seconds)
|
||||
recent_readings = (
|
||||
db.query(VitalReading)
|
||||
.filter(patient_filters, VitalReading.timestamp >= live_cutoff)
|
||||
.order_by(VitalReading.timestamp.desc())
|
||||
.limit(20)
|
||||
.all()
|
||||
)
|
||||
|
||||
if recent_readings:
|
||||
merged_vitals = {
|
||||
"heart_rate": None,
|
||||
"spo2": None,
|
||||
"systolic_bp": None,
|
||||
"diastolic_bp": None,
|
||||
"map_bp": None,
|
||||
"respiratory_rate": None,
|
||||
"temperature": None,
|
||||
"temperature2": None,
|
||||
"temperature_diff": None,
|
||||
"timestamp": recent_readings[0].timestamp.isoformat() if recent_readings[0].timestamp else None,
|
||||
}
|
||||
for r in reversed(recent_readings):
|
||||
if r.heart_rate is not None: merged_vitals["heart_rate"] = r.heart_rate
|
||||
if r.spo2 is not None: merged_vitals["spo2"] = r.spo2
|
||||
if r.respiratory_rate is not None: merged_vitals["respiratory_rate"] = r.respiratory_rate
|
||||
if r.temperature is not None: merged_vitals["temperature"] = r.temperature
|
||||
if getattr(r, 'temperature2', None) is not None: merged_vitals["temperature2"] = r.temperature2
|
||||
if getattr(r, 'temperature_diff', None) is not None: merged_vitals["temperature_diff"] = r.temperature_diff
|
||||
|
||||
# Check NIBP separately within 15 minute window
|
||||
r_bp = (
|
||||
db.query(VitalReading.systolic_bp, VitalReading.diastolic_bp, VitalReading.map_bp, VitalReading.timestamp)
|
||||
.filter(patient_filters, VitalReading.systolic_bp != None, VitalReading.timestamp >= nibp_cutoff)
|
||||
.order_by(VitalReading.timestamp.desc())
|
||||
.first()
|
||||
)
|
||||
if r_bp:
|
||||
if merged_vitals is None:
|
||||
merged_vitals = {
|
||||
"heart_rate": None, "spo2": None, "respiratory_rate": None,
|
||||
"temperature": None, "temperature2": None, "temperature_diff": None,
|
||||
"timestamp": r_bp[3].isoformat() if r_bp[3] else None
|
||||
}
|
||||
merged_vitals["systolic_bp"] = r_bp[0]
|
||||
merged_vitals["diastolic_bp"] = r_bp[1]
|
||||
merged_vitals["map_bp"] = r_bp[2]
|
||||
|
||||
device = db.query(Device).filter(Device.device_id == p.device_id).first() if p.device_id else None
|
||||
|
||||
|
||||
Reference in New Issue
Block a user