Initial commit

This commit is contained in:
denshooter
2026-03-09 22:07:19 +01:00
commit daef092099
55 changed files with 39435 additions and 0 deletions
+325
View File
@@ -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)
+66
View File
@@ -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
+519
View File
@@ -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
+14
View File
@@ -0,0 +1,14 @@
fastapi
uvicorn
websockets
httpx
skyfield
asyncio
pydantic
pydantic-settings
pyais
requests
python-dotenv
numpy
sgp4
jplephem
+875
View File
@@ -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"})))
+177
View File
@@ -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 300700 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
+71
View File
@@ -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()))
+244
View File
@@ -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&timespan=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]}")
+124
View File
@@ -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()))
+91
View File
@@ -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']}")
+100
View File
@@ -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']}")
+268
View File
@@ -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 []
+208
View File
@@ -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 (011, ≥9=normal)
"nic": ac.get("nic"), # Navigation Integrity Category (0=no integrity)
"sil": ac.get("sil"), # Source Integrity Level (03)
}
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'])}")
+105
View File
@@ -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}")
+88
View File
@@ -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))
+69
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+99
View File
@@ -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