""" LIFE Live Data API Exponerar realtidsdata från multi-source pipeline """ from fastapi import APIRouter import sqlite3 from datetime import datetime, timedelta router = APIRouter(prefix="/live", tags=["live_data"]) DB_PATH = "/home/bernt/.openclaw/workspace/rivp-pilot-1/rivp.db" def get_db(): conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row return conn @router.get("/public/status") async def get_live_status(): """Get current system status""" conn = get_db() c = conn.cursor() # Total counts c.execute("SELECT COUNT(*) FROM roads") total_roads = c.fetchone()[0] c.execute("SELECT COUNT(*) FROM observations") total_obs = c.fetchone()[0] # Today's observations c.execute("SELECT COUNT(*) FROM observations WHERE detected_date = date('now')") today_obs = c.fetchone()[0] # By source c.execute("SELECT source, COUNT(*) as count FROM observations GROUP BY source ORDER BY count DESC") sources = {row[0]: row[1] for row in c.fetchall()} # Recent activity (last hour) c.execute(""" SELECT COUNT(*) FROM observations WHERE datetime(created_at) > datetime('now', '-1 hour') """) last_hour = c.fetchone()[0] # Reality Latency calculation c.execute(""" SELECT AVG( julianday('now') - julianday(detected_date) ) * 24 as avg_latency_hours FROM observations WHERE detected_date >= date('now', '-7 days') """) avg_latency = c.fetchone()[0] or 0 conn.close() return { "timestamp": datetime.now().isoformat(), "system_status": "operational", "data_freshness": { "total_roads": total_roads, "total_observations": total_obs, "observations_today": today_obs, "observations_last_hour": last_hour, "reality_latency_hours": round(avg_latency, 1) }, "sources": sources, "pipeline_status": { "trafikverket": "active", "smhi": "active", "quixzoom": "active", "satellite": "active" } } @router.get("/public/observations/stream") async def get_observation_stream(limit: int = 50): """Get recent observations as a stream""" conn = get_db() c = conn.cursor() c.execute(""" SELECT o.*, r.name as road_name, r.county, r.road_number FROM observations o JOIN roads r ON o.road_id = r.id ORDER BY o.created_at DESC LIMIT ? """, (limit,)) observations = [dict(row) for row in c.fetchall()] conn.close() return { "count": len(observations), "observations": observations } @router.get("/public/alerts") async def get_active_alerts(): """Get active alerts (critical/high severity)""" conn = get_db() c = conn.cursor() c.execute(""" SELECT o.*, r.name as road_name, r.county FROM observations o JOIN roads r ON o.road_id = r.id WHERE o.severity IN ('critical', 'high') AND o.detected_date >= date('now', '-7 days') ORDER BY CASE o.severity WHEN 'critical' THEN 1 WHEN 'high' THEN 2 ELSE 3 END, o.detected_date DESC """) alerts = [dict(row) for row in c.fetchall()] conn.close() return { "alert_count": len(alerts), "critical_count": sum(1 for a in alerts if a.get('severity') == 'critical'), "high_count": sum(1 for a in alerts if a.get('severity') == 'high'), "alerts": alerts } @router.get("/public/coverage") async def get_coverage_map(): """Get coverage data for map visualization""" conn = get_db() c = conn.cursor() # Get all observations with coordinates c.execute(""" SELECT o.latitude, o.longitude, o.observation_type, o.severity, o.confidence, r.name as road_name, r.county FROM observations o JOIN roads r ON o.road_id = r.id WHERE o.latitude != 0 AND o.longitude != 0 ORDER BY o.detected_date DESC LIMIT 500 """) points = [] for row in c.fetchall(): points.append({ "lat": row[0], "lon": row[1], "type": row[2], "severity": row[3], "confidence": row[4], "road": row[5], "county": row[6] }) # Coverage by county c.execute(""" SELECT r.county, COUNT(DISTINCT r.id) as roads, COUNT(o.id) as observations, AVG(o.confidence) as avg_confidence FROM roads r LEFT JOIN observations o ON r.id = o.road_id GROUP BY r.county ORDER BY observations DESC """) coverage = [] for row in c.fetchall(): coverage.append({ "county": row[0], "roads": row[1], "observations": row[2], "avg_confidence": round(row[3] or 0, 2) }) conn.close() return { "total_points": len(points), "points": points, "coverage_by_county": coverage }