updated with database backend + graph view
This commit is contained in:
Binary file not shown.
@@ -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()
|
||||
@@ -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
|
||||
# ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user