From 2342346d3966fe911a16d1b0a1595e6163e4d690 Mon Sep 17 00:00:00 2001 From: Arun Date: Thu, 17 Sep 2026 19:36:21 +0530 Subject: [PATCH] Strictly eliminate fake and stale vitals and enforce machine-to-MRN isolation --- background_tasks.py | 33 ------ contec_server.py | 25 +--- fiveparaminte-main/background_tasks.py | 33 ------ fiveparaminte-main/contec_server.py | 25 +--- fiveparaminte-main/routers/dashboard.py | 141 ++++++++++++++--------- fiveparaminte-main/static/dashboard.html | 127 ++++++++++++++------ routers/dashboard.py | 141 ++++++++++++++--------- static/dashboard.html | 127 ++++++++++++++------ 8 files changed, 360 insertions(+), 292 deletions(-) diff --git a/background_tasks.py b/background_tasks.py index 837c648..71bb6c9 100644 --- a/background_tasks.py +++ b/background_tasks.py @@ -122,39 +122,6 @@ async def sync_cloud_state(): "patient_type": getattr(p, "patient_type", None) } 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: db.close() except Exception as e: diff --git a/contec_server.py b/contec_server.py index 31e170d..68ac479 100644 --- a/contec_server.py +++ b/contec_server.py @@ -528,13 +528,16 @@ async def process_vitals(vitals): if val is not None: setattr(cached, field, val) device_field_timestamps[device_id][field] = now + else: + setattr(cached, field, None) + device_field_timestamps[device_id].pop(field, None) # Clean up stale fields: - # Live vitals (HR, SpO2, Temp, RR) have a 60-second window because monitor cycles take ~24s - # NIBP (BP, MAP) has a 1-hour window because it is measured intermittently + # Live vitals (HR, SpO2, Temp, RR) have a 15-second window + # NIBP (BP, MAP) has a 15-minute window because it is measured intermittently for field in list(device_field_timestamps[device_id].keys()): 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: setattr(cached, 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): 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( device_id=vitals.device_id, patient_id=vitals.patient_id or (patient.mrn if patient else "UNKNOWN"), diff --git a/fiveparaminte-main/background_tasks.py b/fiveparaminte-main/background_tasks.py index 837c648..71bb6c9 100644 --- a/fiveparaminte-main/background_tasks.py +++ b/fiveparaminte-main/background_tasks.py @@ -122,39 +122,6 @@ async def sync_cloud_state(): "patient_type": getattr(p, "patient_type", None) } 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: db.close() except Exception as e: diff --git a/fiveparaminte-main/contec_server.py b/fiveparaminte-main/contec_server.py index 31e170d..68ac479 100644 --- a/fiveparaminte-main/contec_server.py +++ b/fiveparaminte-main/contec_server.py @@ -528,13 +528,16 @@ async def process_vitals(vitals): if val is not None: setattr(cached, field, val) device_field_timestamps[device_id][field] = now + else: + setattr(cached, field, None) + device_field_timestamps[device_id].pop(field, None) # Clean up stale fields: - # Live vitals (HR, SpO2, Temp, RR) have a 60-second window because monitor cycles take ~24s - # NIBP (BP, MAP) has a 1-hour window because it is measured intermittently + # Live vitals (HR, SpO2, Temp, RR) have a 15-second window + # NIBP (BP, MAP) has a 15-minute window because it is measured intermittently for field in list(device_field_timestamps[device_id].keys()): 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: setattr(cached, 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): 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( device_id=vitals.device_id, patient_id=vitals.patient_id or (patient.mrn if patient else "UNKNOWN"), diff --git a/fiveparaminte-main/routers/dashboard.py b/fiveparaminte-main/routers/dashboard.py index e9bd0e8..2507e26 100644 --- a/fiveparaminte-main/routers/dashboard.py +++ b/fiveparaminte-main/routers/dashboard.py @@ -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 diff --git a/fiveparaminte-main/static/dashboard.html b/fiveparaminte-main/static/dashboard.html index ff53a1f..d06a6d6 100644 --- a/fiveparaminte-main/static/dashboard.html +++ b/fiveparaminte-main/static/dashboard.html @@ -3107,11 +3107,26 @@ const readings = await readResp.json(); if (readings && readings.length > 0) { const merged = {}; + const now = Date.now(); for (let i = readings.length - 1; i >= 0; i--) { const r = readings[i]; - for (const k in r) { - if (r[k] !== null && r[k] !== undefined) merged[k] = r[k]; + const rAge = r.timestamp ? (now - new Date(r.timestamp).getTime()) : 9999999; + 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); 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) { const readResp = await fetch(`${API_BASE}/latest-readings?mrn=${encodeURIComponent(targetId)}&limit=10`); if (readResp.ok) { const readings = await readResp.json(); if (readings && readings.length > 0) { const merged = {}; + const now = Date.now(); for (let i = readings.length - 1; i >= 0; i--) { const r = readings[i]; - for (const k in r) { - if (r[k] !== null && r[k] !== undefined) merged[k] = r[k]; + const rAge = r.timestamp ? (now - new Date(r.timestamp).getTime()) : 9999999; + 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); } @@ -3204,46 +3234,48 @@ if (!data) return; // Heart Rate - if (data.heart_rate !== undefined && data.heart_rate !== null) { - machVitals.heart_rate = (data.heart_rate > 0) ? Math.round(data.heart_rate) : null; + if ('heart_rate' in data) { + machVitals.heart_rate = (data.heart_rate !== null && data.heart_rate !== undefined && data.heart_rate > 0) ? Math.round(data.heart_rate) : null; } // SpO2 - if (data.spo2 !== undefined && data.spo2 !== null) { - machVitals.spo2 = (data.spo2 > 0) ? Math.round(data.spo2) : null; + if ('spo2' in data) { + machVitals.spo2 = (data.spo2 !== null && data.spo2 !== undefined && data.spo2 > 0) ? Math.round(data.spo2) : null; } // Respiration Rate - if (data.respiratory_rate !== undefined && data.respiratory_rate !== null) { - machVitals.respiratory_rate = (data.respiratory_rate > 0) ? Math.round(data.respiratory_rate) : null; + if ('respiratory_rate' in data) { + machVitals.respiratory_rate = (data.respiratory_rate !== null && data.respiratory_rate !== undefined && data.respiratory_rate > 0) ? Math.round(data.respiratory_rate) : null; } // NIBP - if (data.systolic_bp !== undefined && data.systolic_bp !== null) { - machVitals.systolic_bp = (data.systolic_bp > 0) ? Math.round(data.systolic_bp) : null; + if ('systolic_bp' in data) { + 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) { - machVitals.diastolic_bp = (data.diastolic_bp > 0) ? Math.round(data.diastolic_bp) : null; + if ('diastolic_bp' in data) { + 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) { - machVitals.map_bp = (data.map_bp > 0) ? Math.round(data.map_bp) : null; + if ('map_bp' in data) { + machVitals.map_bp = (data.map_bp !== null && data.map_bp !== undefined && data.map_bp > 0) ? Math.round(data.map_bp) : null; } // Temperature 1 (T1) - if (data.temperature !== undefined && data.temperature !== null) { - machVitals.temperature = (data.temperature > 0) ? Number(data.temperature) : null; + if ('temperature' in data) { + machVitals.temperature = (data.temperature !== null && data.temperature !== undefined && data.temperature > 0) ? Number(data.temperature) : null; } // Temperature 2 (T2) - if (data.temperature2 !== undefined && data.temperature2 !== null) { - machVitals.temperature2 = (data.temperature2 > 0) ? Number(data.temperature2) : null; + if ('temperature2' in data) { + machVitals.temperature2 = (data.temperature2 !== null && data.temperature2 !== undefined && data.temperature2 > 0) ? Number(data.temperature2) : null; } // Temperature Difference (TD) - if (data.temperature_diff !== undefined && data.temperature_diff !== null) { - machVitals.temperature_diff = Number(data.temperature_diff); + if ('temperature_diff' in data) { + machVitals.temperature_diff = (data.temperature_diff !== null && data.temperature_diff !== undefined) ? Number(data.temperature_diff) : null; } else if (machVitals.temperature != null && machVitals.temperature2 != null) { 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; @@ -3601,7 +3633,22 @@ const readings = await response.json(); if (readings.length > 0) { + const now = Date.now(); 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) { lastReadingId = r.id; readingCount++; @@ -3674,17 +3721,23 @@ const readResp = await fetch(backfillUrl); if (readResp.ok) { const readings = await readResp.json(); + const now = Date.now(); readings.forEach(r => { const devKey = r.device_id; if (devKey && 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; - if ((dev.spo2 === undefined || dev.spo2 === null) && r.spo2 !== null && r.spo2 !== undefined) dev.spo2 = r.spo2; - 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.diastolic_bp === undefined || dev.diastolic_bp === null) && r.diastolic_bp !== null && r.diastolic_bp !== undefined) dev.diastolic_bp = r.diastolic_bp; - 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 && r.respiratory_rate !== undefined) dev.respiratory_rate = r.respiratory_rate; - if ((dev.temperature === undefined || dev.temperature === null) && r.temperature !== null && r.temperature !== undefined) dev.temperature = r.temperature; + const rAge = r.timestamp ? (now - new Date(r.timestamp).getTime()) : 9999999; + if (rAge <= 25000) { + if ((dev.heart_rate === undefined || dev.heart_rate === null) && r.heart_rate != null) dev.heart_rate = r.heart_rate; + if ((dev.spo2 === undefined || dev.spo2 === null) && r.spo2 != null) dev.spo2 = r.spo2; + if ((dev.respiratory_rate === undefined || dev.respiratory_rate === null) && r.respiratory_rate != null) dev.respiratory_rate = r.respiratory_rate; + if ((dev.temperature === undefined || dev.temperature === null) && r.temperature != null) 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); } }); @@ -4083,13 +4136,13 @@ connectedDevices[devId] = { ...existing, ...data, - heart_rate: retainVital('heart_rate', 60000), - spo2: retainVital('spo2', 60000), - temperature: retainVital('temperature', 60000), - respiratory_rate: retainVital('respiratory_rate', 60000), - systolic_bp: retainVital('systolic_bp', 3600000), - diastolic_bp: retainVital('diastolic_bp', 3600000), - map_bp: retainVital('map_bp', 3600000), + heart_rate: retainVital('heart_rate', 15000), + spo2: retainVital('spo2', 15000), + temperature: retainVital('temperature', 20000), + respiratory_rate: retainVital('respiratory_rate', 15000), + systolic_bp: retainVital('systolic_bp', 900000), + diastolic_bp: retainVital('diastolic_bp', 900000), + map_bp: retainVital('map_bp', 900000), patient_id: (data.mrn && data.mrn !== 'UNKNOWN') ? data.mrn : (existing.patient_id || '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), diff --git a/routers/dashboard.py b/routers/dashboard.py index e9bd0e8..2507e26 100644 --- a/routers/dashboard.py +++ b/routers/dashboard.py @@ -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 diff --git a/static/dashboard.html b/static/dashboard.html index ff53a1f..d06a6d6 100644 --- a/static/dashboard.html +++ b/static/dashboard.html @@ -3107,11 +3107,26 @@ const readings = await readResp.json(); if (readings && readings.length > 0) { const merged = {}; + const now = Date.now(); for (let i = readings.length - 1; i >= 0; i--) { const r = readings[i]; - for (const k in r) { - if (r[k] !== null && r[k] !== undefined) merged[k] = r[k]; + const rAge = r.timestamp ? (now - new Date(r.timestamp).getTime()) : 9999999; + 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); 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) { const readResp = await fetch(`${API_BASE}/latest-readings?mrn=${encodeURIComponent(targetId)}&limit=10`); if (readResp.ok) { const readings = await readResp.json(); if (readings && readings.length > 0) { const merged = {}; + const now = Date.now(); for (let i = readings.length - 1; i >= 0; i--) { const r = readings[i]; - for (const k in r) { - if (r[k] !== null && r[k] !== undefined) merged[k] = r[k]; + const rAge = r.timestamp ? (now - new Date(r.timestamp).getTime()) : 9999999; + 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); } @@ -3204,46 +3234,48 @@ if (!data) return; // Heart Rate - if (data.heart_rate !== undefined && data.heart_rate !== null) { - machVitals.heart_rate = (data.heart_rate > 0) ? Math.round(data.heart_rate) : null; + if ('heart_rate' in data) { + machVitals.heart_rate = (data.heart_rate !== null && data.heart_rate !== undefined && data.heart_rate > 0) ? Math.round(data.heart_rate) : null; } // SpO2 - if (data.spo2 !== undefined && data.spo2 !== null) { - machVitals.spo2 = (data.spo2 > 0) ? Math.round(data.spo2) : null; + if ('spo2' in data) { + machVitals.spo2 = (data.spo2 !== null && data.spo2 !== undefined && data.spo2 > 0) ? Math.round(data.spo2) : null; } // Respiration Rate - if (data.respiratory_rate !== undefined && data.respiratory_rate !== null) { - machVitals.respiratory_rate = (data.respiratory_rate > 0) ? Math.round(data.respiratory_rate) : null; + if ('respiratory_rate' in data) { + machVitals.respiratory_rate = (data.respiratory_rate !== null && data.respiratory_rate !== undefined && data.respiratory_rate > 0) ? Math.round(data.respiratory_rate) : null; } // NIBP - if (data.systolic_bp !== undefined && data.systolic_bp !== null) { - machVitals.systolic_bp = (data.systolic_bp > 0) ? Math.round(data.systolic_bp) : null; + if ('systolic_bp' in data) { + 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) { - machVitals.diastolic_bp = (data.diastolic_bp > 0) ? Math.round(data.diastolic_bp) : null; + if ('diastolic_bp' in data) { + 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) { - machVitals.map_bp = (data.map_bp > 0) ? Math.round(data.map_bp) : null; + if ('map_bp' in data) { + machVitals.map_bp = (data.map_bp !== null && data.map_bp !== undefined && data.map_bp > 0) ? Math.round(data.map_bp) : null; } // Temperature 1 (T1) - if (data.temperature !== undefined && data.temperature !== null) { - machVitals.temperature = (data.temperature > 0) ? Number(data.temperature) : null; + if ('temperature' in data) { + machVitals.temperature = (data.temperature !== null && data.temperature !== undefined && data.temperature > 0) ? Number(data.temperature) : null; } // Temperature 2 (T2) - if (data.temperature2 !== undefined && data.temperature2 !== null) { - machVitals.temperature2 = (data.temperature2 > 0) ? Number(data.temperature2) : null; + if ('temperature2' in data) { + machVitals.temperature2 = (data.temperature2 !== null && data.temperature2 !== undefined && data.temperature2 > 0) ? Number(data.temperature2) : null; } // Temperature Difference (TD) - if (data.temperature_diff !== undefined && data.temperature_diff !== null) { - machVitals.temperature_diff = Number(data.temperature_diff); + if ('temperature_diff' in data) { + machVitals.temperature_diff = (data.temperature_diff !== null && data.temperature_diff !== undefined) ? Number(data.temperature_diff) : null; } else if (machVitals.temperature != null && machVitals.temperature2 != null) { 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; @@ -3601,7 +3633,22 @@ const readings = await response.json(); if (readings.length > 0) { + const now = Date.now(); 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) { lastReadingId = r.id; readingCount++; @@ -3674,17 +3721,23 @@ const readResp = await fetch(backfillUrl); if (readResp.ok) { const readings = await readResp.json(); + const now = Date.now(); readings.forEach(r => { const devKey = r.device_id; if (devKey && 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; - if ((dev.spo2 === undefined || dev.spo2 === null) && r.spo2 !== null && r.spo2 !== undefined) dev.spo2 = r.spo2; - 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.diastolic_bp === undefined || dev.diastolic_bp === null) && r.diastolic_bp !== null && r.diastolic_bp !== undefined) dev.diastolic_bp = r.diastolic_bp; - 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 && r.respiratory_rate !== undefined) dev.respiratory_rate = r.respiratory_rate; - if ((dev.temperature === undefined || dev.temperature === null) && r.temperature !== null && r.temperature !== undefined) dev.temperature = r.temperature; + const rAge = r.timestamp ? (now - new Date(r.timestamp).getTime()) : 9999999; + if (rAge <= 25000) { + if ((dev.heart_rate === undefined || dev.heart_rate === null) && r.heart_rate != null) dev.heart_rate = r.heart_rate; + if ((dev.spo2 === undefined || dev.spo2 === null) && r.spo2 != null) dev.spo2 = r.spo2; + if ((dev.respiratory_rate === undefined || dev.respiratory_rate === null) && r.respiratory_rate != null) dev.respiratory_rate = r.respiratory_rate; + if ((dev.temperature === undefined || dev.temperature === null) && r.temperature != null) 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); } }); @@ -4083,13 +4136,13 @@ connectedDevices[devId] = { ...existing, ...data, - heart_rate: retainVital('heart_rate', 60000), - spo2: retainVital('spo2', 60000), - temperature: retainVital('temperature', 60000), - respiratory_rate: retainVital('respiratory_rate', 60000), - systolic_bp: retainVital('systolic_bp', 3600000), - diastolic_bp: retainVital('diastolic_bp', 3600000), - map_bp: retainVital('map_bp', 3600000), + heart_rate: retainVital('heart_rate', 15000), + spo2: retainVital('spo2', 15000), + temperature: retainVital('temperature', 20000), + respiratory_rate: retainVital('respiratory_rate', 15000), + systolic_bp: retainVital('systolic_bp', 900000), + diastolic_bp: retainVital('diastolic_bp', 900000), + map_bp: retainVital('map_bp', 900000), patient_id: (data.mrn && data.mrn !== 'UNKNOWN') ? data.mrn : (existing.patient_id || '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),