diff --git a/src/data_push.py b/src/data_push.py new file mode 100644 index 0000000..1deb30d --- /dev/null +++ b/src/data_push.py @@ -0,0 +1,121 @@ +"""Auto-Push: Sendet Globe-Ereignisse an den AegisSight Monitor.""" +import asyncio +import logging +import os +import time + +import httpx + +logger = logging.getLogger("globe.push") + +_MONITOR_URL = os.getenv("MONITOR_API_URL", "https://monitor.aegis-sight.de/api/public") +_MONITOR_KEY = os.getenv("MONITOR_API_KEY", "") +_DISASTER_INCIDENT_ID = int(os.getenv("DISASTER_INCIDENT_ID", "0")) + +_task = None +_last_push: dict = {"eonet": 0, "usgs": 0} + + +async def _push_to_monitor(events: list): + """Sendet Events an den Monitor-Ingest-Endpoint.""" + if not _DISASTER_INCIDENT_ID or not _MONITOR_KEY or not events: + return 0 + try: + async with httpx.AsyncClient(timeout=15) as client: + r = await client.post( + f"{_MONITOR_URL}/globe-ingest", + json={"incident_id": _DISASTER_INCIDENT_ID, "events": events}, + headers={"X-API-Key": _MONITOR_KEY}, + ) + if r.status_code == 200: + data = r.json() + inserted = data.get("inserted", 0) + if inserted > 0: + logger.info(f"Push: {inserted} Ereignisse an Monitor gesendet") + return inserted + else: + logger.warning(f"Push Fehler: {r.status_code} {r.text[:200]}") + except Exception as e: + logger.warning(f"Push Fehler: {e}") + return 0 + + +async def _push_loop(): + """Periodisch EONET + USGS Daten an Monitor pushen.""" + await asyncio.sleep(30) # Warten bis Daten geladen + while True: + try: + events = [] + + # NASA EONET Katastrophen + try: + async with httpx.AsyncClient(timeout=10) as client: + r = await client.get("https://eonet.gsfc.nasa.gov/api/v3/events?status=open&limit=50") + if r.status_code == 200: + for evt in r.json().get("events", []): + geom = evt.get("geometry", []) + if not geom: + continue + latest = geom[-1] + coords = latest.get("coordinates", []) + if len(coords) < 2: + continue + cats = evt.get("categories", []) + cat_name = cats[0]["title"] if cats else "Unbekannt" + events.append({ + "title": f"[{cat_name}] {evt.get('title', '?')}", + "source": "NASA EONET", + "url": evt.get("link", ""), + "description": f"Naturereignis: {evt.get('title', '')}. " + f"Kategorie: {cat_name}. " + f"Quellen: {', '.join(s.get('id','') for s in evt.get('sources',[]))}", + "lat": coords[1], + "lon": coords[0], + "location": evt.get("title", "")[:50], + "category": "primary", + }) + except Exception as e: + logger.warning(f"EONET fetch: {e}") + + # USGS Erdbeben (nur M4.5+ fuer Monitor — kleinere sind zu viele) + try: + async with httpx.AsyncClient(timeout=10) as client: + r = await client.get("https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/4.5_day.geojson") + if r.status_code == 200: + for f in r.json().get("features", []): + c = f["geometry"]["coordinates"] + p = f["properties"] + mag = p.get("mag", 0) + events.append({ + "title": f"[Erdbeben M{mag:.1f}] {p.get('place', '?')}", + "source": "USGS Earthquake", + "url": p.get("url", ""), + "description": f"Erdbeben der Staerke {mag:.1f} bei {p.get('place', '?')}. " + f"Tiefe: {c[2]:.0f} km. " + f"Zeit: {p.get('time', '')}", + "lat": c[1], + "lon": c[0], + "location": p.get("place", "")[:50], + "category": "primary" if mag >= 6 else "secondary", + }) + except Exception as e: + logger.warning(f"USGS fetch: {e}") + + if events: + await _push_to_monitor(events) + + except Exception as e: + logger.warning(f"Push loop error: {e}") + + await asyncio.sleep(600) # Alle 10 Minuten + + +def start_push_service(): + """Startet den Auto-Push Background-Task.""" + global _task + if not _DISASTER_INCIDENT_ID: + logger.info("Push: DISASTER_INCIDENT_ID nicht gesetzt, Push deaktiviert") + return + if _task is None or _task.done(): + _task = asyncio.create_task(_push_loop()) + logger.info(f"Push-Service gestartet (Lage-ID: {_DISASTER_INCIDENT_ID})") diff --git a/src/main.py b/src/main.py index 7f26d5d..d397f46 100644 --- a/src/main.py +++ b/src/main.py @@ -34,6 +34,7 @@ from data_gdelt import router as gdelt_router from data_satellites import router as satellites_router from data_disasters import router as disasters_router from data_monitor import router as monitor_router +from data_push import start_push_service # Alle Daten-APIs hinter Auth app.include_router(flights_router, prefix="/api", dependencies=[Depends(get_current_user)]) @@ -65,3 +66,4 @@ async def startup(): logger.info("AegisSight Globe gestartet") start_ais_collector() start_flight_collector() + start_push_service() diff --git a/static/js/layers/disasters.js b/static/js/layers/disasters.js index 11e5944..f7e5c57 100644 --- a/static/js/layers/disasters.js +++ b/static/js/layers/disasters.js @@ -1,11 +1,13 @@ /** - * Katastrophen-Layer: NASA EONET + USGS Erdbeben kombiniert. + * Katastrophen-Layer: NASA EONET + USGS Erdbeben. + * Bei Klick: zeigt Details + Monitor-Zusammenfassung falls verfuegbar. */ const DisastersLayer = { _viewer: null, _dataSource: null, _interval: null, _count: 0, + _monitorData: null, start(viewer) { if (this._dataSource) return; @@ -13,6 +15,7 @@ const DisastersLayer = { this._dataSource = new Cesium.CustomDataSource('disasters'); viewer.dataSources.add(this._dataSource); this._fetch(); + this._fetchMonitorContext(); var self = this; this._interval = setInterval(function() { self._fetch(); }, 600000); }, @@ -23,6 +26,47 @@ const DisastersLayer = { this._count = 0; }, + _fetchMonitorContext() { + var self = this; + fetch('/api/monitor-feed') + .then(function(r) { return r.json(); }) + .then(function(data) { self._monitorData = data; }) + .catch(function() {}); + }, + + _findMonitorSummary(lat, lon) { + if (!this._monitorData || !this._monitorData.incidents) return null; + // Finde naechsten Monitor-Punkt und dessen Lage-Summary + var features = this._monitorData.features || []; + var best = null, bestDist = 999; + for (var i = 0; i < features.length; i++) { + var c = features[i].geometry.coordinates; + var d = Math.abs(c[1] - lat) + Math.abs(c[0] - lon); + if (d < bestDist) { bestDist = d; best = features[i]; } + } + if (!best || bestDist > 5) return null; + var incId = best.properties.incident_id; + var incidents = this._monitorData.incidents || []; + for (var j = 0; j < incidents.length; j++) { + if (incidents[j].id === incId && incidents[j].summary) return incidents[j]; + } + return null; + }, + + _buildMonitorHtml(incident) { + if (!incident || !incident.summary) return ''; + var summary = incident.summary + .replace(/##?\s*/g, '').replace(/\*\*/g, '').replace(/\[(\d+)\]/g, '') + .substring(0, 600); + return '