diff --git a/src/data_infra.py b/src/data_infra.py new file mode 100644 index 0000000..92afac6 --- /dev/null +++ b/src/data_infra.py @@ -0,0 +1,52 @@ +"""Infrastruktur: Seekabel, Kernkraftwerke, Militaerbasen.""" +import logging, time, httpx +from fastapi import APIRouter + +logger = logging.getLogger("globe.infra") +router = APIRouter() +_cache = {} + +async def _get_cached(key, url, ttl=86400, parser=None): + if key in _cache and time.time() - _cache[key][0] < ttl: + return _cache[key][1] + try: + async with httpx.AsyncClient(timeout=30) as client: + r = await client.get(url) + r.raise_for_status() + data = parser(r) if parser else r.json() + _cache[key] = (time.time(), data) + return data + except Exception as e: + logger.warning(f"{key}: {e}") + return _cache.get(key, (0, {}))[1] + +@router.get("/submarine-cables") +async def get_cables(): + return await _get_cached("cables", + "https://www.submarinecablemap.com/api/v3/cable/cable-geo.json", 86400) + +@router.get("/nuclear-plants") +async def get_nuclear(): + data = await _get_cached("nuclear", + "https://overpass-api.de/api/interpreter?data=%5Bout%3Ajson%5D%3Bnode%5B%22generator%3Asource%22%3D%22nuclear%22%5D%3Bout%3B", + 86400) + plants = [] + for e in (data.get("elements") or []): + name = e.get("tags", {}).get("name", "Kernkraftwerk") + plants.append({"name": name, "lat": e["lat"], "lon": e["lon"]}) + return {"plants": plants, "total": len(plants)} + +@router.get("/military-bases") +async def get_bases(): + data = await _get_cached("bases", + "https://overpass-api.de/api/interpreter?data=%5Bout%3Ajson%5D%3Bnode%5B%22military%22%3D%22airfield%22%5D%3Bout%3B", + 86400) + bases = [] + for e in (data.get("elements") or []): + name = e.get("tags", {}).get("name", "Militaerbasis") + bases.append({"name": name, "lat": e["lat"], "lon": e["lon"]}) + return {"bases": bases, "total": len(bases)} + +@router.get("/iss") +async def get_iss(): + return await _get_cached("iss", "http://api.open-notify.org/iss-now.json", 5) diff --git a/src/data_military.py b/src/data_military.py new file mode 100644 index 0000000..4c77fc4 --- /dev/null +++ b/src/data_military.py @@ -0,0 +1,49 @@ +"""Militaerflugverkehr: adsb.lol /v2/mil Endpoint.""" +import asyncio, logging, time, httpx +from fastapi import APIRouter + +logger = logging.getLogger("globe.military") +router = APIRouter() +_cache = {"data": None, "ts": 0} +_task = None + +async def _fetch(): + now = time.time() + if _cache["data"] and now - _cache["ts"] < 15: + return _cache["data"] + try: + async with httpx.AsyncClient(timeout=15) as client: + r = await client.get("https://api.adsb.lol/v2/mil") + r.raise_for_status() + ac = [] + for a in r.json().get("ac", []): + if a.get("lat") and a.get("lon"): + ac.append({ + "hex": a.get("hex",""), "flight": (a.get("flight") or "").strip(), + "reg": a.get("r",""), "type": a.get("t",""), + "lat": a["lat"], "lon": a["lon"], + "alt_baro": a.get("alt_baro"), "gs": a.get("gs"), + "track": a.get("track"), "squawk": a.get("squawk",""), + "dbFlags": a.get("dbFlags", 0), + }) + _cache["data"] = {"ac": ac, "total": len(ac)} + _cache["ts"] = time.time() + logger.info(f"Military: {len(ac)} Flugzeuge") + except Exception as e: + logger.warning(f"Military: {e}") + return _cache["data"] or {"ac": [], "total": 0} + +async def _loop(): + await asyncio.sleep(5) + while True: + await _fetch() + await asyncio.sleep(20) + +def start_mil_collector(): + global _task + if _task is None or _task.done(): + _task = asyncio.create_task(_loop()) + +@router.get("/military") +async def get_military(): + return await _fetch() diff --git a/src/main.py b/src/main.py index d397f46..c5263bc 100644 --- a/src/main.py +++ b/src/main.py @@ -33,6 +33,8 @@ from data_quakes import router as quakes_router 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_military import router as military_router, start_mil_collector +from data_infra import router as infra_router from data_monitor import router as monitor_router from data_push import start_push_service @@ -42,6 +44,8 @@ app.include_router(ships_router, prefix="/api", dependencies=[Depends(get_curren app.include_router(quakes_router, prefix="/api", dependencies=[Depends(get_current_user)]) app.include_router(gdelt_router, prefix="/api", dependencies=[Depends(get_current_user)]) app.include_router(satellites_router, prefix="/api", dependencies=[Depends(get_current_user)]) +app.include_router(military_router, prefix="/api", dependencies=[Depends(get_current_user)]) +app.include_router(infra_router, prefix="/api", dependencies=[Depends(get_current_user)]) app.include_router(disasters_router, prefix="/api", dependencies=[Depends(get_current_user)]) app.include_router(monitor_router, prefix="/api", dependencies=[Depends(get_current_user)]) @@ -66,4 +70,5 @@ async def startup(): logger.info("AegisSight Globe gestartet") start_ais_collector() start_flight_collector() + start_mil_collector() start_push_service() diff --git a/static/css/globe.css b/static/css/globe.css index 9286f99..ef66001 100644 --- a/static/css/globe.css +++ b/static/css/globe.css @@ -569,3 +569,8 @@ html, body { height: 100%; overflow: hidden; background: var(--bg-primary); colo } .imagery-select:hover { border-color: var(--accent); } .imagery-select option { background: var(--bg-primary); color: var(--text); } + +.dot-military { background: #ff2222; box-shadow: 0 0 4px rgba(255,34,34,0.5); } +.dot-cables { background: #00ccff; } +.dot-infra { background: #ffdd00; } +.dot-iss { background: #ff4444; box-shadow: 0 0 6px rgba(255,68,68,0.6); } diff --git a/static/index.html b/static/index.html index ef09bb1..db8075b 100644 --- a/static/index.html +++ b/static/index.html @@ -59,6 +59,13 @@
+ +
+ +
+ +
+