add real-time patient vital signs monitoring dashboard and API routers
This commit is contained in:
+61
-23
@@ -3185,9 +3185,11 @@
|
||||
p = patients.find(pt => pt.device_id === activeDeviceId);
|
||||
}
|
||||
if (!p && patients.length > 0) {
|
||||
// Find patient corresponding to a live transmitting device
|
||||
// Find patient corresponding to a live transmitting device, active device, patient with latest vitals, or first patient
|
||||
p = patients.find(pt => pt.device_id && connectedDevices[pt.device_id]?._liveData) ||
|
||||
patients.find(pt => pt.device_id && pt.device_id === activeDeviceId);
|
||||
patients.find(pt => pt.device_id && pt.device_id === activeDeviceId) ||
|
||||
patients.find(pt => pt.latest_vitals != null) ||
|
||||
patients[0];
|
||||
}
|
||||
|
||||
if (p) {
|
||||
@@ -3195,6 +3197,9 @@
|
||||
machVitals.patient_id = p.mrn || p.patient_id || null;
|
||||
machVitals.bed_id = p.bed_id || null;
|
||||
machVitals.device_id = p.device_id || activeDeviceId || '';
|
||||
if (!activeDeviceId && p.device_id) {
|
||||
activeDeviceId = p.device_id;
|
||||
}
|
||||
|
||||
const elName = document.getElementById('machPatientName');
|
||||
if (elName) elName.textContent = machVitals.patient_name ? machVitals.patient_name.toUpperCase() : '--';
|
||||
@@ -3462,6 +3467,7 @@
|
||||
};
|
||||
let waveformCanvases = {};
|
||||
let useWebSocket = false;
|
||||
let lastWebSocketMessageTime = 0;
|
||||
let ws = null;
|
||||
let audioAlertEnabled = false;
|
||||
let audioContext = null;
|
||||
@@ -3646,9 +3652,10 @@
|
||||
: 'All sent ✓';
|
||||
}
|
||||
|
||||
// If WebSocket is connected, it delivers sub-second live machine telemetry directly.
|
||||
// Do NOT overwrite live WebSocket data with older throttled DB snapshots.
|
||||
if (useWebSocket) {
|
||||
// If WebSocket is connected and actively receiving messages, it delivers live telemetry.
|
||||
// Only skip polling if WebSocket messages were received within the last 5 seconds.
|
||||
const hasRecentWs = lastWebSocketMessageTime && (Date.now() - lastWebSocketMessageTime < 5000);
|
||||
if (useWebSocket && hasRecentWs) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3765,7 +3772,28 @@
|
||||
if (mrn && mrn !== 'UNKNOWN') {
|
||||
patientRegistry[mrn] = p;
|
||||
}
|
||||
if (p.device_id && connectedDevices[p.device_id]) {
|
||||
if (p.device_id) {
|
||||
if (!connectedDevices[p.device_id]) {
|
||||
connectedDevices[p.device_id] = {
|
||||
device_id: p.device_id,
|
||||
ip_address: p.device_ip || 'remote-push',
|
||||
bedLabel: p.bed_id || getBedLabel(p.device_id, p.device_ip),
|
||||
lastSeen: new Date(),
|
||||
status: p.device_status || 'active',
|
||||
_fromDb: true,
|
||||
_liveData: false
|
||||
};
|
||||
}
|
||||
if (p.latest_vitals) {
|
||||
connectedDevices[p.device_id]._liveData = true;
|
||||
if (p.latest_vitals.heart_rate != null) connectedDevices[p.device_id].heart_rate = p.latest_vitals.heart_rate;
|
||||
if (p.latest_vitals.spo2 != null) connectedDevices[p.device_id].spo2 = p.latest_vitals.spo2;
|
||||
if (p.latest_vitals.respiratory_rate != null) connectedDevices[p.device_id].respiratory_rate = p.latest_vitals.respiratory_rate;
|
||||
if (p.latest_vitals.temperature != null) connectedDevices[p.device_id].temperature = p.latest_vitals.temperature;
|
||||
if (p.latest_vitals.systolic_bp != null) connectedDevices[p.device_id].systolic_bp = p.latest_vitals.systolic_bp;
|
||||
if (p.latest_vitals.diastolic_bp != null) connectedDevices[p.device_id].diastolic_bp = p.latest_vitals.diastolic_bp;
|
||||
if (p.latest_vitals.map_bp != null) connectedDevices[p.device_id].map_bp = p.latest_vitals.map_bp;
|
||||
}
|
||||
Object.assign(connectedDevices[p.device_id], {
|
||||
patient_id: mrn,
|
||||
mrn: mrn,
|
||||
@@ -3804,6 +3832,8 @@
|
||||
renderSingleDevice(connectedDevices[activeDeviceId]);
|
||||
}
|
||||
} else {
|
||||
const devIds = Object.keys(connectedDevices);
|
||||
if (!activeDeviceId && devIds.length > 0) activeDeviceId = devIds[0];
|
||||
if (viewMode === 'grid') renderMultiBedGrid();
|
||||
else if (activeDeviceId && connectedDevices[activeDeviceId]) renderSingleDevice(connectedDevices[activeDeviceId]);
|
||||
}
|
||||
@@ -3868,8 +3898,11 @@
|
||||
const tabsContainer = document.getElementById('deviceTabs');
|
||||
if (!tabsContainer) return;
|
||||
|
||||
// Only show devices that have received a live WebSocket packet
|
||||
let devIds = Object.keys(connectedDevices).filter(id => connectedDevices[id]._liveData === true);
|
||||
// Show devices that are live via WebSocket, or have recent data/active status
|
||||
let devIds = Object.keys(connectedDevices).filter(id => {
|
||||
const dev = connectedDevices[id];
|
||||
return dev._liveData === true || dev.status === 'active' || (dev.lastSeen && (Date.now() - dev.lastSeen.getTime()) < 900000);
|
||||
});
|
||||
if (hasTargetFilter) {
|
||||
devIds = devIds.filter(id => matchesTargetPatient(connectedDevices[id], id));
|
||||
}
|
||||
@@ -3899,8 +3932,11 @@
|
||||
|
||||
function renderMultiBedGrid() {
|
||||
const multiGrid = document.getElementById('multiBedGrid');
|
||||
// Only show devices confirmed live via WebSocket — exclude DB-only ghost devices
|
||||
let devIds = Object.keys(connectedDevices).filter(id => connectedDevices[id]._liveData === true);
|
||||
// Show devices confirmed live via WebSocket or active with vitals
|
||||
let devIds = Object.keys(connectedDevices).filter(id => {
|
||||
const dev = connectedDevices[id];
|
||||
return dev._liveData === true || dev.status === 'active' || (dev.lastSeen && (Date.now() - dev.lastSeen.getTime()) < 900000);
|
||||
});
|
||||
if (hasTargetFilter) {
|
||||
devIds = devIds.filter(id => matchesTargetPatient(connectedDevices[id], id));
|
||||
}
|
||||
@@ -3985,28 +4021,29 @@
|
||||
const pGender = (dev.patient_gender && dev.patient_gender !== 'Unknown') ? dev.patient_gender : null;
|
||||
const pDob = (dev.patient_dob && dev.patient_dob !== 'Unknown') ? dev.patient_dob : null;
|
||||
|
||||
// A device is "live" only if it has received a live WebSocket packet within 8 seconds
|
||||
// Devices loaded only from DB (never seen via WebSocket) are shown as DISCONNECTED
|
||||
const isLive = dev._liveData === true && dev.lastSeen && (Date.now() - dev.lastSeen.getTime()) < 8000;
|
||||
// Has active vitals (probe connected and sending data)
|
||||
const hasVitals = isLive && (dev.heart_rate != null || dev.spo2 != null || dev.temperature != null);
|
||||
const hr = hasVitals && dev.heart_rate != null ? Math.round(dev.heart_rate) : '---';
|
||||
const spo2 = hasVitals && dev.spo2 != null ? Math.round(dev.spo2) : '---';
|
||||
const sys = hasVitals && dev.systolic_bp != null ? Math.round(dev.systolic_bp) : '--';
|
||||
const dia = hasVitals && dev.diastolic_bp != null ? Math.round(dev.diastolic_bp) : '--';
|
||||
// A device is live if actively transmitting or active in DB within 30 seconds
|
||||
const isLive = (dev._liveData === true || dev.status === 'active') && dev.lastSeen && (Date.now() - dev.lastSeen.getTime()) < 30000;
|
||||
// Has recorded vitals
|
||||
const hasVitals = (dev.heart_rate != null || dev.spo2 != null || dev.temperature != null);
|
||||
const hr = dev.heart_rate != null ? Math.round(dev.heart_rate) : '---';
|
||||
const spo2 = dev.spo2 != null ? Math.round(dev.spo2) : '---';
|
||||
const sys = dev.systolic_bp != null ? Math.round(dev.systolic_bp) : '--';
|
||||
const dia = dev.diastolic_bp != null ? Math.round(dev.diastolic_bp) : '--';
|
||||
const bp = (sys !== '--' || dia !== '--') ? `${sys}/${dia}` : '---';
|
||||
const temp = hasVitals && dev.temperature != null ? dev.temperature.toFixed(1) + '°C' : '---';
|
||||
const rr = hasVitals && dev.respiratory_rate != null ? Math.round(dev.respiratory_rate) : '---';
|
||||
const temp = dev.temperature != null ? dev.temperature.toFixed(1) + '°C' : '---';
|
||||
const rr = dev.respiratory_rate != null ? Math.round(dev.respiratory_rate) : '---';
|
||||
const timeStr = dev.lastSeen ? dev.lastSeen.toLocaleTimeString('en-US', { hour12: false }) : '--';
|
||||
|
||||
const genderIcon = pGender ? (pGender.toLowerCase().startsWith('m') ? '♂ Male' : pGender.toLowerCase().startsWith('f') ? '♀ Female' : pGender) : '';
|
||||
|
||||
// Badge: LIVE (data flowing) | CONNECTED (monitor on, probe off) | DISCONNECTED
|
||||
// Badge: LIVE (data flowing) | CONNECTED (monitor on / recent data) | DISCONNECTED
|
||||
const statusBadge = isLive
|
||||
? (hasVitals
|
||||
? `<span class="status-badge normal">Live ↗</span>`
|
||||
: `<span class="status-badge warning" style="background:rgba(234,179,8,.15);color:#fbbf24;border:1px solid rgba(234,179,8,.3);">Connected</span>`)
|
||||
: `<span class="status-badge offline" style="background:rgba(239,68,68,.15);color:#f87171;border:1px solid rgba(239,68,68,.3);">Disconnected</span>`;
|
||||
: (hasVitals
|
||||
? `<span class="status-badge warning" style="background:rgba(234,179,8,.15);color:#fbbf24;border:1px solid rgba(234,179,8,.3);">Recent</span>`
|
||||
: `<span class="status-badge offline" style="background:rgba(239,68,68,.15);color:#f87171;border:1px solid rgba(239,68,68,.3);">Disconnected</span>`);
|
||||
|
||||
html += `
|
||||
<div class="bed-card" onclick="selectDevice('${id}')" title="Click to view detailed waveforms for ${label}">
|
||||
@@ -4121,6 +4158,7 @@
|
||||
}
|
||||
|
||||
if (!data.device_id) return;
|
||||
lastWebSocketMessageTime = Date.now();
|
||||
|
||||
// Hide setup guide once data starts flowing
|
||||
if (!hasReceivedData) {
|
||||
|
||||
Reference in New Issue
Block a user