Initial commit
This commit is contained in:
+325
@@ -0,0 +1,325 @@
|
||||
import asyncio
|
||||
import json
|
||||
from fastapi import FastAPI, WebSocket
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from contextlib import asynccontextmanager
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables before anything else
|
||||
load_dotenv()
|
||||
|
||||
from services.opensky import fetch_planes
|
||||
from services.satellites import get_satellite_positions
|
||||
from services.news import fetch_news
|
||||
from services.ai import analyze_threat
|
||||
from services.webcams import fetch_webcams
|
||||
from services.bgp import fetch_bgp_status
|
||||
from services.ais import fetch_ships, ais_worker, ais_pruner
|
||||
from services.conflicts import fetch_conflicts
|
||||
from services.cyber import fetch_cyber_warfare
|
||||
|
||||
HISTORY_DIR = Path(__file__).parent / "data_history"
|
||||
HISTORY_DIR.mkdir(exist_ok=True)
|
||||
MAX_SNAPSHOTS = 120 # keep last ~2 hours at 1min intervals
|
||||
|
||||
clients: set = set()
|
||||
current_data = {
|
||||
"planes": [],
|
||||
"satellites": [],
|
||||
"news": [],
|
||||
"gps_interference": [],
|
||||
"emergency_squawks": [],
|
||||
"ai_analysis": {
|
||||
"summary": "System initializing...",
|
||||
"threat_level": "UNKNOWN",
|
||||
"predictions": []
|
||||
},
|
||||
"webcams": [],
|
||||
"bgp": {},
|
||||
"ships": [],
|
||||
"conflicts": [],
|
||||
"cyber_attacks": [],
|
||||
"last_updated": {}
|
||||
}
|
||||
|
||||
# Broadcast cache — only rebuilt when data changes
|
||||
_full_payload_cache: str | None = None
|
||||
_full_payload_version: int = 0
|
||||
_data_version: dict[str, int] = {}
|
||||
|
||||
def _ts():
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
def _mark_changed(key: str):
|
||||
global _full_payload_cache
|
||||
_data_version[key] = _data_version.get(key, 0) + 1
|
||||
_full_payload_cache = None
|
||||
|
||||
def _get_broadcast_payload() -> str:
|
||||
global _full_payload_cache
|
||||
# No aggressive version caching to ensure immediate movement
|
||||
|
||||
return json.dumps({"type": "update", **current_data}, separators=(',', ':'))
|
||||
|
||||
|
||||
async def update_maritime():
|
||||
while True:
|
||||
try:
|
||||
current_data["ships"] = await fetch_ships()
|
||||
current_data["last_updated"]["ships"] = _ts()
|
||||
_mark_changed("ships")
|
||||
except Exception as e:
|
||||
print(f"[MARITIME] Error: {e}")
|
||||
await asyncio.sleep(30)
|
||||
|
||||
async def update_planes_and_gps():
|
||||
while True:
|
||||
try:
|
||||
result = await fetch_planes()
|
||||
if isinstance(result, dict) and result.get("planes"):
|
||||
current_data["planes"] = result.get("planes", [])
|
||||
current_data["gps_interference"] = result.get("interference", [])
|
||||
current_data["emergency_squawks"] = result.get("emergencies", [])
|
||||
current_data["last_updated"]["planes"] = _ts()
|
||||
_mark_changed("planes")
|
||||
except Exception as e:
|
||||
print(f"[AIRSPACE] Error: {e}")
|
||||
await asyncio.sleep(20)
|
||||
|
||||
async def update_satellites():
|
||||
while True:
|
||||
try:
|
||||
# Satellites positions are computed from TLEs.
|
||||
# The TLE fetch itself should be cached in satellites.py.
|
||||
current_data["satellites"] = await get_satellite_positions()
|
||||
current_data["last_updated"]["satellites"] = _ts()
|
||||
_mark_changed("satellites")
|
||||
except Exception as e:
|
||||
print(f"[SPACE] Error: {e}")
|
||||
await asyncio.sleep(60) # Position computation update interval
|
||||
|
||||
async def update_cyber():
|
||||
while True:
|
||||
try:
|
||||
bgp = current_data.get("bgp")
|
||||
cyber_data = await fetch_cyber_warfare(bgp)
|
||||
if cyber_data: # Only update if we got real data
|
||||
current_data["cyber_attacks"] = cyber_data
|
||||
current_data["last_updated"]["cyber_attacks"] = _ts()
|
||||
_mark_changed("cyber_attacks")
|
||||
except Exception as e:
|
||||
print(f"[CYBER] Error: {e}")
|
||||
await asyncio.sleep(300) # ThreatFox limits (5 mins)
|
||||
|
||||
async def update_news_and_ai():
|
||||
while True:
|
||||
try:
|
||||
news = await fetch_news()
|
||||
bgp = await fetch_bgp_status()
|
||||
if news: current_data["news"] = news
|
||||
if bgp: current_data["bgp"] = bgp
|
||||
current_data["last_updated"]["news"] = _ts()
|
||||
_mark_changed("news")
|
||||
|
||||
if current_data["news"] or current_data["conflicts"]:
|
||||
analysis = await analyze_threat(
|
||||
current_data["news"],
|
||||
current_data["gps_interference"],
|
||||
current_data["bgp"],
|
||||
conflicts=current_data["conflicts"],
|
||||
ships=current_data["ships"],
|
||||
planes=current_data["planes"],
|
||||
)
|
||||
current_data["ai_analysis"] = analysis
|
||||
_mark_changed("ai")
|
||||
except Exception as e:
|
||||
print(f"[INTEL] Error: {e}")
|
||||
await asyncio.sleep(300) # RSS and AI limits (5 mins)
|
||||
|
||||
|
||||
async def update_conflicts():
|
||||
while True:
|
||||
try:
|
||||
conflicts = await fetch_conflicts()
|
||||
if conflicts:
|
||||
current_data["conflicts"] = conflicts
|
||||
current_data["last_updated"]["conflicts"] = _ts()
|
||||
_mark_changed("conflicts")
|
||||
except Exception as e:
|
||||
print(f"[CONFLICTS] Error: {e}")
|
||||
await asyncio.sleep(600) # GDELT data (10 mins)
|
||||
|
||||
|
||||
async def update_webcams():
|
||||
while True:
|
||||
try:
|
||||
webcams = await fetch_webcams()
|
||||
if webcams:
|
||||
current_data["webcams"] = webcams
|
||||
current_data["last_updated"]["webcams"] = _ts()
|
||||
_mark_changed("webcams")
|
||||
except Exception as e:
|
||||
print(f"[WEBCAMS] Error: {e}")
|
||||
await asyncio.sleep(3600) # Rarely changes
|
||||
|
||||
|
||||
async def broadcast_updates():
|
||||
last_version = -1
|
||||
while True:
|
||||
if clients and _full_payload_version != last_version:
|
||||
payload = _get_broadcast_payload()
|
||||
last_version = _full_payload_version
|
||||
dead_clients: set = set()
|
||||
for client in list(clients):
|
||||
try:
|
||||
await client.send_text(payload)
|
||||
except Exception:
|
||||
dead_clients.add(client)
|
||||
clients.difference_update(dead_clients)
|
||||
await asyncio.sleep(2)
|
||||
|
||||
|
||||
def _save_snapshot():
|
||||
"""Save a compact snapshot to disk for historical review."""
|
||||
ts = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
||||
snapshot = {
|
||||
"timestamp": _ts(),
|
||||
"planes_count": len(current_data["planes"]),
|
||||
"ships_count": len(current_data["ships"]),
|
||||
"satellites_count": len(current_data["satellites"]),
|
||||
"planes": current_data["planes"],
|
||||
"ships": current_data["ships"],
|
||||
"gps_interference": current_data["gps_interference"],
|
||||
"emergency_squawks": current_data["emergency_squawks"],
|
||||
"news": current_data["news"],
|
||||
"conflicts": current_data["conflicts"],
|
||||
"ai_analysis": current_data["ai_analysis"],
|
||||
}
|
||||
path = HISTORY_DIR / f"{ts}.json"
|
||||
path.write_text(json.dumps(snapshot, separators=(',', ':')))
|
||||
# Prune old snapshots
|
||||
files = sorted(HISTORY_DIR.glob("*.json"))
|
||||
for f in files[:-MAX_SNAPSHOTS]:
|
||||
f.unlink(missing_ok=True)
|
||||
|
||||
|
||||
async def save_snapshots():
|
||||
"""Periodically save data snapshots for time-travel review."""
|
||||
while True:
|
||||
await asyncio.sleep(60)
|
||||
try:
|
||||
_save_snapshot()
|
||||
except Exception as e:
|
||||
print(f"[HISTORY] Snapshot error: {e}")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
tasks = [
|
||||
asyncio.create_task(ais_worker()),
|
||||
asyncio.create_task(ais_pruner()),
|
||||
asyncio.create_task(update_maritime()),
|
||||
asyncio.create_task(update_planes_and_gps()),
|
||||
asyncio.create_task(update_satellites()),
|
||||
asyncio.create_task(update_cyber()),
|
||||
asyncio.create_task(update_news_and_ai()),
|
||||
asyncio.create_task(update_webcams()),
|
||||
asyncio.create_task(update_conflicts()),
|
||||
asyncio.create_task(broadcast_updates()),
|
||||
asyncio.create_task(save_snapshots()),
|
||||
]
|
||||
# Save initial snapshot once data loads
|
||||
print("[GOD'S EYE] All 9 data pipelines online.")
|
||||
yield
|
||||
for t in tasks:
|
||||
t.cancel()
|
||||
|
||||
|
||||
app = FastAPI(lifespan=lifespan)
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {
|
||||
"status": "ONLINE",
|
||||
"system": "GOD'S EYE",
|
||||
"version": "2.1.0",
|
||||
"active_clients": len(clients),
|
||||
"data_counts": {
|
||||
"planes": len(current_data["planes"]),
|
||||
"satellites": len(current_data["satellites"]),
|
||||
"ships": len(current_data["ships"]),
|
||||
"news": len(current_data["news"]),
|
||||
"webcams": len(current_data["webcams"]),
|
||||
"gps_interference": len(current_data["gps_interference"]),
|
||||
"emergencies": len(current_data["emergency_squawks"]),
|
||||
"conflicts": len(current_data["conflicts"]),
|
||||
},
|
||||
"last_updated": current_data["last_updated"]
|
||||
}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "healthy", "timestamp": _ts()}
|
||||
|
||||
|
||||
@app.get("/history")
|
||||
async def list_history():
|
||||
"""List available data snapshots for time-travel review."""
|
||||
files = sorted(HISTORY_DIR.glob("*.json"), reverse=True)
|
||||
snapshots = []
|
||||
for f in files[:MAX_SNAPSHOTS]:
|
||||
try:
|
||||
meta = json.loads(f.read_text())
|
||||
snapshots.append({
|
||||
"id": f.stem,
|
||||
"timestamp": meta.get("timestamp", ""),
|
||||
"planes": meta.get("planes_count", 0),
|
||||
"ships": meta.get("ships_count", 0),
|
||||
"gps_zones": len(meta.get("gps_interference", [])),
|
||||
"news": len(meta.get("news", [])),
|
||||
"threat_level": meta.get("ai_analysis", {}).get("threat_level", ""),
|
||||
})
|
||||
except Exception:
|
||||
continue
|
||||
return {"snapshots": snapshots}
|
||||
|
||||
|
||||
@app.get("/history/{snapshot_id}")
|
||||
async def get_snapshot(snapshot_id: str):
|
||||
"""Retrieve a specific historical data snapshot."""
|
||||
path = HISTORY_DIR / f"{snapshot_id}.json"
|
||||
if not path.exists():
|
||||
return {"error": "Snapshot not found"}, 404
|
||||
return json.loads(path.read_text())
|
||||
|
||||
|
||||
@app.websocket("/ws")
|
||||
async def websocket_endpoint(websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
clients.add(websocket)
|
||||
print(f"[WS] Client connected. Total: {len(clients)}")
|
||||
try:
|
||||
await websocket.send_text(_get_broadcast_payload())
|
||||
while True:
|
||||
await websocket.receive_text()
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
clients.discard(websocket)
|
||||
print(f"[WS] Client disconnected. Total: {len(clients)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
@@ -0,0 +1,66 @@
|
||||
SAPPHIRE
|
||||
1 39088U 13009C 26065.47075351 .00000262 00000+0 10652-3 0 9998
|
||||
2 39088 98.4217 247.3748 0011019 189.2237 170.8748 14.35207448681840
|
||||
PRAETORIAN SDA_602
|
||||
1 65565U 25203A 26065.53646411 .00000295 00000+0 25293-3 0 9997
|
||||
2 65565 81.3005 105.8613 0007137 109.7019 250.4908 13.84333321 24493
|
||||
PRAETORIAN SDA_603
|
||||
1 65566U 25203B 26065.16936989 .00000276 00000+0 23831-3 0 9994
|
||||
2 65566 81.3005 106.0435 0010786 110.9189 249.3123 13.83902996 24456
|
||||
PRAETORIAN SDA_604
|
||||
1 65567U 25203C 26065.55906540 .00000072 00000+0 54953-4 0 9992
|
||||
2 65567 81.3025 105.9374 0006760 115.4529 244.7327 13.83033880 24490
|
||||
PRAETORIAN SDA_605
|
||||
1 65568U 25203D 26065.33832279 .00000074 00000+0 56700-4 0 9990
|
||||
2 65568 81.2987 105.9095 0009511 106.4694 253.7508 13.83528375 24474
|
||||
PRAETORIAN SDA_606
|
||||
1 65569U 25203E 26065.25155414 .00000322 00000+0 28109-3 0 9998
|
||||
2 65569 81.3026 106.1779 0007077 107.1531 253.0400 13.83531623 24457
|
||||
PRAETORIAN SDA_607
|
||||
1 65570U 25203F 26065.24940318 .00000296 00000+0 25385-3 0 9997
|
||||
2 65570 81.3009 105.9952 0010091 109.8985 250.3260 13.84465383 24467
|
||||
PRAETORIAN SDA_608
|
||||
1 65571U 25203G 26065.19059354 .00000074 00000+0 55683-4 0 9992
|
||||
2 65571 81.2978 106.1674 0007160 115.8619 244.3276 13.83886404 24448
|
||||
PRAETORIAN SDA_609
|
||||
1 65572U 25203H 26065.51803163 .00000278 00000+0 23808-3 0 9993
|
||||
2 65572 81.3023 105.8730 0008193 109.0518 251.1527 13.84314594 24497
|
||||
PRAETORIAN SDA_610
|
||||
1 65573U 25203J 26065.24703745 .00000085 00000+0 65617-4 0 9990
|
||||
2 65573 81.2980 106.0813 0007780 111.1410 249.0578 13.84010751 24456
|
||||
PRAETORIAN SDA_611
|
||||
1 65574U 25203K 26065.34769689 .00000179 00000+0 15255-3 0 9997
|
||||
2 65574 81.2971 106.0593 0006835 118.6750 241.5094 13.83295429 24468
|
||||
PRAETORIAN SDA_612
|
||||
1 65575U 25203L 26065.24894417 .00000292 00000+0 24752-3 0 9993
|
||||
2 65575 81.3000 105.9897 0010519 111.0294 249.1989 13.85045152 24467
|
||||
PRAETORIAN SDA_613
|
||||
1 65576U 25203M 26065.54178680 .00000257 00000+0 22489-3 0 9990
|
||||
2 65576 81.2970 105.8198 0006998 110.7862 249.4044 13.82987768 24492
|
||||
PRAETORIAN SDA_614
|
||||
1 65577U 25203N 26065.50777911 .00000081 00000+0 61192-4 0 9999
|
||||
2 65577 81.2984 105.8019 0008398 108.5892 251.6178 13.84765324 24503
|
||||
PRAETORIAN SDA_615
|
||||
1 65578U 25203P 26065.24244925 .00000314 00000+0 26979-3 0 9997
|
||||
2 65578 81.2996 105.9469 0010262 109.1811 251.0457 13.84422280 24468
|
||||
PRAETORIAN SDA_616
|
||||
1 65579U 25203Q 26065.56391622 .00000291 00000+0 25411-3 0 9994
|
||||
2 65579 81.3034 105.9428 0006564 117.3925 242.7900 13.83298187 24494
|
||||
PRAETORIAN SDA_617
|
||||
1 65580U 25203R 26065.57654382 .00000075 00000+0 57336-4 0 9994
|
||||
2 65580 81.3007 105.7715 0008747 108.7614 251.4492 13.83191038 24509
|
||||
PRAETORIAN SDA_618
|
||||
1 65581U 25203S 26065.23234500 .00000354 00000+0 30400-3 0 9996
|
||||
2 65581 81.2995 105.9388 0011031 110.9885 249.2453 13.84762398 24469
|
||||
PRAETORIAN SDA_619
|
||||
1 65582U 25203T 26065.35075271 .00000334 00000+0 29031-3 0 9994
|
||||
2 65582 81.3014 105.9683 0009185 108.4245 251.7911 13.83930042 25101
|
||||
PRAETORIAN SDA_620
|
||||
1 65583U 25203U 26065.50393821 .00000386 00000+0 33873-3 0 9992
|
||||
2 65583 81.3001 105.8217 0008644 105.1119 255.0995 13.83705885 24498
|
||||
PRAETORIAN SDA_621
|
||||
1 65584U 25203V 26065.56050255 .00000089 00000+0 69455-4 0 9996
|
||||
2 65584 81.2992 105.7187 0008669 103.5537 256.6586 13.83787042 24505
|
||||
PRAETORIAN SDA_601
|
||||
1 65585U 25203W 26065.49920927 .00000441 00000+0 38976-3 0 9996
|
||||
2 65585 81.3000 105.9863 0006526 116.4493 243.7334 13.83484967 24489
|
||||
@@ -0,0 +1,519 @@
|
||||
GPS BIIR-2 (PRN 13)
|
||||
1 24876U 97035A 26064.99563334 .00000005 00000+0 00000+0 0 9994
|
||||
2 24876 55.9363 102.6088 0097990 56.2514 304.7521 2.00564004209882
|
||||
GPS BIIR-5 (PRN 22)
|
||||
1 26407U 00040A 26065.82464189 .00000025 00000+0 00000+0 0 9999
|
||||
2 26407 54.8750 219.2256 0122644 302.5332 230.2733 2.00568995187891
|
||||
GPS BIIR-8 (PRN 16)
|
||||
1 27663U 03005A 26065.41226067 .00000020 00000+0 00000+0 0 9999
|
||||
2 27663 54.9069 219.0498 0148732 52.7313 318.1949 2.00566200169257
|
||||
GPS BIIR-11 (PRN 19)
|
||||
1 28190U 04009A 26064.93315625 -.00000023 00000+0 00000+0 0 9999
|
||||
2 28190 54.9146 279.8578 0111025 169.1048 11.7386 2.00558087160879
|
||||
GPS BIIR-13 (PRN 02)
|
||||
1 28474U 04045A 26065.34106905 -.00000095 00000+0 00000+0 0 9998
|
||||
2 28474 55.1936 329.0412 0170812 312.9072 225.9723 2.00573554156362
|
||||
GPS BIIRM-1 (PRN 17)
|
||||
1 28874U 05038A 26065.65681979 -.00000017 00000+0 00000+0 0 9992
|
||||
2 28874 54.9613 277.3140 0128744 295.3788 71.8812 2.00572910149783
|
||||
INMARSAT 4-F2 (SOUTHPA*)
|
||||
1 28899U 05044A 26065.32477566 -.00000264 00000+0 00000+0 0 9999
|
||||
2 28899 4.9598 43.8038 0002699 323.6854 56.9722 1.00271905 74496
|
||||
GPS BIIRM-2 (PRN 31)
|
||||
1 29486U 06042A 26065.30659479 .00000009 00000+0 00000+0 0 9990
|
||||
2 29486 54.6979 155.4247 0106709 52.9095 318.4746 2.00575309142339
|
||||
GPS BIIRM-3 (PRN 12)
|
||||
1 29601U 06052A 26065.20822791 .00000018 00000+0 00000+0 0 9998
|
||||
2 29601 54.9564 218.0858 0087842 89.3907 271.5694 2.00551801141349
|
||||
GPS BIIRM-4 (PRN 15)
|
||||
1 32260U 07047A 26065.01635843 -.00000015 00000+0 00000+0 0 9995
|
||||
2 32260 54.0170 84.3168 0163613 85.9747 275.9459 2.00560687134746
|
||||
COSMOS 2433 (720)
|
||||
1 32275U 07052A 26065.14823072 -.00000060 00000+0 00000+0 0 9994
|
||||
2 32275 65.6339 319.4027 0001925 271.0893 88.9473 2.13103993142867
|
||||
COSMOS 2432 (719)
|
||||
1 32276U 07052B 26062.91914729 -.00000053 00000+0 00000+0 0 9999
|
||||
2 32276 65.6511 319.6039 0010630 330.1271 64.0556 2.13102981142846
|
||||
GPS BIIRM-5 (PRN 29)
|
||||
1 32384U 07062A 26065.07949970 -.00000020 00000+0 00000+0 0 9990
|
||||
2 32384 55.1135 278.2961 0032970 160.6738 12.9295 2.00553677133447
|
||||
COSMOS 2434 (721)
|
||||
1 32393U 07065A 26064.58450806 .00000016 00000+0 00000+0 0 9997
|
||||
2 32393 64.3181 195.4072 0003739 70.4545 107.4157 2.13101183141562
|
||||
COSMOS 2436 (723)
|
||||
1 32395U 07065C 26063.04733503 .00000000 00000+0 00000+0 0 9990
|
||||
2 32395 64.3269 195.5300 0017530 7.4777 203.8673 2.13099973141485
|
||||
GPS BIIRM-6 (PRN 07)
|
||||
1 32711U 08012A 26064.43600902 .00000009 00000+0 00000+0 0 9994
|
||||
2 32711 54.4824 154.0710 0207038 246.2040 111.6495 2.00569259131672
|
||||
GPS BIIRM-8 (PRN 05)
|
||||
1 35752U 09043A 26065.15231838 -.00000084 00000+0 00000+0 0 9999
|
||||
2 35752 56.1494 31.4989 0051734 80.7052 92.3739 2.00560376121296
|
||||
COSMOS 2456 (730)
|
||||
1 36111U 09070A 26065.71551972 -.00000003 00000+0 00000+0 0 9996
|
||||
2 36111 64.9504 74.4664 0006339 306.1102 176.9939 2.13103461126247
|
||||
COSMOS 2457 (733)
|
||||
1 36112U 09070B 26065.86026214 -.00000003 00000+0 00000+0 0 9999
|
||||
2 36112 64.9361 74.3074 0002155 196.6002 165.9006 2.13100897126250
|
||||
COSMOS 2460 (732)
|
||||
1 36402U 10007C 26063.07006766 -.00000051 00000+0 00000+0 0 9997
|
||||
2 36402 65.4801 318.0258 0001792 14.3302 8.4003 2.13106010124499
|
||||
GPS BIIF-1 (PRN 25)
|
||||
1 36585U 10022A 26064.75428881 .00000012 00000+0 00000+0 0 9994
|
||||
2 36585 54.2886 212.7266 0127338 65.8860 296.5732 2.00557356115526
|
||||
BEIDOU-2 IGSO-1 (C06)
|
||||
1 36828U 10036A 26064.71564830 -.00000126 00000+0 00000+0 0 9997
|
||||
2 36828 54.2927 163.8986 0054017 218.9744 143.1840 1.00252203 57065
|
||||
BEIDOU-2 G4 (C04)
|
||||
1 37210U 10057A 26065.82717685 -.00000114 00000+0 00000+0 0 9994
|
||||
2 37210 3.3928 69.5393 0008762 211.9137 340.8851 1.00272420 56268
|
||||
BEIDOU-2 IGSO-2 (C07)
|
||||
1 37256U 10068A 26063.55016868 -.00000178 00000+0 00000+0 0 9999
|
||||
2 37256 47.7317 273.1244 0047330 213.1251 341.1802 1.00258965 55745
|
||||
BEIDOU-2 IGSO-3 (C08)
|
||||
1 37384U 11013A 26058.90884667 -.00000154 00000+0 00000+0 0 9999
|
||||
2 37384 62.2642 41.1818 0035029 190.8154 358.7681 1.00294022 54567
|
||||
GSAT-8 (GAGAN/PRN 127)
|
||||
1 37605U 11022A 26065.80646343 .00000082 00000+0 00000+0 0 9998
|
||||
2 37605 1.8663 83.1264 0007140 216.5939 210.1807 1.00273406 52197
|
||||
BEIDOU-2 IGSO-4 (C09)
|
||||
1 37763U 11038A 26064.72437745 -.00000100 00000+0 00000+0 0 9996
|
||||
2 37763 54.5899 166.6081 0154581 230.9953 121.0281 1.00265577 53603
|
||||
GSAT0101 (GALILEO-PFM)
|
||||
1 37846U 11060A 26053.46432651 -.00000082 00000+0 00000+0 0 9992
|
||||
2 37846 57.0318 344.8758 0003975 357.0591 2.9533 1.70475478 89166
|
||||
GSAT0102 (GALILEO-FM2)
|
||||
1 37847U 11060B 26060.06318653 -.00000106 00000+0 00000+0 0 9999
|
||||
2 37847 57.0301 344.6971 0005422 341.2092 154.1953 1.70475778 89286
|
||||
COSMOS 2476 (744)
|
||||
1 37867U 11064A 26065.17118656 .00000001 00000+0 00000+0 0 9996
|
||||
2 37867 65.2104 76.3481 0025186 241.3746 92.4806 2.13103631111562
|
||||
COSMOS 2477 (745)
|
||||
1 37868U 11064B 26062.25639741 .00000024 00000+0 00000+0 0 9991
|
||||
2 37868 65.2151 76.5253 0022241 242.8811 195.4983 2.13102695111505
|
||||
COSMOS 2475 (743)
|
||||
1 37869U 11064C 26065.66634538 .00000000 00000+0 00000+0 0 9993
|
||||
2 37869 65.2241 76.4304 0028031 254.4778 235.4579 2.13101515111580
|
||||
BEIDOU-2 IGSO-5 (C10)
|
||||
1 37948U 11073A 26063.54783174 -.00000153 00000+0 00000+0 0 9997
|
||||
2 37948 47.8676 272.8369 0108588 221.1171 320.7210 1.00272598 52087
|
||||
LUCH 5A (SDCM/PRN 140)
|
||||
1 37951U 11074B 26065.82856353 -.00000049 00000+0 00000+0 0 9999
|
||||
2 37951 8.4932 75.3103 0003865 248.4746 306.1672 1.00276045 52028
|
||||
BEIDOU-2 G5 (C05)
|
||||
1 38091U 12008A 26065.87846221 .00000054 00000+0 00000+0 0 9996
|
||||
2 38091 3.3183 70.3001 0014143 255.5790 213.7869 1.00272389 51349
|
||||
BEIDOU-2 M3 (C11)
|
||||
1 38250U 12018A 26064.01894681 -.00000067 00000+0 00000+0 0 9997
|
||||
2 38250 55.8459 308.3876 0021147 279.6506 80.1690 1.86229821 94007
|
||||
BEIDOU-2 M4 (C12)
|
||||
1 38251U 12018B 26065.02500567 -.00000069 00000+0 00000+0 0 9999
|
||||
2 38251 55.7456 307.5760 0012630 286.4840 73.4334 1.86229543 94024
|
||||
SES-5 (EGNOS/PRN 136)
|
||||
1 38652U 12036A 26065.22597522 .00000028 00000+0 00000+0 0 9997
|
||||
2 38652 0.0597 310.4117 0001901 31.4449 268.4077 1.00271218 43429
|
||||
BEIDOU-2 M6 (C14)
|
||||
1 38775U 12050B 26063.66128696 -.00000027 00000+0 00000+0 0 9997
|
||||
2 38775 56.4533 66.3489 0013968 341.8059 18.1670 1.86230964 91362
|
||||
GSAT-10 (GAGAN/PRN 128)
|
||||
1 38779U 12051B 26065.81201012 -.00000168 00000+0 00000+0 0 9991
|
||||
2 38779 0.0676 261.9938 0004551 177.4731 100.3598 1.00273399 49078
|
||||
GPS BIIF-3 (PRN 24)
|
||||
1 38833U 12053A 26065.11266852 .00000008 00000+0 00000+0 0 9992
|
||||
2 38833 53.5660 147.8234 0176954 64.7855 297.0886 2.00565873 97370
|
||||
GSAT0103 (GALILEO-FM3)
|
||||
1 38857U 12055A 26064.64990116 .00000009 00000+0 00000+0 0 9995
|
||||
2 38857 55.7393 104.3859 0005425 285.5287 74.4915 1.70473457 83254
|
||||
BEIDOU-2 G6 (C02)
|
||||
1 38953U 12059A 26063.10227411 -.00000185 00000+0 00000+0 0 9993
|
||||
2 38953 4.1554 74.6500 0010671 304.2337 263.7683 1.00269514 48856
|
||||
LUCH 5B (SDCM/PRN 125)
|
||||
1 38977U 12061A 26065.76608216 -.00000139 00000+0 00000+0 0 9991
|
||||
2 38977 10.2623 50.8817 0003692 226.3927 146.6875 1.00272261 48583
|
||||
COSMOS 2485 (747)
|
||||
1 39155U 13019A 26063.73454937 .00000012 00000+0 00000+0 0 9997
|
||||
2 39155 65.3600 77.1091 0025497 236.8511 122.9458 2.13102928100058
|
||||
GPS BIIF-4 (PRN 27)
|
||||
1 39166U 13023A 26064.97752527 -.00000015 00000+0 00000+0 0 9991
|
||||
2 39166 54.5849 273.1765 0141644 49.9392 311.3205 2.00564238 93815
|
||||
IRNSS-1A
|
||||
1 39199U 13034A 26062.59712745 .00000081 00000+0 00000+0 0 9995
|
||||
2 39199 35.4874 65.3779 0017825 190.7092 175.2371 1.00269823 46313
|
||||
GPS BIIF-5 (PRN 30)
|
||||
1 39533U 14008A 26065.47472284 .00000007 00000+0 00000+0 0 9998
|
||||
2 39533 53.6379 153.5106 0082368 228.3720 130.9608 2.00553229 87628
|
||||
ASTRA 5B (EGNOS/PRN 123)
|
||||
1 39617U 14011B 26065.80022338 .00000134 00000+0 00000+0 0 9994
|
||||
2 39617 0.0860 301.0296 0004015 28.7722 146.2633 1.00272325 43645
|
||||
COSMOS 2492 (754)
|
||||
1 39620U 14012A 26065.57044016 -.00000059 00000+0 00000+0 0 9997
|
||||
2 39620 65.3564 317.4255 0013141 326.4399 33.5365 2.13103231 93052
|
||||
IRNSS-1B
|
||||
1 39635U 14017A 26062.54882074 .00000102 00000+0 00000+0 0 9994
|
||||
2 39635 29.1127 242.2056 0021171 179.2461 352.3815 1.00270890 43734
|
||||
LUCH 5V (SDCM/PRN 141)
|
||||
1 39727U 14023A 26065.60036917 -.00000271 00000+0 00000+0 0 9998
|
||||
2 39727 4.8804 70.7549 0003084 285.1366 119.2902 1.00271330 43336
|
||||
GPS BIIF-6 (PRN 06)
|
||||
1 39741U 14026A 26064.75554898 -.00000099 00000+0 00000+0 0 9993
|
||||
2 39741 56.5458 335.5470 0039849 324.6009 35.1717 2.00568639 86460
|
||||
COSMOS 2500 (755)
|
||||
1 40001U 14032A 26064.33376667 -.00000055 00000+0 00000+0 0 9993
|
||||
2 40001 65.2924 317.2230 0005183 201.3822 158.6566 2.13105772 91249
|
||||
GPS BIIF-7 (PRN 09)
|
||||
1 40105U 14045A 26063.84593997 .00000005 00000+0 00000+0 0 9998
|
||||
2 40105 55.2815 92.6206 0031508 120.4743 239.9033 2.00551558 83989
|
||||
GSAT0201 (GALILEO 5)
|
||||
1 40128U 14050A 26062.23675925 -.00000027 00000+0 00000+0 0 9998
|
||||
2 40128 48.9889 277.7718 1657553 175.2875 186.5034 1.85520013 76352
|
||||
GSAT0202 (GALILEO 6)
|
||||
1 40129U 14050B 26065.18759423 -.00000036 00000+0 00000+0 0 9992
|
||||
2 40129 49.0025 276.7356 1658927 176.1364 185.3302 1.85520810 78579
|
||||
IRNSS-1C
|
||||
1 40269U 14061A 26065.58305772 -.00000163 00000+0 00000+0 0 9998
|
||||
2 40269 6.2543 90.9291 0019872 356.6405 9.5984 1.00269479 41669
|
||||
GPS BIIF-8 (PRN 03)
|
||||
1 40294U 14068A 26065.21974178 -.00000078 00000+0 00000+0 0 9990
|
||||
2 40294 56.9208 34.9218 0062856 68.0586 292.5929 2.00569976 83146
|
||||
COSMOS 2501 (702K)
|
||||
1 40315U 14075A 26060.84023701 -.00000015 00000+0 00000+0 0 9994
|
||||
2 40315 63.6170 194.0851 0020735 205.7160 154.1408 2.13101099 87569
|
||||
GPS BIIF-9 (PRN 26)
|
||||
1 40534U 15013A 26065.35117279 .00000016 00000+0 00000+0 0 9995
|
||||
2 40534 53.1859 208.4290 0108755 39.8054 320.9401 2.00557521 79764
|
||||
GSAT0203 (GALILEO 7)
|
||||
1 40544U 15017A 26058.52158611 -.00000103 00000+0 00000+0 0 9994
|
||||
2 40544 56.8234 344.5881 0005161 295.0116 64.9415 1.70475495 67337
|
||||
GSAT0204 (GALILEO 8)
|
||||
1 40545U 15017B 26058.19060141 -.00000102 00000+0 00000+0 0 9997
|
||||
2 40545 56.8295 344.6151 0004389 286.3298 73.6287 1.70475370 30371
|
||||
BEIDOU-3S IGSO-1S (C31)
|
||||
1 40549U 15019A 26065.60071583 -.00000164 00000+0 00000+0 0 9997
|
||||
2 40549 49.3862 296.6971 0035764 190.6092 349.7460 1.00273571 40045
|
||||
GPS BIIF-10 (PRN 08)
|
||||
1 40730U 15033A 26065.51165918 -.00000012 00000+0 00000+0 0 9993
|
||||
2 40730 54.0631 271.3180 0114550 29.0223 331.6302 2.00561568 77938
|
||||
BEIDOU-3S M2S (C58)
|
||||
1 40748U 15037A 26064.15514271 -.00000065 00000+0 00000+0 0 9993
|
||||
2 40748 54.9621 305.6294 0008821 278.3568 81.6004 1.86231809 72184
|
||||
BEIDOU-3S M1S (C57)
|
||||
1 40749U 15037B 26061.69084880 -.00000053 00000+0 00000+0 0 9995
|
||||
2 40749 54.9610 305.7025 0004647 29.9008 330.1863 1.86253746 72142
|
||||
GSAT0205 (GALILEO 9)
|
||||
1 40889U 15045A 26043.95434207 .00000078 00000+0 00000+0 0 9996
|
||||
2 40889 53.6403 225.6057 0359599 59.9658 303.5577 1.67410800 64211
|
||||
GSAT0206 (GALILEO 10)
|
||||
1 40890U 15045B 26065.21845735 .00000020 00000+0 00000+0 0 9990
|
||||
2 40890 54.9806 224.4444 0003724 50.3393 309.6334 1.70473773 65286
|
||||
GPS BIIF-11 (PRN 10)
|
||||
1 41019U 15062A 26065.57045984 -.00000077 00000+0 00000+0 0 9993
|
||||
2 41019 56.8944 34.7646 0111446 231.5122 127.4715 2.00567146 75770
|
||||
GSAT-15 (GAGAN/PRN 139)
|
||||
1 41028U 15065A 26065.10082801 -.00000263 00000+0 00000+0 0 9991
|
||||
2 41028 0.0855 269.7149 0001104 106.5589 277.3204 1.00271608 37766
|
||||
GSAT0209 (GALILEO 12)
|
||||
1 41174U 15079A 26064.80003714 .00000008 00000+0 00000+0 0 9996
|
||||
2 41174 55.7602 104.0969 0004738 323.5937 36.4535 1.70474567 63014
|
||||
GSAT0208 (GALILEO 11)
|
||||
1 41175U 15079B 26062.08735472 .00000015 00000+0 00000+0 0 9996
|
||||
2 41175 55.7569 104.1685 0004163 323.7694 36.2890 1.70474318 63383
|
||||
IRNSS-1E
|
||||
1 41241U 16003A 26065.38994928 -.00000293 00000+0 00000+0 0 9996
|
||||
2 41241 33.0116 61.2294 0017646 189.2265 165.8466 1.00273983 37042
|
||||
GPS BIIF-12 (PRN 32)
|
||||
1 41328U 16007A 26065.15731679 -.00000004 00000+0 00000+0 0 9993
|
||||
2 41328 55.4568 93.5431 0095487 246.0433 113.0135 2.00556703 73779
|
||||
COSMOS 2514 (751)
|
||||
1 41330U 16008A 26065.04801300 -.00000057 00000+0 00000+0 0 9992
|
||||
2 41330 65.0816 316.5487 0009105 208.8848 151.1249 2.13102742 78422
|
||||
IRNSS-1F
|
||||
1 41384U 16015A 26065.80187006 .00000156 00000+0 00000+0 0 9992
|
||||
2 41384 5.0902 99.2104 0019116 188.2385 198.1485 1.00272507 36645
|
||||
BEIDOU-2 IGSO-6 (C13)
|
||||
1 41434U 16021A 26065.38665594 -.00000150 00000+0 00000+0 0 9996
|
||||
2 41434 60.0894 39.0582 0059788 233.3814 126.6015 1.00278312 36456
|
||||
IRNSS-1G
|
||||
1 41469U 16027A 26065.78253050 -.00000337 00000+0 00000+0 0 9991
|
||||
2 41469 4.9410 99.9517 0005342 314.3156 161.4455 1.00267587 36158
|
||||
GSAT0211 (GALILEO 14)
|
||||
1 41549U 16030A 26064.55763471 .00000016 00000+0 00000+0 0 9994
|
||||
2 41549 55.1310 224.5419 0003581 25.3389 334.6168 1.70473873 60900
|
||||
GSAT0210 (GALILEO 13)
|
||||
1 41550U 16030B 26064.81491220 .00000018 00000+0 00000+0 0 9997
|
||||
2 41550 55.1283 224.5329 0001200 195.2538 164.6814 1.70473987 60905
|
||||
BEIDOU-2 G7 (C03)
|
||||
1 41586U 16037A 26065.81738347 -.00000355 00000+0 00000+0 0 9993
|
||||
2 41586 1.4675 68.2443 0007914 342.8303 158.1757 1.00266463 5861
|
||||
EUTELSAT 117 WEST B (W*)
|
||||
1 41589U 16038B 26064.98792982 -.00000018 00000+0 00000+0 0 9995
|
||||
2 41589 0.0104 328.8615 0000350 267.7539 165.7653 1.00273781 35622
|
||||
GSAT0207 (GALILEO 15)
|
||||
1 41859U 16069A 26059.22748061 .00000008 00000+0 00000+0 0 9997
|
||||
2 41859 55.4267 104.1007 0005031 309.1503 50.8842 1.70474383 57546
|
||||
GSAT0212 (GALILEO 16)
|
||||
1 41860U 16069B 26064.94685676 .00000007 00000+0 00000+0 0 9995
|
||||
2 41860 55.4278 103.9372 0003643 335.6888 24.3725 1.70474702 57898
|
||||
GSAT0213 (GALILEO 17)
|
||||
1 41861U 16069C 26064.72771558 .00000008 00000+0 00000+0 0 9998
|
||||
2 41861 55.4302 103.9512 0005295 293.0089 67.0145 1.70474927 57752
|
||||
GSAT0214 (GALILEO 18)
|
||||
1 41862U 16069D 26065.46034919 .00000006 00000+0 00000+0 0 9992
|
||||
2 41862 55.4293 103.9236 0004341 297.3479 62.6840 1.70474815 57890
|
||||
SES-15 (WAAS/PRN 133)
|
||||
1 42709U 17026A 26064.01780892 .00000050 00000+0 00000+0 0 9994
|
||||
2 42709 0.0426 307.9199 0000713 39.5952 52.4724 1.00271060 6386
|
||||
QZS-2 (QZSS/PRN 194)
|
||||
1 42738U 17028A 26064.73548161 -.00000170 00000+0 00000+0 0 9992
|
||||
2 42738 39.5883 244.6601 0745486 270.3415 54.7958 1.00261653 6582
|
||||
QZS-3 (QZSS/PRN 199)
|
||||
1 42917U 17048A 26065.78864368 -.00000351 00000+0 00000+0 0 9990
|
||||
2 42917 0.0678 212.5623 0002297 148.1009 214.7581 1.00275228 31234
|
||||
COSMOS 2522 (752)
|
||||
1 42939U 17055A 26064.88589677 .00000019 00000+0 00000+0 0 9999
|
||||
2 42939 64.0788 195.3973 0007310 227.5424 132.3669 2.13101691 65775
|
||||
QZS-4 (QZSS/PRN 195)
|
||||
1 42965U 17062A 26064.74126170 -.00000341 00000+0 00000+0 0 9995
|
||||
2 42965 40.1722 344.8064 0748494 269.9150 316.0784 1.00280691 30763
|
||||
BEIDOU-3 M1 (C19)
|
||||
1 43001U 17069A 26063.96368704 -.00000029 00000+0 00000+0 0 9991
|
||||
2 43001 56.6017 66.5157 0012381 309.6653 50.2473 1.86231070 56664
|
||||
BEIDOU-3 M2 (C20)
|
||||
1 43002U 17069B 26063.36523573 -.00000024 00000+0 00000+0 0 9998
|
||||
2 43002 56.5998 66.5715 0009709 329.0395 30.9280 1.86230543 56644
|
||||
GSAT0215 (GALILEO 19)
|
||||
1 43055U 17079A 26062.43085966 .00000010 00000+0 00000+0 0 9997
|
||||
2 43055 55.1080 224.4135 0000631 13.1955 346.7446 1.70474475 51181
|
||||
GSAT0216 (GALILEO 20)
|
||||
1 43056U 17079B 26063.89524153 .00000013 00000+0 00000+0 0 9998
|
||||
2 43056 55.1080 224.3734 0001703 320.6652 39.2594 1.70474648 51222
|
||||
GSAT0217 (GALILEO 21)
|
||||
1 43057U 17079C 26065.28916355 .00000020 00000+0 00000+0 0 9991
|
||||
2 43057 55.1073 224.3328 0002074 355.7864 4.1520 1.70474591 51238
|
||||
GSAT0218 (GALILEO 22)
|
||||
1 43058U 17079D 26063.74946031 .00000013 00000+0 00000+0 0 9997
|
||||
2 43058 55.1048 224.3730 0001921 303.9467 55.9718 1.70474529 51236
|
||||
BEIDOU-3 M7 (C27)
|
||||
1 43107U 18003A 26064.18114576 -.00000067 00000+0 00000+0 0 9990
|
||||
2 43107 54.4201 305.9428 0004668 71.1796 288.9274 1.86232262 55416
|
||||
BEIDOU-3 M8 (C28)
|
||||
1 43108U 18003B 26065.20061454 -.00000069 00000+0 00000+0 0 9993
|
||||
2 43108 54.4118 305.8896 0002953 294.8433 65.1800 1.86230867 55441
|
||||
BEIDOU-3 M4 (C22)
|
||||
1 43207U 18018A 26062.42109766 -.00000016 00000+0 00000+0 0 9991
|
||||
2 43207 56.5592 66.6343 0006867 350.5022 9.5144 1.86230898 54773
|
||||
BEIDOU-3 M3 (C21)
|
||||
1 43208U 18018B 26064.63534936 -.00000033 00000+0 00000+0 0 9997
|
||||
2 43208 56.5606 66.5651 0009923 319.6810 40.2646 1.86231141 54814
|
||||
BEIDOU-3 M9 (C29)
|
||||
1 43245U 18029A 26063.77860799 -.00000062 00000+0 00000+0 0 9995
|
||||
2 43245 54.2878 303.6009 0000937 21.7845 338.2765 1.86228195 53959
|
||||
BEIDOU-3 M10 (C30)
|
||||
1 43246U 18029B 26063.17653141 -.00000059 00000+0 00000+0 0 9997
|
||||
2 43246 54.2860 303.6493 0005102 23.7928 336.2892 1.86228884 53922
|
||||
IRNSS-1I
|
||||
1 43286U 18035A 26065.59256913 .00000086 00000+0 00000+0 0 9991
|
||||
2 43286 28.9346 75.7801 0018963 190.6828 166.3188 1.00278580 29045
|
||||
COSMOS 2527 (756)
|
||||
1 43508U 18053A 26064.84716644 .00000005 00000+0 00000+0 0 9998
|
||||
2 43508 65.5190 77.3533 0009601 245.4642 114.4730 2.13102075 60073
|
||||
BEIDOU-2 IGSO-7 (C16)
|
||||
1 43539U 18057A 26056.73984515 -.00000161 00000+0 00000+0 0 9993
|
||||
2 43539 55.1859 164.0342 0099962 236.5988 133.5173 1.00284731 28015
|
||||
GSAT0221 (GALILEO 25)
|
||||
1 43564U 18060A 26059.54670036 -.00000105 00000+0 00000+0 0 9995
|
||||
2 43564 57.2051 344.7173 0005804 306.7848 53.1689 1.70475369 47315
|
||||
GSAT0222 (GALILEO 26)
|
||||
1 43565U 18060B 26055.66007047 -.00000092 00000+0 00000+0 0 9996
|
||||
2 43565 57.2093 344.8256 0004435 285.4820 74.4799 1.70474973 47292
|
||||
GSAT0219 (GALILEO 23)
|
||||
1 43566U 18060C 26065.26653775 -.00000104 00000+0 00000+0 0 9993
|
||||
2 43566 57.2056 344.5677 0004933 321.2111 38.7755 1.70475265 47437
|
||||
GSAT0220 (GALILEO 24)
|
||||
1 43567U 18060D 26058.44840234 -.00000102 00000+0 00000+0 0 9992
|
||||
2 43567 57.2075 344.7516 0005301 308.5821 51.3768 1.70475159 47302
|
||||
BEIDOU-3 M5 (C23)
|
||||
1 43581U 18062A 26065.01153201 .00000011 00000+0 00000+0 0 9990
|
||||
2 43581 54.0879 185.8116 0002277 307.0485 52.9036 1.86228711 51716
|
||||
BEIDOU-3 M6 (C24)
|
||||
1 43582U 18062B 26062.72660318 -.00000014 00000+0 00000+0 0 9992
|
||||
2 43582 54.0877 185.8781 0006255 44.1915 315.8167 1.86227526 51674
|
||||
BEIDOU-3 M12 (C26)
|
||||
1 43602U 18067A 26064.81287722 .00000009 00000+0 00000+0 0 9999
|
||||
2 43602 54.1822 184.5453 0008548 39.5961 320.4401 1.86227815 51197
|
||||
BEIDOU-3 M11 (C25)
|
||||
1 43603U 18067B 26064.94763062 .00000010 00000+0 00000+0 0 9992
|
||||
2 43603 54.1797 184.5047 0004933 32.9662 327.0394 1.86227200 51223
|
||||
BEIDOU-3 M13 (C32)
|
||||
1 43622U 18072A 26063.82956810 -.00000029 00000+0 00000+0 0 9991
|
||||
2 43622 56.5333 66.1018 0008916 307.8246 52.1164 1.86230808 50718
|
||||
BEIDOU-3 M14 (C33)
|
||||
1 43623U 18072B 26063.69502611 -.00000028 00000+0 00000+0 0 9992
|
||||
2 43623 56.5350 66.1003 0006376 328.8150 31.1695 1.86230841 50709
|
||||
BEIDOU-3 M16 (C35)
|
||||
1 43647U 18078A 26064.38883562 -.00000064 00000+0 00000+0 0 9998
|
||||
2 43647 54.1837 303.6261 0008456 31.3305 328.7753 1.86232381 50221
|
||||
BEIDOU-3 M15 (C34)
|
||||
1 43648U 18078B 26062.37035185 -.00000055 00000+0 00000+0 0 9996
|
||||
2 43648 54.1808 303.6975 0005342 38.8897 321.2084 1.86232017 50200
|
||||
BEIDOU-3 G1 (C59)
|
||||
1 43683U 18085A 26065.82327684 -.00000281 00000+0 00000+0 0 9995
|
||||
2 43683 2.8167 100.3177 0001536 15.9186 124.6875 1.00267936 27033
|
||||
COSMOS 2529 (757)
|
||||
1 43687U 18086A 26058.37448306 .00000008 00000+0 00000+0 0 9993
|
||||
2 43687 64.2955 196.2992 0007959 253.0448 106.8346 2.13101880 56941
|
||||
BEIDOU-3 M17 (C36)
|
||||
1 43706U 18093A 26065.21496850 .00000012 00000+0 00000+0 0 9996
|
||||
2 43706 54.1711 185.7894 0007108 296.9682 63.1399 1.86228456 49625
|
||||
BEIDOU-3 M18 (C37)
|
||||
1 43707U 18093B 26065.61398683 .00000015 00000+0 00000+0 0 9999
|
||||
2 43707 54.1720 185.7676 0006029 341.6621 18.2927 1.86226630 49626
|
||||
GPS BIII-1 (PRN 04)
|
||||
1 43873U 18109A 26065.54477595 -.00000004 00000+0 00000+0 0 9992
|
||||
2 43873 55.6164 96.2348 0037951 198.3878 333.9441 2.00556975 53010
|
||||
BEIDOU-3 IGSO-1 (C38)
|
||||
1 44204U 19023A 26065.81911682 -.00000207 00000+0 00000+0 0 9992
|
||||
2 44204 58.5573 38.7007 0025971 236.3637 303.4808 1.00270441 6188
|
||||
BEIDOU-2 G8 (C01)
|
||||
1 44231U 19027A 26065.82490331 -.00000250 00000+0 00000+0 0 9998
|
||||
2 44231 1.3990 74.2547 0011620 271.5694 260.0996 1.00275696 25051
|
||||
COSMOS 2534 (758)
|
||||
1 44299U 19030A 26063.82267120 .00000008 00000+0 00000+0 0 9992
|
||||
2 44299 64.3687 196.1275 0009607 279.9650 74.0554 2.13102294 52696
|
||||
BEIDOU-3 IGSO-2 (C39)
|
||||
1 44337U 19035A 26057.68879618 -.00000176 00000+0 00000+0 0 9995
|
||||
2 44337 55.2338 159.8353 0038734 206.4752 153.7223 1.00247364 24561
|
||||
GPS BIII-2 (PRN 18)
|
||||
1 44506U 19056A 26065.18297285 -.00000100 00000+0 00000+0 0 9991
|
||||
2 44506 55.6839 335.2387 0058046 198.0933 333.3685 2.00568454 48001
|
||||
BEIDOU-3 M24 (C46)
|
||||
1 44542U 19061A 26065.14698524 .00000012 00000+0 00000+0 0 9995
|
||||
2 44542 54.3990 186.2022 0008583 21.0011 339.0071 1.86228818 43870
|
||||
BEIDOU-3 M23 (C45)
|
||||
1 44543U 19061B 26065.81391679 .00000016 00000+0 00000+0 0 9995
|
||||
2 44543 54.3975 186.1481 0005058 13.8953 345.4949 1.86227427 43881
|
||||
EUTELSAT 5 WEST B (EGN*)
|
||||
1 44624U 19067A 26065.79450338 -.00000046 00000+0 00000+0 0 9990
|
||||
2 44624 0.0906 313.0550 0003219 44.2294 88.2197 1.00273147 23414
|
||||
BEIDOU-3 IGSO-3 (C40)
|
||||
1 44709U 19073A 26063.99963149 -.00000162 00000+0 00000+0 0 9999
|
||||
2 44709 54.9773 282.0232 0040551 188.9231 171.0493 1.00269134 23418
|
||||
BEIDOU-3 M22 (C44)
|
||||
1 44793U 19078A 26063.37544912 -.00000060 00000+0 00000+0 0 9999
|
||||
2 44793 54.0307 303.5169 0007273 35.6258 324.4804 1.86231851 42694
|
||||
BEIDOU-3 M21 (C43)
|
||||
1 44794U 19078B 26062.97806659 -.00000058 00000+0 00000+0 0 9991
|
||||
2 44794 54.0042 303.4937 0005911 39.9322 320.1697 1.86227541 42693
|
||||
COSMOS 2544 (759)
|
||||
1 44850U 19088A 26065.26106663 .00000003 00000+0 00000+0 0 9997
|
||||
2 44850 65.5071 77.1646 0015771 259.7283 100.1293 2.13101860 48522
|
||||
BEIDOU-3 M19 (C41)
|
||||
1 44864U 19090A 26064.56032794 -.00000033 00000+0 00000+0 0 9994
|
||||
2 44864 56.4687 66.2141 0020433 291.4658 245.9437 1.86231373 42308
|
||||
BEIDOU-3 M20 (C42)
|
||||
1 44865U 19090B 26064.42720734 -.00000032 00000+0 00000+0 0 9997
|
||||
2 44865 56.4702 66.2529 0017835 302.9017 234.8674 1.86231388 42289
|
||||
BEIDOU-3 G2 (C60)
|
||||
1 45344U 20017A 26065.81140345 -.00000143 00000+0 00000+0 0 9992
|
||||
2 45344 2.6592 45.6301 0002728 310.2866 180.7453 1.00272736 22235
|
||||
COSMOS 2545 (760)
|
||||
1 45358U 20018A 26064.04887093 -.00000054 00000+0 00000+0 0 9991
|
||||
2 45358 64.4172 316.0811 0008063 250.0323 109.9417 2.13102426 46443
|
||||
BEIDOU-3 G3 (C61)
|
||||
1 45807U 20040A 26065.81738347 -.00000356 00000+0 00000+0 0 9993
|
||||
2 45807 2.1959 67.1365 0004591 319.6533 182.5056 1.00274500 21042
|
||||
GPS BIII-3 (PRN 23)
|
||||
1 45854U 20041A 26065.51925653 -.00000081 00000+0 00000+0 0 9996
|
||||
2 45854 56.6073 32.9268 0061430 205.2409 151.1260 2.00560728 41978
|
||||
GALAXY 30 (WAAS/PRN 135)
|
||||
1 46114U 20056C 26065.55677565 .00000000 00000+0 00000+0 0 9992
|
||||
2 46114 0.0219 155.3844 0002048 200.5806 243.7632 1.00271670 20266
|
||||
COSMOS 2547 (705K)
|
||||
1 46805U 20075A 26065.64518105 .00000026 00000+0 00000+0 0 9992
|
||||
2 46805 64.5994 195.8945 0006875 230.4824 129.4314 2.13100263 41728
|
||||
GPS BIII-4 (PRN 14)
|
||||
1 46826U 20078A 26064.79964802 .00000012 00000+0 00000+0 0 9997
|
||||
2 46826 54.0013 214.9691 0068278 204.3383 325.7195 2.00565559 39435
|
||||
GPS BIII-5 (PRN 11)
|
||||
1 48859U 21054A 26064.07032190 -.00000101 00000+0 00000+0 0 9997
|
||||
2 48859 55.2207 336.6471 0023861 232.0319 315.6765 2.00575594 34666
|
||||
QZS-1R (QZSS/PRN 196)
|
||||
1 49336U 21096A 26065.82405683 -.00000265 00000+0 00000+0 0 9990
|
||||
2 49336 37.2488 81.0937 0747759 269.7649 244.5984 1.00290649 15989
|
||||
GSAT0223 (GALILEO 27)
|
||||
1 49809U 21116A 26048.03988929 -.00000078 00000+0 00000+0 0 9993
|
||||
2 49809 57.2005 344.9238 0002865 278.1950 264.6358 1.70475508 26130
|
||||
GSAT0224 (GALILEO 28)
|
||||
1 49810U 21116B 26064.29072237 -.00000105 00000+0 00000+0 0 9998
|
||||
2 49810 57.1948 344.4822 0001942 295.2728 253.6293 1.70475760 26438
|
||||
COSMOS 2557 (706K)
|
||||
1 52984U 22075A 26063.93321719 -.00000057 00000+0 00000+0 0 9997
|
||||
2 52984 64.3588 318.1599 0011575 283.5582 76.3723 2.13101614 28492
|
||||
COSMOS 2559 (707K)
|
||||
1 54031U 22130A 26064.69111461 -.00000060 00000+0 00000+0 0 9993
|
||||
2 54031 64.3310 318.2203 0010609 181.3136 178.7423 2.13101297 26488
|
||||
COSMOS 2564 (761)
|
||||
1 54377U 22161A 26064.53462226 .00000016 00000+0 00000+0 0 9991
|
||||
2 54377 64.7419 196.1496 0009447 210.0603 149.8542 2.13101972 25423
|
||||
GPS BIII-6 (PRN 28)
|
||||
1 55268U 23009A 26064.24350115 .00000010 00000+0 00000+0 0 9998
|
||||
2 55268 55.1095 152.6049 0004568 336.6055 15.2776 2.00549197 23157
|
||||
BEIDOU-3 G4 (C62)
|
||||
1 56564U 23066A 26065.82726352 -.00000115 00000+0 00000+0 0 9993
|
||||
2 56564 0.6443 311.2389 0002266 156.3494 154.8091 1.00272440 10368
|
||||
NVS-01 (IRNSS-1J)
|
||||
1 56759U 23076A 26065.78864368 -.00000341 00000+0 00000+0 0 9996
|
||||
2 56759 2.4130 248.5405 0007203 46.0615 283.3391 1.00273048 10089
|
||||
COSMOS 2569 (703K)
|
||||
1 57517U 23114A 26063.60906547 .00000011 00000+0 00000+0 0 9990
|
||||
2 57517 65.2420 76.1460 0012898 291.6250 68.2783 2.13103963 20030
|
||||
BEIDOU-3 M28 (C50)
|
||||
1 58654U 23207A 26064.12943781 -.00000033 00000+0 00000+0 0 9990
|
||||
2 58654 55.6609 65.8120 0004920 303.0244 56.9487 1.86229789 14909
|
||||
BEIDOU-3 M26 (C48)
|
||||
1 58655U 23207B 26065.53778212 -.00000039 00000+0 00000+0 0 9998
|
||||
2 58655 55.6644 65.7507 0005731 288.8087 71.1446 1.86229716 14918
|
||||
GSAT0225 (GALILEO 29)
|
||||
1 59598U 24079A 26064.58030672 .00000007 00000+0 00000+0 0 9992
|
||||
2 59598 55.0507 103.9743 0002603 257.7426 101.8321 1.70474989 11547
|
||||
GSAT0227 (GALILEO 30)
|
||||
1 59600U 24079C 26062.41821347 .00000012 00000+0 00000+0 0 9999
|
||||
2 59600 55.0440 104.0338 0000359 68.5105 291.5792 1.70474834 11567
|
||||
GSAT0232 (GALILEO 32)
|
||||
1 61182U 24167A 26048.66150565 .00000069 00000+0 00000+0 0 9995
|
||||
2 61182 55.2225 224.5881 0003600 296.6314 116.1306 1.70443939 8265
|
||||
GSAT0226 (GALILEO 31)
|
||||
1 61183U 24167B 26063.09230740 .00000011 00000+0 00000+0 0 9991
|
||||
2 61183 55.2176 224.2005 0001588 128.6581 232.0353 1.70474186 9066
|
||||
BEIDOU-3 M25 (C47)
|
||||
1 61186U 24168B 26064.44771763 -.00000067 00000+0 00000+0 0 9992
|
||||
2 61186 54.5648 305.8483 0004268 318.9478 147.1808 1.86230220 9852
|
||||
BEIDOU-3 M27 (C49)
|
||||
1 61187U 24168C 26064.31689484 -.00000067 00000+0 00000+0 0 9997
|
||||
2 61187 54.5604 305.8361 0002980 20.4343 132.9740 1.86230358 7324
|
||||
GPS BIII-7 (PRN 01)
|
||||
1 62339U 24242A 26065.64478888 -.00000104 00000+0 00000+0 0 9993
|
||||
2 62339 54.8910 338.3407 0015837 356.9340 8.8099 2.00569825 9206
|
||||
NVS-02 (IRNSS-1K)
|
||||
1 62850U 25020A 26065.61865175 .00001002 00000+0 81966-3 0 9993
|
||||
2 62850 20.8382 296.9052 7349990 81.2147 348.1780 2.17722557 8741
|
||||
QZS-6 (QZSS/PRN 200)
|
||||
1 62876U 25023A 26065.81348344 -.00000236 00000+0 00000+0 0 9995
|
||||
2 62876 0.0338 296.0742 0001829 48.7188 203.0676 1.00269676 3937
|
||||
COSMOS 2584 (704K)
|
||||
1 63130U 25042A 26059.74188024 .00000039 00000+0 00000+0 0 9992
|
||||
2 63130 65.0213 76.4877 0007822 236.2440 123.7373 2.13103267 7739
|
||||
GPS BIII-8 (PRN 21)
|
||||
1 64202U 25116A 26062.39238152 -.00000077 00000+0 00000+0 0 9991
|
||||
2 64202 55.1633 35.3874 0008474 325.1064 37.8908 2.00553198 5758
|
||||
COSMOS 2596 (708K)
|
||||
1 65590U 25206B 26064.99806025 -.00000060 00000+0 00000+0 0 9992
|
||||
2 65590 64.6934 318.5798 0012868 315.8316 234.0611 2.13100363 3719
|
||||
GSAT0233 (GALILEO 33)
|
||||
1 67160U 25302A 26044.50000000 -.00000008 00000+0 00000+0 0 9990
|
||||
2 67160 54.3935 105.2952 0003011 207.0582 359.4109 1.70474578 20
|
||||
GSAT0234 (GALILEO 34)
|
||||
1 67162U 25302C 26044.50000000 -.00000008 00000+0 00000+0 0 9992
|
||||
2 67162 54.3967 105.2938 0003081 266.8423 119.7309 1.70475065 127
|
||||
GPS BIII-9 (PRN 20)
|
||||
1 67588U 26017A 26062.99831483 .00000012 00000+0 00000+0 0 9992
|
||||
2 67588 55.0184 92.9751 0001516 18.2827 341.7919 2.00560918429508
|
||||
@@ -0,0 +1,14 @@
|
||||
fastapi
|
||||
uvicorn
|
||||
websockets
|
||||
httpx
|
||||
skyfield
|
||||
asyncio
|
||||
pydantic
|
||||
pydantic-settings
|
||||
pyais
|
||||
requests
|
||||
python-dotenv
|
||||
numpy
|
||||
sgp4
|
||||
jplephem
|
||||
@@ -0,0 +1,875 @@
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import httpx
|
||||
|
||||
OLLAMA_URL = "http://localhost:11434/api/generate"
|
||||
MODEL_NAME = "llama3.2"
|
||||
|
||||
# CLI backend: "claude" uses Claude Code CLI (no API key needed if logged in),
|
||||
# "gemini" uses Gemini CLI (free with Google account), "ollama" = local llama3.2
|
||||
# Auto-detect: prefers claude > gemini > ollama
|
||||
def _detect_cli_backend() -> str:
|
||||
for tool in ("gemini", "claude"):
|
||||
if shutil.which(tool):
|
||||
return tool
|
||||
return "ollama"
|
||||
|
||||
_CLI_BACKEND = _detect_cli_backend()
|
||||
|
||||
|
||||
async def _query_cli(prompt: str) -> dict | None:
|
||||
"""Call claude or gemini CLI as subprocess, parse JSON from response."""
|
||||
try:
|
||||
cmd = [_CLI_BACKEND, "-p", prompt]
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
*cmd,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=30.0)
|
||||
raw = stdout.decode().strip()
|
||||
if not raw:
|
||||
return None
|
||||
# Strip markdown code fences if present
|
||||
if "```json" in raw:
|
||||
raw = raw.split("```json", 1)[1].split("```", 1)[0].strip()
|
||||
elif "```" in raw:
|
||||
raw = raw.split("```", 1)[1].split("```", 1)[0].strip()
|
||||
# Extract first JSON object if there's surrounding text
|
||||
m = re.search(r'\{.*\}', raw, re.DOTALL)
|
||||
if m:
|
||||
raw = m.group(0)
|
||||
parsed = json.loads(raw)
|
||||
return parsed if isinstance(parsed, dict) else None
|
||||
except Exception as exc:
|
||||
print(f"[AI] CLI query failed ({_CLI_BACKEND}): {exc}")
|
||||
return None
|
||||
THREAT_LEVELS = ("LOW", "GUARDED", "ELEVATED", "HIGH", "SEVERE")
|
||||
|
||||
KEYWORD_SCORES = {
|
||||
"war": 4,
|
||||
"missile": 4,
|
||||
"strike": 4,
|
||||
"attack": 4,
|
||||
"explosion": 3,
|
||||
"drone": 3,
|
||||
"nuclear": 5,
|
||||
"cyber": 3,
|
||||
"hijack": 4,
|
||||
"sanction": 2,
|
||||
"military": 3,
|
||||
"troops": 3,
|
||||
"border": 2,
|
||||
"earthquake": 2,
|
||||
"tsunami": 4,
|
||||
}
|
||||
|
||||
# Hotspot regions with bounding boxes for spatial movement analysis
|
||||
# bbox = (lat_min, lon_min, lat_max, lon_max)
|
||||
HOTSPOT_REGIONS = {
|
||||
"ukraine": {
|
||||
"name": "Ukraine / Black Sea", "lat": 48.38, "lon": 31.17,
|
||||
"bbox": (44, 25, 52, 40),
|
||||
"keywords": ["ukraine", "kyiv", "kharkiv", "crimea", "donbas", "odesa", "bakhmut", "zaporizhzhia"],
|
||||
"baseline": "Escalation risk around frontline",
|
||||
},
|
||||
"russia": {
|
||||
"name": "Western Russia", "lat": 55.76, "lon": 37.62,
|
||||
"bbox": (52, 30, 62, 48),
|
||||
"keywords": ["russia", "moscow", "kursk"],
|
||||
"baseline": "Military posture shifts",
|
||||
},
|
||||
"israel": {
|
||||
"name": "Israel / Palestine", "lat": 31.05, "lon": 34.85,
|
||||
"bbox": (29, 33, 33.5, 36),
|
||||
"keywords": ["israel", "gaza", "west bank", "tel aviv", "jerusalem", "rafah", "idf", "hamas", "hezbollah"],
|
||||
"baseline": "Cross-border retaliation risk",
|
||||
},
|
||||
"iran": {
|
||||
"name": "Iran", "lat": 32.43, "lon": 53.69,
|
||||
"bbox": (25, 44, 40, 63),
|
||||
"keywords": ["iran", "tehran", "irgc"],
|
||||
"baseline": "Strategic force posture shift",
|
||||
},
|
||||
"syria_lebanon": {
|
||||
"name": "Syria / Lebanon", "lat": 34.80, "lon": 37.00,
|
||||
"bbox": (32, 35, 37.5, 42),
|
||||
"keywords": ["syria", "damascus", "lebanon", "aleppo"],
|
||||
"baseline": "Proxy activity risk",
|
||||
},
|
||||
"taiwan": {
|
||||
"name": "Taiwan Strait", "lat": 23.70, "lon": 120.96,
|
||||
"bbox": (21, 117, 27, 124),
|
||||
"keywords": ["taiwan", "taipei", "taiwan strait"],
|
||||
"baseline": "Maritime and airspace pressure",
|
||||
},
|
||||
"south_china_sea": {
|
||||
"name": "South China Sea", "lat": 12.0, "lon": 113.0,
|
||||
"bbox": (5, 107, 22, 120),
|
||||
"keywords": ["south china sea", "spratly", "paracel"],
|
||||
"baseline": "Naval maneuvering risk",
|
||||
},
|
||||
"korea": {
|
||||
"name": "Korean Peninsula", "lat": 37.57, "lon": 126.98,
|
||||
"bbox": (33, 124, 43, 131),
|
||||
"keywords": ["korea", "pyongyang", "dprk", "north korea", "south korea", "seoul"],
|
||||
"baseline": "Missile test / military response risk",
|
||||
},
|
||||
"red_sea": {
|
||||
"name": "Red Sea / Bab-el-Mandeb", "lat": 15.0, "lon": 42.0,
|
||||
"bbox": (10, 38, 22, 46),
|
||||
"keywords": ["red sea", "houthi", "yemen", "bab-el-mandeb", "aden"],
|
||||
"baseline": "Shipping disruption risk",
|
||||
},
|
||||
"hormuz": {
|
||||
"name": "Strait of Hormuz", "lat": 26.58, "lon": 56.25,
|
||||
"bbox": (24, 54, 28, 58),
|
||||
"keywords": ["hormuz", "persian gulf"],
|
||||
"baseline": "Energy transit disruption risk",
|
||||
},
|
||||
"suez": {
|
||||
"name": "Suez Canal", "lat": 29.93, "lon": 32.55,
|
||||
"bbox": (28, 31, 32, 34),
|
||||
"keywords": ["suez"],
|
||||
"baseline": "Maritime chokepoint risk",
|
||||
},
|
||||
"sahel": {
|
||||
"name": "Sahel / West Africa", "lat": 14.0, "lon": 0.0,
|
||||
"bbox": (8, -10, 20, 15),
|
||||
"keywords": ["mali", "niger", "burkina faso", "sahel", "nigeria"],
|
||||
"baseline": "Insurgency / instability risk",
|
||||
},
|
||||
}
|
||||
|
||||
# ── Region definitions for grouping events into theaters ──
|
||||
REGION_DEFS: dict[str, list[str]] = {
|
||||
"Eastern Europe": [
|
||||
"Ukraine", "Russia", "Crimea", "Donbas", "Donbass", "Kharkiv", "Kyiv",
|
||||
"Kiev", "Odesa", "Mariupol", "Bakhmut", "Zaporizhzhia", "Kherson",
|
||||
"Moldova", "Belarus", "Avdiivka",
|
||||
],
|
||||
"Middle East": [
|
||||
"Israel", "Gaza", "Palestine", "Lebanon", "Hezbollah", "Syria",
|
||||
"Damascus", "Iran", "Tehran", "Iraq", "Baghdad", "Jordan",
|
||||
"West Bank", "Rafah", "Jenin", "Nablus", "Jerusalem", "Tel Aviv",
|
||||
],
|
||||
"Red Sea & Horn of Africa": [
|
||||
"Red Sea", "Yemen", "Houthi", "Somalia", "Sudan", "Ethiopia",
|
||||
"Bab-el-Mandeb", "Suez",
|
||||
],
|
||||
"East Asia": [
|
||||
"China", "Taiwan", "Taipei", "North Korea", "Pyongyang",
|
||||
"South Korea", "Japan", "South China Sea", "Taiwan Strait",
|
||||
"East China Sea",
|
||||
],
|
||||
"South Asia": [
|
||||
"India", "Pakistan", "Afghanistan", "Kashmir", "Myanmar", "Kabul",
|
||||
],
|
||||
"Sub-Saharan Africa": [
|
||||
"Mali", "Niger", "Nigeria", "Congo", "Sahel", "Burkina Faso",
|
||||
"Mozambique", "Libya",
|
||||
],
|
||||
"Europe / NATO": [
|
||||
"NATO", "Poland", "Finland", "Sweden", "Norway", "Baltic",
|
||||
"Germany", "France", "UK", "Serbia", "Kosovo", "Georgia", "Romania",
|
||||
],
|
||||
}
|
||||
|
||||
_WEIGHT_ORDER = {"CRITICAL": 0, "HIGH": 1, "ELEVATED": 2, "GUARDED": 3, "ACTIVE": 4}
|
||||
|
||||
|
||||
def _match_region(title: str) -> str | None:
|
||||
"""Match a headline to a geopolitical region."""
|
||||
tl = title.lower()
|
||||
for region, keywords in REGION_DEFS.items():
|
||||
for kw in keywords:
|
||||
if kw.lower() in tl:
|
||||
return region
|
||||
return None
|
||||
|
||||
|
||||
def _regional_briefs(news_items: list, conflicts: list) -> list[dict]:
|
||||
"""Group events by region and produce per-region threat assessments."""
|
||||
region_events: dict[str, list] = {}
|
||||
|
||||
for item in (conflicts or []):
|
||||
region = _match_region(item.get("title", ""))
|
||||
if region:
|
||||
region_events.setdefault(region, []).append(item)
|
||||
|
||||
for item in (news_items or [])[:30]:
|
||||
region = _match_region(item.get("title", ""))
|
||||
if region:
|
||||
region_events.setdefault(region, []).append(
|
||||
{"title": item["title"], "severity": "INFO", "event_type": "INTEL"}
|
||||
)
|
||||
|
||||
briefs = []
|
||||
for region, events in region_events.items():
|
||||
sev_counts: dict[str, int] = {}
|
||||
for ev in events:
|
||||
s = ev.get("severity", "INFO")
|
||||
sev_counts[s] = sev_counts.get(s, 0) + 1
|
||||
|
||||
critical = sev_counts.get("CRITICAL", 0)
|
||||
high = sev_counts.get("HIGH", 0)
|
||||
|
||||
if critical >= 3:
|
||||
status = "CRITICAL"
|
||||
elif critical >= 1 or high >= 3:
|
||||
status = "HIGH"
|
||||
elif high >= 1:
|
||||
status = "ELEVATED"
|
||||
else:
|
||||
status = "ACTIVE"
|
||||
|
||||
top = [e for e in events if e.get("severity") in ("CRITICAL", "HIGH")][:3]
|
||||
if top:
|
||||
summary = "; ".join(
|
||||
e.get("event_type", e.get("title", "")[:40]) for e in top[:2]
|
||||
)
|
||||
else:
|
||||
summary = f"{len(events)} events tracked in region"
|
||||
|
||||
briefs.append({
|
||||
"region": region,
|
||||
"status": status,
|
||||
"event_count": len(events),
|
||||
"critical": critical,
|
||||
"high": high,
|
||||
"summary": summary[:120],
|
||||
})
|
||||
|
||||
briefs.sort(key=lambda b: (_WEIGHT_ORDER.get(b["status"], 5), -b["event_count"]))
|
||||
return briefs[:6]
|
||||
|
||||
|
||||
def _key_drivers(
|
||||
score: int, reasons: list[str], gps_data: list, bgp_data: dict,
|
||||
conflicts: list, space_weather: dict, planes: list = None, ships: list = None,
|
||||
) -> list[dict]:
|
||||
"""Identify and rank the top threat signal drivers."""
|
||||
drivers: list[dict] = []
|
||||
|
||||
# GPS Interference
|
||||
gps_count = len(gps_data or [])
|
||||
if gps_count:
|
||||
drivers.append({
|
||||
"signal": "GPS Interference",
|
||||
"detail": f"{gps_count} active jamming zone{'s' if gps_count > 1 else ''} detected via ADS-B navigation anomalies",
|
||||
"weight": "CRITICAL" if gps_count >= 3 else "HIGH",
|
||||
"icon": "⬡",
|
||||
})
|
||||
|
||||
# Military Conflicts
|
||||
if conflicts:
|
||||
critical = sum(1 for c in conflicts if c.get("severity") == "CRITICAL")
|
||||
high = sum(1 for c in conflicts if c.get("severity") == "HIGH")
|
||||
active_regions = set(
|
||||
_match_region(c.get("title", "")) for c in conflicts
|
||||
if _match_region(c.get("title", ""))
|
||||
)
|
||||
if critical or high:
|
||||
drivers.append({
|
||||
"signal": "Military Activity",
|
||||
"detail": f"{critical} CRITICAL, {high} HIGH severity events across {len(active_regions)} region{'s' if len(active_regions) != 1 else ''}",
|
||||
"weight": "CRITICAL" if critical >= 5 else "HIGH" if critical >= 1 else "ELEVATED",
|
||||
"icon": "⚔",
|
||||
})
|
||||
|
||||
# BGP Routing
|
||||
bgp_status = str((bgp_data or {}).get("status", "STABLE")).upper()
|
||||
if bgp_status in ("ELEVATED", "CRITICAL"):
|
||||
updates = (bgp_data or {}).get("total_updates", 0)
|
||||
drivers.append({
|
||||
"signal": "BGP Routing",
|
||||
"detail": f"Internet routing instability: {updates:,} updates/hour ({bgp_status})",
|
||||
"weight": bgp_status,
|
||||
"icon": "📡",
|
||||
})
|
||||
|
||||
# Space Weather
|
||||
sw = space_weather or {}
|
||||
kp = sw.get("kp_index", 0)
|
||||
if isinstance(kp, (int, float)) and kp >= 4:
|
||||
drivers.append({
|
||||
"signal": "Space Weather",
|
||||
"detail": f"Geomagnetic storm Kp={kp:.1f} — {sw.get('description', 'HF radio / satellite disruption risk')}",
|
||||
"weight": "CRITICAL" if kp >= 7 else "HIGH" if kp >= 5 else "ELEVATED",
|
||||
"icon": "☀",
|
||||
})
|
||||
|
||||
# News keywords
|
||||
keyword_drivers = [r for r in reasons if "×" in r]
|
||||
if keyword_drivers:
|
||||
top_kw = keyword_drivers[:3]
|
||||
drivers.append({
|
||||
"signal": "Intel Keywords",
|
||||
"detail": f"High-weight terms in news: {', '.join(top_kw)}",
|
||||
"weight": "ELEVATED" if score >= 8 else "GUARDED",
|
||||
"icon": "📰",
|
||||
})
|
||||
|
||||
# Military air movement
|
||||
mil_count = sum(1 for p in (planes or []) if p.get("military"))
|
||||
total_planes = len(planes or [])
|
||||
if mil_count >= 5:
|
||||
drivers.append({
|
||||
"signal": "Military Air Movement",
|
||||
"detail": f"{mil_count} military aircraft tracked globally ({total_planes:,} total airframes monitored)",
|
||||
"weight": "HIGH" if mil_count >= 30 else "ELEVATED" if mil_count >= 15 else "GUARDED",
|
||||
"icon": "✈",
|
||||
})
|
||||
|
||||
# Maritime movement
|
||||
ship_count = len(ships or [])
|
||||
if ship_count >= 10:
|
||||
tankers = sum(1 for s in (ships or []) if str(s.get("type", "")).lower() in ("tanker", "lng carrier"))
|
||||
drivers.append({
|
||||
"signal": "Maritime Activity",
|
||||
"detail": f"{ship_count} vessels tracked globally" + (f" ({tankers} tankers near chokepoints)" if tankers >= 5 else ""),
|
||||
"weight": "ELEVATED" if ship_count >= 100 else "GUARDED",
|
||||
"icon": "🚢",
|
||||
})
|
||||
|
||||
drivers.sort(key=lambda d: _WEIGHT_ORDER.get(d["weight"], 5))
|
||||
return drivers[:6]
|
||||
|
||||
|
||||
def _correlations(
|
||||
gps_data: list, conflicts: list, bgp_data: dict, space_weather: dict,
|
||||
) -> list[str]:
|
||||
"""Find cross-domain signal correlations."""
|
||||
out: list[str] = []
|
||||
|
||||
conflict_regions = set(
|
||||
_match_region(c.get("title", "")) for c in (conflicts or [])
|
||||
if _match_region(c.get("title", ""))
|
||||
)
|
||||
|
||||
if gps_data and conflicts:
|
||||
for z in gps_data:
|
||||
glat, glon = z.get("lat", 0), z.get("lon", 0)
|
||||
if 35 < glat < 55 and 25 < glon < 45 and "Eastern Europe" in conflict_regions:
|
||||
out.append(
|
||||
"GPS jamming in Eastern Europe coincides with active military operations "
|
||||
"— likely electronic warfare activity"
|
||||
)
|
||||
break
|
||||
for z in gps_data:
|
||||
glat, glon = z.get("lat", 0), z.get("lon", 0)
|
||||
if 25 < glat < 40 and 30 < glon < 50 and "Middle East" in conflict_regions:
|
||||
out.append(
|
||||
"GPS interference near Middle Eastern conflict zones suggests coordinated "
|
||||
"electronic warfare alongside kinetic operations"
|
||||
)
|
||||
break
|
||||
|
||||
bgp_status = str((bgp_data or {}).get("status", "STABLE")).upper()
|
||||
if bgp_status in ("ELEVATED", "CRITICAL") and conflicts:
|
||||
out.append(
|
||||
f"BGP routing instability ({bgp_status}) during active military operations "
|
||||
"may indicate cyber operations or infrastructure targeting"
|
||||
)
|
||||
|
||||
sw = space_weather or {}
|
||||
kp = sw.get("kp_index", 0) if isinstance(sw.get("kp_index"), (int, float)) else 0
|
||||
if kp >= 4 and gps_data:
|
||||
out.append(
|
||||
f"Elevated geomagnetic activity (Kp={kp:.1f}) may amplify GPS signal "
|
||||
"degradation in existing interference zones"
|
||||
)
|
||||
if kp >= 5:
|
||||
out.append(
|
||||
f"Geomagnetic storm (Kp={kp:.1f}) affecting HF radio propagation — "
|
||||
"military comms may shift to satellite links"
|
||||
)
|
||||
|
||||
if not out:
|
||||
out.append("No significant cross-domain signal correlations detected at this time")
|
||||
return out[:4]
|
||||
|
||||
|
||||
def _watch_items(conflicts: list, gps_data: list, space_weather: dict) -> list[str]:
|
||||
"""Generate actionable intelligence watch items."""
|
||||
items: list[str] = []
|
||||
|
||||
if conflicts:
|
||||
critical = [c for c in conflicts if c.get("severity") == "CRITICAL"]
|
||||
if critical:
|
||||
regions: dict[str, list] = {}
|
||||
for c in critical[:15]:
|
||||
r = _match_region(c.get("title", "")) or "Global"
|
||||
regions.setdefault(r, []).append(c)
|
||||
for region, evts in sorted(regions.items(), key=lambda x: -len(x[1])):
|
||||
items.append(
|
||||
f"Monitor {region}: {len(evts)} critical event{'s' if len(evts) > 1 else ''} "
|
||||
f"— {evts[0].get('event_type', 'military activity')}"
|
||||
)
|
||||
|
||||
if gps_data:
|
||||
items.append(
|
||||
f"Track {len(gps_data)} GPS interference zone{'s' if len(gps_data) > 1 else ''} "
|
||||
"— assess impact on aviation safety and military navigation"
|
||||
)
|
||||
|
||||
sw = space_weather or {}
|
||||
if sw.get("alerts"):
|
||||
items.append("Space weather alerts active — monitor satellite communication reliability")
|
||||
|
||||
if not items:
|
||||
items.append("Maintain standard surveillance posture across all domains")
|
||||
return items[:5]
|
||||
|
||||
|
||||
def _threat_from_score(score: int) -> str:
|
||||
if score >= 18:
|
||||
return "SEVERE"
|
||||
if score >= 13:
|
||||
return "HIGH"
|
||||
if score >= 8:
|
||||
return "ELEVATED"
|
||||
if score >= 4:
|
||||
return "GUARDED"
|
||||
return "LOW"
|
||||
|
||||
|
||||
def _score_from_signals(news_items, gps_data, bgp_data) -> tuple[int, list[str]]:
|
||||
score = 0
|
||||
reasons: list[str] = []
|
||||
|
||||
titles = [str(item.get("title", "")).lower() for item in news_items[:20]]
|
||||
title_blob = " ".join(titles)
|
||||
|
||||
for kw, weight in KEYWORD_SCORES.items():
|
||||
hits = title_blob.count(kw)
|
||||
if hits:
|
||||
inc = min(hits, 3) * weight
|
||||
score += inc
|
||||
reasons.append(f"{kw}×{hits}")
|
||||
|
||||
gps_count = len(gps_data or [])
|
||||
if gps_count:
|
||||
bump = min(gps_count, 12)
|
||||
score += bump
|
||||
reasons.append(f"gps_anomalies={gps_count}")
|
||||
|
||||
bgp_status = str((bgp_data or {}).get("status", "STABLE")).upper()
|
||||
if bgp_status == "ELEVATED":
|
||||
score += 3
|
||||
reasons.append("bgp=elevated")
|
||||
elif bgp_status == "CRITICAL":
|
||||
score += 6
|
||||
reasons.append("bgp=critical")
|
||||
elif bgp_status == "OFFLINE":
|
||||
score += 2
|
||||
reasons.append("bgp=offline")
|
||||
|
||||
return score, reasons
|
||||
|
||||
|
||||
def _probability(score: int, offset: int) -> str:
|
||||
pct = max(20, min(95, 35 + score * 3 + offset))
|
||||
return f"{pct}%"
|
||||
|
||||
|
||||
def _in_bbox(lat: float, lon: float, bbox: tuple) -> bool:
|
||||
return bbox[0] <= lat <= bbox[2] and bbox[1] <= lon <= bbox[3]
|
||||
|
||||
|
||||
def _heuristic_predictions(
|
||||
news_items, gps_data, score: int,
|
||||
conflicts=None, planes=None, ships=None,
|
||||
) -> list[dict]:
|
||||
"""
|
||||
Generate predicted hotspots by cross-referencing:
|
||||
- news keyword mentions (intel signal)
|
||||
- conflict event severity (OSINT signal)
|
||||
- military aircraft concentrations (movement signal)
|
||||
- ship density in strategic chokepoints (maritime signal)
|
||||
- GPS interference zones (EW signal)
|
||||
"""
|
||||
titles = [str(item.get("title", "")).lower() for item in (news_items or [])[:30]]
|
||||
title_blob = " ".join(titles)
|
||||
planes = planes or []
|
||||
ships = ships or []
|
||||
conflicts = conflicts or []
|
||||
|
||||
scored: list[tuple[float, str, dict]] = []
|
||||
|
||||
for key, region in HOTSPOT_REGIONS.items():
|
||||
bbox = region["bbox"]
|
||||
|
||||
# ── 1. News keyword mentions ──
|
||||
news_hits = sum(1 for kw in region["keywords"] if kw in title_blob)
|
||||
news_score = news_hits * 3.0
|
||||
|
||||
# ── 2. Conflict event severity ──
|
||||
conflict_crit = 0
|
||||
conflict_high = 0
|
||||
for c in conflicts:
|
||||
ct = (c.get("title") or "").lower()
|
||||
if any(kw in ct for kw in region["keywords"]):
|
||||
sev = c.get("severity", "")
|
||||
if sev == "CRITICAL":
|
||||
conflict_crit += 1
|
||||
elif sev == "HIGH":
|
||||
conflict_high += 1
|
||||
conflict_score = conflict_crit * 5.0 + conflict_high * 2.0
|
||||
|
||||
# ── 3. Military aircraft in region ──
|
||||
mil_count = 0
|
||||
civ_count = 0
|
||||
for p in planes:
|
||||
plat = p.get("lat")
|
||||
plon = p.get("lon")
|
||||
if isinstance(plat, (int, float)) and isinstance(plon, (int, float)):
|
||||
if _in_bbox(plat, plon, bbox):
|
||||
if p.get("military"):
|
||||
mil_count += 1
|
||||
else:
|
||||
civ_count += 1
|
||||
mil_score = min(mil_count * 2.0, 16.0)
|
||||
|
||||
# ── 4. Ship density near strategic chokepoints ──
|
||||
ship_count = 0
|
||||
tanker_count = 0
|
||||
for s in ships:
|
||||
slat = s.get("lat")
|
||||
slon = s.get("lon")
|
||||
if isinstance(slat, (int, float)) and isinstance(slon, (int, float)):
|
||||
if _in_bbox(slat, slon, bbox):
|
||||
ship_count += 1
|
||||
if str(s.get("type", "")).lower() in ("tanker", "lng carrier"):
|
||||
tanker_count += 1
|
||||
ship_score = min(ship_count * 0.8 + tanker_count * 1.2, 10.0)
|
||||
|
||||
# ── 5. GPS interference overlap ──
|
||||
gps_overlap = 0
|
||||
for z in (gps_data or []):
|
||||
zlat = z.get("lat", 0)
|
||||
zlon = z.get("lon", 0)
|
||||
if isinstance(zlat, (int, float)) and isinstance(zlon, (int, float)):
|
||||
if _in_bbox(zlat, zlon, bbox):
|
||||
gps_overlap += 1
|
||||
ew_score = gps_overlap * 4.0
|
||||
|
||||
total = news_score + conflict_score + mil_score + ship_score + ew_score
|
||||
if total < 1.0:
|
||||
continue
|
||||
|
||||
# Build explanation string from active signals
|
||||
signals = []
|
||||
if news_hits:
|
||||
signals.append(f"{news_hits} intel mentions")
|
||||
if conflict_crit or conflict_high:
|
||||
parts = []
|
||||
if conflict_crit:
|
||||
parts.append(f"{conflict_crit} CRITICAL")
|
||||
if conflict_high:
|
||||
parts.append(f"{conflict_high} HIGH")
|
||||
signals.append(f"{'+'.join(parts)} events")
|
||||
if mil_count:
|
||||
signals.append(f"{mil_count} military aircraft tracked")
|
||||
if ship_count:
|
||||
desc = f"{ship_count} vessels"
|
||||
if tanker_count:
|
||||
desc += f" ({tanker_count} tankers)"
|
||||
signals.append(desc)
|
||||
if gps_overlap:
|
||||
signals.append(f"{gps_overlap} GPS jamming zone{'s' if gps_overlap > 1 else ''}")
|
||||
|
||||
event = region["baseline"]
|
||||
if signals:
|
||||
event += " — " + ", ".join(signals[:3])
|
||||
|
||||
prob_pct = max(25, min(95, int(30 + total * 2.5 + score * 1.5)))
|
||||
|
||||
# Build AI-style reasoning text from active signals
|
||||
reason_parts = []
|
||||
if news_hits:
|
||||
matching_kws = [kw for kw in region["keywords"] if kw in title_blob][:3]
|
||||
reason_parts.append(
|
||||
f"Multiple intelligence feeds ({news_hits}) reference "
|
||||
f"{', '.join(matching_kws) if matching_kws else 'this region'}, "
|
||||
f"indicating heightened international attention."
|
||||
)
|
||||
if conflict_crit:
|
||||
reason_parts.append(
|
||||
f"{conflict_crit} CRITICAL-severity conflict event{'s' if conflict_crit > 1 else ''} "
|
||||
f"detected via OSINT, suggesting active kinetic operations or imminent escalation."
|
||||
)
|
||||
if conflict_high:
|
||||
reason_parts.append(
|
||||
f"{conflict_high} HIGH-severity event{'s' if conflict_high > 1 else ''} "
|
||||
f"reported in the area, indicating elevated security posture."
|
||||
)
|
||||
if mil_count:
|
||||
reason_parts.append(
|
||||
f"ADS-B tracking shows {mil_count} military aircraft operating within the region"
|
||||
f"{f' alongside {civ_count} civilian flights' if civ_count > 20 else ''}, "
|
||||
f"{'which represents unusual concentration' if mil_count >= 5 else 'consistent with ongoing monitoring'}."
|
||||
)
|
||||
if ship_count:
|
||||
s_detail = f"{ship_count} vessels tracked"
|
||||
if tanker_count:
|
||||
s_detail += f" including {tanker_count} tanker{'s' if tanker_count > 1 else ''}"
|
||||
reason_parts.append(
|
||||
f"Maritime surveillance identifies {s_detail} in strategic waters, "
|
||||
f"{'raising chokepoint disruption concerns' if tanker_count >= 2 else 'indicating normal commerce flow'}."
|
||||
)
|
||||
if gps_overlap:
|
||||
reason_parts.append(
|
||||
f"Active GPS jamming/spoofing detected ({gps_overlap} zone{'s' if gps_overlap > 1 else ''}), "
|
||||
f"a strong indicator of electronic warfare activity in the area."
|
||||
)
|
||||
if not reason_parts:
|
||||
reason_parts.append(region["baseline"])
|
||||
|
||||
reason = " ".join(reason_parts)
|
||||
|
||||
scored.append((total, key, {
|
||||
"location": region["name"],
|
||||
"lat": region["lat"],
|
||||
"lon": region["lon"],
|
||||
"event": event[:200],
|
||||
"reason": reason[:500],
|
||||
"probability": f"{prob_pct}%",
|
||||
"_signals": {
|
||||
"news": news_hits, "conflicts": conflict_crit + conflict_high,
|
||||
"mil_planes": mil_count, "ships": ship_count, "gps": gps_overlap,
|
||||
},
|
||||
}))
|
||||
|
||||
scored.sort(key=lambda x: x[0], reverse=True)
|
||||
predictions = []
|
||||
for _, _, item in scored[:4]:
|
||||
# Strip internal fields
|
||||
item.pop("_signals", None)
|
||||
predictions.append(item)
|
||||
|
||||
# Fallback if nothing ranked
|
||||
if not predictions:
|
||||
predictions.append({
|
||||
"location": "Global Monitoring",
|
||||
"lat": 20.0, "lon": 10.0,
|
||||
"event": "No dominant hotspot; maintain broad surveillance",
|
||||
"reason": "Current intelligence signals do not indicate a concentrated threat in any single region. Continuing broad-spectrum monitoring across all sensor feeds.",
|
||||
"probability": _probability(score, 0),
|
||||
})
|
||||
|
||||
return predictions
|
||||
|
||||
|
||||
def _sanitize_prediction(item: dict, fallback_prob: str) -> dict | None:
|
||||
if not isinstance(item, dict):
|
||||
return None
|
||||
lat = item.get("lat")
|
||||
lon = item.get("lon")
|
||||
try:
|
||||
lat = float(lat)
|
||||
lon = float(lon)
|
||||
except Exception:
|
||||
return None
|
||||
if not (-90 <= lat <= 90 and -180 <= lon <= 180):
|
||||
return None
|
||||
|
||||
location = str(item.get("location", "Unknown Location")).strip()[:80]
|
||||
event = str(item.get("event", "Potential activity shift")).strip()[:180]
|
||||
prob = str(item.get("probability", fallback_prob)).strip()[:8]
|
||||
if not re.match(r"^\d{1,3}%$", prob):
|
||||
prob = fallback_prob
|
||||
|
||||
return {
|
||||
"location": location or "Unknown Location",
|
||||
"lat": lat,
|
||||
"lon": lon,
|
||||
"event": event or "Potential activity shift",
|
||||
"reason": str(item.get("reason", "")).strip()[:500],
|
||||
"probability": prob,
|
||||
}
|
||||
|
||||
|
||||
def _sanitize_llm_output(raw: dict, fallback_level: str, fallback_predictions: list[dict]) -> dict:
|
||||
summary = str(raw.get("summary", "")).strip()
|
||||
if not summary:
|
||||
summary = "AI produced no summary; heuristic assessment applied."
|
||||
summary = summary[:220]
|
||||
|
||||
level = str(raw.get("threat_level", "")).upper().strip()
|
||||
if level not in THREAT_LEVELS:
|
||||
level = fallback_level
|
||||
|
||||
# Build lookup of heuristic reasons by location name for fallback
|
||||
heuristic_reasons: dict[str, str] = {}
|
||||
for fp in fallback_predictions:
|
||||
loc = str(fp.get("location", "")).lower().strip()
|
||||
if loc and fp.get("reason"):
|
||||
heuristic_reasons[loc] = fp["reason"]
|
||||
|
||||
clean_predictions: list[dict] = []
|
||||
for p in raw.get("predictions", [])[:4]:
|
||||
cleaned = _sanitize_prediction(p, fallback_predictions[0]["probability"])
|
||||
if cleaned:
|
||||
# If LLM didn't provide a reason, try to merge from heuristic
|
||||
if not cleaned.get("reason"):
|
||||
loc_key = str(cleaned.get("location", "")).lower().strip()
|
||||
cleaned["reason"] = heuristic_reasons.get(loc_key, "")
|
||||
clean_predictions.append(cleaned)
|
||||
if len(clean_predictions) >= 4:
|
||||
break
|
||||
|
||||
if len(clean_predictions) < 2:
|
||||
clean_predictions = fallback_predictions
|
||||
|
||||
return {
|
||||
"summary": summary,
|
||||
"threat_level": level,
|
||||
"predictions": clean_predictions,
|
||||
}
|
||||
|
||||
|
||||
async def _query_ollama(prompt: str) -> dict | None:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(
|
||||
OLLAMA_URL,
|
||||
json={
|
||||
"model": MODEL_NAME,
|
||||
"prompt": prompt,
|
||||
"stream": False,
|
||||
"format": "json",
|
||||
},
|
||||
timeout=20.0,
|
||||
)
|
||||
if response.status_code != 200:
|
||||
return None
|
||||
|
||||
payload = response.json()
|
||||
raw = payload.get("response", "")
|
||||
if not raw:
|
||||
return None
|
||||
|
||||
try:
|
||||
parsed = json.loads(raw)
|
||||
return parsed if isinstance(parsed, dict) else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
async def analyze_threat(
|
||||
news_items,
|
||||
gps_data=None,
|
||||
bgp_data=None,
|
||||
conflicts=None,
|
||||
space_weather=None,
|
||||
ships=None,
|
||||
planes=None,
|
||||
):
|
||||
if not news_items and not conflicts:
|
||||
return {
|
||||
"summary": "Awaiting intelligence data...",
|
||||
"threat_level": "LOW",
|
||||
"predictions": [],
|
||||
"key_drivers": [],
|
||||
"regional_briefs": [],
|
||||
"correlations": [],
|
||||
"watch_items": ["Maintain standard surveillance posture across all domains"],
|
||||
"source": "heuristic",
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
|
||||
score, reasons = _score_from_signals(news_items, gps_data or [], bgp_data or {})
|
||||
|
||||
# Boost score from conflicts
|
||||
if conflicts:
|
||||
crit = sum(1 for c in conflicts if c.get("severity") == "CRITICAL")
|
||||
high = sum(1 for c in conflicts if c.get("severity") == "HIGH")
|
||||
conflict_bump = min(crit * 2 + high, 10)
|
||||
score += conflict_bump
|
||||
if conflict_bump:
|
||||
reasons.append(f"conflicts={crit}C/{high}H")
|
||||
|
||||
base_level = _threat_from_score(score)
|
||||
base_predictions = _heuristic_predictions(
|
||||
news_items, gps_data or [], score,
|
||||
conflicts=conflicts or [],
|
||||
planes=planes or [],
|
||||
ships=ships or [],
|
||||
)
|
||||
|
||||
# Build enhanced intel sections
|
||||
regional = _regional_briefs(news_items, conflicts or [])
|
||||
drivers = _key_drivers(score, reasons, gps_data or [], bgp_data or {}, conflicts or [], space_weather or {}, planes=planes, ships=ships)
|
||||
corr = _correlations(gps_data or [], conflicts or [], bgp_data or {}, space_weather or {})
|
||||
watch = _watch_items(conflicts or [], gps_data or [], space_weather or {})
|
||||
|
||||
# Build movement summary for LLM prompt
|
||||
mil_planes_total = sum(1 for p in (planes or []) if p.get("military"))
|
||||
ship_total = len(ships or [])
|
||||
movement_ctx = (
|
||||
f"Military aircraft tracked: {mil_planes_total}. "
|
||||
f"Vessels tracked: {ship_total}. "
|
||||
)
|
||||
# Summarize heuristic predictions for LLM context
|
||||
hotspot_ctx = " | ".join(
|
||||
f"{p['location']}({p['probability']}): {p['event'][:60]}"
|
||||
for p in base_predictions[:3]
|
||||
)
|
||||
|
||||
headlines = " | ".join(str(item.get('title', ''))[:80] for item in news_items[:6])
|
||||
prompt = (
|
||||
f"STRATCOM AI. Strict JSON only, no markdown.\n"
|
||||
f"News: {headlines}\n"
|
||||
f"Signals: GPS={len(gps_data or [])}, BGP={(bgp_data or {}).get('status','?')}, {movement_ctx.strip()}\n"
|
||||
f"Hotspots: {hotspot_ctx}\n"
|
||||
f'Return: {{"summary":"<1 sentence>","threat_level":"LOW|GUARDED|ELEVATED|HIGH|SEVERE",'
|
||||
f'"predictions":[{{"location":"","lat":0,"lon":0,"event":"","probability":"NN%","reason":""}}]}}'
|
||||
)
|
||||
|
||||
try:
|
||||
if _CLI_BACKEND != "ollama":
|
||||
llm_raw = await _query_cli(prompt)
|
||||
source_name = _CLI_BACKEND
|
||||
else:
|
||||
llm_raw = await _query_ollama(prompt)
|
||||
source_name = MODEL_NAME
|
||||
if llm_raw:
|
||||
sanitized = _sanitize_llm_output(llm_raw, base_level, base_predictions)
|
||||
sanitized["source"] = source_name
|
||||
sanitized["generated_at"] = datetime.now(timezone.utc).isoformat()
|
||||
sanitized["key_drivers"] = drivers
|
||||
sanitized["regional_briefs"] = regional
|
||||
sanitized["correlations"] = corr
|
||||
sanitized["watch_items"] = watch
|
||||
return sanitized
|
||||
except Exception as exc:
|
||||
print(f"[AI] LLM path failed ({_CLI_BACKEND}), fallback engaged: {exc}")
|
||||
|
||||
summary = f"{base_level} risk posture based on {len(news_items)} intelligence items"
|
||||
if conflicts:
|
||||
summary += f" + {len(conflicts)} military events"
|
||||
if reasons:
|
||||
summary += f"; key drivers: {', '.join(reasons[:4])}."
|
||||
else:
|
||||
summary += "."
|
||||
|
||||
return {
|
||||
"summary": summary[:220],
|
||||
"threat_level": base_level,
|
||||
"predictions": base_predictions,
|
||||
"key_drivers": drivers,
|
||||
"regional_briefs": regional,
|
||||
"correlations": corr,
|
||||
"watch_items": watch,
|
||||
"source": "heuristic",
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
TEST = [{"title": "Missile strike reported near border region"}, {"title": "Cyberattack impacts telecom routing"}]
|
||||
print(asyncio.run(analyze_threat(TEST, gps_data=[{"lat": 32.1, "lon": 35.0}], bgp_data={"status": "ELEVATED"})))
|
||||
@@ -0,0 +1,177 @@
|
||||
import asyncio
|
||||
import gzip
|
||||
import json
|
||||
import websockets
|
||||
import httpx
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
AIS_STREAM_URL = "wss://stream.aisstream.io/v1/stream"
|
||||
API_KEY = os.getenv("AIS_STREAM_KEY", "185a6c61223ce81ed70cec58eab359009ebd5a0e")
|
||||
|
||||
_real_vessel_cache: dict = {}
|
||||
_real_vessel_lock = asyncio.Lock()
|
||||
|
||||
def get_vessel_category(ship_type):
|
||||
if not ship_type: return "Other"
|
||||
if 70 <= ship_type <= 79: return "Cargo"
|
||||
if 80 <= ship_type <= 89: return "Tanker"
|
||||
if 60 <= ship_type <= 69: return "Passenger"
|
||||
if ship_type == 35: return "Military"
|
||||
if 30 <= ship_type <= 34: return "Fishing"
|
||||
return "Other"
|
||||
|
||||
async def ais_worker():
|
||||
if not API_KEY:
|
||||
print("[MARITIME] Missing API Key.")
|
||||
return
|
||||
print("[MARITIME] Starting Global AIS Stream...")
|
||||
while True:
|
||||
try:
|
||||
async with websockets.connect(AIS_STREAM_URL, ping_interval=20, ping_timeout=20) as ws:
|
||||
await ws.send(json.dumps({
|
||||
"APIKey": API_KEY,
|
||||
"BoundingBoxes": [[[-90, -180], [90, 180]]],
|
||||
"FilterMessageTypes": ["PositionReport"]
|
||||
}))
|
||||
msg_count = 0
|
||||
async for message in ws:
|
||||
msg = json.loads(message)
|
||||
meta = msg.get("MetaData", {})
|
||||
mmsi = meta.get("MMSI")
|
||||
if not mmsi:
|
||||
continue
|
||||
async with _real_vessel_lock:
|
||||
mid = str(mmsi)
|
||||
if mid not in _real_vessel_cache:
|
||||
_real_vessel_cache[mid] = {"id": mid, "source": "aisstream"}
|
||||
v = _real_vessel_cache[mid]
|
||||
v["last_seen"] = datetime.now(timezone.utc)
|
||||
name = meta.get("ShipName", "").strip()
|
||||
if name:
|
||||
v["name"] = name
|
||||
elif "name" not in v:
|
||||
v["name"] = f"MMSI-{mmsi}"
|
||||
ship_type = meta.get("ShipType", 0)
|
||||
v["type"] = get_vessel_category(ship_type)
|
||||
if msg["MessageType"] == "PositionReport":
|
||||
pos = msg["Message"]["PositionReport"]
|
||||
lat = pos.get("Latitude")
|
||||
lon = pos.get("Longitude")
|
||||
# Filter invalid coordinates (land-based noise, 0/0, out of range)
|
||||
if (lat is None or lon is None
|
||||
or abs(lat) > 90 or abs(lon) > 180
|
||||
or (abs(lat) < 0.1 and abs(lon) < 0.1)):
|
||||
continue
|
||||
v["lat"] = round(lat, 5)
|
||||
v["lon"] = round(lon, 5)
|
||||
v["velocity"] = round(pos.get("Sog", 0) * 0.5144, 2)
|
||||
v["heading"] = pos.get("Cog", 0)
|
||||
msg_count += 1
|
||||
if msg_count % 500 == 0:
|
||||
print(f"[MARITIME] {len(_real_vessel_cache)} vessels cached")
|
||||
except Exception as e:
|
||||
print(f"[MARITIME] Stream error: {e}. Reconnecting in 10s...")
|
||||
await asyncio.sleep(10)
|
||||
|
||||
|
||||
async def ais_pruner():
|
||||
while True:
|
||||
await asyncio.sleep(60)
|
||||
now = datetime.now(timezone.utc)
|
||||
async with _real_vessel_lock:
|
||||
stale = [k for k, v in _real_vessel_cache.items()
|
||||
if (now - v["last_seen"]).total_seconds() > 1200]
|
||||
for k in stale:
|
||||
del _real_vessel_cache[k]
|
||||
|
||||
|
||||
async def _fetch_digitraffic() -> list:
|
||||
"""
|
||||
Digitraffic AIS — free, no key, Baltic/European coverage.
|
||||
Uses `from` parameter to get only vessels seen in the last 20 minutes.
|
||||
Typically returns 300–700 active vessels with fresh positions.
|
||||
"""
|
||||
import time
|
||||
try:
|
||||
from_ms = int((time.time() - 1200) * 1000) # last 20 minutes
|
||||
headers = {
|
||||
"Accept": "application/json",
|
||||
"Accept-Encoding": "gzip",
|
||||
"Digitraffic-User": "GodsEye/3.0",
|
||||
}
|
||||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||||
r = await client.get(
|
||||
f"https://meri.digitraffic.fi/api/ais/v1/locations?from={from_ms}",
|
||||
headers=headers,
|
||||
)
|
||||
if r.status_code != 200:
|
||||
print(f"[MARITIME] Digitraffic {r.status_code}")
|
||||
return []
|
||||
raw = r.content
|
||||
try:
|
||||
data = json.loads(gzip.decompress(raw))
|
||||
except Exception:
|
||||
data = r.json()
|
||||
|
||||
features = data.get("features", []) if isinstance(data, dict) else []
|
||||
ships = []
|
||||
for item in features:
|
||||
geo = (item.get("geometry") or {}).get("coordinates", [])
|
||||
if len(geo) < 2:
|
||||
continue
|
||||
lon, lat = geo[0], geo[1]
|
||||
if abs(lat) > 90 or abs(lon) > 180 or (abs(lat) < 0.01 and abs(lon) < 0.01):
|
||||
continue
|
||||
props = item.get("properties", {})
|
||||
mmsi = str(item.get("mmsi") or props.get("mmsi", ""))
|
||||
sog = props.get("sog", 0) or 0
|
||||
heading = props.get("heading", 0) or props.get("cog", 0) or 0
|
||||
ships.append({
|
||||
"id": f"dt_{mmsi}",
|
||||
"name": f"MMSI-{mmsi}",
|
||||
"lat": round(lat, 5),
|
||||
"lon": round(lon, 5),
|
||||
"velocity": round(sog * 0.5144, 2),
|
||||
"heading": heading,
|
||||
"type": "Cargo",
|
||||
"source": "digitraffic",
|
||||
})
|
||||
print(f"[MARITIME] Digitraffic: {len(ships)} vessels (last 20min)")
|
||||
return ships
|
||||
except Exception as e:
|
||||
print(f"[MARITIME] Digitraffic error: {e}")
|
||||
return []
|
||||
|
||||
|
||||
_dt_cache: list = []
|
||||
_dt_cache_time: float = 0.0
|
||||
_DT_TTL = 300.0 # refresh Digitraffic every 5 minutes
|
||||
|
||||
|
||||
async def fetch_ships() -> list:
|
||||
"""Merge AISStream (global, real-time) + Digitraffic (Baltic, 5-min cache)."""
|
||||
import time
|
||||
global _dt_cache, _dt_cache_time
|
||||
|
||||
# Refresh Digitraffic cache if stale
|
||||
if time.monotonic() - _dt_cache_time > _DT_TTL:
|
||||
_dt_cache = await _fetch_digitraffic()
|
||||
_dt_cache_time = time.monotonic()
|
||||
|
||||
async with _real_vessel_lock:
|
||||
stream_ships = [
|
||||
v for v in _real_vessel_cache.values()
|
||||
if v.get("lat") is not None and v.get("lon") is not None
|
||||
]
|
||||
|
||||
# Merge: AISStream takes priority, Digitraffic fills in the rest
|
||||
if stream_ships:
|
||||
stream_ids = {s["id"] for s in stream_ships}
|
||||
dt_extra = [s for s in _dt_cache if s["id"] not in stream_ids]
|
||||
return stream_ships + dt_extra
|
||||
|
||||
return _dt_cache
|
||||
@@ -0,0 +1,71 @@
|
||||
import httpx
|
||||
import asyncio
|
||||
from datetime import datetime, timezone
|
||||
|
||||
# RIPE Stat - free API, no key required
|
||||
# Fetch BGP update activity (withdrawals/announcements) for a given time window
|
||||
RIPE_UPDATES_URL = "https://stat.ripe.net/data/bgp-updates/data.json"
|
||||
RIPE_ROUTING_URL = "https://stat.ripe.net/data/routing-status/data.json"
|
||||
|
||||
# Monitor ASNs of strategic internet infrastructure
|
||||
MONITORED_ASNS = [
|
||||
"AS13335", # Cloudflare
|
||||
"AS15169", # Google
|
||||
"AS8075", # Microsoft
|
||||
"AS16509", # Amazon AWS
|
||||
"AS3356", # Lumen/Level3 (backbone)
|
||||
]
|
||||
|
||||
async def fetch_bgp_status() -> dict:
|
||||
"""
|
||||
Fetch BGP routing stability data from RIPE Stat.
|
||||
Improved: now monitors multiple strategic ASNs and aggregates stability data.
|
||||
"""
|
||||
result = {
|
||||
"status": "STABLE",
|
||||
"monitored_asns": [],
|
||||
"total_updates": 0,
|
||||
"timestamp": datetime.now(timezone.utc).isoformat()
|
||||
}
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
# Fetch routing status for a major backbone provider
|
||||
response = await client.get(
|
||||
RIPE_ROUTING_URL,
|
||||
params={"resource": "1.1.1.1"},
|
||||
timeout=10.0
|
||||
)
|
||||
if response.status_code == 200:
|
||||
data = response.json().get("data", {})
|
||||
result["routing_data"] = {
|
||||
"prefixes_originated": data.get("prefixes_originated", []),
|
||||
"visibility": data.get("visibility", {}),
|
||||
"resource": data.get("resource", "1.1.1.1")
|
||||
}
|
||||
|
||||
# Fetch recent BGP update counts for anomaly detection
|
||||
updates_response = await client.get(
|
||||
RIPE_UPDATES_URL,
|
||||
params={"resource": "0.0.0.0/0", "hours": 1},
|
||||
timeout=10.0
|
||||
)
|
||||
if updates_response.status_code == 200:
|
||||
updates_data = updates_response.json().get("data", {})
|
||||
nr_updates = updates_data.get("nr_updates", 0)
|
||||
result["total_updates"] = nr_updates
|
||||
# Flag elevated BGP activity (>5000 updates/hour is unusual)
|
||||
if nr_updates > 20000:
|
||||
result["status"] = "CRITICAL"
|
||||
elif nr_updates > 10000:
|
||||
result["status"] = "ELEVATED"
|
||||
|
||||
except Exception as e:
|
||||
print(f"[BGP] Fetch error: {e}")
|
||||
result["status"] = "OFFLINE"
|
||||
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(asyncio.run(fetch_bgp_status()))
|
||||
@@ -0,0 +1,244 @@
|
||||
import httpx
|
||||
import asyncio
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
from difflib import SequenceMatcher
|
||||
|
||||
# GDELT DOC 2.0 API — free, no key
|
||||
# Focused specifically on military attacks and kinetic events
|
||||
GDELT_URL = (
|
||||
"https://api.gdeltproject.org/api/v2/doc/doc?"
|
||||
"query=(missile attack OR rocket attack OR airstrike OR air strike OR "
|
||||
"bombing OR drone strike OR artillery fire OR shelling OR warship attack OR "
|
||||
"naval attack OR military strike OR armed attack OR mortar OR explosion site OR "
|
||||
"IED explosion OR sniper OR combat OR military offensive OR invasion force)"
|
||||
"&mode=artlist&format=json&maxrecords=75&sourcelang=english×pan=12h"
|
||||
)
|
||||
|
||||
# Geo-coder: keyword → (lat, lon)
|
||||
GEO_DATA = {
|
||||
# Ukraine conflict zones
|
||||
"Ukraine": [48.3794, 31.1656], "Crimea": [44.9521, 34.1024],
|
||||
"Donbas": [48.0159, 37.8028], "Donbass": [48.0159, 37.8028],
|
||||
"Kherson": [46.6354, 32.6169], "Zaporizhzhia": [47.8388, 35.1396],
|
||||
"Kharkiv": [49.9935, 36.2304], "Kyiv": [50.4501, 30.5234],
|
||||
"Kiev": [50.4501, 30.5234], "Odesa": [46.4825, 30.7233],
|
||||
"Mariupol": [47.0958, 37.5483], "Bakhmut": [48.5963, 38.0000],
|
||||
"Avdiivka": [48.1344, 37.7490],
|
||||
# Russia
|
||||
"Russia": [61.5240, 105.3188], "Moscow": [55.7558, 37.6173],
|
||||
"Belarus": [53.7098, 27.9534],
|
||||
# Middle East
|
||||
"Israel": [31.0461, 34.8516], "Gaza": [31.3547, 34.3088],
|
||||
"West Bank": [31.9466, 35.3027], "Rafah": [31.2969, 34.2455],
|
||||
"Jenin": [32.4607, 35.3027], "Lebanon": [33.8547, 35.8623],
|
||||
"Hezbollah": [33.8547, 35.8623], "Syria": [34.8021, 38.9968],
|
||||
"Damascus": [33.5138, 36.2765], "Aleppo": [36.2021, 37.1343],
|
||||
"Iran": [32.4279, 53.6880], "Tehran": [35.6892, 51.3890],
|
||||
"Iraq": [33.2232, 43.6793], "Baghdad": [33.3152, 44.3661],
|
||||
"Yemen": [15.5527, 48.5164], "Houthi": [15.5527, 48.5164],
|
||||
"Saudi Arabia": [23.8859, 45.0792],
|
||||
# Asia-Pacific
|
||||
"China": [35.8617, 104.1954], "Taiwan": [23.6978, 120.9605],
|
||||
"Taipei": [25.0330, 121.5654], "Taiwan Strait": [24.0000, 119.5000],
|
||||
"North Korea": [40.3399, 127.5101], "Pyongyang": [39.0194, 125.7381],
|
||||
"South Korea": [35.9078, 127.7669], "Japan": [36.2048, 138.2529],
|
||||
"Philippines": [12.8797, 121.7740], "South China Sea": [12.0000, 113.0000],
|
||||
"Myanmar": [21.9162, 95.9560], "India": [20.5937, 78.9629],
|
||||
"Pakistan": [30.3753, 69.3451], "Afghanistan": [33.9391, 67.7100],
|
||||
"Kashmir": [34.0837, 74.7973],
|
||||
# Africa
|
||||
"Sudan": [12.8628, 30.2176], "Ethiopia": [9.1450, 40.4897],
|
||||
"Somalia": [5.1521, 46.1996], "Libya": [26.3351, 17.2283],
|
||||
"Mali": [17.5707, -3.9962], "Niger": [17.6078, 8.0817],
|
||||
"Nigeria": [9.0820, 8.6753], "Congo": [-4.0383, 21.7587],
|
||||
"Sahel": [15.4542, 0.0000], "Burkina Faso": [12.2383, -1.5616],
|
||||
"Mozambique": [-18.6657, 35.5296],
|
||||
# Americas
|
||||
"Venezuela": [6.4238, -66.5897], "Colombia": [4.5709, -74.2973],
|
||||
"Mexico": [23.6345, -102.5528],
|
||||
# Strategic waterways
|
||||
"Red Sea": [20.0000, 38.0000], "Strait of Hormuz": [26.5667, 56.2500],
|
||||
"Bab-el-Mandeb": [12.6, 43.5], "Suez Canal": [30.4550, 32.3500],
|
||||
"Black Sea": [43.4000, 34.0000], "Baltic Sea": [58.0000, 20.0000],
|
||||
"East China Sea": [30.0000, 126.0000], "Sea of Azov": [46.0000, 36.5000],
|
||||
"Persian Gulf": [26.0000, 52.0000],
|
||||
# Palestine
|
||||
"Palestine": [31.9522, 35.2332], "Jerusalem": [31.7683, 35.2137],
|
||||
"Tel Aviv": [32.0853, 34.7818], "Nablus": [32.2211, 35.2544],
|
||||
}
|
||||
|
||||
# Attack type classifier — ordered by priority (most specific first)
|
||||
# Each entry: (search_term, event_label, severity)
|
||||
ATTACK_CLASSIFIER = [
|
||||
("ballistic missile", "BALLISTIC MISSILE", "CRITICAL"),
|
||||
("cruise missile", "CRUISE MISSILE", "CRITICAL"),
|
||||
("hypersonic missile", "HYPERSONIC MISSILE", "CRITICAL"),
|
||||
("missile strike", "MISSILE STRIKE", "CRITICAL"),
|
||||
("missile attack", "MISSILE ATTACK", "CRITICAL"),
|
||||
("rocket attack", "ROCKET ATTACK", "CRITICAL"),
|
||||
("rocket barrage", "ROCKET BARRAGE", "CRITICAL"),
|
||||
("drone strike", "DRONE STRIKE", "CRITICAL"),
|
||||
("drone attack", "DRONE ATTACK", "CRITICAL"),
|
||||
("airstrike", "AIRSTRIKE", "CRITICAL"),
|
||||
("air strike", "AIRSTRIKE", "CRITICAL"),
|
||||
("air raid", "AIR RAID", "CRITICAL"),
|
||||
("bombing", "BOMBING", "CRITICAL"),
|
||||
("bomb", "EXPLOSION", "HIGH"),
|
||||
("shelling", "ARTILLERY SHELLING", "HIGH"),
|
||||
("artillery", "ARTILLERY FIRE", "HIGH"),
|
||||
("mortar", "MORTAR ATTACK", "HIGH"),
|
||||
("ied explosion", "IED EXPLOSION", "HIGH"),
|
||||
("explosion", "EXPLOSION", "HIGH"),
|
||||
("sniper", "SNIPER ACTIVITY", "MODERATE"),
|
||||
("naval attack", "NAVAL ATTACK", "HIGH"),
|
||||
("warship", "NAVAL ACTIVITY", "MODERATE"),
|
||||
("invasion", "INVASION", "CRITICAL"),
|
||||
("offensive", "MILITARY OFFENSIVE", "HIGH"),
|
||||
("combat", "COMBAT", "HIGH"),
|
||||
("attack", "ATTACK", "HIGH"),
|
||||
("strike", "STRIKE", "HIGH"),
|
||||
("troops", "GROUND FORCES", "MODERATE"),
|
||||
("military", "MILITARY ACTIVITY", "MODERATE"),
|
||||
]
|
||||
|
||||
SEVERITY_ORDER = {"CRITICAL": 0, "HIGH": 1, "MODERATE": 2}
|
||||
|
||||
EXCLUDE_KEYWORDS = [
|
||||
"sport", "football", "soccer", "la liga", "cup", "olympics",
|
||||
"tennis", "nfl", "nba", "golf", "cricket", "rugby",
|
||||
"celebrity", "fashion", "movie", "film", "oscars", "grammy",
|
||||
"recipe", "horoscope", "box office", "netflix",
|
||||
"rocket launch", "spacex", "starship", "nasa launch", # civilian launches
|
||||
]
|
||||
|
||||
|
||||
def classify_event(title: str) -> tuple[str, str]:
|
||||
"""Returns (event_label, severity) for a conflict title."""
|
||||
tl = title.lower()
|
||||
for term, label, severity in ATTACK_CLASSIFIER:
|
||||
if term in tl:
|
||||
return label, severity
|
||||
return "MILITARY EVENT", "MODERATE"
|
||||
|
||||
|
||||
def _title_similarity(a: str, b: str) -> float:
|
||||
return SequenceMatcher(None, a.lower(), b.lower()).ratio()
|
||||
|
||||
|
||||
def _parse_gdelt_date(seendate: str) -> str:
|
||||
try:
|
||||
dt = datetime.strptime(seendate, "%Y%m%dT%H%M%SZ")
|
||||
return dt.replace(tzinfo=timezone.utc).isoformat()
|
||||
except Exception:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
async def fetch_conflicts() -> list:
|
||||
"""
|
||||
Fetch military attack/conflict events from GDELT DOC API.
|
||||
Returns events classified by attack type and severity.
|
||||
"""
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
# Retry with backoff for 429 rate limits
|
||||
for attempt in range(3):
|
||||
if attempt > 0:
|
||||
await asyncio.sleep(6 * attempt)
|
||||
response = await client.get(GDELT_URL, timeout=12.0)
|
||||
if response.status_code == 429:
|
||||
print(f"[CONFLICTS] GDELT rate limited, retry {attempt+1}/3")
|
||||
continue
|
||||
break
|
||||
if response.status_code != 200:
|
||||
print(f"[CONFLICTS] GDELT returned {response.status_code}")
|
||||
return []
|
||||
|
||||
try:
|
||||
data = response.json() if response.text.strip() else {}
|
||||
except Exception:
|
||||
print(f"[CONFLICTS] GDELT returned non-JSON body")
|
||||
return []
|
||||
raw_articles = data.get("articles", [])
|
||||
if not raw_articles:
|
||||
print("[CONFLICTS] No articles in GDELT response")
|
||||
return []
|
||||
|
||||
events = []
|
||||
|
||||
for article in raw_articles:
|
||||
title = (article.get("title") or "").strip()
|
||||
if not title:
|
||||
continue
|
||||
|
||||
tl = title.lower()
|
||||
|
||||
# Filter out non-conflict content
|
||||
if any(kw in tl for kw in EXCLUDE_KEYWORDS):
|
||||
continue
|
||||
|
||||
# Geocode
|
||||
lat, lon = None, None
|
||||
for region, coords in GEO_DATA.items():
|
||||
if re.search(r'\b' + re.escape(region) + r'\b', title, re.IGNORECASE):
|
||||
lat, lon = coords
|
||||
break
|
||||
|
||||
if lat is None or lon is None:
|
||||
continue
|
||||
if not (-90 <= lat <= 90) or not (-180 <= lon <= 180):
|
||||
continue
|
||||
|
||||
# Deduplicate by title similarity
|
||||
is_duplicate = any(
|
||||
_title_similarity(title, ev["title"]) > 0.75
|
||||
for ev in events
|
||||
)
|
||||
if is_duplicate:
|
||||
continue
|
||||
|
||||
event_label, severity = classify_event(title)
|
||||
seendate = article.get("seendate", "")
|
||||
published = _parse_gdelt_date(seendate) if seendate else datetime.now(timezone.utc).isoformat()
|
||||
|
||||
events.append({
|
||||
"title": title,
|
||||
"url": article.get("url", ""),
|
||||
"image": article.get("urlMobileImage", ""),
|
||||
"source": article.get("domain", "Unknown"),
|
||||
"domain": article.get("sourcecountry", "Unknown"),
|
||||
"lat": lat,
|
||||
"lon": lon,
|
||||
"published_at": published,
|
||||
"event_type": event_label,
|
||||
"severity": severity,
|
||||
"type": "conflict",
|
||||
})
|
||||
|
||||
# Sort: CRITICAL first, then by date
|
||||
events.sort(key=lambda e: (
|
||||
SEVERITY_ORDER.get(e["severity"], 3),
|
||||
e["published_at"]
|
||||
), reverse=False)
|
||||
events.sort(key=lambda e: SEVERITY_ORDER.get(e["severity"], 3))
|
||||
|
||||
events = events[:40]
|
||||
|
||||
by_sev = {}
|
||||
for e in events:
|
||||
s = e["severity"]
|
||||
by_sev[s] = by_sev.get(s, 0) + 1
|
||||
|
||||
print(f"[CONFLICTS] {len(events)} military events — {by_sev} (from {len(raw_articles)} raw)")
|
||||
return events
|
||||
|
||||
except Exception as e:
|
||||
print(f"[CONFLICTS] Fetch error: {e}")
|
||||
return []
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
result = asyncio.run(fetch_conflicts())
|
||||
print(f"\nMilitary events: {len(result)}")
|
||||
for ev in result[:15]:
|
||||
print(f" [{ev['severity']:8s}] [{ev['event_type']:22s}] {ev['title'][:70]}")
|
||||
@@ -0,0 +1,124 @@
|
||||
import httpx
|
||||
import asyncio
|
||||
import random
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timezone
|
||||
|
||||
# Abuse.ch ThreatFox API - Real-time indicators of compromise
|
||||
THREATFOX_URL = "https://threatfox-api.abuse.ch/api/v1/"
|
||||
|
||||
CACHE_FILE = Path(__file__).parent.parent / ".cache" / "cyber.json"
|
||||
CACHE_DURATION_SEC = 300 # 5 minutes
|
||||
|
||||
def _get_cached_cyber():
|
||||
if CACHE_FILE.exists() and (datetime.now().timestamp() - CACHE_FILE.stat().st_mtime) < CACHE_DURATION_SEC:
|
||||
try:
|
||||
with open(CACHE_FILE, "r") as f:
|
||||
return json.load(f)
|
||||
except: pass
|
||||
return None
|
||||
|
||||
def _save_cache(data):
|
||||
CACHE_FILE.parent.mkdir(exist_ok=True, parents=True)
|
||||
try:
|
||||
with open(CACHE_FILE, "w") as f:
|
||||
json.dump(data, f)
|
||||
except: pass
|
||||
|
||||
# Fallback geocoding for major infrastructure if IP geocoding fails or to show targets
|
||||
CYBER_TARGETS = [
|
||||
{"name": "AWS US-East", "lat": 39.04, "lon": -77.48},
|
||||
{"name": "Google Cloud Europe", "lat": 50.11, "lon": 8.68},
|
||||
{"name": "Azure East Asia", "lat": 22.28, "lon": 114.17},
|
||||
{"name": "DE-CIX Frankfurt", "lat": 50.12, "lon": 8.67},
|
||||
{"name": "London Internet Exchange", "lat": 51.51, "lon": -0.12},
|
||||
{"name": "Equinix Ashburn", "lat": 39.03, "lon": -77.45},
|
||||
]
|
||||
|
||||
async def _get_ip_geo(client, ip):
|
||||
"""Real-time geocoding for malicious IPs using ip-api.com (free for non-commercial)."""
|
||||
try:
|
||||
# Rate limit is 45 requests per minute
|
||||
resp = await client.get(f"http://ip-api.com/json/{ip}?fields=status,lat,lon,country", timeout=2.0)
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
if data.get("status") == "success":
|
||||
return data.get("lat"), data.get("lon"), data.get("country")
|
||||
except: pass
|
||||
return None, None, None
|
||||
|
||||
async def fetch_cyber_warfare(bgp_data=None):
|
||||
"""
|
||||
Fetches REAL cyber threat data from Abuse.ch ThreatFox.
|
||||
Geocodes the malicious sources and maps them to critical infrastructure targets.
|
||||
"""
|
||||
cached = _get_cached_cyber()
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
# Query latest 20 malware/botnet indicators
|
||||
payload = {"query": "get_iocs", "days": 1}
|
||||
resp = await client.post(THREATFOX_URL, json=payload, timeout=10.0)
|
||||
|
||||
if resp.status_code != 200:
|
||||
if CACHE_FILE.exists():
|
||||
try:
|
||||
with open(CACHE_FILE, "r") as f: return json.load(f)
|
||||
except: pass
|
||||
return []
|
||||
|
||||
data = resp.json()
|
||||
if data.get("query_status") != "ok":
|
||||
if CACHE_FILE.exists():
|
||||
try:
|
||||
with open(CACHE_FILE, "r") as f: return json.load(f)
|
||||
except: pass
|
||||
return []
|
||||
|
||||
iocs = data.get("data", [])
|
||||
# Filter for IP addresses (IPv4)
|
||||
ip_iocs = [i for i in iocs if i.get("threat_type") in ["botnet_cc", "payload_delivery"] and ":" not in i.get("ioc", "")]
|
||||
|
||||
attacks = []
|
||||
# Geocode only a subset to respect ip-api limits
|
||||
sample_size = min(len(ip_iocs), 8)
|
||||
sampled = random.sample(ip_iocs, sample_size) if len(ip_iocs) > sample_size else ip_iocs
|
||||
|
||||
for ioc in sampled:
|
||||
ip = ioc["ioc"].split(":")[0]
|
||||
lat, lon, country = await _get_ip_geo(client, ip)
|
||||
|
||||
if lat and lon:
|
||||
target = random.choice(CYBER_TARGETS)
|
||||
intensity = random.uniform(0.4, 1.0)
|
||||
|
||||
attacks.append({
|
||||
"id": f"real_cyb_{ioc['id']}",
|
||||
"source_name": f"{ioc['threat_type'].upper()} ({country or 'Unknown'})",
|
||||
"target_name": target["name"],
|
||||
"startLat": lat,
|
||||
"startLng": lon,
|
||||
"endLat": target["lat"],
|
||||
"endLng": target["lon"],
|
||||
"type": ioc["malware_printable"] or "Unknown Malware",
|
||||
"intensity": intensity,
|
||||
"color": "#ff003c" if "botnet" in ioc["threat_type"] else "#ff8800",
|
||||
"timestamp": ioc["first_seen"]
|
||||
})
|
||||
|
||||
_save_cache(attacks)
|
||||
return attacks
|
||||
|
||||
except Exception as e:
|
||||
print(f"[CYBER] Real data fetch error: {e}")
|
||||
if CACHE_FILE.exists():
|
||||
try:
|
||||
with open(CACHE_FILE, "r") as f: return json.load(f)
|
||||
except: pass
|
||||
return []
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(asyncio.run(fetch_cyber_warfare()))
|
||||
@@ -0,0 +1,91 @@
|
||||
import httpx
|
||||
import asyncio
|
||||
from datetime import datetime, timezone
|
||||
|
||||
# USGS Earthquake Hazards Program - completely free, no API key
|
||||
# Significant earthquakes (M 4.5+) from the past 7 days
|
||||
USGS_URL = "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/4.5_week.geojson"
|
||||
|
||||
|
||||
async def fetch_earthquakes() -> list:
|
||||
"""
|
||||
Fetches real earthquake data from USGS.
|
||||
Returns quakes with M >= 4.5 from the past week.
|
||||
"""
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(USGS_URL, timeout=12.0)
|
||||
if response.status_code != 200:
|
||||
print(f"[SEISMIC] USGS returned {response.status_code}")
|
||||
return []
|
||||
|
||||
data = response.json()
|
||||
quakes = []
|
||||
|
||||
for feature in data.get("features", []):
|
||||
props = feature.get("properties", {})
|
||||
geom = feature.get("geometry", {})
|
||||
coords = geom.get("coordinates", [])
|
||||
|
||||
if len(coords) < 2:
|
||||
continue
|
||||
|
||||
lon, lat = coords[0], coords[1]
|
||||
depth_km = coords[2] if len(coords) > 2 else 0
|
||||
mag = props.get("mag")
|
||||
|
||||
if mag is None:
|
||||
continue
|
||||
# Validate coordinates
|
||||
if not (-90 <= lat <= 90) or not (-180 <= lon <= 180):
|
||||
continue
|
||||
|
||||
# Severity classification
|
||||
if mag >= 7.0:
|
||||
severity = "MAJOR"
|
||||
elif mag >= 6.0:
|
||||
severity = "STRONG"
|
||||
elif mag >= 5.0:
|
||||
severity = "MODERATE"
|
||||
else:
|
||||
severity = "MINOR"
|
||||
|
||||
# Convert USGS epoch ms to ISO string
|
||||
epoch_ms = props.get("time", 0)
|
||||
try:
|
||||
dt = datetime.fromtimestamp(epoch_ms / 1000, tz=timezone.utc)
|
||||
time_str = dt.strftime("%Y-%m-%d %H:%M UTC")
|
||||
except Exception:
|
||||
time_str = "Unknown"
|
||||
|
||||
quakes.append({
|
||||
"id": feature.get("id", ""),
|
||||
"title": props.get("title", "Earthquake"),
|
||||
"place": props.get("place", "Unknown Location"),
|
||||
"lat": lat,
|
||||
"lon": lon,
|
||||
"depth_km": round(depth_km, 1),
|
||||
"magnitude": mag,
|
||||
"severity": severity,
|
||||
"time": time_str,
|
||||
"url": props.get("url", ""),
|
||||
"type": "earthquake",
|
||||
"felt": props.get("felt", 0),
|
||||
"tsunami": props.get("tsunami", 0),
|
||||
})
|
||||
|
||||
# Sort by magnitude descending
|
||||
quakes.sort(key=lambda q: q["magnitude"], reverse=True)
|
||||
print(f"[SEISMIC] {len(quakes)} earthquakes (M≥4.5) — strongest: M{quakes[0]['magnitude'] if quakes else 'N/A'}")
|
||||
return quakes
|
||||
|
||||
except Exception as e:
|
||||
print(f"[SEISMIC] Fetch error: {e}")
|
||||
return []
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
result = asyncio.run(fetch_earthquakes())
|
||||
print(f"Earthquakes: {len(result)}")
|
||||
for q in result[:5]:
|
||||
print(f" M{q['magnitude']} {q['severity']} — {q['place']}")
|
||||
@@ -0,0 +1,100 @@
|
||||
import httpx
|
||||
import asyncio
|
||||
from datetime import datetime, timezone
|
||||
|
||||
# Launch Library 2 API — completely free, no API key required
|
||||
UPCOMING_URL = "https://ll.thespacedevs.com/2.2.0/launch/upcoming/?limit=25&format=json"
|
||||
RECENT_URL = "https://ll.thespacedevs.com/2.2.0/launch/previous/?limit=10&format=json"
|
||||
|
||||
|
||||
def _parse_launch(item: dict) -> dict | None:
|
||||
"""Parse a single launch object from the LL2 API response."""
|
||||
pad = item.get("pad") or {}
|
||||
location = pad.get("location") or {}
|
||||
|
||||
try:
|
||||
lat = float(pad.get("latitude", ""))
|
||||
lon = float(pad.get("longitude", ""))
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
if not (-90 <= lat <= 90) or not (-180 <= lon <= 180):
|
||||
return None
|
||||
|
||||
provider = (item.get("launch_service_provider") or {}).get("name", "Unknown")
|
||||
rocket = ((item.get("rocket") or {}).get("configuration") or {}).get("name", "Unknown")
|
||||
mission = item.get("mission") or {}
|
||||
status = (item.get("status") or {}).get("name", "Unknown")
|
||||
|
||||
return {
|
||||
"id": item.get("id", ""),
|
||||
"name": item.get("name", "Unknown Launch"),
|
||||
"status": status,
|
||||
"net": item.get("net", ""),
|
||||
"provider": provider,
|
||||
"country": location.get("country_code", ""),
|
||||
"pad": pad.get("name", "Unknown Pad"),
|
||||
"lat": lat,
|
||||
"lon": lon,
|
||||
"image": item.get("image", ""),
|
||||
"rocket": rocket,
|
||||
"mission": mission.get("name") or "",
|
||||
"mission_description": mission.get("description") or "",
|
||||
"type": "launch",
|
||||
}
|
||||
|
||||
|
||||
async def fetch_launches() -> list:
|
||||
"""
|
||||
Fetches real rocket launch data from Launch Library 2.
|
||||
Returns combined upcoming + recent launches, sorted by date.
|
||||
"""
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
upcoming_resp, recent_resp = await asyncio.gather(
|
||||
client.get(UPCOMING_URL, timeout=15.0),
|
||||
client.get(RECENT_URL, timeout=15.0),
|
||||
)
|
||||
|
||||
upcoming = []
|
||||
if upcoming_resp.status_code == 200:
|
||||
for item in upcoming_resp.json().get("results", []):
|
||||
parsed = _parse_launch(item)
|
||||
if parsed:
|
||||
upcoming.append(parsed)
|
||||
else:
|
||||
print(f"[LAUNCHES] Upcoming API returned {upcoming_resp.status_code}")
|
||||
|
||||
recent = []
|
||||
if recent_resp.status_code == 200:
|
||||
for item in recent_resp.json().get("results", []):
|
||||
parsed = _parse_launch(item)
|
||||
if parsed:
|
||||
recent.append(parsed)
|
||||
else:
|
||||
print(f"[LAUNCHES] Recent API returned {recent_resp.status_code}")
|
||||
|
||||
combined = upcoming + recent
|
||||
|
||||
# Sort by NET date (earliest first), unknown dates go last
|
||||
def sort_key(launch):
|
||||
try:
|
||||
return datetime.fromisoformat(launch["net"].replace("Z", "+00:00"))
|
||||
except Exception:
|
||||
return datetime.max.replace(tzinfo=timezone.utc)
|
||||
|
||||
combined.sort(key=sort_key)
|
||||
|
||||
print(f"[LAUNCHES] {len(upcoming)} upcoming, {len(recent)} recent launches")
|
||||
return combined
|
||||
|
||||
except Exception as e:
|
||||
print(f"[LAUNCHES] Fetch error: {e}")
|
||||
return []
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
result = asyncio.run(fetch_launches())
|
||||
print(f"Total launches: {len(result)}")
|
||||
for launch in result[:5]:
|
||||
print(f" {launch['net'][:16]} {launch['status']:15s} {launch['name']}")
|
||||
@@ -0,0 +1,268 @@
|
||||
import httpx
|
||||
import asyncio
|
||||
import re
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timezone
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
CACHE_FILE = Path(__file__).parent.parent / ".cache" / "news.json"
|
||||
CACHE_DURATION_SEC = 300 # 5 minutes
|
||||
|
||||
def _get_cached_news():
|
||||
if CACHE_FILE.exists() and (datetime.now().timestamp() - CACHE_FILE.stat().st_mtime) < CACHE_DURATION_SEC:
|
||||
try:
|
||||
with open(CACHE_FILE, "r") as f:
|
||||
return json.load(f)
|
||||
except: pass
|
||||
return None
|
||||
|
||||
def _save_cache(data):
|
||||
CACHE_FILE.parent.mkdir(exist_ok=True, parents=True)
|
||||
try:
|
||||
with open(CACHE_FILE, "w") as f:
|
||||
json.dump(data, f)
|
||||
except: pass
|
||||
|
||||
FEEDS = [
|
||||
"http://www.aljazeera.com/xml/rss/all.xml",
|
||||
"http://feeds.bbci.co.uk/news/world/rss.xml",
|
||||
"https://www.reutersagency.com/feed/?best-topics=political-general&post_type=best",
|
||||
"https://www.theguardian.com/world/rss",
|
||||
"https://feeds.npr.org/1004/rss.xml",
|
||||
"https://foreignpolicy.com/feed/",
|
||||
"https://www.cnbc.com/id/100727362/device/rss/rss.html",
|
||||
"https://rss.nytimes.com/services/xml/rss/nyt/World.xml",
|
||||
"https://feeds.washingtonpost.com/rss/world",
|
||||
]
|
||||
|
||||
# Expanded keyword geocoder — covers most geopolitically relevant regions
|
||||
GEO_DATA = {
|
||||
# Middle East
|
||||
"Iran": [32.4279, 53.6880], "Israel": [31.0461, 34.8516],
|
||||
"Gaza": [31.3547, 34.3088], "West Bank": [31.9466, 35.3027],
|
||||
"Lebanon": [33.8547, 35.8623], "Syria": [34.8021, 38.9968],
|
||||
"Yemen": [15.5527, 48.5164], "Iraq": [33.2232, 43.6793],
|
||||
"Saudi Arabia": [23.8859, 45.0792], "Jordan": [30.5852, 36.2384],
|
||||
"Kuwait": [29.3117, 47.4818], "Qatar": [25.3548, 51.1839],
|
||||
"UAE": [23.4241, 53.8478], "Bahrain": [26.0667, 50.5577],
|
||||
"Oman": [21.5126, 55.9233],
|
||||
# Europe
|
||||
"Ukraine": [48.3794, 31.1656], "Russia": [61.5240, 105.3188],
|
||||
"Germany": [51.1657, 10.4515], "France": [46.2276, 2.2137],
|
||||
"UK": [55.3781, -3.4360], "Poland": [51.9194, 19.1451],
|
||||
"Romania": [45.9432, 24.9668], "Finland": [61.9241, 25.7482],
|
||||
"Sweden": [60.1282, 18.6435], "Norway": [60.4720, 8.4689],
|
||||
"NATO": [50.8503, 4.3517], "Belarus": [53.7098, 27.9534],
|
||||
"Moldova": [47.4116, 28.3699], "Georgia": [42.3154, 43.3569],
|
||||
"Serbia": [44.0165, 20.9129], "Kosovo": [42.6026, 20.9030],
|
||||
# Asia-Pacific
|
||||
"China": [35.8617, 104.1954], "Taiwan": [23.6978, 120.9605],
|
||||
"North Korea": [40.3399, 127.5101], "South Korea": [35.9078, 127.7669],
|
||||
"Japan": [36.2048, 138.2529], "India": [20.5937, 78.9629],
|
||||
"Pakistan": [30.3753, 69.3451], "Afghanistan": [33.9391, 67.7100],
|
||||
"Myanmar": [21.9162, 95.9560], "Philippines": [12.8797, 121.7740],
|
||||
"Vietnam": [14.0583, 108.2772], "South China Sea": [12.0000, 113.0000],
|
||||
# Americas
|
||||
"USA": [37.0902, -95.7129], "Mexico": [23.6345, -102.5528],
|
||||
"Venezuela": [6.4238, -66.5897], "Colombia": [4.5709, -74.2973],
|
||||
"Cuba": [21.5218, -77.7812], "Nicaragua": [12.8654, -85.2072],
|
||||
"Haiti": [18.9712, -72.2852], "Brazil": [14.2350, -51.9253],
|
||||
"Argentina": [-38.4161, -63.6167], "Chile": [-35.6751, -71.5430],
|
||||
"Peru": [-9.1900, -75.0152], "Guyana": [4.8604, -58.9302],
|
||||
# Central Asia & Caucasus
|
||||
"Kazakhstan": [48.0196, 66.9237], "Azerbaijan": [40.1431, 47.5769],
|
||||
"Armenia": [40.0691, 45.0382], "Nagorno-Karabakh": [39.8177, 46.7528],
|
||||
"Uzbekistan": [41.3775, 64.5853], "Kyrgyzstan": [41.2044, 74.7661],
|
||||
# Specific Conflict Regions & Strategic Spots
|
||||
"Gaza": [31.3547, 34.3088], "West Bank": [31.9466, 35.3027],
|
||||
"Donbas": [48.0159, 37.8028], "Kashmir": [34.0837, 74.7973],
|
||||
"Sudan": [12.8628, 30.2176], "Darfur": [13.4175, 24.3311],
|
||||
"Tigray": [14.0323, 38.3166], "Somalia": [5.1521, 46.1996],
|
||||
"Suez Canal": [29.9329, 32.5539], "Panama Canal": [9.1012, -79.6967],
|
||||
"Bering Strait": [66.0, -169.0], "Malacca": [2.5, 102.0],
|
||||
# Cities
|
||||
"New York": [40.7128, -74.0060], "London": [51.5074, -0.1278],
|
||||
"Paris": [48.8566, 2.3522], "Brussels": [50.8503, 4.3517],
|
||||
"Geneva": [46.2044, 6.1432], "Vienna": [48.2082, 16.3738],
|
||||
"Istanbul": [41.0082, 28.9784], "Kyiv": [50.4501, 30.5234],
|
||||
"Moscow": [55.7558, 37.6173], "Tehran": [35.6892, 51.3890],
|
||||
"Beijing": [39.9042, 116.4074], "Tokyo": [35.6762, 139.6503],
|
||||
"Seoul": [37.5665, 126.9780],
|
||||
}
|
||||
|
||||
EXCLUDE_KEYWORDS = [
|
||||
"sport", "football", "soccer", "la liga", "champions league", "cup", "match",
|
||||
"olympics", "tennis", "nfl", "nba", "score", "goal", "premier league",
|
||||
"formula 1", "f1", "golf", "cricket", "rugby", "boxing", "mma",
|
||||
"celebrity", "oscars", "grammy", "fashion", "movie", "film", "series",
|
||||
"recipe", "weather forecast", "horoscope"
|
||||
]
|
||||
|
||||
# Strip HTML tags from RSS descriptions
|
||||
_TAG_RE = re.compile(r'<[^>]+>')
|
||||
|
||||
|
||||
def _strip_html(s: str) -> str:
|
||||
return _TAG_RE.sub('', s).strip()
|
||||
|
||||
|
||||
def _find_text(el: ET.Element, tag: str) -> str:
|
||||
"""Find text for a tag, checking common RSS/Atom namespaces."""
|
||||
node = el.find(tag)
|
||||
if node is not None and node.text:
|
||||
return node.text.strip()
|
||||
# Try with common namespaces
|
||||
for ns in ['{http://purl.org/dc/elements/1.1/}', '{http://purl.org/rss/1.0/}']:
|
||||
node = el.find(f'{ns}{tag}')
|
||||
if node is not None and node.text:
|
||||
return node.text.strip()
|
||||
return ''
|
||||
|
||||
|
||||
async def _fetch_single_feed(client: httpx.AsyncClient, feed_url: str) -> list[dict]:
|
||||
"""Fetch a single RSS feed directly and parse XML items."""
|
||||
articles: list[dict] = []
|
||||
try:
|
||||
resp = await client.get(feed_url, timeout=12.0, follow_redirects=True)
|
||||
if resp.status_code != 200:
|
||||
return []
|
||||
|
||||
root = ET.fromstring(resp.content)
|
||||
# Determine feed title
|
||||
channel = root.find('channel')
|
||||
feed_title = 'Global Intel'
|
||||
if channel is not None:
|
||||
ft = channel.findtext('title')
|
||||
if ft:
|
||||
feed_title = ft.strip()
|
||||
else:
|
||||
# Atom feed
|
||||
ft = root.findtext('{http://www.w3.org/2005/Atom}title')
|
||||
if ft:
|
||||
feed_title = ft.strip()
|
||||
|
||||
# Find items — RSS uses <item>, Atom uses <entry>
|
||||
items = root.findall('.//item')
|
||||
if not items:
|
||||
items = root.findall('.//{http://www.w3.org/2005/Atom}entry')
|
||||
|
||||
for item in items[:10]:
|
||||
title = _find_text(item, 'title')
|
||||
if not title:
|
||||
# Atom title
|
||||
title = item.findtext('{http://www.w3.org/2005/Atom}title') or ''
|
||||
title = title.strip()
|
||||
if not title:
|
||||
continue
|
||||
|
||||
if any(kw in title.lower() for kw in EXCLUDE_KEYWORDS):
|
||||
continue
|
||||
|
||||
# Geocode from keywords
|
||||
lat, lon = None, None
|
||||
for region, coords in GEO_DATA.items():
|
||||
if re.search(r'\b' + re.escape(region) + r'\b', title, re.IGNORECASE):
|
||||
lat, lon = coords
|
||||
break
|
||||
|
||||
# Link
|
||||
link = _find_text(item, 'link')
|
||||
if not link:
|
||||
link_el = item.find('{http://www.w3.org/2005/Atom}link')
|
||||
if link_el is not None:
|
||||
link = link_el.get('href', '')
|
||||
|
||||
# Description
|
||||
desc = _find_text(item, 'description') or _find_text(item, 'summary')
|
||||
if not desc:
|
||||
desc = item.findtext('{http://www.w3.org/2005/Atom}summary') or ''
|
||||
desc = _strip_html(desc)[:200]
|
||||
|
||||
# Publication date
|
||||
pub_date = (
|
||||
_find_text(item, 'pubDate')
|
||||
or _find_text(item, 'published')
|
||||
or item.findtext('{http://www.w3.org/2005/Atom}published')
|
||||
or datetime.now(timezone.utc).isoformat()
|
||||
)
|
||||
|
||||
# Image from enclosure or media:content
|
||||
image = ''
|
||||
enc = item.find('enclosure')
|
||||
if enc is not None:
|
||||
enc_url = enc.get('url', '')
|
||||
enc_type = enc.get('type', '')
|
||||
if 'image' in enc_type or enc_url.lower().endswith(('.jpg', '.jpeg', '.png', '.gif', '.webp')):
|
||||
image = enc_url
|
||||
if not image:
|
||||
media = item.find('{http://search.yahoo.com/mrss/}content')
|
||||
if media is not None:
|
||||
murl = media.get('url', '')
|
||||
if murl.lower().endswith(('.jpg', '.jpeg', '.png', '.gif', '.webp')):
|
||||
image = murl
|
||||
|
||||
# Determine Category and Severity
|
||||
title_lower = title.lower()
|
||||
category = "GEOPOLITICS"
|
||||
if any(w in title_lower for w in ["cyber", "hacking", "breach", "malware", "botnet"]):
|
||||
category = "CYBER"
|
||||
elif any(w in title_lower for w in ["satellite", "orbit", "rocket", "launch", "space", "iss"]):
|
||||
category = "SPACE"
|
||||
elif any(w in title_lower for w in ["military", "army", "navy", "airforce", "missile", "strike", "war", "conflict", "nato", "defense"]):
|
||||
category = "MILITARY"
|
||||
|
||||
severity = "MODERATE"
|
||||
if any(w in title_lower for w in ["attack", "strike", "crisis", "invasion", "nuclear", "killed"]):
|
||||
severity = "HIGH"
|
||||
if any(w in title_lower for w in ["critical", "emergency", "declaration", "imminent"]):
|
||||
severity = "CRITICAL"
|
||||
|
||||
articles.append({
|
||||
"title": title,
|
||||
"source": feed_title,
|
||||
"url": link,
|
||||
"image": image or None,
|
||||
"lat": lat,
|
||||
"lon": lon,
|
||||
"summary": desc or "No details available.",
|
||||
"published_at": pub_date,
|
||||
"category": category,
|
||||
"severity": severity
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"[NEWS] Feed error ({feed_url[:60]}): {e}")
|
||||
return articles
|
||||
|
||||
|
||||
async def fetch_news():
|
||||
"""Fetch all RSS feeds in parallel and return combined articles."""
|
||||
cached = _get_cached_news()
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(
|
||||
headers={"User-Agent": "GodsEye/2.0 RSS Reader"},
|
||||
) as client:
|
||||
results = await asyncio.gather(
|
||||
*[_fetch_single_feed(client, url) for url in FEEDS],
|
||||
return_exceptions=True,
|
||||
)
|
||||
articles: list[dict] = []
|
||||
for r in results:
|
||||
if isinstance(r, list):
|
||||
articles.extend(r)
|
||||
|
||||
print(f"[NEWS] Fetched {len(articles)} intelligence items from {len(FEEDS)} feeds.")
|
||||
_save_cache(articles)
|
||||
return articles
|
||||
except Exception as e:
|
||||
print(f"[NEWS] Critical error: {e}")
|
||||
# fallback to stale cache if error
|
||||
if CACHE_FILE.exists():
|
||||
try:
|
||||
with open(CACHE_FILE, "r") as f:
|
||||
return json.load(f)
|
||||
except: pass
|
||||
return []
|
||||
@@ -0,0 +1,208 @@
|
||||
import httpx
|
||||
import asyncio
|
||||
|
||||
ADSB_LOL_BASE = "https://api.adsb.lol/v2"
|
||||
|
||||
EMERGENCY_SQUAWKS = {
|
||||
"7700": "GENERAL EMERGENCY",
|
||||
"7600": "RADIO FAILURE",
|
||||
"7500": "HIJACK / UNLAWFUL INTERFERENCE",
|
||||
}
|
||||
|
||||
_HEADERS = {
|
||||
"User-Agent": "Mozilla/5.0 (compatible; GodsEye/3.0)",
|
||||
"Accept": "application/json",
|
||||
}
|
||||
|
||||
# Regional civilian coverage queries: (lat, lon, radius_nm, label)
|
||||
REGIONS = [
|
||||
(45.0, -95.0, 2500, "N.America"),
|
||||
(52.0, 8.0, 2000, "Europe"),
|
||||
(25.0, 60.0, 2000, "MiddleEast"),
|
||||
(35.0, 125.0, 2000, "E.Asia"),
|
||||
( 5.0, 110.0, 2000, "SE.Asia"),
|
||||
(-10.0, -40.0, 2500, "S.America+Africa"),
|
||||
]
|
||||
|
||||
|
||||
def _parse_ac(ac: dict, military: bool = False) -> dict | None:
|
||||
lat = ac.get("lat")
|
||||
lon = ac.get("lon")
|
||||
if lat is None or lon is None:
|
||||
return None
|
||||
alt_baro = ac.get("alt_baro", 0)
|
||||
if alt_baro == "ground":
|
||||
alt_m = 0
|
||||
else:
|
||||
alt_m = round(float(alt_baro) * 0.3048, 0) if isinstance(alt_baro, (int, float)) else 0
|
||||
squawk = str(ac.get("squawk", "")).strip()
|
||||
is_mil = military or bool(ac.get("mil")) or bool((ac.get("dbFlags", 0) or 0) & 1)
|
||||
return {
|
||||
"id": ac.get("hex", "UNKNOWN"),
|
||||
"callsign": str(ac.get("flight", "")).strip() or ac.get("hex", "UNKNOWN"),
|
||||
"country": ac.get("ownOp", ac.get("native", "")),
|
||||
"lon": round(lon, 4),
|
||||
"lat": round(lat, 4),
|
||||
"alt": alt_m,
|
||||
"velocity": round(float(ac.get("gs", 0) or 0) * 0.5144, 1),
|
||||
"heading": round(float(ac.get("track", 0) or 0), 1),
|
||||
"military": is_mil,
|
||||
"squawk": squawk,
|
||||
"_emergency": squawk in EMERGENCY_SQUAWKS,
|
||||
"type": "plane",
|
||||
# GPS quality fields for jamming detection
|
||||
"nac_p": ac.get("nac_p"), # Navigation Accuracy Category (0–11, ≥9=normal)
|
||||
"nic": ac.get("nic"), # Navigation Integrity Category (0=no integrity)
|
||||
"sil": ac.get("sil"), # Source Integrity Level (0–3)
|
||||
}
|
||||
|
||||
|
||||
async def _fetch_military(client: httpx.AsyncClient) -> list[dict]:
|
||||
try:
|
||||
r = await client.get(f"{ADSB_LOL_BASE}/mil", headers=_HEADERS, timeout=20.0)
|
||||
if r.status_code != 200:
|
||||
print(f"[AIRSPACE] /v2/mil returned {r.status_code}")
|
||||
return []
|
||||
ac_list = r.json().get("ac", [])
|
||||
planes = []
|
||||
for ac in ac_list:
|
||||
p = _parse_ac(ac, military=True)
|
||||
if p:
|
||||
planes.append(p)
|
||||
print(f"[AIRSPACE] Military: {len(planes)} aircraft")
|
||||
return planes
|
||||
except Exception as e:
|
||||
print(f"[AIRSPACE] Military fetch error: {e}")
|
||||
return []
|
||||
|
||||
|
||||
async def _fetch_region(client: httpx.AsyncClient, lat: float, lon: float, radius: int, label: str) -> list[dict]:
|
||||
try:
|
||||
url = f"{ADSB_LOL_BASE}/point/{lat}/{lon}/{radius}"
|
||||
r = await client.get(url, headers=_HEADERS, timeout=20.0)
|
||||
if r.status_code != 200:
|
||||
return []
|
||||
ac_list = r.json().get("ac", [])
|
||||
planes = []
|
||||
for ac in ac_list:
|
||||
p = _parse_ac(ac)
|
||||
if p:
|
||||
planes.append(p)
|
||||
return planes
|
||||
except Exception as e:
|
||||
print(f"[AIRSPACE] Region {label} error: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def _compute_gps_interference(planes: list[dict]) -> list[dict]:
|
||||
"""
|
||||
Real GPS jamming detection from ADS-B navigation accuracy fields.
|
||||
Same method as gpsjam.org: cluster aircraft with degraded NAC_P / NIC values.
|
||||
|
||||
nac_p (Navigation Accuracy Category for Position):
|
||||
≥ 9 = normal GPS (HPU < 30m)
|
||||
7 = HPU < 0.1 NM (~185m)
|
||||
≤ 4 = HPU > 0.3 NM (~555m) → GPS quality degraded, likely jamming
|
||||
0 = HPU unknown / no fix
|
||||
|
||||
nic (Navigation Integrity Category):
|
||||
0 = no integrity assurance → strong spoofing/jamming indicator
|
||||
≥ 7 = normal
|
||||
"""
|
||||
# Grid cell size in degrees
|
||||
CELL = 2.0
|
||||
MIN_ANOMALOUS = 3 # need at least 3 aircraft showing anomalies per cell
|
||||
|
||||
cell_counts: dict[tuple, dict] = {}
|
||||
|
||||
for p in planes:
|
||||
nac = p.get("nac_p")
|
||||
nic = p.get("nic")
|
||||
lat, lon = p.get("lat"), p.get("lon")
|
||||
if lat is None or lon is None:
|
||||
continue
|
||||
|
||||
# Detect GPS-degraded aircraft
|
||||
gps_degraded = (
|
||||
(nac is not None and nac <= 4) or
|
||||
(nic is not None and nic == 0 and nac is not None and nac < 9)
|
||||
)
|
||||
if not gps_degraded:
|
||||
continue
|
||||
|
||||
# Snap to grid cell
|
||||
cell = (round(lat / CELL) * CELL, round(lon / CELL) * CELL)
|
||||
if cell not in cell_counts:
|
||||
cell_counts[cell] = {"count": 0, "lats": [], "lons": [], "min_nac": 99}
|
||||
cell_counts[cell]["count"] += 1
|
||||
cell_counts[cell]["lats"].append(lat)
|
||||
cell_counts[cell]["lons"].append(lon)
|
||||
if nac is not None:
|
||||
cell_counts[cell]["min_nac"] = min(cell_counts[cell]["min_nac"], nac)
|
||||
|
||||
zones = []
|
||||
for (clat, clon), info in cell_counts.items():
|
||||
if info["count"] < MIN_ANOMALOUS:
|
||||
continue
|
||||
count = info["count"]
|
||||
min_nac = info["min_nac"]
|
||||
intensity = (
|
||||
"CRITICAL" if count >= 10 or min_nac == 0 else
|
||||
"HIGH" if count >= 5 or min_nac <= 2 else
|
||||
"ACTIVE"
|
||||
)
|
||||
# Use centroid of affected aircraft for more accurate placement
|
||||
center_lat = sum(info["lats"]) / len(info["lats"])
|
||||
center_lon = sum(info["lons"]) / len(info["lons"])
|
||||
zones.append({
|
||||
"lat": round(center_lat, 2),
|
||||
"lon": round(center_lon, 2),
|
||||
"intensity": intensity,
|
||||
"aircraft_count": count,
|
||||
"min_nac_p": min_nac if min_nac < 99 else None,
|
||||
"source": "ADS-B NAC/NIC",
|
||||
})
|
||||
|
||||
if zones:
|
||||
print(f"[GPS] {len(zones)} jamming zones detected from {sum(z['aircraft_count'] for z in zones)} anomalous aircraft")
|
||||
return zones
|
||||
|
||||
|
||||
async def fetch_planes() -> dict:
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
# Military first, then all regions in parallel
|
||||
tasks = [_fetch_military(client)] + [
|
||||
_fetch_region(client, lat, lon, r, label)
|
||||
for lat, lon, r, label in REGIONS
|
||||
]
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
mil_planes = results[0] if isinstance(results[0], list) else []
|
||||
mil_ids = {p["id"] for p in mil_planes}
|
||||
|
||||
# Merge regional civilian results, deduplicate
|
||||
seen: set[str] = set(mil_ids)
|
||||
civilian: list[dict] = []
|
||||
for region_result in results[1:]:
|
||||
if not isinstance(region_result, list):
|
||||
continue
|
||||
for p in region_result:
|
||||
if p["id"] not in seen:
|
||||
seen.add(p["id"])
|
||||
civilian.append(p)
|
||||
|
||||
emergencies = [p for p in civilian if p.pop("_emergency", False)]
|
||||
for p in mil_planes:
|
||||
p.pop("_emergency", None)
|
||||
regular = [p for p in civilian if not p.get("_emergency")]
|
||||
|
||||
final = mil_planes + emergencies + regular
|
||||
interference = _compute_gps_interference(final)
|
||||
print(f"[AIRSPACE] Synced {len(final)} aircraft ({len(mil_planes)} mil, {len(emergencies)} emergency) | {len(interference)} GPS zones")
|
||||
return {"planes": final, "interference": interference, "emergencies": emergencies}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
result = asyncio.run(fetch_planes())
|
||||
print(f"Total: {len(result['planes'])}, Military: {sum(1 for p in result['planes'] if p['military'])}")
|
||||
@@ -0,0 +1,105 @@
|
||||
from skyfield.api import load
|
||||
import asyncio
|
||||
|
||||
SATELLITE_GROUPS = {
|
||||
"military": "https://celestrak.org/NORAD/elements/gp.php?GROUP=military&FORMAT=tle",
|
||||
"stations": "https://celestrak.org/NORAD/elements/gp.php?GROUP=stations&FORMAT=tle",
|
||||
"navigation": "https://celestrak.org/NORAD/elements/gp.php?GROUP=gnss&FORMAT=tle",
|
||||
}
|
||||
|
||||
GROUP_LIMITS = {
|
||||
"military": 800,
|
||||
"stations": 50,
|
||||
"navigation": 120,
|
||||
}
|
||||
|
||||
satellites_data: dict = {}
|
||||
|
||||
|
||||
def load_satellites():
|
||||
"""
|
||||
Blocking I/O: loads TLE data from Celestrak.
|
||||
Must be called via asyncio.to_thread to avoid blocking the event loop.
|
||||
"""
|
||||
global satellites_data
|
||||
try:
|
||||
ts = load.timescale()
|
||||
loaded = {}
|
||||
for group, url in SATELLITE_GROUPS.items():
|
||||
try:
|
||||
filename = f"{group}.tle"
|
||||
sats = load.tle_file(url, filename=filename)
|
||||
loaded[group] = sats
|
||||
print(f"[SPACE] {group}: {len(sats)} satellites loaded.")
|
||||
except Exception as e:
|
||||
print(f"[SPACE] Failed to load {group}: {e}")
|
||||
satellites_data = {**loaded, "ts": ts}
|
||||
except Exception as e:
|
||||
print(f"[SPACE] Critical load failure: {e}")
|
||||
|
||||
|
||||
def _compute_positions_sync(sats: list, category: str, t, limit: int) -> list:
|
||||
"""
|
||||
CPU-bound position computation — runs in a thread executor.
|
||||
Returns dicts, does NOT mutate shared state.
|
||||
"""
|
||||
positions = []
|
||||
for sat in sats[:limit]:
|
||||
try:
|
||||
geocentric = sat.at(t)
|
||||
subpoint = geocentric.subpoint()
|
||||
lat = subpoint.latitude.degrees
|
||||
lon = subpoint.longitude.degrees
|
||||
alt = subpoint.elevation.m
|
||||
|
||||
if not (-90 <= lat <= 90) or not (-180 <= lon <= 180):
|
||||
continue
|
||||
|
||||
positions.append({
|
||||
"id": sat.model.satnum,
|
||||
"name": sat.name,
|
||||
"lat": lat,
|
||||
"lon": lon,
|
||||
"alt": alt,
|
||||
"category": category,
|
||||
"type": "satellite"
|
||||
})
|
||||
except Exception:
|
||||
pass # skip satellites with bad TLE data
|
||||
return positions
|
||||
|
||||
|
||||
async def get_satellite_positions() -> list:
|
||||
if "ts" not in satellites_data:
|
||||
# First run: load everything in a background thread
|
||||
await asyncio.to_thread(load_satellites)
|
||||
|
||||
if "ts" not in satellites_data:
|
||||
print("[SPACE] Satellite data unavailable.")
|
||||
return []
|
||||
|
||||
t = satellites_data["ts"].now()
|
||||
all_positions = []
|
||||
|
||||
for group, limit in GROUP_LIMITS.items():
|
||||
if group in satellites_data:
|
||||
result = await asyncio.to_thread(
|
||||
_compute_positions_sync,
|
||||
satellites_data[group],
|
||||
group,
|
||||
t,
|
||||
limit
|
||||
)
|
||||
all_positions.extend(result)
|
||||
|
||||
print(f"[SPACE] {len(all_positions)} satellite positions computed.")
|
||||
return all_positions
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
load_satellites()
|
||||
positions = asyncio.run(get_satellite_positions())
|
||||
print(f"Total: {len(positions)}")
|
||||
from collections import Counter
|
||||
for cat, count in Counter(p["category"] for p in positions).items():
|
||||
print(f" {cat}: {count}")
|
||||
@@ -0,0 +1,88 @@
|
||||
import httpx
|
||||
import asyncio
|
||||
|
||||
# NOAA Space Weather Prediction Center - free, no key
|
||||
NOAA_KP_URL = "https://services.swpc.noaa.gov/products/noaa-planetary-k-index.json"
|
||||
NOAA_ALERTS_URL = "https://services.swpc.noaa.gov/products/alerts.json"
|
||||
NOAA_SOLAR_URL = "https://services.swpc.noaa.gov/json/solar-cycle/observed-solar-cycle-indices.json"
|
||||
|
||||
KP_STATUS = {
|
||||
(0, 2): ("QUIET", "No significant geomagnetic activity."),
|
||||
(2, 4): ("UNSETTLED", "Minor geomagnetic activity."),
|
||||
(4, 5): ("ACTIVE", "Active geomagnetic conditions."),
|
||||
(5, 6): ("STORM-G1", "Minor geomagnetic storm (G1)."),
|
||||
(6, 7): ("STORM-G2", "Moderate geomagnetic storm (G2). HF radio disruption possible."),
|
||||
(7, 8): ("STORM-G3", "Strong geomagnetic storm (G3). Possible power grid fluctuations."),
|
||||
(8, 9): ("STORM-G4", "Severe geomagnetic storm (G4). Widespread power disruption."),
|
||||
(9, 10): ("STORM-G5", "Extreme geomagnetic storm (G5). Grid collapse risk."),
|
||||
}
|
||||
|
||||
def classify_kp(kp: float) -> tuple[str, str]:
|
||||
for (lo, hi), (status, desc) in KP_STATUS.items():
|
||||
if lo <= kp < hi:
|
||||
return status, desc
|
||||
return "STORM-G5", "Extreme geomagnetic storm."
|
||||
|
||||
|
||||
async def fetch_space_weather() -> dict:
|
||||
"""
|
||||
Fetches real space weather data from NOAA SWPC.
|
||||
- Planetary K-index (geomagnetic storm indicator)
|
||||
- Active space weather alerts
|
||||
"""
|
||||
result = {
|
||||
"kp_index": 0.0,
|
||||
"status": "UNKNOWN",
|
||||
"description": "Awaiting space weather data...",
|
||||
"alerts": [],
|
||||
"timestamp": "",
|
||||
}
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
# Fetch Kp index - returns list of [time_tag, Kp, ...]
|
||||
kp_resp = await client.get(NOAA_KP_URL, timeout=10.0)
|
||||
if kp_resp.status_code == 200:
|
||||
kp_data = kp_resp.json()
|
||||
# Skip header row [0], get latest reading from the end
|
||||
if len(kp_data) > 1:
|
||||
latest = kp_data[-1]
|
||||
raw_kp = latest[1] if latest[1] not in ("-1", None, "") else "0"
|
||||
kp = float(raw_kp)
|
||||
status, description = classify_kp(kp)
|
||||
result["kp_index"] = kp
|
||||
result["status"] = status
|
||||
result["description"] = description
|
||||
result["timestamp"] = latest[0]
|
||||
|
||||
# Fetch active alerts
|
||||
alerts_resp = await client.get(NOAA_ALERTS_URL, timeout=10.0)
|
||||
if alerts_resp.status_code == 200:
|
||||
alerts_raw = alerts_resp.json()
|
||||
# Filter for active, non-cancelled alerts
|
||||
active_alerts = []
|
||||
for alert in (alerts_raw or [])[:10]:
|
||||
msg = alert.get("message", "")
|
||||
if "CANCEL" not in msg and "SUMMARY" not in msg:
|
||||
# Extract first meaningful line as title
|
||||
lines = [l.strip() for l in msg.split("\n") if l.strip()]
|
||||
title = lines[0] if lines else "Space Weather Alert"
|
||||
active_alerts.append({
|
||||
"issue_time": alert.get("issue_datetime", ""),
|
||||
"title": title[:100],
|
||||
})
|
||||
result["alerts"] = active_alerts[:5]
|
||||
|
||||
print(f"[SPACEWEATHER] Kp={result['kp_index']} — {result['status']} | {len(result['alerts'])} active alerts")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[SPACEWEATHER] Fetch error: {e}")
|
||||
result["status"] = "OFFLINE"
|
||||
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
data = asyncio.run(fetch_space_weather())
|
||||
print(json.dumps(data, indent=2))
|
||||
@@ -0,0 +1,69 @@
|
||||
import asyncio # kept for __main__ block
|
||||
|
||||
|
||||
def _windy_embed(lat: float, lon: float) -> str:
|
||||
"""Windy's official iframe-embeddable webcam map — always works."""
|
||||
return (
|
||||
f"https://embed.windy.com/embed.html"
|
||||
f"?type=map&location=coordinates&metricRain=default&metricTemp=default"
|
||||
f"&metricWind=default&zoom=12&overlay=webcams&product=ecmwf"
|
||||
f"&level=surface&lat={lat}&lon={lon}"
|
||||
)
|
||||
|
||||
|
||||
def _windy(lat: float, lon: float) -> str:
|
||||
return f"https://www.windy.com/-Webcams/webcams?{lat},{lon},12"
|
||||
|
||||
|
||||
WEBCAM_SOURCES = [
|
||||
# North America
|
||||
{"id": "wc-nyc", "name": "Times Square, NYC", "lat": 40.7580, "lon": -73.9855},
|
||||
{"id": "wc-miami", "name": "Miami Beach, Florida", "lat": 25.7617, "lon": -80.1918},
|
||||
{"id": "wc-sf", "name": "San Francisco Bay", "lat": 37.8083, "lon": -122.4156},
|
||||
{"id": "wc-dc", "name": "Washington DC", "lat": 38.8899, "lon": -77.0091},
|
||||
# Europe
|
||||
{"id": "wc-lon", "name": "Tower Bridge, London", "lat": 51.5055, "lon": -0.0754},
|
||||
{"id": "wc-par", "name": "Eiffel Tower, Paris", "lat": 48.8584, "lon": 2.2945},
|
||||
{"id": "wc-ber", "name": "Brandenburg Gate, Berlin", "lat": 52.5163, "lon": 13.3777},
|
||||
{"id": "wc-rome", "name": "Colosseum, Rome", "lat": 41.8902, "lon": 12.4922},
|
||||
{"id": "wc-barcelona", "name": "Barcelona Beach", "lat": 41.4036, "lon": 2.1744},
|
||||
{"id": "wc-amsterdam", "name": "Dam Square, Amsterdam", "lat": 52.3731, "lon": 4.8932},
|
||||
{"id": "wc-moscow", "name": "Moscow Kremlin View", "lat": 55.7520, "lon": 37.6175},
|
||||
# Middle East & Africa
|
||||
{"id": "wc-istanbul", "name": "Bosphorus, Istanbul", "lat": 41.0422, "lon": 29.0083},
|
||||
{"id": "wc-jerusalem", "name": "Western Wall, Jerusalem", "lat": 31.7767, "lon": 35.2345},
|
||||
{"id": "wc-dxb", "name": "Dubai Marina", "lat": 25.0800, "lon": 55.1400},
|
||||
{"id": "wc-cairo", "name": "Pyramids of Giza, Cairo", "lat": 29.9792, "lon": 31.1342},
|
||||
# Asia & Pacific
|
||||
{"id": "wc-tok", "name": "Shibuya Crossing, Tokyo", "lat": 35.6595, "lon": 139.7001},
|
||||
{"id": "wc-hk", "name": "Victoria Harbour, HK", "lat": 22.2855, "lon": 114.1577},
|
||||
{"id": "wc-sin", "name": "Singapore Skyline", "lat": 1.2897, "lon": 103.8501},
|
||||
{"id": "wc-syd", "name": "Sydney Opera House", "lat": -33.8568, "lon": 151.2153},
|
||||
{"id": "wc-seoul", "name": "Seoul Skyline", "lat": 37.5665, "lon": 126.9780},
|
||||
# Strategic / military-adjacent
|
||||
{"id": "wc-gibraltar", "name": "Strait of Gibraltar", "lat": 36.1408, "lon": -5.3536},
|
||||
]
|
||||
|
||||
|
||||
async def fetch_webcams() -> list:
|
||||
webcams = [
|
||||
{
|
||||
"id": cam["id"],
|
||||
"name": cam["name"],
|
||||
"lat": cam["lat"],
|
||||
"lon": cam["lon"],
|
||||
"url": _windy(cam["lat"], cam["lon"]),
|
||||
"embed_url": _windy_embed(cam["lat"], cam["lon"]),
|
||||
"type": "webcam",
|
||||
"status": "ONLINE",
|
||||
"source": "Windy Webcams",
|
||||
}
|
||||
for cam in WEBCAM_SOURCES
|
||||
]
|
||||
print(f"[WEBCAMS] {len(webcams)} webcam locations loaded.")
|
||||
return webcams
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cams = asyncio.run(fetch_webcams())
|
||||
print(f"Total webcam locations: {len(cams)}")
|
||||
+29223
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,99 @@
|
||||
ISS (ZARYA)
|
||||
1 25544U 98067A 26065.54469341 .00009332 00000+0 18044-3 0 9991
|
||||
2 25544 51.6317 87.9051 0008117 163.3368 196.7888 15.48477653555846
|
||||
POISK
|
||||
1 36086U 09060A 26065.54469341 .00009332 00000+0 18044-3 0 9999
|
||||
2 36086 51.6317 87.9051 0008117 163.3368 196.7888 15.48477653554444
|
||||
CSS (TIANHE)
|
||||
1 48274U 21035A 26065.83578792 .00025906 00000+0 30829-3 0 9990
|
||||
2 48274 41.4663 225.5243 0006950 255.9346 104.0720 15.60495557277162
|
||||
ISS (NAUKA)
|
||||
1 49044U 21066A 26065.54469341 .00009332 00000+0 18044-3 0 9997
|
||||
2 49044 51.6317 87.9051 0008117 163.3368 196.7888 15.48477653554453
|
||||
FREGAT DEB
|
||||
1 49271U 11037PF 26064.97574117 .00028561 00000+0 41642-1 0 9993
|
||||
2 49271 51.6493 311.8987 0957104 50.8853 317.3235 12.39967329212230
|
||||
CSS (WENTIAN)
|
||||
1 53239U 22085A 26065.83578792 .00025906 00000+0 30829-3 0 9993
|
||||
2 53239 41.4663 225.5243 0006950 255.9346 104.0720 15.60495557276843
|
||||
CSS (MENGTIAN)
|
||||
1 54216U 22143A 26065.83578792 .00025906 00000+0 30829-3 0 9994
|
||||
2 54216 41.4663 225.5243 0006950 255.9346 104.0720 15.60495557276816
|
||||
PROGRESS-MS 31
|
||||
1 64751U 25146A 26065.54469341 .00009332 00000+0 18044-3 0 9992
|
||||
2 64751 51.6317 87.9051 0008117 163.3368 196.7888 15.48477653554466
|
||||
TIANZHOU-9
|
||||
1 64786U 25149A 26065.83578792 .00025906 00000+0 30829-3 0 9996
|
||||
2 64786 41.4663 225.5243 0006950 255.9346 104.0720 15.60495557276820
|
||||
PROGRESS-MS 32
|
||||
1 65586U 25204A 26065.54469341 .00009332 00000+0 18044-3 0 9994
|
||||
2 65586 51.6317 87.9051 0008117 163.3368 196.7888 15.48477653554508
|
||||
CYGNUS NG-23
|
||||
1 65616U 25208A 26065.54469341 .00009332 00000+0 18044-3 0 9992
|
||||
2 65616 51.6317 87.9051 0008117 163.3368 196.7888 15.48477653554513
|
||||
ISS OBJECT XK
|
||||
1 65731U 98067XK 26065.35324646 .01473506 15937-2 13181-2 0 9993
|
||||
2 65731 51.6138 63.9395 0004334 87.4881 272.6630 16.12476132 26369
|
||||
YOTSUBA-KULOVER
|
||||
1 65941U 98067XN 26065.22636982 .00207091 00000+0 11219-2 0 9996
|
||||
2 65941 51.6209 75.2348 0005861 46.1481 314.0003 15.78891358 22957
|
||||
E-KAGAKU-1
|
||||
1 65943U 98067XQ 26065.29341579 .00203884 00000+0 11184-2 0 9997
|
||||
2 65943 51.6235 74.9503 0006176 51.7621 308.3935 15.78621403 22968
|
||||
HRC MONOBLOCK CAMERA
|
||||
1 66052U 98067XR 26065.37348952 .00054605 00000+0 62035-3 0 9997
|
||||
2 66052 51.6273 82.2641 0003540 77.2259 282.9130 15.61775816 21872
|
||||
HTV-X1
|
||||
1 66174U 25241A 26065.54469341 .00009332 00000+0 18044-3 0 9999
|
||||
2 66174 51.6317 87.9051 0008117 163.3368 196.7888 15.48477653554478
|
||||
SZ-21 MODULE
|
||||
1 66515U 25246C 26065.50767940 .00041950 00000+0 36494-3 0 9996
|
||||
2 66515 41.4735 222.9439 0001433 292.6372 67.4317 15.68252686 17579
|
||||
SHENZHOU-22
|
||||
1 66645U 25272A 26065.83578792 .00025906 00000+0 30829-3 0 9999
|
||||
2 66645 41.4663 225.5243 0006950 255.9346 104.0720 15.60495557268759
|
||||
SOYUZ-MS 28
|
||||
1 66664U 25275A 26065.54469341 .00009332 00000+0 18044-3 0 9990
|
||||
2 66664 51.6317 87.9051 0008117 163.3368 196.7888 15.48477653554528
|
||||
DUPLEX
|
||||
1 66906U 98067XS 26065.43931271 .00032127 00000+0 45868-3 0 9994
|
||||
2 66906 51.6289 85.7046 0002177 98.7594 261.3644 15.55966204 14621
|
||||
ISS OBJECT XT
|
||||
1 66907U 98067XT 26065.27003484 .00153072 00000+0 13312-2 0 9994
|
||||
2 66907 51.6248 83.0631 0004674 56.5604 303.5839 15.68035860 14642
|
||||
ISS OBJECT XU
|
||||
1 66908U 98067XU 26065.33220126 .00152767 00000+0 13221-2 0 9995
|
||||
2 66908 51.6249 82.7244 0004715 58.9313 301.2145 15.68155385 14650
|
||||
SILVERSAT
|
||||
1 66909U 98067XV 26065.27564600 .00189452 00000+0 14661-2 0 9997
|
||||
2 66909 51.6242 82.3461 0005530 54.5981 305.5533 15.70734240 14654
|
||||
ISS OBJECT XW
|
||||
1 66910U 98067XW 26065.45484626 .00136808 00000+0 12172-2 0 9990
|
||||
2 66910 51.6239 82.0383 0005009 67.5902 292.5625 15.67532901 14675
|
||||
ISS OBJECT XX
|
||||
1 66911U 98067XX 26065.40607185 .00240114 00000+0 16386-2 0 9990
|
||||
2 66911 51.6211 80.9498 0007025 63.2110 296.9607 15.73534846 14679
|
||||
ISS OBJECT XY
|
||||
1 66912U 98067XY 26065.44936360 .00091647 00000+0 98520-3 0 9991
|
||||
2 66912 51.6268 83.5178 0003032 70.8849 289.2473 15.62981143 14640
|
||||
ISS OBJECT XZ
|
||||
1 67683U 98067XZ 26065.44303726 .00040277 00000+0 67173-3 0 9994
|
||||
2 67683 51.6308 87.9775 0009459 174.9626 185.1460 15.51523335 4282
|
||||
GXIBA-1
|
||||
1 67684U 98067YA 26065.35635560 .00076870 00000+0 11728-2 0 9991
|
||||
2 67684 51.6296 88.1497 0010932 164.4811 195.6517 15.53698977 4276
|
||||
CORAL
|
||||
1 67685U 98067YB 26065.37492343 .00044090 00000+0 72551-3 0 9995
|
||||
2 67685 51.6303 88.2731 0011331 171.0094 189.1100 15.51836923 4278
|
||||
ISS OBJECT YC
|
||||
1 67686U 98067YC 26065.37047129 .00051923 00000+0 83919-3 0 9997
|
||||
2 67686 51.6305 88.2448 0007826 138.7374 221.4209 15.52303119 4278
|
||||
LEOPARD
|
||||
1 67687U 98067YD 26065.44105420 .00044085 00000+0 73033-3 0 9992
|
||||
2 67687 51.6311 87.9659 0007779 143.5641 216.5880 15.51685674 4205
|
||||
ISS OBJECT YE
|
||||
1 67688U 98067YE 26065.37246683 .00047802 00000+0 77966-3 0 9991
|
||||
2 67688 51.6307 88.2580 0007752 139.1868 220.9703 15.52085564 4195
|
||||
CREW DRAGON 12
|
||||
1 67796U 26031A 26065.54469341 .00009332 00000+0 18044-3 0 9998
|
||||
2 67796 51.6317 87.9051 0008117 163.3368 196.7888 15.48477653555728
|
||||
Reference in New Issue
Block a user