""" VitalLink Backend API FastAPI server for managing patients, wristbands, and real-time data """ from fastapi import FastAPI, WebSocket, HTTPException from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel from typing import List, Dict, Optional from datetime import datetime, timedelta import asyncio import json from collections import defaultdict app = FastAPI(title="VitalLink API", version="1.0.0") # CORS middleware for frontend app.add_middleware( CORSMiddleware, allow_origins=["*"], # In production, specify your frontend domain allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # ============================================================================ # DATA MODELS # ============================================================================ class PatientCheckIn(BaseModel): firstName: str lastName: str dob: str symptoms: List[str] severity: str class Patient(BaseModel): patient_id: str band_id: str first_name: str last_name: str dob: str symptoms: List[str] severity: str check_in_time: datetime current_tier: str = "NORMAL" last_vitals: Optional[dict] = None is_active: bool = True class VitalsData(BaseModel): band_id: str patient_id: str timestamp: float tier: str hr_bpm: int spo2: int temp_c: float activity: float flags: List[str] seq: int class QueuePosition(BaseModel): patient_id: str band_id: str name: str tier: str priority_score: float wait_time_minutes: int last_hr: int last_spo2: int last_temp: float # ============================================================================ # IN-MEMORY STORAGE (Replace with database in production) # ============================================================================ patients_db: Dict[str, Patient] = {} vitals_history: Dict[str, List[VitalsData]] = defaultdict(list) available_bands = [ f"VitalLink-{hex(i)[2:].upper().zfill(4)}" for i in range(0x1000, 0x2000) ] active_websockets: List[WebSocket] = [] # ============================================================================ # PRIORITY ALGORITHM # ============================================================================ def calculate_priority_score(patient: Patient) -> float: """ Calculate dynamic priority score for queue ordering Higher score = higher priority Factors: - Tier (Emergency=100, Alert=50, Normal=0) - Vital sign trends (worsening = higher) - Wait time (exponential increase after threshold) - Initial severity """ score = 0.0 # Tier contribution (largest factor) tier_scores = {"EMERGENCY": 100, "ALERT": 50, "NORMAL": 0} score += tier_scores.get(patient.current_tier, 0) # Wait time contribution (increases exponentially after 30 min) wait_minutes = (datetime.now() - patient.check_in_time).total_seconds() / 60 if wait_minutes > 30: score += (wait_minutes - 30) * 0.5 # 0.5 points per minute over 30 elif wait_minutes > 60: score += (wait_minutes - 60) * 1.0 # Accelerate after 1 hour # Initial severity contribution severity_scores = {"severe": 20, "moderate": 10, "mild": 5} score += severity_scores.get(patient.severity, 0) # Vital signs contribution (if available) if patient.last_vitals: hr = patient.last_vitals.get("hr_bpm", 75) spo2 = patient.last_vitals.get("spo2", 98) temp = patient.last_vitals.get("temp_c", 37.0) # Abnormal HR if hr > 110 or hr < 50: score += 10 if hr > 140 or hr < 40: score += 30 # Low SpO2 (critical) if spo2 < 92: score += 15 if spo2 < 88: score += 40 # Fever if temp > 38.5: score += 15 if temp > 39.5: score += 25 return score # ============================================================================ # API ENDPOINTS # ============================================================================ @app.post("/api/checkin") async def check_in_patient(data: PatientCheckIn): """Register a new patient and assign wristband""" if not available_bands: raise HTTPException(status_code=503, detail="No wristbands available") # Assign IDs patient_id = f"P{len(patients_db) + 100001}" band_id = available_bands.pop(0) # Create patient record patient = Patient( patient_id=patient_id, band_id=band_id, first_name=data.firstName, last_name=data.lastName, dob=data.dob, symptoms=data.symptoms, severity=data.severity, check_in_time=datetime.now(), current_tier="NORMAL", ) patients_db[patient_id] = patient # Notify connected clients await broadcast_update({"type": "patient_added", "patient": patient.dict()}) return { "patient_id": patient_id, "band_id": band_id, "message": "Check-in successful", } @app.post("/api/vitals") async def receive_vitals(data: VitalsData): """Receive vitals data from base station""" patient_id = data.patient_id if patient_id not in patients_db: raise HTTPException(status_code=404, detail="Patient not found") # Update patient record patient = patients_db[patient_id] patient.current_tier = data.tier patient.last_vitals = data.dict() # Store in history (keep last 1000 readings) vitals_history[patient_id].append(data) if len(vitals_history[patient_id]) > 1000: vitals_history[patient_id] = vitals_history[patient_id][-1000:] # Broadcast to connected clients await broadcast_update( {"type": "vitals_update", "patient_id": patient_id, "vitals": data.dict()} ) return {"status": "received"} @app.get("/api/queue") async def get_queue(): """Get prioritized queue of active patients""" active_patients = [p for p in patients_db.values() if p.is_active] # Calculate priority and sort queue = [] for patient in active_patients: priority_score = calculate_priority_score(patient) wait_minutes = int( (datetime.now() - patient.check_in_time).total_seconds() / 60 ) queue.append( QueuePosition( patient_id=patient.patient_id, band_id=patient.band_id, name=f"{patient.first_name} {patient.last_name}", tier=patient.current_tier, priority_score=priority_score, wait_time_minutes=wait_minutes, last_hr=patient.last_vitals.get("hr_bpm", 0) if patient.last_vitals else 0, last_spo2=patient.last_vitals.get("spo2", 0) if patient.last_vitals else 0, last_temp=patient.last_vitals.get("temp_c", 0) if patient.last_vitals else 0, ) ) # Sort by priority (highest first) queue.sort(key=lambda x: x.priority_score, reverse=True) return queue @app.get("/api/patients/{patient_id}") async def get_patient_details(patient_id: str): """Get detailed information about a specific patient""" if patient_id not in patients_db: raise HTTPException(status_code=404, detail="Patient not found") patient = patients_db[patient_id] history = vitals_history.get(patient_id, []) return { "patient": patient.dict(), "vitals_history": [v.dict() for v in history[-50:]], # Last 50 readings "priority_score": calculate_priority_score(patient), } @app.post("/api/patients/{patient_id}/discharge") async def discharge_patient(patient_id: str): """Discharge a patient and return wristband to pool""" if patient_id not in patients_db: raise HTTPException(status_code=404, detail="Patient not found") patient = patients_db[patient_id] patient.is_active = False # Return band to pool available_bands.append(patient.band_id) # Notify clients await broadcast_update({"type": "patient_discharged", "patient_id": patient_id}) return {"message": "Patient discharged", "band_returned": patient.band_id} @app.get("/api/stats") async def get_statistics(): """Get overall ER statistics""" active_patients = [p for p in patients_db.values() if p.is_active] tier_counts = {"EMERGENCY": 0, "ALERT": 0, "NORMAL": 0} for patient in active_patients: tier_counts[patient.current_tier] += 1 total_vitals = sum(len(v) for v in vitals_history.values()) avg_wait = 0 if active_patients: wait_times = [ (datetime.now() - p.check_in_time).total_seconds() / 60 for p in active_patients ] avg_wait = sum(wait_times) / len(wait_times) return { "total_patients": len(patients_db), "active_patients": len(active_patients), "tier_breakdown": tier_counts, "available_bands": len(available_bands), "total_vitals_received": total_vitals, "average_wait_minutes": round(avg_wait, 1), } # ============================================================================ # WEBSOCKET FOR REAL-TIME UPDATES # ============================================================================ @app.websocket("/ws") async def websocket_endpoint(websocket: WebSocket): """WebSocket connection for real-time updates to frontend""" await websocket.accept() active_websockets.append(websocket) # Send initial data await websocket.send_json( {"type": "connected", "message": "Connected to VitalLink server"} ) try: while True: # Keep connection alive and listen for client messages data = await websocket.receive_text() # Could handle client commands here except: active_websockets.remove(websocket) async def broadcast_update(message: dict): """Broadcast update to all connected WebSocket clients""" disconnected = [] for websocket in active_websockets: try: await websocket.send_json(message) except: disconnected.append(websocket) # Remove disconnected clients for ws in disconnected: active_websockets.remove(ws) # ============================================================================ # STARTUP / SHUTDOWN # ============================================================================ @app.on_event("startup") async def startup_event(): print("=" * 80) print("VitalLink Backend API Started") print("=" * 80) print("API Documentation: http://localhost:8000/docs") print("WebSocket Endpoint: ws://localhost:8000/ws") print("=" * 80) # ============================================================================ # INTEGRATION WITH SIMULATOR # ============================================================================ async def simulator_integration_task(): """ Background task to integrate with wristband simulator In production, this receives data from actual base station """ # This would be replaced with actual base station connection # For now, shows how to integrate the simulator pass # Run with: uvicorn vitalink_backend:app --reload # Then access at http://localhost:8000