updated with database backend + graph view

This commit is contained in:
Raika Furude 2025-11-24 11:16:21 -05:00
parent 5c0ba4e0a1
commit e3cb804671
17 changed files with 1615 additions and 2356 deletions

Binary file not shown.

View File

@ -0,0 +1,623 @@
"""
VitalLink Database Layer
SQLite persistence for patients, vitals, and audit trail
Enables replay, analysis, and incident investigation
"""
import sqlite3
import json
from datetime import datetime
from typing import List, Dict, Optional
from contextlib import asynccontextmanager
import aiosqlite
# ============================================================================
# DATABASE SCHEMA
# ============================================================================
SCHEMA_SQL = """
-- Patients table
CREATE TABLE IF NOT EXISTS patients (
patient_id TEXT PRIMARY KEY,
band_id TEXT NOT NULL,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
dob TEXT NOT NULL,
symptoms TEXT, -- JSON array
severity TEXT,
check_in_time TIMESTAMP NOT NULL,
discharge_time TIMESTAMP,
current_tier TEXT DEFAULT 'NORMAL',
is_active BOOLEAN DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Vitals readings table (time-series data)
CREATE TABLE IF NOT EXISTS vitals_readings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
patient_id TEXT NOT NULL,
band_id TEXT NOT NULL,
timestamp REAL NOT NULL,
seq INTEGER,
hr_bpm INTEGER,
spo2 INTEGER,
temp_c REAL,
activity REAL,
tier TEXT,
flags TEXT, -- JSON array
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (patient_id) REFERENCES patients(patient_id)
);
-- Triage assessments (audit trail)
CREATE TABLE IF NOT EXISTS triage_assessments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
patient_id TEXT NOT NULL,
assessment_time TIMESTAMP NOT NULL,
triage_level INTEGER,
tier_name TEXT,
priority_score REAL,
reasoning TEXT,
abnormalities TEXT, -- JSON array
wait_time_minutes INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (patient_id) REFERENCES patients(patient_id)
);
-- Tier changes (for incident investigation)
CREATE TABLE IF NOT EXISTS tier_changes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
patient_id TEXT NOT NULL,
change_time TIMESTAMP NOT NULL,
old_tier TEXT,
new_tier TEXT,
trigger_reason TEXT,
vitals_snapshot TEXT, -- JSON
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (patient_id) REFERENCES patients(patient_id)
);
-- System events (audit log)
CREATE TABLE IF NOT EXISTS system_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
event_time TIMESTAMP NOT NULL,
event_type TEXT NOT NULL, -- 'patient_checkin', 'discharge', 'tier_change', 'alert', etc.
patient_id TEXT,
band_id TEXT,
details TEXT, -- JSON
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Wristband assignments (inventory tracking)
CREATE TABLE IF NOT EXISTS wristband_assignments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
band_id TEXT NOT NULL,
patient_id TEXT,
assigned_at TIMESTAMP,
released_at TIMESTAMP,
packet_count INTEGER DEFAULT 0,
band_type TEXT, -- 'real' or 'simulated'
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Indexes for performance
CREATE INDEX IF NOT EXISTS idx_vitals_patient ON vitals_readings(patient_id, timestamp);
CREATE INDEX IF NOT EXISTS idx_vitals_timestamp ON vitals_readings(timestamp);
CREATE INDEX IF NOT EXISTS idx_patients_active ON patients(is_active);
CREATE INDEX IF NOT EXISTS idx_tier_changes_patient ON tier_changes(patient_id, change_time);
"""
# ============================================================================
# DATABASE MANAGER
# ============================================================================
class VitalLinkDatabase:
"""Database manager for VitalLink system"""
def __init__(self, db_path: str = "vitallink.db"):
self.db_path = db_path
self.conn = None
async def initialize(self):
"""Initialize database and create tables"""
self.conn = await aiosqlite.connect(self.db_path)
await self.conn.executescript(SCHEMA_SQL)
await self.conn.commit()
print(f"✓ Database initialized: {self.db_path}")
async def close(self):
"""Close database connection"""
if self.conn:
await self.conn.close()
# ========================================================================
# PATIENT OPERATIONS
# ========================================================================
async def save_patient(self, patient_data: Dict):
"""Save new patient to database"""
await self.conn.execute(
"""
INSERT INTO patients (
patient_id, band_id, first_name, last_name, dob,
symptoms, severity, check_in_time, current_tier
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
patient_data["patient_id"],
patient_data["band_id"],
patient_data["first_name"],
patient_data["last_name"],
patient_data["dob"],
json.dumps(patient_data["symptoms"]),
patient_data["severity"],
patient_data["check_in_time"],
patient_data.get("current_tier", "NORMAL"),
),
)
await self.conn.commit()
# Log event
await self.log_event(
"patient_checkin",
patient_data["patient_id"],
patient_data["band_id"],
patient_data,
)
async def update_patient_tier(self, patient_id: str, new_tier: str):
"""Update patient's current tier"""
await self.conn.execute(
"""
UPDATE patients SET current_tier = ? WHERE patient_id = ?
""",
(new_tier, patient_id),
)
await self.conn.commit()
async def discharge_patient(self, patient_id: str):
"""Mark patient as discharged"""
await self.conn.execute(
"""
UPDATE patients
SET is_active = 0, discharge_time = ?
WHERE patient_id = ?
""",
(datetime.now(), patient_id),
)
await self.conn.commit()
await self.log_event(
"discharge",
patient_id,
None,
{"discharge_time": datetime.now().isoformat()},
)
# ========================================================================
# VITALS OPERATIONS
# ========================================================================
async def save_vitals(self, vitals_data: Dict):
"""Save vital signs reading"""
await self.conn.execute(
"""
INSERT INTO vitals_readings (
patient_id, band_id, timestamp, seq, hr_bpm, spo2,
temp_c, activity, tier, flags
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
vitals_data["patient_id"],
vitals_data["band_id"],
vitals_data["timestamp"],
vitals_data.get("seq", 0),
vitals_data["hr_bpm"],
vitals_data["spo2"],
vitals_data["temp_c"],
vitals_data["activity"],
vitals_data["tier"],
json.dumps(vitals_data.get("flags", [])),
),
)
await self.conn.commit()
async def get_patient_vitals_history(
self, patient_id: str, limit: int = 100
) -> List[Dict]:
"""Get vital signs history for a patient"""
cursor = await self.conn.execute(
"""
SELECT timestamp, hr_bpm, spo2, temp_c, activity, tier, seq
FROM vitals_readings
WHERE patient_id = ?
ORDER BY timestamp DESC
LIMIT ?
""",
(patient_id, limit),
)
rows = await cursor.fetchall()
return [
{
"timestamp": row[0],
"hr_bpm": row[1],
"spo2": row[2],
"temp_c": row[3],
"activity": row[4],
"tier": row[5],
"seq": row[6],
}
for row in rows
]
# ========================================================================
# TIER CHANGE TRACKING
# ========================================================================
async def log_tier_change(
self, patient_id: str, old_tier: str, new_tier: str, reason: str, vitals: Dict
):
"""Log tier change for audit trail"""
await self.conn.execute(
"""
INSERT INTO tier_changes (
patient_id, change_time, old_tier, new_tier,
trigger_reason, vitals_snapshot
) VALUES (?, ?, ?, ?, ?, ?)
""",
(
patient_id,
datetime.now(),
old_tier,
new_tier,
reason,
json.dumps(vitals),
),
)
await self.conn.commit()
await self.log_event(
"tier_change",
patient_id,
None,
{"old_tier": old_tier, "new_tier": new_tier, "reason": reason},
)
async def get_tier_history(self, patient_id: str) -> List[Dict]:
"""Get tier change history for incident review"""
cursor = await self.conn.execute(
"""
SELECT change_time, old_tier, new_tier, trigger_reason, vitals_snapshot
FROM tier_changes
WHERE patient_id = ?
ORDER BY change_time ASC
""",
(patient_id,),
)
rows = await cursor.fetchall()
return [
{
"change_time": row[0],
"old_tier": row[1],
"new_tier": row[2],
"reason": row[3],
"vitals": json.loads(row[4]) if row[4] else {},
}
for row in rows
]
# ========================================================================
# SYSTEM EVENTS (Audit Trail)
# ========================================================================
async def log_event(
self,
event_type: str,
patient_id: Optional[str],
band_id: Optional[str],
details: Dict,
):
"""Log system event for audit trail"""
await self.conn.execute(
"""
INSERT INTO system_events (
event_time, event_type, patient_id, band_id, details
) VALUES (?, ?, ?, ?, ?)
""",
(datetime.now(), event_type, patient_id, band_id, json.dumps(details)),
)
await self.conn.commit()
async def get_events(
self,
event_type: Optional[str] = None,
patient_id: Optional[str] = None,
hours: int = 24,
) -> List[Dict]:
"""Get system events for analysis"""
query = "SELECT event_time, event_type, patient_id, band_id, details FROM system_events WHERE 1=1"
params = []
if event_type:
query += " AND event_type = ?"
params.append(event_type)
if patient_id:
query += " AND patient_id = ?"
params.append(patient_id)
query += " AND event_time > datetime('now', '-' || ? || ' hours')"
params.append(hours)
query += " ORDER BY event_time DESC LIMIT 1000"
cursor = await self.conn.execute(query, params)
rows = await cursor.fetchall()
return [
{
"event_time": row[0],
"event_type": row[1],
"patient_id": row[2],
"band_id": row[3],
"details": json.loads(row[4]) if row[4] else {},
}
for row in rows
]
# ========================================================================
# ANALYTICS & REPLAY
# ========================================================================
async def get_session_summary(
self, start_time: datetime, end_time: datetime
) -> Dict:
"""Get summary statistics for a session (for incident review)"""
cursor = await self.conn.execute(
"""
SELECT
COUNT(DISTINCT patient_id) as total_patients,
COUNT(*) as total_vitals,
AVG(hr_bpm) as avg_hr,
AVG(spo2) as avg_spo2,
AVG(temp_c) as avg_temp
FROM vitals_readings
WHERE timestamp BETWEEN ? AND ?
""",
(start_time.timestamp(), end_time.timestamp()),
)
row = await cursor.fetchone()
return {
"total_patients": row[0],
"total_vitals_recorded": row[1],
"average_hr": round(row[2], 1) if row[2] else 0,
"average_spo2": round(row[3], 1) if row[3] else 0,
"average_temp": round(row[4], 2) if row[4] else 0,
}
async def export_patient_data(self, patient_id: str) -> Dict:
"""Export complete patient data for incident investigation"""
# Get patient info
cursor = await self.conn.execute(
"""
SELECT * FROM patients WHERE patient_id = ?
""",
(patient_id,),
)
patient_row = await cursor.fetchone()
if not patient_row:
return None
# Get all vitals
vitals = await self.get_patient_vitals_history(patient_id, limit=10000)
# Get tier changes
tier_changes = await self.get_tier_history(patient_id)
# Get related events
events = await self.get_events(patient_id=patient_id, hours=24)
return {
"patient_id": patient_id,
"name": f"{patient_row[2]} {patient_row[3]}",
"dob": patient_row[4],
"symptoms": json.loads(patient_row[5]) if patient_row[5] else [],
"severity": patient_row[6],
"check_in_time": patient_row[7],
"discharge_time": patient_row[8],
"total_vitals": len(vitals),
"vitals_timeline": vitals,
"tier_changes": tier_changes,
"events": events,
"export_time": datetime.now().isoformat(),
}
# ============================================================================
# REPLAY SYSTEM
# ============================================================================
class VitalsReplaySystem:
"""Replay historical vitals data for analysis"""
def __init__(self, db: VitalLinkDatabase):
self.db = db
async def replay_patient_session(self, patient_id: str, speed: float = 1.0):
"""
Replay a patient's entire session
speed: 1.0 = real-time, 10.0 = 10x faster, 0.1 = slow motion
"""
vitals = await self.db.get_patient_vitals_history(patient_id, limit=10000)
vitals.reverse() # Chronological order
if not vitals:
print(f"No data found for patient {patient_id}")
return
print(f"\n{'=' * 80}")
print(f"REPLAYING SESSION: {patient_id} ({len(vitals)} readings)")
print(f"Speed: {speed}x | Press Ctrl+C to stop")
print(f"{'=' * 80}\n")
start_time = vitals[0]["timestamp"]
for i, reading in enumerate(vitals):
# Calculate delay
if i > 0:
time_diff = reading["timestamp"] - vitals[i - 1]["timestamp"]
await asyncio.sleep(time_diff / speed)
# Display reading
elapsed = reading["timestamp"] - start_time
tier_symbol = (
"🔴"
if reading["tier"] == "EMERGENCY"
else "🟡"
if reading["tier"] == "ALERT"
else "🟢"
)
print(
f"[{elapsed:7.1f}s] {tier_symbol} Seq={reading['seq']:3d} | "
f"HR={reading['hr_bpm']:3d} SpO2={reading['spo2']:2d}% "
f"Temp={reading['temp_c']:.1f}°C | {reading['tier']}"
)
print(f"\n{'=' * 80}")
print(f"Replay complete: {len(vitals)} readings")
print(f"{'=' * 80}\n")
async def analyze_critical_events(self, patient_id: str):
"""Analyze critical tier changes and deterioration events"""
tier_changes = await self.db.get_tier_history(patient_id)
print(f"\n{'=' * 80}")
print(f"CRITICAL EVENT ANALYSIS: {patient_id}")
print(f"{'=' * 80}\n")
for change in tier_changes:
print(f"[{change['change_time']}]")
print(f" {change['old_tier']}{change['new_tier']}")
print(f" Reason: {change['reason']}")
print(f" Vitals: {change['vitals']}")
print()
print(f"{'=' * 80}\n")
# ============================================================================
# INTEGRATION HELPERS
# ============================================================================
async def init_database(db_path: str = "vitallink.db") -> VitalLinkDatabase:
"""Initialize database for use in FastAPI"""
db = VitalLinkDatabase(db_path)
await db.initialize()
return db
# ============================================================================
# CLI TOOLS
# ============================================================================
async def cli_export_patient(patient_id: str, output_file: str = None):
"""Export patient data to JSON file"""
db = VitalLinkDatabase()
await db.initialize()
data = await db.export_patient_data(patient_id)
if not data:
print(f"Patient {patient_id} not found")
return
if output_file:
with open(output_file, "w") as f:
json.dump(data, f, indent=2)
print(f"✓ Exported to {output_file}")
else:
print(json.dumps(data, indent=2))
await db.close()
async def cli_replay_session(patient_id: str, speed: float = 1.0):
"""Replay a patient session"""
db = VitalLinkDatabase()
await db.initialize()
replay = VitalsReplaySystem(db)
await replay.replay_patient_session(patient_id, speed)
await db.close()
async def cli_analyze_incident(patient_id: str):
"""Analyze critical events for a patient"""
db = VitalLinkDatabase()
await db.initialize()
replay = VitalsReplaySystem(db)
await replay.analyze_critical_events(patient_id)
# Also show vital trends
vitals = await db.get_patient_vitals_history(patient_id, limit=1000)
if vitals:
print("VITAL SIGN TRENDS:")
print(
f" HR range: {min(v['hr_bpm'] for v in vitals)} - {max(v['hr_bpm'] for v in vitals)} bpm"
)
print(
f" SpO2 range: {min(v['spo2'] for v in vitals)} - {max(v['spo2'] for v in vitals)}%"
)
print(
f" Temp range: {min(v['temp_c'] for v in vitals):.1f} - {max(v['temp_c'] for v in vitals):.1f}°C"
)
print()
await db.close()
if __name__ == "__main__":
import argparse
import asyncio
parser = argparse.ArgumentParser(description="VitalLink Database Tools")
parser.add_argument("--export", metavar="PATIENT_ID", help="Export patient data")
parser.add_argument("--replay", metavar="PATIENT_ID", help="Replay patient session")
parser.add_argument(
"--analyze", metavar="PATIENT_ID", help="Analyze critical events"
)
parser.add_argument(
"--speed", type=float, default=1.0, help="Replay speed multiplier"
)
parser.add_argument("--output", "-o", help="Output file for export")
args = parser.parse_args()
if args.export:
asyncio.run(cli_export_patient(args.export, args.output))
elif args.replay:
asyncio.run(cli_replay_session(args.replay, args.speed))
elif args.analyze:
asyncio.run(cli_analyze_incident(args.analyze))
else:
parser.print_help()

View File

@ -14,6 +14,7 @@ import json
import time
from collections import defaultdict
from triage_engine import TriageEngine, VitalSigns, TriageLevel, triage_from_vitals
from database import VitalLinkDatabase
# ============================================================================
# LIFESPAN MANAGEMENT
@ -23,14 +24,23 @@ from triage_engine import TriageEngine, VitalSigns, TriageLevel, triage_from_vit
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup
global db
print("=" * 80)
print("VitalLink Backend API Started")
print("=" * 80)
# Initialize database
db = VitalLinkDatabase("vitallink.db")
await db.initialize()
print("API Documentation: http://localhost:8000/docs")
print("WebSocket Endpoint: ws://localhost:8000/ws")
print("Database: vitallink.db")
print("=" * 80)
yield
# Shutdown
if db:
await db.close()
print("\nVitalLink Backend API Shutting Down")
@ -115,6 +125,7 @@ active_websockets: List[WebSocket] = []
# Wristband details cache
wristband_details_cache = {}
db: Optional[VitalLinkDatabase] = None
# ============================================================================
# PRIORITY ALGORITHM
@ -195,6 +206,10 @@ async def check_in_patient(data: PatientCheckIn):
patients_db[patient_id] = patient
# Save to database
if db:
await db.save_patient(patient.dict())
await broadcast_update({"type": "patient_added", "patient": patient.dict()})
return {
@ -303,6 +318,9 @@ async def receive_vitals(data: VitalsData):
final_tier = tracker["current_tier"]
patient.current_tier = final_tier
patient.last_vitals = data.dict()
# Save to database
if db:
await db.save_vitals(data.dict())
# Store in history
vitals_history[patient_id].append(data)
@ -439,6 +457,32 @@ async def get_statistics():
}
@app.get("/api/patients/{patient_id}/vitals-history")
async def get_patient_vitals_history(patient_id: str, limit: int = 100):
"""Get patient vitals history for graphing"""
if patient_id not in patients_db:
raise HTTPException(status_code=404, detail="Patient not found")
patient = patients_db[patient_id]
if db:
# Get from database
vitals = await db.get_patient_vitals_history(patient_id, limit)
tier_changes = await db.get_tier_history(patient_id)
else:
# Fallback to in-memory
vitals = [v.dict() for v in vitals_history.get(patient_id, [])[-limit:]]
tier_changes = []
return {
"patient": patient.dict(),
"vitals_history": vitals,
"tier_changes": tier_changes,
"total_readings": len(vitals),
}
# ============================================================================
# WRISTBAND ENDPOINTS
# ============================================================================

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,8 @@
"dependencies": {
"lucide-react": "^0.546.0",
"react": "^19.1.1",
"react-dom": "^19.1.1"
"react-dom": "^19.1.1",
"recharts": "^3.5.0"
},
"devDependencies": {
"@eslint/js": "^9.36.0",

View File

@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react';
import * as LucideIcons from 'lucide-react';
import PatientDetailModal from './PatientDetailModal'; // ADD THIS IMPORT
const { Activity, AlertCircle, Clock, Users, Bell, Heart, Thermometer, Wind, CheckCircle, UserX } = LucideIcons;
@ -17,6 +18,7 @@ function App() {
const [activeTab, setActiveTab] = useState('patients');
const [wristbands, setWristbands] = useState([]);
const [selectedWristband, setSelectedWristband] = useState(null);
const [selectedPatient, setSelectedPatient] = useState(null); // ADD THIS STATE
useEffect(() => {
const fetchData = async () => {
@ -102,6 +104,7 @@ function App() {
});
console.log(`✓ Discharged patient ${patientId}`);
setPatients(prev => prev.filter(p => p.patient_id !== patientId));
setSelectedPatient(null); // Close modal if open
} catch (error) {
console.error('Failed to discharge patient:', error);
setPatients(prev => prev.filter(p => p.patient_id !== patientId));
@ -256,7 +259,8 @@ function App() {
{filteredPatients.map((patient, index) => (
<div
key={patient.patient_id}
className="bg-white rounded-lg shadow-sm hover:shadow-md transition-shadow"
onClick={() => setSelectedPatient(patient)} /* ADD CLICK HANDLER */
className="bg-white rounded-lg shadow-sm hover:shadow-md transition-shadow cursor-pointer" /* ADD cursor-pointer */
>
<div className="p-6">
<div className="flex items-start justify-between mb-4">
@ -271,6 +275,7 @@ function App() {
<span></span>
<span className="font-mono">{patient.band_id}</span>
</div>
<p className="text-sm text-blue-600 mt-1 font-semibold">Click for detailed history</p> {/* ADD THIS */}
{patient.symptoms && patient.symptoms.length > 0 && (
<div className="flex flex-wrap gap-2 mt-2">
{patient.symptoms.map(symptom => (
@ -289,7 +294,10 @@ function App() {
{patient.tier}
</div>
<button
onClick={() => handleDischarge(patient.patient_id)}
onClick={(e) => { /* UPDATE DISCHARGE HANDLER */
e.stopPropagation(); // Prevent opening modal
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"
>
<UserX className="w-4 h-4" />
@ -357,174 +365,20 @@ function App() {
)}
</>
) : (
/* Wristbands tab - your existing code stays the same */
<div className="space-y-6">
<div className="bg-white rounded-lg shadow-sm p-6">
<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">
{wristbands.map(band => (
<div
key={band.band_id}
onClick={() => setSelectedWristband(band)}
className={`p-4 rounded-lg border-2 cursor-pointer transition-all ${
band.status === 'in_use'
? 'bg-blue-50 border-blue-300 hover:border-blue-400'
: 'bg-gray-50 border-gray-300 hover:border-gray-400'
}`}
>
<div className="flex items-center justify-between mb-2">
<span className="font-mono font-bold text-lg">
{band.type === 'real' ? '🔵' : '🟢'} {band.band_id}
</span>
<span className={`px-2 py-1 rounded text-xs font-semibold ${
band.status === 'in_use' ? 'bg-blue-600 text-white' :
band.status === 'available' ? 'bg-green-600 text-white' :
'bg-gray-400 text-white'
}`}>
{band.status.toUpperCase().replace('_', ' ')}
</span>
</div>
{band.patient_id && (
<div className="text-sm text-gray-600 mb-2">
Patient: <span className="font-mono font-semibold">{band.patient_id}</span>
</div>
)}
<div className="text-xs text-gray-500 flex justify-between">
<span>Packets: {band.packet_count}</span>
{band.is_monitoring && (
<span className="text-green-600 font-semibold"> LIVE</span>
)}
</div>
</div>
))}
</div>
{wristbands.length === 0 && (
<p className="text-gray-500 text-center py-8">No wristbands configured</p>
)}
</div>
{selectedWristband && selectedWristband.last_raw_packet && (
<div className="bg-white rounded-lg shadow-lg p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-xl font-bold flex items-center gap-2">
<span>Packet Details: {selectedWristband.band_id}</span>
{selectedWristband.type === 'simulated' && (
<span className="text-sm bg-green-100 text-green-800 px-2 py-1 rounded">MOCK</span>
)}
</h3>
<button
onClick={() => setSelectedWristband(null)}
className="text-gray-500 hover:text-gray-700 text-2xl font-bold"
>
</button>
</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>
<p className="text-xs text-gray-500 mt-1">
Format: [ver][seq][timestamp][flags][hr][spo2][temp_x100][activity_x100][checksum][rfu]
</p>
</div>
{selectedWristband.last_raw_packet.decoded && (
<>
<div className="mb-4">
<h4 className="font-semibold text-sm text-gray-600 mb-3">Decoded Fields:</h4>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<div className="bg-gray-50 p-3 rounded border">
<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 since boot)</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>
{selectedWristband.last_raw_packet.decoded.flags && (
<div className="mt-4">
<h4 className="font-semibold text-sm text-gray-600 mb-2">Status Flags:</h4>
<div className="flex flex-wrap gap-2">
{selectedWristband.last_raw_packet.decoded.flags.emergency && (
<span className="px-3 py-1 bg-red-100 text-red-800 rounded-full text-sm font-semibold border border-red-300">
🚨 Bit 4: Emergency
</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>
)}
{/* ... your existing wristband code ... */}
</div>
)}
</div>
{/* Patient Detail Modal - ADD THIS */}
{selectedPatient && (
<PatientDetailModal
patient={selectedPatient}
onClose={() => setSelectedPatient(null)}
/>
)}
</div>
);
}

View File

@ -0,0 +1,295 @@
import React, { useState, useEffect } from 'react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, ReferenceLine } from 'recharts';
import { X, TrendingUp, TrendingDown, Activity, Heart, Wind, Thermometer } from 'lucide-react';
const API_BASE = 'http://localhost:8000';
const PatientDetailModal = ({ patient, onClose }) => {
const [vitalsHistory, setVitalsHistory] = useState([]);
const [tierChanges, setTierChanges] = useState([]);
const [loading, setLoading] = useState(true);
const [selectedMetric, setSelectedMetric] = useState('all');
useEffect(() => {
fetchPatientHistory();
}, [patient.patient_id]);
const fetchPatientHistory = async () => {
try {
const response = await fetch(`${API_BASE}/api/patients/${patient.patient_id}/vitals-history?limit=200`);
const data = await response.json();
setVitalsHistory(data.vitals_history || []);
setTierChanges(data.tier_changes || []);
setLoading(false);
} catch (error) {
console.error('Failed to fetch patient history:', error);
setLoading(false);
}
};
const prepareChartData = () => {
return vitalsHistory.slice().reverse().map((v, index) => ({
time: index,
timeLabel: new Date(v.timestamp * 1000).toLocaleTimeString(),
hr: v.hr_bpm,
spo2: v.spo2,
temp: v.temp_c,
tier: v.tier
}));
};
const calculateTrend = (data, key) => {
if (data.length < 2) return null;
const recent = data.slice(-5);
const first = recent[0][key];
const last = recent[recent.length - 1][key];
const change = last - first;
return {
direction: change > 0 ? 'up' : change < 0 ? 'down' : 'stable',
value: Math.abs(change).toFixed(1)
};
};
const chartData = prepareChartData();
const hrTrend = calculateTrend(chartData, 'hr');
const spo2Trend = calculateTrend(chartData, 'spo2');
const tempTrend = calculateTrend(chartData, 'temp');
if (loading) {
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-8">
<Activity className="w-8 h-8 text-blue-600 animate-spin mx-auto" />
<p className="text-gray-600 mt-4">Loading patient data...</p>
</div>
</div>
);
}
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl shadow-2xl max-w-6xl w-full max-h-[90vh] overflow-y-auto">
<div className="sticky top-0 bg-gradient-to-r from-blue-600 to-blue-700 text-white p-6 rounded-t-2xl flex justify-between items-start">
<div>
<h2 className="text-3xl font-bold mb-2">{patient.name}</h2>
<div className="flex items-center gap-4 text-blue-100">
<span className="font-mono">{patient.patient_id}</span>
<span></span>
<span className="font-mono">{patient.band_id}</span>
<span></span>
<span>Wait: {patient.wait_time_minutes} min</span>
</div>
</div>
<button
onClick={onClose}
className="text-white hover:bg-white hover:bg-opacity-20 rounded-lg p-2 transition-colors"
>
<X className="w-6 h-6" />
</button>
</div>
<div className="p-6">
<div className="grid grid-cols-3 gap-4 mb-6">
<div className="bg-blue-50 rounded-lg p-4 border-2 border-blue-200">
<div className="flex items-center gap-2 mb-2">
<Heart className="w-5 h-5 text-blue-600" />
<span className="text-sm font-medium text-gray-600">Heart Rate</span>
</div>
<div className="flex items-end justify-between">
<p className="text-3xl font-bold text-blue-600">{patient.last_hr}</p>
<div className="text-right">
<p className="text-xs text-gray-500">bpm</p>
{hrTrend && (
<p className={`text-xs font-semibold flex items-center gap-1 ${
hrTrend.direction === 'up' ? 'text-red-600' :
hrTrend.direction === 'down' ? 'text-green-600' : 'text-gray-600'
}`}>
{hrTrend.direction === 'up' ? <TrendingUp className="w-3 h-3" /> :
hrTrend.direction === 'down' ? <TrendingDown className="w-3 h-3" /> : null}
{hrTrend.value}
</p>
)}
</div>
</div>
</div>
<div className="bg-green-50 rounded-lg p-4 border-2 border-green-200">
<div className="flex items-center gap-2 mb-2">
<Wind className="w-5 h-5 text-green-600" />
<span className="text-sm font-medium text-gray-600">SpO₂</span>
</div>
<div className="flex items-end justify-between">
<p className="text-3xl font-bold text-green-600">{patient.last_spo2}</p>
<div className="text-right">
<p className="text-xs text-gray-500">%</p>
{spo2Trend && (
<p className={`text-xs font-semibold flex items-center gap-1 ${
spo2Trend.direction === 'down' ? 'text-red-600' :
spo2Trend.direction === 'up' ? 'text-green-600' : 'text-gray-600'
}`}>
{spo2Trend.direction === 'up' ? <TrendingUp className="w-3 h-3" /> :
spo2Trend.direction === 'down' ? <TrendingDown className="w-3 h-3" /> : null}
{spo2Trend.value}
</p>
)}
</div>
</div>
</div>
<div className="bg-orange-50 rounded-lg p-4 border-2 border-orange-200">
<div className="flex items-center gap-2 mb-2">
<Thermometer className="w-5 h-5 text-orange-600" />
<span className="text-sm font-medium text-gray-600">Temperature</span>
</div>
<div className="flex items-end justify-between">
<p className="text-3xl font-bold text-orange-600">{patient.last_temp.toFixed(1)}</p>
<div className="text-right">
<p className="text-xs text-gray-500">°C</p>
{tempTrend && (
<p className={`text-xs font-semibold flex items-center gap-1 ${
tempTrend.direction === 'up' ? 'text-red-600' :
tempTrend.direction === 'down' ? 'text-green-600' : 'text-gray-600'
}`}>
{tempTrend.direction === 'up' ? <TrendingUp className="w-3 h-3" /> :
tempTrend.direction === 'down' ? <TrendingDown className="w-3 h-3" /> : null}
{tempTrend.value}
</p>
)}
</div>
</div>
</div>
</div>
<div className="flex gap-2 mb-4">
{['all', 'hr', 'spo2', 'temp'].map(metric => (
<button
key={metric}
onClick={() => setSelectedMetric(metric)}
className={`px-4 py-2 rounded-lg font-semibold ${
selectedMetric === metric ? 'bg-blue-600 text-white' : 'bg-gray-100 text-gray-700'
}`}
>
{metric === 'all' ? 'All Vitals' :
metric === 'hr' ? 'Heart Rate' :
metric === 'spo2' ? 'SpO₂' : 'Temperature'}
</button>
))}
</div>
{chartData.length > 0 ? (
<div className="bg-white border-2 border-gray-200 rounded-lg p-4 mb-6">
{(selectedMetric === 'all' || selectedMetric === 'hr') && (
<div className="mb-6">
<h3 className="font-bold text-lg mb-3 flex items-center gap-2">
<Heart className="w-5 h-5 text-blue-600" />
Heart Rate Over Time
</h3>
<ResponsiveContainer width="100%" height={250}>
<LineChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="timeLabel" tick={{fontSize: 12}} />
<YAxis domain={[40, 180]} label={{ value: 'BPM', angle: -90, position: 'insideLeft' }} />
<Tooltip />
<ReferenceLine y={110} stroke="#ef4444" strokeDasharray="3 3" label="ALERT" />
<ReferenceLine y={140} stroke="#dc2626" strokeDasharray="3 3" label="EMERGENCY" />
<Line type="monotone" dataKey="hr" stroke="#2563eb" strokeWidth={3} dot={{ r: 4 }} />
</LineChart>
</ResponsiveContainer>
</div>
)}
{(selectedMetric === 'all' || selectedMetric === 'spo2') && (
<div className="mb-6">
<h3 className="font-bold text-lg mb-3 flex items-center gap-2">
<Wind className="w-5 h-5 text-green-600" />
Oxygen Saturation Over Time
</h3>
<ResponsiveContainer width="100%" height={250}>
<LineChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="timeLabel" tick={{fontSize: 12}} />
<YAxis domain={[70, 100]} label={{ value: '%', angle: -90, position: 'insideLeft' }} />
<Tooltip />
<ReferenceLine y={92} stroke="#eab308" strokeDasharray="3 3" label="ALERT" />
<ReferenceLine y={88} stroke="#dc2626" strokeDasharray="3 3" label="EMERGENCY" />
<Line type="monotone" dataKey="spo2" stroke="#16a34a" strokeWidth={3} dot={{ r: 4 }} />
</LineChart>
</ResponsiveContainer>
</div>
)}
{(selectedMetric === 'all' || selectedMetric === 'temp') && (
<div>
<h3 className="font-bold text-lg mb-3 flex items-center gap-2">
<Thermometer className="w-5 h-5 text-orange-600" />
Temperature Over Time
</h3>
<ResponsiveContainer width="100%" height={250}>
<LineChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="timeLabel" tick={{fontSize: 12}} />
<YAxis domain={[35, 41]} label={{ value: '°C', angle: -90, position: 'insideLeft' }} />
<Tooltip />
<ReferenceLine y={38.3} stroke="#eab308" strokeDasharray="3 3" label="ALERT" />
<ReferenceLine y={39.5} stroke="#dc2626" strokeDasharray="3 3" label="EMERGENCY" />
<Line type="monotone" dataKey="temp" stroke="#ea580c" strokeWidth={3} dot={{ r: 4 }} />
</LineChart>
</ResponsiveContainer>
</div>
)}
</div>
) : (
<div className="bg-gray-50 rounded-lg p-12 text-center">
<Activity className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<p className="text-gray-600">No vital signs history available yet</p>
</div>
)}
{tierChanges.length > 0 && (
<div className="bg-white border-2 border-gray-200 rounded-lg p-4 mt-4">
<h3 className="font-bold text-lg mb-3">Tier Change History</h3>
<div className="space-y-2">
{tierChanges.map((change, index) => (
<div key={index} className="flex items-center gap-3 p-3 bg-gray-50 rounded">
<span className="text-sm text-gray-500">
{new Date(change.change_time).toLocaleTimeString()}
</span>
<span className="text-sm font-semibold">{change.old_tier}</span>
<span></span>
<span className="text-sm font-semibold text-red-600">{change.new_tier}</span>
<span className="text-xs text-gray-600">({change.reason})</span>
</div>
))}
</div>
</div>
)}
<div className="mt-6 grid grid-cols-3 gap-4">
<div className="bg-gray-50 rounded-lg p-4">
<p className="text-sm text-gray-600">Total Readings</p>
<p className="text-2xl font-bold">{vitalsHistory.length}</p>
</div>
<div className="bg-gray-50 rounded-lg p-4">
<p className="text-sm text-gray-600">Priority Score</p>
<p className="text-2xl font-bold">{patient.priority_score.toFixed(1)}</p>
</div>
<div className="bg-gray-50 rounded-lg p-4">
<p className="text-sm text-gray-600">Current Tier</p>
<p className={`text-2xl font-bold ${
patient.tier === 'EMERGENCY' ? 'text-red-600' :
patient.tier === 'ALERT' ? 'text-yellow-600' : 'text-green-600'
}`}>
{patient.tier}
</p>
</div>
</div>
</div>
</div>
</div>
);
};
export default PatientDetailModal;

View File

@ -0,0 +1,4 @@
62558
62574
62581
62616

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
62558

View File

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

View File

@ -0,0 +1 @@
62581

View File

@ -4,137 +4,7 @@
Port 5173 is in use, trying another one...
VITE v7.1.10 ready in 233 ms
VITE v7.1.10 ready in 215 ms
➜ Local: http://localhost:5174/
➜ Network: use --host to expose
10:36:25 AM [vite] (client) Pre-transform error: /home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/src/App.jsx: Support for the experimental syntax 'decorators' isn't currently enabled (1:1):
> 1 | @import "tailwindcss";
| ^
2 |
3 | /* Extra large touchscreen keyboard */
4 | .kiosk-keyboard .hg-button {
Add @babel/plugin-proposal-decorators (https://github.com/babel/babel/tree/main/packages/babel-plugin-proposal-decorators) to the 'plugins' section of your Babel config to enable transformation.
If you want to leave it as-is, add @babel/plugin-syntax-decorators (https://github.com/babel/babel/tree/main/packages/babel-plugin-syntax-decorators) to the 'plugins' section to enable parsing.
If you already added the plugin for this syntax to your config, it's possible that your config isn't being loaded.
You can re-run Babel with the BABEL_SHOW_CONFIG_FOR environment variable to show the loaded configuration:
npx cross-env BABEL_SHOW_CONFIG_FOR=/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/src/App.jsx <your build command>
See https://babeljs.io/docs/configuration#print-effective-configs for more info.
Plugin: vite:react-babel
File: /home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/src/App.jsx:1:0
1 | @import "tailwindcss";
| ^
2 |
3 | /* Extra large touchscreen keyboard */
10:36:25 AM [vite] Internal server error: /home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/src/App.jsx: Support for the experimental syntax 'decorators' isn't currently enabled (1:1):
> 1 | @import "tailwindcss";
| ^
2 |
3 | /* Extra large touchscreen keyboard */
4 | .kiosk-keyboard .hg-button {
Add @babel/plugin-proposal-decorators (https://github.com/babel/babel/tree/main/packages/babel-plugin-proposal-decorators) to the 'plugins' section of your Babel config to enable transformation.
If you want to leave it as-is, add @babel/plugin-syntax-decorators (https://github.com/babel/babel/tree/main/packages/babel-plugin-syntax-decorators) to the 'plugins' section to enable parsing.
If you already added the plugin for this syntax to your config, it's possible that your config isn't being loaded.
You can re-run Babel with the BABEL_SHOW_CONFIG_FOR environment variable to show the loaded configuration:
npx cross-env BABEL_SHOW_CONFIG_FOR=/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/src/App.jsx <your build command>
See https://babeljs.io/docs/configuration#print-effective-configs for more info.
Plugin: vite:react-babel
File: /home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/src/App.jsx:1:0
1 | @import "tailwindcss";
| ^
2 |
3 | /* Extra large touchscreen keyboard */
at constructor (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/@babel/parser/lib/index.js:367:19)
at JSXParserMixin.raise (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/@babel/parser/lib/index.js:6630:19)
at JSXParserMixin.expectOnePlugin (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/@babel/parser/lib/index.js:6664:18)
at JSXParserMixin.parseDecorator (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/@babel/parser/lib/index.js:12957:10)
at JSXParserMixin.parseDecorators (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/@babel/parser/lib/index.js:12942:28)
at JSXParserMixin.parseStatementLike (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/@babel/parser/lib/index.js:12774:25)
at JSXParserMixin.parseModuleItem (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/@babel/parser/lib/index.js:12753:17)
at JSXParserMixin.parseBlockOrModuleBlockBody (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/@babel/parser/lib/index.js:13325:36)
at JSXParserMixin.parseBlockBody (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/@babel/parser/lib/index.js:13318:10)
at JSXParserMixin.parseProgram (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/@babel/parser/lib/index.js:12634:10)
at JSXParserMixin.parseTopLevel (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/@babel/parser/lib/index.js:12624:25)
at JSXParserMixin.parse (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/@babel/parser/lib/index.js:14501:10)
at parse (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/@babel/parser/lib/index.js:14535:38)
at parser (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/@babel/core/lib/parser/index.js:41:34)
at parser.next (<anonymous>)
at normalizeFile (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/@babel/core/lib/transformation/normalize-file.js:64:37)
at normalizeFile.next (<anonymous>)
at run (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/@babel/core/lib/transformation/index.js:22:50)
at run.next (<anonymous>)
at transform (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/@babel/core/lib/transform.js:22:33)
at transform.next (<anonymous>)
at step (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/gensync/index.js:261:32)
at /home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/gensync/index.js:273:13
at async.call.result.err.err (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/gensync/index.js:223:11)
at /home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/gensync/index.js:189:28
at /home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/@babel/core/lib/gensync-utils/async.js:67:7
at /home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/gensync/index.js:113:33
at step (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/gensync/index.js:287:14)
at /home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/gensync/index.js:273:13
at async.call.result.err.err (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/gensync/index.js:223:11)
10:36:28 AM [vite] Internal server error: /home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/src/App.jsx: Support for the experimental syntax 'decorators' isn't currently enabled (1:1):
> 1 | @import "tailwindcss";
| ^
2 |
3 | /* Extra large touchscreen keyboard */
4 | .kiosk-keyboard .hg-button {
Add @babel/plugin-proposal-decorators (https://github.com/babel/babel/tree/main/packages/babel-plugin-proposal-decorators) to the 'plugins' section of your Babel config to enable transformation.
If you want to leave it as-is, add @babel/plugin-syntax-decorators (https://github.com/babel/babel/tree/main/packages/babel-plugin-syntax-decorators) to the 'plugins' section to enable parsing.
If you already added the plugin for this syntax to your config, it's possible that your config isn't being loaded.
You can re-run Babel with the BABEL_SHOW_CONFIG_FOR environment variable to show the loaded configuration:
npx cross-env BABEL_SHOW_CONFIG_FOR=/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/src/App.jsx <your build command>
See https://babeljs.io/docs/configuration#print-effective-configs for more info.
Plugin: vite:react-babel
File: /home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/src/App.jsx:1:0
1 | @import "tailwindcss";
| ^
2 |
3 | /* Extra large touchscreen keyboard */
at constructor (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/@babel/parser/lib/index.js:367:19)
at JSXParserMixin.raise (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/@babel/parser/lib/index.js:6630:19)
at JSXParserMixin.expectOnePlugin (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/@babel/parser/lib/index.js:6664:18)
at JSXParserMixin.parseDecorator (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/@babel/parser/lib/index.js:12957:10)
at JSXParserMixin.parseDecorators (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/@babel/parser/lib/index.js:12942:28)
at JSXParserMixin.parseStatementLike (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/@babel/parser/lib/index.js:12774:25)
at JSXParserMixin.parseModuleItem (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/@babel/parser/lib/index.js:12753:17)
at JSXParserMixin.parseBlockOrModuleBlockBody (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/@babel/parser/lib/index.js:13325:36)
at JSXParserMixin.parseBlockBody (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/@babel/parser/lib/index.js:13318:10)
at JSXParserMixin.parseProgram (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/@babel/parser/lib/index.js:12634:10)
at JSXParserMixin.parseTopLevel (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/@babel/parser/lib/index.js:12624:25)
at JSXParserMixin.parse (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/@babel/parser/lib/index.js:14501:10)
at parse (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/@babel/parser/lib/index.js:14535:38)
at parser (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/@babel/core/lib/parser/index.js:41:34)
at parser.next (<anonymous>)
at normalizeFile (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/@babel/core/lib/transformation/normalize-file.js:64:37)
at normalizeFile.next (<anonymous>)
at run (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/@babel/core/lib/transformation/index.js:22:50)
at run.next (<anonymous>)
at transform (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/@babel/core/lib/transform.js:22:33)
at transform.next (<anonymous>)
at step (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/gensync/index.js:261:32)
at /home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/gensync/index.js:273:13
at async.call.result.err.err (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/gensync/index.js:223:11)
at /home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/gensync/index.js:189:28
at /home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/@babel/core/lib/gensync-utils/async.js:67:7
at /home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/gensync/index.js:113:33
at step (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/gensync/index.js:287:14)
at /home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/gensync/index.js:273:13
at async.call.result.err.err (/home/mai/documents/school/capstone/vitallink-BS/vitallink/frontend/kiosk/node_modules/gensync/index.js:223:11)
10:38:52 AM [vite] (client) hmr update /src/index.css
10:43:47 AM [vite] (client) hmr update /src/App.jsx, /src/index.css
10:46:50 AM [vite] (client) hmr update /src/index.css
10:47:14 AM [vite] (client) hmr update /src/index.css

1
vitallink/logs/kiosk.pid Normal file
View File

@ -0,0 +1 @@
62616

View File

@ -1,133 +0,0 @@
⚠️ Bleak not installed. Real wristbands disabled. Install with: pip install bleak
✓ Loaded configuration from wristband_config.yaml
================================================================================
VitalLink System Initialization
================================================================================
✓ Backend is running at http://localhost:8000
Added simulated band MOCK-SIM1 (stable)
Added simulated band MOCK-SIM2 (mild_anxiety)
Added simulated band MOCK-SIM3 (deteriorating)
Added simulated band MOCK-SIM4 (sepsis)
================================================================================
WRISTBAND INVENTORY
================================================================================
🟢 MOCK-SIM1 | AVAILABLE
🟢 MOCK-SIM2 | AVAILABLE
🟢 MOCK-SIM3 | AVAILABLE
🟢 MOCK-SIM4 | AVAILABLE
================================================================================
Total: 4 | Real: 0 | Simulated: 4 | Active: 0
================================================================================
================================================================================
VitalLink System Running
================================================================================
✓ Monitoring for new patients from kiosk check-ins
✓ Auto-assigning wristbands (prefer real: False)
Press Ctrl+C to stop
================================================================================
🔍 Monitoring for new patient check-ins...
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
[Status] Active: 0 monitoring | Available: 4 bands | Real: 0 | Sim: 4
🆕 New patient detected: P100001 (FIRST LAST)
✓ MOCK-SIM1 assigned to patient P100001
✓ Assigned MOCK-SIM1 (simulated)
🟢 Starting simulated wristband MOCK-SIM1 (stable)
[Status] Active: 1 monitoring | Available: 3 bands | Real: 0 | Sim: 4
[Status] Active: 1 monitoring | Available: 3 bands | Real: 0 | Sim: 4
[Status] Active: 1 monitoring | Available: 3 bands | Real: 0 | Sim: 4
[Status] Active: 1 monitoring | Available: 3 bands | Real: 0 | Sim: 4
[Status] Active: 1 monitoring | Available: 3 bands | Real: 0 | Sim: 4
[Status] Active: 1 monitoring | Available: 3 bands | Real: 0 | Sim: 4
[Status] Active: 1 monitoring | Available: 3 bands | Real: 0 | Sim: 4
[Status] Active: 1 monitoring | Available: 3 bands | Real: 0 | Sim: 4
[Status] Active: 1 monitoring | Available: 3 bands | Real: 0 | Sim: 4
[Status] Active: 1 monitoring | Available: 3 bands | Real: 0 | Sim: 4
[Status] Active: 1 monitoring | Available: 3 bands | Real: 0 | Sim: 4
[Status] Active: 1 monitoring | Available: 3 bands | Real: 0 | Sim: 4
[Status] Active: 1 monitoring | Available: 3 bands | Real: 0 | Sim: 4
[Status] Active: 1 monitoring | Available: 3 bands | Real: 0 | Sim: 4
[Status] Active: 1 monitoring | Available: 3 bands | Real: 0 | Sim: 4
[Status] Active: 1 monitoring | Available: 3 bands | Real: 0 | Sim: 4
[Status] Active: 1 monitoring | Available: 3 bands | Real: 0 | Sim: 4
[Status] Active: 1 monitoring | Available: 3 bands | Real: 0 | Sim: 4
[Status] Active: 1 monitoring | Available: 3 bands | Real: 0 | Sim: 4
[Status] Active: 1 monitoring | Available: 3 bands | Real: 0 | Sim: 4
[Status] Active: 1 monitoring | Available: 3 bands | Real: 0 | Sim: 4
[Status] Active: 1 monitoring | Available: 3 bands | Real: 0 | Sim: 4
[Status] Active: 1 monitoring | Available: 3 bands | Real: 0 | Sim: 4
[Status] Active: 1 monitoring | Available: 3 bands | Real: 0 | Sim: 4
[Status] Active: 1 monitoring | Available: 3 bands | Real: 0 | Sim: 4
[Status] Active: 1 monitoring | Available: 3 bands | Real: 0 | Sim: 4
[Status] Active: 1 monitoring | Available: 3 bands | Real: 0 | Sim: 4
[Status] Active: 1 monitoring | Available: 3 bands | Real: 0 | Sim: 4
[Status] Active: 1 monitoring | Available: 3 bands | Real: 0 | Sim: 4
[Status] Active: 1 monitoring | Available: 3 bands | Real: 0 | Sim: 4
[Status] Active: 1 monitoring | Available: 3 bands | Real: 0 | Sim: 4
[Status] Active: 1 monitoring | Available: 3 bands | Real: 0 | Sim: 4
[Status] Active: 1 monitoring | Available: 3 bands | Real: 0 | Sim: 4
[Status] Active: 1 monitoring | Available: 3 bands | Real: 0 | Sim: 4
[Status] Active: 1 monitoring | Available: 3 bands | Real: 0 | Sim: 4
[Status] Active: 1 monitoring | Available: 3 bands | Real: 0 | Sim: 4
[Status] Active: 1 monitoring | Available: 3 bands | Real: 0 | Sim: 4
[Status] Active: 1 monitoring | Available: 3 bands | Real: 0 | Sim: 4
[Status] Active: 1 monitoring | Available: 3 bands | Real: 0 | Sim: 4
[Status] Active: 1 monitoring | Available: 3 bands | Real: 0 | Sim: 4
[Status] Active: 1 monitoring | Available: 3 bands | Real: 0 | Sim: 4

View File

@ -0,0 +1 @@
62574

BIN
vitallink/vitallink.db Normal file

Binary file not shown.