updated with working dropdown graph

This commit is contained in:
Raika Furude 2025-12-04 23:30:21 -05:00
parent 77f2f1bf5b
commit 31333c8ba3
7 changed files with 745 additions and 1230 deletions

View File

@ -3,6 +3,7 @@ VitalLink Backend API
FastAPI server for managing patients, wristbands, and real-time data FastAPI server for managing patients, wristbands, and real-time data
""" """
import uuid
import os import os
import socket import socket
from fastapi import FastAPI, WebSocket, HTTPException from fastapi import FastAPI, WebSocket, HTTPException
@ -228,7 +229,8 @@ async def check_in_patient(data: PatientCheckIn):
if not available_bands: if not available_bands:
raise HTTPException(status_code=503, detail="No wristbands available") raise HTTPException(status_code=503, detail="No wristbands available")
patient_id = f"P{len(patients_db) + 100001}" # patient_id = f"P{len(patients_db) + 100001}"
patient_id = f"P{int(time.time())}-{uuid.uuid4().hex[:4].upper()}"
band_id = available_bands.pop(0) band_id = available_bands.pop(0)
patient = Patient( patient = Patient(

View File

@ -1,9 +1,10 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import * as LucideIcons from 'lucide-react'; import * as LucideIcons from 'lucide-react';
import PatientDetailModal from './PatientDetailModal'; // ADD THIS IMPORT import PatientDetailModal from './PatientDetailModal';
const { Activity, AlertCircle, Clock, Users, Bell, Heart, Thermometer, Wind, CheckCircle, UserX } = LucideIcons; const { Activity, AlertCircle, Clock, Users, Bell, Heart, Thermometer, Wind, CheckCircle, UserX } = LucideIcons;
// Dynamic API URL based on where the frontend is loaded
const API_BASE = `http://${window.location.hostname}:8000`; const API_BASE = `http://${window.location.hostname}:8000`;
function App() { function App() {
@ -18,7 +19,7 @@ function App() {
const [activeTab, setActiveTab] = useState('patients'); const [activeTab, setActiveTab] = useState('patients');
const [wristbands, setWristbands] = useState([]); const [wristbands, setWristbands] = useState([]);
const [selectedWristband, setSelectedWristband] = useState(null); const [selectedWristband, setSelectedWristband] = useState(null);
const [selectedPatient, setSelectedPatient] = useState(null); // ADD THIS STATE const [selectedPatient, setSelectedPatient] = useState(null);
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
@ -48,7 +49,6 @@ function App() {
setStats(statsData); setStats(statsData);
setWristbands(wristbandData.wristbands || []); setWristbands(wristbandData.wristbands || []);
console.log(`✓ Fetched ${queueData.length} patients and ${wristbandData.wristbands?.length || 0} wristbands`);
} catch (error) { } catch (error) {
console.error('Failed to fetch from backend:', error); console.error('Failed to fetch from backend:', error);
} }
@ -104,10 +104,9 @@ function App() {
}); });
console.log(`✓ Discharged patient ${patientId}`); console.log(`✓ Discharged patient ${patientId}`);
setPatients(prev => prev.filter(p => p.patient_id !== patientId)); setPatients(prev => prev.filter(p => p.patient_id !== patientId));
setSelectedPatient(null); // Close modal if open setSelectedPatient(null);
} catch (error) { } catch (error) {
console.error('Failed to discharge patient:', error); console.error('Failed to discharge patient:', error);
setPatients(prev => prev.filter(p => p.patient_id !== patientId));
} }
}; };
@ -117,6 +116,7 @@ function App() {
return ( return (
<div className="min-h-screen bg-gray-100"> <div className="min-h-screen bg-gray-100">
{/* HEADER SECTION */}
<div className="bg-gradient-to-r from-blue-600 to-blue-700 text-white p-6 shadow-lg"> <div className="bg-gradient-to-r from-blue-600 to-blue-700 text-white p-6 shadow-lg">
<div className="max-w-7xl mx-auto"> <div className="max-w-7xl mx-auto">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -125,70 +125,47 @@ function App() {
<p className="text-blue-100">Emergency Department Patient Monitoring</p> <p className="text-blue-100">Emergency Department Patient Monitoring</p>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="bg-white bg-opacity-20 rounded-lg px-4 py-2"> <div className="bg-black bg-opacity-20 rounded-lg px-4 py-2 border border-white/10">
<p className="text-sm text-blue-100">Last Update</p> <p className="text-sm text-blue-200">Last Update</p>
<p className="text-lg font-semibold">{new Date().toLocaleTimeString()}</p> <p className="text-lg font-semibold text-white">{new Date().toLocaleTimeString()}</p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{/* STATS BAR */}
<div className="bg-white border-b shadow-sm"> <div className="bg-white border-b shadow-sm">
<div className="max-w-7xl mx-auto p-6"> <div className="max-w-7xl mx-auto p-6">
<div className="grid grid-cols-4 gap-6"> <div className="grid grid-cols-4 gap-6">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="bg-blue-100 p-3 rounded-lg"> <div className="bg-blue-100 p-3 rounded-lg"><Users className="w-6 h-6 text-blue-600" /></div>
<Users className="w-6 h-6 text-blue-600" /> <div><p className="text-sm text-gray-600">Active Patients</p><p className="text-2xl font-bold text-gray-800">{stats.active_patients}</p></div>
</div>
<div>
<p className="text-sm text-gray-600">Active Patients</p>
<p className="text-2xl font-bold text-gray-800">{stats.active_patients}</p>
</div>
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="bg-red-100 p-3 rounded-lg"> <div className="bg-red-100 p-3 rounded-lg"><AlertCircle className="w-6 h-6 text-red-600" /></div>
<AlertCircle className="w-6 h-6 text-red-600" /> <div><p className="text-sm text-gray-600">Emergency</p><p className="text-2xl font-bold text-red-600">{stats.tier_breakdown.EMERGENCY}</p></div>
</div>
<div>
<p className="text-sm text-gray-600">Emergency</p>
<p className="text-2xl font-bold text-red-600">{stats.tier_breakdown.EMERGENCY}</p>
</div>
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="bg-yellow-100 p-3 rounded-lg"> <div className="bg-yellow-100 p-3 rounded-lg"><Bell className="w-6 h-6 text-yellow-600" /></div>
<Bell className="w-6 h-6 text-yellow-600" /> <div><p className="text-sm text-gray-600">Alert</p><p className="text-2xl font-bold text-yellow-600">{stats.tier_breakdown.ALERT}</p></div>
</div>
<div>
<p className="text-sm text-gray-600">Alert</p>
<p className="text-2xl font-bold text-yellow-600">{stats.tier_breakdown.ALERT}</p>
</div>
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="bg-green-100 p-3 rounded-lg"> <div className="bg-green-100 p-3 rounded-lg"><Clock className="w-6 h-6 text-green-600" /></div>
<Clock className="w-6 h-6 text-green-600" /> <div><p className="text-sm text-gray-600">Avg Wait Time</p><p className="text-2xl font-bold text-gray-800">{stats.average_wait_minutes} min</p></div>
</div>
<div>
<p className="text-sm text-gray-600">Avg Wait Time</p>
<p className="text-2xl font-bold text-gray-800">{stats.average_wait_minutes} min</p>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{/* TABS */}
<div className="bg-white border-b shadow-sm"> <div className="bg-white border-b shadow-sm">
<div className="max-w-7xl mx-auto px-6"> <div className="max-w-7xl mx-auto px-6">
<div className="flex gap-4 py-4"> <div className="flex gap-4 py-4">
<button <button
onClick={() => setActiveTab('patients')} onClick={() => setActiveTab('patients')}
className={`px-6 py-2 rounded-t-lg font-semibold transition-colors ${ className={`px-6 py-2 rounded-t-lg font-semibold transition-colors ${
activeTab === 'patients' activeTab === 'patients' ? 'bg-blue-100 text-blue-700 border-b-2 border-blue-600' : 'text-gray-600 hover:text-gray-800'
? 'bg-blue-100 text-blue-700 border-b-2 border-blue-600'
: 'text-gray-600 hover:text-gray-800'
}`} }`}
> >
📋 Patients ({patients.length}) 📋 Patients ({patients.length})
@ -196,9 +173,7 @@ function App() {
<button <button
onClick={() => setActiveTab('wristbands')} onClick={() => setActiveTab('wristbands')}
className={`px-6 py-2 rounded-t-lg font-semibold transition-colors ${ className={`px-6 py-2 rounded-t-lg font-semibold transition-colors ${
activeTab === 'wristbands' activeTab === 'wristbands' ? 'bg-purple-100 text-purple-700 border-b-2 border-purple-600' : 'text-gray-600 hover:text-gray-800'
? 'bg-purple-100 text-purple-700 border-b-2 border-purple-600'
: 'text-gray-600 hover:text-gray-800'
}`} }`}
> >
Wristbands ({wristbands.length}) Wristbands ({wristbands.length})
@ -207,51 +182,17 @@ function App() {
</div> </div>
</div> </div>
{/* MAIN CONTENT AREA */}
<div className="max-w-7xl mx-auto p-6"> <div className="max-w-7xl mx-auto p-6">
{activeTab === 'patients' ? ( {activeTab === 'patients' ? (
/* ==================== PATIENTS TAB ==================== */
<> <>
<div className="bg-white rounded-lg shadow-sm p-4 mb-6"> <div className="bg-white rounded-lg shadow-sm p-4 mb-6">
<div className="flex gap-2"> <div className="flex gap-2">
<button <button onClick={() => setFilter('all')} className={`px-6 py-2 rounded-lg font-semibold transition-colors ${filter === 'all' ? 'bg-blue-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}`}>All Patients ({patients.length})</button>
onClick={() => setFilter('all')} <button onClick={() => setFilter('EMERGENCY')} className={`px-6 py-2 rounded-lg font-semibold transition-colors ${filter === 'EMERGENCY' ? 'bg-red-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}`}>Emergency ({stats.tier_breakdown.EMERGENCY})</button>
className={`px-6 py-2 rounded-lg font-semibold transition-colors ${ <button onClick={() => setFilter('ALERT')} className={`px-6 py-2 rounded-lg font-semibold transition-colors ${filter === 'ALERT' ? 'bg-yellow-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}`}>Alert ({stats.tier_breakdown.ALERT})</button>
filter === 'all' <button onClick={() => setFilter('NORMAL')} className={`px-6 py-2 rounded-lg font-semibold transition-colors ${filter === 'NORMAL' ? 'bg-green-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}`}>Stable ({stats.tier_breakdown.NORMAL})</button>
? 'bg-blue-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
All Patients ({patients.length})
</button>
<button
onClick={() => setFilter('EMERGENCY')}
className={`px-6 py-2 rounded-lg font-semibold transition-colors ${
filter === 'EMERGENCY'
? 'bg-red-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
Emergency ({stats.tier_breakdown.EMERGENCY})
</button>
<button
onClick={() => setFilter('ALERT')}
className={`px-6 py-2 rounded-lg font-semibold transition-colors ${
filter === 'ALERT'
? 'bg-yellow-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
Alert ({stats.tier_breakdown.ALERT})
</button>
<button
onClick={() => setFilter('NORMAL')}
className={`px-6 py-2 rounded-lg font-semibold transition-colors ${
filter === 'NORMAL'
? 'bg-green-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
Stable ({stats.tier_breakdown.NORMAL})
</button>
</div> </div>
</div> </div>
@ -259,15 +200,13 @@ function App() {
{filteredPatients.map((patient, index) => ( {filteredPatients.map((patient, index) => (
<div <div
key={patient.patient_id} key={patient.patient_id}
onClick={() => setSelectedPatient(patient)} /* ADD CLICK HANDLER */ onClick={() => setSelectedPatient(patient)}
className="bg-white rounded-lg shadow-sm hover:shadow-md transition-shadow cursor-pointer" /* ADD cursor-pointer */ className="bg-white rounded-lg shadow-sm hover:shadow-md transition-shadow cursor-pointer"
> >
<div className="p-6"> <div className="p-6">
<div className="flex items-start justify-between mb-4"> <div className="flex items-start justify-between mb-4">
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<div className="text-2xl font-bold text-gray-400 min-w-12 text-center pt-1"> <div className="text-2xl font-bold text-gray-400 min-w-12 text-center pt-1">#{index + 1}</div>
#{index + 1}
</div>
<div> <div>
<h3 className="text-xl font-bold text-gray-800 mb-1">{patient.name}</h3> <h3 className="text-xl font-bold text-gray-800 mb-1">{patient.name}</h3>
<div className="flex items-center gap-3 text-sm text-gray-600"> <div className="flex items-center gap-3 text-sm text-gray-600">
@ -275,13 +214,11 @@ function App() {
<span></span> <span></span>
<span className="font-mono">{patient.band_id}</span> <span className="font-mono">{patient.band_id}</span>
</div> </div>
<p className="text-sm text-blue-600 mt-1 font-semibold">Click for detailed history</p> {/* ADD THIS */} <p className="text-sm text-blue-600 mt-1 font-semibold">Click for detailed history</p>
{patient.symptoms && patient.symptoms.length > 0 && ( {patient.symptoms && patient.symptoms.length > 0 && (
<div className="flex flex-wrap gap-2 mt-2"> <div className="flex flex-wrap gap-2 mt-2">
{patient.symptoms.map(symptom => ( {patient.symptoms.map(symptom => (
<span key={symptom} className="bg-gray-100 text-gray-700 px-3 py-1 rounded-full text-xs font-medium"> <span key={symptom} className="bg-gray-100 text-gray-700 px-3 py-1 rounded-full text-xs font-medium">{symptom}</span>
{symptom}
</span>
))} ))}
</div> </div>
)} )}
@ -294,8 +231,8 @@ function App() {
{patient.tier} {patient.tier}
</div> </div>
<button <button
onClick={(e) => { /* UPDATE DISCHARGE HANDLER */ onClick={(e) => {
e.stopPropagation(); // Prevent opening modal e.stopPropagation();
handleDischarge(patient.patient_id); handleDischarge(patient.patient_id);
}} }}
className="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors flex items-center gap-2 font-semibold" className="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors flex items-center gap-2 font-semibold"
@ -308,46 +245,23 @@ function App() {
<div className="grid grid-cols-4 gap-4 pt-4 border-t"> <div className="grid grid-cols-4 gap-4 pt-4 border-t">
<div className="bg-gray-50 rounded-lg p-3"> <div className="bg-gray-50 rounded-lg p-3">
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1"><Heart className="w-4 h-4 text-gray-600" /><span className="text-xs text-gray-600 font-medium">Heart Rate</span></div>
<Heart className="w-4 h-4 text-gray-600" /> <p className={`text-2xl font-bold ${getVitalStatus('hr', patient.last_hr)}`}>{patient.last_hr}</p>
<span className="text-xs text-gray-600 font-medium">Heart Rate</span>
</div>
<p className={`text-2xl font-bold ${getVitalStatus('hr', patient.last_hr)}`}>
{patient.last_hr}
</p>
<p className="text-xs text-gray-500 mt-1">bpm</p> <p className="text-xs text-gray-500 mt-1">bpm</p>
</div> </div>
<div className="bg-gray-50 rounded-lg p-3"> <div className="bg-gray-50 rounded-lg p-3">
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1"><Wind className="w-4 h-4 text-gray-600" /><span className="text-xs text-gray-600 font-medium">SpO₂</span></div>
<Wind className="w-4 h-4 text-gray-600" /> <p className={`text-2xl font-bold ${getVitalStatus('spo2', patient.last_spo2)}`}>{patient.last_spo2}</p>
<span className="text-xs text-gray-600 font-medium">SpO₂</span>
</div>
<p className={`text-2xl font-bold ${getVitalStatus('spo2', patient.last_spo2)}`}>
{patient.last_spo2}
</p>
<p className="text-xs text-gray-500 mt-1">%</p> <p className="text-xs text-gray-500 mt-1">%</p>
</div> </div>
<div className="bg-gray-50 rounded-lg p-3"> <div className="bg-gray-50 rounded-lg p-3">
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1"><Thermometer className="w-4 h-4 text-gray-600" /><span className="text-xs text-gray-600 font-medium">Temperature</span></div>
<Thermometer className="w-4 h-4 text-gray-600" /> <p className={`text-2xl font-bold ${getVitalStatus('temp', patient.last_temp)}`}>{patient.last_temp.toFixed(1)}</p>
<span className="text-xs text-gray-600 font-medium">Temperature</span>
</div>
<p className={`text-2xl font-bold ${getVitalStatus('temp', patient.last_temp)}`}>
{patient.last_temp.toFixed(1)}
</p>
<p className="text-xs text-gray-500 mt-1">°C</p> <p className="text-xs text-gray-500 mt-1">°C</p>
</div> </div>
<div className="bg-gray-50 rounded-lg p-3"> <div className="bg-gray-50 rounded-lg p-3">
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1"><Clock className="w-4 h-4 text-gray-600" /><span className="text-xs text-gray-600 font-medium">Wait Time</span></div>
<Clock className="w-4 h-4 text-gray-600" /> <p className="text-2xl font-bold text-gray-700">{patient.wait_time_minutes}</p>
<span className="text-xs text-gray-600 font-medium">Wait Time</span>
</div>
<p className="text-2xl font-bold text-gray-700">
{patient.wait_time_minutes}
</p>
<p className="text-xs text-gray-500 mt-1">minutes</p> <p className="text-xs text-gray-500 mt-1">minutes</p>
</div> </div>
</div> </div>
@ -355,20 +269,19 @@ function App() {
</div> </div>
))} ))}
</div> </div>
{filteredPatients.length === 0 && ( {filteredPatients.length === 0 && (
<div className="bg-white rounded-lg shadow-sm p-12 text-center"> <div className="bg-white rounded-lg shadow-sm p-12 text-center">
<Activity className="w-16 h-16 text-gray-300 mx-auto mb-4" /> <Activity className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-xl font-semibold text-gray-600 mb-2">No patients in this category</h3> <h3 className="text-xl font-semibold text-gray-600 mb-2">No patients in this category</h3>
<p className="text-gray-500">Patients will appear here as they check in</p>
</div> </div>
)} )}
</> </>
) : ( ) : (
/* ==================== WRISTBANDS TAB ==================== */
<div className="space-y-6"> <div className="space-y-6">
<div className="bg-white rounded-lg shadow-sm p-6"> <div className="bg-white rounded-lg shadow-sm p-6">
<h2 className="text-2xl font-bold mb-4">Wristband Inventory</h2> <h2 className="text-2xl font-bold mb-4">Wristband Inventory</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{wristbands.map(band => ( {wristbands.map(band => (
<div <div
@ -386,147 +299,104 @@ function App() {
</span> </span>
<span className={`px-2 py-1 rounded text-xs font-semibold ${ <span className={`px-2 py-1 rounded text-xs font-semibold ${
band.status === 'in_use' ? 'bg-blue-600 text-white' : band.status === 'in_use' ? 'bg-blue-600 text-white' :
band.status === 'available' ? 'bg-green-600 text-white' : band.status === 'available' ? 'bg-green-600 text-white' : 'bg-gray-400 text-white'
'bg-gray-400 text-white'
}`}> }`}>
{band.status.toUpperCase().replace('_', ' ')} {band.status.toUpperCase().replace('_', ' ')}
</span> </span>
</div> </div>
{band.patient_id && ( {band.patient_id && (
<div className="text-sm text-gray-600 mb-2"> <div className="text-sm text-gray-600 mb-2">Patient: <span className="font-mono font-semibold">{band.patient_id}</span></div>
Patient: <span className="font-mono font-semibold">{band.patient_id}</span>
</div>
)} )}
<div className="text-xs text-gray-500 flex justify-between"> <div className="text-xs text-gray-500 flex justify-between">
<span>Packets: {band.packet_count}</span> <span>Packets: {band.packet_count}</span>
{band.is_monitoring && ( {band.is_monitoring && <span className="text-green-600 font-semibold"> LIVE</span>}
<span className="text-green-600 font-semibold"> LIVE</span>
)}
</div> </div>
</div> </div>
))} ))}
</div> </div>
{wristbands.length === 0 && <p className="text-gray-500 text-center py-8">No wristbands configured</p>}
{wristbands.length === 0 && (
<p className="text-gray-500 text-center py-8">No wristbands configured</p>
)}
</div> </div>
{selectedWristband && selectedWristband.last_raw_packet && ( {/* --- WRISTBAND DETAILS SECTION --- */}
<div className="bg-white rounded-lg shadow-lg p-6"> {selectedWristband && (
<div className="flex items-center justify-between mb-4"> <div className="bg-white rounded-lg shadow-lg p-6 mt-6 border-2 border-blue-100 animate-fade-in">
<h3 className="text-xl font-bold flex items-center gap-2"> {/* Header */}
<span>Packet Details: {selectedWristband.band_id}</span> <div className="flex items-center justify-between mb-6 pb-4 border-b">
{selectedWristband.type === 'simulated' && ( <div>
<span className="text-sm bg-green-100 text-green-800 px-2 py-1 rounded">MOCK</span> <h3 className="text-xl font-bold flex items-center gap-2 text-gray-800">
)} <span>Wristband Details: <span className="font-mono text-blue-600">{selectedWristband.band_id}</span></span>
</h3> {selectedWristband.type === 'simulated' && <span className="text-xs bg-green-100 text-green-800 px-2 py-1 rounded border border-green-200">MOCK</span>}
<button {selectedWristband.type === 'real' && <span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded border border-blue-200">BLE HARDWARE</span>}
onClick={() => setSelectedWristband(null)} </h3>
className="text-gray-500 hover:text-gray-700 text-2xl font-bold" <p className="text-sm text-gray-500 mt-1">
> Status: <span className="font-semibold text-gray-700">{selectedWristband.status.toUpperCase().replace('_', ' ')}</span>
{selectedWristband.patient_id && <span> Assigned to: <span className="font-semibold text-gray-700">{selectedWristband.patient_id}</span></span>}
</button> </p>
</div>
<div className="mb-4">
<h4 className="font-semibold text-sm text-gray-600 mb-2">Raw Packet (16 bytes, Hex):</h4>
<div className="bg-gray-900 text-green-400 p-4 rounded font-mono text-sm overflow-x-auto">
{selectedWristband.last_raw_packet.hex.toUpperCase().match(/.{1,2}/g).join(' ')}
</div> </div>
<p className="text-xs text-gray-500 mt-1"> <button onClick={() => setSelectedWristband(null)} className="w-8 h-8 flex items-center justify-center rounded-full bg-gray-100 hover:bg-gray-200 text-gray-500 hover:text-gray-700 transition-colors"></button>
Format: [ver][seq][timestamp][flags][hr][spo2][temp_x100][activity_x100][checksum][rfu]
</p>
</div> </div>
{selectedWristband.last_raw_packet.decoded && ( {/* Conditional Body */}
{selectedWristband.last_raw_packet ? (
/* CASE A: HAVE DATA */
<> <>
<div className="mb-4"> <div className="mb-6">
<h4 className="font-semibold text-sm text-gray-600 mb-3">Decoded Fields:</h4> <div className="flex justify-between items-end mb-2">
<div className="grid grid-cols-2 md:grid-cols-4 gap-3"> <h4 className="font-bold text-sm text-gray-700">Latest Packet (Raw Hex)</h4>
<div className="bg-gray-50 p-3 rounded border"> <span className="text-xs text-gray-400 font-mono">16 Bytes</span>
<div className="text-xs text-gray-600 mb-1">Version</div>
<div className="font-mono text-lg font-bold">0x{selectedWristband.last_raw_packet.decoded.version.toString(16).padStart(2, '0')}</div>
</div>
<div className="bg-gray-50 p-3 rounded border">
<div className="text-xs text-gray-600 mb-1">Sequence #</div>
<div className="font-mono text-lg font-bold">{selectedWristband.last_raw_packet.decoded.sequence}</div>
</div>
<div className="bg-gray-50 p-3 rounded border col-span-2">
<div className="text-xs text-gray-600 mb-1">Timestamp (ms)</div>
<div className="font-mono text-lg font-bold">{selectedWristband.last_raw_packet.decoded.timestamp_ms.toLocaleString()}</div>
</div>
<div className="bg-blue-50 p-3 rounded border">
<div className="text-xs text-gray-600 mb-1">Heart Rate</div>
<div className="font-mono text-2xl font-bold text-blue-600">{selectedWristband.last_raw_packet.decoded.hr_bpm}</div>
<div className="text-xs text-gray-500">bpm</div>
</div>
<div className="bg-green-50 p-3 rounded border">
<div className="text-xs text-gray-600 mb-1">SpO₂</div>
<div className="font-mono text-2xl font-bold text-green-600">{selectedWristband.last_raw_packet.decoded.spo2}</div>
<div className="text-xs text-gray-500">%</div>
</div>
<div className="bg-orange-50 p-3 rounded border">
<div className="text-xs text-gray-600 mb-1">Temperature</div>
<div className="font-mono text-2xl font-bold text-orange-600">{selectedWristband.last_raw_packet.decoded.temperature_c.toFixed(2)}</div>
<div className="text-xs text-gray-500">°C</div>
</div>
<div className="bg-purple-50 p-3 rounded border">
<div className="text-xs text-gray-600 mb-1">Activity</div>
<div className="font-mono text-2xl font-bold text-purple-600">{selectedWristband.last_raw_packet.decoded.activity.toFixed(2)}</div>
<div className="text-xs text-gray-500">RMS</div>
</div>
<div className="bg-gray-50 p-3 rounded border">
<div className="text-xs text-gray-600 mb-1">Checksum</div>
<div className="font-mono text-lg font-bold">{selectedWristband.last_raw_packet.decoded.checksum}</div>
</div>
<div className="bg-gray-50 p-3 rounded border">
<div className="text-xs text-gray-600 mb-1">Flags (raw)</div>
<div className="font-mono text-lg font-bold">0x{selectedWristband.last_raw_packet.decoded.flags.raw.toString(16).padStart(2, '0')}</div>
</div>
</div> </div>
<div className="bg-slate-900 text-emerald-400 p-4 rounded-lg font-mono text-sm shadow-inner tracking-wider overflow-x-auto">
{selectedWristband.last_raw_packet.hex ? selectedWristband.last_raw_packet.hex.toUpperCase().match(/.{1,2}/g).join(' ') : "ERROR"}
</div>
<p className="text-xs text-gray-400 mt-2 font-mono">Structure: [Ver][Seq][Time....][Flags][HR][SpO2][Temp][Act][Chk][RFU]</p>
</div> </div>
{selectedWristband.last_raw_packet.decoded.flags && ( {selectedWristband.last_raw_packet.decoded && (
<div className="mt-4"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<h4 className="font-semibold text-sm text-gray-600 mb-2">Status Flags:</h4> <div className="col-span-2 md:col-span-4 flex items-center gap-2 pb-2 mt-2"><Activity className="w-4 h-4 text-gray-400" /><h4 className="font-bold text-sm text-gray-700">Decoded Telemetry</h4></div>
<div className="flex flex-wrap gap-2">
{selectedWristband.last_raw_packet.decoded.flags.emergency && ( <div className="bg-slate-50 p-3 rounded-lg border border-slate-200">
<span className="px-3 py-1 bg-red-100 text-red-800 rounded-full text-sm font-semibold border border-red-300"> <div className="text-xs text-slate-500 uppercase font-bold tracking-wider mb-1">Heart Rate</div>
🚨 Bit 4: Emergency <div className="flex items-baseline gap-1"><span className="text-2xl font-bold text-blue-600">{selectedWristband.last_raw_packet.decoded.hr_bpm}</span><span className="text-xs text-slate-400">bpm</span></div>
</span>
)}
{selectedWristband.last_raw_packet.decoded.flags.alert && (
<span className="px-3 py-1 bg-yellow-100 text-yellow-800 rounded-full text-sm font-semibold border border-yellow-300">
Bit 3: Alert
</span>
)}
{selectedWristband.last_raw_packet.decoded.flags.sensor_fault && (
<span className="px-3 py-1 bg-purple-100 text-purple-800 rounded-full text-sm font-semibold border border-purple-300">
Bit 2: Sensor Fault
</span>
)}
{selectedWristband.last_raw_packet.decoded.flags.low_battery && (
<span className="px-3 py-1 bg-orange-100 text-orange-800 rounded-full text-sm font-semibold border border-orange-300">
🔋 Bit 1: Low Battery
</span>
)}
{selectedWristband.last_raw_packet.decoded.flags.motion_artifact && (
<span className="px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm font-semibold border border-blue-300">
👋 Bit 0: Motion Artifact
</span>
)}
{!Object.values(selectedWristband.last_raw_packet.decoded.flags).some(v => v === true) && (
<span className="px-3 py-1 bg-gray-100 text-gray-600 rounded-full text-sm font-semibold">
No flags set (all normal)
</span>
)}
</div> </div>
<div className="bg-slate-50 p-3 rounded-lg border border-slate-200">
<div className="text-xs text-slate-500 uppercase font-bold tracking-wider mb-1">SpO₂</div>
<div className="flex items-baseline gap-1"><span className="text-2xl font-bold text-green-600">{selectedWristband.last_raw_packet.decoded.spo2}</span><span className="text-xs text-slate-400">%</span></div>
</div>
<div className="bg-slate-50 p-3 rounded-lg border border-slate-200">
<div className="text-xs text-slate-500 uppercase font-bold tracking-wider mb-1">Temp</div>
<div className="flex items-baseline gap-1"><span className="text-2xl font-bold text-orange-600">{selectedWristband.last_raw_packet.decoded.temperature_c.toFixed(1)}</span><span className="text-xs text-slate-400">°C</span></div>
</div>
<div className="bg-slate-50 p-3 rounded-lg border border-slate-200">
<div className="text-xs text-slate-500 uppercase font-bold tracking-wider mb-1">Sequence</div>
<div className="flex items-baseline gap-1"><span className="text-2xl font-bold text-purple-600">{selectedWristband.last_raw_packet.decoded.sequence}</span><span className="text-xs text-slate-400">#</span></div>
</div>
{selectedWristband.last_raw_packet.decoded.flags && (
<div className="col-span-2 md:col-span-4 bg-slate-50 p-3 rounded-lg border border-slate-200 mt-2">
<div className="text-xs text-slate-500 uppercase font-bold tracking-wider mb-2">System Flags</div>
<div className="flex flex-wrap gap-2">
{!Object.values(selectedWristband.last_raw_packet.decoded.flags).some(v => v === true)
? <span className="px-2 py-1 bg-green-100 text-green-700 text-xs rounded font-medium"> Normal</span>
: <span className="px-2 py-1 bg-yellow-100 text-yellow-700 text-xs rounded font-medium">! Flags Set</span>
}
</div>
</div>
)}
</div> </div>
)} )}
</> </>
) : (
/* CASE B: NO DATA */
<div className="flex flex-col items-center justify-center py-12 bg-gray-50 rounded-lg border-2 border-dashed border-gray-300">
<div className="bg-white p-4 rounded-full shadow-sm mb-4"><Wind className="w-8 h-8 text-gray-300 animate-pulse" /></div>
<h4 className="text-lg font-semibold text-gray-600">Waiting for Data...</h4>
<p className="text-sm text-gray-500 mt-2 max-w-xs text-center">
{selectedWristband.status === 'available'
? "Wristband available. Assign a patient to start."
: "Connected. Waiting for first packet..."}
</p>
</div>
)} )}
</div> </div>
)} )}
@ -534,7 +404,7 @@ function App() {
)} )}
</div> </div>
{/* Patient Detail Modal */} {/* MODALS */}
{selectedPatient && ( {selectedPatient && (
<PatientDetailModal <PatientDetailModal
patient={selectedPatient} patient={selectedPatient}

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@
> vite > vite
VITE v7.1.10 ready in 220 ms VITE v7.1.10 ready in 262 ms
➜ Local: http://localhost:5173/ ➜ Local: http://localhost:5173/
➜ Network: use --host to expose ➜ Network: use --host to expose

View File

@ -4,7 +4,7 @@
Port 5173 is in use, trying another one... Port 5173 is in use, trying another one...
VITE v7.1.10 ready in 207 ms VITE v7.1.10 ready in 236 ms
➜ Local: http://localhost:5174/ ➜ Local: http://localhost:5174/
➜ Network: use --host to expose ➜ Network: use --host to expose

Binary file not shown.

View File

@ -23,6 +23,31 @@ simulated_bands:
- band_id: "VitalLink-SIM4" - band_id: "VitalLink-SIM4"
profile: "sepsis" profile: "sepsis"
- band_id: "VitalLink-SIM5"
profile: "stable"
- band_id: "VitalLink-SIM6"
profile: "stable"
- band_id: "VitalLink-SIM7"
profile: "stable"
- band_id: "VitalLink-SIM8"
profile: "stable"
- band_id: "VitalLink-SIM9"
profile: "stable"
- band_id: "VitalLink-SIM10"
profile: "stable"
- band_id: "VitalLink-SIM11"
profile: "stable"
- band_id: "VitalLink-SIM12"
profile: "stable"
# Real Wristbands (Hardware) # Real Wristbands (Hardware)
# Add BLE addresses of your physical wristbands # Add BLE addresses of your physical wristbands
# You can find these by running: python -m wristband_manager --scan # You can find these by running: python -m wristband_manager --scan