feat: implement 4-point demographic synchronization & patient validation

This commit is contained in:
2026-07-15 11:33:48 +05:30
parent ae5a928671
commit 554771abfe
6 changed files with 407 additions and 34 deletions
+178 -26
View File
@@ -759,13 +759,67 @@
display: none !important;
}
/* === Scrollbar === */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.1); border-radius: 3px; }
/* === Verification Failure Overlay === */
.verification-overlay {
position: fixed;
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>
</head>
<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-glow"></div>
@@ -1025,6 +1079,65 @@
let activeDevices = [];
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 ===
document.addEventListener('DOMContentLoaded', () => {
const urlParams = new URLSearchParams(window.location.search);
@@ -1033,17 +1146,49 @@
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();
updateClock();
setInterval(updateClock, 1000);
fetchDevices().then(() => {
tryWebSocket();
startPolling();
});
if (isMatchingMode) {
// 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();
fetchNetworkInfo();
// Periodically refresh the list of active devices
setInterval(fetchDevices, 4000);
});
// === Fetch Network Info ===
@@ -1250,14 +1395,16 @@
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
// 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);
}
if (data.device_id === selectedDeviceId) {
if (data.device_id && data.device_id === selectedDeviceId) {
updateVitals(data);
}
// Refresh device list status
fetchDevices();
if (!isMatchingMode) {
fetchDevices();
}
};
ws.onclose = () => {
useWebSocket = false;
@@ -1279,6 +1426,9 @@
}
async function fetchLatestReadings() {
if (isMatchingMode && !selectedDeviceId) {
return;
}
try {
const url = selectedDeviceId
? `${API_BASE}/latest-readings?device_id=${encodeURIComponent(selectedDeviceId)}&limit=1`
@@ -1304,19 +1454,21 @@
}
// Fetch system status
const statusResp = await fetch(`${API_BASE}/status`);
if (statusResp.ok) {
const status = await statusResp.json();
document.getElementById('totalReadings').textContent = status.total_readings_stored;
document.getElementById('apiStatus').textContent =
status.pending_transmissions > 0
? `${status.pending_transmissions} pending`
: 'All sent ✓';
// Update active ports list in guide
const portsEl = document.getElementById('activePortsList');
if (portsEl && status.active_ports && status.active_ports.length > 0) {
portsEl.textContent = status.active_ports.join(', ');
if (!isMatchingMode) {
const statusResp = await fetch(`${API_BASE}/status`);
if (statusResp.ok) {
const status = await statusResp.json();
document.getElementById('totalReadings').textContent = status.total_readings_stored;
document.getElementById('apiStatus').textContent =
status.pending_transmissions > 0
? `${status.pending_transmissions} pending`
: 'All sent ✓';
// Update active ports list in guide
const portsEl = document.getElementById('activePortsList');
if (portsEl && status.active_ports && status.active_ports.length > 0) {
portsEl.textContent = status.active_ports.join(', ');
}
}
}
} catch (e) {