feat: implement 4-point demographic synchronization & patient validation
This commit is contained in:
@@ -133,7 +133,7 @@ def parse_contec_hl7_text(raw_text: str, client_ip: str) -> Optional[NormalizedV
|
|||||||
|
|
||||||
found_any = False
|
found_any = False
|
||||||
|
|
||||||
# Parse PID and OBX segments
|
# Parse PID, PV1 and OBX segments
|
||||||
for seg in segments[1:]:
|
for seg in segments[1:]:
|
||||||
fields = seg.split("|")
|
fields = seg.split("|")
|
||||||
if not fields:
|
if not fields:
|
||||||
@@ -142,10 +142,37 @@ def parse_contec_hl7_text(raw_text: str, client_ip: str) -> Optional[NormalizedV
|
|||||||
seg_name = fields[0]
|
seg_name = fields[0]
|
||||||
|
|
||||||
if seg_name == "PID":
|
if seg_name == "PID":
|
||||||
|
# PID-3: MRN / Patient ID
|
||||||
if len(fields) > 3 and fields[3]:
|
if len(fields) > 3 and fields[3]:
|
||||||
patient_id = fields[3].split("^")[0]
|
patient_id = fields[3].split("^")[0].strip()
|
||||||
vitals_dict["patient_id"] = patient_id
|
vitals_dict["patient_id"] = patient_id
|
||||||
|
|
||||||
|
# PID-5: Patient Name (format: FamilyName^GivenName^...)
|
||||||
|
if len(fields) > 5 and fields[5]:
|
||||||
|
name_parts = fields[5].split("^")
|
||||||
|
family = name_parts[0].strip() if len(name_parts) > 0 else ""
|
||||||
|
given = name_parts[1].strip() if len(name_parts) > 1 else ""
|
||||||
|
full_name = f"{given} {family}".strip() or family or given
|
||||||
|
if full_name:
|
||||||
|
vitals_dict["patient_name"] = full_name
|
||||||
|
|
||||||
|
# PID-7: Date of Birth (YYYYMMDD or YYYY-MM-DD)
|
||||||
|
if len(fields) > 7 and fields[7]:
|
||||||
|
vitals_dict["patient_dob"] = fields[7].strip()
|
||||||
|
|
||||||
|
# PID-8: Administrative Sex (M / F / U / O)
|
||||||
|
if len(fields) > 8 and fields[8]:
|
||||||
|
vitals_dict["patient_gender"] = fields[8].strip().upper()
|
||||||
|
|
||||||
|
elif seg_name == "PV1":
|
||||||
|
# PV1-3: Assigned Patient Location (Room^Bed^...)
|
||||||
|
if len(fields) > 3 and fields[3]:
|
||||||
|
location_parts = fields[3].split("^")
|
||||||
|
# Use second component (Bed) if present, else first (Room)
|
||||||
|
bed = location_parts[1].strip() if len(location_parts) > 1 else location_parts[0].strip()
|
||||||
|
if bed:
|
||||||
|
vitals_dict["bed_id"] = bed
|
||||||
|
|
||||||
elif seg_name == "OBX":
|
elif seg_name == "OBX":
|
||||||
if len(fields) > 5:
|
if len(fields) > 5:
|
||||||
obs_val = fields[5].strip()
|
obs_val = fields[5].strip()
|
||||||
|
|||||||
@@ -261,11 +261,29 @@ async def process_vitals(vitals):
|
|||||||
device.status = "active"
|
device.status = "active"
|
||||||
device.last_seen = datetime.now(timezone.utc)
|
device.last_seen = datetime.now(timezone.utc)
|
||||||
|
|
||||||
# 2. Patient tracking
|
# 2. Patient tracking — store/update demographics from HL7
|
||||||
patient = db.query(Patient).filter(Patient.patient_id == vitals.patient_id).first()
|
patient = db.query(Patient).filter(Patient.patient_id == vitals.patient_id).first()
|
||||||
if not patient:
|
if not patient:
|
||||||
patient = Patient(patient_id=vitals.patient_id, name="Unknown")
|
patient = Patient(
|
||||||
|
patient_id=vitals.patient_id,
|
||||||
|
mrn=vitals.patient_id,
|
||||||
|
name=vitals.patient_name or "Unknown",
|
||||||
|
gender=vitals.patient_gender,
|
||||||
|
dob=vitals.patient_dob,
|
||||||
|
bed_id=vitals.bed_id,
|
||||||
|
)
|
||||||
db.add(patient)
|
db.add(patient)
|
||||||
|
else:
|
||||||
|
# Refresh demographics whenever monitor sends updated PID fields
|
||||||
|
if vitals.patient_name:
|
||||||
|
patient.name = vitals.patient_name
|
||||||
|
if vitals.patient_gender:
|
||||||
|
patient.gender = vitals.patient_gender
|
||||||
|
if vitals.patient_dob:
|
||||||
|
patient.dob = vitals.patient_dob
|
||||||
|
if vitals.bed_id:
|
||||||
|
patient.bed_id = vitals.bed_id
|
||||||
|
|
||||||
|
|
||||||
# 3. Create Reading Entry
|
# 3. Create Reading Entry
|
||||||
reading = VitalReading(
|
reading = VitalReading(
|
||||||
|
|||||||
@@ -12,9 +12,13 @@ class Device(Base):
|
|||||||
|
|
||||||
class Patient(Base):
|
class Patient(Base):
|
||||||
__tablename__ = "patients"
|
__tablename__ = "patients"
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
patient_id = Column(String, unique=True, index=True)
|
patient_id = Column(String, unique=True, index=True) # MRN from monitor PID-3
|
||||||
name = Column(String)
|
mrn = Column(String, index=True, nullable=True) # explicit MRN (same as patient_id)
|
||||||
|
name = Column(String, nullable=True) # full name from PID-5
|
||||||
|
gender = Column(String, nullable=True) # M/F/U from PID-8
|
||||||
|
dob = Column(String, nullable=True) # YYYYMMDD from PID-7
|
||||||
|
bed_id = Column(String, nullable=True) # bed from PV1-3
|
||||||
|
|
||||||
class VitalReading(Base):
|
class VitalReading(Base):
|
||||||
__tablename__ = "vital_readings"
|
__tablename__ = "vital_readings"
|
||||||
|
|||||||
@@ -269,3 +269,170 @@ async def push_vitals(vitals: NormalizedVitals, api_key: str = None, db: Session
|
|||||||
logger.error(f"Push-vitals error: {e}")
|
logger.error(f"Push-vitals error: {e}")
|
||||||
db.rollback()
|
db.rollback()
|
||||||
raise HTTPException(status_code=500, detail=f"Failed to store vitals: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"Failed to store vitals: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
# === Patient-to-Monitor Matching Endpoint ===
|
||||||
|
def _normalize_name(s: str) -> str:
|
||||||
|
"""Lowercase, strip punctuation and extra spaces for fuzzy name comparison."""
|
||||||
|
import re
|
||||||
|
if not s:
|
||||||
|
return ""
|
||||||
|
return re.sub(r"[^a-z0-9\s]", "", s.lower()).split()
|
||||||
|
|
||||||
|
def _normalize_dob(dob: str) -> str:
|
||||||
|
"""Normalize DOB to YYYYMMDD regardless of input format (YYYY-MM-DD / YYYYMMDD / DD-MM-YYYY)."""
|
||||||
|
if not dob:
|
||||||
|
return ""
|
||||||
|
dob = dob.strip().replace("/", "-")
|
||||||
|
if len(dob) == 8 and dob.isdigit():
|
||||||
|
return dob # already YYYYMMDD
|
||||||
|
parts = dob.split("-")
|
||||||
|
if len(parts) == 3:
|
||||||
|
if len(parts[0]) == 4: # YYYY-MM-DD
|
||||||
|
return "".join(parts)
|
||||||
|
elif len(parts[2]) == 4: # DD-MM-YYYY
|
||||||
|
return parts[2] + parts[1] + parts[0]
|
||||||
|
return dob.replace("-", "")
|
||||||
|
|
||||||
|
def _gender_code(g: str) -> str:
|
||||||
|
"""Return single-letter gender code: M / F / U."""
|
||||||
|
if not g:
|
||||||
|
return ""
|
||||||
|
g = g.strip().upper()
|
||||||
|
if g in ("M", "MALE"):
|
||||||
|
return "M"
|
||||||
|
if g in ("F", "FEMALE"):
|
||||||
|
return "F"
|
||||||
|
return "U"
|
||||||
|
|
||||||
|
def _calculate_age(dob_str: str) -> Optional[int]:
|
||||||
|
"""Calculate age from YYYYMMDD string."""
|
||||||
|
if not dob_str:
|
||||||
|
return None
|
||||||
|
normalized = _normalize_dob(dob_str)
|
||||||
|
if len(normalized) != 8 or not normalized.isdigit():
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
birth_year = int(normalized[:4])
|
||||||
|
birth_month = int(normalized[4:6])
|
||||||
|
birth_day = int(normalized[6:])
|
||||||
|
today = datetime.now()
|
||||||
|
return today.year - birth_year - ((today.month, today.day) < (birth_month, birth_day))
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@router.get("/match-patient")
|
||||||
|
def match_patient(
|
||||||
|
mrn: str = None,
|
||||||
|
name: str = None,
|
||||||
|
gender: str = None,
|
||||||
|
dob: str = None,
|
||||||
|
age: int = None,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Match incoming patient demographics to an active Contec monitor.
|
||||||
|
|
||||||
|
Call this from the dashboard iframe URL:
|
||||||
|
/api/dashboard?minimal=true&mrn=MRN001&name=John+Doe&gender=M&dob=19900115&age=36
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{ matched: true/false, device_id, patient_id, bed_id, device_status, ... }
|
||||||
|
"""
|
||||||
|
if not mrn and not name:
|
||||||
|
return {"matched": False, "reason": "No patient identifiers supplied"}
|
||||||
|
|
||||||
|
# Fetch all patients
|
||||||
|
all_patients = db.query(Patient).all()
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
# Normalize incoming params
|
||||||
|
in_mrn = (mrn or "").strip().upper()
|
||||||
|
in_name = _normalize_name(name or "")
|
||||||
|
in_gender = _gender_code(gender or "")
|
||||||
|
in_dob = _normalize_dob(dob or "")
|
||||||
|
in_age = age
|
||||||
|
|
||||||
|
matched_patient = None
|
||||||
|
match_score = 0
|
||||||
|
|
||||||
|
for p in all_patients:
|
||||||
|
score = 0
|
||||||
|
|
||||||
|
# --- MRN check (primary) ---
|
||||||
|
p_mrn = (p.mrn or p.patient_id or "").strip().upper()
|
||||||
|
if in_mrn and p_mrn == in_mrn:
|
||||||
|
score += 10
|
||||||
|
elif in_mrn:
|
||||||
|
continue # MRN supplied but doesn't match → skip
|
||||||
|
|
||||||
|
# --- Name check ---
|
||||||
|
if in_name:
|
||||||
|
p_name = _normalize_name(p.name or "")
|
||||||
|
if p_name and any(w in p_name for w in in_name):
|
||||||
|
score += 3
|
||||||
|
|
||||||
|
# --- Gender check ---
|
||||||
|
if in_gender and p.gender:
|
||||||
|
if _gender_code(p.gender) == in_gender:
|
||||||
|
score += 2
|
||||||
|
|
||||||
|
# --- DOB check ---
|
||||||
|
if in_dob and p.dob:
|
||||||
|
if _normalize_dob(p.dob) == in_dob:
|
||||||
|
score += 3
|
||||||
|
|
||||||
|
# --- Age check ---
|
||||||
|
if in_age is not None and p.dob:
|
||||||
|
calc_p_age = _calculate_age(p.dob)
|
||||||
|
if calc_p_age is not None and calc_p_age == in_age:
|
||||||
|
score += 2
|
||||||
|
|
||||||
|
if score > match_score:
|
||||||
|
match_score = score
|
||||||
|
matched_patient = p
|
||||||
|
|
||||||
|
# To be matched, MRN must be found, and name/gender/dob must also be validated
|
||||||
|
# (Require at least a score of 12: MRN + Name, or MRN + Gender, etc.)
|
||||||
|
if not matched_patient or match_score < 12:
|
||||||
|
return {"matched": False, "reason": "No matching patient found in monitor database with 4-point verification"}
|
||||||
|
|
||||||
|
# Find the most recent vital reading for this patient
|
||||||
|
latest = (
|
||||||
|
db.query(VitalReading)
|
||||||
|
.filter(VitalReading.patient_id == matched_patient.patient_id)
|
||||||
|
.order_by(VitalReading.timestamp.desc())
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
if not latest:
|
||||||
|
return {
|
||||||
|
"matched": True,
|
||||||
|
"patient_id": matched_patient.patient_id,
|
||||||
|
"device_id": None,
|
||||||
|
"bed_id": matched_patient.bed_id,
|
||||||
|
"reason": "Patient found but no readings yet"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get device status
|
||||||
|
device = db.query(Device).filter(Device.device_id == latest.device_id).first()
|
||||||
|
device_status = "unknown"
|
||||||
|
if device:
|
||||||
|
last_seen = device.last_seen
|
||||||
|
if last_seen.tzinfo is None:
|
||||||
|
last_seen = last_seen.replace(tzinfo=timezone.utc)
|
||||||
|
device_status = "active" if (now - last_seen).total_seconds() < 15 else "offline"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"matched": True,
|
||||||
|
"patient_id": matched_patient.patient_id,
|
||||||
|
"patient_name": matched_patient.name,
|
||||||
|
"patient_gender": matched_patient.gender,
|
||||||
|
"patient_dob": matched_patient.dob,
|
||||||
|
"device_id": latest.device_id,
|
||||||
|
"bed_id": matched_patient.bed_id,
|
||||||
|
"device_status": device_status,
|
||||||
|
"last_reading_at": latest.timestamp.isoformat() if latest.timestamp else None,
|
||||||
|
"match_score": match_score,
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ class NormalizedVitals(BaseModel):
|
|||||||
respiratory_rate: Optional[float] = None
|
respiratory_rate: Optional[float] = None
|
||||||
temperature: Optional[float] = None
|
temperature: Optional[float] = None
|
||||||
present_fields: Optional[list] = None
|
present_fields: Optional[list] = None
|
||||||
|
# Patient demographics parsed from HL7 PID/PV1 segments
|
||||||
|
patient_name: Optional[str] = None # PID-5: "Given Family"
|
||||||
|
patient_gender: Optional[str] = None # PID-8: M / F / U
|
||||||
|
patient_dob: Optional[str] = None # PID-7: YYYYMMDD
|
||||||
|
bed_id: Optional[str] = None # PV1-3: bed / room ID
|
||||||
|
|
||||||
class DeviceStatus(BaseModel):
|
class DeviceStatus(BaseModel):
|
||||||
device_id: str
|
device_id: str
|
||||||
|
|||||||
@@ -759,13 +759,67 @@
|
|||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* === Scrollbar === */
|
/* === Verification Failure Overlay === */
|
||||||
::-webkit-scrollbar { width: 6px; }
|
.verification-overlay {
|
||||||
::-webkit-scrollbar-track { background: transparent; }
|
position: fixed;
|
||||||
::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.1); border-radius: 3px; }
|
inset: 0;
|
||||||
|
background: rgba(248, 250, 252, 0.98);
|
||||||
|
z-index: 9999;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
padding: 24px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
.verification-overlay .error-card {
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid rgba(220, 38, 38, 0.12);
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.05);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 32px;
|
||||||
|
max-width: 460px;
|
||||||
|
width: 100%;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.verification-overlay .error-icon {
|
||||||
|
font-size: 40px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.verification-overlay h2 {
|
||||||
|
font-size: 19px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
.verification-overlay p {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.verification-overlay.loading h2 {
|
||||||
|
color: var(--spo2-color);
|
||||||
|
}
|
||||||
|
.verification-overlay.loading .error-icon {
|
||||||
|
animation: rotateGlow 2s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes rotateGlow {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<!-- Patient Verification Overlay -->
|
||||||
|
<div id="verificationOverlay" class="verification-overlay" style="display: none;">
|
||||||
|
<div class="error-card" id="verificationCard">
|
||||||
|
<div class="error-icon" id="verificationIcon">🔒</div>
|
||||||
|
<h2 id="verificationTitle">Patient Verification Required</h2>
|
||||||
|
<p id="verificationMsg">Telemetry data is only displayed once the patient's MRN, Name, Gender, and DOB are fully verified against the active monitor.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="bg-grid"></div>
|
<div class="bg-grid"></div>
|
||||||
<div class="bg-glow"></div>
|
<div class="bg-glow"></div>
|
||||||
|
|
||||||
@@ -1025,6 +1079,65 @@
|
|||||||
let activeDevices = [];
|
let activeDevices = [];
|
||||||
let selectedDeviceId = localStorage.getItem('selectedDeviceId') || '';
|
let selectedDeviceId = localStorage.getItem('selectedDeviceId') || '';
|
||||||
|
|
||||||
|
// Verification / Matching Mode State
|
||||||
|
let isMatchingMode = false;
|
||||||
|
let matchParams = {};
|
||||||
|
|
||||||
|
async function verifyPatientMatch() {
|
||||||
|
const overlay = document.getElementById('verificationOverlay');
|
||||||
|
const icon = document.getElementById('verificationIcon');
|
||||||
|
const title = document.getElementById('verificationTitle');
|
||||||
|
const msg = document.getElementById('verificationMsg');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query = new URLSearchParams(matchParams).toString();
|
||||||
|
const response = await fetch(`${API_BASE}/match-patient?${query}`);
|
||||||
|
if (!response.ok) throw new Error('Match verification API error');
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.matched) {
|
||||||
|
if (data.device_id) {
|
||||||
|
// Success - matched and connected!
|
||||||
|
overlay.style.display = 'none';
|
||||||
|
// Hide device selector element to restrict access
|
||||||
|
const selectorContainer = document.querySelector('.device-selector-container');
|
||||||
|
if (selectorContainer) selectorContainer.style.display = 'none';
|
||||||
|
|
||||||
|
if (selectedDeviceId !== data.device_id) {
|
||||||
|
selectedDeviceId = data.device_id;
|
||||||
|
clearVitalsDisplay();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Matched but device not transmitting yet
|
||||||
|
overlay.style.display = 'flex';
|
||||||
|
overlay.className = 'verification-overlay loading';
|
||||||
|
icon.textContent = '⏳';
|
||||||
|
title.textContent = 'Connecting Monitor...';
|
||||||
|
msg.textContent = `Patient ${data.patient_name || 'Record'} verified. Waiting for monitor to start transmitting...`;
|
||||||
|
selectedDeviceId = '';
|
||||||
|
clearVitalsDisplay();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Match failed
|
||||||
|
overlay.style.display = 'flex';
|
||||||
|
overlay.className = 'verification-overlay';
|
||||||
|
icon.textContent = '❌';
|
||||||
|
title.textContent = 'Access Restricted';
|
||||||
|
msg.textContent = data.reason || 'This patient record does not match the active monitor configuration.';
|
||||||
|
selectedDeviceId = '';
|
||||||
|
clearVitalsDisplay();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Match verification failed:", e);
|
||||||
|
// On API error, show connecting/retry state
|
||||||
|
overlay.style.display = 'flex';
|
||||||
|
overlay.className = 'verification-overlay loading';
|
||||||
|
icon.textContent = '🔄';
|
||||||
|
title.textContent = 'Verification Server Offline';
|
||||||
|
msg.textContent = 'Retrying verification match...';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// === Initialize ===
|
// === Initialize ===
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
@@ -1033,17 +1146,49 @@
|
|||||||
document.body.classList.add('minimal-mode');
|
document.body.classList.add('minimal-mode');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract matching credentials from URL query params
|
||||||
|
const mrn = urlParams.get('mrn');
|
||||||
|
const name = urlParams.get('name');
|
||||||
|
const gender = urlParams.get('gender');
|
||||||
|
const dob = urlParams.get('dob');
|
||||||
|
const age = urlParams.get('age');
|
||||||
|
|
||||||
|
if (mrn || name) {
|
||||||
|
isMatchingMode = true;
|
||||||
|
matchParams = { mrn, name, gender, dob, age };
|
||||||
|
|
||||||
|
// Show verification loading overlay initially
|
||||||
|
const overlay = document.getElementById('verificationOverlay');
|
||||||
|
overlay.style.display = 'flex';
|
||||||
|
overlay.className = 'verification-overlay loading';
|
||||||
|
document.getElementById('verificationIcon').textContent = '🔄';
|
||||||
|
document.getElementById('verificationTitle').textContent = 'Verifying Patient Credentials...';
|
||||||
|
document.getElementById('verificationMsg').textContent = 'Matching MRN, Name, Gender, DOB, and Age with live patient monitors...';
|
||||||
|
}
|
||||||
|
|
||||||
initWaveforms();
|
initWaveforms();
|
||||||
updateClock();
|
updateClock();
|
||||||
setInterval(updateClock, 1000);
|
setInterval(updateClock, 1000);
|
||||||
fetchDevices().then(() => {
|
|
||||||
tryWebSocket();
|
if (isMatchingMode) {
|
||||||
startPolling();
|
// In matching mode, verify credentials first, then poll/ws
|
||||||
});
|
verifyPatientMatch().then(() => {
|
||||||
|
tryWebSocket();
|
||||||
|
startPolling();
|
||||||
|
});
|
||||||
|
// Periodically verify the match and update device mapping
|
||||||
|
setInterval(verifyPatientMatch, 5000);
|
||||||
|
} else {
|
||||||
|
fetchDevices().then(() => {
|
||||||
|
tryWebSocket();
|
||||||
|
startPolling();
|
||||||
|
});
|
||||||
|
// Periodically refresh the list of active devices
|
||||||
|
setInterval(fetchDevices, 4000);
|
||||||
|
}
|
||||||
|
|
||||||
animateWaveforms();
|
animateWaveforms();
|
||||||
fetchNetworkInfo();
|
fetchNetworkInfo();
|
||||||
// Periodically refresh the list of active devices
|
|
||||||
setInterval(fetchDevices, 4000);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// === Fetch Network Info ===
|
// === Fetch Network Info ===
|
||||||
@@ -1250,14 +1395,16 @@
|
|||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
// Filter messages by selected device or auto-select if nothing selected yet
|
// Filter messages by selected device or auto-select if nothing selected yet
|
||||||
if (!selectedDeviceId && data.device_id) {
|
if (!selectedDeviceId && data.device_id && !isMatchingMode) {
|
||||||
selectDevice(data.device_id);
|
selectDevice(data.device_id);
|
||||||
}
|
}
|
||||||
if (data.device_id === selectedDeviceId) {
|
if (data.device_id && data.device_id === selectedDeviceId) {
|
||||||
updateVitals(data);
|
updateVitals(data);
|
||||||
}
|
}
|
||||||
// Refresh device list status
|
// Refresh device list status
|
||||||
fetchDevices();
|
if (!isMatchingMode) {
|
||||||
|
fetchDevices();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
ws.onclose = () => {
|
ws.onclose = () => {
|
||||||
useWebSocket = false;
|
useWebSocket = false;
|
||||||
@@ -1279,6 +1426,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchLatestReadings() {
|
async function fetchLatestReadings() {
|
||||||
|
if (isMatchingMode && !selectedDeviceId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const url = selectedDeviceId
|
const url = selectedDeviceId
|
||||||
? `${API_BASE}/latest-readings?device_id=${encodeURIComponent(selectedDeviceId)}&limit=1`
|
? `${API_BASE}/latest-readings?device_id=${encodeURIComponent(selectedDeviceId)}&limit=1`
|
||||||
@@ -1304,19 +1454,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fetch system status
|
// Fetch system status
|
||||||
const statusResp = await fetch(`${API_BASE}/status`);
|
if (!isMatchingMode) {
|
||||||
if (statusResp.ok) {
|
const statusResp = await fetch(`${API_BASE}/status`);
|
||||||
const status = await statusResp.json();
|
if (statusResp.ok) {
|
||||||
document.getElementById('totalReadings').textContent = status.total_readings_stored;
|
const status = await statusResp.json();
|
||||||
document.getElementById('apiStatus').textContent =
|
document.getElementById('totalReadings').textContent = status.total_readings_stored;
|
||||||
status.pending_transmissions > 0
|
document.getElementById('apiStatus').textContent =
|
||||||
? `${status.pending_transmissions} pending`
|
status.pending_transmissions > 0
|
||||||
: 'All sent ✓';
|
? `${status.pending_transmissions} pending`
|
||||||
|
: 'All sent ✓';
|
||||||
// Update active ports list in guide
|
|
||||||
const portsEl = document.getElementById('activePortsList');
|
// Update active ports list in guide
|
||||||
if (portsEl && status.active_ports && status.active_ports.length > 0) {
|
const portsEl = document.getElementById('activePortsList');
|
||||||
portsEl.textContent = status.active_ports.join(', ');
|
if (portsEl && status.active_ports && status.active_ports.length > 0) {
|
||||||
|
portsEl.textContent = status.active_ports.join(', ');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
Reference in New Issue
Block a user