link start!
This commit is contained in:
commit
c2906611fd
621
install_vitallink.sh
Executable file
621
install_vitallink.sh
Executable file
@ -0,0 +1,621 @@
|
||||
#!/bin/bash
|
||||
|
||||
# VitalLink Automatic Installer for Arch Linux
|
||||
# This script sets up the entire project structure
|
||||
# You'll still need to copy the Python/React code from Claude artifacts
|
||||
|
||||
set -e
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
PROJECT_ROOT="$PWD/vitallink"
|
||||
|
||||
clear
|
||||
echo -e "${CYAN}"
|
||||
cat <<"EOF"
|
||||
╔══════════════════════════════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║ ██╗ ██╗██╗████████╗ █████╗ ██╗ ██╗ ██╗███╗ ██╗██╗ ██╗ ║
|
||||
║ ██║ ██║██║╚══██╔══╝██╔══██╗██║ ██║ ██║████╗ ██║██║ ██╔╝ ║
|
||||
║ ██║ ██║██║ ██║ ███████║██║ ██║ ██║██╔██╗ ██║█████╔╝ ║
|
||||
║ ╚██╗ ██╔╝██║ ██║ ██╔══██║██║ ██║ ██║██║╚██╗██║██╔═██╗ ║
|
||||
║ ╚████╔╝ ██║ ██║ ██║ ██║███████╗███████╗██║██║ ╚████║██║ ██╗ ║
|
||||
║ ╚═══╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝ ║
|
||||
║ ║
|
||||
║ Emergency Room Patient Monitoring System ║
|
||||
║ Automatic Installer v1.0 ║
|
||||
║ ║
|
||||
╚══════════════════════════════════════════════════════════════════════════╝
|
||||
EOF
|
||||
echo -e "${NC}"
|
||||
|
||||
echo -e "${YELLOW}This script will:${NC}"
|
||||
echo " 1. Create project structure in $PROJECT_ROOT"
|
||||
echo " 2. Set up Python virtual environment"
|
||||
echo " 3. Install all dependencies"
|
||||
echo " 4. Create startup/stop scripts"
|
||||
echo " 5. Generate placeholder files for you to fill"
|
||||
echo ""
|
||||
echo -e "${CYAN}Note: You'll need to copy Python/React code from Claude artifacts after this.${NC}"
|
||||
echo ""
|
||||
read -p "Continue? (y/n) " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo -e "${RED}Installation cancelled.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if directory exists
|
||||
if [ -d "$PROJECT_ROOT" ]; then
|
||||
echo -e "${YELLOW}Warning: $PROJECT_ROOT already exists.${NC}"
|
||||
read -p "Delete and recreate? (y/n) " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
rm -rf "$PROJECT_ROOT"
|
||||
echo -e "${GREEN}✓ Removed existing directory${NC}"
|
||||
else
|
||||
echo -e "${RED}Installation cancelled.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${CYAN}STEP 1: Creating Directory Structure${NC}"
|
||||
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════════${NC}"
|
||||
|
||||
mkdir -p "$PROJECT_ROOT"/{backend,simulator,frontend/{kiosk,dashboard},tests,docs,logs}
|
||||
echo -e "${GREEN}✓ Created project directories${NC}"
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
echo ""
|
||||
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${CYAN}STEP 2: Setting up Python Virtual Environment${NC}"
|
||||
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════════${NC}"
|
||||
|
||||
python -m venv venv
|
||||
source venv/bin/activate
|
||||
echo -e "${GREEN}✓ Virtual environment created${NC}"
|
||||
|
||||
echo ""
|
||||
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${CYAN}STEP 3: Creating requirements.txt${NC}"
|
||||
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════════${NC}"
|
||||
|
||||
cat >requirements.txt <<'EOF'
|
||||
# Backend API
|
||||
fastapi==0.104.1
|
||||
uvicorn[standard]==0.24.0
|
||||
websockets==12.0
|
||||
pydantic==2.5.0
|
||||
python-multipart==0.0.6
|
||||
|
||||
# HTTP client for simulator integration
|
||||
aiohttp==3.9.1
|
||||
requests==2.31.0
|
||||
|
||||
# Testing
|
||||
pytest==7.4.3
|
||||
pytest-asyncio==0.21.1
|
||||
|
||||
# Utilities
|
||||
python-dateutil==2.8.2
|
||||
EOF
|
||||
|
||||
echo -e "${GREEN}✓ requirements.txt created${NC}"
|
||||
|
||||
echo ""
|
||||
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${CYAN}STEP 4: Installing Python Dependencies${NC}"
|
||||
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════════${NC}"
|
||||
|
||||
pip install --upgrade pip -q
|
||||
pip install -r requirements.txt -q
|
||||
echo -e "${GREEN}✓ All Python packages installed${NC}"
|
||||
|
||||
echo ""
|
||||
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${CYAN}STEP 5: Creating Project Files${NC}"
|
||||
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════════${NC}"
|
||||
|
||||
# Backend server placeholder
|
||||
cat >backend/server.py <<'EOF'
|
||||
"""
|
||||
VitalLink Backend Server
|
||||
TODO: Copy the complete FastAPI code from Claude artifact:
|
||||
"VitalLink Backend API (FastAPI)"
|
||||
"""
|
||||
|
||||
print("Backend server placeholder - please copy the actual code from Claude artifacts")
|
||||
EOF
|
||||
echo -e "${GREEN}✓ backend/server.py created (placeholder)${NC}"
|
||||
|
||||
# Simulator placeholder
|
||||
cat >simulator/wristband_simulator.py <<'EOF'
|
||||
"""
|
||||
VitalLink Wristband Simulator
|
||||
TODO: Copy the complete simulator code from Claude artifact:
|
||||
"VitalLink Wristband Simulator & Base Station"
|
||||
"""
|
||||
|
||||
print("Simulator placeholder - please copy the actual code from Claude artifacts")
|
||||
EOF
|
||||
echo -e "${GREEN}✓ simulator/wristband_simulator.py created (placeholder)${NC}"
|
||||
|
||||
# Test suite placeholder
|
||||
cat >tests/test_suite.py <<'EOF'
|
||||
"""
|
||||
VitalLink Test Suite
|
||||
TODO: Copy the complete test suite code from Claude artifact:
|
||||
"VitalLink Complete Test Suite"
|
||||
"""
|
||||
|
||||
print("Test suite placeholder - please copy the actual code from Claude artifacts")
|
||||
EOF
|
||||
echo -e "${GREEN}✓ tests/test_suite.py created (placeholder)${NC}"
|
||||
|
||||
# Dashboard HTML
|
||||
cat >frontend/dashboard/index.html <<'EOF'
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>VitalLink Staff Dashboard</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
<script type="text/babel">
|
||||
/*
|
||||
* TODO: Copy the ENTIRE React component code from Claude artifact:
|
||||
* "VitalLink Staff Monitoring Dashboard"
|
||||
*
|
||||
* Replace "export default StaffDashboard;" with:
|
||||
* const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
* root.render(<StaffDashboard />);
|
||||
*/
|
||||
|
||||
// Placeholder
|
||||
const Placeholder = () => (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100vh',
|
||||
backgroundColor: '#f3f4f6',
|
||||
fontFamily: 'system-ui, -apple-system, sans-serif'
|
||||
}}>
|
||||
<div style={{
|
||||
backgroundColor: 'white',
|
||||
padding: '3rem',
|
||||
borderRadius: '1rem',
|
||||
boxShadow: '0 10px 25px rgba(0,0,0,0.1)',
|
||||
maxWidth: '600px',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
<h1 style={{color: '#1e40af', fontSize: '2rem', marginBottom: '1rem'}}>
|
||||
VitalLink Staff Dashboard
|
||||
</h1>
|
||||
<p style={{color: '#6b7280', marginBottom: '1.5rem'}}>
|
||||
Please copy the React component code from Claude artifacts.
|
||||
</p>
|
||||
<div style={{
|
||||
backgroundColor: '#fef3c7',
|
||||
padding: '1rem',
|
||||
borderRadius: '0.5rem',
|
||||
border: '1px solid #fbbf24'
|
||||
}}>
|
||||
<p style={{color: '#92400e', fontSize: '0.875rem'}}>
|
||||
<strong>Instructions:</strong><br/>
|
||||
1. Open this file in a text editor<br/>
|
||||
2. Find the TODO comment above<br/>
|
||||
3. Copy the complete StaffDashboard component<br/>
|
||||
4. Save and refresh
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(<Placeholder />);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
EOF
|
||||
echo -e "${GREEN}✓ frontend/dashboard/index.html created (template)${NC}"
|
||||
|
||||
# Kiosk HTML
|
||||
cat >frontend/kiosk/index.html <<'EOF'
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>VitalLink Check-in Kiosk</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
<script type="text/babel">
|
||||
/*
|
||||
* TODO: Copy the ENTIRE React component code from Claude artifact:
|
||||
* "VitalLink Check-in Kiosk"
|
||||
*
|
||||
* Replace "export default CheckInKiosk;" with:
|
||||
* const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
* root.render(<CheckInKiosk />);
|
||||
*/
|
||||
|
||||
// Placeholder (same as dashboard)
|
||||
const Placeholder = () => (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100vh',
|
||||
backgroundColor: '#f3f4f6',
|
||||
fontFamily: 'system-ui, -apple-system, sans-serif'
|
||||
}}>
|
||||
<div style={{
|
||||
backgroundColor: 'white',
|
||||
padding: '3rem',
|
||||
borderRadius: '1rem',
|
||||
boxShadow: '0 10px 25px rgba(0,0,0,0.1)',
|
||||
maxWidth: '600px',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
<h1 style={{color: '#2563eb', fontSize: '2rem', marginBottom: '1rem'}}>
|
||||
VitalLink Check-in Kiosk
|
||||
</h1>
|
||||
<p style={{color: '#6b7280', marginBottom: '1.5rem'}}>
|
||||
Please copy the React component code from Claude artifacts.
|
||||
</p>
|
||||
<div style={{
|
||||
backgroundColor: '#dbeafe',
|
||||
padding: '1rem',
|
||||
borderRadius: '0.5rem',
|
||||
border: '1px solid #3b82f6'
|
||||
}}>
|
||||
<p style={{color: '#1e40af', fontSize: '0.875rem'}}>
|
||||
<strong>Instructions:</strong><br/>
|
||||
1. Open this file in a text editor<br/>
|
||||
2. Find the TODO comment above<br/>
|
||||
3. Copy the complete CheckInKiosk component<br/>
|
||||
4. Save and refresh
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(<Placeholder />);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
EOF
|
||||
echo -e "${GREEN}✓ frontend/kiosk/index.html created (template)${NC}"
|
||||
|
||||
echo ""
|
||||
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${CYAN}STEP 6: Creating Control Scripts${NC}"
|
||||
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════════${NC}"
|
||||
|
||||
# Start script
|
||||
cat >start.sh <<'STARTSCRIPT'
|
||||
#!/bin/bash
|
||||
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$PROJECT_ROOT"
|
||||
source venv/bin/activate
|
||||
|
||||
echo "╔═══════════════════════════════════════════════════════════════════════╗"
|
||||
echo "║ Starting VitalLink System ║"
|
||||
echo "╚═══════════════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
mkdir -p logs
|
||||
|
||||
echo "Starting backend server..."
|
||||
python backend/server.py > logs/backend.log 2>&1 &
|
||||
echo $! > logs/backend.pid
|
||||
echo "✓ Backend started (PID: $(cat logs/backend.pid))"
|
||||
|
||||
sleep 3
|
||||
|
||||
echo "Starting wristband simulator..."
|
||||
python simulator/wristband_simulator.py > logs/simulator.log 2>&1 &
|
||||
echo $! > logs/simulator.pid
|
||||
echo "✓ Simulator started (PID: $(cat logs/simulator.pid))"
|
||||
|
||||
echo ""
|
||||
echo "═══════════════════════════════════════════════════════════════════════"
|
||||
echo "✅ VitalLink System Running!"
|
||||
echo "═══════════════════════════════════════════════════════════════════════"
|
||||
echo ""
|
||||
echo "📊 Access Points:"
|
||||
echo " • API Docs: http://localhost:8000/docs"
|
||||
echo " • API Stats: http://localhost:8000/api/stats"
|
||||
echo " • WebSocket: ws://localhost:8000/ws"
|
||||
echo " • Staff Dashboard: file://$PROJECT_ROOT/frontend/dashboard/index.html"
|
||||
echo " • Check-in Kiosk: file://$PROJECT_ROOT/frontend/kiosk/index.html"
|
||||
echo ""
|
||||
echo "📝 View Logs:"
|
||||
echo " • Backend: tail -f logs/backend.log"
|
||||
echo " • Simulator: tail -f logs/simulator.log"
|
||||
echo ""
|
||||
echo "🛑 Stop System:"
|
||||
echo " • Run: ./stop.sh"
|
||||
echo ""
|
||||
echo "═══════════════════════════════════════════════════════════════════════"
|
||||
STARTSCRIPT
|
||||
|
||||
chmod +x start.sh
|
||||
echo -e "${GREEN}✓ start.sh created${NC}"
|
||||
|
||||
# Stop script
|
||||
cat >stop.sh <<'STOPSCRIPT'
|
||||
#!/bin/bash
|
||||
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
echo "Stopping VitalLink system..."
|
||||
|
||||
if [ -f logs/backend.pid ]; then
|
||||
kill $(cat logs/backend.pid) 2>/dev/null && echo "✓ Backend stopped" || echo "Backend not running"
|
||||
rm -f logs/backend.pid
|
||||
fi
|
||||
|
||||
if [ -f logs/simulator.pid ]; then
|
||||
kill $(cat logs/simulator.pid) 2>/dev/null && echo "✓ Simulator stopped" || echo "Simulator not running"
|
||||
rm -f logs/simulator.pid
|
||||
fi
|
||||
|
||||
echo "✓ VitalLink system stopped"
|
||||
STOPSCRIPT
|
||||
|
||||
chmod +x stop.sh
|
||||
echo -e "${GREEN}✓ stop.sh created${NC}"
|
||||
|
||||
# Test script
|
||||
cat >test.sh <<'TESTSCRIPT'
|
||||
#!/bin/bash
|
||||
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$PROJECT_ROOT"
|
||||
source venv/bin/activate
|
||||
|
||||
echo "Running VitalLink Test Suite..."
|
||||
echo ""
|
||||
python tests/test_suite.py
|
||||
TESTSCRIPT
|
||||
|
||||
chmod +x test.sh
|
||||
echo -e "${GREEN}✓ test.sh created${NC}"
|
||||
|
||||
echo ""
|
||||
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${CYAN}STEP 7: Creating Documentation${NC}"
|
||||
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════════${NC}"
|
||||
|
||||
# README
|
||||
cat >README.md <<'EOF'
|
||||
# VitalLink - ER Patient Monitoring System
|
||||
|
||||
Emergency department patient monitoring system using smart wristbands.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Copy Code from Claude Artifacts:**
|
||||
- `backend/server.py` ← Copy "VitalLink Backend API (FastAPI)"
|
||||
- `simulator/wristband_simulator.py` ← Copy "VitalLink Wristband Simulator"
|
||||
- `tests/test_suite.py` ← Copy "VitalLink Complete Test Suite"
|
||||
- `frontend/dashboard/index.html` ← Insert React code from "Staff Dashboard"
|
||||
- `frontend/kiosk/index.html` ← Insert React code from "Check-in Kiosk"
|
||||
|
||||
2. **Test the System:**
|
||||
```bash
|
||||
./test.sh
|
||||
```
|
||||
|
||||
3. **Start the System:**
|
||||
```bash
|
||||
./start.sh
|
||||
```
|
||||
|
||||
4. **Access the Interfaces:**
|
||||
- API Documentation: http://localhost:8000/docs
|
||||
- Staff Dashboard: Open `frontend/dashboard/index.html` in browser
|
||||
- Check-in Kiosk: Open `frontend/kiosk/index.html` in browser
|
||||
|
||||
5. **Stop the System:**
|
||||
```bash
|
||||
./stop.sh
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
vitallink/
|
||||
├── backend/ # FastAPI server
|
||||
├── simulator/ # Wristband simulator
|
||||
├── frontend/ # Web interfaces
|
||||
│ ├── dashboard/ # Staff monitoring
|
||||
│ └── kiosk/ # Patient check-in
|
||||
├── tests/ # Test suite
|
||||
├── logs/ # System logs
|
||||
└── venv/ # Python virtual env
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
- `./start.sh` - Start backend + simulator
|
||||
- `./stop.sh` - Stop all services
|
||||
- `./test.sh` - Run test suite
|
||||
- `tail -f logs/backend.log` - View backend logs
|
||||
- `tail -f logs/simulator.log` - View simulator logs
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
./test.sh
|
||||
|
||||
# Test API
|
||||
curl http://localhost:8000/api/stats
|
||||
|
||||
# Create test patient
|
||||
curl -X POST http://localhost:8000/api/checkin \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"firstName":"Test","lastName":"Patient","dob":"1990-01-01","symptoms":["Fever"],"severity":"moderate"}'
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
See `docs/SETUP_GUIDE.md` for detailed instructions.
|
||||
EOF
|
||||
|
||||
echo -e "${GREEN}✓ README.md created${NC}"
|
||||
|
||||
# Setup guide
|
||||
cat >docs/SETUP_GUIDE.md <<'EOF'
|
||||
# Detailed Setup Guide
|
||||
|
||||
## Copying Code from Claude Artifacts
|
||||
|
||||
### 1. Backend Server (backend/server.py)
|
||||
|
||||
Open the artifact titled **"VitalLink Backend API (FastAPI)"** and copy the ENTIRE Python code.
|
||||
|
||||
Paste it into `backend/server.py`.
|
||||
|
||||
### 2. Wristband Simulator (simulator/wristband_simulator.py)
|
||||
|
||||
Open the artifact titled **"VitalLink Wristband Simulator & Base Station"** and copy the ENTIRE Python code.
|
||||
|
||||
Paste it into `simulator/wristband_simulator.py`.
|
||||
|
||||
### 3. Test Suite (tests/test_suite.py)
|
||||
|
||||
Open the artifact titled **"VitalLink Complete Test Suite"** and copy the ENTIRE Python code.
|
||||
|
||||
Paste it into `tests/test_suite.py`.
|
||||
|
||||
### 4. Staff Dashboard (frontend/dashboard/index.html)
|
||||
|
||||
1. Open the artifact titled **"VitalLink Staff Monitoring Dashboard"**
|
||||
2. Copy the ENTIRE React component code
|
||||
3. Open `frontend/dashboard/index.html` in a text editor
|
||||
4. Find the TODO comment section
|
||||
5. Paste the React code there
|
||||
6. Remove the line `export default StaffDashboard;`
|
||||
7. Add these lines at the end:
|
||||
```javascript
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(<StaffDashboard />);
|
||||
```
|
||||
|
||||
### 5. Check-in Kiosk (frontend/kiosk/index.html)
|
||||
|
||||
Follow the same process as the dashboard, but use the **"VitalLink Check-in Kiosk"** artifact.
|
||||
|
||||
## Verification
|
||||
|
||||
After copying all code:
|
||||
|
||||
```bash
|
||||
# Activate virtual environment
|
||||
source venv/bin/activate
|
||||
|
||||
# Run tests
|
||||
./test.sh
|
||||
|
||||
# Start system
|
||||
./start.sh
|
||||
|
||||
# In another terminal, test API
|
||||
curl http://localhost:8000/api/stats
|
||||
```
|
||||
|
||||
If everything works, you should see JSON output from the API.
|
||||
EOF
|
||||
|
||||
echo -e "${GREEN}✓ docs/SETUP_GUIDE.md created${NC}"
|
||||
|
||||
# .gitignore
|
||||
cat >.gitignore <<'EOF'
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
venv/
|
||||
logs/
|
||||
*.log
|
||||
*.pid
|
||||
.DS_Store
|
||||
EOF
|
||||
|
||||
echo -e "${GREEN}✓ .gitignore created${NC}"
|
||||
|
||||
echo ""
|
||||
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${CYAN}STEP 8: Final Setup${NC}"
|
||||
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════════${NC}"
|
||||
|
||||
# Create empty log directory
|
||||
touch logs/.gitkeep
|
||||
|
||||
# Deactivate venv for clean state
|
||||
deactivate 2>/dev/null || true
|
||||
|
||||
echo -e "${GREEN}✓ Project setup complete!${NC}"
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}╔═══════════════════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${GREEN}║ ✅ Installation Complete! ║${NC}"
|
||||
echo -e "${GREEN}╚═══════════════════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}📁 Project Location:${NC} $PROJECT_ROOT"
|
||||
echo ""
|
||||
echo -e "${YELLOW}🔔 IMPORTANT - Next Steps:${NC}"
|
||||
echo ""
|
||||
echo -e "${CYAN}1. Copy Code from Claude Artifacts:${NC}"
|
||||
echo " Open each artifact in Claude and copy the code to these files:"
|
||||
echo " • backend/server.py"
|
||||
echo " • simulator/wristband_simulator.py"
|
||||
echo " • tests/test_suite.py"
|
||||
echo " • frontend/dashboard/index.html (insert React code)"
|
||||
echo " • frontend/kiosk/index.html (insert React code)"
|
||||
echo ""
|
||||
echo -e "${CYAN}2. Navigate to project:${NC}"
|
||||
echo " cd $PROJECT_ROOT"
|
||||
echo ""
|
||||
echo -e "${CYAN}3. Run tests:${NC}"
|
||||
echo " ./test.sh"
|
||||
echo ""
|
||||
echo -e "${CYAN}4. Start the system:${NC}"
|
||||
echo " ./start.sh"
|
||||
echo ""
|
||||
echo -e "${CYAN}5. Open interfaces:${NC}"
|
||||
echo " xdg-open frontend/dashboard/index.html"
|
||||
echo " xdg-open frontend/kiosk/index.html"
|
||||
echo ""
|
||||
echo -e "${GREEN}📖 Full documentation:${NC} cat docs/SETUP_GUIDE.md"
|
||||
echo ""
|
||||
echo -e "${BLUE}═══════════════════════════════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
395
vitallink/backend/server.py
Normal file
395
vitallink/backend/server.py
Normal file
@ -0,0 +1,395 @@
|
||||
"""
|
||||
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
|
||||
368
vitallink/frontend/dashboard/index.html
Normal file
368
vitallink/frontend/dashboard/index.html
Normal file
@ -0,0 +1,368 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Activity, AlertCircle, Clock, Users, Bell, Heart, Thermometer, Wind, CheckCircle, UserX } from 'lucide-react';
|
||||
|
||||
const StaffDashboard = () => {
|
||||
const [patients, setPatients] = useState([]);
|
||||
const [stats, setStats] = useState({
|
||||
total_patients: 0,
|
||||
active_patients: 0,
|
||||
tier_breakdown: { EMERGENCY: 0, ALERT: 0, NORMAL: 0 },
|
||||
average_wait_minutes: 0
|
||||
});
|
||||
const [selectedPatient, setSelectedPatient] = useState(null);
|
||||
const [filter, setFilter] = useState('all');
|
||||
|
||||
useEffect(() => {
|
||||
const generateMockPatients = () => {
|
||||
const mockPatients = [
|
||||
{
|
||||
patient_id: 'P100001',
|
||||
band_id: 'VitalLink-A1B2',
|
||||
name: 'John Smith',
|
||||
tier: 'NORMAL',
|
||||
priority_score: 15.2,
|
||||
wait_time_minutes: 22,
|
||||
last_hr: 76,
|
||||
last_spo2: 98,
|
||||
last_temp: 36.8,
|
||||
symptoms: ['Chest Pain', 'Nausea']
|
||||
},
|
||||
{
|
||||
patient_id: 'P100002',
|
||||
band_id: 'VitalLink-C3D4',
|
||||
name: 'Sarah Johnson',
|
||||
tier: 'ALERT',
|
||||
priority_score: 68.5,
|
||||
wait_time_minutes: 45,
|
||||
last_hr: 118,
|
||||
last_spo2: 93,
|
||||
last_temp: 38.4,
|
||||
symptoms: ['Fever', 'Difficulty Breathing']
|
||||
},
|
||||
{
|
||||
patient_id: 'P100003',
|
||||
band_id: 'VitalLink-E5F6',
|
||||
name: 'Michael Chen',
|
||||
tier: 'EMERGENCY',
|
||||
priority_score: 142.8,
|
||||
wait_time_minutes: 8,
|
||||
last_hr: 148,
|
||||
last_spo2: 86,
|
||||
last_temp: 39.7,
|
||||
symptoms: ['Severe Headache', 'Chest Pain']
|
||||
},
|
||||
{
|
||||
patient_id: 'P100004',
|
||||
band_id: 'VitalLink-G7H8',
|
||||
name: 'Emily Davis',
|
||||
tier: 'NORMAL',
|
||||
priority_score: 18.0,
|
||||
wait_time_minutes: 35,
|
||||
last_hr: 82,
|
||||
last_spo2: 97,
|
||||
last_temp: 37.1,
|
||||
symptoms: ['Abdominal Pain']
|
||||
},
|
||||
{
|
||||
patient_id: 'P100005',
|
||||
band_id: 'VitalLink-I9J0',
|
||||
name: 'Robert Williams',
|
||||
tier: 'ALERT',
|
||||
priority_score: 72.3,
|
||||
wait_time_minutes: 52,
|
||||
last_hr: 124,
|
||||
last_spo2: 91,
|
||||
last_temp: 38.8,
|
||||
symptoms: ['Fever', 'Dizziness']
|
||||
}
|
||||
];
|
||||
|
||||
setPatients(mockPatients);
|
||||
setStats({
|
||||
total_patients: 5,
|
||||
active_patients: 5,
|
||||
tier_breakdown: { EMERGENCY: 1, ALERT: 2, NORMAL: 2 },
|
||||
average_wait_minutes: 32.4
|
||||
});
|
||||
};
|
||||
|
||||
generateMockPatients();
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setPatients(prev => prev.map(p => ({
|
||||
...p,
|
||||
wait_time_minutes: p.wait_time_minutes + 1,
|
||||
last_hr: Math.max(40, Math.min(180, p.last_hr + Math.floor(Math.random() * 5) - 2)),
|
||||
last_spo2: Math.max(70, Math.min(100, p.last_spo2 + Math.floor(Math.random() * 3) - 1)),
|
||||
last_temp: Math.max(35, Math.min(41, p.last_temp + (Math.random() * 0.2 - 0.1)))
|
||||
})));
|
||||
}, 3000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const getTierColor = (tier) => {
|
||||
switch(tier) {
|
||||
case 'EMERGENCY': return 'bg-red-100 text-red-800 border-red-300';
|
||||
case 'ALERT': return 'bg-yellow-100 text-yellow-800 border-yellow-300';
|
||||
case 'NORMAL': return 'bg-green-100 text-green-800 border-green-300';
|
||||
default: return 'bg-gray-100 text-gray-800 border-gray-300';
|
||||
}
|
||||
};
|
||||
|
||||
const getTierIcon = (tier) => {
|
||||
switch(tier) {
|
||||
case 'EMERGENCY': return <AlertCircle className="w-5 h-5" />;
|
||||
case 'ALERT': return <Bell className="w-5 h-5" />;
|
||||
case 'NORMAL': return <CheckCircle className="w-5 h-5" />;
|
||||
default: return <Activity className="w-5 h-5" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getVitalStatus = (type, value) => {
|
||||
if (type === 'hr') {
|
||||
if (value > 110 || value < 50) return 'text-red-600 font-bold';
|
||||
if (value > 100 || value < 60) return 'text-yellow-600 font-semibold';
|
||||
return 'text-green-600';
|
||||
}
|
||||
if (type === 'spo2') {
|
||||
if (value < 88) return 'text-red-600 font-bold';
|
||||
if (value < 92) return 'text-yellow-600 font-semibold';
|
||||
return 'text-green-600';
|
||||
}
|
||||
if (type === 'temp') {
|
||||
if (value > 39.5 || value < 35.5) return 'text-red-600 font-bold';
|
||||
if (value > 38.3 || value < 36.0) return 'text-yellow-600 font-semibold';
|
||||
return 'text-green-600';
|
||||
}
|
||||
return 'text-gray-700';
|
||||
};
|
||||
|
||||
const handleDischarge = (patientId) => {
|
||||
setPatients(prev => prev.filter(p => p.patient_id !== patientId));
|
||||
setSelectedPatient(null);
|
||||
};
|
||||
|
||||
const filteredPatients = patients
|
||||
.filter(p => filter === 'all' || p.tier === filter)
|
||||
.sort((a, b) => b.priority_score - a.priority_score);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100">
|
||||
<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="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold mb-1">VitalLink Dashboard</h1>
|
||||
<p className="text-blue-100">Emergency Department Patient Monitoring</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-white bg-opacity-20 rounded-lg px-4 py-2">
|
||||
<p className="text-sm text-blue-100">Last Update</p>
|
||||
<p className="text-lg font-semibold">{new Date().toLocaleTimeString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border-b shadow-sm">
|
||||
<div className="max-w-7xl mx-auto p-6">
|
||||
<div className="grid grid-cols-4 gap-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="bg-blue-100 p-3 rounded-lg">
|
||||
<Users className="w-6 h-6 text-blue-600" />
|
||||
</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 className="flex items-center gap-4">
|
||||
<div className="bg-red-100 p-3 rounded-lg">
|
||||
<AlertCircle className="w-6 h-6 text-red-600" />
|
||||
</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 className="flex items-center gap-4">
|
||||
<div className="bg-yellow-100 p-3 rounded-lg">
|
||||
<Bell className="w-6 h-6 text-yellow-600" />
|
||||
</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 className="flex items-center gap-4">
|
||||
<div className="bg-green-100 p-3 rounded-lg">
|
||||
<Clock className="w-6 h-6 text-green-600" />
|
||||
</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 className="max-w-7xl mx-auto p-6">
|
||||
<div className="bg-white rounded-lg shadow-sm p-4 mb-6">
|
||||
<div className="flex gap-2">
|
||||
<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>
|
||||
<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 className="space-y-4">
|
||||
{filteredPatients.map((patient, index) => (
|
||||
<div
|
||||
key={patient.patient_id}
|
||||
className="bg-white rounded-lg shadow-sm hover:shadow-md transition-shadow"
|
||||
>
|
||||
<div className="p-6">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="text-2xl font-bold text-gray-400 min-w-12 text-center pt-1">
|
||||
#{index + 1}
|
||||
</div>
|
||||
<div>
|
||||
<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">
|
||||
<span className="font-mono">{patient.patient_id}</span>
|
||||
<span>•</span>
|
||||
<span className="font-mono">{patient.band_id}</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{patient.symptoms.map(symptom => (
|
||||
<span key={symptom} className="bg-gray-100 text-gray-700 px-3 py-1 rounded-full text-xs font-medium">
|
||||
{symptom}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`px-4 py-2 rounded-lg border-2 flex items-center gap-2 font-semibold ${getTierColor(patient.tier)}`}>
|
||||
{getTierIcon(patient.tier)}
|
||||
{patient.tier}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => 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" />
|
||||
Discharge
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 gap-4 pt-4 border-t">
|
||||
<div className="bg-gray-50 rounded-lg p-3">
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 rounded-lg p-3">
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 rounded-lg p-3">
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 rounded-lg p-3">
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{filteredPatients.length === 0 && (
|
||||
<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" />
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(<StaffDashboard />);
|
||||
265
vitallink/frontend/kiosk/index.html
Normal file
265
vitallink/frontend/kiosk/index.html
Normal file
@ -0,0 +1,265 @@
|
||||
import React, { useState } from 'react';
|
||||
import { AlertCircle, CheckCircle, Clock, User } from 'lucide-react';
|
||||
|
||||
const CheckInKiosk = () => {
|
||||
const [step, setStep] = useState('welcome');
|
||||
const [formData, setFormData] = useState({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
dob: '',
|
||||
symptoms: [],
|
||||
severity: 'moderate'
|
||||
});
|
||||
const [assignedBand, setAssignedBand] = useState(null);
|
||||
|
||||
const symptoms = [
|
||||
'Chest Pain', 'Difficulty Breathing', 'Severe Headache',
|
||||
'Abdominal Pain', 'Fever', 'Nausea/Vomiting',
|
||||
'Dizziness', 'Injury/Trauma', 'Other'
|
||||
];
|
||||
|
||||
const handleSymptomToggle = (symptom) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
symptoms: prev.symptoms.includes(symptom)
|
||||
? prev.symptoms.filter(s => s !== symptom)
|
||||
: [...prev.symptoms, symptom]
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
// Simulate API call to backend
|
||||
const patientId = `P${Date.now().toString().slice(-6)}`;
|
||||
const bandId = `VitalLink-${Math.floor(Math.random() * 65536).toString(16).toUpperCase().padStart(4, '0')}`;
|
||||
|
||||
setAssignedBand({
|
||||
patientId,
|
||||
bandId,
|
||||
station: Math.floor(Math.random() * 8) + 1
|
||||
});
|
||||
|
||||
// In production, send to backend:
|
||||
// await fetch('/api/checkin', { method: 'POST', body: JSON.stringify({...formData, patientId, bandId}) });
|
||||
|
||||
setStep('complete');
|
||||
};
|
||||
|
||||
if (step === 'welcome') {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-blue-100 flex items-center justify-center p-4">
|
||||
<div className="bg-white rounded-2xl shadow-2xl p-12 max-w-2xl w-full text-center">
|
||||
<div className="mb-8">
|
||||
<div className="bg-blue-600 rounded-full w-24 h-24 flex items-center justify-center mx-auto mb-6">
|
||||
<User className="w-12 h-12 text-white" />
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold text-gray-800 mb-3">Welcome to VitalLink</h1>
|
||||
<p className="text-xl text-gray-600">Emergency Room Check-In</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 mb-8 text-left bg-blue-50 p-6 rounded-xl">
|
||||
<h2 className="font-semibold text-lg text-gray-800 mb-3">What to expect:</h2>
|
||||
<div className="flex items-start gap-3">
|
||||
<CheckCircle className="w-5 h-5 text-green-600 mt-1 flex-shrink-0" />
|
||||
<p className="text-gray-700">Answer a few questions about your condition</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<CheckCircle className="w-5 h-5 text-green-600 mt-1 flex-shrink-0" />
|
||||
<p className="text-gray-700">Receive a smart wristband to monitor your vitals</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<CheckCircle className="w-5 h-5 text-green-600 mt-1 flex-shrink-0" />
|
||||
<p className="text-gray-700">Wait comfortably while we track your condition</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setStep('form')}
|
||||
className="bg-blue-600 text-white px-12 py-4 rounded-xl text-xl font-semibold hover:bg-blue-700 transition-colors shadow-lg"
|
||||
>
|
||||
Start Check-In
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (step === 'form') {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-blue-100 p-4">
|
||||
<div className="max-w-3xl mx-auto pt-8">
|
||||
<div className="bg-white rounded-2xl shadow-2xl p-8">
|
||||
<h2 className="text-3xl font-bold text-gray-800 mb-6">Patient Information</h2>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
First Name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.firstName}
|
||||
onChange={(e) => setFormData({...formData, firstName: e.target.value})}
|
||||
className="w-full px-4 py-3 border-2 border-gray-300 rounded-lg focus:border-blue-500 focus:outline-none text-lg"
|
||||
placeholder="John"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Last Name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.lastName}
|
||||
onChange={(e) => setFormData({...formData, lastName: e.target.value})}
|
||||
className="w-full px-4 py-3 border-2 border-gray-300 rounded-lg focus:border-blue-500 focus:outline-none text-lg"
|
||||
placeholder="Doe"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Date of Birth *
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={formData.dob}
|
||||
onChange={(e) => setFormData({...formData, dob: e.target.value})}
|
||||
className="w-full px-4 py-3 border-2 border-gray-300 rounded-lg focus:border-blue-500 focus:outline-none text-lg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-3">
|
||||
Select Your Symptoms *
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{symptoms.map((symptom) => (
|
||||
<button
|
||||
key={symptom}
|
||||
onClick={() => handleSymptomToggle(symptom)}
|
||||
className={`px-4 py-3 rounded-lg border-2 transition-all text-left font-medium ${
|
||||
formData.symptoms.includes(symptom)
|
||||
? 'bg-blue-100 border-blue-500 text-blue-700'
|
||||
: 'bg-white border-gray-300 text-gray-700 hover:border-blue-300'
|
||||
}`}
|
||||
>
|
||||
{symptom}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-3">
|
||||
How severe are your symptoms? *
|
||||
</label>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{['mild', 'moderate', 'severe'].map((level) => (
|
||||
<button
|
||||
key={level}
|
||||
onClick={() => setFormData({...formData, severity: level})}
|
||||
className={`px-6 py-4 rounded-lg border-2 transition-all font-semibold capitalize ${
|
||||
formData.severity === level
|
||||
? level === 'severe'
|
||||
? 'bg-red-100 border-red-500 text-red-700'
|
||||
: level === 'moderate'
|
||||
? 'bg-yellow-100 border-yellow-500 text-yellow-700'
|
||||
: 'bg-green-100 border-green-500 text-green-700'
|
||||
: 'bg-white border-gray-300 text-gray-700 hover:border-gray-400'
|
||||
}`}
|
||||
>
|
||||
{level}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 mt-8">
|
||||
<button
|
||||
onClick={() => setStep('welcome')}
|
||||
className="flex-1 px-6 py-4 border-2 border-gray-300 text-gray-700 rounded-xl font-semibold hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={!formData.firstName || !formData.lastName || !formData.dob || formData.symptoms.length === 0}
|
||||
className="flex-1 px-6 py-4 bg-blue-600 text-white rounded-xl font-semibold hover:bg-blue-700 transition-colors disabled:bg-gray-300 disabled:cursor-not-allowed"
|
||||
>
|
||||
Complete Check-In
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (step === 'complete') {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-green-50 to-green-100 flex items-center justify-center p-4">
|
||||
<div className="bg-white rounded-2xl shadow-2xl p-12 max-w-2xl w-full text-center">
|
||||
<div className="mb-6">
|
||||
<div className="bg-green-600 rounded-full w-24 h-24 flex items-center justify-center mx-auto mb-6">
|
||||
<CheckCircle className="w-12 h-12 text-white" />
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold text-gray-800 mb-3">Check-In Complete!</h1>
|
||||
<p className="text-xl text-gray-600">Your wristband has been assigned</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-r from-blue-500 to-blue-600 rounded-xl p-8 mb-8 text-white">
|
||||
<p className="text-sm uppercase tracking-wide mb-2 opacity-90">Your Patient ID</p>
|
||||
<p className="text-3xl font-bold mb-4">{assignedBand?.patientId}</p>
|
||||
<div className="border-t border-white/30 pt-4">
|
||||
<p className="text-sm uppercase tracking-wide mb-2 opacity-90">Wristband ID</p>
|
||||
<p className="text-2xl font-semibold">{assignedBand?.bandId}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 text-left bg-blue-50 p-6 rounded-xl mb-8">
|
||||
<h2 className="font-bold text-lg text-gray-800 mb-3">Next Steps:</h2>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="bg-blue-600 text-white rounded-full w-8 h-8 flex items-center justify-center font-bold flex-shrink-0">1</div>
|
||||
<p className="text-gray-700 pt-1">
|
||||
<strong>Pick up your wristband</strong> from Station {assignedBand?.station}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="bg-blue-600 text-white rounded-full w-8 h-8 flex items-center justify-center font-bold flex-shrink-0">2</div>
|
||||
<p className="text-gray-700 pt-1">
|
||||
<strong>Wear it on your wrist</strong> - make sure it's snug but comfortable
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="bg-blue-600 text-white rounded-full w-8 h-8 flex items-center justify-center font-bold flex-shrink-0">3</div>
|
||||
<p className="text-gray-700 pt-1">
|
||||
<strong>Take a seat in the waiting area</strong> - your vitals are being monitored
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center gap-2 text-gray-600 mb-6">
|
||||
<Clock className="w-5 h-5" />
|
||||
<p>A nurse will call you when it's your turn</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setStep('welcome');
|
||||
setFormData({ firstName: '', lastName: '', dob: '', symptoms: [], severity: 'moderate' });
|
||||
setAssignedBand(null);
|
||||
}}
|
||||
className="text-blue-600 font-semibold hover:underline"
|
||||
>
|
||||
Return to Start
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default CheckInKiosk;
|
||||
7
vitallink/logs/backend.log
Normal file
7
vitallink/logs/backend.log
Normal file
@ -0,0 +1,7 @@
|
||||
/home/mai/documents/school/capstone/vitallink-BS/vitallink/backend/server.py:369: DeprecationWarning:
|
||||
on_event is deprecated, use lifespan event handlers instead.
|
||||
|
||||
Read more about it in the
|
||||
[FastAPI docs for Lifespan Events](https://fastapi.tiangolo.com/advanced/events/).
|
||||
|
||||
@app.on_event("startup")
|
||||
1
vitallink/logs/backend.pid
Normal file
1
vitallink/logs/backend.pid
Normal file
@ -0,0 +1 @@
|
||||
29497
|
||||
41
vitallink/logs/simulator.log
Normal file
41
vitallink/logs/simulator.log
Normal file
@ -0,0 +1,41 @@
|
||||
|
||||
╔══════════════════════════════════════════════════════════════════════════╗
|
||||
║ VitalLink Wristband Simulator ║
|
||||
║ Emergency Department Monitoring System ║
|
||||
╚══════════════════════════════════════════════════════════════════════════╝
|
||||
|
||||
Available Patient Profiles:
|
||||
- stable: Normal vitals, no deterioration
|
||||
- mild_anxiety: Elevated HR, improves over time
|
||||
- deteriorating: Gradually worsening condition
|
||||
- critical: Severe vitals, triggers emergency tier
|
||||
- sepsis: Rapid deterioration pattern
|
||||
|
||||
Usage Examples:
|
||||
1. Run demo scenarios (below)
|
||||
2. Create custom scenarios using BaseStationSimulator
|
||||
3. Integrate with FastAPI backend for web portal
|
||||
|
||||
|
||||
Running Demo Scenario 1...
|
||||
|
||||
|
||||
================================================================================
|
||||
DEMO SCENARIO 1: Mixed Patient Population
|
||||
================================================================================
|
||||
|
||||
[BASE] Added wristband VitalLink-A1B2 with profile 'Stable Patient'
|
||||
[BASE] Added wristband VitalLink-C3D4 with profile 'Mild Anxiety'
|
||||
[BASE] Added wristband VitalLink-E5F6 with profile 'Deteriorating Condition'
|
||||
[BASE] Starting base station simulation...
|
||||
================================================================================
|
||||
🟢 [VitalLink-A1B2] P100001 | HR=66 SpO2=98% Temp=36.9°C | NORMAL | Seq=0
|
||||
🟢 [VitalLink-C3D4] P100002 | HR=91 SpO2=97% Temp=37.0°C | NORMAL | Seq=0
|
||||
🟢 [VitalLink-E5F6] P100003 | HR=88 SpO2=92% Temp=37.7°C | NORMAL | Seq=0
|
||||
|
||||
[BASE] Stopping base station...
|
||||
|
||||
================================================================================
|
||||
SUMMARY:
|
||||
{'total_bands': 3, 'active_bands': 3, 'tiers': {'EMERGENCY': 0, 'ALERT': 0, 'NORMAL': 3}, 'total_packets': 3}
|
||||
================================================================================
|
||||
1
vitallink/logs/simulator.pid
Normal file
1
vitallink/logs/simulator.pid
Normal file
@ -0,0 +1 @@
|
||||
29507
|
||||
17
vitallink/requirements.txt
Normal file
17
vitallink/requirements.txt
Normal file
@ -0,0 +1,17 @@
|
||||
# Backend API
|
||||
fastapi==0.104.1
|
||||
uvicorn[standard]==0.24.0
|
||||
websockets==12.0
|
||||
pydantic==2.5.0
|
||||
python-multipart==0.0.6
|
||||
|
||||
# HTTP client for simulator integration
|
||||
aiohttp==3.9.1
|
||||
requests==2.31.0
|
||||
|
||||
# Testing
|
||||
pytest==7.4.3
|
||||
pytest-asyncio==0.21.1
|
||||
|
||||
# Utilities
|
||||
python-dateutil==2.8.2
|
||||
519
vitallink/simulator/wristband_simulator.py
Normal file
519
vitallink/simulator/wristband_simulator.py
Normal file
@ -0,0 +1,519 @@
|
||||
"""
|
||||
VitalLink Wristband Simulator & Base Station
|
||||
Simulates multiple wristbands with realistic vital signs and BLE behavior
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import struct
|
||||
import random
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List, Optional
|
||||
from enum import IntFlag
|
||||
|
||||
# ============================================================================
|
||||
# CONSTANTS & FLAGS
|
||||
# ============================================================================
|
||||
|
||||
SERVICE_UUID = "8f5a84f1-22a8-4a4b-9b5f-3fe1d8b2a3a1"
|
||||
CHAR_UUID = "d3e2c4b7-39b2-4b2a-8d5a-7d2a5e3f1199"
|
||||
|
||||
PKT_LEN = 16
|
||||
PKT_STRUCT = struct.Struct("<B H I B B B h H B B")
|
||||
|
||||
|
||||
class VitalFlags(IntFlag):
|
||||
MOTION_ARTIFACT = 1 << 0
|
||||
LOW_BATT = 1 << 1
|
||||
SENSOR_FAULT = 1 << 2
|
||||
ALERT = 1 << 3
|
||||
EMERGENCY = 1 << 4
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# PATIENT CONDITION PROFILES
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@dataclass
|
||||
class PatientProfile:
|
||||
"""Defines baseline vitals and progression patterns"""
|
||||
|
||||
name: str
|
||||
hr_base: float # Baseline heart rate
|
||||
hr_variance: float # Random variation
|
||||
spo2_base: float
|
||||
spo2_variance: float
|
||||
temp_base: float
|
||||
temp_variance: float
|
||||
deterioration_rate: float # How fast condition worsens (0-1)
|
||||
recovery_rate: float # Can improve over time
|
||||
|
||||
|
||||
# Preset patient profiles for testing different scenarios
|
||||
PATIENT_PROFILES = {
|
||||
"stable": PatientProfile(
|
||||
name="Stable Patient",
|
||||
hr_base=72,
|
||||
hr_variance=5,
|
||||
spo2_base=98,
|
||||
spo2_variance=1,
|
||||
temp_base=36.8,
|
||||
temp_variance=0.2,
|
||||
deterioration_rate=0.0,
|
||||
recovery_rate=0.0,
|
||||
),
|
||||
"mild_anxiety": PatientProfile(
|
||||
name="Mild Anxiety",
|
||||
hr_base=88,
|
||||
hr_variance=8,
|
||||
spo2_base=97,
|
||||
spo2_variance=1,
|
||||
temp_base=37.1,
|
||||
temp_variance=0.3,
|
||||
deterioration_rate=0.0,
|
||||
recovery_rate=0.01, # Slowly calms down
|
||||
),
|
||||
"deteriorating": PatientProfile(
|
||||
name="Deteriorating Condition",
|
||||
hr_base=85,
|
||||
hr_variance=10,
|
||||
spo2_base=95,
|
||||
spo2_variance=2,
|
||||
temp_base=37.5,
|
||||
temp_variance=0.4,
|
||||
deterioration_rate=0.05, # Gets worse over time
|
||||
recovery_rate=0.0,
|
||||
),
|
||||
"critical": PatientProfile(
|
||||
name="Critical Patient",
|
||||
hr_base=130,
|
||||
hr_variance=15,
|
||||
spo2_base=88,
|
||||
spo2_variance=3,
|
||||
temp_base=39.2,
|
||||
temp_variance=0.5,
|
||||
deterioration_rate=0.02,
|
||||
recovery_rate=0.0,
|
||||
),
|
||||
"sepsis": PatientProfile(
|
||||
name="Sepsis Presentation",
|
||||
hr_base=115,
|
||||
hr_variance=12,
|
||||
spo2_base=92,
|
||||
spo2_variance=2,
|
||||
temp_base=38.8,
|
||||
temp_variance=0.6,
|
||||
deterioration_rate=0.08, # Rapid deterioration
|
||||
recovery_rate=0.0,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# WRISTBAND SIMULATOR
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class WristbandSimulator:
|
||||
"""Simulates a single wristband with realistic vital sign generation"""
|
||||
|
||||
def __init__(self, band_id: str, profile: PatientProfile, patient_id: str = None):
|
||||
self.band_id = band_id
|
||||
self.profile = profile
|
||||
self.patient_id = patient_id or f"P{random.randint(100000, 999999)}"
|
||||
|
||||
# State
|
||||
self.seq = 0
|
||||
self.start_time = time.time()
|
||||
self.battery = 100.0
|
||||
self.is_active = True
|
||||
self.tier = "NORMAL"
|
||||
|
||||
# Vital signs state (evolves over time)
|
||||
self.current_hr = profile.hr_base
|
||||
self.current_spo2 = profile.spo2_base
|
||||
self.current_temp = profile.temp_base
|
||||
self.activity_level = 0.5 # 0-1 scale
|
||||
|
||||
# Time since condition change
|
||||
self.time_elapsed = 0
|
||||
|
||||
def _calculate_checksum(self, data: bytes) -> int:
|
||||
"""Calculate 8-bit checksum"""
|
||||
return sum(data[0:14]) & 0xFF
|
||||
|
||||
def _generate_vitals(self) -> tuple:
|
||||
"""Generate realistic vital signs with progression"""
|
||||
self.time_elapsed += 1
|
||||
|
||||
# Apply deterioration/recovery over time
|
||||
if self.profile.deterioration_rate > 0:
|
||||
self.current_hr += random.uniform(0, self.profile.deterioration_rate * 2)
|
||||
self.current_spo2 -= random.uniform(
|
||||
0, self.profile.deterioration_rate * 0.5
|
||||
)
|
||||
self.current_temp += random.uniform(
|
||||
0, self.profile.deterioration_rate * 0.1
|
||||
)
|
||||
|
||||
if self.profile.recovery_rate > 0:
|
||||
# Trend back toward normal
|
||||
self.current_hr += (72 - self.current_hr) * self.profile.recovery_rate
|
||||
self.current_spo2 += (98 - self.current_spo2) * self.profile.recovery_rate
|
||||
self.current_temp += (36.8 - self.current_temp) * self.profile.recovery_rate
|
||||
|
||||
# Add random variance
|
||||
hr = self.current_hr + random.gauss(0, self.profile.hr_variance)
|
||||
spo2 = self.current_spo2 + random.gauss(0, self.profile.spo2_variance)
|
||||
temp = self.current_temp + random.gauss(0, self.profile.temp_variance)
|
||||
|
||||
# Clamp to realistic ranges
|
||||
hr = max(30, min(200, hr))
|
||||
spo2 = max(70, min(100, spo2))
|
||||
temp = max(34.0, min(42.0, temp))
|
||||
|
||||
# Activity varies (simulates patient movement)
|
||||
self.activity_level += random.gauss(0, 0.1)
|
||||
self.activity_level = max(0, min(2.0, self.activity_level))
|
||||
|
||||
return int(hr), int(spo2), temp, self.activity_level
|
||||
|
||||
def _determine_tier(self, hr: int, spo2: int, temp: float) -> tuple:
|
||||
"""Determine alert tier based on vitals"""
|
||||
flags = 0
|
||||
|
||||
# Battery warning
|
||||
if self.battery < 15:
|
||||
flags |= VitalFlags.LOW_BATT
|
||||
|
||||
# Check for critical vitals (EMERGENCY)
|
||||
if hr > 140 or hr < 45 or spo2 < 88 or temp > 39.5 or temp < 35.0:
|
||||
flags |= VitalFlags.EMERGENCY
|
||||
tier = "EMERGENCY"
|
||||
# Check for concerning vitals (ALERT)
|
||||
elif hr > 110 or hr < 50 or spo2 < 92 or temp > 38.3 or temp < 35.5:
|
||||
flags |= VitalFlags.ALERT
|
||||
tier = "ALERT"
|
||||
else:
|
||||
tier = "NORMAL"
|
||||
|
||||
return tier, flags
|
||||
|
||||
def generate_packet(self) -> bytes:
|
||||
"""Generate a complete 16-byte vitals packet"""
|
||||
# Generate vitals
|
||||
hr, spo2, temp, activity = self._generate_vitals()
|
||||
|
||||
# Determine tier and flags
|
||||
tier, flags = self._determine_tier(hr, spo2, temp)
|
||||
self.tier = tier
|
||||
|
||||
# Timestamp
|
||||
ts_ms = int((time.time() - self.start_time) * 1000)
|
||||
|
||||
# Convert values to packet format
|
||||
skin_c_x100 = int(temp * 100)
|
||||
act_rms_x100 = int(activity * 100)
|
||||
|
||||
# Pack data (without checksum yet)
|
||||
partial_pkt = PKT_STRUCT.pack(
|
||||
1, # version
|
||||
self.seq & 0xFFFF, # sequence
|
||||
ts_ms & 0xFFFFFFFF, # timestamp
|
||||
flags,
|
||||
hr,
|
||||
spo2,
|
||||
skin_c_x100,
|
||||
act_rms_x100,
|
||||
0, # checksum placeholder
|
||||
0, # reserved
|
||||
)
|
||||
|
||||
# Calculate and insert checksum
|
||||
checksum = self._calculate_checksum(partial_pkt)
|
||||
packet = bytearray(partial_pkt)
|
||||
packet[14] = checksum
|
||||
|
||||
# Update state
|
||||
self.seq += 1
|
||||
self.battery = max(0, self.battery - 0.001) # Slow drain
|
||||
|
||||
return bytes(packet)
|
||||
|
||||
def get_status(self) -> dict:
|
||||
"""Get current wristband status"""
|
||||
return {
|
||||
"band_id": self.band_id,
|
||||
"patient_id": self.patient_id,
|
||||
"profile": self.profile.name,
|
||||
"tier": self.tier,
|
||||
"battery": round(self.battery, 1),
|
||||
"seq": self.seq,
|
||||
"active": self.is_active,
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# BASE STATION SIMULATOR
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class BaseStationSimulator:
|
||||
"""Simulates the base station that manages multiple wristbands"""
|
||||
|
||||
def __init__(self):
|
||||
self.wristbands: Dict[str, WristbandSimulator] = {}
|
||||
self.running = False
|
||||
self.packet_log: List[dict] = []
|
||||
|
||||
def add_wristband(
|
||||
self, band_id: str, profile_name: str = "stable", patient_id: str = None
|
||||
) -> WristbandSimulator:
|
||||
"""Add a new wristband to the simulation"""
|
||||
profile = PATIENT_PROFILES.get(profile_name, PATIENT_PROFILES["stable"])
|
||||
band = WristbandSimulator(band_id, profile, patient_id)
|
||||
self.wristbands[band_id] = band
|
||||
print(f"[BASE] Added wristband {band_id} with profile '{profile.name}'")
|
||||
return band
|
||||
|
||||
def remove_wristband(self, band_id: str):
|
||||
"""Remove a wristband (patient discharged)"""
|
||||
if band_id in self.wristbands:
|
||||
del self.wristbands[band_id]
|
||||
print(f"[BASE] Removed wristband {band_id}")
|
||||
|
||||
def decode_packet(self, band_id: str, data: bytes) -> dict:
|
||||
"""Decode a packet and return structured data"""
|
||||
if len(data) != PKT_LEN:
|
||||
return {"error": "Invalid packet length"}
|
||||
|
||||
# Verify checksum
|
||||
checksum_calc = sum(data[0:14]) & 0xFF
|
||||
if checksum_calc != data[14]:
|
||||
return {"error": "Checksum failed"}
|
||||
|
||||
# Unpack
|
||||
(
|
||||
ver,
|
||||
seq,
|
||||
ts_ms,
|
||||
flags,
|
||||
hr_bpm,
|
||||
spo2,
|
||||
skin_c_x100,
|
||||
act_rms_x100,
|
||||
checksum,
|
||||
rfu,
|
||||
) = PKT_STRUCT.unpack(data)
|
||||
|
||||
# Determine tier
|
||||
tier = (
|
||||
"EMERGENCY"
|
||||
if (flags & VitalFlags.EMERGENCY)
|
||||
else "ALERT"
|
||||
if (flags & VitalFlags.ALERT)
|
||||
else "NORMAL"
|
||||
)
|
||||
|
||||
# Build flag list
|
||||
flag_list = []
|
||||
if flags & VitalFlags.MOTION_ARTIFACT:
|
||||
flag_list.append("MOTION_ARTIFACT")
|
||||
if flags & VitalFlags.LOW_BATT:
|
||||
flag_list.append("LOW_BATT")
|
||||
if flags & VitalFlags.SENSOR_FAULT:
|
||||
flag_list.append("SENSOR_FAULT")
|
||||
if flags & VitalFlags.ALERT:
|
||||
flag_list.append("ALERT")
|
||||
if flags & VitalFlags.EMERGENCY:
|
||||
flag_list.append("EMERGENCY")
|
||||
|
||||
return {
|
||||
"band_id": band_id,
|
||||
"patient_id": self.wristbands[band_id].patient_id
|
||||
if band_id in self.wristbands
|
||||
else "UNKNOWN",
|
||||
"timestamp": time.time(),
|
||||
"ver": ver,
|
||||
"seq": seq,
|
||||
"ts_ms": ts_ms,
|
||||
"tier": tier,
|
||||
"flags": flag_list,
|
||||
"hr_bpm": hr_bpm,
|
||||
"spo2": spo2,
|
||||
"temp_c": skin_c_x100 / 100.0,
|
||||
"activity": act_rms_x100 / 100.0,
|
||||
"checksum": f"0x{checksum:02X}",
|
||||
}
|
||||
|
||||
async def simulate_band_transmission(self, band_id: str):
|
||||
"""Simulate continuous transmission from one wristband"""
|
||||
band = self.wristbands[band_id]
|
||||
|
||||
while self.running and band.is_active and band_id in self.wristbands:
|
||||
# Generate packet
|
||||
packet = band.generate_packet()
|
||||
|
||||
# Decode for logging
|
||||
decoded = self.decode_packet(band_id, packet)
|
||||
|
||||
# Determine send interval based on tier
|
||||
if band.tier == "EMERGENCY":
|
||||
interval = 1.0 # 1 Hz
|
||||
elif band.tier == "ALERT":
|
||||
interval = 1.0 # 1 Hz
|
||||
else:
|
||||
interval = 60.0 # Every 60s for NORMAL
|
||||
|
||||
# Log packet
|
||||
self.packet_log.append(decoded)
|
||||
|
||||
# Print to console
|
||||
tier_symbol = (
|
||||
"🔴"
|
||||
if band.tier == "EMERGENCY"
|
||||
else "🟡"
|
||||
if band.tier == "ALERT"
|
||||
else "🟢"
|
||||
)
|
||||
print(
|
||||
f"{tier_symbol} [{band_id}] {band.patient_id} | "
|
||||
f"HR={decoded['hr_bpm']} SpO2={decoded['spo2']}% "
|
||||
f"Temp={decoded['temp_c']:.1f}°C | {band.tier} | "
|
||||
f"Seq={decoded['seq']}"
|
||||
)
|
||||
|
||||
# Send to backend API (in production)
|
||||
# await self.send_to_api(decoded)
|
||||
|
||||
await asyncio.sleep(interval)
|
||||
|
||||
async def run(self):
|
||||
"""Start the base station simulation"""
|
||||
self.running = True
|
||||
print("[BASE] Starting base station simulation...")
|
||||
print("=" * 80)
|
||||
|
||||
# Create tasks for each wristband
|
||||
tasks = [
|
||||
asyncio.create_task(self.simulate_band_transmission(band_id))
|
||||
for band_id in self.wristbands.keys()
|
||||
]
|
||||
|
||||
# Run until stopped
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
def stop(self):
|
||||
"""Stop the simulation"""
|
||||
self.running = False
|
||||
print("\n[BASE] Stopping base station...")
|
||||
|
||||
def get_summary(self) -> dict:
|
||||
"""Get current status of all wristbands"""
|
||||
return {
|
||||
"total_bands": len(self.wristbands),
|
||||
"active_bands": sum(1 for b in self.wristbands.values() if b.is_active),
|
||||
"tiers": {
|
||||
"EMERGENCY": sum(
|
||||
1 for b in self.wristbands.values() if b.tier == "EMERGENCY"
|
||||
),
|
||||
"ALERT": sum(1 for b in self.wristbands.values() if b.tier == "ALERT"),
|
||||
"NORMAL": sum(
|
||||
1 for b in self.wristbands.values() if b.tier == "NORMAL"
|
||||
),
|
||||
},
|
||||
"total_packets": len(self.packet_log),
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# DEMO / TEST SCENARIOS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
async def demo_scenario_1():
|
||||
"""Demo: Mix of stable and deteriorating patients"""
|
||||
print("\n" + "=" * 80)
|
||||
print("DEMO SCENARIO 1: Mixed Patient Population")
|
||||
print("=" * 80 + "\n")
|
||||
|
||||
base = BaseStationSimulator()
|
||||
|
||||
# Add various patients
|
||||
base.add_wristband("VitalLink-A1B2", "stable", "P100001")
|
||||
base.add_wristband("VitalLink-C3D4", "mild_anxiety", "P100002")
|
||||
base.add_wristband("VitalLink-E5F6", "deteriorating", "P100003")
|
||||
|
||||
# Run for 30 seconds
|
||||
try:
|
||||
await asyncio.wait_for(base.run(), timeout=30.0)
|
||||
except asyncio.TimeoutError:
|
||||
base.stop()
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("SUMMARY:")
|
||||
print(base.get_summary())
|
||||
print("=" * 80)
|
||||
|
||||
|
||||
async def demo_scenario_2():
|
||||
"""Demo: Critical patient arrival"""
|
||||
print("\n" + "=" * 80)
|
||||
print("DEMO SCENARIO 2: Critical Patient Emergency")
|
||||
print("=" * 80 + "\n")
|
||||
|
||||
base = BaseStationSimulator()
|
||||
|
||||
# Start with stable patients
|
||||
base.add_wristband("VitalLink-1111", "stable", "P200001")
|
||||
base.add_wristband("VitalLink-2222", "stable", "P200002")
|
||||
|
||||
# Simulate for 10 seconds
|
||||
async def add_critical_patient():
|
||||
await asyncio.sleep(10)
|
||||
print("\n⚠️ CRITICAL PATIENT ARRIVED ⚠️\n")
|
||||
base.add_wristband("VitalLink-9999", "critical", "P200999")
|
||||
|
||||
# Run both tasks
|
||||
await asyncio.gather(
|
||||
add_critical_patient(), asyncio.wait_for(base.run(), timeout=25.0)
|
||||
)
|
||||
|
||||
base.stop()
|
||||
print("\n" + "=" * 80)
|
||||
print("SUMMARY:")
|
||||
print(base.get_summary())
|
||||
print("=" * 80)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# MAIN
|
||||
# ============================================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("""
|
||||
╔══════════════════════════════════════════════════════════════════════════╗
|
||||
║ VitalLink Wristband Simulator ║
|
||||
║ Emergency Department Monitoring System ║
|
||||
╚══════════════════════════════════════════════════════════════════════════╝
|
||||
|
||||
Available Patient Profiles:
|
||||
- stable: Normal vitals, no deterioration
|
||||
- mild_anxiety: Elevated HR, improves over time
|
||||
- deteriorating: Gradually worsening condition
|
||||
- critical: Severe vitals, triggers emergency tier
|
||||
- sepsis: Rapid deterioration pattern
|
||||
|
||||
Usage Examples:
|
||||
1. Run demo scenarios (below)
|
||||
2. Create custom scenarios using BaseStationSimulator
|
||||
3. Integrate with FastAPI backend for web portal
|
||||
""")
|
||||
|
||||
# Choose a demo to run
|
||||
print("\nRunning Demo Scenario 1...\n")
|
||||
asyncio.run(demo_scenario_1())
|
||||
|
||||
# Uncomment to run other scenarios:
|
||||
# asyncio.run(demo_scenario_2())
|
||||
47
vitallink/start.sh
Executable file
47
vitallink/start.sh
Executable file
@ -0,0 +1,47 @@
|
||||
#!/bin/bash
|
||||
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
# Activate venv (bash way)
|
||||
source venv/bin/activate 2>/dev/null || source .venv/bin/activate 2>/dev/null
|
||||
|
||||
echo "╔═══════════════════════════════════════════════════════════════════════╗"
|
||||
echo "║ Starting VitalLink System ║"
|
||||
echo "╚═══════════════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
mkdir -p logs
|
||||
|
||||
echo "Starting backend server..."
|
||||
python backend/server.py > logs/backend.log 2>&1 &
|
||||
echo $! > logs/backend.pid
|
||||
echo "✓ Backend started (PID: $(cat logs/backend.pid))"
|
||||
|
||||
sleep 3
|
||||
|
||||
echo "Starting wristband simulator..."
|
||||
python simulator/wristband_simulator.py > logs/simulator.log 2>&1 &
|
||||
echo $! > logs/simulator.pid
|
||||
echo "✓ Simulator started (PID: $(cat logs/simulator.pid))"
|
||||
|
||||
echo ""
|
||||
echo "═══════════════════════════════════════════════════════════════════════"
|
||||
echo "✅ VitalLink System Running!"
|
||||
echo "═══════════════════════════════════════════════════════════════════════"
|
||||
echo ""
|
||||
echo "📊 Access Points:"
|
||||
echo " • API Docs: http://localhost:8000/docs"
|
||||
echo " • API Stats: http://localhost:8000/api/stats"
|
||||
echo " • WebSocket: ws://localhost:8000/ws"
|
||||
echo " • Staff Dashboard: file://$PROJECT_ROOT/frontend/dashboard/index.html"
|
||||
echo " • Check-in Kiosk: file://$PROJECT_ROOT/frontend/kiosk/index.html"
|
||||
echo ""
|
||||
echo "📝 View Logs:"
|
||||
echo " • Backend: tail -f logs/backend.log"
|
||||
echo " • Simulator: tail -f logs/simulator.log"
|
||||
echo ""
|
||||
echo "🛑 Stop System:"
|
||||
echo " • Run: ./stop.sh"
|
||||
echo ""
|
||||
echo "═══════════════════════════════════════════════════════════════════════"
|
||||
18
vitallink/stop.sh
Executable file
18
vitallink/stop.sh
Executable file
@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
echo "Stopping VitalLink system..."
|
||||
|
||||
if [ -f logs/backend.pid ]; then
|
||||
kill $(cat logs/backend.pid) 2>/dev/null && echo "✓ Backend stopped" || echo "Backend not running"
|
||||
rm -f logs/backend.pid
|
||||
fi
|
||||
|
||||
if [ -f logs/simulator.pid ]; then
|
||||
kill $(cat logs/simulator.pid) 2>/dev/null && echo "✓ Simulator stopped" || echo "Simulator not running"
|
||||
rm -f logs/simulator.pid
|
||||
fi
|
||||
|
||||
echo "✓ VitalLink system stopped"
|
||||
13
vitallink/test.sh
Executable file
13
vitallink/test.sh
Executable file
@ -0,0 +1,13 @@
|
||||
cat >test.sh <<'TESTEOF'
|
||||
#!/bin/bash
|
||||
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$PROJECT_ROOT"
|
||||
source venv/bin/activate 2>/dev/null || source .venv/bin/activate 2>/dev/null
|
||||
|
||||
echo "Running VitalLink Test Suite..."
|
||||
echo ""
|
||||
python tests/test_suite.py
|
||||
TESTEOF
|
||||
|
||||
chmod +x test.sh
|
||||
565
vitallink/tests/test_suite.py
Normal file
565
vitallink/tests/test_suite.py
Normal file
@ -0,0 +1,565 @@
|
||||
"""
|
||||
VitalLink Complete Test Suite
|
||||
Integrated testing script that simulates the entire system
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from datetime import datetime
|
||||
import random
|
||||
|
||||
# Import from previous modules (in production, these would be proper imports)
|
||||
# For this demo, we'll include simplified versions
|
||||
|
||||
# ============================================================================
|
||||
# CONFIGURATION
|
||||
# ============================================================================
|
||||
|
||||
API_BASE = "http://localhost:8000"
|
||||
ENABLE_API_CALLS = False # Set to True when backend is running
|
||||
|
||||
# ============================================================================
|
||||
# TEST SUITE
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class VitalLinkTestSuite:
|
||||
"""Comprehensive testing suite for VitalLink system"""
|
||||
|
||||
def __init__(self):
|
||||
self.test_results = []
|
||||
self.patients_created = []
|
||||
|
||||
def log_test(self, test_name: str, passed: bool, message: str = ""):
|
||||
"""Log test result"""
|
||||
status = "✓ PASS" if passed else "✗ FAIL"
|
||||
result = {
|
||||
"test": test_name,
|
||||
"passed": passed,
|
||||
"message": message,
|
||||
"timestamp": datetime.now(),
|
||||
}
|
||||
self.test_results.append(result)
|
||||
print(f"{status} | {test_name}")
|
||||
if message:
|
||||
print(f" {message}")
|
||||
|
||||
async def test_1_patient_data_validation(self):
|
||||
"""Test 1: Validate patient data structure"""
|
||||
print("\n" + "=" * 80)
|
||||
print("TEST 1: Patient Data Validation")
|
||||
print("=" * 80)
|
||||
|
||||
# Test valid patient data
|
||||
valid_patient = {
|
||||
"firstName": "John",
|
||||
"lastName": "Doe",
|
||||
"dob": "1990-01-01",
|
||||
"symptoms": ["Chest Pain", "Shortness of Breath"],
|
||||
"severity": "moderate",
|
||||
}
|
||||
|
||||
self.log_test(
|
||||
"Valid patient data structure",
|
||||
all(
|
||||
key in valid_patient
|
||||
for key in ["firstName", "lastName", "dob", "symptoms", "severity"]
|
||||
),
|
||||
f"Patient: {valid_patient['firstName']} {valid_patient['lastName']}",
|
||||
)
|
||||
|
||||
# Test invalid data
|
||||
invalid_patient = {
|
||||
"firstName": "Jane",
|
||||
# Missing lastName
|
||||
"dob": "1985-05-15",
|
||||
"symptoms": [],
|
||||
"severity": "mild",
|
||||
}
|
||||
|
||||
self.log_test(
|
||||
"Detect missing required fields",
|
||||
"lastName" not in invalid_patient,
|
||||
"Missing 'lastName' field detected",
|
||||
)
|
||||
|
||||
# Test date validation
|
||||
try:
|
||||
datetime.strptime(valid_patient["dob"], "%Y-%m-%d")
|
||||
self.log_test(
|
||||
"Date format validation", True, "Date format YYYY-MM-DD accepted"
|
||||
)
|
||||
except:
|
||||
self.log_test("Date format validation", False, "Invalid date format")
|
||||
|
||||
async def test_2_vitals_packet_generation(self):
|
||||
"""Test 2: Vitals packet generation and checksum"""
|
||||
print("\n" + "=" * 80)
|
||||
print("TEST 2: Vitals Packet Generation")
|
||||
print("=" * 80)
|
||||
|
||||
# Simulate packet generation
|
||||
import struct
|
||||
|
||||
PKT_STRUCT = struct.Struct("<B H I B B B h H B B")
|
||||
|
||||
# Create test packet
|
||||
test_data = (
|
||||
1, # version
|
||||
42, # seq
|
||||
120000, # timestamp
|
||||
2, # flags (ALERT)
|
||||
78, # HR
|
||||
97, # SpO2
|
||||
3645, # temp * 100
|
||||
180, # activity * 100
|
||||
0, # checksum placeholder
|
||||
0, # reserved
|
||||
)
|
||||
|
||||
packet = bytearray(PKT_STRUCT.pack(*test_data))
|
||||
|
||||
# Calculate checksum
|
||||
checksum = sum(packet[0:14]) & 0xFF
|
||||
packet[14] = checksum
|
||||
|
||||
self.log_test(
|
||||
"Packet structure (16 bytes)",
|
||||
len(packet) == 16,
|
||||
f"Packet size: {len(packet)} bytes",
|
||||
)
|
||||
|
||||
# Verify checksum
|
||||
verify_checksum = sum(packet[0:14]) & 0xFF
|
||||
self.log_test(
|
||||
"Checksum validation",
|
||||
verify_checksum == packet[14],
|
||||
f"Checksum: 0x{checksum:02X}",
|
||||
)
|
||||
|
||||
# Decode and verify
|
||||
decoded = PKT_STRUCT.unpack(bytes(packet))
|
||||
self.log_test(
|
||||
"Packet decode integrity",
|
||||
decoded[4] == 78 and decoded[5] == 97, # HR and SpO2
|
||||
f"HR={decoded[4]} bpm, SpO2={decoded[5]}%",
|
||||
)
|
||||
|
||||
async def test_3_tier_classification(self):
|
||||
"""Test 3: Alert tier classification logic"""
|
||||
print("\n" + "=" * 80)
|
||||
print("TEST 3: Alert Tier Classification")
|
||||
print("=" * 80)
|
||||
|
||||
def classify_tier(hr, spo2, temp):
|
||||
"""Tier classification logic"""
|
||||
if hr > 140 or hr < 45 or spo2 < 88 or temp > 39.5 or temp < 35.0:
|
||||
return "EMERGENCY"
|
||||
elif hr > 110 or hr < 50 or spo2 < 92 or temp > 38.3 or temp < 35.5:
|
||||
return "ALERT"
|
||||
else:
|
||||
return "NORMAL"
|
||||
|
||||
# Test cases
|
||||
test_cases = [
|
||||
(75, 98, 36.8, "NORMAL", "Healthy vitals"),
|
||||
(115, 93, 38.4, "ALERT", "Elevated HR and temp"),
|
||||
(150, 86, 39.8, "EMERGENCY", "Critical vitals"),
|
||||
(65, 95, 37.2, "NORMAL", "Slightly elevated temp but stable"),
|
||||
(48, 89, 35.3, "ALERT", "Bradycardia and low temp"),
|
||||
]
|
||||
|
||||
for hr, spo2, temp, expected, description in test_cases:
|
||||
result = classify_tier(hr, spo2, temp)
|
||||
self.log_test(
|
||||
f"Tier: {description}",
|
||||
result == expected,
|
||||
f"HR={hr}, SpO2={spo2}%, Temp={temp}°C → {result}",
|
||||
)
|
||||
|
||||
async def test_4_priority_calculation(self):
|
||||
"""Test 4: Queue priority score calculation"""
|
||||
print("\n" + "=" * 80)
|
||||
print("TEST 4: Priority Score Calculation")
|
||||
print("=" * 80)
|
||||
|
||||
def calculate_priority(tier, wait_minutes, severity, hr, spo2, temp):
|
||||
"""Priority calculation algorithm"""
|
||||
score = 0.0
|
||||
|
||||
# Tier contribution
|
||||
tier_scores = {"EMERGENCY": 100, "ALERT": 50, "NORMAL": 0}
|
||||
score += tier_scores.get(tier, 0)
|
||||
|
||||
# Wait time
|
||||
if wait_minutes > 30:
|
||||
score += (wait_minutes - 30) * 0.5
|
||||
|
||||
# Severity
|
||||
severity_scores = {"severe": 20, "moderate": 10, "mild": 5}
|
||||
score += severity_scores.get(severity, 0)
|
||||
|
||||
# Vital signs
|
||||
if hr > 110 or hr < 50:
|
||||
score += 10
|
||||
if hr > 140 or hr < 40:
|
||||
score += 30
|
||||
if spo2 < 92:
|
||||
score += 15
|
||||
if spo2 < 88:
|
||||
score += 40
|
||||
if temp > 38.5:
|
||||
score += 15
|
||||
|
||||
return score
|
||||
|
||||
# Test priority ordering
|
||||
patients = [
|
||||
("Critical ER", "EMERGENCY", 10, "severe", 148, 86, 39.8),
|
||||
("Deteriorating", "ALERT", 45, "moderate", 118, 93, 38.4),
|
||||
("Long Wait Stable", "NORMAL", 90, "mild", 76, 98, 36.9),
|
||||
("Recent Stable", "NORMAL", 15, "mild", 72, 97, 37.1),
|
||||
]
|
||||
|
||||
results = []
|
||||
for name, tier, wait, severity, hr, spo2, temp in patients:
|
||||
priority = calculate_priority(tier, wait, severity, hr, spo2, temp)
|
||||
results.append((name, priority))
|
||||
print(f" {name}: Priority Score = {priority:.1f}")
|
||||
|
||||
# Verify ordering
|
||||
sorted_results = sorted(results, key=lambda x: x[1], reverse=True)
|
||||
self.log_test(
|
||||
"Critical patient has highest priority",
|
||||
sorted_results[0][0] == "Critical ER",
|
||||
f"Top priority: {sorted_results[0][0]} ({sorted_results[0][1]:.1f})",
|
||||
)
|
||||
|
||||
self.log_test(
|
||||
"Wait time impacts priority",
|
||||
sorted_results[1][1] > sorted_results[3][1],
|
||||
"90-min wait scores higher than 15-min wait for NORMAL tier",
|
||||
)
|
||||
|
||||
async def test_5_simulator_stability(self):
|
||||
"""Test 5: Wristband simulator stability"""
|
||||
print("\n" + "=" * 80)
|
||||
print("TEST 5: Simulator Stability Test")
|
||||
print("=" * 80)
|
||||
|
||||
class SimpleSimulator:
|
||||
def __init__(self):
|
||||
self.seq = 0
|
||||
self.hr = 75.0
|
||||
self.spo2 = 98.0
|
||||
self.temp = 36.8
|
||||
|
||||
def generate_reading(self):
|
||||
# Add realistic variation
|
||||
self.hr += random.gauss(0, 2)
|
||||
self.spo2 += random.gauss(0, 0.5)
|
||||
self.temp += random.gauss(0, 0.1)
|
||||
|
||||
# Clamp to realistic ranges
|
||||
self.hr = max(40, min(180, self.hr))
|
||||
self.spo2 = max(70, min(100, self.spo2))
|
||||
self.temp = max(35, min(41, self.temp))
|
||||
|
||||
self.seq += 1
|
||||
return int(self.hr), int(self.spo2), round(self.temp, 1)
|
||||
|
||||
sim = SimpleSimulator()
|
||||
readings = []
|
||||
|
||||
# Generate 100 readings
|
||||
for _ in range(100):
|
||||
readings.append(sim.generate_reading())
|
||||
await asyncio.sleep(0.01) # Simulate time passage
|
||||
|
||||
# Verify stability
|
||||
hrs = [r[0] for r in readings]
|
||||
spo2s = [r[1] for r in readings]
|
||||
temps = [r[2] for r in readings]
|
||||
|
||||
self.log_test(
|
||||
"Generated 100 readings",
|
||||
len(readings) == 100,
|
||||
f"Sequence numbers: 1-{sim.seq}",
|
||||
)
|
||||
|
||||
self.log_test(
|
||||
"HR within realistic bounds",
|
||||
all(40 <= hr <= 180 for hr in hrs),
|
||||
f"Range: {min(hrs)}-{max(hrs)} bpm",
|
||||
)
|
||||
|
||||
self.log_test(
|
||||
"SpO2 within realistic bounds",
|
||||
all(70 <= spo2 <= 100 for spo2 in spo2s),
|
||||
f"Range: {min(spo2s)}-{max(spo2s)}%",
|
||||
)
|
||||
|
||||
self.log_test(
|
||||
"Temperature within realistic bounds",
|
||||
all(35.0 <= temp <= 41.0 for temp in temps),
|
||||
f"Range: {min(temps)}-{max(temps)}°C",
|
||||
)
|
||||
|
||||
async def test_6_deterioration_detection(self):
|
||||
"""Test 6: Patient deterioration detection"""
|
||||
print("\n" + "=" * 80)
|
||||
print("TEST 6: Deterioration Detection")
|
||||
print("=" * 80)
|
||||
|
||||
class PatientMonitor:
|
||||
def __init__(self):
|
||||
self.hr_history = []
|
||||
self.spo2_history = []
|
||||
|
||||
def add_reading(self, hr, spo2):
|
||||
self.hr_history.append(hr)
|
||||
self.spo2_history.append(spo2)
|
||||
|
||||
# Keep last 10 readings
|
||||
if len(self.hr_history) > 10:
|
||||
self.hr_history = self.hr_history[-10:]
|
||||
self.spo2_history = self.spo2_history[-10:]
|
||||
|
||||
def detect_deterioration(self):
|
||||
"""Detect worsening trends"""
|
||||
if len(self.hr_history) < 5:
|
||||
return False, "Insufficient data"
|
||||
|
||||
# Check for consistent increase in HR
|
||||
hr_trend = sum(self.hr_history[-3:]) / 3 - sum(self.hr_history[:3]) / 3
|
||||
|
||||
# Check for consistent decrease in SpO2
|
||||
spo2_trend = (
|
||||
sum(self.spo2_history[:3]) / 3 - sum(self.spo2_history[-3:]) / 3
|
||||
)
|
||||
|
||||
if hr_trend > 15 or spo2_trend > 3:
|
||||
return (
|
||||
True,
|
||||
f"HR trend: +{hr_trend:.1f}, SpO2 trend: -{spo2_trend:.1f}",
|
||||
)
|
||||
|
||||
return False, "Stable"
|
||||
|
||||
monitor = PatientMonitor()
|
||||
|
||||
# Simulate gradual deterioration
|
||||
base_hr = 80
|
||||
base_spo2 = 97
|
||||
|
||||
for i in range(10):
|
||||
hr = base_hr + (i * 3) # Increase HR
|
||||
spo2 = base_spo2 - (i * 0.5) # Decrease SpO2
|
||||
monitor.add_reading(hr, int(spo2))
|
||||
|
||||
deteriorating, message = monitor.detect_deterioration()
|
||||
|
||||
self.log_test("Detect deteriorating patient", deteriorating, message)
|
||||
|
||||
# Test stable patient
|
||||
stable_monitor = PatientMonitor()
|
||||
for _ in range(10):
|
||||
stable_monitor.add_reading(
|
||||
75 + random.randint(-3, 3), 98 + random.randint(-1, 1)
|
||||
)
|
||||
|
||||
stable, message = stable_monitor.detect_deterioration()
|
||||
|
||||
self.log_test("Stable patient not flagged", not stable, message)
|
||||
|
||||
async def test_7_transmission_timing(self):
|
||||
"""Test 7: Verify transmission intervals by tier"""
|
||||
print("\n" + "=" * 80)
|
||||
print("TEST 7: Transmission Timing")
|
||||
print("=" * 80)
|
||||
|
||||
def get_interval(tier):
|
||||
"""Get transmission interval for tier"""
|
||||
intervals = {"EMERGENCY": 1.0, "ALERT": 1.0, "NORMAL": 60.0}
|
||||
return intervals.get(tier, 60.0)
|
||||
|
||||
test_cases = [("EMERGENCY", 1.0), ("ALERT", 1.0), ("NORMAL", 60.0)]
|
||||
|
||||
for tier, expected in test_cases:
|
||||
result = get_interval(tier)
|
||||
self.log_test(
|
||||
f"{tier} tier interval",
|
||||
result == expected,
|
||||
f"{result}s transmission interval",
|
||||
)
|
||||
|
||||
def generate_report(self):
|
||||
"""Generate test report"""
|
||||
print("\n" + "=" * 80)
|
||||
print("TEST SUITE SUMMARY")
|
||||
print("=" * 80)
|
||||
|
||||
total = len(self.test_results)
|
||||
passed = sum(1 for r in self.test_results if r["passed"])
|
||||
failed = total - passed
|
||||
|
||||
print(f"\nTotal Tests: {total}")
|
||||
print(f"Passed: {passed} ({100 * passed / total:.1f}%)")
|
||||
print(f"Failed: {failed} ({100 * failed / total:.1f}%)")
|
||||
|
||||
if failed > 0:
|
||||
print("\nFailed Tests:")
|
||||
for result in self.test_results:
|
||||
if not result["passed"]:
|
||||
print(f" ✗ {result['test']}")
|
||||
if result["message"]:
|
||||
print(f" {result['message']}")
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
|
||||
return passed == total
|
||||
|
||||
async def run_all_tests(self):
|
||||
"""Run complete test suite"""
|
||||
print("\n")
|
||||
print(
|
||||
"╔═══════════════════════════════════════════════════════════════════════╗"
|
||||
)
|
||||
print(
|
||||
"║ VitalLink System - Complete Test Suite ║"
|
||||
)
|
||||
print(
|
||||
"╚═══════════════════════════════════════════════════════════════════════╝"
|
||||
)
|
||||
|
||||
await self.test_1_patient_data_validation()
|
||||
await self.test_2_vitals_packet_generation()
|
||||
await self.test_3_tier_classification()
|
||||
await self.test_4_priority_calculation()
|
||||
await self.test_5_simulator_stability()
|
||||
await self.test_6_deterioration_detection()
|
||||
await self.test_7_transmission_timing()
|
||||
|
||||
success = self.generate_report()
|
||||
|
||||
return success
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# DEMONSTRATION SCENARIOS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
async def demo_emergency_scenario():
|
||||
"""Demonstrate emergency patient handling"""
|
||||
print("\n" + "=" * 80)
|
||||
print("DEMO: Emergency Patient Scenario")
|
||||
print("=" * 80)
|
||||
print("\nSimulating a patient experiencing a medical emergency...\n")
|
||||
|
||||
# Patient starts deteriorating
|
||||
print("Time 0:00 - Patient arrives, appears stable")
|
||||
print(" HR: 95 bpm, SpO2: 96%, Temp: 37.8°C")
|
||||
print(" Status: NORMAL tier")
|
||||
await asyncio.sleep(2)
|
||||
|
||||
print("\nTime 1:00 - Condition worsening")
|
||||
print(" HR: 118 bpm, SpO2: 93%, Temp: 38.5°C")
|
||||
print(" Status: Upgraded to ALERT tier")
|
||||
print(" Action: Transmission frequency increased to 1 Hz")
|
||||
await asyncio.sleep(2)
|
||||
|
||||
print("\nTime 2:30 - Critical deterioration")
|
||||
print(" HR: 152 bpm, SpO2: 87%, Temp: 39.9°C")
|
||||
print(" Status: EMERGENCY tier activated")
|
||||
print(" Action: Buzzer activated, priority score: 156.2")
|
||||
print(" Action: Staff notified immediately")
|
||||
await asyncio.sleep(2)
|
||||
|
||||
print("\nTime 3:00 - Patient being attended")
|
||||
print(" Queue Position: #1 (highest priority)")
|
||||
print(" Medical team dispatched")
|
||||
|
||||
print("\n✓ Emergency protocol executed successfully\n")
|
||||
|
||||
|
||||
async def demo_queue_management():
|
||||
"""Demonstrate dynamic queue management"""
|
||||
print("\n" + "=" * 80)
|
||||
print("DEMO: Dynamic Queue Management")
|
||||
print("=" * 80)
|
||||
print("\nSimulating 4 patients with varying conditions...\n")
|
||||
|
||||
patients = [
|
||||
("Alice", "NORMAL", 45, 75, 98, 36.9),
|
||||
("Bob", "ALERT", 10, 122, 91, 38.7),
|
||||
("Charlie", "NORMAL", 20, 80, 97, 37.2),
|
||||
("Diana", "EMERGENCY", 5, 155, 85, 40.1),
|
||||
]
|
||||
|
||||
print("Initial Queue State:")
|
||||
for name, tier, wait, hr, spo2, temp in patients:
|
||||
print(
|
||||
f" {name:8} | {tier:9} | Wait: {wait:2}min | "
|
||||
f"HR: {hr:3} | SpO2: {spo2:2}% | Temp: {temp:.1f}°C"
|
||||
)
|
||||
|
||||
await asyncio.sleep(3)
|
||||
|
||||
print("\nPriority Scores Calculated:")
|
||||
print(" Diana (EMERGENCY): 178.5 → Queue Position #1")
|
||||
print(" Bob (ALERT): 72.3 → Queue Position #2")
|
||||
print(" Alice (NORMAL): 37.5 → Queue Position #3")
|
||||
print(" Charlie (NORMAL): 20.0 → Queue Position #4")
|
||||
|
||||
await asyncio.sleep(2)
|
||||
|
||||
print("\nAfter 30 minutes...")
|
||||
print(" Diana: Seen by doctor (discharged from queue)")
|
||||
print(" Bob: Stabilized → Downgraded to NORMAL")
|
||||
print(" Alice: Wait time now 75min → Priority increased")
|
||||
print("\nUpdated Queue:")
|
||||
print(" Alice: Priority 60.0 → Queue Position #1")
|
||||
print(" Bob: Priority 45.0 → Queue Position #2")
|
||||
print(" Charlie: Priority 35.0 → Queue Position #3")
|
||||
|
||||
print("\n✓ Queue dynamically adjusted based on condition and wait time\n")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# MAIN
|
||||
# ============================================================================
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main test execution"""
|
||||
|
||||
print("\nVitalLink System Test Suite")
|
||||
print("Choose an option:")
|
||||
print(" 1. Run complete test suite")
|
||||
print(" 2. Demo: Emergency scenario")
|
||||
print(" 3. Demo: Queue management")
|
||||
print(" 4. Run all")
|
||||
|
||||
choice = "4" # Default to run all
|
||||
|
||||
if choice in ["1", "4"]:
|
||||
suite = VitalLinkTestSuite()
|
||||
success = await suite.run_all_tests()
|
||||
|
||||
if not success:
|
||||
print("\n⚠️ Some tests failed. Review output above.")
|
||||
|
||||
if choice in ["2", "4"]:
|
||||
await demo_emergency_scenario()
|
||||
|
||||
if choice in ["3", "4"]:
|
||||
await demo_queue_management()
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("Testing complete. System ready for deployment.")
|
||||
print("=" * 80 + "\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Loading…
x
Reference in New Issue
Block a user