Strictly eliminate fake and stale vitals and enforce machine-to-MRN isolation
This commit is contained in:
@@ -122,39 +122,6 @@ async def sync_cloud_state():
|
|||||||
"patient_type": getattr(p, "patient_type", None)
|
"patient_type": getattr(p, "patient_type", None)
|
||||||
}
|
}
|
||||||
await sync_patient_assignment_to_cloud(p_data, p.device_id)
|
await sync_patient_assignment_to_cloud(p_data, p.device_id)
|
||||||
|
|
||||||
# 2. Backfill and forward latest complete non-null vitals to Render
|
|
||||||
patient_filters = or_(
|
|
||||||
VitalReading.patient_id == p.mrn,
|
|
||||||
VitalReading.patient_id == p.patient_id,
|
|
||||||
VitalReading.device_id == p.device_id
|
|
||||||
)
|
|
||||||
recent_r = db.query(VitalReading).filter(patient_filters).order_by(VitalReading.timestamp.desc()).first()
|
|
||||||
if recent_r:
|
|
||||||
spo2_r = db.query(VitalReading.spo2).filter(patient_filters, VitalReading.spo2 != None).order_by(VitalReading.timestamp.desc()).first()
|
|
||||||
hr_r = db.query(VitalReading.heart_rate).filter(patient_filters, VitalReading.heart_rate != None).order_by(VitalReading.timestamp.desc()).first()
|
|
||||||
bp_r = db.query(VitalReading.systolic_bp, VitalReading.diastolic_bp, VitalReading.map_bp).filter(patient_filters, VitalReading.systolic_bp != None).order_by(VitalReading.timestamp.desc()).first()
|
|
||||||
temp_r = db.query(VitalReading.temperature).filter(patient_filters, VitalReading.temperature != None).order_by(VitalReading.timestamp.desc()).first()
|
|
||||||
rr_r = db.query(VitalReading.respiratory_rate).filter(patient_filters, VitalReading.respiratory_rate != None).order_by(VitalReading.timestamp.desc()).first()
|
|
||||||
|
|
||||||
merged = NormalizedVitals(
|
|
||||||
device_id=p.device_id,
|
|
||||||
patient_id=p.mrn or p.patient_id,
|
|
||||||
patient_name=p.name,
|
|
||||||
patient_gender=p.gender,
|
|
||||||
patient_dob=p.dob,
|
|
||||||
bed_id=p.bed_id,
|
|
||||||
patient_type=getattr(p, "patient_type", None),
|
|
||||||
timestamp=recent_r.timestamp or datetime.now(timezone.utc),
|
|
||||||
heart_rate=hr_r[0] if hr_r else None,
|
|
||||||
spo2=spo2_r[0] if spo2_r else None,
|
|
||||||
systolic_bp=bp_r[0] if bp_r else None,
|
|
||||||
diastolic_bp=bp_r[1] if bp_r else None,
|
|
||||||
map_bp=bp_r[2] if bp_r else None,
|
|
||||||
temperature=temp_r[0] if temp_r else None,
|
|
||||||
respiratory_rate=rr_r[0] if rr_r else None
|
|
||||||
)
|
|
||||||
await forward_vitals_to_api(merged)
|
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
+6
-19
@@ -528,13 +528,16 @@ async def process_vitals(vitals):
|
|||||||
if val is not None:
|
if val is not None:
|
||||||
setattr(cached, field, val)
|
setattr(cached, field, val)
|
||||||
device_field_timestamps[device_id][field] = now
|
device_field_timestamps[device_id][field] = now
|
||||||
|
else:
|
||||||
|
setattr(cached, field, None)
|
||||||
|
device_field_timestamps[device_id].pop(field, None)
|
||||||
|
|
||||||
# Clean up stale fields:
|
# Clean up stale fields:
|
||||||
# Live vitals (HR, SpO2, Temp, RR) have a 60-second window because monitor cycles take ~24s
|
# Live vitals (HR, SpO2, Temp, RR) have a 15-second window
|
||||||
# NIBP (BP, MAP) has a 1-hour window because it is measured intermittently
|
# NIBP (BP, MAP) has a 15-minute window because it is measured intermittently
|
||||||
for field in list(device_field_timestamps[device_id].keys()):
|
for field in list(device_field_timestamps[device_id].keys()):
|
||||||
last_updated = device_field_timestamps[device_id][field]
|
last_updated = device_field_timestamps[device_id][field]
|
||||||
timeout = 3600.0 if field in ("systolic_bp", "diastolic_bp", "map_bp") else 60.0
|
timeout = 900.0 if field in ("systolic_bp", "diastolic_bp", "map_bp") else 15.0
|
||||||
if (now - last_updated).total_seconds() > timeout:
|
if (now - last_updated).total_seconds() > timeout:
|
||||||
setattr(cached, field, None)
|
setattr(cached, field, None)
|
||||||
device_field_timestamps[device_id].pop(field, None)
|
device_field_timestamps[device_id].pop(field, None)
|
||||||
@@ -671,22 +674,6 @@ async def _persist_and_forward_bg(vitals, should_persist):
|
|||||||
if not getattr(vitals, "patient_type", None) and getattr(patient, "patient_type", None):
|
if not getattr(vitals, "patient_type", None) and getattr(patient, "patient_type", None):
|
||||||
vitals.patient_type = patient.patient_type
|
vitals.patient_type = patient.patient_type
|
||||||
|
|
||||||
# Backfill non-null vitals from device cache so REST forwarders always get complete vitals
|
|
||||||
cached = device_cache.get(vitals.device_id)
|
|
||||||
if cached:
|
|
||||||
if vitals.heart_rate is None and cached.heart_rate is not None:
|
|
||||||
vitals.heart_rate = cached.heart_rate
|
|
||||||
if vitals.spo2 is None and cached.spo2 is not None:
|
|
||||||
vitals.spo2 = cached.spo2
|
|
||||||
if vitals.systolic_bp is None and cached.systolic_bp is not None:
|
|
||||||
vitals.systolic_bp = cached.systolic_bp
|
|
||||||
vitals.diastolic_bp = cached.diastolic_bp
|
|
||||||
vitals.map_bp = cached.map_bp
|
|
||||||
if vitals.respiratory_rate is None and cached.respiratory_rate is not None:
|
|
||||||
vitals.respiratory_rate = cached.respiratory_rate
|
|
||||||
if vitals.temperature is None and cached.temperature is not None:
|
|
||||||
vitals.temperature = cached.temperature
|
|
||||||
|
|
||||||
reading = VitalReading(
|
reading = VitalReading(
|
||||||
device_id=vitals.device_id,
|
device_id=vitals.device_id,
|
||||||
patient_id=vitals.patient_id or (patient.mrn if patient else "UNKNOWN"),
|
patient_id=vitals.patient_id or (patient.mrn if patient else "UNKNOWN"),
|
||||||
|
|||||||
@@ -122,39 +122,6 @@ async def sync_cloud_state():
|
|||||||
"patient_type": getattr(p, "patient_type", None)
|
"patient_type": getattr(p, "patient_type", None)
|
||||||
}
|
}
|
||||||
await sync_patient_assignment_to_cloud(p_data, p.device_id)
|
await sync_patient_assignment_to_cloud(p_data, p.device_id)
|
||||||
|
|
||||||
# 2. Backfill and forward latest complete non-null vitals to Render
|
|
||||||
patient_filters = or_(
|
|
||||||
VitalReading.patient_id == p.mrn,
|
|
||||||
VitalReading.patient_id == p.patient_id,
|
|
||||||
VitalReading.device_id == p.device_id
|
|
||||||
)
|
|
||||||
recent_r = db.query(VitalReading).filter(patient_filters).order_by(VitalReading.timestamp.desc()).first()
|
|
||||||
if recent_r:
|
|
||||||
spo2_r = db.query(VitalReading.spo2).filter(patient_filters, VitalReading.spo2 != None).order_by(VitalReading.timestamp.desc()).first()
|
|
||||||
hr_r = db.query(VitalReading.heart_rate).filter(patient_filters, VitalReading.heart_rate != None).order_by(VitalReading.timestamp.desc()).first()
|
|
||||||
bp_r = db.query(VitalReading.systolic_bp, VitalReading.diastolic_bp, VitalReading.map_bp).filter(patient_filters, VitalReading.systolic_bp != None).order_by(VitalReading.timestamp.desc()).first()
|
|
||||||
temp_r = db.query(VitalReading.temperature).filter(patient_filters, VitalReading.temperature != None).order_by(VitalReading.timestamp.desc()).first()
|
|
||||||
rr_r = db.query(VitalReading.respiratory_rate).filter(patient_filters, VitalReading.respiratory_rate != None).order_by(VitalReading.timestamp.desc()).first()
|
|
||||||
|
|
||||||
merged = NormalizedVitals(
|
|
||||||
device_id=p.device_id,
|
|
||||||
patient_id=p.mrn or p.patient_id,
|
|
||||||
patient_name=p.name,
|
|
||||||
patient_gender=p.gender,
|
|
||||||
patient_dob=p.dob,
|
|
||||||
bed_id=p.bed_id,
|
|
||||||
patient_type=getattr(p, "patient_type", None),
|
|
||||||
timestamp=recent_r.timestamp or datetime.now(timezone.utc),
|
|
||||||
heart_rate=hr_r[0] if hr_r else None,
|
|
||||||
spo2=spo2_r[0] if spo2_r else None,
|
|
||||||
systolic_bp=bp_r[0] if bp_r else None,
|
|
||||||
diastolic_bp=bp_r[1] if bp_r else None,
|
|
||||||
map_bp=bp_r[2] if bp_r else None,
|
|
||||||
temperature=temp_r[0] if temp_r else None,
|
|
||||||
respiratory_rate=rr_r[0] if rr_r else None
|
|
||||||
)
|
|
||||||
await forward_vitals_to_api(merged)
|
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -528,13 +528,16 @@ async def process_vitals(vitals):
|
|||||||
if val is not None:
|
if val is not None:
|
||||||
setattr(cached, field, val)
|
setattr(cached, field, val)
|
||||||
device_field_timestamps[device_id][field] = now
|
device_field_timestamps[device_id][field] = now
|
||||||
|
else:
|
||||||
|
setattr(cached, field, None)
|
||||||
|
device_field_timestamps[device_id].pop(field, None)
|
||||||
|
|
||||||
# Clean up stale fields:
|
# Clean up stale fields:
|
||||||
# Live vitals (HR, SpO2, Temp, RR) have a 60-second window because monitor cycles take ~24s
|
# Live vitals (HR, SpO2, Temp, RR) have a 15-second window
|
||||||
# NIBP (BP, MAP) has a 1-hour window because it is measured intermittently
|
# NIBP (BP, MAP) has a 15-minute window because it is measured intermittently
|
||||||
for field in list(device_field_timestamps[device_id].keys()):
|
for field in list(device_field_timestamps[device_id].keys()):
|
||||||
last_updated = device_field_timestamps[device_id][field]
|
last_updated = device_field_timestamps[device_id][field]
|
||||||
timeout = 3600.0 if field in ("systolic_bp", "diastolic_bp", "map_bp") else 60.0
|
timeout = 900.0 if field in ("systolic_bp", "diastolic_bp", "map_bp") else 15.0
|
||||||
if (now - last_updated).total_seconds() > timeout:
|
if (now - last_updated).total_seconds() > timeout:
|
||||||
setattr(cached, field, None)
|
setattr(cached, field, None)
|
||||||
device_field_timestamps[device_id].pop(field, None)
|
device_field_timestamps[device_id].pop(field, None)
|
||||||
@@ -671,22 +674,6 @@ async def _persist_and_forward_bg(vitals, should_persist):
|
|||||||
if not getattr(vitals, "patient_type", None) and getattr(patient, "patient_type", None):
|
if not getattr(vitals, "patient_type", None) and getattr(patient, "patient_type", None):
|
||||||
vitals.patient_type = patient.patient_type
|
vitals.patient_type = patient.patient_type
|
||||||
|
|
||||||
# Backfill non-null vitals from device cache so REST forwarders always get complete vitals
|
|
||||||
cached = device_cache.get(vitals.device_id)
|
|
||||||
if cached:
|
|
||||||
if vitals.heart_rate is None and cached.heart_rate is not None:
|
|
||||||
vitals.heart_rate = cached.heart_rate
|
|
||||||
if vitals.spo2 is None and cached.spo2 is not None:
|
|
||||||
vitals.spo2 = cached.spo2
|
|
||||||
if vitals.systolic_bp is None and cached.systolic_bp is not None:
|
|
||||||
vitals.systolic_bp = cached.systolic_bp
|
|
||||||
vitals.diastolic_bp = cached.diastolic_bp
|
|
||||||
vitals.map_bp = cached.map_bp
|
|
||||||
if vitals.respiratory_rate is None and cached.respiratory_rate is not None:
|
|
||||||
vitals.respiratory_rate = cached.respiratory_rate
|
|
||||||
if vitals.temperature is None and cached.temperature is not None:
|
|
||||||
vitals.temperature = cached.temperature
|
|
||||||
|
|
||||||
reading = VitalReading(
|
reading = VitalReading(
|
||||||
device_id=vitals.device_id,
|
device_id=vitals.device_id,
|
||||||
patient_id=vitals.patient_id or (patient.mrn if patient else "UNKNOWN"),
|
patient_id=vitals.patient_id or (patient.mrn if patient else "UNKNOWN"),
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from fastapi import APIRouter, Depends, HTTPException, WebSocket, WebSocketDisco
|
|||||||
from fastapi.responses import HTMLResponse
|
from fastapi.responses import HTMLResponse
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from sqlalchemy import or_
|
from sqlalchemy import or_
|
||||||
|
from datetime import datetime, timezone, timedelta
|
||||||
from database import get_db
|
from database import get_db
|
||||||
from models import Device, Patient, VitalReading, TransmissionLog
|
from models import Device, Patient, VitalReading, TransmissionLog
|
||||||
|
|
||||||
@@ -161,74 +162,100 @@ def get_latest_readings(
|
|||||||
def list_patients(db: Session = Depends(get_db)):
|
def list_patients(db: Session = Depends(get_db)):
|
||||||
"""
|
"""
|
||||||
List all patients with their assigned device and latest vitals readings.
|
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()
|
patients = db.query(Patient).filter(Patient.patient_id != "UNKNOWN", Patient.mrn != "UNKNOWN").all()
|
||||||
results = []
|
results = []
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
live_cutoff = now - timedelta(seconds=25)
|
||||||
|
nibp_cutoff = now - timedelta(minutes=15)
|
||||||
|
|
||||||
for p in patients:
|
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
|
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_(
|
patient_filters = or_(
|
||||||
VitalReading.patient_id == p.mrn,
|
VitalReading.patient_id == p.mrn,
|
||||||
VitalReading.patient_id == p.patient_id,
|
VitalReading.patient_id == p.patient_id,
|
||||||
(VitalReading.device_id == p.device_id) if p.device_id else False
|
(VitalReading.device_id == p.device_id) if p.device_id else False
|
||||||
)
|
)
|
||||||
if merged_vitals["spo2"] is None:
|
# Find readings within the live cutoff window (last 25 seconds)
|
||||||
r_spo2 = db.query(VitalReading.spo2).filter(patient_filters, VitalReading.spo2 != None).order_by(VitalReading.timestamp.desc()).first()
|
recent_readings = (
|
||||||
if r_spo2: merged_vitals["spo2"] = r_spo2[0]
|
db.query(VitalReading)
|
||||||
if merged_vitals["heart_rate"] is None:
|
.filter(patient_filters, VitalReading.timestamp >= live_cutoff)
|
||||||
r_hr = db.query(VitalReading.heart_rate).filter(patient_filters, VitalReading.heart_rate != None).order_by(VitalReading.timestamp.desc()).first()
|
.order_by(VitalReading.timestamp.desc())
|
||||||
if r_hr: merged_vitals["heart_rate"] = r_hr[0]
|
.limit(20)
|
||||||
if merged_vitals["systolic_bp"] is None:
|
.all()
|
||||||
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]
|
if recent_readings:
|
||||||
merged_vitals["diastolic_bp"] = r_bp[1]
|
merged_vitals = {
|
||||||
merged_vitals["map_bp"] = r_bp[2]
|
"heart_rate": None,
|
||||||
if merged_vitals["respiratory_rate"] is None:
|
"spo2": None,
|
||||||
r_rr = db.query(VitalReading.respiratory_rate).filter(patient_filters, VitalReading.respiratory_rate != None).order_by(VitalReading.timestamp.desc()).first()
|
"systolic_bp": None,
|
||||||
if r_rr: merged_vitals["respiratory_rate"] = r_rr[0]
|
"diastolic_bp": None,
|
||||||
if merged_vitals["temperature"] is None:
|
"map_bp": None,
|
||||||
r_temp = db.query(VitalReading.temperature).filter(patient_filters, VitalReading.temperature != None).order_by(VitalReading.timestamp.desc()).first()
|
"respiratory_rate": None,
|
||||||
if r_temp: merged_vitals["temperature"] = r_temp[0]
|
"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
|
device = db.query(Device).filter(Device.device_id == p.device_id).first() if p.device_id else None
|
||||||
|
|
||||||
|
|||||||
@@ -3107,11 +3107,26 @@
|
|||||||
const readings = await readResp.json();
|
const readings = await readResp.json();
|
||||||
if (readings && readings.length > 0) {
|
if (readings && readings.length > 0) {
|
||||||
const merged = {};
|
const merged = {};
|
||||||
|
const now = Date.now();
|
||||||
for (let i = readings.length - 1; i >= 0; i--) {
|
for (let i = readings.length - 1; i >= 0; i--) {
|
||||||
const r = readings[i];
|
const r = readings[i];
|
||||||
for (const k in r) {
|
const rAge = r.timestamp ? (now - new Date(r.timestamp).getTime()) : 9999999;
|
||||||
if (r[k] !== null && r[k] !== undefined) merged[k] = r[k];
|
if (rAge <= 25000) {
|
||||||
|
if (r.heart_rate != null) merged.heart_rate = r.heart_rate;
|
||||||
|
if (r.spo2 != null) merged.spo2 = r.spo2;
|
||||||
|
if (r.respiratory_rate != null) merged.respiratory_rate = r.respiratory_rate;
|
||||||
|
if (r.temperature != null) merged.temperature = r.temperature;
|
||||||
|
if (r.temperature2 != null) merged.temperature2 = r.temperature2;
|
||||||
|
if (r.temperature_diff != null) merged.temperature_diff = r.temperature_diff;
|
||||||
}
|
}
|
||||||
|
if (rAge <= 900000) {
|
||||||
|
if (r.systolic_bp != null) merged.systolic_bp = r.systolic_bp;
|
||||||
|
if (r.diastolic_bp != null) merged.diastolic_bp = r.diastolic_bp;
|
||||||
|
if (r.map_bp != null) merged.map_bp = r.map_bp;
|
||||||
|
}
|
||||||
|
if (r.patient_name) merged.patient_name = r.patient_name;
|
||||||
|
if (r.mrn || r.patient_id) merged.mrn = r.mrn || r.patient_id;
|
||||||
|
if (r.bed_id) merged.bed_id = r.bed_id;
|
||||||
}
|
}
|
||||||
applyVitalsToMachine(merged);
|
applyVitalsToMachine(merged);
|
||||||
return;
|
return;
|
||||||
@@ -3178,18 +3193,33 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Fallback: Fetch latest readings directly from DB (merge up to 10 recent readings so all active parameters display)
|
// 2. Fallback: Fetch latest readings directly from DB (only accept recent readings)
|
||||||
if (targetId) {
|
if (targetId) {
|
||||||
const readResp = await fetch(`${API_BASE}/latest-readings?mrn=${encodeURIComponent(targetId)}&limit=10`);
|
const readResp = await fetch(`${API_BASE}/latest-readings?mrn=${encodeURIComponent(targetId)}&limit=10`);
|
||||||
if (readResp.ok) {
|
if (readResp.ok) {
|
||||||
const readings = await readResp.json();
|
const readings = await readResp.json();
|
||||||
if (readings && readings.length > 0) {
|
if (readings && readings.length > 0) {
|
||||||
const merged = {};
|
const merged = {};
|
||||||
|
const now = Date.now();
|
||||||
for (let i = readings.length - 1; i >= 0; i--) {
|
for (let i = readings.length - 1; i >= 0; i--) {
|
||||||
const r = readings[i];
|
const r = readings[i];
|
||||||
for (const k in r) {
|
const rAge = r.timestamp ? (now - new Date(r.timestamp).getTime()) : 9999999;
|
||||||
if (r[k] !== null && r[k] !== undefined) merged[k] = r[k];
|
if (rAge <= 25000) {
|
||||||
|
if (r.heart_rate != null) merged.heart_rate = r.heart_rate;
|
||||||
|
if (r.spo2 != null) merged.spo2 = r.spo2;
|
||||||
|
if (r.respiratory_rate != null) merged.respiratory_rate = r.respiratory_rate;
|
||||||
|
if (r.temperature != null) merged.temperature = r.temperature;
|
||||||
|
if (r.temperature2 != null) merged.temperature2 = r.temperature2;
|
||||||
|
if (r.temperature_diff != null) merged.temperature_diff = r.temperature_diff;
|
||||||
}
|
}
|
||||||
|
if (rAge <= 900000) {
|
||||||
|
if (r.systolic_bp != null) merged.systolic_bp = r.systolic_bp;
|
||||||
|
if (r.diastolic_bp != null) merged.diastolic_bp = r.diastolic_bp;
|
||||||
|
if (r.map_bp != null) merged.map_bp = r.map_bp;
|
||||||
|
}
|
||||||
|
if (r.patient_name) merged.patient_name = r.patient_name;
|
||||||
|
if (r.mrn || r.patient_id) merged.mrn = r.mrn || r.patient_id;
|
||||||
|
if (r.bed_id) merged.bed_id = r.bed_id;
|
||||||
}
|
}
|
||||||
applyVitalsToMachine(merged);
|
applyVitalsToMachine(merged);
|
||||||
}
|
}
|
||||||
@@ -3204,46 +3234,48 @@
|
|||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
// Heart Rate
|
// Heart Rate
|
||||||
if (data.heart_rate !== undefined && data.heart_rate !== null) {
|
if ('heart_rate' in data) {
|
||||||
machVitals.heart_rate = (data.heart_rate > 0) ? Math.round(data.heart_rate) : null;
|
machVitals.heart_rate = (data.heart_rate !== null && data.heart_rate !== undefined && data.heart_rate > 0) ? Math.round(data.heart_rate) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// SpO2
|
// SpO2
|
||||||
if (data.spo2 !== undefined && data.spo2 !== null) {
|
if ('spo2' in data) {
|
||||||
machVitals.spo2 = (data.spo2 > 0) ? Math.round(data.spo2) : null;
|
machVitals.spo2 = (data.spo2 !== null && data.spo2 !== undefined && data.spo2 > 0) ? Math.round(data.spo2) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Respiration Rate
|
// Respiration Rate
|
||||||
if (data.respiratory_rate !== undefined && data.respiratory_rate !== null) {
|
if ('respiratory_rate' in data) {
|
||||||
machVitals.respiratory_rate = (data.respiratory_rate > 0) ? Math.round(data.respiratory_rate) : null;
|
machVitals.respiratory_rate = (data.respiratory_rate !== null && data.respiratory_rate !== undefined && data.respiratory_rate > 0) ? Math.round(data.respiratory_rate) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// NIBP
|
// NIBP
|
||||||
if (data.systolic_bp !== undefined && data.systolic_bp !== null) {
|
if ('systolic_bp' in data) {
|
||||||
machVitals.systolic_bp = (data.systolic_bp > 0) ? Math.round(data.systolic_bp) : null;
|
machVitals.systolic_bp = (data.systolic_bp !== null && data.systolic_bp !== undefined && data.systolic_bp > 0) ? Math.round(data.systolic_bp) : null;
|
||||||
}
|
}
|
||||||
if (data.diastolic_bp !== undefined && data.diastolic_bp !== null) {
|
if ('diastolic_bp' in data) {
|
||||||
machVitals.diastolic_bp = (data.diastolic_bp > 0) ? Math.round(data.diastolic_bp) : null;
|
machVitals.diastolic_bp = (data.diastolic_bp !== null && data.diastolic_bp !== undefined && data.diastolic_bp > 0) ? Math.round(data.diastolic_bp) : null;
|
||||||
}
|
}
|
||||||
if (data.map_bp !== undefined && data.map_bp !== null) {
|
if ('map_bp' in data) {
|
||||||
machVitals.map_bp = (data.map_bp > 0) ? Math.round(data.map_bp) : null;
|
machVitals.map_bp = (data.map_bp !== null && data.map_bp !== undefined && data.map_bp > 0) ? Math.round(data.map_bp) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temperature 1 (T1)
|
// Temperature 1 (T1)
|
||||||
if (data.temperature !== undefined && data.temperature !== null) {
|
if ('temperature' in data) {
|
||||||
machVitals.temperature = (data.temperature > 0) ? Number(data.temperature) : null;
|
machVitals.temperature = (data.temperature !== null && data.temperature !== undefined && data.temperature > 0) ? Number(data.temperature) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temperature 2 (T2)
|
// Temperature 2 (T2)
|
||||||
if (data.temperature2 !== undefined && data.temperature2 !== null) {
|
if ('temperature2' in data) {
|
||||||
machVitals.temperature2 = (data.temperature2 > 0) ? Number(data.temperature2) : null;
|
machVitals.temperature2 = (data.temperature2 !== null && data.temperature2 !== undefined && data.temperature2 > 0) ? Number(data.temperature2) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temperature Difference (TD)
|
// Temperature Difference (TD)
|
||||||
if (data.temperature_diff !== undefined && data.temperature_diff !== null) {
|
if ('temperature_diff' in data) {
|
||||||
machVitals.temperature_diff = Number(data.temperature_diff);
|
machVitals.temperature_diff = (data.temperature_diff !== null && data.temperature_diff !== undefined) ? Number(data.temperature_diff) : null;
|
||||||
} else if (machVitals.temperature != null && machVitals.temperature2 != null) {
|
} else if (machVitals.temperature != null && machVitals.temperature2 != null) {
|
||||||
machVitals.temperature_diff = Math.round(Math.abs(machVitals.temperature - machVitals.temperature2) * 10) / 10;
|
machVitals.temperature_diff = Math.round(Math.abs(machVitals.temperature - machVitals.temperature2) * 10) / 10;
|
||||||
|
} else {
|
||||||
|
machVitals.temperature_diff = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.patient_name && data.patient_name !== 'Unknown' && data.patient_name !== 'UNKNOWN') machVitals.patient_name = data.patient_name;
|
if (data.patient_name && data.patient_name !== 'Unknown' && data.patient_name !== 'UNKNOWN') machVitals.patient_name = data.patient_name;
|
||||||
@@ -3601,7 +3633,22 @@
|
|||||||
|
|
||||||
const readings = await response.json();
|
const readings = await response.json();
|
||||||
if (readings.length > 0) {
|
if (readings.length > 0) {
|
||||||
|
const now = Date.now();
|
||||||
readings.forEach(r => {
|
readings.forEach(r => {
|
||||||
|
const rAge = r.timestamp ? (now - new Date(r.timestamp).getTime()) : 9999999;
|
||||||
|
if (rAge > 25000) {
|
||||||
|
r.heart_rate = null;
|
||||||
|
r.spo2 = null;
|
||||||
|
r.respiratory_rate = null;
|
||||||
|
r.temperature = null;
|
||||||
|
r.temperature2 = null;
|
||||||
|
r.temperature_diff = null;
|
||||||
|
}
|
||||||
|
if (rAge > 900000) {
|
||||||
|
r.systolic_bp = null;
|
||||||
|
r.diastolic_bp = null;
|
||||||
|
r.map_bp = null;
|
||||||
|
}
|
||||||
if (r.id !== lastReadingId) {
|
if (r.id !== lastReadingId) {
|
||||||
lastReadingId = r.id;
|
lastReadingId = r.id;
|
||||||
readingCount++;
|
readingCount++;
|
||||||
@@ -3674,17 +3721,23 @@
|
|||||||
const readResp = await fetch(backfillUrl);
|
const readResp = await fetch(backfillUrl);
|
||||||
if (readResp.ok) {
|
if (readResp.ok) {
|
||||||
const readings = await readResp.json();
|
const readings = await readResp.json();
|
||||||
|
const now = Date.now();
|
||||||
readings.forEach(r => {
|
readings.forEach(r => {
|
||||||
const devKey = r.device_id;
|
const devKey = r.device_id;
|
||||||
if (devKey && connectedDevices[devKey]) {
|
if (devKey && connectedDevices[devKey]) {
|
||||||
const dev = connectedDevices[devKey];
|
const dev = connectedDevices[devKey];
|
||||||
if ((dev.heart_rate === undefined || dev.heart_rate === null) && r.heart_rate !== null && r.heart_rate !== undefined) dev.heart_rate = r.heart_rate;
|
const rAge = r.timestamp ? (now - new Date(r.timestamp).getTime()) : 9999999;
|
||||||
if ((dev.spo2 === undefined || dev.spo2 === null) && r.spo2 !== null && r.spo2 !== undefined) dev.spo2 = r.spo2;
|
if (rAge <= 25000) {
|
||||||
if ((dev.systolic_bp === undefined || dev.systolic_bp === null) && r.systolic_bp !== null && r.systolic_bp !== undefined) dev.systolic_bp = r.systolic_bp;
|
if ((dev.heart_rate === undefined || dev.heart_rate === null) && r.heart_rate != null) dev.heart_rate = r.heart_rate;
|
||||||
if ((dev.diastolic_bp === undefined || dev.diastolic_bp === null) && r.diastolic_bp !== null && r.diastolic_bp !== undefined) dev.diastolic_bp = r.diastolic_bp;
|
if ((dev.spo2 === undefined || dev.spo2 === null) && r.spo2 != null) dev.spo2 = r.spo2;
|
||||||
if ((dev.map_bp === undefined || dev.map_bp === null) && r.map_bp !== null && r.map_bp !== undefined) dev.map_bp = r.map_bp;
|
if ((dev.respiratory_rate === undefined || dev.respiratory_rate === null) && r.respiratory_rate != null) dev.respiratory_rate = r.respiratory_rate;
|
||||||
if ((dev.respiratory_rate === undefined || dev.respiratory_rate === null) && r.respiratory_rate !== null && r.respiratory_rate !== undefined) dev.respiratory_rate = r.respiratory_rate;
|
if ((dev.temperature === undefined || dev.temperature === null) && r.temperature != null) dev.temperature = r.temperature;
|
||||||
if ((dev.temperature === undefined || dev.temperature === null) && r.temperature !== null && r.temperature !== undefined) dev.temperature = r.temperature;
|
}
|
||||||
|
if (rAge <= 900000) {
|
||||||
|
if ((dev.systolic_bp === undefined || dev.systolic_bp === null) && r.systolic_bp != null) dev.systolic_bp = r.systolic_bp;
|
||||||
|
if ((dev.diastolic_bp === undefined || dev.diastolic_bp === null) && r.diastolic_bp != null) dev.diastolic_bp = r.diastolic_bp;
|
||||||
|
if ((dev.map_bp === undefined || dev.map_bp === null) && r.map_bp != null) dev.map_bp = r.map_bp;
|
||||||
|
}
|
||||||
if (!dev.lastSeen && r.timestamp) dev.lastSeen = new Date(r.timestamp);
|
if (!dev.lastSeen && r.timestamp) dev.lastSeen = new Date(r.timestamp);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -4083,13 +4136,13 @@
|
|||||||
connectedDevices[devId] = {
|
connectedDevices[devId] = {
|
||||||
...existing,
|
...existing,
|
||||||
...data,
|
...data,
|
||||||
heart_rate: retainVital('heart_rate', 60000),
|
heart_rate: retainVital('heart_rate', 15000),
|
||||||
spo2: retainVital('spo2', 60000),
|
spo2: retainVital('spo2', 15000),
|
||||||
temperature: retainVital('temperature', 60000),
|
temperature: retainVital('temperature', 20000),
|
||||||
respiratory_rate: retainVital('respiratory_rate', 60000),
|
respiratory_rate: retainVital('respiratory_rate', 15000),
|
||||||
systolic_bp: retainVital('systolic_bp', 3600000),
|
systolic_bp: retainVital('systolic_bp', 900000),
|
||||||
diastolic_bp: retainVital('diastolic_bp', 3600000),
|
diastolic_bp: retainVital('diastolic_bp', 900000),
|
||||||
map_bp: retainVital('map_bp', 3600000),
|
map_bp: retainVital('map_bp', 900000),
|
||||||
patient_id: (data.mrn && data.mrn !== 'UNKNOWN') ? data.mrn : (existing.patient_id || 'UNKNOWN'),
|
patient_id: (data.mrn && data.mrn !== 'UNKNOWN') ? data.mrn : (existing.patient_id || 'UNKNOWN'),
|
||||||
mrn: (data.mrn && data.mrn !== 'UNKNOWN') ? data.mrn : (existing.mrn || 'UNKNOWN'),
|
mrn: (data.mrn && data.mrn !== 'UNKNOWN') ? data.mrn : (existing.mrn || 'UNKNOWN'),
|
||||||
patient_name: (data.patient_name && data.patient_name !== 'Unknown') ? data.patient_name : (existing.patient_name || null),
|
patient_name: (data.patient_name && data.patient_name !== 'Unknown') ? data.patient_name : (existing.patient_name || null),
|
||||||
|
|||||||
+84
-57
@@ -11,6 +11,7 @@ from fastapi import APIRouter, Depends, HTTPException, WebSocket, WebSocketDisco
|
|||||||
from fastapi.responses import HTMLResponse
|
from fastapi.responses import HTMLResponse
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from sqlalchemy import or_
|
from sqlalchemy import or_
|
||||||
|
from datetime import datetime, timezone, timedelta
|
||||||
from database import get_db
|
from database import get_db
|
||||||
from models import Device, Patient, VitalReading, TransmissionLog
|
from models import Device, Patient, VitalReading, TransmissionLog
|
||||||
|
|
||||||
@@ -161,74 +162,100 @@ def get_latest_readings(
|
|||||||
def list_patients(db: Session = Depends(get_db)):
|
def list_patients(db: Session = Depends(get_db)):
|
||||||
"""
|
"""
|
||||||
List all patients with their assigned device and latest vitals readings.
|
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()
|
patients = db.query(Patient).filter(Patient.patient_id != "UNKNOWN", Patient.mrn != "UNKNOWN").all()
|
||||||
results = []
|
results = []
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
live_cutoff = now - timedelta(seconds=25)
|
||||||
|
nibp_cutoff = now - timedelta(minutes=15)
|
||||||
|
|
||||||
for p in patients:
|
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
|
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_(
|
patient_filters = or_(
|
||||||
VitalReading.patient_id == p.mrn,
|
VitalReading.patient_id == p.mrn,
|
||||||
VitalReading.patient_id == p.patient_id,
|
VitalReading.patient_id == p.patient_id,
|
||||||
(VitalReading.device_id == p.device_id) if p.device_id else False
|
(VitalReading.device_id == p.device_id) if p.device_id else False
|
||||||
)
|
)
|
||||||
if merged_vitals["spo2"] is None:
|
# Find readings within the live cutoff window (last 25 seconds)
|
||||||
r_spo2 = db.query(VitalReading.spo2).filter(patient_filters, VitalReading.spo2 != None).order_by(VitalReading.timestamp.desc()).first()
|
recent_readings = (
|
||||||
if r_spo2: merged_vitals["spo2"] = r_spo2[0]
|
db.query(VitalReading)
|
||||||
if merged_vitals["heart_rate"] is None:
|
.filter(patient_filters, VitalReading.timestamp >= live_cutoff)
|
||||||
r_hr = db.query(VitalReading.heart_rate).filter(patient_filters, VitalReading.heart_rate != None).order_by(VitalReading.timestamp.desc()).first()
|
.order_by(VitalReading.timestamp.desc())
|
||||||
if r_hr: merged_vitals["heart_rate"] = r_hr[0]
|
.limit(20)
|
||||||
if merged_vitals["systolic_bp"] is None:
|
.all()
|
||||||
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]
|
if recent_readings:
|
||||||
merged_vitals["diastolic_bp"] = r_bp[1]
|
merged_vitals = {
|
||||||
merged_vitals["map_bp"] = r_bp[2]
|
"heart_rate": None,
|
||||||
if merged_vitals["respiratory_rate"] is None:
|
"spo2": None,
|
||||||
r_rr = db.query(VitalReading.respiratory_rate).filter(patient_filters, VitalReading.respiratory_rate != None).order_by(VitalReading.timestamp.desc()).first()
|
"systolic_bp": None,
|
||||||
if r_rr: merged_vitals["respiratory_rate"] = r_rr[0]
|
"diastolic_bp": None,
|
||||||
if merged_vitals["temperature"] is None:
|
"map_bp": None,
|
||||||
r_temp = db.query(VitalReading.temperature).filter(patient_filters, VitalReading.temperature != None).order_by(VitalReading.timestamp.desc()).first()
|
"respiratory_rate": None,
|
||||||
if r_temp: merged_vitals["temperature"] = r_temp[0]
|
"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
|
device = db.query(Device).filter(Device.device_id == p.device_id).first() if p.device_id else None
|
||||||
|
|
||||||
|
|||||||
+90
-37
@@ -3107,11 +3107,26 @@
|
|||||||
const readings = await readResp.json();
|
const readings = await readResp.json();
|
||||||
if (readings && readings.length > 0) {
|
if (readings && readings.length > 0) {
|
||||||
const merged = {};
|
const merged = {};
|
||||||
|
const now = Date.now();
|
||||||
for (let i = readings.length - 1; i >= 0; i--) {
|
for (let i = readings.length - 1; i >= 0; i--) {
|
||||||
const r = readings[i];
|
const r = readings[i];
|
||||||
for (const k in r) {
|
const rAge = r.timestamp ? (now - new Date(r.timestamp).getTime()) : 9999999;
|
||||||
if (r[k] !== null && r[k] !== undefined) merged[k] = r[k];
|
if (rAge <= 25000) {
|
||||||
|
if (r.heart_rate != null) merged.heart_rate = r.heart_rate;
|
||||||
|
if (r.spo2 != null) merged.spo2 = r.spo2;
|
||||||
|
if (r.respiratory_rate != null) merged.respiratory_rate = r.respiratory_rate;
|
||||||
|
if (r.temperature != null) merged.temperature = r.temperature;
|
||||||
|
if (r.temperature2 != null) merged.temperature2 = r.temperature2;
|
||||||
|
if (r.temperature_diff != null) merged.temperature_diff = r.temperature_diff;
|
||||||
}
|
}
|
||||||
|
if (rAge <= 900000) {
|
||||||
|
if (r.systolic_bp != null) merged.systolic_bp = r.systolic_bp;
|
||||||
|
if (r.diastolic_bp != null) merged.diastolic_bp = r.diastolic_bp;
|
||||||
|
if (r.map_bp != null) merged.map_bp = r.map_bp;
|
||||||
|
}
|
||||||
|
if (r.patient_name) merged.patient_name = r.patient_name;
|
||||||
|
if (r.mrn || r.patient_id) merged.mrn = r.mrn || r.patient_id;
|
||||||
|
if (r.bed_id) merged.bed_id = r.bed_id;
|
||||||
}
|
}
|
||||||
applyVitalsToMachine(merged);
|
applyVitalsToMachine(merged);
|
||||||
return;
|
return;
|
||||||
@@ -3178,18 +3193,33 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Fallback: Fetch latest readings directly from DB (merge up to 10 recent readings so all active parameters display)
|
// 2. Fallback: Fetch latest readings directly from DB (only accept recent readings)
|
||||||
if (targetId) {
|
if (targetId) {
|
||||||
const readResp = await fetch(`${API_BASE}/latest-readings?mrn=${encodeURIComponent(targetId)}&limit=10`);
|
const readResp = await fetch(`${API_BASE}/latest-readings?mrn=${encodeURIComponent(targetId)}&limit=10`);
|
||||||
if (readResp.ok) {
|
if (readResp.ok) {
|
||||||
const readings = await readResp.json();
|
const readings = await readResp.json();
|
||||||
if (readings && readings.length > 0) {
|
if (readings && readings.length > 0) {
|
||||||
const merged = {};
|
const merged = {};
|
||||||
|
const now = Date.now();
|
||||||
for (let i = readings.length - 1; i >= 0; i--) {
|
for (let i = readings.length - 1; i >= 0; i--) {
|
||||||
const r = readings[i];
|
const r = readings[i];
|
||||||
for (const k in r) {
|
const rAge = r.timestamp ? (now - new Date(r.timestamp).getTime()) : 9999999;
|
||||||
if (r[k] !== null && r[k] !== undefined) merged[k] = r[k];
|
if (rAge <= 25000) {
|
||||||
|
if (r.heart_rate != null) merged.heart_rate = r.heart_rate;
|
||||||
|
if (r.spo2 != null) merged.spo2 = r.spo2;
|
||||||
|
if (r.respiratory_rate != null) merged.respiratory_rate = r.respiratory_rate;
|
||||||
|
if (r.temperature != null) merged.temperature = r.temperature;
|
||||||
|
if (r.temperature2 != null) merged.temperature2 = r.temperature2;
|
||||||
|
if (r.temperature_diff != null) merged.temperature_diff = r.temperature_diff;
|
||||||
}
|
}
|
||||||
|
if (rAge <= 900000) {
|
||||||
|
if (r.systolic_bp != null) merged.systolic_bp = r.systolic_bp;
|
||||||
|
if (r.diastolic_bp != null) merged.diastolic_bp = r.diastolic_bp;
|
||||||
|
if (r.map_bp != null) merged.map_bp = r.map_bp;
|
||||||
|
}
|
||||||
|
if (r.patient_name) merged.patient_name = r.patient_name;
|
||||||
|
if (r.mrn || r.patient_id) merged.mrn = r.mrn || r.patient_id;
|
||||||
|
if (r.bed_id) merged.bed_id = r.bed_id;
|
||||||
}
|
}
|
||||||
applyVitalsToMachine(merged);
|
applyVitalsToMachine(merged);
|
||||||
}
|
}
|
||||||
@@ -3204,46 +3234,48 @@
|
|||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
// Heart Rate
|
// Heart Rate
|
||||||
if (data.heart_rate !== undefined && data.heart_rate !== null) {
|
if ('heart_rate' in data) {
|
||||||
machVitals.heart_rate = (data.heart_rate > 0) ? Math.round(data.heart_rate) : null;
|
machVitals.heart_rate = (data.heart_rate !== null && data.heart_rate !== undefined && data.heart_rate > 0) ? Math.round(data.heart_rate) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// SpO2
|
// SpO2
|
||||||
if (data.spo2 !== undefined && data.spo2 !== null) {
|
if ('spo2' in data) {
|
||||||
machVitals.spo2 = (data.spo2 > 0) ? Math.round(data.spo2) : null;
|
machVitals.spo2 = (data.spo2 !== null && data.spo2 !== undefined && data.spo2 > 0) ? Math.round(data.spo2) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Respiration Rate
|
// Respiration Rate
|
||||||
if (data.respiratory_rate !== undefined && data.respiratory_rate !== null) {
|
if ('respiratory_rate' in data) {
|
||||||
machVitals.respiratory_rate = (data.respiratory_rate > 0) ? Math.round(data.respiratory_rate) : null;
|
machVitals.respiratory_rate = (data.respiratory_rate !== null && data.respiratory_rate !== undefined && data.respiratory_rate > 0) ? Math.round(data.respiratory_rate) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// NIBP
|
// NIBP
|
||||||
if (data.systolic_bp !== undefined && data.systolic_bp !== null) {
|
if ('systolic_bp' in data) {
|
||||||
machVitals.systolic_bp = (data.systolic_bp > 0) ? Math.round(data.systolic_bp) : null;
|
machVitals.systolic_bp = (data.systolic_bp !== null && data.systolic_bp !== undefined && data.systolic_bp > 0) ? Math.round(data.systolic_bp) : null;
|
||||||
}
|
}
|
||||||
if (data.diastolic_bp !== undefined && data.diastolic_bp !== null) {
|
if ('diastolic_bp' in data) {
|
||||||
machVitals.diastolic_bp = (data.diastolic_bp > 0) ? Math.round(data.diastolic_bp) : null;
|
machVitals.diastolic_bp = (data.diastolic_bp !== null && data.diastolic_bp !== undefined && data.diastolic_bp > 0) ? Math.round(data.diastolic_bp) : null;
|
||||||
}
|
}
|
||||||
if (data.map_bp !== undefined && data.map_bp !== null) {
|
if ('map_bp' in data) {
|
||||||
machVitals.map_bp = (data.map_bp > 0) ? Math.round(data.map_bp) : null;
|
machVitals.map_bp = (data.map_bp !== null && data.map_bp !== undefined && data.map_bp > 0) ? Math.round(data.map_bp) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temperature 1 (T1)
|
// Temperature 1 (T1)
|
||||||
if (data.temperature !== undefined && data.temperature !== null) {
|
if ('temperature' in data) {
|
||||||
machVitals.temperature = (data.temperature > 0) ? Number(data.temperature) : null;
|
machVitals.temperature = (data.temperature !== null && data.temperature !== undefined && data.temperature > 0) ? Number(data.temperature) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temperature 2 (T2)
|
// Temperature 2 (T2)
|
||||||
if (data.temperature2 !== undefined && data.temperature2 !== null) {
|
if ('temperature2' in data) {
|
||||||
machVitals.temperature2 = (data.temperature2 > 0) ? Number(data.temperature2) : null;
|
machVitals.temperature2 = (data.temperature2 !== null && data.temperature2 !== undefined && data.temperature2 > 0) ? Number(data.temperature2) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temperature Difference (TD)
|
// Temperature Difference (TD)
|
||||||
if (data.temperature_diff !== undefined && data.temperature_diff !== null) {
|
if ('temperature_diff' in data) {
|
||||||
machVitals.temperature_diff = Number(data.temperature_diff);
|
machVitals.temperature_diff = (data.temperature_diff !== null && data.temperature_diff !== undefined) ? Number(data.temperature_diff) : null;
|
||||||
} else if (machVitals.temperature != null && machVitals.temperature2 != null) {
|
} else if (machVitals.temperature != null && machVitals.temperature2 != null) {
|
||||||
machVitals.temperature_diff = Math.round(Math.abs(machVitals.temperature - machVitals.temperature2) * 10) / 10;
|
machVitals.temperature_diff = Math.round(Math.abs(machVitals.temperature - machVitals.temperature2) * 10) / 10;
|
||||||
|
} else {
|
||||||
|
machVitals.temperature_diff = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.patient_name && data.patient_name !== 'Unknown' && data.patient_name !== 'UNKNOWN') machVitals.patient_name = data.patient_name;
|
if (data.patient_name && data.patient_name !== 'Unknown' && data.patient_name !== 'UNKNOWN') machVitals.patient_name = data.patient_name;
|
||||||
@@ -3601,7 +3633,22 @@
|
|||||||
|
|
||||||
const readings = await response.json();
|
const readings = await response.json();
|
||||||
if (readings.length > 0) {
|
if (readings.length > 0) {
|
||||||
|
const now = Date.now();
|
||||||
readings.forEach(r => {
|
readings.forEach(r => {
|
||||||
|
const rAge = r.timestamp ? (now - new Date(r.timestamp).getTime()) : 9999999;
|
||||||
|
if (rAge > 25000) {
|
||||||
|
r.heart_rate = null;
|
||||||
|
r.spo2 = null;
|
||||||
|
r.respiratory_rate = null;
|
||||||
|
r.temperature = null;
|
||||||
|
r.temperature2 = null;
|
||||||
|
r.temperature_diff = null;
|
||||||
|
}
|
||||||
|
if (rAge > 900000) {
|
||||||
|
r.systolic_bp = null;
|
||||||
|
r.diastolic_bp = null;
|
||||||
|
r.map_bp = null;
|
||||||
|
}
|
||||||
if (r.id !== lastReadingId) {
|
if (r.id !== lastReadingId) {
|
||||||
lastReadingId = r.id;
|
lastReadingId = r.id;
|
||||||
readingCount++;
|
readingCount++;
|
||||||
@@ -3674,17 +3721,23 @@
|
|||||||
const readResp = await fetch(backfillUrl);
|
const readResp = await fetch(backfillUrl);
|
||||||
if (readResp.ok) {
|
if (readResp.ok) {
|
||||||
const readings = await readResp.json();
|
const readings = await readResp.json();
|
||||||
|
const now = Date.now();
|
||||||
readings.forEach(r => {
|
readings.forEach(r => {
|
||||||
const devKey = r.device_id;
|
const devKey = r.device_id;
|
||||||
if (devKey && connectedDevices[devKey]) {
|
if (devKey && connectedDevices[devKey]) {
|
||||||
const dev = connectedDevices[devKey];
|
const dev = connectedDevices[devKey];
|
||||||
if ((dev.heart_rate === undefined || dev.heart_rate === null) && r.heart_rate !== null && r.heart_rate !== undefined) dev.heart_rate = r.heart_rate;
|
const rAge = r.timestamp ? (now - new Date(r.timestamp).getTime()) : 9999999;
|
||||||
if ((dev.spo2 === undefined || dev.spo2 === null) && r.spo2 !== null && r.spo2 !== undefined) dev.spo2 = r.spo2;
|
if (rAge <= 25000) {
|
||||||
if ((dev.systolic_bp === undefined || dev.systolic_bp === null) && r.systolic_bp !== null && r.systolic_bp !== undefined) dev.systolic_bp = r.systolic_bp;
|
if ((dev.heart_rate === undefined || dev.heart_rate === null) && r.heart_rate != null) dev.heart_rate = r.heart_rate;
|
||||||
if ((dev.diastolic_bp === undefined || dev.diastolic_bp === null) && r.diastolic_bp !== null && r.diastolic_bp !== undefined) dev.diastolic_bp = r.diastolic_bp;
|
if ((dev.spo2 === undefined || dev.spo2 === null) && r.spo2 != null) dev.spo2 = r.spo2;
|
||||||
if ((dev.map_bp === undefined || dev.map_bp === null) && r.map_bp !== null && r.map_bp !== undefined) dev.map_bp = r.map_bp;
|
if ((dev.respiratory_rate === undefined || dev.respiratory_rate === null) && r.respiratory_rate != null) dev.respiratory_rate = r.respiratory_rate;
|
||||||
if ((dev.respiratory_rate === undefined || dev.respiratory_rate === null) && r.respiratory_rate !== null && r.respiratory_rate !== undefined) dev.respiratory_rate = r.respiratory_rate;
|
if ((dev.temperature === undefined || dev.temperature === null) && r.temperature != null) dev.temperature = r.temperature;
|
||||||
if ((dev.temperature === undefined || dev.temperature === null) && r.temperature !== null && r.temperature !== undefined) dev.temperature = r.temperature;
|
}
|
||||||
|
if (rAge <= 900000) {
|
||||||
|
if ((dev.systolic_bp === undefined || dev.systolic_bp === null) && r.systolic_bp != null) dev.systolic_bp = r.systolic_bp;
|
||||||
|
if ((dev.diastolic_bp === undefined || dev.diastolic_bp === null) && r.diastolic_bp != null) dev.diastolic_bp = r.diastolic_bp;
|
||||||
|
if ((dev.map_bp === undefined || dev.map_bp === null) && r.map_bp != null) dev.map_bp = r.map_bp;
|
||||||
|
}
|
||||||
if (!dev.lastSeen && r.timestamp) dev.lastSeen = new Date(r.timestamp);
|
if (!dev.lastSeen && r.timestamp) dev.lastSeen = new Date(r.timestamp);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -4083,13 +4136,13 @@
|
|||||||
connectedDevices[devId] = {
|
connectedDevices[devId] = {
|
||||||
...existing,
|
...existing,
|
||||||
...data,
|
...data,
|
||||||
heart_rate: retainVital('heart_rate', 60000),
|
heart_rate: retainVital('heart_rate', 15000),
|
||||||
spo2: retainVital('spo2', 60000),
|
spo2: retainVital('spo2', 15000),
|
||||||
temperature: retainVital('temperature', 60000),
|
temperature: retainVital('temperature', 20000),
|
||||||
respiratory_rate: retainVital('respiratory_rate', 60000),
|
respiratory_rate: retainVital('respiratory_rate', 15000),
|
||||||
systolic_bp: retainVital('systolic_bp', 3600000),
|
systolic_bp: retainVital('systolic_bp', 900000),
|
||||||
diastolic_bp: retainVital('diastolic_bp', 3600000),
|
diastolic_bp: retainVital('diastolic_bp', 900000),
|
||||||
map_bp: retainVital('map_bp', 3600000),
|
map_bp: retainVital('map_bp', 900000),
|
||||||
patient_id: (data.mrn && data.mrn !== 'UNKNOWN') ? data.mrn : (existing.patient_id || 'UNKNOWN'),
|
patient_id: (data.mrn && data.mrn !== 'UNKNOWN') ? data.mrn : (existing.patient_id || 'UNKNOWN'),
|
||||||
mrn: (data.mrn && data.mrn !== 'UNKNOWN') ? data.mrn : (existing.mrn || 'UNKNOWN'),
|
mrn: (data.mrn && data.mrn !== 'UNKNOWN') ? data.mrn : (existing.mrn || 'UNKNOWN'),
|
||||||
patient_name: (data.patient_name && data.patient_name !== 'Unknown') ? data.patient_name : (existing.patient_name || null),
|
patient_name: (data.patient_name && data.patient_name !== 'Unknown') ? data.patient_name : (existing.patient_name || null),
|
||||||
|
|||||||
Reference in New Issue
Block a user