feat: implement 4-point demographic synchronization & patient validation
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user