diff --git a/USAGE.md b/USAGE.md index e4aa8e7..3bb4d60 100644 --- a/USAGE.md +++ b/USAGE.md @@ -268,6 +268,7 @@ The web server exposes a REST API: | Method | Endpoint | Description | |--------|----------|-------------| +| GET | `/api/health` | Health check for monitoring | | POST | `/api/scan` | Trigger new scan | | GET | `/api/latest` | Get most recent scan | | GET | `/api/scans` | List all scans | diff --git a/docs/API.md b/docs/API.md index 29bbc37..bad5280 100644 --- a/docs/API.md +++ b/docs/API.md @@ -6,6 +6,65 @@ REST API documentation for RF Mapper web interface. --- +## System + +### Health Check + +Returns health status for monitoring and load balancers. + +``` +GET /api/health +``` + +**Response:** + +```json +{ + "status": "healthy", + "version": "1.0.0", + "uptime_seconds": 3600, + "uptime_human": "1h 0m", + "scanner_id": "rpios", + "components": { + "database": { + "status": "ok", + "device_count": 100 + }, + "peer_sync": { + "status": "ok", + "peer_count": 2 + }, + "auto_scanner": { + "status": "stopped" + } + } +} +``` + +**Status Codes:** +- `200` - Healthy +- `503` - Unhealthy (component error) + +**Component Status Values:** +- `ok` - Component working normally +- `disabled` - Component not enabled in config +- `error` - Component has errors +- `running` / `stopped` - For auto_scanner + +**Example:** + +```bash +# Simple health check +curl -s http://localhost:5000/api/health | jq '.status' + +# Use in monitoring scripts +if curl -sf http://localhost:5000/api/health > /dev/null; then + echo "RF Mapper is healthy" +fi +``` + +--- + ## Scanning ### Trigger Scan diff --git a/src/rf_mapper/web/app.py b/src/rf_mapper/web/app.py index d3d783e..e70f79f 100644 --- a/src/rf_mapper/web/app.py +++ b/src/rf_mapper/web/app.py @@ -17,6 +17,7 @@ from ..config import Config, get_config from ..bluetooth_identify import identify_single_device, identify_device from ..database import DeviceDatabase, init_database, get_database from ..homeassistant import HAWebhooks, HAWebhookConfig +from .. import __version__ # Module-level SocketIO instance socketio = SocketIO() @@ -237,6 +238,7 @@ def create_app(config: Config | None = None) -> Flask: # Store config reference app.config["RF_CONFIG"] = config + app.config["START_TIME"] = datetime.now() # Initialize SocketIO with threading mode (compatible with existing threads) socketio.init_app(app, cors_allowed_origins="*", async_mode="threading") @@ -397,6 +399,70 @@ def create_app(config: Config | None = None) -> Flask: } ) + @app.route("/api/health") + def api_health(): + """Health check endpoint for monitoring""" + start_time = app.config.get("START_TIME", datetime.now()) + uptime_seconds = (datetime.now() - start_time).total_seconds() + + # Check component status + db = app.config.get("DATABASE") + peer_sync = app.config.get("PEER_SYNC") + auto_scanner = app.config.get("AUTO_SCANNER") + + # Database status + db_status = "disabled" + db_device_count = 0 + if db: + try: + db_device_count = len(db.get_all_devices()) + db_status = "ok" + except Exception: + db_status = "error" + + # Peer sync status + peers_status = "disabled" + peers_count = 0 + if peer_sync: + try: + sync_status = peer_sync.get_status() + peers_count = len(sync_status.get("peers", [])) + peers_status = "ok" + except Exception: + peers_status = "error" + + # Auto scanner status + autoscan_status = "stopped" + if auto_scanner and auto_scanner._enabled: + autoscan_status = "running" + + # Overall health + is_healthy = db_status != "error" and peers_status != "error" + + response = { + "status": "healthy" if is_healthy else "unhealthy", + "version": __version__, + "uptime_seconds": int(uptime_seconds), + "uptime_human": f"{int(uptime_seconds // 3600)}h {int((uptime_seconds % 3600) // 60)}m", + "scanner_id": app.config.get("SCANNER_IDENTITY", {}).get("id", ""), + "components": { + "database": { + "status": db_status, + "device_count": db_device_count + }, + "peer_sync": { + "status": peers_status, + "peer_count": peers_count + }, + "auto_scanner": { + "status": autoscan_status + } + } + } + + status_code = 200 if is_healthy else 503 + return jsonify(response), status_code + @app.route("/api/scan", methods=["POST"]) def api_scan(): """Trigger a new RF scan"""