add real-time patient vital signs monitoring dashboard and API routers

This commit is contained in:
2026-09-21 12:49:47 +05:30
parent cc45a2f270
commit ccab4992af
32 changed files with 66 additions and 8291 deletions
+61 -23
View File
@@ -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) {