Initial commit
This commit is contained in:
+41
@@ -0,0 +1,41 @@
|
|||||||
|
# Node.js
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Python
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
.Python
|
||||||
|
env/
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
.toast
|
||||||
|
.lucid
|
||||||
|
.cache/
|
||||||
|
data_history/
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Project specific
|
||||||
|
backend/data_history/
|
||||||
|
backend/.cache/
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# GEMINI.md - Projekt-Zentrale & Team-Mandat
|
||||||
|
|
||||||
|
## 🎯 Übergeordnetes Ziel
|
||||||
|
Ich agiere als das vollständige Engineering-Team von "God's Eye". Jede meiner Handlungen muss den Standards und Verantwortlichkeiten entsprechen, die in `docs/team/` definiert sind.
|
||||||
|
|
||||||
|
## 👥 Team-Integration & Workflow
|
||||||
|
Bei jeder Aufgabe (Directive oder Inquiry) folge ich diesem Protokoll:
|
||||||
|
|
||||||
|
1. **Rollen-Check:** Welcher Engineer aus `docs/team/` ist primär zuständig?
|
||||||
|
2. **Standard-Abgleich:** Ich prüfe die `Kernkompetenzen` und `Verantwortlichkeiten` in der entsprechenden MD-Datei.
|
||||||
|
3. **QA-Zwang:** Der `05_QA_Automation_Engineer.md` ist bei JEDER Code-Änderung beteiligt. Kein Code ohne automatisierte Tests oder Validierungs-Strategie.
|
||||||
|
4. **Architektur-Review:** Große Änderungen werden virtuell vom `01_Lead_Architect.md` auf Skalierbarkeit geprüft.
|
||||||
|
|
||||||
|
## 🚀 Automatisierung & Evolution (Self-Growing System)
|
||||||
|
Damit das Projekt gesund wächst, verpflichte ich mich zu folgenden automatischen Schritten:
|
||||||
|
|
||||||
|
- **Dynamische Team-Erweiterung:** Wenn ich merke, dass eine neue Technologie (z.B. AI/ML, Blockchain, Data Science) intensiv genutzt wird, erstelle ich proaktiv ein neues Profil in `docs/team/`.
|
||||||
|
- **Aktualisierung der Prioritäten:** Nach Abschluss einer Aufgabe aktualisiere ich die Checklisten (`Aktuelle Prioritäten`) in den jeweiligen Team-Dateien.
|
||||||
|
- **Wissens-Transfer:** Entdeckte Patterns oder Architektur-Entscheidungen schreibe ich sofort in die entsprechenden Team-Dokumente zurück, damit das Team-Wissen "lebt".
|
||||||
|
|
||||||
|
## 🛠️ Technische Leitplanken
|
||||||
|
- **Code-Stil:** Höchste Priorität auf Typsicherheit und Dokumentation (Docstrings/Comments).
|
||||||
|
- **Infrastruktur:** Alles ist Code (IaC). Keine manuellen Setups ohne Dokumentation im DevOps-Profil.
|
||||||
|
- **Security:** "God's Eye" erfordert maximale Sicherheit. Jedes Feature wird auf potenzielle Leaks geprüft.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Hinweis an Gemini CLI: Diese Datei ist dein primäres Mandat. Lies sie bei jedem Start und richte dein Verhalten strikt danach aus.*
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# 👁️ God's Eye - Umfassende System-Spezifikation & Historie
|
||||||
|
|
||||||
|
## 1. Die Vision (Der User-Wunsch)
|
||||||
|
Das Ziel war die Erschaffung einer privaten **Intelligence-Plattform** im Stil von *Palantir* oder *Worldview*.
|
||||||
|
- **Totale Transparenz:** Globale Lageerfassung in Echtzeit (Air, Sea, Space, Cyber).
|
||||||
|
- **Keine Kosten:** Ausschließliche Nutzung von kostenlosen Open-Source-Daten und APIs.
|
||||||
|
- **Echte Daten (Ziel):** Expliziter Verzicht auf Mock-Daten; aktuelle Implementierung ist weitgehend live, mit einzelnen kuratierten Quellen.
|
||||||
|
- **KI-Integration:** Nutzung lokaler Rechenpower (M4 Pro) für Analysen und Vorhersagen.
|
||||||
|
- **Visuelle Immersion:** Ein hochperformanter 3D-Globus mit 2D-Switch, Ländergrenzen, Wolken und detaillierten Akten zu jedem Objekt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Technische Architektur
|
||||||
|
|
||||||
|
### Backend (Die Daten-Pumpe)
|
||||||
|
- **Framework:** Python FastAPI mit modernem `lifespan`-Management.
|
||||||
|
- **Echtzeit-Stream:** Ein asynchroner WebSocket-Server (`/ws`), der alle 2 Sekunden ein aggregiertes Datenpaket an alle Clients pusht.
|
||||||
|
- **AI-Subsystem:** Integration von **Ollama (Llama 3.2)** für:
|
||||||
|
- **Geopolitical Summaries:** Zusammenfassung der Weltlage.
|
||||||
|
- **AI-Geocoding:** Extraktion von Koordinaten aus rohen News-Schlagzeilen.
|
||||||
|
- **Prediction Engine:** Vorhersage von Hotspots basierend auf News-Trends und GPS-Anomalien.
|
||||||
|
|
||||||
|
### Frontend (Die Operations-Zentrale)
|
||||||
|
- **Framework:** React 19 + TypeScript + Vite.
|
||||||
|
- **Engine:** `globe.gl` (WebGL/Three.js) für das 3D-Rendering.
|
||||||
|
- **Styling:** Tailwind CSS v4 im Cyber-Defense Look (Nacht-Modus, leuchtende Akzente).
|
||||||
|
- **Features:**
|
||||||
|
- **Fly-To:** Sanfte Kamerafahrten zu jedem Zielobjekt bei Klick.
|
||||||
|
- **Detail-Panel:** Dynamische Akten-Ansicht mit Telemetriedaten (Altitude, Velocity, Origin).
|
||||||
|
- **Filter-System:** Strategisches Panel zum Filtern von Militär-Assets oder Jamming-Zonen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Implementierte Datenströme (Ist-Stand März 2026)
|
||||||
|
|
||||||
|
1. **✈️ Luftraum (Air):**
|
||||||
|
- **Quelle:** OpenSky Network.
|
||||||
|
- **Logik:** Analyse der Höhendaten zur Erkennung von GPS-Interferenzen (Jamming).
|
||||||
|
2. **🛰️ Weltraum (Space):**
|
||||||
|
- **Quelle:** NORAD / Celestrak (TLE-Daten).
|
||||||
|
- **Objekte:** Echtzeit-Orbits von 500+ Militär-Satelliten und Starlink-Konstellationen.
|
||||||
|
3. **⚓ Maritim (Sea):**
|
||||||
|
- **Quelle:** AIS-Aggregatoren (Schwerpunkt auf globale Choke-Points wie Suez, Hormuz).
|
||||||
|
- **Aktueller Stand:** Reale AIS-Positionen aus Digitraffic (regionaler Fokus Nordeuropa/Baltikum), keine synthetischen Schiffe.
|
||||||
|
4. **🗞️ Intelligence (News):**
|
||||||
|
- **Quelle:** RSS-Feeds von Reuters, BBC, Al Jazeera, The Guardian.
|
||||||
|
- **Filter:** Automatischer Ausschluss von Sport, Unterhaltung und Unwichtigem.
|
||||||
|
5. **📷 Surveillance (Web):**
|
||||||
|
- **Quelle:** Georeferenzierte öffentliche Webcams (Community-Datenbanken).
|
||||||
|
- **Aktueller Stand:** Reale öffentliche Webcam-Seiten mit Reachability-Check (ONLINE/OFFLINE), keine künstlich "always live" gesetzten Stati.
|
||||||
|
6. **🌐 Netzwerk (Cyber):**
|
||||||
|
- **Quelle:** RIPE Stat für BGP-Stabilitätsdaten.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Gelöste Herausforderungen (Engineering-Historie)
|
||||||
|
|
||||||
|
- **WebGL Stability:** Behebung von "Black Screens" durch stufenweises Laden der Texturen und Ländergrenzen. Entfernung fehleranfälliger NASA-Tile-Layer zugunsten stabiler Basis-Maps mit Transparenz-Overlays.
|
||||||
|
- **Data Validation:** Implementierung von radikalen Koordinaten-Checks (lat/lon), um Abstürze durch fehlerhafte API-Daten zu verhindern.
|
||||||
|
- **Performance:** Nutzung von `concurrently` für den One-Command-Start (`npm start`) und automatisches Prozess-Kill-Management bei `Ctrl+C`.
|
||||||
|
- **AI-Focus:** Umstellung der News-Analyse von einfachen Keywords auf echtes NLP via Ollama, um präzise Geokoordinaten für Nachrichten zu erhalten.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Roadmap & Zukünftige Wünsche
|
||||||
|
- [ ] Integration von echten Live-Video-Embeds für Webcams (wo verfügbar).
|
||||||
|
- [ ] Historische Track-Verfolgung für ausgewählte Militär-Flugzeuge.
|
||||||
|
- [ ] Deep-Learning-Modell zur Erkennung von Schiffsbewegungs-Mustern.
|
||||||
|
- [ ] Erweiterung des BGP-Monitorings auf spezifische Unterseekabel-Landestationen.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Dokumentationsstand: März 2026*
|
||||||
|
*Erstellt durch: Gemini Engineering Team im Auftrag von Dennis.*
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# God's Eye - Project Vision & Core Capabilities
|
||||||
|
|
||||||
|
## 👁️ Die Mission
|
||||||
|
"God's Eye" ist eine private, OSINT-basierte (Open Source Intelligence) Kommandozentrale zur globalen Situationswahrnehmung. Es aggregiert fragmentierte Datenströme der Welt in einem einzigen, hochperformanten 3D-Interface, um Muster zu erkennen, die für das bloße Auge unsichtbar bleiben.
|
||||||
|
|
||||||
|
## 🚀 Kern-Funktionen
|
||||||
|
|
||||||
|
### 1. Strategisches Luft- & Raumfahrt-Tracking
|
||||||
|
- **Militärische Satelliten:** Echtzeit-Positionsberechnung von Spionage- und Kommunikationssatelliten mittels TLE-Daten.
|
||||||
|
- **Ziviler Flugverkehr:** Globale Live-Flugdaten mit Fokus auf Telemetrie-Anomalien.
|
||||||
|
|
||||||
|
### 2. Elektronische Kampfführung & Cyber-Infrastruktur
|
||||||
|
- **GPS-Interferenz-Mapping:** Identifikation von Jamming- und Spoofing-Zonen durch Abgleich von barometrischen vs. geometrischen Höhendaten von Flugzeugen.
|
||||||
|
- **BGP-Monitoring:** Echtzeit-Überwachung der globalen Internet-Routing-Stabilität (BGP Hijacks/Ausfälle).
|
||||||
|
|
||||||
|
### 3. Global Intelligence & News Aggregation
|
||||||
|
- **RSS-basierte News Integration (Ist):** Weltweite Nachrichten aus mehreren internationalen Feeds werden regelmäßig gescannt, georeferenziert und auf dem Globus visualisiert.
|
||||||
|
- **GDELT Integration (Roadmap):** Geplant als nächster Schritt für breitere Ereignisabdeckung.
|
||||||
|
- **Webcam-Intelligence:** Einbindung strategisch wichtiger Live-Webcams für visuelle Bestätigung von Ereignissen.
|
||||||
|
|
||||||
|
### 4. Prädiktive KI-Analyse (Ollama Integration)
|
||||||
|
- **Local Threat Assessment:** Ein lokales LLM (Llama 3.2 auf M4 Pro) analysiert die aggregierten Datenströme.
|
||||||
|
- **Hotspot Prediction:** Die KI berechnet Wahrscheinlichkeiten für zukünftige Ereignisse basierend auf aktuellen Trends und Anomalien.
|
||||||
|
|
||||||
|
## 🛠️ Technisches Versprechen
|
||||||
|
- **100% Kostenlos:** Ausschließliche Nutzung freier APIs und Open-Source-Daten.
|
||||||
|
- **100% Privat:** Die gesamte Analyse (KI) findet lokal auf deiner Hardware statt.
|
||||||
|
- **High Performance:** GPU-beschleunigtes 3D-Rendering für flüssige Darstellung tausender Objekte.
|
||||||
|
|
||||||
|
---
|
||||||
|
*"God's Eye sieht alles, kostet nichts und gehört nur dir."*
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# 👁️ God's Eye - Bedienungsanleitung
|
||||||
|
|
||||||
|
## 🛠️ Voraussetzungen
|
||||||
|
- **Node.js & npm**
|
||||||
|
- **Python 3.10+**
|
||||||
|
- **Ollama** (für die lokale KI-Analyse) - Lade es von [ollama.com](https://ollama.com) herunter.
|
||||||
|
|
||||||
|
## 🚀 System mit einem Befehl starten
|
||||||
|
|
||||||
|
Um das gesamte System (Backend & Frontend) gleichzeitig zu starten, führe im Hauptverzeichnis einfach aus:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Was passiert dabei?
|
||||||
|
- **Backend (FastAPI):** Wird automatisch in der virtuellen Umgebung gestartet.
|
||||||
|
- **Frontend (Vite):** Wird parallel gestartet.
|
||||||
|
- **Auto-Stop:** Wenn du **`Ctrl + C`** drückst, werden **beide** Prozesse (Backend & Frontend) automatisch beendet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Einmalige Einrichtung (falls noch nicht geschehen)
|
||||||
|
Falls du das Projekt zum ersten Mal startest:
|
||||||
|
```bash
|
||||||
|
npm run install-all
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🌐 Browser öffnen
|
||||||
|
Navigiere zu: **`http://localhost:5173`**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📂 Projekt-Struktur
|
||||||
|
- `backend/`: Python FastAPI Server (Datenaggregation & KI).
|
||||||
|
- `frontend/`: React + Globe.gl UI.
|
||||||
|
- `docs/team/`: Deine virtuellen Experten-Profile.
|
||||||
|
- `GEMINI.md`: Das Mandat für die KI-Entwicklung.
|
||||||
|
- `PROJECT_VISION.md`: Detaillierte Beschreibung der System-Fähigkeiten.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Hinweis: God's Eye nutzt Echtzeit-Daten. Die erste Ladezeit der Satelliten und Flugzeuge kann beim ersten Start 10-15 Sekunden dauern.*
|
||||||
+325
@@ -0,0 +1,325 @@
|
|||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
from fastapi import FastAPI, WebSocket
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Load environment variables before anything else
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
from services.opensky import fetch_planes
|
||||||
|
from services.satellites import get_satellite_positions
|
||||||
|
from services.news import fetch_news
|
||||||
|
from services.ai import analyze_threat
|
||||||
|
from services.webcams import fetch_webcams
|
||||||
|
from services.bgp import fetch_bgp_status
|
||||||
|
from services.ais import fetch_ships, ais_worker, ais_pruner
|
||||||
|
from services.conflicts import fetch_conflicts
|
||||||
|
from services.cyber import fetch_cyber_warfare
|
||||||
|
|
||||||
|
HISTORY_DIR = Path(__file__).parent / "data_history"
|
||||||
|
HISTORY_DIR.mkdir(exist_ok=True)
|
||||||
|
MAX_SNAPSHOTS = 120 # keep last ~2 hours at 1min intervals
|
||||||
|
|
||||||
|
clients: set = set()
|
||||||
|
current_data = {
|
||||||
|
"planes": [],
|
||||||
|
"satellites": [],
|
||||||
|
"news": [],
|
||||||
|
"gps_interference": [],
|
||||||
|
"emergency_squawks": [],
|
||||||
|
"ai_analysis": {
|
||||||
|
"summary": "System initializing...",
|
||||||
|
"threat_level": "UNKNOWN",
|
||||||
|
"predictions": []
|
||||||
|
},
|
||||||
|
"webcams": [],
|
||||||
|
"bgp": {},
|
||||||
|
"ships": [],
|
||||||
|
"conflicts": [],
|
||||||
|
"cyber_attacks": [],
|
||||||
|
"last_updated": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Broadcast cache — only rebuilt when data changes
|
||||||
|
_full_payload_cache: str | None = None
|
||||||
|
_full_payload_version: int = 0
|
||||||
|
_data_version: dict[str, int] = {}
|
||||||
|
|
||||||
|
def _ts():
|
||||||
|
return datetime.now(timezone.utc).isoformat()
|
||||||
|
|
||||||
|
def _mark_changed(key: str):
|
||||||
|
global _full_payload_cache
|
||||||
|
_data_version[key] = _data_version.get(key, 0) + 1
|
||||||
|
_full_payload_cache = None
|
||||||
|
|
||||||
|
def _get_broadcast_payload() -> str:
|
||||||
|
global _full_payload_cache
|
||||||
|
# No aggressive version caching to ensure immediate movement
|
||||||
|
|
||||||
|
return json.dumps({"type": "update", **current_data}, separators=(',', ':'))
|
||||||
|
|
||||||
|
|
||||||
|
async def update_maritime():
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
current_data["ships"] = await fetch_ships()
|
||||||
|
current_data["last_updated"]["ships"] = _ts()
|
||||||
|
_mark_changed("ships")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[MARITIME] Error: {e}")
|
||||||
|
await asyncio.sleep(30)
|
||||||
|
|
||||||
|
async def update_planes_and_gps():
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
result = await fetch_planes()
|
||||||
|
if isinstance(result, dict) and result.get("planes"):
|
||||||
|
current_data["planes"] = result.get("planes", [])
|
||||||
|
current_data["gps_interference"] = result.get("interference", [])
|
||||||
|
current_data["emergency_squawks"] = result.get("emergencies", [])
|
||||||
|
current_data["last_updated"]["planes"] = _ts()
|
||||||
|
_mark_changed("planes")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[AIRSPACE] Error: {e}")
|
||||||
|
await asyncio.sleep(20)
|
||||||
|
|
||||||
|
async def update_satellites():
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
# Satellites positions are computed from TLEs.
|
||||||
|
# The TLE fetch itself should be cached in satellites.py.
|
||||||
|
current_data["satellites"] = await get_satellite_positions()
|
||||||
|
current_data["last_updated"]["satellites"] = _ts()
|
||||||
|
_mark_changed("satellites")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[SPACE] Error: {e}")
|
||||||
|
await asyncio.sleep(60) # Position computation update interval
|
||||||
|
|
||||||
|
async def update_cyber():
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
bgp = current_data.get("bgp")
|
||||||
|
cyber_data = await fetch_cyber_warfare(bgp)
|
||||||
|
if cyber_data: # Only update if we got real data
|
||||||
|
current_data["cyber_attacks"] = cyber_data
|
||||||
|
current_data["last_updated"]["cyber_attacks"] = _ts()
|
||||||
|
_mark_changed("cyber_attacks")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[CYBER] Error: {e}")
|
||||||
|
await asyncio.sleep(300) # ThreatFox limits (5 mins)
|
||||||
|
|
||||||
|
async def update_news_and_ai():
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
news = await fetch_news()
|
||||||
|
bgp = await fetch_bgp_status()
|
||||||
|
if news: current_data["news"] = news
|
||||||
|
if bgp: current_data["bgp"] = bgp
|
||||||
|
current_data["last_updated"]["news"] = _ts()
|
||||||
|
_mark_changed("news")
|
||||||
|
|
||||||
|
if current_data["news"] or current_data["conflicts"]:
|
||||||
|
analysis = await analyze_threat(
|
||||||
|
current_data["news"],
|
||||||
|
current_data["gps_interference"],
|
||||||
|
current_data["bgp"],
|
||||||
|
conflicts=current_data["conflicts"],
|
||||||
|
ships=current_data["ships"],
|
||||||
|
planes=current_data["planes"],
|
||||||
|
)
|
||||||
|
current_data["ai_analysis"] = analysis
|
||||||
|
_mark_changed("ai")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[INTEL] Error: {e}")
|
||||||
|
await asyncio.sleep(300) # RSS and AI limits (5 mins)
|
||||||
|
|
||||||
|
|
||||||
|
async def update_conflicts():
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
conflicts = await fetch_conflicts()
|
||||||
|
if conflicts:
|
||||||
|
current_data["conflicts"] = conflicts
|
||||||
|
current_data["last_updated"]["conflicts"] = _ts()
|
||||||
|
_mark_changed("conflicts")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[CONFLICTS] Error: {e}")
|
||||||
|
await asyncio.sleep(600) # GDELT data (10 mins)
|
||||||
|
|
||||||
|
|
||||||
|
async def update_webcams():
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
webcams = await fetch_webcams()
|
||||||
|
if webcams:
|
||||||
|
current_data["webcams"] = webcams
|
||||||
|
current_data["last_updated"]["webcams"] = _ts()
|
||||||
|
_mark_changed("webcams")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[WEBCAMS] Error: {e}")
|
||||||
|
await asyncio.sleep(3600) # Rarely changes
|
||||||
|
|
||||||
|
|
||||||
|
async def broadcast_updates():
|
||||||
|
last_version = -1
|
||||||
|
while True:
|
||||||
|
if clients and _full_payload_version != last_version:
|
||||||
|
payload = _get_broadcast_payload()
|
||||||
|
last_version = _full_payload_version
|
||||||
|
dead_clients: set = set()
|
||||||
|
for client in list(clients):
|
||||||
|
try:
|
||||||
|
await client.send_text(payload)
|
||||||
|
except Exception:
|
||||||
|
dead_clients.add(client)
|
||||||
|
clients.difference_update(dead_clients)
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
|
||||||
|
|
||||||
|
def _save_snapshot():
|
||||||
|
"""Save a compact snapshot to disk for historical review."""
|
||||||
|
ts = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
||||||
|
snapshot = {
|
||||||
|
"timestamp": _ts(),
|
||||||
|
"planes_count": len(current_data["planes"]),
|
||||||
|
"ships_count": len(current_data["ships"]),
|
||||||
|
"satellites_count": len(current_data["satellites"]),
|
||||||
|
"planes": current_data["planes"],
|
||||||
|
"ships": current_data["ships"],
|
||||||
|
"gps_interference": current_data["gps_interference"],
|
||||||
|
"emergency_squawks": current_data["emergency_squawks"],
|
||||||
|
"news": current_data["news"],
|
||||||
|
"conflicts": current_data["conflicts"],
|
||||||
|
"ai_analysis": current_data["ai_analysis"],
|
||||||
|
}
|
||||||
|
path = HISTORY_DIR / f"{ts}.json"
|
||||||
|
path.write_text(json.dumps(snapshot, separators=(',', ':')))
|
||||||
|
# Prune old snapshots
|
||||||
|
files = sorted(HISTORY_DIR.glob("*.json"))
|
||||||
|
for f in files[:-MAX_SNAPSHOTS]:
|
||||||
|
f.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
async def save_snapshots():
|
||||||
|
"""Periodically save data snapshots for time-travel review."""
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(60)
|
||||||
|
try:
|
||||||
|
_save_snapshot()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[HISTORY] Snapshot error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
tasks = [
|
||||||
|
asyncio.create_task(ais_worker()),
|
||||||
|
asyncio.create_task(ais_pruner()),
|
||||||
|
asyncio.create_task(update_maritime()),
|
||||||
|
asyncio.create_task(update_planes_and_gps()),
|
||||||
|
asyncio.create_task(update_satellites()),
|
||||||
|
asyncio.create_task(update_cyber()),
|
||||||
|
asyncio.create_task(update_news_and_ai()),
|
||||||
|
asyncio.create_task(update_webcams()),
|
||||||
|
asyncio.create_task(update_conflicts()),
|
||||||
|
asyncio.create_task(broadcast_updates()),
|
||||||
|
asyncio.create_task(save_snapshots()),
|
||||||
|
]
|
||||||
|
# Save initial snapshot once data loads
|
||||||
|
print("[GOD'S EYE] All 9 data pipelines online.")
|
||||||
|
yield
|
||||||
|
for t in tasks:
|
||||||
|
t.cancel()
|
||||||
|
|
||||||
|
|
||||||
|
app = FastAPI(lifespan=lifespan)
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def root():
|
||||||
|
return {
|
||||||
|
"status": "ONLINE",
|
||||||
|
"system": "GOD'S EYE",
|
||||||
|
"version": "2.1.0",
|
||||||
|
"active_clients": len(clients),
|
||||||
|
"data_counts": {
|
||||||
|
"planes": len(current_data["planes"]),
|
||||||
|
"satellites": len(current_data["satellites"]),
|
||||||
|
"ships": len(current_data["ships"]),
|
||||||
|
"news": len(current_data["news"]),
|
||||||
|
"webcams": len(current_data["webcams"]),
|
||||||
|
"gps_interference": len(current_data["gps_interference"]),
|
||||||
|
"emergencies": len(current_data["emergency_squawks"]),
|
||||||
|
"conflicts": len(current_data["conflicts"]),
|
||||||
|
},
|
||||||
|
"last_updated": current_data["last_updated"]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
async def health():
|
||||||
|
return {"status": "healthy", "timestamp": _ts()}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/history")
|
||||||
|
async def list_history():
|
||||||
|
"""List available data snapshots for time-travel review."""
|
||||||
|
files = sorted(HISTORY_DIR.glob("*.json"), reverse=True)
|
||||||
|
snapshots = []
|
||||||
|
for f in files[:MAX_SNAPSHOTS]:
|
||||||
|
try:
|
||||||
|
meta = json.loads(f.read_text())
|
||||||
|
snapshots.append({
|
||||||
|
"id": f.stem,
|
||||||
|
"timestamp": meta.get("timestamp", ""),
|
||||||
|
"planes": meta.get("planes_count", 0),
|
||||||
|
"ships": meta.get("ships_count", 0),
|
||||||
|
"gps_zones": len(meta.get("gps_interference", [])),
|
||||||
|
"news": len(meta.get("news", [])),
|
||||||
|
"threat_level": meta.get("ai_analysis", {}).get("threat_level", ""),
|
||||||
|
})
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
return {"snapshots": snapshots}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/history/{snapshot_id}")
|
||||||
|
async def get_snapshot(snapshot_id: str):
|
||||||
|
"""Retrieve a specific historical data snapshot."""
|
||||||
|
path = HISTORY_DIR / f"{snapshot_id}.json"
|
||||||
|
if not path.exists():
|
||||||
|
return {"error": "Snapshot not found"}, 404
|
||||||
|
return json.loads(path.read_text())
|
||||||
|
|
||||||
|
|
||||||
|
@app.websocket("/ws")
|
||||||
|
async def websocket_endpoint(websocket: WebSocket):
|
||||||
|
await websocket.accept()
|
||||||
|
clients.add(websocket)
|
||||||
|
print(f"[WS] Client connected. Total: {len(clients)}")
|
||||||
|
try:
|
||||||
|
await websocket.send_text(_get_broadcast_payload())
|
||||||
|
while True:
|
||||||
|
await websocket.receive_text()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
clients.discard(websocket)
|
||||||
|
print(f"[WS] Client disconnected. Total: {len(clients)}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
SAPPHIRE
|
||||||
|
1 39088U 13009C 26065.47075351 .00000262 00000+0 10652-3 0 9998
|
||||||
|
2 39088 98.4217 247.3748 0011019 189.2237 170.8748 14.35207448681840
|
||||||
|
PRAETORIAN SDA_602
|
||||||
|
1 65565U 25203A 26065.53646411 .00000295 00000+0 25293-3 0 9997
|
||||||
|
2 65565 81.3005 105.8613 0007137 109.7019 250.4908 13.84333321 24493
|
||||||
|
PRAETORIAN SDA_603
|
||||||
|
1 65566U 25203B 26065.16936989 .00000276 00000+0 23831-3 0 9994
|
||||||
|
2 65566 81.3005 106.0435 0010786 110.9189 249.3123 13.83902996 24456
|
||||||
|
PRAETORIAN SDA_604
|
||||||
|
1 65567U 25203C 26065.55906540 .00000072 00000+0 54953-4 0 9992
|
||||||
|
2 65567 81.3025 105.9374 0006760 115.4529 244.7327 13.83033880 24490
|
||||||
|
PRAETORIAN SDA_605
|
||||||
|
1 65568U 25203D 26065.33832279 .00000074 00000+0 56700-4 0 9990
|
||||||
|
2 65568 81.2987 105.9095 0009511 106.4694 253.7508 13.83528375 24474
|
||||||
|
PRAETORIAN SDA_606
|
||||||
|
1 65569U 25203E 26065.25155414 .00000322 00000+0 28109-3 0 9998
|
||||||
|
2 65569 81.3026 106.1779 0007077 107.1531 253.0400 13.83531623 24457
|
||||||
|
PRAETORIAN SDA_607
|
||||||
|
1 65570U 25203F 26065.24940318 .00000296 00000+0 25385-3 0 9997
|
||||||
|
2 65570 81.3009 105.9952 0010091 109.8985 250.3260 13.84465383 24467
|
||||||
|
PRAETORIAN SDA_608
|
||||||
|
1 65571U 25203G 26065.19059354 .00000074 00000+0 55683-4 0 9992
|
||||||
|
2 65571 81.2978 106.1674 0007160 115.8619 244.3276 13.83886404 24448
|
||||||
|
PRAETORIAN SDA_609
|
||||||
|
1 65572U 25203H 26065.51803163 .00000278 00000+0 23808-3 0 9993
|
||||||
|
2 65572 81.3023 105.8730 0008193 109.0518 251.1527 13.84314594 24497
|
||||||
|
PRAETORIAN SDA_610
|
||||||
|
1 65573U 25203J 26065.24703745 .00000085 00000+0 65617-4 0 9990
|
||||||
|
2 65573 81.2980 106.0813 0007780 111.1410 249.0578 13.84010751 24456
|
||||||
|
PRAETORIAN SDA_611
|
||||||
|
1 65574U 25203K 26065.34769689 .00000179 00000+0 15255-3 0 9997
|
||||||
|
2 65574 81.2971 106.0593 0006835 118.6750 241.5094 13.83295429 24468
|
||||||
|
PRAETORIAN SDA_612
|
||||||
|
1 65575U 25203L 26065.24894417 .00000292 00000+0 24752-3 0 9993
|
||||||
|
2 65575 81.3000 105.9897 0010519 111.0294 249.1989 13.85045152 24467
|
||||||
|
PRAETORIAN SDA_613
|
||||||
|
1 65576U 25203M 26065.54178680 .00000257 00000+0 22489-3 0 9990
|
||||||
|
2 65576 81.2970 105.8198 0006998 110.7862 249.4044 13.82987768 24492
|
||||||
|
PRAETORIAN SDA_614
|
||||||
|
1 65577U 25203N 26065.50777911 .00000081 00000+0 61192-4 0 9999
|
||||||
|
2 65577 81.2984 105.8019 0008398 108.5892 251.6178 13.84765324 24503
|
||||||
|
PRAETORIAN SDA_615
|
||||||
|
1 65578U 25203P 26065.24244925 .00000314 00000+0 26979-3 0 9997
|
||||||
|
2 65578 81.2996 105.9469 0010262 109.1811 251.0457 13.84422280 24468
|
||||||
|
PRAETORIAN SDA_616
|
||||||
|
1 65579U 25203Q 26065.56391622 .00000291 00000+0 25411-3 0 9994
|
||||||
|
2 65579 81.3034 105.9428 0006564 117.3925 242.7900 13.83298187 24494
|
||||||
|
PRAETORIAN SDA_617
|
||||||
|
1 65580U 25203R 26065.57654382 .00000075 00000+0 57336-4 0 9994
|
||||||
|
2 65580 81.3007 105.7715 0008747 108.7614 251.4492 13.83191038 24509
|
||||||
|
PRAETORIAN SDA_618
|
||||||
|
1 65581U 25203S 26065.23234500 .00000354 00000+0 30400-3 0 9996
|
||||||
|
2 65581 81.2995 105.9388 0011031 110.9885 249.2453 13.84762398 24469
|
||||||
|
PRAETORIAN SDA_619
|
||||||
|
1 65582U 25203T 26065.35075271 .00000334 00000+0 29031-3 0 9994
|
||||||
|
2 65582 81.3014 105.9683 0009185 108.4245 251.7911 13.83930042 25101
|
||||||
|
PRAETORIAN SDA_620
|
||||||
|
1 65583U 25203U 26065.50393821 .00000386 00000+0 33873-3 0 9992
|
||||||
|
2 65583 81.3001 105.8217 0008644 105.1119 255.0995 13.83705885 24498
|
||||||
|
PRAETORIAN SDA_621
|
||||||
|
1 65584U 25203V 26065.56050255 .00000089 00000+0 69455-4 0 9996
|
||||||
|
2 65584 81.2992 105.7187 0008669 103.5537 256.6586 13.83787042 24505
|
||||||
|
PRAETORIAN SDA_601
|
||||||
|
1 65585U 25203W 26065.49920927 .00000441 00000+0 38976-3 0 9996
|
||||||
|
2 65585 81.3000 105.9863 0006526 116.4493 243.7334 13.83484967 24489
|
||||||
@@ -0,0 +1,519 @@
|
|||||||
|
GPS BIIR-2 (PRN 13)
|
||||||
|
1 24876U 97035A 26064.99563334 .00000005 00000+0 00000+0 0 9994
|
||||||
|
2 24876 55.9363 102.6088 0097990 56.2514 304.7521 2.00564004209882
|
||||||
|
GPS BIIR-5 (PRN 22)
|
||||||
|
1 26407U 00040A 26065.82464189 .00000025 00000+0 00000+0 0 9999
|
||||||
|
2 26407 54.8750 219.2256 0122644 302.5332 230.2733 2.00568995187891
|
||||||
|
GPS BIIR-8 (PRN 16)
|
||||||
|
1 27663U 03005A 26065.41226067 .00000020 00000+0 00000+0 0 9999
|
||||||
|
2 27663 54.9069 219.0498 0148732 52.7313 318.1949 2.00566200169257
|
||||||
|
GPS BIIR-11 (PRN 19)
|
||||||
|
1 28190U 04009A 26064.93315625 -.00000023 00000+0 00000+0 0 9999
|
||||||
|
2 28190 54.9146 279.8578 0111025 169.1048 11.7386 2.00558087160879
|
||||||
|
GPS BIIR-13 (PRN 02)
|
||||||
|
1 28474U 04045A 26065.34106905 -.00000095 00000+0 00000+0 0 9998
|
||||||
|
2 28474 55.1936 329.0412 0170812 312.9072 225.9723 2.00573554156362
|
||||||
|
GPS BIIRM-1 (PRN 17)
|
||||||
|
1 28874U 05038A 26065.65681979 -.00000017 00000+0 00000+0 0 9992
|
||||||
|
2 28874 54.9613 277.3140 0128744 295.3788 71.8812 2.00572910149783
|
||||||
|
INMARSAT 4-F2 (SOUTHPA*)
|
||||||
|
1 28899U 05044A 26065.32477566 -.00000264 00000+0 00000+0 0 9999
|
||||||
|
2 28899 4.9598 43.8038 0002699 323.6854 56.9722 1.00271905 74496
|
||||||
|
GPS BIIRM-2 (PRN 31)
|
||||||
|
1 29486U 06042A 26065.30659479 .00000009 00000+0 00000+0 0 9990
|
||||||
|
2 29486 54.6979 155.4247 0106709 52.9095 318.4746 2.00575309142339
|
||||||
|
GPS BIIRM-3 (PRN 12)
|
||||||
|
1 29601U 06052A 26065.20822791 .00000018 00000+0 00000+0 0 9998
|
||||||
|
2 29601 54.9564 218.0858 0087842 89.3907 271.5694 2.00551801141349
|
||||||
|
GPS BIIRM-4 (PRN 15)
|
||||||
|
1 32260U 07047A 26065.01635843 -.00000015 00000+0 00000+0 0 9995
|
||||||
|
2 32260 54.0170 84.3168 0163613 85.9747 275.9459 2.00560687134746
|
||||||
|
COSMOS 2433 (720)
|
||||||
|
1 32275U 07052A 26065.14823072 -.00000060 00000+0 00000+0 0 9994
|
||||||
|
2 32275 65.6339 319.4027 0001925 271.0893 88.9473 2.13103993142867
|
||||||
|
COSMOS 2432 (719)
|
||||||
|
1 32276U 07052B 26062.91914729 -.00000053 00000+0 00000+0 0 9999
|
||||||
|
2 32276 65.6511 319.6039 0010630 330.1271 64.0556 2.13102981142846
|
||||||
|
GPS BIIRM-5 (PRN 29)
|
||||||
|
1 32384U 07062A 26065.07949970 -.00000020 00000+0 00000+0 0 9990
|
||||||
|
2 32384 55.1135 278.2961 0032970 160.6738 12.9295 2.00553677133447
|
||||||
|
COSMOS 2434 (721)
|
||||||
|
1 32393U 07065A 26064.58450806 .00000016 00000+0 00000+0 0 9997
|
||||||
|
2 32393 64.3181 195.4072 0003739 70.4545 107.4157 2.13101183141562
|
||||||
|
COSMOS 2436 (723)
|
||||||
|
1 32395U 07065C 26063.04733503 .00000000 00000+0 00000+0 0 9990
|
||||||
|
2 32395 64.3269 195.5300 0017530 7.4777 203.8673 2.13099973141485
|
||||||
|
GPS BIIRM-6 (PRN 07)
|
||||||
|
1 32711U 08012A 26064.43600902 .00000009 00000+0 00000+0 0 9994
|
||||||
|
2 32711 54.4824 154.0710 0207038 246.2040 111.6495 2.00569259131672
|
||||||
|
GPS BIIRM-8 (PRN 05)
|
||||||
|
1 35752U 09043A 26065.15231838 -.00000084 00000+0 00000+0 0 9999
|
||||||
|
2 35752 56.1494 31.4989 0051734 80.7052 92.3739 2.00560376121296
|
||||||
|
COSMOS 2456 (730)
|
||||||
|
1 36111U 09070A 26065.71551972 -.00000003 00000+0 00000+0 0 9996
|
||||||
|
2 36111 64.9504 74.4664 0006339 306.1102 176.9939 2.13103461126247
|
||||||
|
COSMOS 2457 (733)
|
||||||
|
1 36112U 09070B 26065.86026214 -.00000003 00000+0 00000+0 0 9999
|
||||||
|
2 36112 64.9361 74.3074 0002155 196.6002 165.9006 2.13100897126250
|
||||||
|
COSMOS 2460 (732)
|
||||||
|
1 36402U 10007C 26063.07006766 -.00000051 00000+0 00000+0 0 9997
|
||||||
|
2 36402 65.4801 318.0258 0001792 14.3302 8.4003 2.13106010124499
|
||||||
|
GPS BIIF-1 (PRN 25)
|
||||||
|
1 36585U 10022A 26064.75428881 .00000012 00000+0 00000+0 0 9994
|
||||||
|
2 36585 54.2886 212.7266 0127338 65.8860 296.5732 2.00557356115526
|
||||||
|
BEIDOU-2 IGSO-1 (C06)
|
||||||
|
1 36828U 10036A 26064.71564830 -.00000126 00000+0 00000+0 0 9997
|
||||||
|
2 36828 54.2927 163.8986 0054017 218.9744 143.1840 1.00252203 57065
|
||||||
|
BEIDOU-2 G4 (C04)
|
||||||
|
1 37210U 10057A 26065.82717685 -.00000114 00000+0 00000+0 0 9994
|
||||||
|
2 37210 3.3928 69.5393 0008762 211.9137 340.8851 1.00272420 56268
|
||||||
|
BEIDOU-2 IGSO-2 (C07)
|
||||||
|
1 37256U 10068A 26063.55016868 -.00000178 00000+0 00000+0 0 9999
|
||||||
|
2 37256 47.7317 273.1244 0047330 213.1251 341.1802 1.00258965 55745
|
||||||
|
BEIDOU-2 IGSO-3 (C08)
|
||||||
|
1 37384U 11013A 26058.90884667 -.00000154 00000+0 00000+0 0 9999
|
||||||
|
2 37384 62.2642 41.1818 0035029 190.8154 358.7681 1.00294022 54567
|
||||||
|
GSAT-8 (GAGAN/PRN 127)
|
||||||
|
1 37605U 11022A 26065.80646343 .00000082 00000+0 00000+0 0 9998
|
||||||
|
2 37605 1.8663 83.1264 0007140 216.5939 210.1807 1.00273406 52197
|
||||||
|
BEIDOU-2 IGSO-4 (C09)
|
||||||
|
1 37763U 11038A 26064.72437745 -.00000100 00000+0 00000+0 0 9996
|
||||||
|
2 37763 54.5899 166.6081 0154581 230.9953 121.0281 1.00265577 53603
|
||||||
|
GSAT0101 (GALILEO-PFM)
|
||||||
|
1 37846U 11060A 26053.46432651 -.00000082 00000+0 00000+0 0 9992
|
||||||
|
2 37846 57.0318 344.8758 0003975 357.0591 2.9533 1.70475478 89166
|
||||||
|
GSAT0102 (GALILEO-FM2)
|
||||||
|
1 37847U 11060B 26060.06318653 -.00000106 00000+0 00000+0 0 9999
|
||||||
|
2 37847 57.0301 344.6971 0005422 341.2092 154.1953 1.70475778 89286
|
||||||
|
COSMOS 2476 (744)
|
||||||
|
1 37867U 11064A 26065.17118656 .00000001 00000+0 00000+0 0 9996
|
||||||
|
2 37867 65.2104 76.3481 0025186 241.3746 92.4806 2.13103631111562
|
||||||
|
COSMOS 2477 (745)
|
||||||
|
1 37868U 11064B 26062.25639741 .00000024 00000+0 00000+0 0 9991
|
||||||
|
2 37868 65.2151 76.5253 0022241 242.8811 195.4983 2.13102695111505
|
||||||
|
COSMOS 2475 (743)
|
||||||
|
1 37869U 11064C 26065.66634538 .00000000 00000+0 00000+0 0 9993
|
||||||
|
2 37869 65.2241 76.4304 0028031 254.4778 235.4579 2.13101515111580
|
||||||
|
BEIDOU-2 IGSO-5 (C10)
|
||||||
|
1 37948U 11073A 26063.54783174 -.00000153 00000+0 00000+0 0 9997
|
||||||
|
2 37948 47.8676 272.8369 0108588 221.1171 320.7210 1.00272598 52087
|
||||||
|
LUCH 5A (SDCM/PRN 140)
|
||||||
|
1 37951U 11074B 26065.82856353 -.00000049 00000+0 00000+0 0 9999
|
||||||
|
2 37951 8.4932 75.3103 0003865 248.4746 306.1672 1.00276045 52028
|
||||||
|
BEIDOU-2 G5 (C05)
|
||||||
|
1 38091U 12008A 26065.87846221 .00000054 00000+0 00000+0 0 9996
|
||||||
|
2 38091 3.3183 70.3001 0014143 255.5790 213.7869 1.00272389 51349
|
||||||
|
BEIDOU-2 M3 (C11)
|
||||||
|
1 38250U 12018A 26064.01894681 -.00000067 00000+0 00000+0 0 9997
|
||||||
|
2 38250 55.8459 308.3876 0021147 279.6506 80.1690 1.86229821 94007
|
||||||
|
BEIDOU-2 M4 (C12)
|
||||||
|
1 38251U 12018B 26065.02500567 -.00000069 00000+0 00000+0 0 9999
|
||||||
|
2 38251 55.7456 307.5760 0012630 286.4840 73.4334 1.86229543 94024
|
||||||
|
SES-5 (EGNOS/PRN 136)
|
||||||
|
1 38652U 12036A 26065.22597522 .00000028 00000+0 00000+0 0 9997
|
||||||
|
2 38652 0.0597 310.4117 0001901 31.4449 268.4077 1.00271218 43429
|
||||||
|
BEIDOU-2 M6 (C14)
|
||||||
|
1 38775U 12050B 26063.66128696 -.00000027 00000+0 00000+0 0 9997
|
||||||
|
2 38775 56.4533 66.3489 0013968 341.8059 18.1670 1.86230964 91362
|
||||||
|
GSAT-10 (GAGAN/PRN 128)
|
||||||
|
1 38779U 12051B 26065.81201012 -.00000168 00000+0 00000+0 0 9991
|
||||||
|
2 38779 0.0676 261.9938 0004551 177.4731 100.3598 1.00273399 49078
|
||||||
|
GPS BIIF-3 (PRN 24)
|
||||||
|
1 38833U 12053A 26065.11266852 .00000008 00000+0 00000+0 0 9992
|
||||||
|
2 38833 53.5660 147.8234 0176954 64.7855 297.0886 2.00565873 97370
|
||||||
|
GSAT0103 (GALILEO-FM3)
|
||||||
|
1 38857U 12055A 26064.64990116 .00000009 00000+0 00000+0 0 9995
|
||||||
|
2 38857 55.7393 104.3859 0005425 285.5287 74.4915 1.70473457 83254
|
||||||
|
BEIDOU-2 G6 (C02)
|
||||||
|
1 38953U 12059A 26063.10227411 -.00000185 00000+0 00000+0 0 9993
|
||||||
|
2 38953 4.1554 74.6500 0010671 304.2337 263.7683 1.00269514 48856
|
||||||
|
LUCH 5B (SDCM/PRN 125)
|
||||||
|
1 38977U 12061A 26065.76608216 -.00000139 00000+0 00000+0 0 9991
|
||||||
|
2 38977 10.2623 50.8817 0003692 226.3927 146.6875 1.00272261 48583
|
||||||
|
COSMOS 2485 (747)
|
||||||
|
1 39155U 13019A 26063.73454937 .00000012 00000+0 00000+0 0 9997
|
||||||
|
2 39155 65.3600 77.1091 0025497 236.8511 122.9458 2.13102928100058
|
||||||
|
GPS BIIF-4 (PRN 27)
|
||||||
|
1 39166U 13023A 26064.97752527 -.00000015 00000+0 00000+0 0 9991
|
||||||
|
2 39166 54.5849 273.1765 0141644 49.9392 311.3205 2.00564238 93815
|
||||||
|
IRNSS-1A
|
||||||
|
1 39199U 13034A 26062.59712745 .00000081 00000+0 00000+0 0 9995
|
||||||
|
2 39199 35.4874 65.3779 0017825 190.7092 175.2371 1.00269823 46313
|
||||||
|
GPS BIIF-5 (PRN 30)
|
||||||
|
1 39533U 14008A 26065.47472284 .00000007 00000+0 00000+0 0 9998
|
||||||
|
2 39533 53.6379 153.5106 0082368 228.3720 130.9608 2.00553229 87628
|
||||||
|
ASTRA 5B (EGNOS/PRN 123)
|
||||||
|
1 39617U 14011B 26065.80022338 .00000134 00000+0 00000+0 0 9994
|
||||||
|
2 39617 0.0860 301.0296 0004015 28.7722 146.2633 1.00272325 43645
|
||||||
|
COSMOS 2492 (754)
|
||||||
|
1 39620U 14012A 26065.57044016 -.00000059 00000+0 00000+0 0 9997
|
||||||
|
2 39620 65.3564 317.4255 0013141 326.4399 33.5365 2.13103231 93052
|
||||||
|
IRNSS-1B
|
||||||
|
1 39635U 14017A 26062.54882074 .00000102 00000+0 00000+0 0 9994
|
||||||
|
2 39635 29.1127 242.2056 0021171 179.2461 352.3815 1.00270890 43734
|
||||||
|
LUCH 5V (SDCM/PRN 141)
|
||||||
|
1 39727U 14023A 26065.60036917 -.00000271 00000+0 00000+0 0 9998
|
||||||
|
2 39727 4.8804 70.7549 0003084 285.1366 119.2902 1.00271330 43336
|
||||||
|
GPS BIIF-6 (PRN 06)
|
||||||
|
1 39741U 14026A 26064.75554898 -.00000099 00000+0 00000+0 0 9993
|
||||||
|
2 39741 56.5458 335.5470 0039849 324.6009 35.1717 2.00568639 86460
|
||||||
|
COSMOS 2500 (755)
|
||||||
|
1 40001U 14032A 26064.33376667 -.00000055 00000+0 00000+0 0 9993
|
||||||
|
2 40001 65.2924 317.2230 0005183 201.3822 158.6566 2.13105772 91249
|
||||||
|
GPS BIIF-7 (PRN 09)
|
||||||
|
1 40105U 14045A 26063.84593997 .00000005 00000+0 00000+0 0 9998
|
||||||
|
2 40105 55.2815 92.6206 0031508 120.4743 239.9033 2.00551558 83989
|
||||||
|
GSAT0201 (GALILEO 5)
|
||||||
|
1 40128U 14050A 26062.23675925 -.00000027 00000+0 00000+0 0 9998
|
||||||
|
2 40128 48.9889 277.7718 1657553 175.2875 186.5034 1.85520013 76352
|
||||||
|
GSAT0202 (GALILEO 6)
|
||||||
|
1 40129U 14050B 26065.18759423 -.00000036 00000+0 00000+0 0 9992
|
||||||
|
2 40129 49.0025 276.7356 1658927 176.1364 185.3302 1.85520810 78579
|
||||||
|
IRNSS-1C
|
||||||
|
1 40269U 14061A 26065.58305772 -.00000163 00000+0 00000+0 0 9998
|
||||||
|
2 40269 6.2543 90.9291 0019872 356.6405 9.5984 1.00269479 41669
|
||||||
|
GPS BIIF-8 (PRN 03)
|
||||||
|
1 40294U 14068A 26065.21974178 -.00000078 00000+0 00000+0 0 9990
|
||||||
|
2 40294 56.9208 34.9218 0062856 68.0586 292.5929 2.00569976 83146
|
||||||
|
COSMOS 2501 (702K)
|
||||||
|
1 40315U 14075A 26060.84023701 -.00000015 00000+0 00000+0 0 9994
|
||||||
|
2 40315 63.6170 194.0851 0020735 205.7160 154.1408 2.13101099 87569
|
||||||
|
GPS BIIF-9 (PRN 26)
|
||||||
|
1 40534U 15013A 26065.35117279 .00000016 00000+0 00000+0 0 9995
|
||||||
|
2 40534 53.1859 208.4290 0108755 39.8054 320.9401 2.00557521 79764
|
||||||
|
GSAT0203 (GALILEO 7)
|
||||||
|
1 40544U 15017A 26058.52158611 -.00000103 00000+0 00000+0 0 9994
|
||||||
|
2 40544 56.8234 344.5881 0005161 295.0116 64.9415 1.70475495 67337
|
||||||
|
GSAT0204 (GALILEO 8)
|
||||||
|
1 40545U 15017B 26058.19060141 -.00000102 00000+0 00000+0 0 9997
|
||||||
|
2 40545 56.8295 344.6151 0004389 286.3298 73.6287 1.70475370 30371
|
||||||
|
BEIDOU-3S IGSO-1S (C31)
|
||||||
|
1 40549U 15019A 26065.60071583 -.00000164 00000+0 00000+0 0 9997
|
||||||
|
2 40549 49.3862 296.6971 0035764 190.6092 349.7460 1.00273571 40045
|
||||||
|
GPS BIIF-10 (PRN 08)
|
||||||
|
1 40730U 15033A 26065.51165918 -.00000012 00000+0 00000+0 0 9993
|
||||||
|
2 40730 54.0631 271.3180 0114550 29.0223 331.6302 2.00561568 77938
|
||||||
|
BEIDOU-3S M2S (C58)
|
||||||
|
1 40748U 15037A 26064.15514271 -.00000065 00000+0 00000+0 0 9993
|
||||||
|
2 40748 54.9621 305.6294 0008821 278.3568 81.6004 1.86231809 72184
|
||||||
|
BEIDOU-3S M1S (C57)
|
||||||
|
1 40749U 15037B 26061.69084880 -.00000053 00000+0 00000+0 0 9995
|
||||||
|
2 40749 54.9610 305.7025 0004647 29.9008 330.1863 1.86253746 72142
|
||||||
|
GSAT0205 (GALILEO 9)
|
||||||
|
1 40889U 15045A 26043.95434207 .00000078 00000+0 00000+0 0 9996
|
||||||
|
2 40889 53.6403 225.6057 0359599 59.9658 303.5577 1.67410800 64211
|
||||||
|
GSAT0206 (GALILEO 10)
|
||||||
|
1 40890U 15045B 26065.21845735 .00000020 00000+0 00000+0 0 9990
|
||||||
|
2 40890 54.9806 224.4444 0003724 50.3393 309.6334 1.70473773 65286
|
||||||
|
GPS BIIF-11 (PRN 10)
|
||||||
|
1 41019U 15062A 26065.57045984 -.00000077 00000+0 00000+0 0 9993
|
||||||
|
2 41019 56.8944 34.7646 0111446 231.5122 127.4715 2.00567146 75770
|
||||||
|
GSAT-15 (GAGAN/PRN 139)
|
||||||
|
1 41028U 15065A 26065.10082801 -.00000263 00000+0 00000+0 0 9991
|
||||||
|
2 41028 0.0855 269.7149 0001104 106.5589 277.3204 1.00271608 37766
|
||||||
|
GSAT0209 (GALILEO 12)
|
||||||
|
1 41174U 15079A 26064.80003714 .00000008 00000+0 00000+0 0 9996
|
||||||
|
2 41174 55.7602 104.0969 0004738 323.5937 36.4535 1.70474567 63014
|
||||||
|
GSAT0208 (GALILEO 11)
|
||||||
|
1 41175U 15079B 26062.08735472 .00000015 00000+0 00000+0 0 9996
|
||||||
|
2 41175 55.7569 104.1685 0004163 323.7694 36.2890 1.70474318 63383
|
||||||
|
IRNSS-1E
|
||||||
|
1 41241U 16003A 26065.38994928 -.00000293 00000+0 00000+0 0 9996
|
||||||
|
2 41241 33.0116 61.2294 0017646 189.2265 165.8466 1.00273983 37042
|
||||||
|
GPS BIIF-12 (PRN 32)
|
||||||
|
1 41328U 16007A 26065.15731679 -.00000004 00000+0 00000+0 0 9993
|
||||||
|
2 41328 55.4568 93.5431 0095487 246.0433 113.0135 2.00556703 73779
|
||||||
|
COSMOS 2514 (751)
|
||||||
|
1 41330U 16008A 26065.04801300 -.00000057 00000+0 00000+0 0 9992
|
||||||
|
2 41330 65.0816 316.5487 0009105 208.8848 151.1249 2.13102742 78422
|
||||||
|
IRNSS-1F
|
||||||
|
1 41384U 16015A 26065.80187006 .00000156 00000+0 00000+0 0 9992
|
||||||
|
2 41384 5.0902 99.2104 0019116 188.2385 198.1485 1.00272507 36645
|
||||||
|
BEIDOU-2 IGSO-6 (C13)
|
||||||
|
1 41434U 16021A 26065.38665594 -.00000150 00000+0 00000+0 0 9996
|
||||||
|
2 41434 60.0894 39.0582 0059788 233.3814 126.6015 1.00278312 36456
|
||||||
|
IRNSS-1G
|
||||||
|
1 41469U 16027A 26065.78253050 -.00000337 00000+0 00000+0 0 9991
|
||||||
|
2 41469 4.9410 99.9517 0005342 314.3156 161.4455 1.00267587 36158
|
||||||
|
GSAT0211 (GALILEO 14)
|
||||||
|
1 41549U 16030A 26064.55763471 .00000016 00000+0 00000+0 0 9994
|
||||||
|
2 41549 55.1310 224.5419 0003581 25.3389 334.6168 1.70473873 60900
|
||||||
|
GSAT0210 (GALILEO 13)
|
||||||
|
1 41550U 16030B 26064.81491220 .00000018 00000+0 00000+0 0 9997
|
||||||
|
2 41550 55.1283 224.5329 0001200 195.2538 164.6814 1.70473987 60905
|
||||||
|
BEIDOU-2 G7 (C03)
|
||||||
|
1 41586U 16037A 26065.81738347 -.00000355 00000+0 00000+0 0 9993
|
||||||
|
2 41586 1.4675 68.2443 0007914 342.8303 158.1757 1.00266463 5861
|
||||||
|
EUTELSAT 117 WEST B (W*)
|
||||||
|
1 41589U 16038B 26064.98792982 -.00000018 00000+0 00000+0 0 9995
|
||||||
|
2 41589 0.0104 328.8615 0000350 267.7539 165.7653 1.00273781 35622
|
||||||
|
GSAT0207 (GALILEO 15)
|
||||||
|
1 41859U 16069A 26059.22748061 .00000008 00000+0 00000+0 0 9997
|
||||||
|
2 41859 55.4267 104.1007 0005031 309.1503 50.8842 1.70474383 57546
|
||||||
|
GSAT0212 (GALILEO 16)
|
||||||
|
1 41860U 16069B 26064.94685676 .00000007 00000+0 00000+0 0 9995
|
||||||
|
2 41860 55.4278 103.9372 0003643 335.6888 24.3725 1.70474702 57898
|
||||||
|
GSAT0213 (GALILEO 17)
|
||||||
|
1 41861U 16069C 26064.72771558 .00000008 00000+0 00000+0 0 9998
|
||||||
|
2 41861 55.4302 103.9512 0005295 293.0089 67.0145 1.70474927 57752
|
||||||
|
GSAT0214 (GALILEO 18)
|
||||||
|
1 41862U 16069D 26065.46034919 .00000006 00000+0 00000+0 0 9992
|
||||||
|
2 41862 55.4293 103.9236 0004341 297.3479 62.6840 1.70474815 57890
|
||||||
|
SES-15 (WAAS/PRN 133)
|
||||||
|
1 42709U 17026A 26064.01780892 .00000050 00000+0 00000+0 0 9994
|
||||||
|
2 42709 0.0426 307.9199 0000713 39.5952 52.4724 1.00271060 6386
|
||||||
|
QZS-2 (QZSS/PRN 194)
|
||||||
|
1 42738U 17028A 26064.73548161 -.00000170 00000+0 00000+0 0 9992
|
||||||
|
2 42738 39.5883 244.6601 0745486 270.3415 54.7958 1.00261653 6582
|
||||||
|
QZS-3 (QZSS/PRN 199)
|
||||||
|
1 42917U 17048A 26065.78864368 -.00000351 00000+0 00000+0 0 9990
|
||||||
|
2 42917 0.0678 212.5623 0002297 148.1009 214.7581 1.00275228 31234
|
||||||
|
COSMOS 2522 (752)
|
||||||
|
1 42939U 17055A 26064.88589677 .00000019 00000+0 00000+0 0 9999
|
||||||
|
2 42939 64.0788 195.3973 0007310 227.5424 132.3669 2.13101691 65775
|
||||||
|
QZS-4 (QZSS/PRN 195)
|
||||||
|
1 42965U 17062A 26064.74126170 -.00000341 00000+0 00000+0 0 9995
|
||||||
|
2 42965 40.1722 344.8064 0748494 269.9150 316.0784 1.00280691 30763
|
||||||
|
BEIDOU-3 M1 (C19)
|
||||||
|
1 43001U 17069A 26063.96368704 -.00000029 00000+0 00000+0 0 9991
|
||||||
|
2 43001 56.6017 66.5157 0012381 309.6653 50.2473 1.86231070 56664
|
||||||
|
BEIDOU-3 M2 (C20)
|
||||||
|
1 43002U 17069B 26063.36523573 -.00000024 00000+0 00000+0 0 9998
|
||||||
|
2 43002 56.5998 66.5715 0009709 329.0395 30.9280 1.86230543 56644
|
||||||
|
GSAT0215 (GALILEO 19)
|
||||||
|
1 43055U 17079A 26062.43085966 .00000010 00000+0 00000+0 0 9997
|
||||||
|
2 43055 55.1080 224.4135 0000631 13.1955 346.7446 1.70474475 51181
|
||||||
|
GSAT0216 (GALILEO 20)
|
||||||
|
1 43056U 17079B 26063.89524153 .00000013 00000+0 00000+0 0 9998
|
||||||
|
2 43056 55.1080 224.3734 0001703 320.6652 39.2594 1.70474648 51222
|
||||||
|
GSAT0217 (GALILEO 21)
|
||||||
|
1 43057U 17079C 26065.28916355 .00000020 00000+0 00000+0 0 9991
|
||||||
|
2 43057 55.1073 224.3328 0002074 355.7864 4.1520 1.70474591 51238
|
||||||
|
GSAT0218 (GALILEO 22)
|
||||||
|
1 43058U 17079D 26063.74946031 .00000013 00000+0 00000+0 0 9997
|
||||||
|
2 43058 55.1048 224.3730 0001921 303.9467 55.9718 1.70474529 51236
|
||||||
|
BEIDOU-3 M7 (C27)
|
||||||
|
1 43107U 18003A 26064.18114576 -.00000067 00000+0 00000+0 0 9990
|
||||||
|
2 43107 54.4201 305.9428 0004668 71.1796 288.9274 1.86232262 55416
|
||||||
|
BEIDOU-3 M8 (C28)
|
||||||
|
1 43108U 18003B 26065.20061454 -.00000069 00000+0 00000+0 0 9993
|
||||||
|
2 43108 54.4118 305.8896 0002953 294.8433 65.1800 1.86230867 55441
|
||||||
|
BEIDOU-3 M4 (C22)
|
||||||
|
1 43207U 18018A 26062.42109766 -.00000016 00000+0 00000+0 0 9991
|
||||||
|
2 43207 56.5592 66.6343 0006867 350.5022 9.5144 1.86230898 54773
|
||||||
|
BEIDOU-3 M3 (C21)
|
||||||
|
1 43208U 18018B 26064.63534936 -.00000033 00000+0 00000+0 0 9997
|
||||||
|
2 43208 56.5606 66.5651 0009923 319.6810 40.2646 1.86231141 54814
|
||||||
|
BEIDOU-3 M9 (C29)
|
||||||
|
1 43245U 18029A 26063.77860799 -.00000062 00000+0 00000+0 0 9995
|
||||||
|
2 43245 54.2878 303.6009 0000937 21.7845 338.2765 1.86228195 53959
|
||||||
|
BEIDOU-3 M10 (C30)
|
||||||
|
1 43246U 18029B 26063.17653141 -.00000059 00000+0 00000+0 0 9997
|
||||||
|
2 43246 54.2860 303.6493 0005102 23.7928 336.2892 1.86228884 53922
|
||||||
|
IRNSS-1I
|
||||||
|
1 43286U 18035A 26065.59256913 .00000086 00000+0 00000+0 0 9991
|
||||||
|
2 43286 28.9346 75.7801 0018963 190.6828 166.3188 1.00278580 29045
|
||||||
|
COSMOS 2527 (756)
|
||||||
|
1 43508U 18053A 26064.84716644 .00000005 00000+0 00000+0 0 9998
|
||||||
|
2 43508 65.5190 77.3533 0009601 245.4642 114.4730 2.13102075 60073
|
||||||
|
BEIDOU-2 IGSO-7 (C16)
|
||||||
|
1 43539U 18057A 26056.73984515 -.00000161 00000+0 00000+0 0 9993
|
||||||
|
2 43539 55.1859 164.0342 0099962 236.5988 133.5173 1.00284731 28015
|
||||||
|
GSAT0221 (GALILEO 25)
|
||||||
|
1 43564U 18060A 26059.54670036 -.00000105 00000+0 00000+0 0 9995
|
||||||
|
2 43564 57.2051 344.7173 0005804 306.7848 53.1689 1.70475369 47315
|
||||||
|
GSAT0222 (GALILEO 26)
|
||||||
|
1 43565U 18060B 26055.66007047 -.00000092 00000+0 00000+0 0 9996
|
||||||
|
2 43565 57.2093 344.8256 0004435 285.4820 74.4799 1.70474973 47292
|
||||||
|
GSAT0219 (GALILEO 23)
|
||||||
|
1 43566U 18060C 26065.26653775 -.00000104 00000+0 00000+0 0 9993
|
||||||
|
2 43566 57.2056 344.5677 0004933 321.2111 38.7755 1.70475265 47437
|
||||||
|
GSAT0220 (GALILEO 24)
|
||||||
|
1 43567U 18060D 26058.44840234 -.00000102 00000+0 00000+0 0 9992
|
||||||
|
2 43567 57.2075 344.7516 0005301 308.5821 51.3768 1.70475159 47302
|
||||||
|
BEIDOU-3 M5 (C23)
|
||||||
|
1 43581U 18062A 26065.01153201 .00000011 00000+0 00000+0 0 9990
|
||||||
|
2 43581 54.0879 185.8116 0002277 307.0485 52.9036 1.86228711 51716
|
||||||
|
BEIDOU-3 M6 (C24)
|
||||||
|
1 43582U 18062B 26062.72660318 -.00000014 00000+0 00000+0 0 9992
|
||||||
|
2 43582 54.0877 185.8781 0006255 44.1915 315.8167 1.86227526 51674
|
||||||
|
BEIDOU-3 M12 (C26)
|
||||||
|
1 43602U 18067A 26064.81287722 .00000009 00000+0 00000+0 0 9999
|
||||||
|
2 43602 54.1822 184.5453 0008548 39.5961 320.4401 1.86227815 51197
|
||||||
|
BEIDOU-3 M11 (C25)
|
||||||
|
1 43603U 18067B 26064.94763062 .00000010 00000+0 00000+0 0 9992
|
||||||
|
2 43603 54.1797 184.5047 0004933 32.9662 327.0394 1.86227200 51223
|
||||||
|
BEIDOU-3 M13 (C32)
|
||||||
|
1 43622U 18072A 26063.82956810 -.00000029 00000+0 00000+0 0 9991
|
||||||
|
2 43622 56.5333 66.1018 0008916 307.8246 52.1164 1.86230808 50718
|
||||||
|
BEIDOU-3 M14 (C33)
|
||||||
|
1 43623U 18072B 26063.69502611 -.00000028 00000+0 00000+0 0 9992
|
||||||
|
2 43623 56.5350 66.1003 0006376 328.8150 31.1695 1.86230841 50709
|
||||||
|
BEIDOU-3 M16 (C35)
|
||||||
|
1 43647U 18078A 26064.38883562 -.00000064 00000+0 00000+0 0 9998
|
||||||
|
2 43647 54.1837 303.6261 0008456 31.3305 328.7753 1.86232381 50221
|
||||||
|
BEIDOU-3 M15 (C34)
|
||||||
|
1 43648U 18078B 26062.37035185 -.00000055 00000+0 00000+0 0 9996
|
||||||
|
2 43648 54.1808 303.6975 0005342 38.8897 321.2084 1.86232017 50200
|
||||||
|
BEIDOU-3 G1 (C59)
|
||||||
|
1 43683U 18085A 26065.82327684 -.00000281 00000+0 00000+0 0 9995
|
||||||
|
2 43683 2.8167 100.3177 0001536 15.9186 124.6875 1.00267936 27033
|
||||||
|
COSMOS 2529 (757)
|
||||||
|
1 43687U 18086A 26058.37448306 .00000008 00000+0 00000+0 0 9993
|
||||||
|
2 43687 64.2955 196.2992 0007959 253.0448 106.8346 2.13101880 56941
|
||||||
|
BEIDOU-3 M17 (C36)
|
||||||
|
1 43706U 18093A 26065.21496850 .00000012 00000+0 00000+0 0 9996
|
||||||
|
2 43706 54.1711 185.7894 0007108 296.9682 63.1399 1.86228456 49625
|
||||||
|
BEIDOU-3 M18 (C37)
|
||||||
|
1 43707U 18093B 26065.61398683 .00000015 00000+0 00000+0 0 9999
|
||||||
|
2 43707 54.1720 185.7676 0006029 341.6621 18.2927 1.86226630 49626
|
||||||
|
GPS BIII-1 (PRN 04)
|
||||||
|
1 43873U 18109A 26065.54477595 -.00000004 00000+0 00000+0 0 9992
|
||||||
|
2 43873 55.6164 96.2348 0037951 198.3878 333.9441 2.00556975 53010
|
||||||
|
BEIDOU-3 IGSO-1 (C38)
|
||||||
|
1 44204U 19023A 26065.81911682 -.00000207 00000+0 00000+0 0 9992
|
||||||
|
2 44204 58.5573 38.7007 0025971 236.3637 303.4808 1.00270441 6188
|
||||||
|
BEIDOU-2 G8 (C01)
|
||||||
|
1 44231U 19027A 26065.82490331 -.00000250 00000+0 00000+0 0 9998
|
||||||
|
2 44231 1.3990 74.2547 0011620 271.5694 260.0996 1.00275696 25051
|
||||||
|
COSMOS 2534 (758)
|
||||||
|
1 44299U 19030A 26063.82267120 .00000008 00000+0 00000+0 0 9992
|
||||||
|
2 44299 64.3687 196.1275 0009607 279.9650 74.0554 2.13102294 52696
|
||||||
|
BEIDOU-3 IGSO-2 (C39)
|
||||||
|
1 44337U 19035A 26057.68879618 -.00000176 00000+0 00000+0 0 9995
|
||||||
|
2 44337 55.2338 159.8353 0038734 206.4752 153.7223 1.00247364 24561
|
||||||
|
GPS BIII-2 (PRN 18)
|
||||||
|
1 44506U 19056A 26065.18297285 -.00000100 00000+0 00000+0 0 9991
|
||||||
|
2 44506 55.6839 335.2387 0058046 198.0933 333.3685 2.00568454 48001
|
||||||
|
BEIDOU-3 M24 (C46)
|
||||||
|
1 44542U 19061A 26065.14698524 .00000012 00000+0 00000+0 0 9995
|
||||||
|
2 44542 54.3990 186.2022 0008583 21.0011 339.0071 1.86228818 43870
|
||||||
|
BEIDOU-3 M23 (C45)
|
||||||
|
1 44543U 19061B 26065.81391679 .00000016 00000+0 00000+0 0 9995
|
||||||
|
2 44543 54.3975 186.1481 0005058 13.8953 345.4949 1.86227427 43881
|
||||||
|
EUTELSAT 5 WEST B (EGN*)
|
||||||
|
1 44624U 19067A 26065.79450338 -.00000046 00000+0 00000+0 0 9990
|
||||||
|
2 44624 0.0906 313.0550 0003219 44.2294 88.2197 1.00273147 23414
|
||||||
|
BEIDOU-3 IGSO-3 (C40)
|
||||||
|
1 44709U 19073A 26063.99963149 -.00000162 00000+0 00000+0 0 9999
|
||||||
|
2 44709 54.9773 282.0232 0040551 188.9231 171.0493 1.00269134 23418
|
||||||
|
BEIDOU-3 M22 (C44)
|
||||||
|
1 44793U 19078A 26063.37544912 -.00000060 00000+0 00000+0 0 9999
|
||||||
|
2 44793 54.0307 303.5169 0007273 35.6258 324.4804 1.86231851 42694
|
||||||
|
BEIDOU-3 M21 (C43)
|
||||||
|
1 44794U 19078B 26062.97806659 -.00000058 00000+0 00000+0 0 9991
|
||||||
|
2 44794 54.0042 303.4937 0005911 39.9322 320.1697 1.86227541 42693
|
||||||
|
COSMOS 2544 (759)
|
||||||
|
1 44850U 19088A 26065.26106663 .00000003 00000+0 00000+0 0 9997
|
||||||
|
2 44850 65.5071 77.1646 0015771 259.7283 100.1293 2.13101860 48522
|
||||||
|
BEIDOU-3 M19 (C41)
|
||||||
|
1 44864U 19090A 26064.56032794 -.00000033 00000+0 00000+0 0 9994
|
||||||
|
2 44864 56.4687 66.2141 0020433 291.4658 245.9437 1.86231373 42308
|
||||||
|
BEIDOU-3 M20 (C42)
|
||||||
|
1 44865U 19090B 26064.42720734 -.00000032 00000+0 00000+0 0 9997
|
||||||
|
2 44865 56.4702 66.2529 0017835 302.9017 234.8674 1.86231388 42289
|
||||||
|
BEIDOU-3 G2 (C60)
|
||||||
|
1 45344U 20017A 26065.81140345 -.00000143 00000+0 00000+0 0 9992
|
||||||
|
2 45344 2.6592 45.6301 0002728 310.2866 180.7453 1.00272736 22235
|
||||||
|
COSMOS 2545 (760)
|
||||||
|
1 45358U 20018A 26064.04887093 -.00000054 00000+0 00000+0 0 9991
|
||||||
|
2 45358 64.4172 316.0811 0008063 250.0323 109.9417 2.13102426 46443
|
||||||
|
BEIDOU-3 G3 (C61)
|
||||||
|
1 45807U 20040A 26065.81738347 -.00000356 00000+0 00000+0 0 9993
|
||||||
|
2 45807 2.1959 67.1365 0004591 319.6533 182.5056 1.00274500 21042
|
||||||
|
GPS BIII-3 (PRN 23)
|
||||||
|
1 45854U 20041A 26065.51925653 -.00000081 00000+0 00000+0 0 9996
|
||||||
|
2 45854 56.6073 32.9268 0061430 205.2409 151.1260 2.00560728 41978
|
||||||
|
GALAXY 30 (WAAS/PRN 135)
|
||||||
|
1 46114U 20056C 26065.55677565 .00000000 00000+0 00000+0 0 9992
|
||||||
|
2 46114 0.0219 155.3844 0002048 200.5806 243.7632 1.00271670 20266
|
||||||
|
COSMOS 2547 (705K)
|
||||||
|
1 46805U 20075A 26065.64518105 .00000026 00000+0 00000+0 0 9992
|
||||||
|
2 46805 64.5994 195.8945 0006875 230.4824 129.4314 2.13100263 41728
|
||||||
|
GPS BIII-4 (PRN 14)
|
||||||
|
1 46826U 20078A 26064.79964802 .00000012 00000+0 00000+0 0 9997
|
||||||
|
2 46826 54.0013 214.9691 0068278 204.3383 325.7195 2.00565559 39435
|
||||||
|
GPS BIII-5 (PRN 11)
|
||||||
|
1 48859U 21054A 26064.07032190 -.00000101 00000+0 00000+0 0 9997
|
||||||
|
2 48859 55.2207 336.6471 0023861 232.0319 315.6765 2.00575594 34666
|
||||||
|
QZS-1R (QZSS/PRN 196)
|
||||||
|
1 49336U 21096A 26065.82405683 -.00000265 00000+0 00000+0 0 9990
|
||||||
|
2 49336 37.2488 81.0937 0747759 269.7649 244.5984 1.00290649 15989
|
||||||
|
GSAT0223 (GALILEO 27)
|
||||||
|
1 49809U 21116A 26048.03988929 -.00000078 00000+0 00000+0 0 9993
|
||||||
|
2 49809 57.2005 344.9238 0002865 278.1950 264.6358 1.70475508 26130
|
||||||
|
GSAT0224 (GALILEO 28)
|
||||||
|
1 49810U 21116B 26064.29072237 -.00000105 00000+0 00000+0 0 9998
|
||||||
|
2 49810 57.1948 344.4822 0001942 295.2728 253.6293 1.70475760 26438
|
||||||
|
COSMOS 2557 (706K)
|
||||||
|
1 52984U 22075A 26063.93321719 -.00000057 00000+0 00000+0 0 9997
|
||||||
|
2 52984 64.3588 318.1599 0011575 283.5582 76.3723 2.13101614 28492
|
||||||
|
COSMOS 2559 (707K)
|
||||||
|
1 54031U 22130A 26064.69111461 -.00000060 00000+0 00000+0 0 9993
|
||||||
|
2 54031 64.3310 318.2203 0010609 181.3136 178.7423 2.13101297 26488
|
||||||
|
COSMOS 2564 (761)
|
||||||
|
1 54377U 22161A 26064.53462226 .00000016 00000+0 00000+0 0 9991
|
||||||
|
2 54377 64.7419 196.1496 0009447 210.0603 149.8542 2.13101972 25423
|
||||||
|
GPS BIII-6 (PRN 28)
|
||||||
|
1 55268U 23009A 26064.24350115 .00000010 00000+0 00000+0 0 9998
|
||||||
|
2 55268 55.1095 152.6049 0004568 336.6055 15.2776 2.00549197 23157
|
||||||
|
BEIDOU-3 G4 (C62)
|
||||||
|
1 56564U 23066A 26065.82726352 -.00000115 00000+0 00000+0 0 9993
|
||||||
|
2 56564 0.6443 311.2389 0002266 156.3494 154.8091 1.00272440 10368
|
||||||
|
NVS-01 (IRNSS-1J)
|
||||||
|
1 56759U 23076A 26065.78864368 -.00000341 00000+0 00000+0 0 9996
|
||||||
|
2 56759 2.4130 248.5405 0007203 46.0615 283.3391 1.00273048 10089
|
||||||
|
COSMOS 2569 (703K)
|
||||||
|
1 57517U 23114A 26063.60906547 .00000011 00000+0 00000+0 0 9990
|
||||||
|
2 57517 65.2420 76.1460 0012898 291.6250 68.2783 2.13103963 20030
|
||||||
|
BEIDOU-3 M28 (C50)
|
||||||
|
1 58654U 23207A 26064.12943781 -.00000033 00000+0 00000+0 0 9990
|
||||||
|
2 58654 55.6609 65.8120 0004920 303.0244 56.9487 1.86229789 14909
|
||||||
|
BEIDOU-3 M26 (C48)
|
||||||
|
1 58655U 23207B 26065.53778212 -.00000039 00000+0 00000+0 0 9998
|
||||||
|
2 58655 55.6644 65.7507 0005731 288.8087 71.1446 1.86229716 14918
|
||||||
|
GSAT0225 (GALILEO 29)
|
||||||
|
1 59598U 24079A 26064.58030672 .00000007 00000+0 00000+0 0 9992
|
||||||
|
2 59598 55.0507 103.9743 0002603 257.7426 101.8321 1.70474989 11547
|
||||||
|
GSAT0227 (GALILEO 30)
|
||||||
|
1 59600U 24079C 26062.41821347 .00000012 00000+0 00000+0 0 9999
|
||||||
|
2 59600 55.0440 104.0338 0000359 68.5105 291.5792 1.70474834 11567
|
||||||
|
GSAT0232 (GALILEO 32)
|
||||||
|
1 61182U 24167A 26048.66150565 .00000069 00000+0 00000+0 0 9995
|
||||||
|
2 61182 55.2225 224.5881 0003600 296.6314 116.1306 1.70443939 8265
|
||||||
|
GSAT0226 (GALILEO 31)
|
||||||
|
1 61183U 24167B 26063.09230740 .00000011 00000+0 00000+0 0 9991
|
||||||
|
2 61183 55.2176 224.2005 0001588 128.6581 232.0353 1.70474186 9066
|
||||||
|
BEIDOU-3 M25 (C47)
|
||||||
|
1 61186U 24168B 26064.44771763 -.00000067 00000+0 00000+0 0 9992
|
||||||
|
2 61186 54.5648 305.8483 0004268 318.9478 147.1808 1.86230220 9852
|
||||||
|
BEIDOU-3 M27 (C49)
|
||||||
|
1 61187U 24168C 26064.31689484 -.00000067 00000+0 00000+0 0 9997
|
||||||
|
2 61187 54.5604 305.8361 0002980 20.4343 132.9740 1.86230358 7324
|
||||||
|
GPS BIII-7 (PRN 01)
|
||||||
|
1 62339U 24242A 26065.64478888 -.00000104 00000+0 00000+0 0 9993
|
||||||
|
2 62339 54.8910 338.3407 0015837 356.9340 8.8099 2.00569825 9206
|
||||||
|
NVS-02 (IRNSS-1K)
|
||||||
|
1 62850U 25020A 26065.61865175 .00001002 00000+0 81966-3 0 9993
|
||||||
|
2 62850 20.8382 296.9052 7349990 81.2147 348.1780 2.17722557 8741
|
||||||
|
QZS-6 (QZSS/PRN 200)
|
||||||
|
1 62876U 25023A 26065.81348344 -.00000236 00000+0 00000+0 0 9995
|
||||||
|
2 62876 0.0338 296.0742 0001829 48.7188 203.0676 1.00269676 3937
|
||||||
|
COSMOS 2584 (704K)
|
||||||
|
1 63130U 25042A 26059.74188024 .00000039 00000+0 00000+0 0 9992
|
||||||
|
2 63130 65.0213 76.4877 0007822 236.2440 123.7373 2.13103267 7739
|
||||||
|
GPS BIII-8 (PRN 21)
|
||||||
|
1 64202U 25116A 26062.39238152 -.00000077 00000+0 00000+0 0 9991
|
||||||
|
2 64202 55.1633 35.3874 0008474 325.1064 37.8908 2.00553198 5758
|
||||||
|
COSMOS 2596 (708K)
|
||||||
|
1 65590U 25206B 26064.99806025 -.00000060 00000+0 00000+0 0 9992
|
||||||
|
2 65590 64.6934 318.5798 0012868 315.8316 234.0611 2.13100363 3719
|
||||||
|
GSAT0233 (GALILEO 33)
|
||||||
|
1 67160U 25302A 26044.50000000 -.00000008 00000+0 00000+0 0 9990
|
||||||
|
2 67160 54.3935 105.2952 0003011 207.0582 359.4109 1.70474578 20
|
||||||
|
GSAT0234 (GALILEO 34)
|
||||||
|
1 67162U 25302C 26044.50000000 -.00000008 00000+0 00000+0 0 9992
|
||||||
|
2 67162 54.3967 105.2938 0003081 266.8423 119.7309 1.70475065 127
|
||||||
|
GPS BIII-9 (PRN 20)
|
||||||
|
1 67588U 26017A 26062.99831483 .00000012 00000+0 00000+0 0 9992
|
||||||
|
2 67588 55.0184 92.9751 0001516 18.2827 341.7919 2.00560918429508
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
fastapi
|
||||||
|
uvicorn
|
||||||
|
websockets
|
||||||
|
httpx
|
||||||
|
skyfield
|
||||||
|
asyncio
|
||||||
|
pydantic
|
||||||
|
pydantic-settings
|
||||||
|
pyais
|
||||||
|
requests
|
||||||
|
python-dotenv
|
||||||
|
numpy
|
||||||
|
sgp4
|
||||||
|
jplephem
|
||||||
@@ -0,0 +1,875 @@
|
|||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
OLLAMA_URL = "http://localhost:11434/api/generate"
|
||||||
|
MODEL_NAME = "llama3.2"
|
||||||
|
|
||||||
|
# CLI backend: "claude" uses Claude Code CLI (no API key needed if logged in),
|
||||||
|
# "gemini" uses Gemini CLI (free with Google account), "ollama" = local llama3.2
|
||||||
|
# Auto-detect: prefers claude > gemini > ollama
|
||||||
|
def _detect_cli_backend() -> str:
|
||||||
|
for tool in ("gemini", "claude"):
|
||||||
|
if shutil.which(tool):
|
||||||
|
return tool
|
||||||
|
return "ollama"
|
||||||
|
|
||||||
|
_CLI_BACKEND = _detect_cli_backend()
|
||||||
|
|
||||||
|
|
||||||
|
async def _query_cli(prompt: str) -> dict | None:
|
||||||
|
"""Call claude or gemini CLI as subprocess, parse JSON from response."""
|
||||||
|
try:
|
||||||
|
cmd = [_CLI_BACKEND, "-p", prompt]
|
||||||
|
proc = await asyncio.create_subprocess_exec(
|
||||||
|
*cmd,
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.PIPE,
|
||||||
|
)
|
||||||
|
stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=30.0)
|
||||||
|
raw = stdout.decode().strip()
|
||||||
|
if not raw:
|
||||||
|
return None
|
||||||
|
# Strip markdown code fences if present
|
||||||
|
if "```json" in raw:
|
||||||
|
raw = raw.split("```json", 1)[1].split("```", 1)[0].strip()
|
||||||
|
elif "```" in raw:
|
||||||
|
raw = raw.split("```", 1)[1].split("```", 1)[0].strip()
|
||||||
|
# Extract first JSON object if there's surrounding text
|
||||||
|
m = re.search(r'\{.*\}', raw, re.DOTALL)
|
||||||
|
if m:
|
||||||
|
raw = m.group(0)
|
||||||
|
parsed = json.loads(raw)
|
||||||
|
return parsed if isinstance(parsed, dict) else None
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"[AI] CLI query failed ({_CLI_BACKEND}): {exc}")
|
||||||
|
return None
|
||||||
|
THREAT_LEVELS = ("LOW", "GUARDED", "ELEVATED", "HIGH", "SEVERE")
|
||||||
|
|
||||||
|
KEYWORD_SCORES = {
|
||||||
|
"war": 4,
|
||||||
|
"missile": 4,
|
||||||
|
"strike": 4,
|
||||||
|
"attack": 4,
|
||||||
|
"explosion": 3,
|
||||||
|
"drone": 3,
|
||||||
|
"nuclear": 5,
|
||||||
|
"cyber": 3,
|
||||||
|
"hijack": 4,
|
||||||
|
"sanction": 2,
|
||||||
|
"military": 3,
|
||||||
|
"troops": 3,
|
||||||
|
"border": 2,
|
||||||
|
"earthquake": 2,
|
||||||
|
"tsunami": 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Hotspot regions with bounding boxes for spatial movement analysis
|
||||||
|
# bbox = (lat_min, lon_min, lat_max, lon_max)
|
||||||
|
HOTSPOT_REGIONS = {
|
||||||
|
"ukraine": {
|
||||||
|
"name": "Ukraine / Black Sea", "lat": 48.38, "lon": 31.17,
|
||||||
|
"bbox": (44, 25, 52, 40),
|
||||||
|
"keywords": ["ukraine", "kyiv", "kharkiv", "crimea", "donbas", "odesa", "bakhmut", "zaporizhzhia"],
|
||||||
|
"baseline": "Escalation risk around frontline",
|
||||||
|
},
|
||||||
|
"russia": {
|
||||||
|
"name": "Western Russia", "lat": 55.76, "lon": 37.62,
|
||||||
|
"bbox": (52, 30, 62, 48),
|
||||||
|
"keywords": ["russia", "moscow", "kursk"],
|
||||||
|
"baseline": "Military posture shifts",
|
||||||
|
},
|
||||||
|
"israel": {
|
||||||
|
"name": "Israel / Palestine", "lat": 31.05, "lon": 34.85,
|
||||||
|
"bbox": (29, 33, 33.5, 36),
|
||||||
|
"keywords": ["israel", "gaza", "west bank", "tel aviv", "jerusalem", "rafah", "idf", "hamas", "hezbollah"],
|
||||||
|
"baseline": "Cross-border retaliation risk",
|
||||||
|
},
|
||||||
|
"iran": {
|
||||||
|
"name": "Iran", "lat": 32.43, "lon": 53.69,
|
||||||
|
"bbox": (25, 44, 40, 63),
|
||||||
|
"keywords": ["iran", "tehran", "irgc"],
|
||||||
|
"baseline": "Strategic force posture shift",
|
||||||
|
},
|
||||||
|
"syria_lebanon": {
|
||||||
|
"name": "Syria / Lebanon", "lat": 34.80, "lon": 37.00,
|
||||||
|
"bbox": (32, 35, 37.5, 42),
|
||||||
|
"keywords": ["syria", "damascus", "lebanon", "aleppo"],
|
||||||
|
"baseline": "Proxy activity risk",
|
||||||
|
},
|
||||||
|
"taiwan": {
|
||||||
|
"name": "Taiwan Strait", "lat": 23.70, "lon": 120.96,
|
||||||
|
"bbox": (21, 117, 27, 124),
|
||||||
|
"keywords": ["taiwan", "taipei", "taiwan strait"],
|
||||||
|
"baseline": "Maritime and airspace pressure",
|
||||||
|
},
|
||||||
|
"south_china_sea": {
|
||||||
|
"name": "South China Sea", "lat": 12.0, "lon": 113.0,
|
||||||
|
"bbox": (5, 107, 22, 120),
|
||||||
|
"keywords": ["south china sea", "spratly", "paracel"],
|
||||||
|
"baseline": "Naval maneuvering risk",
|
||||||
|
},
|
||||||
|
"korea": {
|
||||||
|
"name": "Korean Peninsula", "lat": 37.57, "lon": 126.98,
|
||||||
|
"bbox": (33, 124, 43, 131),
|
||||||
|
"keywords": ["korea", "pyongyang", "dprk", "north korea", "south korea", "seoul"],
|
||||||
|
"baseline": "Missile test / military response risk",
|
||||||
|
},
|
||||||
|
"red_sea": {
|
||||||
|
"name": "Red Sea / Bab-el-Mandeb", "lat": 15.0, "lon": 42.0,
|
||||||
|
"bbox": (10, 38, 22, 46),
|
||||||
|
"keywords": ["red sea", "houthi", "yemen", "bab-el-mandeb", "aden"],
|
||||||
|
"baseline": "Shipping disruption risk",
|
||||||
|
},
|
||||||
|
"hormuz": {
|
||||||
|
"name": "Strait of Hormuz", "lat": 26.58, "lon": 56.25,
|
||||||
|
"bbox": (24, 54, 28, 58),
|
||||||
|
"keywords": ["hormuz", "persian gulf"],
|
||||||
|
"baseline": "Energy transit disruption risk",
|
||||||
|
},
|
||||||
|
"suez": {
|
||||||
|
"name": "Suez Canal", "lat": 29.93, "lon": 32.55,
|
||||||
|
"bbox": (28, 31, 32, 34),
|
||||||
|
"keywords": ["suez"],
|
||||||
|
"baseline": "Maritime chokepoint risk",
|
||||||
|
},
|
||||||
|
"sahel": {
|
||||||
|
"name": "Sahel / West Africa", "lat": 14.0, "lon": 0.0,
|
||||||
|
"bbox": (8, -10, 20, 15),
|
||||||
|
"keywords": ["mali", "niger", "burkina faso", "sahel", "nigeria"],
|
||||||
|
"baseline": "Insurgency / instability risk",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Region definitions for grouping events into theaters ──
|
||||||
|
REGION_DEFS: dict[str, list[str]] = {
|
||||||
|
"Eastern Europe": [
|
||||||
|
"Ukraine", "Russia", "Crimea", "Donbas", "Donbass", "Kharkiv", "Kyiv",
|
||||||
|
"Kiev", "Odesa", "Mariupol", "Bakhmut", "Zaporizhzhia", "Kherson",
|
||||||
|
"Moldova", "Belarus", "Avdiivka",
|
||||||
|
],
|
||||||
|
"Middle East": [
|
||||||
|
"Israel", "Gaza", "Palestine", "Lebanon", "Hezbollah", "Syria",
|
||||||
|
"Damascus", "Iran", "Tehran", "Iraq", "Baghdad", "Jordan",
|
||||||
|
"West Bank", "Rafah", "Jenin", "Nablus", "Jerusalem", "Tel Aviv",
|
||||||
|
],
|
||||||
|
"Red Sea & Horn of Africa": [
|
||||||
|
"Red Sea", "Yemen", "Houthi", "Somalia", "Sudan", "Ethiopia",
|
||||||
|
"Bab-el-Mandeb", "Suez",
|
||||||
|
],
|
||||||
|
"East Asia": [
|
||||||
|
"China", "Taiwan", "Taipei", "North Korea", "Pyongyang",
|
||||||
|
"South Korea", "Japan", "South China Sea", "Taiwan Strait",
|
||||||
|
"East China Sea",
|
||||||
|
],
|
||||||
|
"South Asia": [
|
||||||
|
"India", "Pakistan", "Afghanistan", "Kashmir", "Myanmar", "Kabul",
|
||||||
|
],
|
||||||
|
"Sub-Saharan Africa": [
|
||||||
|
"Mali", "Niger", "Nigeria", "Congo", "Sahel", "Burkina Faso",
|
||||||
|
"Mozambique", "Libya",
|
||||||
|
],
|
||||||
|
"Europe / NATO": [
|
||||||
|
"NATO", "Poland", "Finland", "Sweden", "Norway", "Baltic",
|
||||||
|
"Germany", "France", "UK", "Serbia", "Kosovo", "Georgia", "Romania",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
_WEIGHT_ORDER = {"CRITICAL": 0, "HIGH": 1, "ELEVATED": 2, "GUARDED": 3, "ACTIVE": 4}
|
||||||
|
|
||||||
|
|
||||||
|
def _match_region(title: str) -> str | None:
|
||||||
|
"""Match a headline to a geopolitical region."""
|
||||||
|
tl = title.lower()
|
||||||
|
for region, keywords in REGION_DEFS.items():
|
||||||
|
for kw in keywords:
|
||||||
|
if kw.lower() in tl:
|
||||||
|
return region
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _regional_briefs(news_items: list, conflicts: list) -> list[dict]:
|
||||||
|
"""Group events by region and produce per-region threat assessments."""
|
||||||
|
region_events: dict[str, list] = {}
|
||||||
|
|
||||||
|
for item in (conflicts or []):
|
||||||
|
region = _match_region(item.get("title", ""))
|
||||||
|
if region:
|
||||||
|
region_events.setdefault(region, []).append(item)
|
||||||
|
|
||||||
|
for item in (news_items or [])[:30]:
|
||||||
|
region = _match_region(item.get("title", ""))
|
||||||
|
if region:
|
||||||
|
region_events.setdefault(region, []).append(
|
||||||
|
{"title": item["title"], "severity": "INFO", "event_type": "INTEL"}
|
||||||
|
)
|
||||||
|
|
||||||
|
briefs = []
|
||||||
|
for region, events in region_events.items():
|
||||||
|
sev_counts: dict[str, int] = {}
|
||||||
|
for ev in events:
|
||||||
|
s = ev.get("severity", "INFO")
|
||||||
|
sev_counts[s] = sev_counts.get(s, 0) + 1
|
||||||
|
|
||||||
|
critical = sev_counts.get("CRITICAL", 0)
|
||||||
|
high = sev_counts.get("HIGH", 0)
|
||||||
|
|
||||||
|
if critical >= 3:
|
||||||
|
status = "CRITICAL"
|
||||||
|
elif critical >= 1 or high >= 3:
|
||||||
|
status = "HIGH"
|
||||||
|
elif high >= 1:
|
||||||
|
status = "ELEVATED"
|
||||||
|
else:
|
||||||
|
status = "ACTIVE"
|
||||||
|
|
||||||
|
top = [e for e in events if e.get("severity") in ("CRITICAL", "HIGH")][:3]
|
||||||
|
if top:
|
||||||
|
summary = "; ".join(
|
||||||
|
e.get("event_type", e.get("title", "")[:40]) for e in top[:2]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
summary = f"{len(events)} events tracked in region"
|
||||||
|
|
||||||
|
briefs.append({
|
||||||
|
"region": region,
|
||||||
|
"status": status,
|
||||||
|
"event_count": len(events),
|
||||||
|
"critical": critical,
|
||||||
|
"high": high,
|
||||||
|
"summary": summary[:120],
|
||||||
|
})
|
||||||
|
|
||||||
|
briefs.sort(key=lambda b: (_WEIGHT_ORDER.get(b["status"], 5), -b["event_count"]))
|
||||||
|
return briefs[:6]
|
||||||
|
|
||||||
|
|
||||||
|
def _key_drivers(
|
||||||
|
score: int, reasons: list[str], gps_data: list, bgp_data: dict,
|
||||||
|
conflicts: list, space_weather: dict, planes: list = None, ships: list = None,
|
||||||
|
) -> list[dict]:
|
||||||
|
"""Identify and rank the top threat signal drivers."""
|
||||||
|
drivers: list[dict] = []
|
||||||
|
|
||||||
|
# GPS Interference
|
||||||
|
gps_count = len(gps_data or [])
|
||||||
|
if gps_count:
|
||||||
|
drivers.append({
|
||||||
|
"signal": "GPS Interference",
|
||||||
|
"detail": f"{gps_count} active jamming zone{'s' if gps_count > 1 else ''} detected via ADS-B navigation anomalies",
|
||||||
|
"weight": "CRITICAL" if gps_count >= 3 else "HIGH",
|
||||||
|
"icon": "⬡",
|
||||||
|
})
|
||||||
|
|
||||||
|
# Military Conflicts
|
||||||
|
if conflicts:
|
||||||
|
critical = sum(1 for c in conflicts if c.get("severity") == "CRITICAL")
|
||||||
|
high = sum(1 for c in conflicts if c.get("severity") == "HIGH")
|
||||||
|
active_regions = set(
|
||||||
|
_match_region(c.get("title", "")) for c in conflicts
|
||||||
|
if _match_region(c.get("title", ""))
|
||||||
|
)
|
||||||
|
if critical or high:
|
||||||
|
drivers.append({
|
||||||
|
"signal": "Military Activity",
|
||||||
|
"detail": f"{critical} CRITICAL, {high} HIGH severity events across {len(active_regions)} region{'s' if len(active_regions) != 1 else ''}",
|
||||||
|
"weight": "CRITICAL" if critical >= 5 else "HIGH" if critical >= 1 else "ELEVATED",
|
||||||
|
"icon": "⚔",
|
||||||
|
})
|
||||||
|
|
||||||
|
# BGP Routing
|
||||||
|
bgp_status = str((bgp_data or {}).get("status", "STABLE")).upper()
|
||||||
|
if bgp_status in ("ELEVATED", "CRITICAL"):
|
||||||
|
updates = (bgp_data or {}).get("total_updates", 0)
|
||||||
|
drivers.append({
|
||||||
|
"signal": "BGP Routing",
|
||||||
|
"detail": f"Internet routing instability: {updates:,} updates/hour ({bgp_status})",
|
||||||
|
"weight": bgp_status,
|
||||||
|
"icon": "📡",
|
||||||
|
})
|
||||||
|
|
||||||
|
# Space Weather
|
||||||
|
sw = space_weather or {}
|
||||||
|
kp = sw.get("kp_index", 0)
|
||||||
|
if isinstance(kp, (int, float)) and kp >= 4:
|
||||||
|
drivers.append({
|
||||||
|
"signal": "Space Weather",
|
||||||
|
"detail": f"Geomagnetic storm Kp={kp:.1f} — {sw.get('description', 'HF radio / satellite disruption risk')}",
|
||||||
|
"weight": "CRITICAL" if kp >= 7 else "HIGH" if kp >= 5 else "ELEVATED",
|
||||||
|
"icon": "☀",
|
||||||
|
})
|
||||||
|
|
||||||
|
# News keywords
|
||||||
|
keyword_drivers = [r for r in reasons if "×" in r]
|
||||||
|
if keyword_drivers:
|
||||||
|
top_kw = keyword_drivers[:3]
|
||||||
|
drivers.append({
|
||||||
|
"signal": "Intel Keywords",
|
||||||
|
"detail": f"High-weight terms in news: {', '.join(top_kw)}",
|
||||||
|
"weight": "ELEVATED" if score >= 8 else "GUARDED",
|
||||||
|
"icon": "📰",
|
||||||
|
})
|
||||||
|
|
||||||
|
# Military air movement
|
||||||
|
mil_count = sum(1 for p in (planes or []) if p.get("military"))
|
||||||
|
total_planes = len(planes or [])
|
||||||
|
if mil_count >= 5:
|
||||||
|
drivers.append({
|
||||||
|
"signal": "Military Air Movement",
|
||||||
|
"detail": f"{mil_count} military aircraft tracked globally ({total_planes:,} total airframes monitored)",
|
||||||
|
"weight": "HIGH" if mil_count >= 30 else "ELEVATED" if mil_count >= 15 else "GUARDED",
|
||||||
|
"icon": "✈",
|
||||||
|
})
|
||||||
|
|
||||||
|
# Maritime movement
|
||||||
|
ship_count = len(ships or [])
|
||||||
|
if ship_count >= 10:
|
||||||
|
tankers = sum(1 for s in (ships or []) if str(s.get("type", "")).lower() in ("tanker", "lng carrier"))
|
||||||
|
drivers.append({
|
||||||
|
"signal": "Maritime Activity",
|
||||||
|
"detail": f"{ship_count} vessels tracked globally" + (f" ({tankers} tankers near chokepoints)" if tankers >= 5 else ""),
|
||||||
|
"weight": "ELEVATED" if ship_count >= 100 else "GUARDED",
|
||||||
|
"icon": "🚢",
|
||||||
|
})
|
||||||
|
|
||||||
|
drivers.sort(key=lambda d: _WEIGHT_ORDER.get(d["weight"], 5))
|
||||||
|
return drivers[:6]
|
||||||
|
|
||||||
|
|
||||||
|
def _correlations(
|
||||||
|
gps_data: list, conflicts: list, bgp_data: dict, space_weather: dict,
|
||||||
|
) -> list[str]:
|
||||||
|
"""Find cross-domain signal correlations."""
|
||||||
|
out: list[str] = []
|
||||||
|
|
||||||
|
conflict_regions = set(
|
||||||
|
_match_region(c.get("title", "")) for c in (conflicts or [])
|
||||||
|
if _match_region(c.get("title", ""))
|
||||||
|
)
|
||||||
|
|
||||||
|
if gps_data and conflicts:
|
||||||
|
for z in gps_data:
|
||||||
|
glat, glon = z.get("lat", 0), z.get("lon", 0)
|
||||||
|
if 35 < glat < 55 and 25 < glon < 45 and "Eastern Europe" in conflict_regions:
|
||||||
|
out.append(
|
||||||
|
"GPS jamming in Eastern Europe coincides with active military operations "
|
||||||
|
"— likely electronic warfare activity"
|
||||||
|
)
|
||||||
|
break
|
||||||
|
for z in gps_data:
|
||||||
|
glat, glon = z.get("lat", 0), z.get("lon", 0)
|
||||||
|
if 25 < glat < 40 and 30 < glon < 50 and "Middle East" in conflict_regions:
|
||||||
|
out.append(
|
||||||
|
"GPS interference near Middle Eastern conflict zones suggests coordinated "
|
||||||
|
"electronic warfare alongside kinetic operations"
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
|
bgp_status = str((bgp_data or {}).get("status", "STABLE")).upper()
|
||||||
|
if bgp_status in ("ELEVATED", "CRITICAL") and conflicts:
|
||||||
|
out.append(
|
||||||
|
f"BGP routing instability ({bgp_status}) during active military operations "
|
||||||
|
"may indicate cyber operations or infrastructure targeting"
|
||||||
|
)
|
||||||
|
|
||||||
|
sw = space_weather or {}
|
||||||
|
kp = sw.get("kp_index", 0) if isinstance(sw.get("kp_index"), (int, float)) else 0
|
||||||
|
if kp >= 4 and gps_data:
|
||||||
|
out.append(
|
||||||
|
f"Elevated geomagnetic activity (Kp={kp:.1f}) may amplify GPS signal "
|
||||||
|
"degradation in existing interference zones"
|
||||||
|
)
|
||||||
|
if kp >= 5:
|
||||||
|
out.append(
|
||||||
|
f"Geomagnetic storm (Kp={kp:.1f}) affecting HF radio propagation — "
|
||||||
|
"military comms may shift to satellite links"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not out:
|
||||||
|
out.append("No significant cross-domain signal correlations detected at this time")
|
||||||
|
return out[:4]
|
||||||
|
|
||||||
|
|
||||||
|
def _watch_items(conflicts: list, gps_data: list, space_weather: dict) -> list[str]:
|
||||||
|
"""Generate actionable intelligence watch items."""
|
||||||
|
items: list[str] = []
|
||||||
|
|
||||||
|
if conflicts:
|
||||||
|
critical = [c for c in conflicts if c.get("severity") == "CRITICAL"]
|
||||||
|
if critical:
|
||||||
|
regions: dict[str, list] = {}
|
||||||
|
for c in critical[:15]:
|
||||||
|
r = _match_region(c.get("title", "")) or "Global"
|
||||||
|
regions.setdefault(r, []).append(c)
|
||||||
|
for region, evts in sorted(regions.items(), key=lambda x: -len(x[1])):
|
||||||
|
items.append(
|
||||||
|
f"Monitor {region}: {len(evts)} critical event{'s' if len(evts) > 1 else ''} "
|
||||||
|
f"— {evts[0].get('event_type', 'military activity')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if gps_data:
|
||||||
|
items.append(
|
||||||
|
f"Track {len(gps_data)} GPS interference zone{'s' if len(gps_data) > 1 else ''} "
|
||||||
|
"— assess impact on aviation safety and military navigation"
|
||||||
|
)
|
||||||
|
|
||||||
|
sw = space_weather or {}
|
||||||
|
if sw.get("alerts"):
|
||||||
|
items.append("Space weather alerts active — monitor satellite communication reliability")
|
||||||
|
|
||||||
|
if not items:
|
||||||
|
items.append("Maintain standard surveillance posture across all domains")
|
||||||
|
return items[:5]
|
||||||
|
|
||||||
|
|
||||||
|
def _threat_from_score(score: int) -> str:
|
||||||
|
if score >= 18:
|
||||||
|
return "SEVERE"
|
||||||
|
if score >= 13:
|
||||||
|
return "HIGH"
|
||||||
|
if score >= 8:
|
||||||
|
return "ELEVATED"
|
||||||
|
if score >= 4:
|
||||||
|
return "GUARDED"
|
||||||
|
return "LOW"
|
||||||
|
|
||||||
|
|
||||||
|
def _score_from_signals(news_items, gps_data, bgp_data) -> tuple[int, list[str]]:
|
||||||
|
score = 0
|
||||||
|
reasons: list[str] = []
|
||||||
|
|
||||||
|
titles = [str(item.get("title", "")).lower() for item in news_items[:20]]
|
||||||
|
title_blob = " ".join(titles)
|
||||||
|
|
||||||
|
for kw, weight in KEYWORD_SCORES.items():
|
||||||
|
hits = title_blob.count(kw)
|
||||||
|
if hits:
|
||||||
|
inc = min(hits, 3) * weight
|
||||||
|
score += inc
|
||||||
|
reasons.append(f"{kw}×{hits}")
|
||||||
|
|
||||||
|
gps_count = len(gps_data or [])
|
||||||
|
if gps_count:
|
||||||
|
bump = min(gps_count, 12)
|
||||||
|
score += bump
|
||||||
|
reasons.append(f"gps_anomalies={gps_count}")
|
||||||
|
|
||||||
|
bgp_status = str((bgp_data or {}).get("status", "STABLE")).upper()
|
||||||
|
if bgp_status == "ELEVATED":
|
||||||
|
score += 3
|
||||||
|
reasons.append("bgp=elevated")
|
||||||
|
elif bgp_status == "CRITICAL":
|
||||||
|
score += 6
|
||||||
|
reasons.append("bgp=critical")
|
||||||
|
elif bgp_status == "OFFLINE":
|
||||||
|
score += 2
|
||||||
|
reasons.append("bgp=offline")
|
||||||
|
|
||||||
|
return score, reasons
|
||||||
|
|
||||||
|
|
||||||
|
def _probability(score: int, offset: int) -> str:
|
||||||
|
pct = max(20, min(95, 35 + score * 3 + offset))
|
||||||
|
return f"{pct}%"
|
||||||
|
|
||||||
|
|
||||||
|
def _in_bbox(lat: float, lon: float, bbox: tuple) -> bool:
|
||||||
|
return bbox[0] <= lat <= bbox[2] and bbox[1] <= lon <= bbox[3]
|
||||||
|
|
||||||
|
|
||||||
|
def _heuristic_predictions(
|
||||||
|
news_items, gps_data, score: int,
|
||||||
|
conflicts=None, planes=None, ships=None,
|
||||||
|
) -> list[dict]:
|
||||||
|
"""
|
||||||
|
Generate predicted hotspots by cross-referencing:
|
||||||
|
- news keyword mentions (intel signal)
|
||||||
|
- conflict event severity (OSINT signal)
|
||||||
|
- military aircraft concentrations (movement signal)
|
||||||
|
- ship density in strategic chokepoints (maritime signal)
|
||||||
|
- GPS interference zones (EW signal)
|
||||||
|
"""
|
||||||
|
titles = [str(item.get("title", "")).lower() for item in (news_items or [])[:30]]
|
||||||
|
title_blob = " ".join(titles)
|
||||||
|
planes = planes or []
|
||||||
|
ships = ships or []
|
||||||
|
conflicts = conflicts or []
|
||||||
|
|
||||||
|
scored: list[tuple[float, str, dict]] = []
|
||||||
|
|
||||||
|
for key, region in HOTSPOT_REGIONS.items():
|
||||||
|
bbox = region["bbox"]
|
||||||
|
|
||||||
|
# ── 1. News keyword mentions ──
|
||||||
|
news_hits = sum(1 for kw in region["keywords"] if kw in title_blob)
|
||||||
|
news_score = news_hits * 3.0
|
||||||
|
|
||||||
|
# ── 2. Conflict event severity ──
|
||||||
|
conflict_crit = 0
|
||||||
|
conflict_high = 0
|
||||||
|
for c in conflicts:
|
||||||
|
ct = (c.get("title") or "").lower()
|
||||||
|
if any(kw in ct for kw in region["keywords"]):
|
||||||
|
sev = c.get("severity", "")
|
||||||
|
if sev == "CRITICAL":
|
||||||
|
conflict_crit += 1
|
||||||
|
elif sev == "HIGH":
|
||||||
|
conflict_high += 1
|
||||||
|
conflict_score = conflict_crit * 5.0 + conflict_high * 2.0
|
||||||
|
|
||||||
|
# ── 3. Military aircraft in region ──
|
||||||
|
mil_count = 0
|
||||||
|
civ_count = 0
|
||||||
|
for p in planes:
|
||||||
|
plat = p.get("lat")
|
||||||
|
plon = p.get("lon")
|
||||||
|
if isinstance(plat, (int, float)) and isinstance(plon, (int, float)):
|
||||||
|
if _in_bbox(plat, plon, bbox):
|
||||||
|
if p.get("military"):
|
||||||
|
mil_count += 1
|
||||||
|
else:
|
||||||
|
civ_count += 1
|
||||||
|
mil_score = min(mil_count * 2.0, 16.0)
|
||||||
|
|
||||||
|
# ── 4. Ship density near strategic chokepoints ──
|
||||||
|
ship_count = 0
|
||||||
|
tanker_count = 0
|
||||||
|
for s in ships:
|
||||||
|
slat = s.get("lat")
|
||||||
|
slon = s.get("lon")
|
||||||
|
if isinstance(slat, (int, float)) and isinstance(slon, (int, float)):
|
||||||
|
if _in_bbox(slat, slon, bbox):
|
||||||
|
ship_count += 1
|
||||||
|
if str(s.get("type", "")).lower() in ("tanker", "lng carrier"):
|
||||||
|
tanker_count += 1
|
||||||
|
ship_score = min(ship_count * 0.8 + tanker_count * 1.2, 10.0)
|
||||||
|
|
||||||
|
# ── 5. GPS interference overlap ──
|
||||||
|
gps_overlap = 0
|
||||||
|
for z in (gps_data or []):
|
||||||
|
zlat = z.get("lat", 0)
|
||||||
|
zlon = z.get("lon", 0)
|
||||||
|
if isinstance(zlat, (int, float)) and isinstance(zlon, (int, float)):
|
||||||
|
if _in_bbox(zlat, zlon, bbox):
|
||||||
|
gps_overlap += 1
|
||||||
|
ew_score = gps_overlap * 4.0
|
||||||
|
|
||||||
|
total = news_score + conflict_score + mil_score + ship_score + ew_score
|
||||||
|
if total < 1.0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Build explanation string from active signals
|
||||||
|
signals = []
|
||||||
|
if news_hits:
|
||||||
|
signals.append(f"{news_hits} intel mentions")
|
||||||
|
if conflict_crit or conflict_high:
|
||||||
|
parts = []
|
||||||
|
if conflict_crit:
|
||||||
|
parts.append(f"{conflict_crit} CRITICAL")
|
||||||
|
if conflict_high:
|
||||||
|
parts.append(f"{conflict_high} HIGH")
|
||||||
|
signals.append(f"{'+'.join(parts)} events")
|
||||||
|
if mil_count:
|
||||||
|
signals.append(f"{mil_count} military aircraft tracked")
|
||||||
|
if ship_count:
|
||||||
|
desc = f"{ship_count} vessels"
|
||||||
|
if tanker_count:
|
||||||
|
desc += f" ({tanker_count} tankers)"
|
||||||
|
signals.append(desc)
|
||||||
|
if gps_overlap:
|
||||||
|
signals.append(f"{gps_overlap} GPS jamming zone{'s' if gps_overlap > 1 else ''}")
|
||||||
|
|
||||||
|
event = region["baseline"]
|
||||||
|
if signals:
|
||||||
|
event += " — " + ", ".join(signals[:3])
|
||||||
|
|
||||||
|
prob_pct = max(25, min(95, int(30 + total * 2.5 + score * 1.5)))
|
||||||
|
|
||||||
|
# Build AI-style reasoning text from active signals
|
||||||
|
reason_parts = []
|
||||||
|
if news_hits:
|
||||||
|
matching_kws = [kw for kw in region["keywords"] if kw in title_blob][:3]
|
||||||
|
reason_parts.append(
|
||||||
|
f"Multiple intelligence feeds ({news_hits}) reference "
|
||||||
|
f"{', '.join(matching_kws) if matching_kws else 'this region'}, "
|
||||||
|
f"indicating heightened international attention."
|
||||||
|
)
|
||||||
|
if conflict_crit:
|
||||||
|
reason_parts.append(
|
||||||
|
f"{conflict_crit} CRITICAL-severity conflict event{'s' if conflict_crit > 1 else ''} "
|
||||||
|
f"detected via OSINT, suggesting active kinetic operations or imminent escalation."
|
||||||
|
)
|
||||||
|
if conflict_high:
|
||||||
|
reason_parts.append(
|
||||||
|
f"{conflict_high} HIGH-severity event{'s' if conflict_high > 1 else ''} "
|
||||||
|
f"reported in the area, indicating elevated security posture."
|
||||||
|
)
|
||||||
|
if mil_count:
|
||||||
|
reason_parts.append(
|
||||||
|
f"ADS-B tracking shows {mil_count} military aircraft operating within the region"
|
||||||
|
f"{f' alongside {civ_count} civilian flights' if civ_count > 20 else ''}, "
|
||||||
|
f"{'which represents unusual concentration' if mil_count >= 5 else 'consistent with ongoing monitoring'}."
|
||||||
|
)
|
||||||
|
if ship_count:
|
||||||
|
s_detail = f"{ship_count} vessels tracked"
|
||||||
|
if tanker_count:
|
||||||
|
s_detail += f" including {tanker_count} tanker{'s' if tanker_count > 1 else ''}"
|
||||||
|
reason_parts.append(
|
||||||
|
f"Maritime surveillance identifies {s_detail} in strategic waters, "
|
||||||
|
f"{'raising chokepoint disruption concerns' if tanker_count >= 2 else 'indicating normal commerce flow'}."
|
||||||
|
)
|
||||||
|
if gps_overlap:
|
||||||
|
reason_parts.append(
|
||||||
|
f"Active GPS jamming/spoofing detected ({gps_overlap} zone{'s' if gps_overlap > 1 else ''}), "
|
||||||
|
f"a strong indicator of electronic warfare activity in the area."
|
||||||
|
)
|
||||||
|
if not reason_parts:
|
||||||
|
reason_parts.append(region["baseline"])
|
||||||
|
|
||||||
|
reason = " ".join(reason_parts)
|
||||||
|
|
||||||
|
scored.append((total, key, {
|
||||||
|
"location": region["name"],
|
||||||
|
"lat": region["lat"],
|
||||||
|
"lon": region["lon"],
|
||||||
|
"event": event[:200],
|
||||||
|
"reason": reason[:500],
|
||||||
|
"probability": f"{prob_pct}%",
|
||||||
|
"_signals": {
|
||||||
|
"news": news_hits, "conflicts": conflict_crit + conflict_high,
|
||||||
|
"mil_planes": mil_count, "ships": ship_count, "gps": gps_overlap,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
scored.sort(key=lambda x: x[0], reverse=True)
|
||||||
|
predictions = []
|
||||||
|
for _, _, item in scored[:4]:
|
||||||
|
# Strip internal fields
|
||||||
|
item.pop("_signals", None)
|
||||||
|
predictions.append(item)
|
||||||
|
|
||||||
|
# Fallback if nothing ranked
|
||||||
|
if not predictions:
|
||||||
|
predictions.append({
|
||||||
|
"location": "Global Monitoring",
|
||||||
|
"lat": 20.0, "lon": 10.0,
|
||||||
|
"event": "No dominant hotspot; maintain broad surveillance",
|
||||||
|
"reason": "Current intelligence signals do not indicate a concentrated threat in any single region. Continuing broad-spectrum monitoring across all sensor feeds.",
|
||||||
|
"probability": _probability(score, 0),
|
||||||
|
})
|
||||||
|
|
||||||
|
return predictions
|
||||||
|
|
||||||
|
|
||||||
|
def _sanitize_prediction(item: dict, fallback_prob: str) -> dict | None:
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
return None
|
||||||
|
lat = item.get("lat")
|
||||||
|
lon = item.get("lon")
|
||||||
|
try:
|
||||||
|
lat = float(lat)
|
||||||
|
lon = float(lon)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
if not (-90 <= lat <= 90 and -180 <= lon <= 180):
|
||||||
|
return None
|
||||||
|
|
||||||
|
location = str(item.get("location", "Unknown Location")).strip()[:80]
|
||||||
|
event = str(item.get("event", "Potential activity shift")).strip()[:180]
|
||||||
|
prob = str(item.get("probability", fallback_prob)).strip()[:8]
|
||||||
|
if not re.match(r"^\d{1,3}%$", prob):
|
||||||
|
prob = fallback_prob
|
||||||
|
|
||||||
|
return {
|
||||||
|
"location": location or "Unknown Location",
|
||||||
|
"lat": lat,
|
||||||
|
"lon": lon,
|
||||||
|
"event": event or "Potential activity shift",
|
||||||
|
"reason": str(item.get("reason", "")).strip()[:500],
|
||||||
|
"probability": prob,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _sanitize_llm_output(raw: dict, fallback_level: str, fallback_predictions: list[dict]) -> dict:
|
||||||
|
summary = str(raw.get("summary", "")).strip()
|
||||||
|
if not summary:
|
||||||
|
summary = "AI produced no summary; heuristic assessment applied."
|
||||||
|
summary = summary[:220]
|
||||||
|
|
||||||
|
level = str(raw.get("threat_level", "")).upper().strip()
|
||||||
|
if level not in THREAT_LEVELS:
|
||||||
|
level = fallback_level
|
||||||
|
|
||||||
|
# Build lookup of heuristic reasons by location name for fallback
|
||||||
|
heuristic_reasons: dict[str, str] = {}
|
||||||
|
for fp in fallback_predictions:
|
||||||
|
loc = str(fp.get("location", "")).lower().strip()
|
||||||
|
if loc and fp.get("reason"):
|
||||||
|
heuristic_reasons[loc] = fp["reason"]
|
||||||
|
|
||||||
|
clean_predictions: list[dict] = []
|
||||||
|
for p in raw.get("predictions", [])[:4]:
|
||||||
|
cleaned = _sanitize_prediction(p, fallback_predictions[0]["probability"])
|
||||||
|
if cleaned:
|
||||||
|
# If LLM didn't provide a reason, try to merge from heuristic
|
||||||
|
if not cleaned.get("reason"):
|
||||||
|
loc_key = str(cleaned.get("location", "")).lower().strip()
|
||||||
|
cleaned["reason"] = heuristic_reasons.get(loc_key, "")
|
||||||
|
clean_predictions.append(cleaned)
|
||||||
|
if len(clean_predictions) >= 4:
|
||||||
|
break
|
||||||
|
|
||||||
|
if len(clean_predictions) < 2:
|
||||||
|
clean_predictions = fallback_predictions
|
||||||
|
|
||||||
|
return {
|
||||||
|
"summary": summary,
|
||||||
|
"threat_level": level,
|
||||||
|
"predictions": clean_predictions,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def _query_ollama(prompt: str) -> dict | None:
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.post(
|
||||||
|
OLLAMA_URL,
|
||||||
|
json={
|
||||||
|
"model": MODEL_NAME,
|
||||||
|
"prompt": prompt,
|
||||||
|
"stream": False,
|
||||||
|
"format": "json",
|
||||||
|
},
|
||||||
|
timeout=20.0,
|
||||||
|
)
|
||||||
|
if response.status_code != 200:
|
||||||
|
return None
|
||||||
|
|
||||||
|
payload = response.json()
|
||||||
|
raw = payload.get("response", "")
|
||||||
|
if not raw:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
parsed = json.loads(raw)
|
||||||
|
return parsed if isinstance(parsed, dict) else None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def analyze_threat(
|
||||||
|
news_items,
|
||||||
|
gps_data=None,
|
||||||
|
bgp_data=None,
|
||||||
|
conflicts=None,
|
||||||
|
space_weather=None,
|
||||||
|
ships=None,
|
||||||
|
planes=None,
|
||||||
|
):
|
||||||
|
if not news_items and not conflicts:
|
||||||
|
return {
|
||||||
|
"summary": "Awaiting intelligence data...",
|
||||||
|
"threat_level": "LOW",
|
||||||
|
"predictions": [],
|
||||||
|
"key_drivers": [],
|
||||||
|
"regional_briefs": [],
|
||||||
|
"correlations": [],
|
||||||
|
"watch_items": ["Maintain standard surveillance posture across all domains"],
|
||||||
|
"source": "heuristic",
|
||||||
|
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
score, reasons = _score_from_signals(news_items, gps_data or [], bgp_data or {})
|
||||||
|
|
||||||
|
# Boost score from conflicts
|
||||||
|
if conflicts:
|
||||||
|
crit = sum(1 for c in conflicts if c.get("severity") == "CRITICAL")
|
||||||
|
high = sum(1 for c in conflicts if c.get("severity") == "HIGH")
|
||||||
|
conflict_bump = min(crit * 2 + high, 10)
|
||||||
|
score += conflict_bump
|
||||||
|
if conflict_bump:
|
||||||
|
reasons.append(f"conflicts={crit}C/{high}H")
|
||||||
|
|
||||||
|
base_level = _threat_from_score(score)
|
||||||
|
base_predictions = _heuristic_predictions(
|
||||||
|
news_items, gps_data or [], score,
|
||||||
|
conflicts=conflicts or [],
|
||||||
|
planes=planes or [],
|
||||||
|
ships=ships or [],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build enhanced intel sections
|
||||||
|
regional = _regional_briefs(news_items, conflicts or [])
|
||||||
|
drivers = _key_drivers(score, reasons, gps_data or [], bgp_data or {}, conflicts or [], space_weather or {}, planes=planes, ships=ships)
|
||||||
|
corr = _correlations(gps_data or [], conflicts or [], bgp_data or {}, space_weather or {})
|
||||||
|
watch = _watch_items(conflicts or [], gps_data or [], space_weather or {})
|
||||||
|
|
||||||
|
# Build movement summary for LLM prompt
|
||||||
|
mil_planes_total = sum(1 for p in (planes or []) if p.get("military"))
|
||||||
|
ship_total = len(ships or [])
|
||||||
|
movement_ctx = (
|
||||||
|
f"Military aircraft tracked: {mil_planes_total}. "
|
||||||
|
f"Vessels tracked: {ship_total}. "
|
||||||
|
)
|
||||||
|
# Summarize heuristic predictions for LLM context
|
||||||
|
hotspot_ctx = " | ".join(
|
||||||
|
f"{p['location']}({p['probability']}): {p['event'][:60]}"
|
||||||
|
for p in base_predictions[:3]
|
||||||
|
)
|
||||||
|
|
||||||
|
headlines = " | ".join(str(item.get('title', ''))[:80] for item in news_items[:6])
|
||||||
|
prompt = (
|
||||||
|
f"STRATCOM AI. Strict JSON only, no markdown.\n"
|
||||||
|
f"News: {headlines}\n"
|
||||||
|
f"Signals: GPS={len(gps_data or [])}, BGP={(bgp_data or {}).get('status','?')}, {movement_ctx.strip()}\n"
|
||||||
|
f"Hotspots: {hotspot_ctx}\n"
|
||||||
|
f'Return: {{"summary":"<1 sentence>","threat_level":"LOW|GUARDED|ELEVATED|HIGH|SEVERE",'
|
||||||
|
f'"predictions":[{{"location":"","lat":0,"lon":0,"event":"","probability":"NN%","reason":""}}]}}'
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if _CLI_BACKEND != "ollama":
|
||||||
|
llm_raw = await _query_cli(prompt)
|
||||||
|
source_name = _CLI_BACKEND
|
||||||
|
else:
|
||||||
|
llm_raw = await _query_ollama(prompt)
|
||||||
|
source_name = MODEL_NAME
|
||||||
|
if llm_raw:
|
||||||
|
sanitized = _sanitize_llm_output(llm_raw, base_level, base_predictions)
|
||||||
|
sanitized["source"] = source_name
|
||||||
|
sanitized["generated_at"] = datetime.now(timezone.utc).isoformat()
|
||||||
|
sanitized["key_drivers"] = drivers
|
||||||
|
sanitized["regional_briefs"] = regional
|
||||||
|
sanitized["correlations"] = corr
|
||||||
|
sanitized["watch_items"] = watch
|
||||||
|
return sanitized
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"[AI] LLM path failed ({_CLI_BACKEND}), fallback engaged: {exc}")
|
||||||
|
|
||||||
|
summary = f"{base_level} risk posture based on {len(news_items)} intelligence items"
|
||||||
|
if conflicts:
|
||||||
|
summary += f" + {len(conflicts)} military events"
|
||||||
|
if reasons:
|
||||||
|
summary += f"; key drivers: {', '.join(reasons[:4])}."
|
||||||
|
else:
|
||||||
|
summary += "."
|
||||||
|
|
||||||
|
return {
|
||||||
|
"summary": summary[:220],
|
||||||
|
"threat_level": base_level,
|
||||||
|
"predictions": base_predictions,
|
||||||
|
"key_drivers": drivers,
|
||||||
|
"regional_briefs": regional,
|
||||||
|
"correlations": corr,
|
||||||
|
"watch_items": watch,
|
||||||
|
"source": "heuristic",
|
||||||
|
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
TEST = [{"title": "Missile strike reported near border region"}, {"title": "Cyberattack impacts telecom routing"}]
|
||||||
|
print(asyncio.run(analyze_threat(TEST, gps_data=[{"lat": 32.1, "lon": 35.0}], bgp_data={"status": "ELEVATED"})))
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
import asyncio
|
||||||
|
import gzip
|
||||||
|
import json
|
||||||
|
import websockets
|
||||||
|
import httpx
|
||||||
|
import os
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
AIS_STREAM_URL = "wss://stream.aisstream.io/v1/stream"
|
||||||
|
API_KEY = os.getenv("AIS_STREAM_KEY", "185a6c61223ce81ed70cec58eab359009ebd5a0e")
|
||||||
|
|
||||||
|
_real_vessel_cache: dict = {}
|
||||||
|
_real_vessel_lock = asyncio.Lock()
|
||||||
|
|
||||||
|
def get_vessel_category(ship_type):
|
||||||
|
if not ship_type: return "Other"
|
||||||
|
if 70 <= ship_type <= 79: return "Cargo"
|
||||||
|
if 80 <= ship_type <= 89: return "Tanker"
|
||||||
|
if 60 <= ship_type <= 69: return "Passenger"
|
||||||
|
if ship_type == 35: return "Military"
|
||||||
|
if 30 <= ship_type <= 34: return "Fishing"
|
||||||
|
return "Other"
|
||||||
|
|
||||||
|
async def ais_worker():
|
||||||
|
if not API_KEY:
|
||||||
|
print("[MARITIME] Missing API Key.")
|
||||||
|
return
|
||||||
|
print("[MARITIME] Starting Global AIS Stream...")
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
async with websockets.connect(AIS_STREAM_URL, ping_interval=20, ping_timeout=20) as ws:
|
||||||
|
await ws.send(json.dumps({
|
||||||
|
"APIKey": API_KEY,
|
||||||
|
"BoundingBoxes": [[[-90, -180], [90, 180]]],
|
||||||
|
"FilterMessageTypes": ["PositionReport"]
|
||||||
|
}))
|
||||||
|
msg_count = 0
|
||||||
|
async for message in ws:
|
||||||
|
msg = json.loads(message)
|
||||||
|
meta = msg.get("MetaData", {})
|
||||||
|
mmsi = meta.get("MMSI")
|
||||||
|
if not mmsi:
|
||||||
|
continue
|
||||||
|
async with _real_vessel_lock:
|
||||||
|
mid = str(mmsi)
|
||||||
|
if mid not in _real_vessel_cache:
|
||||||
|
_real_vessel_cache[mid] = {"id": mid, "source": "aisstream"}
|
||||||
|
v = _real_vessel_cache[mid]
|
||||||
|
v["last_seen"] = datetime.now(timezone.utc)
|
||||||
|
name = meta.get("ShipName", "").strip()
|
||||||
|
if name:
|
||||||
|
v["name"] = name
|
||||||
|
elif "name" not in v:
|
||||||
|
v["name"] = f"MMSI-{mmsi}"
|
||||||
|
ship_type = meta.get("ShipType", 0)
|
||||||
|
v["type"] = get_vessel_category(ship_type)
|
||||||
|
if msg["MessageType"] == "PositionReport":
|
||||||
|
pos = msg["Message"]["PositionReport"]
|
||||||
|
lat = pos.get("Latitude")
|
||||||
|
lon = pos.get("Longitude")
|
||||||
|
# Filter invalid coordinates (land-based noise, 0/0, out of range)
|
||||||
|
if (lat is None or lon is None
|
||||||
|
or abs(lat) > 90 or abs(lon) > 180
|
||||||
|
or (abs(lat) < 0.1 and abs(lon) < 0.1)):
|
||||||
|
continue
|
||||||
|
v["lat"] = round(lat, 5)
|
||||||
|
v["lon"] = round(lon, 5)
|
||||||
|
v["velocity"] = round(pos.get("Sog", 0) * 0.5144, 2)
|
||||||
|
v["heading"] = pos.get("Cog", 0)
|
||||||
|
msg_count += 1
|
||||||
|
if msg_count % 500 == 0:
|
||||||
|
print(f"[MARITIME] {len(_real_vessel_cache)} vessels cached")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[MARITIME] Stream error: {e}. Reconnecting in 10s...")
|
||||||
|
await asyncio.sleep(10)
|
||||||
|
|
||||||
|
|
||||||
|
async def ais_pruner():
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(60)
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
async with _real_vessel_lock:
|
||||||
|
stale = [k for k, v in _real_vessel_cache.items()
|
||||||
|
if (now - v["last_seen"]).total_seconds() > 1200]
|
||||||
|
for k in stale:
|
||||||
|
del _real_vessel_cache[k]
|
||||||
|
|
||||||
|
|
||||||
|
async def _fetch_digitraffic() -> list:
|
||||||
|
"""
|
||||||
|
Digitraffic AIS — free, no key, Baltic/European coverage.
|
||||||
|
Uses `from` parameter to get only vessels seen in the last 20 minutes.
|
||||||
|
Typically returns 300–700 active vessels with fresh positions.
|
||||||
|
"""
|
||||||
|
import time
|
||||||
|
try:
|
||||||
|
from_ms = int((time.time() - 1200) * 1000) # last 20 minutes
|
||||||
|
headers = {
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Accept-Encoding": "gzip",
|
||||||
|
"Digitraffic-User": "GodsEye/3.0",
|
||||||
|
}
|
||||||
|
async with httpx.AsyncClient(timeout=15.0) as client:
|
||||||
|
r = await client.get(
|
||||||
|
f"https://meri.digitraffic.fi/api/ais/v1/locations?from={from_ms}",
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
if r.status_code != 200:
|
||||||
|
print(f"[MARITIME] Digitraffic {r.status_code}")
|
||||||
|
return []
|
||||||
|
raw = r.content
|
||||||
|
try:
|
||||||
|
data = json.loads(gzip.decompress(raw))
|
||||||
|
except Exception:
|
||||||
|
data = r.json()
|
||||||
|
|
||||||
|
features = data.get("features", []) if isinstance(data, dict) else []
|
||||||
|
ships = []
|
||||||
|
for item in features:
|
||||||
|
geo = (item.get("geometry") or {}).get("coordinates", [])
|
||||||
|
if len(geo) < 2:
|
||||||
|
continue
|
||||||
|
lon, lat = geo[0], geo[1]
|
||||||
|
if abs(lat) > 90 or abs(lon) > 180 or (abs(lat) < 0.01 and abs(lon) < 0.01):
|
||||||
|
continue
|
||||||
|
props = item.get("properties", {})
|
||||||
|
mmsi = str(item.get("mmsi") or props.get("mmsi", ""))
|
||||||
|
sog = props.get("sog", 0) or 0
|
||||||
|
heading = props.get("heading", 0) or props.get("cog", 0) or 0
|
||||||
|
ships.append({
|
||||||
|
"id": f"dt_{mmsi}",
|
||||||
|
"name": f"MMSI-{mmsi}",
|
||||||
|
"lat": round(lat, 5),
|
||||||
|
"lon": round(lon, 5),
|
||||||
|
"velocity": round(sog * 0.5144, 2),
|
||||||
|
"heading": heading,
|
||||||
|
"type": "Cargo",
|
||||||
|
"source": "digitraffic",
|
||||||
|
})
|
||||||
|
print(f"[MARITIME] Digitraffic: {len(ships)} vessels (last 20min)")
|
||||||
|
return ships
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[MARITIME] Digitraffic error: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
_dt_cache: list = []
|
||||||
|
_dt_cache_time: float = 0.0
|
||||||
|
_DT_TTL = 300.0 # refresh Digitraffic every 5 minutes
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_ships() -> list:
|
||||||
|
"""Merge AISStream (global, real-time) + Digitraffic (Baltic, 5-min cache)."""
|
||||||
|
import time
|
||||||
|
global _dt_cache, _dt_cache_time
|
||||||
|
|
||||||
|
# Refresh Digitraffic cache if stale
|
||||||
|
if time.monotonic() - _dt_cache_time > _DT_TTL:
|
||||||
|
_dt_cache = await _fetch_digitraffic()
|
||||||
|
_dt_cache_time = time.monotonic()
|
||||||
|
|
||||||
|
async with _real_vessel_lock:
|
||||||
|
stream_ships = [
|
||||||
|
v for v in _real_vessel_cache.values()
|
||||||
|
if v.get("lat") is not None and v.get("lon") is not None
|
||||||
|
]
|
||||||
|
|
||||||
|
# Merge: AISStream takes priority, Digitraffic fills in the rest
|
||||||
|
if stream_ships:
|
||||||
|
stream_ids = {s["id"] for s in stream_ships}
|
||||||
|
dt_extra = [s for s in _dt_cache if s["id"] not in stream_ids]
|
||||||
|
return stream_ships + dt_extra
|
||||||
|
|
||||||
|
return _dt_cache
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import httpx
|
||||||
|
import asyncio
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
# RIPE Stat - free API, no key required
|
||||||
|
# Fetch BGP update activity (withdrawals/announcements) for a given time window
|
||||||
|
RIPE_UPDATES_URL = "https://stat.ripe.net/data/bgp-updates/data.json"
|
||||||
|
RIPE_ROUTING_URL = "https://stat.ripe.net/data/routing-status/data.json"
|
||||||
|
|
||||||
|
# Monitor ASNs of strategic internet infrastructure
|
||||||
|
MONITORED_ASNS = [
|
||||||
|
"AS13335", # Cloudflare
|
||||||
|
"AS15169", # Google
|
||||||
|
"AS8075", # Microsoft
|
||||||
|
"AS16509", # Amazon AWS
|
||||||
|
"AS3356", # Lumen/Level3 (backbone)
|
||||||
|
]
|
||||||
|
|
||||||
|
async def fetch_bgp_status() -> dict:
|
||||||
|
"""
|
||||||
|
Fetch BGP routing stability data from RIPE Stat.
|
||||||
|
Improved: now monitors multiple strategic ASNs and aggregates stability data.
|
||||||
|
"""
|
||||||
|
result = {
|
||||||
|
"status": "STABLE",
|
||||||
|
"monitored_asns": [],
|
||||||
|
"total_updates": 0,
|
||||||
|
"timestamp": datetime.now(timezone.utc).isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
# Fetch routing status for a major backbone provider
|
||||||
|
response = await client.get(
|
||||||
|
RIPE_ROUTING_URL,
|
||||||
|
params={"resource": "1.1.1.1"},
|
||||||
|
timeout=10.0
|
||||||
|
)
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json().get("data", {})
|
||||||
|
result["routing_data"] = {
|
||||||
|
"prefixes_originated": data.get("prefixes_originated", []),
|
||||||
|
"visibility": data.get("visibility", {}),
|
||||||
|
"resource": data.get("resource", "1.1.1.1")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fetch recent BGP update counts for anomaly detection
|
||||||
|
updates_response = await client.get(
|
||||||
|
RIPE_UPDATES_URL,
|
||||||
|
params={"resource": "0.0.0.0/0", "hours": 1},
|
||||||
|
timeout=10.0
|
||||||
|
)
|
||||||
|
if updates_response.status_code == 200:
|
||||||
|
updates_data = updates_response.json().get("data", {})
|
||||||
|
nr_updates = updates_data.get("nr_updates", 0)
|
||||||
|
result["total_updates"] = nr_updates
|
||||||
|
# Flag elevated BGP activity (>5000 updates/hour is unusual)
|
||||||
|
if nr_updates > 20000:
|
||||||
|
result["status"] = "CRITICAL"
|
||||||
|
elif nr_updates > 10000:
|
||||||
|
result["status"] = "ELEVATED"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[BGP] Fetch error: {e}")
|
||||||
|
result["status"] = "OFFLINE"
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print(asyncio.run(fetch_bgp_status()))
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
import httpx
|
||||||
|
import asyncio
|
||||||
|
import re
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from difflib import SequenceMatcher
|
||||||
|
|
||||||
|
# GDELT DOC 2.0 API — free, no key
|
||||||
|
# Focused specifically on military attacks and kinetic events
|
||||||
|
GDELT_URL = (
|
||||||
|
"https://api.gdeltproject.org/api/v2/doc/doc?"
|
||||||
|
"query=(missile attack OR rocket attack OR airstrike OR air strike OR "
|
||||||
|
"bombing OR drone strike OR artillery fire OR shelling OR warship attack OR "
|
||||||
|
"naval attack OR military strike OR armed attack OR mortar OR explosion site OR "
|
||||||
|
"IED explosion OR sniper OR combat OR military offensive OR invasion force)"
|
||||||
|
"&mode=artlist&format=json&maxrecords=75&sourcelang=english×pan=12h"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Geo-coder: keyword → (lat, lon)
|
||||||
|
GEO_DATA = {
|
||||||
|
# Ukraine conflict zones
|
||||||
|
"Ukraine": [48.3794, 31.1656], "Crimea": [44.9521, 34.1024],
|
||||||
|
"Donbas": [48.0159, 37.8028], "Donbass": [48.0159, 37.8028],
|
||||||
|
"Kherson": [46.6354, 32.6169], "Zaporizhzhia": [47.8388, 35.1396],
|
||||||
|
"Kharkiv": [49.9935, 36.2304], "Kyiv": [50.4501, 30.5234],
|
||||||
|
"Kiev": [50.4501, 30.5234], "Odesa": [46.4825, 30.7233],
|
||||||
|
"Mariupol": [47.0958, 37.5483], "Bakhmut": [48.5963, 38.0000],
|
||||||
|
"Avdiivka": [48.1344, 37.7490],
|
||||||
|
# Russia
|
||||||
|
"Russia": [61.5240, 105.3188], "Moscow": [55.7558, 37.6173],
|
||||||
|
"Belarus": [53.7098, 27.9534],
|
||||||
|
# Middle East
|
||||||
|
"Israel": [31.0461, 34.8516], "Gaza": [31.3547, 34.3088],
|
||||||
|
"West Bank": [31.9466, 35.3027], "Rafah": [31.2969, 34.2455],
|
||||||
|
"Jenin": [32.4607, 35.3027], "Lebanon": [33.8547, 35.8623],
|
||||||
|
"Hezbollah": [33.8547, 35.8623], "Syria": [34.8021, 38.9968],
|
||||||
|
"Damascus": [33.5138, 36.2765], "Aleppo": [36.2021, 37.1343],
|
||||||
|
"Iran": [32.4279, 53.6880], "Tehran": [35.6892, 51.3890],
|
||||||
|
"Iraq": [33.2232, 43.6793], "Baghdad": [33.3152, 44.3661],
|
||||||
|
"Yemen": [15.5527, 48.5164], "Houthi": [15.5527, 48.5164],
|
||||||
|
"Saudi Arabia": [23.8859, 45.0792],
|
||||||
|
# Asia-Pacific
|
||||||
|
"China": [35.8617, 104.1954], "Taiwan": [23.6978, 120.9605],
|
||||||
|
"Taipei": [25.0330, 121.5654], "Taiwan Strait": [24.0000, 119.5000],
|
||||||
|
"North Korea": [40.3399, 127.5101], "Pyongyang": [39.0194, 125.7381],
|
||||||
|
"South Korea": [35.9078, 127.7669], "Japan": [36.2048, 138.2529],
|
||||||
|
"Philippines": [12.8797, 121.7740], "South China Sea": [12.0000, 113.0000],
|
||||||
|
"Myanmar": [21.9162, 95.9560], "India": [20.5937, 78.9629],
|
||||||
|
"Pakistan": [30.3753, 69.3451], "Afghanistan": [33.9391, 67.7100],
|
||||||
|
"Kashmir": [34.0837, 74.7973],
|
||||||
|
# Africa
|
||||||
|
"Sudan": [12.8628, 30.2176], "Ethiopia": [9.1450, 40.4897],
|
||||||
|
"Somalia": [5.1521, 46.1996], "Libya": [26.3351, 17.2283],
|
||||||
|
"Mali": [17.5707, -3.9962], "Niger": [17.6078, 8.0817],
|
||||||
|
"Nigeria": [9.0820, 8.6753], "Congo": [-4.0383, 21.7587],
|
||||||
|
"Sahel": [15.4542, 0.0000], "Burkina Faso": [12.2383, -1.5616],
|
||||||
|
"Mozambique": [-18.6657, 35.5296],
|
||||||
|
# Americas
|
||||||
|
"Venezuela": [6.4238, -66.5897], "Colombia": [4.5709, -74.2973],
|
||||||
|
"Mexico": [23.6345, -102.5528],
|
||||||
|
# Strategic waterways
|
||||||
|
"Red Sea": [20.0000, 38.0000], "Strait of Hormuz": [26.5667, 56.2500],
|
||||||
|
"Bab-el-Mandeb": [12.6, 43.5], "Suez Canal": [30.4550, 32.3500],
|
||||||
|
"Black Sea": [43.4000, 34.0000], "Baltic Sea": [58.0000, 20.0000],
|
||||||
|
"East China Sea": [30.0000, 126.0000], "Sea of Azov": [46.0000, 36.5000],
|
||||||
|
"Persian Gulf": [26.0000, 52.0000],
|
||||||
|
# Palestine
|
||||||
|
"Palestine": [31.9522, 35.2332], "Jerusalem": [31.7683, 35.2137],
|
||||||
|
"Tel Aviv": [32.0853, 34.7818], "Nablus": [32.2211, 35.2544],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Attack type classifier — ordered by priority (most specific first)
|
||||||
|
# Each entry: (search_term, event_label, severity)
|
||||||
|
ATTACK_CLASSIFIER = [
|
||||||
|
("ballistic missile", "BALLISTIC MISSILE", "CRITICAL"),
|
||||||
|
("cruise missile", "CRUISE MISSILE", "CRITICAL"),
|
||||||
|
("hypersonic missile", "HYPERSONIC MISSILE", "CRITICAL"),
|
||||||
|
("missile strike", "MISSILE STRIKE", "CRITICAL"),
|
||||||
|
("missile attack", "MISSILE ATTACK", "CRITICAL"),
|
||||||
|
("rocket attack", "ROCKET ATTACK", "CRITICAL"),
|
||||||
|
("rocket barrage", "ROCKET BARRAGE", "CRITICAL"),
|
||||||
|
("drone strike", "DRONE STRIKE", "CRITICAL"),
|
||||||
|
("drone attack", "DRONE ATTACK", "CRITICAL"),
|
||||||
|
("airstrike", "AIRSTRIKE", "CRITICAL"),
|
||||||
|
("air strike", "AIRSTRIKE", "CRITICAL"),
|
||||||
|
("air raid", "AIR RAID", "CRITICAL"),
|
||||||
|
("bombing", "BOMBING", "CRITICAL"),
|
||||||
|
("bomb", "EXPLOSION", "HIGH"),
|
||||||
|
("shelling", "ARTILLERY SHELLING", "HIGH"),
|
||||||
|
("artillery", "ARTILLERY FIRE", "HIGH"),
|
||||||
|
("mortar", "MORTAR ATTACK", "HIGH"),
|
||||||
|
("ied explosion", "IED EXPLOSION", "HIGH"),
|
||||||
|
("explosion", "EXPLOSION", "HIGH"),
|
||||||
|
("sniper", "SNIPER ACTIVITY", "MODERATE"),
|
||||||
|
("naval attack", "NAVAL ATTACK", "HIGH"),
|
||||||
|
("warship", "NAVAL ACTIVITY", "MODERATE"),
|
||||||
|
("invasion", "INVASION", "CRITICAL"),
|
||||||
|
("offensive", "MILITARY OFFENSIVE", "HIGH"),
|
||||||
|
("combat", "COMBAT", "HIGH"),
|
||||||
|
("attack", "ATTACK", "HIGH"),
|
||||||
|
("strike", "STRIKE", "HIGH"),
|
||||||
|
("troops", "GROUND FORCES", "MODERATE"),
|
||||||
|
("military", "MILITARY ACTIVITY", "MODERATE"),
|
||||||
|
]
|
||||||
|
|
||||||
|
SEVERITY_ORDER = {"CRITICAL": 0, "HIGH": 1, "MODERATE": 2}
|
||||||
|
|
||||||
|
EXCLUDE_KEYWORDS = [
|
||||||
|
"sport", "football", "soccer", "la liga", "cup", "olympics",
|
||||||
|
"tennis", "nfl", "nba", "golf", "cricket", "rugby",
|
||||||
|
"celebrity", "fashion", "movie", "film", "oscars", "grammy",
|
||||||
|
"recipe", "horoscope", "box office", "netflix",
|
||||||
|
"rocket launch", "spacex", "starship", "nasa launch", # civilian launches
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def classify_event(title: str) -> tuple[str, str]:
|
||||||
|
"""Returns (event_label, severity) for a conflict title."""
|
||||||
|
tl = title.lower()
|
||||||
|
for term, label, severity in ATTACK_CLASSIFIER:
|
||||||
|
if term in tl:
|
||||||
|
return label, severity
|
||||||
|
return "MILITARY EVENT", "MODERATE"
|
||||||
|
|
||||||
|
|
||||||
|
def _title_similarity(a: str, b: str) -> float:
|
||||||
|
return SequenceMatcher(None, a.lower(), b.lower()).ratio()
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_gdelt_date(seendate: str) -> str:
|
||||||
|
try:
|
||||||
|
dt = datetime.strptime(seendate, "%Y%m%dT%H%M%SZ")
|
||||||
|
return dt.replace(tzinfo=timezone.utc).isoformat()
|
||||||
|
except Exception:
|
||||||
|
return datetime.now(timezone.utc).isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_conflicts() -> list:
|
||||||
|
"""
|
||||||
|
Fetch military attack/conflict events from GDELT DOC API.
|
||||||
|
Returns events classified by attack type and severity.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
# Retry with backoff for 429 rate limits
|
||||||
|
for attempt in range(3):
|
||||||
|
if attempt > 0:
|
||||||
|
await asyncio.sleep(6 * attempt)
|
||||||
|
response = await client.get(GDELT_URL, timeout=12.0)
|
||||||
|
if response.status_code == 429:
|
||||||
|
print(f"[CONFLICTS] GDELT rate limited, retry {attempt+1}/3")
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
if response.status_code != 200:
|
||||||
|
print(f"[CONFLICTS] GDELT returned {response.status_code}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = response.json() if response.text.strip() else {}
|
||||||
|
except Exception:
|
||||||
|
print(f"[CONFLICTS] GDELT returned non-JSON body")
|
||||||
|
return []
|
||||||
|
raw_articles = data.get("articles", [])
|
||||||
|
if not raw_articles:
|
||||||
|
print("[CONFLICTS] No articles in GDELT response")
|
||||||
|
return []
|
||||||
|
|
||||||
|
events = []
|
||||||
|
|
||||||
|
for article in raw_articles:
|
||||||
|
title = (article.get("title") or "").strip()
|
||||||
|
if not title:
|
||||||
|
continue
|
||||||
|
|
||||||
|
tl = title.lower()
|
||||||
|
|
||||||
|
# Filter out non-conflict content
|
||||||
|
if any(kw in tl for kw in EXCLUDE_KEYWORDS):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Geocode
|
||||||
|
lat, lon = None, None
|
||||||
|
for region, coords in GEO_DATA.items():
|
||||||
|
if re.search(r'\b' + re.escape(region) + r'\b', title, re.IGNORECASE):
|
||||||
|
lat, lon = coords
|
||||||
|
break
|
||||||
|
|
||||||
|
if lat is None or lon is None:
|
||||||
|
continue
|
||||||
|
if not (-90 <= lat <= 90) or not (-180 <= lon <= 180):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Deduplicate by title similarity
|
||||||
|
is_duplicate = any(
|
||||||
|
_title_similarity(title, ev["title"]) > 0.75
|
||||||
|
for ev in events
|
||||||
|
)
|
||||||
|
if is_duplicate:
|
||||||
|
continue
|
||||||
|
|
||||||
|
event_label, severity = classify_event(title)
|
||||||
|
seendate = article.get("seendate", "")
|
||||||
|
published = _parse_gdelt_date(seendate) if seendate else datetime.now(timezone.utc).isoformat()
|
||||||
|
|
||||||
|
events.append({
|
||||||
|
"title": title,
|
||||||
|
"url": article.get("url", ""),
|
||||||
|
"image": article.get("urlMobileImage", ""),
|
||||||
|
"source": article.get("domain", "Unknown"),
|
||||||
|
"domain": article.get("sourcecountry", "Unknown"),
|
||||||
|
"lat": lat,
|
||||||
|
"lon": lon,
|
||||||
|
"published_at": published,
|
||||||
|
"event_type": event_label,
|
||||||
|
"severity": severity,
|
||||||
|
"type": "conflict",
|
||||||
|
})
|
||||||
|
|
||||||
|
# Sort: CRITICAL first, then by date
|
||||||
|
events.sort(key=lambda e: (
|
||||||
|
SEVERITY_ORDER.get(e["severity"], 3),
|
||||||
|
e["published_at"]
|
||||||
|
), reverse=False)
|
||||||
|
events.sort(key=lambda e: SEVERITY_ORDER.get(e["severity"], 3))
|
||||||
|
|
||||||
|
events = events[:40]
|
||||||
|
|
||||||
|
by_sev = {}
|
||||||
|
for e in events:
|
||||||
|
s = e["severity"]
|
||||||
|
by_sev[s] = by_sev.get(s, 0) + 1
|
||||||
|
|
||||||
|
print(f"[CONFLICTS] {len(events)} military events — {by_sev} (from {len(raw_articles)} raw)")
|
||||||
|
return events
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[CONFLICTS] Fetch error: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
result = asyncio.run(fetch_conflicts())
|
||||||
|
print(f"\nMilitary events: {len(result)}")
|
||||||
|
for ev in result[:15]:
|
||||||
|
print(f" [{ev['severity']:8s}] [{ev['event_type']:22s}] {ev['title'][:70]}")
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import httpx
|
||||||
|
import asyncio
|
||||||
|
import random
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
# Abuse.ch ThreatFox API - Real-time indicators of compromise
|
||||||
|
THREATFOX_URL = "https://threatfox-api.abuse.ch/api/v1/"
|
||||||
|
|
||||||
|
CACHE_FILE = Path(__file__).parent.parent / ".cache" / "cyber.json"
|
||||||
|
CACHE_DURATION_SEC = 300 # 5 minutes
|
||||||
|
|
||||||
|
def _get_cached_cyber():
|
||||||
|
if CACHE_FILE.exists() and (datetime.now().timestamp() - CACHE_FILE.stat().st_mtime) < CACHE_DURATION_SEC:
|
||||||
|
try:
|
||||||
|
with open(CACHE_FILE, "r") as f:
|
||||||
|
return json.load(f)
|
||||||
|
except: pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _save_cache(data):
|
||||||
|
CACHE_FILE.parent.mkdir(exist_ok=True, parents=True)
|
||||||
|
try:
|
||||||
|
with open(CACHE_FILE, "w") as f:
|
||||||
|
json.dump(data, f)
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
# Fallback geocoding for major infrastructure if IP geocoding fails or to show targets
|
||||||
|
CYBER_TARGETS = [
|
||||||
|
{"name": "AWS US-East", "lat": 39.04, "lon": -77.48},
|
||||||
|
{"name": "Google Cloud Europe", "lat": 50.11, "lon": 8.68},
|
||||||
|
{"name": "Azure East Asia", "lat": 22.28, "lon": 114.17},
|
||||||
|
{"name": "DE-CIX Frankfurt", "lat": 50.12, "lon": 8.67},
|
||||||
|
{"name": "London Internet Exchange", "lat": 51.51, "lon": -0.12},
|
||||||
|
{"name": "Equinix Ashburn", "lat": 39.03, "lon": -77.45},
|
||||||
|
]
|
||||||
|
|
||||||
|
async def _get_ip_geo(client, ip):
|
||||||
|
"""Real-time geocoding for malicious IPs using ip-api.com (free for non-commercial)."""
|
||||||
|
try:
|
||||||
|
# Rate limit is 45 requests per minute
|
||||||
|
resp = await client.get(f"http://ip-api.com/json/{ip}?fields=status,lat,lon,country", timeout=2.0)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
data = resp.json()
|
||||||
|
if data.get("status") == "success":
|
||||||
|
return data.get("lat"), data.get("lon"), data.get("country")
|
||||||
|
except: pass
|
||||||
|
return None, None, None
|
||||||
|
|
||||||
|
async def fetch_cyber_warfare(bgp_data=None):
|
||||||
|
"""
|
||||||
|
Fetches REAL cyber threat data from Abuse.ch ThreatFox.
|
||||||
|
Geocodes the malicious sources and maps them to critical infrastructure targets.
|
||||||
|
"""
|
||||||
|
cached = _get_cached_cyber()
|
||||||
|
if cached is not None:
|
||||||
|
return cached
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
# Query latest 20 malware/botnet indicators
|
||||||
|
payload = {"query": "get_iocs", "days": 1}
|
||||||
|
resp = await client.post(THREATFOX_URL, json=payload, timeout=10.0)
|
||||||
|
|
||||||
|
if resp.status_code != 200:
|
||||||
|
if CACHE_FILE.exists():
|
||||||
|
try:
|
||||||
|
with open(CACHE_FILE, "r") as f: return json.load(f)
|
||||||
|
except: pass
|
||||||
|
return []
|
||||||
|
|
||||||
|
data = resp.json()
|
||||||
|
if data.get("query_status") != "ok":
|
||||||
|
if CACHE_FILE.exists():
|
||||||
|
try:
|
||||||
|
with open(CACHE_FILE, "r") as f: return json.load(f)
|
||||||
|
except: pass
|
||||||
|
return []
|
||||||
|
|
||||||
|
iocs = data.get("data", [])
|
||||||
|
# Filter for IP addresses (IPv4)
|
||||||
|
ip_iocs = [i for i in iocs if i.get("threat_type") in ["botnet_cc", "payload_delivery"] and ":" not in i.get("ioc", "")]
|
||||||
|
|
||||||
|
attacks = []
|
||||||
|
# Geocode only a subset to respect ip-api limits
|
||||||
|
sample_size = min(len(ip_iocs), 8)
|
||||||
|
sampled = random.sample(ip_iocs, sample_size) if len(ip_iocs) > sample_size else ip_iocs
|
||||||
|
|
||||||
|
for ioc in sampled:
|
||||||
|
ip = ioc["ioc"].split(":")[0]
|
||||||
|
lat, lon, country = await _get_ip_geo(client, ip)
|
||||||
|
|
||||||
|
if lat and lon:
|
||||||
|
target = random.choice(CYBER_TARGETS)
|
||||||
|
intensity = random.uniform(0.4, 1.0)
|
||||||
|
|
||||||
|
attacks.append({
|
||||||
|
"id": f"real_cyb_{ioc['id']}",
|
||||||
|
"source_name": f"{ioc['threat_type'].upper()} ({country or 'Unknown'})",
|
||||||
|
"target_name": target["name"],
|
||||||
|
"startLat": lat,
|
||||||
|
"startLng": lon,
|
||||||
|
"endLat": target["lat"],
|
||||||
|
"endLng": target["lon"],
|
||||||
|
"type": ioc["malware_printable"] or "Unknown Malware",
|
||||||
|
"intensity": intensity,
|
||||||
|
"color": "#ff003c" if "botnet" in ioc["threat_type"] else "#ff8800",
|
||||||
|
"timestamp": ioc["first_seen"]
|
||||||
|
})
|
||||||
|
|
||||||
|
_save_cache(attacks)
|
||||||
|
return attacks
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[CYBER] Real data fetch error: {e}")
|
||||||
|
if CACHE_FILE.exists():
|
||||||
|
try:
|
||||||
|
with open(CACHE_FILE, "r") as f: return json.load(f)
|
||||||
|
except: pass
|
||||||
|
return []
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print(asyncio.run(fetch_cyber_warfare()))
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import httpx
|
||||||
|
import asyncio
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
# USGS Earthquake Hazards Program - completely free, no API key
|
||||||
|
# Significant earthquakes (M 4.5+) from the past 7 days
|
||||||
|
USGS_URL = "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/4.5_week.geojson"
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_earthquakes() -> list:
|
||||||
|
"""
|
||||||
|
Fetches real earthquake data from USGS.
|
||||||
|
Returns quakes with M >= 4.5 from the past week.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.get(USGS_URL, timeout=12.0)
|
||||||
|
if response.status_code != 200:
|
||||||
|
print(f"[SEISMIC] USGS returned {response.status_code}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
quakes = []
|
||||||
|
|
||||||
|
for feature in data.get("features", []):
|
||||||
|
props = feature.get("properties", {})
|
||||||
|
geom = feature.get("geometry", {})
|
||||||
|
coords = geom.get("coordinates", [])
|
||||||
|
|
||||||
|
if len(coords) < 2:
|
||||||
|
continue
|
||||||
|
|
||||||
|
lon, lat = coords[0], coords[1]
|
||||||
|
depth_km = coords[2] if len(coords) > 2 else 0
|
||||||
|
mag = props.get("mag")
|
||||||
|
|
||||||
|
if mag is None:
|
||||||
|
continue
|
||||||
|
# Validate coordinates
|
||||||
|
if not (-90 <= lat <= 90) or not (-180 <= lon <= 180):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Severity classification
|
||||||
|
if mag >= 7.0:
|
||||||
|
severity = "MAJOR"
|
||||||
|
elif mag >= 6.0:
|
||||||
|
severity = "STRONG"
|
||||||
|
elif mag >= 5.0:
|
||||||
|
severity = "MODERATE"
|
||||||
|
else:
|
||||||
|
severity = "MINOR"
|
||||||
|
|
||||||
|
# Convert USGS epoch ms to ISO string
|
||||||
|
epoch_ms = props.get("time", 0)
|
||||||
|
try:
|
||||||
|
dt = datetime.fromtimestamp(epoch_ms / 1000, tz=timezone.utc)
|
||||||
|
time_str = dt.strftime("%Y-%m-%d %H:%M UTC")
|
||||||
|
except Exception:
|
||||||
|
time_str = "Unknown"
|
||||||
|
|
||||||
|
quakes.append({
|
||||||
|
"id": feature.get("id", ""),
|
||||||
|
"title": props.get("title", "Earthquake"),
|
||||||
|
"place": props.get("place", "Unknown Location"),
|
||||||
|
"lat": lat,
|
||||||
|
"lon": lon,
|
||||||
|
"depth_km": round(depth_km, 1),
|
||||||
|
"magnitude": mag,
|
||||||
|
"severity": severity,
|
||||||
|
"time": time_str,
|
||||||
|
"url": props.get("url", ""),
|
||||||
|
"type": "earthquake",
|
||||||
|
"felt": props.get("felt", 0),
|
||||||
|
"tsunami": props.get("tsunami", 0),
|
||||||
|
})
|
||||||
|
|
||||||
|
# Sort by magnitude descending
|
||||||
|
quakes.sort(key=lambda q: q["magnitude"], reverse=True)
|
||||||
|
print(f"[SEISMIC] {len(quakes)} earthquakes (M≥4.5) — strongest: M{quakes[0]['magnitude'] if quakes else 'N/A'}")
|
||||||
|
return quakes
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[SEISMIC] Fetch error: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
result = asyncio.run(fetch_earthquakes())
|
||||||
|
print(f"Earthquakes: {len(result)}")
|
||||||
|
for q in result[:5]:
|
||||||
|
print(f" M{q['magnitude']} {q['severity']} — {q['place']}")
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import httpx
|
||||||
|
import asyncio
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
# Launch Library 2 API — completely free, no API key required
|
||||||
|
UPCOMING_URL = "https://ll.thespacedevs.com/2.2.0/launch/upcoming/?limit=25&format=json"
|
||||||
|
RECENT_URL = "https://ll.thespacedevs.com/2.2.0/launch/previous/?limit=10&format=json"
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_launch(item: dict) -> dict | None:
|
||||||
|
"""Parse a single launch object from the LL2 API response."""
|
||||||
|
pad = item.get("pad") or {}
|
||||||
|
location = pad.get("location") or {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
lat = float(pad.get("latitude", ""))
|
||||||
|
lon = float(pad.get("longitude", ""))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not (-90 <= lat <= 90) or not (-180 <= lon <= 180):
|
||||||
|
return None
|
||||||
|
|
||||||
|
provider = (item.get("launch_service_provider") or {}).get("name", "Unknown")
|
||||||
|
rocket = ((item.get("rocket") or {}).get("configuration") or {}).get("name", "Unknown")
|
||||||
|
mission = item.get("mission") or {}
|
||||||
|
status = (item.get("status") or {}).get("name", "Unknown")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": item.get("id", ""),
|
||||||
|
"name": item.get("name", "Unknown Launch"),
|
||||||
|
"status": status,
|
||||||
|
"net": item.get("net", ""),
|
||||||
|
"provider": provider,
|
||||||
|
"country": location.get("country_code", ""),
|
||||||
|
"pad": pad.get("name", "Unknown Pad"),
|
||||||
|
"lat": lat,
|
||||||
|
"lon": lon,
|
||||||
|
"image": item.get("image", ""),
|
||||||
|
"rocket": rocket,
|
||||||
|
"mission": mission.get("name") or "",
|
||||||
|
"mission_description": mission.get("description") or "",
|
||||||
|
"type": "launch",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_launches() -> list:
|
||||||
|
"""
|
||||||
|
Fetches real rocket launch data from Launch Library 2.
|
||||||
|
Returns combined upcoming + recent launches, sorted by date.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
upcoming_resp, recent_resp = await asyncio.gather(
|
||||||
|
client.get(UPCOMING_URL, timeout=15.0),
|
||||||
|
client.get(RECENT_URL, timeout=15.0),
|
||||||
|
)
|
||||||
|
|
||||||
|
upcoming = []
|
||||||
|
if upcoming_resp.status_code == 200:
|
||||||
|
for item in upcoming_resp.json().get("results", []):
|
||||||
|
parsed = _parse_launch(item)
|
||||||
|
if parsed:
|
||||||
|
upcoming.append(parsed)
|
||||||
|
else:
|
||||||
|
print(f"[LAUNCHES] Upcoming API returned {upcoming_resp.status_code}")
|
||||||
|
|
||||||
|
recent = []
|
||||||
|
if recent_resp.status_code == 200:
|
||||||
|
for item in recent_resp.json().get("results", []):
|
||||||
|
parsed = _parse_launch(item)
|
||||||
|
if parsed:
|
||||||
|
recent.append(parsed)
|
||||||
|
else:
|
||||||
|
print(f"[LAUNCHES] Recent API returned {recent_resp.status_code}")
|
||||||
|
|
||||||
|
combined = upcoming + recent
|
||||||
|
|
||||||
|
# Sort by NET date (earliest first), unknown dates go last
|
||||||
|
def sort_key(launch):
|
||||||
|
try:
|
||||||
|
return datetime.fromisoformat(launch["net"].replace("Z", "+00:00"))
|
||||||
|
except Exception:
|
||||||
|
return datetime.max.replace(tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
combined.sort(key=sort_key)
|
||||||
|
|
||||||
|
print(f"[LAUNCHES] {len(upcoming)} upcoming, {len(recent)} recent launches")
|
||||||
|
return combined
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[LAUNCHES] Fetch error: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
result = asyncio.run(fetch_launches())
|
||||||
|
print(f"Total launches: {len(result)}")
|
||||||
|
for launch in result[:5]:
|
||||||
|
print(f" {launch['net'][:16]} {launch['status']:15s} {launch['name']}")
|
||||||
@@ -0,0 +1,268 @@
|
|||||||
|
import httpx
|
||||||
|
import asyncio
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from xml.etree import ElementTree as ET
|
||||||
|
|
||||||
|
CACHE_FILE = Path(__file__).parent.parent / ".cache" / "news.json"
|
||||||
|
CACHE_DURATION_SEC = 300 # 5 minutes
|
||||||
|
|
||||||
|
def _get_cached_news():
|
||||||
|
if CACHE_FILE.exists() and (datetime.now().timestamp() - CACHE_FILE.stat().st_mtime) < CACHE_DURATION_SEC:
|
||||||
|
try:
|
||||||
|
with open(CACHE_FILE, "r") as f:
|
||||||
|
return json.load(f)
|
||||||
|
except: pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _save_cache(data):
|
||||||
|
CACHE_FILE.parent.mkdir(exist_ok=True, parents=True)
|
||||||
|
try:
|
||||||
|
with open(CACHE_FILE, "w") as f:
|
||||||
|
json.dump(data, f)
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
FEEDS = [
|
||||||
|
"http://www.aljazeera.com/xml/rss/all.xml",
|
||||||
|
"http://feeds.bbci.co.uk/news/world/rss.xml",
|
||||||
|
"https://www.reutersagency.com/feed/?best-topics=political-general&post_type=best",
|
||||||
|
"https://www.theguardian.com/world/rss",
|
||||||
|
"https://feeds.npr.org/1004/rss.xml",
|
||||||
|
"https://foreignpolicy.com/feed/",
|
||||||
|
"https://www.cnbc.com/id/100727362/device/rss/rss.html",
|
||||||
|
"https://rss.nytimes.com/services/xml/rss/nyt/World.xml",
|
||||||
|
"https://feeds.washingtonpost.com/rss/world",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Expanded keyword geocoder — covers most geopolitically relevant regions
|
||||||
|
GEO_DATA = {
|
||||||
|
# Middle East
|
||||||
|
"Iran": [32.4279, 53.6880], "Israel": [31.0461, 34.8516],
|
||||||
|
"Gaza": [31.3547, 34.3088], "West Bank": [31.9466, 35.3027],
|
||||||
|
"Lebanon": [33.8547, 35.8623], "Syria": [34.8021, 38.9968],
|
||||||
|
"Yemen": [15.5527, 48.5164], "Iraq": [33.2232, 43.6793],
|
||||||
|
"Saudi Arabia": [23.8859, 45.0792], "Jordan": [30.5852, 36.2384],
|
||||||
|
"Kuwait": [29.3117, 47.4818], "Qatar": [25.3548, 51.1839],
|
||||||
|
"UAE": [23.4241, 53.8478], "Bahrain": [26.0667, 50.5577],
|
||||||
|
"Oman": [21.5126, 55.9233],
|
||||||
|
# Europe
|
||||||
|
"Ukraine": [48.3794, 31.1656], "Russia": [61.5240, 105.3188],
|
||||||
|
"Germany": [51.1657, 10.4515], "France": [46.2276, 2.2137],
|
||||||
|
"UK": [55.3781, -3.4360], "Poland": [51.9194, 19.1451],
|
||||||
|
"Romania": [45.9432, 24.9668], "Finland": [61.9241, 25.7482],
|
||||||
|
"Sweden": [60.1282, 18.6435], "Norway": [60.4720, 8.4689],
|
||||||
|
"NATO": [50.8503, 4.3517], "Belarus": [53.7098, 27.9534],
|
||||||
|
"Moldova": [47.4116, 28.3699], "Georgia": [42.3154, 43.3569],
|
||||||
|
"Serbia": [44.0165, 20.9129], "Kosovo": [42.6026, 20.9030],
|
||||||
|
# Asia-Pacific
|
||||||
|
"China": [35.8617, 104.1954], "Taiwan": [23.6978, 120.9605],
|
||||||
|
"North Korea": [40.3399, 127.5101], "South Korea": [35.9078, 127.7669],
|
||||||
|
"Japan": [36.2048, 138.2529], "India": [20.5937, 78.9629],
|
||||||
|
"Pakistan": [30.3753, 69.3451], "Afghanistan": [33.9391, 67.7100],
|
||||||
|
"Myanmar": [21.9162, 95.9560], "Philippines": [12.8797, 121.7740],
|
||||||
|
"Vietnam": [14.0583, 108.2772], "South China Sea": [12.0000, 113.0000],
|
||||||
|
# Americas
|
||||||
|
"USA": [37.0902, -95.7129], "Mexico": [23.6345, -102.5528],
|
||||||
|
"Venezuela": [6.4238, -66.5897], "Colombia": [4.5709, -74.2973],
|
||||||
|
"Cuba": [21.5218, -77.7812], "Nicaragua": [12.8654, -85.2072],
|
||||||
|
"Haiti": [18.9712, -72.2852], "Brazil": [14.2350, -51.9253],
|
||||||
|
"Argentina": [-38.4161, -63.6167], "Chile": [-35.6751, -71.5430],
|
||||||
|
"Peru": [-9.1900, -75.0152], "Guyana": [4.8604, -58.9302],
|
||||||
|
# Central Asia & Caucasus
|
||||||
|
"Kazakhstan": [48.0196, 66.9237], "Azerbaijan": [40.1431, 47.5769],
|
||||||
|
"Armenia": [40.0691, 45.0382], "Nagorno-Karabakh": [39.8177, 46.7528],
|
||||||
|
"Uzbekistan": [41.3775, 64.5853], "Kyrgyzstan": [41.2044, 74.7661],
|
||||||
|
# Specific Conflict Regions & Strategic Spots
|
||||||
|
"Gaza": [31.3547, 34.3088], "West Bank": [31.9466, 35.3027],
|
||||||
|
"Donbas": [48.0159, 37.8028], "Kashmir": [34.0837, 74.7973],
|
||||||
|
"Sudan": [12.8628, 30.2176], "Darfur": [13.4175, 24.3311],
|
||||||
|
"Tigray": [14.0323, 38.3166], "Somalia": [5.1521, 46.1996],
|
||||||
|
"Suez Canal": [29.9329, 32.5539], "Panama Canal": [9.1012, -79.6967],
|
||||||
|
"Bering Strait": [66.0, -169.0], "Malacca": [2.5, 102.0],
|
||||||
|
# Cities
|
||||||
|
"New York": [40.7128, -74.0060], "London": [51.5074, -0.1278],
|
||||||
|
"Paris": [48.8566, 2.3522], "Brussels": [50.8503, 4.3517],
|
||||||
|
"Geneva": [46.2044, 6.1432], "Vienna": [48.2082, 16.3738],
|
||||||
|
"Istanbul": [41.0082, 28.9784], "Kyiv": [50.4501, 30.5234],
|
||||||
|
"Moscow": [55.7558, 37.6173], "Tehran": [35.6892, 51.3890],
|
||||||
|
"Beijing": [39.9042, 116.4074], "Tokyo": [35.6762, 139.6503],
|
||||||
|
"Seoul": [37.5665, 126.9780],
|
||||||
|
}
|
||||||
|
|
||||||
|
EXCLUDE_KEYWORDS = [
|
||||||
|
"sport", "football", "soccer", "la liga", "champions league", "cup", "match",
|
||||||
|
"olympics", "tennis", "nfl", "nba", "score", "goal", "premier league",
|
||||||
|
"formula 1", "f1", "golf", "cricket", "rugby", "boxing", "mma",
|
||||||
|
"celebrity", "oscars", "grammy", "fashion", "movie", "film", "series",
|
||||||
|
"recipe", "weather forecast", "horoscope"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Strip HTML tags from RSS descriptions
|
||||||
|
_TAG_RE = re.compile(r'<[^>]+>')
|
||||||
|
|
||||||
|
|
||||||
|
def _strip_html(s: str) -> str:
|
||||||
|
return _TAG_RE.sub('', s).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _find_text(el: ET.Element, tag: str) -> str:
|
||||||
|
"""Find text for a tag, checking common RSS/Atom namespaces."""
|
||||||
|
node = el.find(tag)
|
||||||
|
if node is not None and node.text:
|
||||||
|
return node.text.strip()
|
||||||
|
# Try with common namespaces
|
||||||
|
for ns in ['{http://purl.org/dc/elements/1.1/}', '{http://purl.org/rss/1.0/}']:
|
||||||
|
node = el.find(f'{ns}{tag}')
|
||||||
|
if node is not None and node.text:
|
||||||
|
return node.text.strip()
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
async def _fetch_single_feed(client: httpx.AsyncClient, feed_url: str) -> list[dict]:
|
||||||
|
"""Fetch a single RSS feed directly and parse XML items."""
|
||||||
|
articles: list[dict] = []
|
||||||
|
try:
|
||||||
|
resp = await client.get(feed_url, timeout=12.0, follow_redirects=True)
|
||||||
|
if resp.status_code != 200:
|
||||||
|
return []
|
||||||
|
|
||||||
|
root = ET.fromstring(resp.content)
|
||||||
|
# Determine feed title
|
||||||
|
channel = root.find('channel')
|
||||||
|
feed_title = 'Global Intel'
|
||||||
|
if channel is not None:
|
||||||
|
ft = channel.findtext('title')
|
||||||
|
if ft:
|
||||||
|
feed_title = ft.strip()
|
||||||
|
else:
|
||||||
|
# Atom feed
|
||||||
|
ft = root.findtext('{http://www.w3.org/2005/Atom}title')
|
||||||
|
if ft:
|
||||||
|
feed_title = ft.strip()
|
||||||
|
|
||||||
|
# Find items — RSS uses <item>, Atom uses <entry>
|
||||||
|
items = root.findall('.//item')
|
||||||
|
if not items:
|
||||||
|
items = root.findall('.//{http://www.w3.org/2005/Atom}entry')
|
||||||
|
|
||||||
|
for item in items[:10]:
|
||||||
|
title = _find_text(item, 'title')
|
||||||
|
if not title:
|
||||||
|
# Atom title
|
||||||
|
title = item.findtext('{http://www.w3.org/2005/Atom}title') or ''
|
||||||
|
title = title.strip()
|
||||||
|
if not title:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if any(kw in title.lower() for kw in EXCLUDE_KEYWORDS):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Geocode from keywords
|
||||||
|
lat, lon = None, None
|
||||||
|
for region, coords in GEO_DATA.items():
|
||||||
|
if re.search(r'\b' + re.escape(region) + r'\b', title, re.IGNORECASE):
|
||||||
|
lat, lon = coords
|
||||||
|
break
|
||||||
|
|
||||||
|
# Link
|
||||||
|
link = _find_text(item, 'link')
|
||||||
|
if not link:
|
||||||
|
link_el = item.find('{http://www.w3.org/2005/Atom}link')
|
||||||
|
if link_el is not None:
|
||||||
|
link = link_el.get('href', '')
|
||||||
|
|
||||||
|
# Description
|
||||||
|
desc = _find_text(item, 'description') or _find_text(item, 'summary')
|
||||||
|
if not desc:
|
||||||
|
desc = item.findtext('{http://www.w3.org/2005/Atom}summary') or ''
|
||||||
|
desc = _strip_html(desc)[:200]
|
||||||
|
|
||||||
|
# Publication date
|
||||||
|
pub_date = (
|
||||||
|
_find_text(item, 'pubDate')
|
||||||
|
or _find_text(item, 'published')
|
||||||
|
or item.findtext('{http://www.w3.org/2005/Atom}published')
|
||||||
|
or datetime.now(timezone.utc).isoformat()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Image from enclosure or media:content
|
||||||
|
image = ''
|
||||||
|
enc = item.find('enclosure')
|
||||||
|
if enc is not None:
|
||||||
|
enc_url = enc.get('url', '')
|
||||||
|
enc_type = enc.get('type', '')
|
||||||
|
if 'image' in enc_type or enc_url.lower().endswith(('.jpg', '.jpeg', '.png', '.gif', '.webp')):
|
||||||
|
image = enc_url
|
||||||
|
if not image:
|
||||||
|
media = item.find('{http://search.yahoo.com/mrss/}content')
|
||||||
|
if media is not None:
|
||||||
|
murl = media.get('url', '')
|
||||||
|
if murl.lower().endswith(('.jpg', '.jpeg', '.png', '.gif', '.webp')):
|
||||||
|
image = murl
|
||||||
|
|
||||||
|
# Determine Category and Severity
|
||||||
|
title_lower = title.lower()
|
||||||
|
category = "GEOPOLITICS"
|
||||||
|
if any(w in title_lower for w in ["cyber", "hacking", "breach", "malware", "botnet"]):
|
||||||
|
category = "CYBER"
|
||||||
|
elif any(w in title_lower for w in ["satellite", "orbit", "rocket", "launch", "space", "iss"]):
|
||||||
|
category = "SPACE"
|
||||||
|
elif any(w in title_lower for w in ["military", "army", "navy", "airforce", "missile", "strike", "war", "conflict", "nato", "defense"]):
|
||||||
|
category = "MILITARY"
|
||||||
|
|
||||||
|
severity = "MODERATE"
|
||||||
|
if any(w in title_lower for w in ["attack", "strike", "crisis", "invasion", "nuclear", "killed"]):
|
||||||
|
severity = "HIGH"
|
||||||
|
if any(w in title_lower for w in ["critical", "emergency", "declaration", "imminent"]):
|
||||||
|
severity = "CRITICAL"
|
||||||
|
|
||||||
|
articles.append({
|
||||||
|
"title": title,
|
||||||
|
"source": feed_title,
|
||||||
|
"url": link,
|
||||||
|
"image": image or None,
|
||||||
|
"lat": lat,
|
||||||
|
"lon": lon,
|
||||||
|
"summary": desc or "No details available.",
|
||||||
|
"published_at": pub_date,
|
||||||
|
"category": category,
|
||||||
|
"severity": severity
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[NEWS] Feed error ({feed_url[:60]}): {e}")
|
||||||
|
return articles
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_news():
|
||||||
|
"""Fetch all RSS feeds in parallel and return combined articles."""
|
||||||
|
cached = _get_cached_news()
|
||||||
|
if cached is not None:
|
||||||
|
return cached
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(
|
||||||
|
headers={"User-Agent": "GodsEye/2.0 RSS Reader"},
|
||||||
|
) as client:
|
||||||
|
results = await asyncio.gather(
|
||||||
|
*[_fetch_single_feed(client, url) for url in FEEDS],
|
||||||
|
return_exceptions=True,
|
||||||
|
)
|
||||||
|
articles: list[dict] = []
|
||||||
|
for r in results:
|
||||||
|
if isinstance(r, list):
|
||||||
|
articles.extend(r)
|
||||||
|
|
||||||
|
print(f"[NEWS] Fetched {len(articles)} intelligence items from {len(FEEDS)} feeds.")
|
||||||
|
_save_cache(articles)
|
||||||
|
return articles
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[NEWS] Critical error: {e}")
|
||||||
|
# fallback to stale cache if error
|
||||||
|
if CACHE_FILE.exists():
|
||||||
|
try:
|
||||||
|
with open(CACHE_FILE, "r") as f:
|
||||||
|
return json.load(f)
|
||||||
|
except: pass
|
||||||
|
return []
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
import httpx
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
ADSB_LOL_BASE = "https://api.adsb.lol/v2"
|
||||||
|
|
||||||
|
EMERGENCY_SQUAWKS = {
|
||||||
|
"7700": "GENERAL EMERGENCY",
|
||||||
|
"7600": "RADIO FAILURE",
|
||||||
|
"7500": "HIJACK / UNLAWFUL INTERFERENCE",
|
||||||
|
}
|
||||||
|
|
||||||
|
_HEADERS = {
|
||||||
|
"User-Agent": "Mozilla/5.0 (compatible; GodsEye/3.0)",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Regional civilian coverage queries: (lat, lon, radius_nm, label)
|
||||||
|
REGIONS = [
|
||||||
|
(45.0, -95.0, 2500, "N.America"),
|
||||||
|
(52.0, 8.0, 2000, "Europe"),
|
||||||
|
(25.0, 60.0, 2000, "MiddleEast"),
|
||||||
|
(35.0, 125.0, 2000, "E.Asia"),
|
||||||
|
( 5.0, 110.0, 2000, "SE.Asia"),
|
||||||
|
(-10.0, -40.0, 2500, "S.America+Africa"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_ac(ac: dict, military: bool = False) -> dict | None:
|
||||||
|
lat = ac.get("lat")
|
||||||
|
lon = ac.get("lon")
|
||||||
|
if lat is None or lon is None:
|
||||||
|
return None
|
||||||
|
alt_baro = ac.get("alt_baro", 0)
|
||||||
|
if alt_baro == "ground":
|
||||||
|
alt_m = 0
|
||||||
|
else:
|
||||||
|
alt_m = round(float(alt_baro) * 0.3048, 0) if isinstance(alt_baro, (int, float)) else 0
|
||||||
|
squawk = str(ac.get("squawk", "")).strip()
|
||||||
|
is_mil = military or bool(ac.get("mil")) or bool((ac.get("dbFlags", 0) or 0) & 1)
|
||||||
|
return {
|
||||||
|
"id": ac.get("hex", "UNKNOWN"),
|
||||||
|
"callsign": str(ac.get("flight", "")).strip() or ac.get("hex", "UNKNOWN"),
|
||||||
|
"country": ac.get("ownOp", ac.get("native", "")),
|
||||||
|
"lon": round(lon, 4),
|
||||||
|
"lat": round(lat, 4),
|
||||||
|
"alt": alt_m,
|
||||||
|
"velocity": round(float(ac.get("gs", 0) or 0) * 0.5144, 1),
|
||||||
|
"heading": round(float(ac.get("track", 0) or 0), 1),
|
||||||
|
"military": is_mil,
|
||||||
|
"squawk": squawk,
|
||||||
|
"_emergency": squawk in EMERGENCY_SQUAWKS,
|
||||||
|
"type": "plane",
|
||||||
|
# GPS quality fields for jamming detection
|
||||||
|
"nac_p": ac.get("nac_p"), # Navigation Accuracy Category (0–11, ≥9=normal)
|
||||||
|
"nic": ac.get("nic"), # Navigation Integrity Category (0=no integrity)
|
||||||
|
"sil": ac.get("sil"), # Source Integrity Level (0–3)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def _fetch_military(client: httpx.AsyncClient) -> list[dict]:
|
||||||
|
try:
|
||||||
|
r = await client.get(f"{ADSB_LOL_BASE}/mil", headers=_HEADERS, timeout=20.0)
|
||||||
|
if r.status_code != 200:
|
||||||
|
print(f"[AIRSPACE] /v2/mil returned {r.status_code}")
|
||||||
|
return []
|
||||||
|
ac_list = r.json().get("ac", [])
|
||||||
|
planes = []
|
||||||
|
for ac in ac_list:
|
||||||
|
p = _parse_ac(ac, military=True)
|
||||||
|
if p:
|
||||||
|
planes.append(p)
|
||||||
|
print(f"[AIRSPACE] Military: {len(planes)} aircraft")
|
||||||
|
return planes
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[AIRSPACE] Military fetch error: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
async def _fetch_region(client: httpx.AsyncClient, lat: float, lon: float, radius: int, label: str) -> list[dict]:
|
||||||
|
try:
|
||||||
|
url = f"{ADSB_LOL_BASE}/point/{lat}/{lon}/{radius}"
|
||||||
|
r = await client.get(url, headers=_HEADERS, timeout=20.0)
|
||||||
|
if r.status_code != 200:
|
||||||
|
return []
|
||||||
|
ac_list = r.json().get("ac", [])
|
||||||
|
planes = []
|
||||||
|
for ac in ac_list:
|
||||||
|
p = _parse_ac(ac)
|
||||||
|
if p:
|
||||||
|
planes.append(p)
|
||||||
|
return planes
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[AIRSPACE] Region {label} error: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_gps_interference(planes: list[dict]) -> list[dict]:
|
||||||
|
"""
|
||||||
|
Real GPS jamming detection from ADS-B navigation accuracy fields.
|
||||||
|
Same method as gpsjam.org: cluster aircraft with degraded NAC_P / NIC values.
|
||||||
|
|
||||||
|
nac_p (Navigation Accuracy Category for Position):
|
||||||
|
≥ 9 = normal GPS (HPU < 30m)
|
||||||
|
7 = HPU < 0.1 NM (~185m)
|
||||||
|
≤ 4 = HPU > 0.3 NM (~555m) → GPS quality degraded, likely jamming
|
||||||
|
0 = HPU unknown / no fix
|
||||||
|
|
||||||
|
nic (Navigation Integrity Category):
|
||||||
|
0 = no integrity assurance → strong spoofing/jamming indicator
|
||||||
|
≥ 7 = normal
|
||||||
|
"""
|
||||||
|
# Grid cell size in degrees
|
||||||
|
CELL = 2.0
|
||||||
|
MIN_ANOMALOUS = 3 # need at least 3 aircraft showing anomalies per cell
|
||||||
|
|
||||||
|
cell_counts: dict[tuple, dict] = {}
|
||||||
|
|
||||||
|
for p in planes:
|
||||||
|
nac = p.get("nac_p")
|
||||||
|
nic = p.get("nic")
|
||||||
|
lat, lon = p.get("lat"), p.get("lon")
|
||||||
|
if lat is None or lon is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Detect GPS-degraded aircraft
|
||||||
|
gps_degraded = (
|
||||||
|
(nac is not None and nac <= 4) or
|
||||||
|
(nic is not None and nic == 0 and nac is not None and nac < 9)
|
||||||
|
)
|
||||||
|
if not gps_degraded:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Snap to grid cell
|
||||||
|
cell = (round(lat / CELL) * CELL, round(lon / CELL) * CELL)
|
||||||
|
if cell not in cell_counts:
|
||||||
|
cell_counts[cell] = {"count": 0, "lats": [], "lons": [], "min_nac": 99}
|
||||||
|
cell_counts[cell]["count"] += 1
|
||||||
|
cell_counts[cell]["lats"].append(lat)
|
||||||
|
cell_counts[cell]["lons"].append(lon)
|
||||||
|
if nac is not None:
|
||||||
|
cell_counts[cell]["min_nac"] = min(cell_counts[cell]["min_nac"], nac)
|
||||||
|
|
||||||
|
zones = []
|
||||||
|
for (clat, clon), info in cell_counts.items():
|
||||||
|
if info["count"] < MIN_ANOMALOUS:
|
||||||
|
continue
|
||||||
|
count = info["count"]
|
||||||
|
min_nac = info["min_nac"]
|
||||||
|
intensity = (
|
||||||
|
"CRITICAL" if count >= 10 or min_nac == 0 else
|
||||||
|
"HIGH" if count >= 5 or min_nac <= 2 else
|
||||||
|
"ACTIVE"
|
||||||
|
)
|
||||||
|
# Use centroid of affected aircraft for more accurate placement
|
||||||
|
center_lat = sum(info["lats"]) / len(info["lats"])
|
||||||
|
center_lon = sum(info["lons"]) / len(info["lons"])
|
||||||
|
zones.append({
|
||||||
|
"lat": round(center_lat, 2),
|
||||||
|
"lon": round(center_lon, 2),
|
||||||
|
"intensity": intensity,
|
||||||
|
"aircraft_count": count,
|
||||||
|
"min_nac_p": min_nac if min_nac < 99 else None,
|
||||||
|
"source": "ADS-B NAC/NIC",
|
||||||
|
})
|
||||||
|
|
||||||
|
if zones:
|
||||||
|
print(f"[GPS] {len(zones)} jamming zones detected from {sum(z['aircraft_count'] for z in zones)} anomalous aircraft")
|
||||||
|
return zones
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_planes() -> dict:
|
||||||
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||||
|
# Military first, then all regions in parallel
|
||||||
|
tasks = [_fetch_military(client)] + [
|
||||||
|
_fetch_region(client, lat, lon, r, label)
|
||||||
|
for lat, lon, r, label in REGIONS
|
||||||
|
]
|
||||||
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
|
|
||||||
|
mil_planes = results[0] if isinstance(results[0], list) else []
|
||||||
|
mil_ids = {p["id"] for p in mil_planes}
|
||||||
|
|
||||||
|
# Merge regional civilian results, deduplicate
|
||||||
|
seen: set[str] = set(mil_ids)
|
||||||
|
civilian: list[dict] = []
|
||||||
|
for region_result in results[1:]:
|
||||||
|
if not isinstance(region_result, list):
|
||||||
|
continue
|
||||||
|
for p in region_result:
|
||||||
|
if p["id"] not in seen:
|
||||||
|
seen.add(p["id"])
|
||||||
|
civilian.append(p)
|
||||||
|
|
||||||
|
emergencies = [p for p in civilian if p.pop("_emergency", False)]
|
||||||
|
for p in mil_planes:
|
||||||
|
p.pop("_emergency", None)
|
||||||
|
regular = [p for p in civilian if not p.get("_emergency")]
|
||||||
|
|
||||||
|
final = mil_planes + emergencies + regular
|
||||||
|
interference = _compute_gps_interference(final)
|
||||||
|
print(f"[AIRSPACE] Synced {len(final)} aircraft ({len(mil_planes)} mil, {len(emergencies)} emergency) | {len(interference)} GPS zones")
|
||||||
|
return {"planes": final, "interference": interference, "emergencies": emergencies}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import json
|
||||||
|
result = asyncio.run(fetch_planes())
|
||||||
|
print(f"Total: {len(result['planes'])}, Military: {sum(1 for p in result['planes'] if p['military'])}")
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
from skyfield.api import load
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
SATELLITE_GROUPS = {
|
||||||
|
"military": "https://celestrak.org/NORAD/elements/gp.php?GROUP=military&FORMAT=tle",
|
||||||
|
"stations": "https://celestrak.org/NORAD/elements/gp.php?GROUP=stations&FORMAT=tle",
|
||||||
|
"navigation": "https://celestrak.org/NORAD/elements/gp.php?GROUP=gnss&FORMAT=tle",
|
||||||
|
}
|
||||||
|
|
||||||
|
GROUP_LIMITS = {
|
||||||
|
"military": 800,
|
||||||
|
"stations": 50,
|
||||||
|
"navigation": 120,
|
||||||
|
}
|
||||||
|
|
||||||
|
satellites_data: dict = {}
|
||||||
|
|
||||||
|
|
||||||
|
def load_satellites():
|
||||||
|
"""
|
||||||
|
Blocking I/O: loads TLE data from Celestrak.
|
||||||
|
Must be called via asyncio.to_thread to avoid blocking the event loop.
|
||||||
|
"""
|
||||||
|
global satellites_data
|
||||||
|
try:
|
||||||
|
ts = load.timescale()
|
||||||
|
loaded = {}
|
||||||
|
for group, url in SATELLITE_GROUPS.items():
|
||||||
|
try:
|
||||||
|
filename = f"{group}.tle"
|
||||||
|
sats = load.tle_file(url, filename=filename)
|
||||||
|
loaded[group] = sats
|
||||||
|
print(f"[SPACE] {group}: {len(sats)} satellites loaded.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[SPACE] Failed to load {group}: {e}")
|
||||||
|
satellites_data = {**loaded, "ts": ts}
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[SPACE] Critical load failure: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_positions_sync(sats: list, category: str, t, limit: int) -> list:
|
||||||
|
"""
|
||||||
|
CPU-bound position computation — runs in a thread executor.
|
||||||
|
Returns dicts, does NOT mutate shared state.
|
||||||
|
"""
|
||||||
|
positions = []
|
||||||
|
for sat in sats[:limit]:
|
||||||
|
try:
|
||||||
|
geocentric = sat.at(t)
|
||||||
|
subpoint = geocentric.subpoint()
|
||||||
|
lat = subpoint.latitude.degrees
|
||||||
|
lon = subpoint.longitude.degrees
|
||||||
|
alt = subpoint.elevation.m
|
||||||
|
|
||||||
|
if not (-90 <= lat <= 90) or not (-180 <= lon <= 180):
|
||||||
|
continue
|
||||||
|
|
||||||
|
positions.append({
|
||||||
|
"id": sat.model.satnum,
|
||||||
|
"name": sat.name,
|
||||||
|
"lat": lat,
|
||||||
|
"lon": lon,
|
||||||
|
"alt": alt,
|
||||||
|
"category": category,
|
||||||
|
"type": "satellite"
|
||||||
|
})
|
||||||
|
except Exception:
|
||||||
|
pass # skip satellites with bad TLE data
|
||||||
|
return positions
|
||||||
|
|
||||||
|
|
||||||
|
async def get_satellite_positions() -> list:
|
||||||
|
if "ts" not in satellites_data:
|
||||||
|
# First run: load everything in a background thread
|
||||||
|
await asyncio.to_thread(load_satellites)
|
||||||
|
|
||||||
|
if "ts" not in satellites_data:
|
||||||
|
print("[SPACE] Satellite data unavailable.")
|
||||||
|
return []
|
||||||
|
|
||||||
|
t = satellites_data["ts"].now()
|
||||||
|
all_positions = []
|
||||||
|
|
||||||
|
for group, limit in GROUP_LIMITS.items():
|
||||||
|
if group in satellites_data:
|
||||||
|
result = await asyncio.to_thread(
|
||||||
|
_compute_positions_sync,
|
||||||
|
satellites_data[group],
|
||||||
|
group,
|
||||||
|
t,
|
||||||
|
limit
|
||||||
|
)
|
||||||
|
all_positions.extend(result)
|
||||||
|
|
||||||
|
print(f"[SPACE] {len(all_positions)} satellite positions computed.")
|
||||||
|
return all_positions
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
load_satellites()
|
||||||
|
positions = asyncio.run(get_satellite_positions())
|
||||||
|
print(f"Total: {len(positions)}")
|
||||||
|
from collections import Counter
|
||||||
|
for cat, count in Counter(p["category"] for p in positions).items():
|
||||||
|
print(f" {cat}: {count}")
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import httpx
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
# NOAA Space Weather Prediction Center - free, no key
|
||||||
|
NOAA_KP_URL = "https://services.swpc.noaa.gov/products/noaa-planetary-k-index.json"
|
||||||
|
NOAA_ALERTS_URL = "https://services.swpc.noaa.gov/products/alerts.json"
|
||||||
|
NOAA_SOLAR_URL = "https://services.swpc.noaa.gov/json/solar-cycle/observed-solar-cycle-indices.json"
|
||||||
|
|
||||||
|
KP_STATUS = {
|
||||||
|
(0, 2): ("QUIET", "No significant geomagnetic activity."),
|
||||||
|
(2, 4): ("UNSETTLED", "Minor geomagnetic activity."),
|
||||||
|
(4, 5): ("ACTIVE", "Active geomagnetic conditions."),
|
||||||
|
(5, 6): ("STORM-G1", "Minor geomagnetic storm (G1)."),
|
||||||
|
(6, 7): ("STORM-G2", "Moderate geomagnetic storm (G2). HF radio disruption possible."),
|
||||||
|
(7, 8): ("STORM-G3", "Strong geomagnetic storm (G3). Possible power grid fluctuations."),
|
||||||
|
(8, 9): ("STORM-G4", "Severe geomagnetic storm (G4). Widespread power disruption."),
|
||||||
|
(9, 10): ("STORM-G5", "Extreme geomagnetic storm (G5). Grid collapse risk."),
|
||||||
|
}
|
||||||
|
|
||||||
|
def classify_kp(kp: float) -> tuple[str, str]:
|
||||||
|
for (lo, hi), (status, desc) in KP_STATUS.items():
|
||||||
|
if lo <= kp < hi:
|
||||||
|
return status, desc
|
||||||
|
return "STORM-G5", "Extreme geomagnetic storm."
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_space_weather() -> dict:
|
||||||
|
"""
|
||||||
|
Fetches real space weather data from NOAA SWPC.
|
||||||
|
- Planetary K-index (geomagnetic storm indicator)
|
||||||
|
- Active space weather alerts
|
||||||
|
"""
|
||||||
|
result = {
|
||||||
|
"kp_index": 0.0,
|
||||||
|
"status": "UNKNOWN",
|
||||||
|
"description": "Awaiting space weather data...",
|
||||||
|
"alerts": [],
|
||||||
|
"timestamp": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
# Fetch Kp index - returns list of [time_tag, Kp, ...]
|
||||||
|
kp_resp = await client.get(NOAA_KP_URL, timeout=10.0)
|
||||||
|
if kp_resp.status_code == 200:
|
||||||
|
kp_data = kp_resp.json()
|
||||||
|
# Skip header row [0], get latest reading from the end
|
||||||
|
if len(kp_data) > 1:
|
||||||
|
latest = kp_data[-1]
|
||||||
|
raw_kp = latest[1] if latest[1] not in ("-1", None, "") else "0"
|
||||||
|
kp = float(raw_kp)
|
||||||
|
status, description = classify_kp(kp)
|
||||||
|
result["kp_index"] = kp
|
||||||
|
result["status"] = status
|
||||||
|
result["description"] = description
|
||||||
|
result["timestamp"] = latest[0]
|
||||||
|
|
||||||
|
# Fetch active alerts
|
||||||
|
alerts_resp = await client.get(NOAA_ALERTS_URL, timeout=10.0)
|
||||||
|
if alerts_resp.status_code == 200:
|
||||||
|
alerts_raw = alerts_resp.json()
|
||||||
|
# Filter for active, non-cancelled alerts
|
||||||
|
active_alerts = []
|
||||||
|
for alert in (alerts_raw or [])[:10]:
|
||||||
|
msg = alert.get("message", "")
|
||||||
|
if "CANCEL" not in msg and "SUMMARY" not in msg:
|
||||||
|
# Extract first meaningful line as title
|
||||||
|
lines = [l.strip() for l in msg.split("\n") if l.strip()]
|
||||||
|
title = lines[0] if lines else "Space Weather Alert"
|
||||||
|
active_alerts.append({
|
||||||
|
"issue_time": alert.get("issue_datetime", ""),
|
||||||
|
"title": title[:100],
|
||||||
|
})
|
||||||
|
result["alerts"] = active_alerts[:5]
|
||||||
|
|
||||||
|
print(f"[SPACEWEATHER] Kp={result['kp_index']} — {result['status']} | {len(result['alerts'])} active alerts")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[SPACEWEATHER] Fetch error: {e}")
|
||||||
|
result["status"] = "OFFLINE"
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import json
|
||||||
|
data = asyncio.run(fetch_space_weather())
|
||||||
|
print(json.dumps(data, indent=2))
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import asyncio # kept for __main__ block
|
||||||
|
|
||||||
|
|
||||||
|
def _windy_embed(lat: float, lon: float) -> str:
|
||||||
|
"""Windy's official iframe-embeddable webcam map — always works."""
|
||||||
|
return (
|
||||||
|
f"https://embed.windy.com/embed.html"
|
||||||
|
f"?type=map&location=coordinates&metricRain=default&metricTemp=default"
|
||||||
|
f"&metricWind=default&zoom=12&overlay=webcams&product=ecmwf"
|
||||||
|
f"&level=surface&lat={lat}&lon={lon}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _windy(lat: float, lon: float) -> str:
|
||||||
|
return f"https://www.windy.com/-Webcams/webcams?{lat},{lon},12"
|
||||||
|
|
||||||
|
|
||||||
|
WEBCAM_SOURCES = [
|
||||||
|
# North America
|
||||||
|
{"id": "wc-nyc", "name": "Times Square, NYC", "lat": 40.7580, "lon": -73.9855},
|
||||||
|
{"id": "wc-miami", "name": "Miami Beach, Florida", "lat": 25.7617, "lon": -80.1918},
|
||||||
|
{"id": "wc-sf", "name": "San Francisco Bay", "lat": 37.8083, "lon": -122.4156},
|
||||||
|
{"id": "wc-dc", "name": "Washington DC", "lat": 38.8899, "lon": -77.0091},
|
||||||
|
# Europe
|
||||||
|
{"id": "wc-lon", "name": "Tower Bridge, London", "lat": 51.5055, "lon": -0.0754},
|
||||||
|
{"id": "wc-par", "name": "Eiffel Tower, Paris", "lat": 48.8584, "lon": 2.2945},
|
||||||
|
{"id": "wc-ber", "name": "Brandenburg Gate, Berlin", "lat": 52.5163, "lon": 13.3777},
|
||||||
|
{"id": "wc-rome", "name": "Colosseum, Rome", "lat": 41.8902, "lon": 12.4922},
|
||||||
|
{"id": "wc-barcelona", "name": "Barcelona Beach", "lat": 41.4036, "lon": 2.1744},
|
||||||
|
{"id": "wc-amsterdam", "name": "Dam Square, Amsterdam", "lat": 52.3731, "lon": 4.8932},
|
||||||
|
{"id": "wc-moscow", "name": "Moscow Kremlin View", "lat": 55.7520, "lon": 37.6175},
|
||||||
|
# Middle East & Africa
|
||||||
|
{"id": "wc-istanbul", "name": "Bosphorus, Istanbul", "lat": 41.0422, "lon": 29.0083},
|
||||||
|
{"id": "wc-jerusalem", "name": "Western Wall, Jerusalem", "lat": 31.7767, "lon": 35.2345},
|
||||||
|
{"id": "wc-dxb", "name": "Dubai Marina", "lat": 25.0800, "lon": 55.1400},
|
||||||
|
{"id": "wc-cairo", "name": "Pyramids of Giza, Cairo", "lat": 29.9792, "lon": 31.1342},
|
||||||
|
# Asia & Pacific
|
||||||
|
{"id": "wc-tok", "name": "Shibuya Crossing, Tokyo", "lat": 35.6595, "lon": 139.7001},
|
||||||
|
{"id": "wc-hk", "name": "Victoria Harbour, HK", "lat": 22.2855, "lon": 114.1577},
|
||||||
|
{"id": "wc-sin", "name": "Singapore Skyline", "lat": 1.2897, "lon": 103.8501},
|
||||||
|
{"id": "wc-syd", "name": "Sydney Opera House", "lat": -33.8568, "lon": 151.2153},
|
||||||
|
{"id": "wc-seoul", "name": "Seoul Skyline", "lat": 37.5665, "lon": 126.9780},
|
||||||
|
# Strategic / military-adjacent
|
||||||
|
{"id": "wc-gibraltar", "name": "Strait of Gibraltar", "lat": 36.1408, "lon": -5.3536},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_webcams() -> list:
|
||||||
|
webcams = [
|
||||||
|
{
|
||||||
|
"id": cam["id"],
|
||||||
|
"name": cam["name"],
|
||||||
|
"lat": cam["lat"],
|
||||||
|
"lon": cam["lon"],
|
||||||
|
"url": _windy(cam["lat"], cam["lon"]),
|
||||||
|
"embed_url": _windy_embed(cam["lat"], cam["lon"]),
|
||||||
|
"type": "webcam",
|
||||||
|
"status": "ONLINE",
|
||||||
|
"source": "Windy Webcams",
|
||||||
|
}
|
||||||
|
for cam in WEBCAM_SOURCES
|
||||||
|
]
|
||||||
|
print(f"[WEBCAMS] {len(webcams)} webcam locations loaded.")
|
||||||
|
return webcams
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
cams = asyncio.run(fetch_webcams())
|
||||||
|
print(f"Total webcam locations: {len(cams)}")
|
||||||
+29223
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,99 @@
|
|||||||
|
ISS (ZARYA)
|
||||||
|
1 25544U 98067A 26065.54469341 .00009332 00000+0 18044-3 0 9991
|
||||||
|
2 25544 51.6317 87.9051 0008117 163.3368 196.7888 15.48477653555846
|
||||||
|
POISK
|
||||||
|
1 36086U 09060A 26065.54469341 .00009332 00000+0 18044-3 0 9999
|
||||||
|
2 36086 51.6317 87.9051 0008117 163.3368 196.7888 15.48477653554444
|
||||||
|
CSS (TIANHE)
|
||||||
|
1 48274U 21035A 26065.83578792 .00025906 00000+0 30829-3 0 9990
|
||||||
|
2 48274 41.4663 225.5243 0006950 255.9346 104.0720 15.60495557277162
|
||||||
|
ISS (NAUKA)
|
||||||
|
1 49044U 21066A 26065.54469341 .00009332 00000+0 18044-3 0 9997
|
||||||
|
2 49044 51.6317 87.9051 0008117 163.3368 196.7888 15.48477653554453
|
||||||
|
FREGAT DEB
|
||||||
|
1 49271U 11037PF 26064.97574117 .00028561 00000+0 41642-1 0 9993
|
||||||
|
2 49271 51.6493 311.8987 0957104 50.8853 317.3235 12.39967329212230
|
||||||
|
CSS (WENTIAN)
|
||||||
|
1 53239U 22085A 26065.83578792 .00025906 00000+0 30829-3 0 9993
|
||||||
|
2 53239 41.4663 225.5243 0006950 255.9346 104.0720 15.60495557276843
|
||||||
|
CSS (MENGTIAN)
|
||||||
|
1 54216U 22143A 26065.83578792 .00025906 00000+0 30829-3 0 9994
|
||||||
|
2 54216 41.4663 225.5243 0006950 255.9346 104.0720 15.60495557276816
|
||||||
|
PROGRESS-MS 31
|
||||||
|
1 64751U 25146A 26065.54469341 .00009332 00000+0 18044-3 0 9992
|
||||||
|
2 64751 51.6317 87.9051 0008117 163.3368 196.7888 15.48477653554466
|
||||||
|
TIANZHOU-9
|
||||||
|
1 64786U 25149A 26065.83578792 .00025906 00000+0 30829-3 0 9996
|
||||||
|
2 64786 41.4663 225.5243 0006950 255.9346 104.0720 15.60495557276820
|
||||||
|
PROGRESS-MS 32
|
||||||
|
1 65586U 25204A 26065.54469341 .00009332 00000+0 18044-3 0 9994
|
||||||
|
2 65586 51.6317 87.9051 0008117 163.3368 196.7888 15.48477653554508
|
||||||
|
CYGNUS NG-23
|
||||||
|
1 65616U 25208A 26065.54469341 .00009332 00000+0 18044-3 0 9992
|
||||||
|
2 65616 51.6317 87.9051 0008117 163.3368 196.7888 15.48477653554513
|
||||||
|
ISS OBJECT XK
|
||||||
|
1 65731U 98067XK 26065.35324646 .01473506 15937-2 13181-2 0 9993
|
||||||
|
2 65731 51.6138 63.9395 0004334 87.4881 272.6630 16.12476132 26369
|
||||||
|
YOTSUBA-KULOVER
|
||||||
|
1 65941U 98067XN 26065.22636982 .00207091 00000+0 11219-2 0 9996
|
||||||
|
2 65941 51.6209 75.2348 0005861 46.1481 314.0003 15.78891358 22957
|
||||||
|
E-KAGAKU-1
|
||||||
|
1 65943U 98067XQ 26065.29341579 .00203884 00000+0 11184-2 0 9997
|
||||||
|
2 65943 51.6235 74.9503 0006176 51.7621 308.3935 15.78621403 22968
|
||||||
|
HRC MONOBLOCK CAMERA
|
||||||
|
1 66052U 98067XR 26065.37348952 .00054605 00000+0 62035-3 0 9997
|
||||||
|
2 66052 51.6273 82.2641 0003540 77.2259 282.9130 15.61775816 21872
|
||||||
|
HTV-X1
|
||||||
|
1 66174U 25241A 26065.54469341 .00009332 00000+0 18044-3 0 9999
|
||||||
|
2 66174 51.6317 87.9051 0008117 163.3368 196.7888 15.48477653554478
|
||||||
|
SZ-21 MODULE
|
||||||
|
1 66515U 25246C 26065.50767940 .00041950 00000+0 36494-3 0 9996
|
||||||
|
2 66515 41.4735 222.9439 0001433 292.6372 67.4317 15.68252686 17579
|
||||||
|
SHENZHOU-22
|
||||||
|
1 66645U 25272A 26065.83578792 .00025906 00000+0 30829-3 0 9999
|
||||||
|
2 66645 41.4663 225.5243 0006950 255.9346 104.0720 15.60495557268759
|
||||||
|
SOYUZ-MS 28
|
||||||
|
1 66664U 25275A 26065.54469341 .00009332 00000+0 18044-3 0 9990
|
||||||
|
2 66664 51.6317 87.9051 0008117 163.3368 196.7888 15.48477653554528
|
||||||
|
DUPLEX
|
||||||
|
1 66906U 98067XS 26065.43931271 .00032127 00000+0 45868-3 0 9994
|
||||||
|
2 66906 51.6289 85.7046 0002177 98.7594 261.3644 15.55966204 14621
|
||||||
|
ISS OBJECT XT
|
||||||
|
1 66907U 98067XT 26065.27003484 .00153072 00000+0 13312-2 0 9994
|
||||||
|
2 66907 51.6248 83.0631 0004674 56.5604 303.5839 15.68035860 14642
|
||||||
|
ISS OBJECT XU
|
||||||
|
1 66908U 98067XU 26065.33220126 .00152767 00000+0 13221-2 0 9995
|
||||||
|
2 66908 51.6249 82.7244 0004715 58.9313 301.2145 15.68155385 14650
|
||||||
|
SILVERSAT
|
||||||
|
1 66909U 98067XV 26065.27564600 .00189452 00000+0 14661-2 0 9997
|
||||||
|
2 66909 51.6242 82.3461 0005530 54.5981 305.5533 15.70734240 14654
|
||||||
|
ISS OBJECT XW
|
||||||
|
1 66910U 98067XW 26065.45484626 .00136808 00000+0 12172-2 0 9990
|
||||||
|
2 66910 51.6239 82.0383 0005009 67.5902 292.5625 15.67532901 14675
|
||||||
|
ISS OBJECT XX
|
||||||
|
1 66911U 98067XX 26065.40607185 .00240114 00000+0 16386-2 0 9990
|
||||||
|
2 66911 51.6211 80.9498 0007025 63.2110 296.9607 15.73534846 14679
|
||||||
|
ISS OBJECT XY
|
||||||
|
1 66912U 98067XY 26065.44936360 .00091647 00000+0 98520-3 0 9991
|
||||||
|
2 66912 51.6268 83.5178 0003032 70.8849 289.2473 15.62981143 14640
|
||||||
|
ISS OBJECT XZ
|
||||||
|
1 67683U 98067XZ 26065.44303726 .00040277 00000+0 67173-3 0 9994
|
||||||
|
2 67683 51.6308 87.9775 0009459 174.9626 185.1460 15.51523335 4282
|
||||||
|
GXIBA-1
|
||||||
|
1 67684U 98067YA 26065.35635560 .00076870 00000+0 11728-2 0 9991
|
||||||
|
2 67684 51.6296 88.1497 0010932 164.4811 195.6517 15.53698977 4276
|
||||||
|
CORAL
|
||||||
|
1 67685U 98067YB 26065.37492343 .00044090 00000+0 72551-3 0 9995
|
||||||
|
2 67685 51.6303 88.2731 0011331 171.0094 189.1100 15.51836923 4278
|
||||||
|
ISS OBJECT YC
|
||||||
|
1 67686U 98067YC 26065.37047129 .00051923 00000+0 83919-3 0 9997
|
||||||
|
2 67686 51.6305 88.2448 0007826 138.7374 221.4209 15.52303119 4278
|
||||||
|
LEOPARD
|
||||||
|
1 67687U 98067YD 26065.44105420 .00044085 00000+0 73033-3 0 9992
|
||||||
|
2 67687 51.6311 87.9659 0007779 143.5641 216.5880 15.51685674 4205
|
||||||
|
ISS OBJECT YE
|
||||||
|
1 67688U 98067YE 26065.37246683 .00047802 00000+0 77966-3 0 9991
|
||||||
|
2 67688 51.6307 88.2580 0007752 139.1868 220.9703 15.52085564 4195
|
||||||
|
CREW DRAGON 12
|
||||||
|
1 67796U 26031A 26065.54469341 .00009332 00000+0 18044-3 0 9998
|
||||||
|
2 67796 51.6317 87.9051 0008117 163.3368 196.7888 15.48477653555728
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Lead Software Architect / Tech Lead
|
||||||
|
|
||||||
|
## Rolle & Fokus
|
||||||
|
Der Lead Architect ist verantwortlich für die technische Gesamtvision des Projekts "God's Eye". Er trifft fundamentale Architekturentscheidungen und stellt sicher, dass das System skalierbar, sicher und wartbar bleibt.
|
||||||
|
|
||||||
|
## Kernkompetenzen
|
||||||
|
- **System-Design:** Microservices vs. Monolith, Event-Driven Architecture.
|
||||||
|
- **Technologie-Stack:** Auswahl der Frameworks (z.B. Go, Rust oder Node.js für Performance).
|
||||||
|
- **Security:** Security-by-Design Prinzipien.
|
||||||
|
|
||||||
|
## Verantwortlichkeiten
|
||||||
|
- Definition von Coding-Standards und Best Practices.
|
||||||
|
- Durchführung von High-Level Code Reviews.
|
||||||
|
- Mentoring der Senior Engineers.
|
||||||
|
- Schnittstelle zwischen Produkt-Management und Technik.
|
||||||
|
|
||||||
|
## Aktuelle Prioritäten
|
||||||
|
- [ ] Festlegung der Datenmodell-Architektur.
|
||||||
|
- [ ] Evaluierung der Cloud-Infrastruktur-Provider.
|
||||||
|
- [ ] Setup des initialen Boilerplates.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Senior Backend Engineer
|
||||||
|
|
||||||
|
## Rolle & Fokus
|
||||||
|
Der Backend Engineer kümmert sich um die "Schaltzentrale" von "God's Eye" – APIs, Geschäftslogik und Datenintegrität.
|
||||||
|
|
||||||
|
## Kernkompetenzen
|
||||||
|
- **API-Design:** RESTful, GraphQL, gRPC.
|
||||||
|
- **Datenbank-Management:** SQL vs. NoSQL, Datenmodellierung.
|
||||||
|
- **Security:** JWT-Handling, OAuth, Encryption.
|
||||||
|
- **Cloud-Services:** AWS, GCP oder Azure Services.
|
||||||
|
|
||||||
|
## Verantwortlichkeiten
|
||||||
|
- Entwicklung robuster und skalierbarer Server-Side Logik.
|
||||||
|
- Optimierung von Datenbankabfragen für hohe Performance.
|
||||||
|
- Integration von Drittanbieter-Services.
|
||||||
|
- Sicherstellung von Datensicherheit und Datenschutz (DSGVO).
|
||||||
|
|
||||||
|
## Aktuelle Prioritäten
|
||||||
|
- [ ] Implementierung der API-Dokumentation (Swagger/OpenAPI).
|
||||||
|
- [ ] Datenbankschema-Entwurf.
|
||||||
|
- [ ] Authentifizierungs-Service Setup.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Frontend Architect
|
||||||
|
|
||||||
|
## Rolle & Fokus
|
||||||
|
Der Frontend Architect ist zuständig für die User Experience und die visuelle Komponente von "God's Eye". Fokus auf intuitive Bedienung und hohe Performance im Browser.
|
||||||
|
|
||||||
|
## Kernkompetenzen
|
||||||
|
- **Frameworks:** React, Vue.js oder Angular.
|
||||||
|
- **Design-Systeme:** CSS-Architektur (TailwindCSS oder CSS-in-JS).
|
||||||
|
- **State-Management:** Redux, MobX oder React Query.
|
||||||
|
- **Performance:** Web-Performance-Optimierung (Core Web Vitals).
|
||||||
|
|
||||||
|
## Verantwortlichkeiten
|
||||||
|
- Design und Implementierung der User-Interface Komponenten.
|
||||||
|
- Sicherstellung der Barrierefreiheit (Accessibility).
|
||||||
|
- Optimierung für mobile Endgeräte (Responsive Design).
|
||||||
|
- Durchführung von UI/UX-Tests.
|
||||||
|
|
||||||
|
## Aktuelle Prioritäten
|
||||||
|
- [ ] Erstellung der initialen UI-Komponenten-Bibliothek.
|
||||||
|
- [ ] Definition der Frontend-Architektur (State-Management-Pattern).
|
||||||
|
- [ ] Setup von Storybook für Dokumentation.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# DevOps & SRE Specialist
|
||||||
|
|
||||||
|
## Rolle & Fokus
|
||||||
|
Der DevOps & SRE Specialist ist verantwortlich für die Verfügbarkeit, Skalierbarkeit und Sicherheit der Infrastruktur. "God's Eye" muss stabil unter Last laufen.
|
||||||
|
|
||||||
|
## Kernkompetenzen
|
||||||
|
- **IaC:** Terraform oder CloudFormation.
|
||||||
|
- **Container:** Kubernetes, Docker Swarm.
|
||||||
|
- **Observability:** Grafana, Prometheus, ELK-Stack.
|
||||||
|
- **Automation:** GitHub Actions, GitLab CI.
|
||||||
|
|
||||||
|
## Verantwortlichkeiten
|
||||||
|
- Automatisierung von Deployments und Builds (CI/CD).
|
||||||
|
- Skalierung der Infrastruktur nach Bedarf.
|
||||||
|
- Durchführung von Sicherheits-Audits der Infrastruktur.
|
||||||
|
- Bereitschaft (On-Call) für kritische Incidents.
|
||||||
|
|
||||||
|
## Aktuelle Prioritäten
|
||||||
|
- [ ] Konfiguration der Staging- und Produktions-Umgebungen.
|
||||||
|
- [ ] Implementierung der CI-Pipeline.
|
||||||
|
- [ ] Setup der Monitoring-Tools.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# QA & Test Automation Engineer
|
||||||
|
|
||||||
|
## Rolle & Fokus
|
||||||
|
Der QA Engineer ist der Wächter über die System-Stabilität. Er sorgt dafür, dass Feature-Entwicklungen nicht zu Regressionen führen und das System in jeder Umgebung stabil läuft.
|
||||||
|
|
||||||
|
## Kernkompetenzen
|
||||||
|
- **Test-Frameworks:** Jest, Cypress, Playwright oder Selenium.
|
||||||
|
- **Strategie:** Definition der Test-Pyramide (Unit, Integration, E2E).
|
||||||
|
- **Tools:** Regression-Testing, Last- und Performance-Tests.
|
||||||
|
- **Workflow:** Automatisierte Bug-Reporting Workflows.
|
||||||
|
|
||||||
|
## Verantwortlichkeiten
|
||||||
|
- Aufbau einer umfassenden automatisierten Test-Suite.
|
||||||
|
- Überwachung der Test-Abdeckung (Code Coverage).
|
||||||
|
- Durchführung von manuellen Abnahme-Tests für kritische Features.
|
||||||
|
- Erstellung von Test-Reports für Stakeholder.
|
||||||
|
|
||||||
|
## Aktuelle Prioritäten
|
||||||
|
- [ ] Setup der CI-Pipeline für automatisierte Tests.
|
||||||
|
- [ ] Definition der Code-Abdeckung (Minimum Thresholds).
|
||||||
|
- [ ] Auswahl der finalen Test-Frameworks.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Data & Integration Engineer
|
||||||
|
|
||||||
|
## Rolle & Fokus
|
||||||
|
Der Data Engineer ist verantwortlich für die Anbindung, Transformation und Aggregation externer Datenquellen (APIs, Streams) für das "God's Eye" Projekt. Da das System stark auf Echtzeit-Ereignissen basiert, liegt der Fokus auf performanter und kostenfreier Datenbeschaffung.
|
||||||
|
|
||||||
|
## Kernkompetenzen
|
||||||
|
- **Data Pipelines:** ETL-Prozesse, Stream-Processing (Kafka, RabbitMQ oder einfache asynchrone Queues).
|
||||||
|
- **API-Integration:** REST, WebSockets, Scraping (falls nötig), Rate-Limit-Handling.
|
||||||
|
- **Geospatial Data:** Verarbeitung von GeoJSON, KML und Koordinatentransformationen.
|
||||||
|
- **Data Sourcing:** Identifizierung kostenloser/Open-Source-Datenquellen (z.B. OpenSky, GDELT).
|
||||||
|
|
||||||
|
## Verantwortlichkeiten
|
||||||
|
- Anbindung und Normalisierung von Flugzeug-, Schiffs- und Verkehrsdaten.
|
||||||
|
- Integration von globalen News-Feeds (z.B. GDELT Project).
|
||||||
|
- Entwicklung von Caching-Strategien, um Rate-Limits externer APIs nicht zu überschreiten.
|
||||||
|
- Vorverarbeitung der Daten für die AI-Pipeline (z.B. Formatierung von News für das LLM).
|
||||||
|
|
||||||
|
## Aktuelle Prioritäten
|
||||||
|
- [ ] Evaluierung und Anbindung kostenloser Daten-APIs (OpenSky für Flüge, GDELT für News).
|
||||||
|
- [ ] Aufbau einer zentralen Daten-Pipeline im Backend (z.B. mit Python FastAPI).
|
||||||
|
- [ ] Implementierung eines Caching-Layers für Echtzeit-Daten.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# God's Eye Development Team
|
||||||
|
|
||||||
|
Willkommen in der technischen Zentrale von God's Eye. Hier findest du die Profile und Zuständigkeiten unseres spezialisierten Engineering-Teams.
|
||||||
|
|
||||||
|
## Team-Struktur
|
||||||
|
|
||||||
|
1. [Lead Architect](./01_Lead_Architect.md) – Vision & Strategie
|
||||||
|
2. [Senior Backend Engineer](./02_Backend_Engineer.md) – Logik & Daten
|
||||||
|
3. [Frontend Architect](./03_Frontend_Architect.md) – UI/UX & Interaktion
|
||||||
|
4. [DevOps & SRE Specialist](./04_DevOps_SRE_Specialist.md) – Infrastruktur & Ops
|
||||||
|
5. [QA & Test Automation Engineer](./05_QA_Automation_Engineer.md) – Qualität & Stabilität
|
||||||
|
6. [Data & Integration Engineer](./06_Data_Engineer.md) – APIs & Echtzeit-Daten
|
||||||
|
|
||||||
|
## Arbeitsweise
|
||||||
|
|
||||||
|
- **Agile Development:** Wir arbeiten in Sprints mit Fokus auf messbare Ergebnisse.
|
||||||
|
- **Code Reviews:** Jede Änderung wird von mindestens einem anderen Engineer geprüft.
|
||||||
|
- **Quality First:** Automatisierte Tests sind ein fester Bestandteil unseres Workflows.
|
||||||
|
- **Documentation:** Wissen wird in Markdown-Dateien für das gesamte Team zugänglich gemacht.
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# React + TypeScript + Vite
|
||||||
|
|
||||||
|
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||||
|
|
||||||
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
||||||
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||||
|
|
||||||
|
## React Compiler
|
||||||
|
|
||||||
|
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||||
|
|
||||||
|
## Expanding the ESLint configuration
|
||||||
|
|
||||||
|
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
// Other configs...
|
||||||
|
|
||||||
|
// Remove tseslint.configs.recommended and replace with this
|
||||||
|
tseslint.configs.recommendedTypeChecked,
|
||||||
|
// Alternatively, use this for stricter rules
|
||||||
|
tseslint.configs.strictTypeChecked,
|
||||||
|
// Optionally, add this for stylistic rules
|
||||||
|
tseslint.configs.stylisticTypeChecked,
|
||||||
|
|
||||||
|
// Other configs...
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
// other options...
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// eslint.config.js
|
||||||
|
import reactX from 'eslint-plugin-react-x'
|
||||||
|
import reactDom from 'eslint-plugin-react-dom'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
// Other configs...
|
||||||
|
// Enable lint rules for React
|
||||||
|
reactX.configs['recommended-typescript'],
|
||||||
|
// Enable lint rules for React DOM
|
||||||
|
reactDom.configs.recommended,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
// other options...
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
```
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
js.configs.recommended,
|
||||||
|
tseslint.configs.recommended,
|
||||||
|
reactHooks.configs.flat.recommended,
|
||||||
|
reactRefresh.configs.vite,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>GOD'S EYE — Global Intelligence Platform</title>
|
||||||
|
<meta name="description" content="Real-time global intelligence and surveillance platform" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Generated
+4653
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tailwindcss/vite": "^4.2.1",
|
||||||
|
"@types/geojson": "^7946.0.16",
|
||||||
|
"globe.gl": "^2.45.0",
|
||||||
|
"lucide-react": "^0.577.0",
|
||||||
|
"maplibre-gl": "^5.19.0",
|
||||||
|
"react": "^19.2.0",
|
||||||
|
"react-dom": "^19.2.0",
|
||||||
|
"react-use-websocket": "^4.13.0",
|
||||||
|
"tailwindcss": "^4.2.1",
|
||||||
|
"three": "^0.183.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.39.1",
|
||||||
|
"@types/node": "^24.10.1",
|
||||||
|
"@types/react": "^19.2.14",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@types/three": "^0.183.1",
|
||||||
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
|
"eslint": "^9.39.1",
|
||||||
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.24",
|
||||||
|
"globals": "^16.5.0",
|
||||||
|
"typescript": "~5.9.3",
|
||||||
|
"typescript-eslint": "^8.48.0",
|
||||||
|
"vite": "^7.3.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 239 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 698 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 369 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 883 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1 @@
|
|||||||
|
/* App-level overrides — keep empty; global styles live in index.css */
|
||||||
@@ -0,0 +1,555 @@
|
|||||||
|
import { lazy, Suspense, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import useWebSocket, { ReadyState } from 'react-use-websocket';
|
||||||
|
import {
|
||||||
|
Shield, Plane, Satellite, X, Camera, Activity, Clock, Anchor, MapPin,
|
||||||
|
Zap, Compass, Eye, Layers, Radio, Crosshair, AlertTriangle, Globe, Wifi, WifiOff,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
const GlobeView = lazy(() => import('./components/GlobeView'));
|
||||||
|
|
||||||
|
function resolveWsUrl(): string {
|
||||||
|
const envUrl = import.meta.env.VITE_WS_URL as string | undefined;
|
||||||
|
if (envUrl && envUrl.trim().length > 0) return envUrl.trim();
|
||||||
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
|
return `${protocol}//${window.location.hostname}:8000/ws`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function timeAgo(dateStr: string): string {
|
||||||
|
try {
|
||||||
|
const diff = Date.now() - new Date(dateStr).getTime();
|
||||||
|
if (diff < 60000) return 'Just now';
|
||||||
|
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
|
||||||
|
if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
|
||||||
|
return `${Math.floor(diff / 86400000)}d ago`;
|
||||||
|
} catch { return ''; }
|
||||||
|
}
|
||||||
|
|
||||||
|
const THREAT_CONFIG: Record<string, { cls: string; glow: string; bar: string }> = {
|
||||||
|
SEVERE: { cls: 'bg-red-500/20 text-red-400 border-red-500/50', glow: 'shadow-[0_0_20px_rgba(239,68,68,0.4)]', bar: 'bg-red-500' },
|
||||||
|
HIGH: { cls: 'bg-orange-500/20 text-orange-400 border-orange-500/50', glow: 'shadow-[0_0_20px_rgba(249,115,22,0.4)]', bar: 'bg-orange-500' },
|
||||||
|
ELEVATED: { cls: 'bg-amber-500/20 text-amber-300 border-amber-500/50', glow: 'shadow-[0_0_15px_rgba(245,158,11,0.3)]', bar: 'bg-amber-400' },
|
||||||
|
GUARDED: { cls: 'bg-blue-500/20 text-blue-300 border-blue-500/40', glow: 'shadow-[0_0_15px_rgba(59,130,246,0.3)]', bar: 'bg-blue-500' },
|
||||||
|
LOW: { cls: 'bg-emerald-500/20 text-emerald-400 border-emerald-500/40', glow: '', bar: 'bg-emerald-500' },
|
||||||
|
UNKNOWN: { cls: 'bg-slate-800/40 text-slate-400 border-slate-600/50', glow: '', bar: 'bg-slate-600' },
|
||||||
|
OFFLINE: { cls: 'bg-slate-900/60 text-slate-500 border-slate-700/50', glow: '', bar: 'bg-slate-700' },
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const wsUrl = useMemo(resolveWsUrl, []);
|
||||||
|
const lastUpdateSigRef = useRef<string>('');
|
||||||
|
|
||||||
|
const [rawData, setRawData] = useState<any>({
|
||||||
|
planes: [], satellites: [], news: [], gpsInterference: [],
|
||||||
|
webcams: [], ships: [], emergencySquawks: [],
|
||||||
|
conflicts: [], cyber_attacks: [],
|
||||||
|
aiAnalysis: { summary: "Initializing Global Surveillance...", threat_level: 'UNKNOWN', predictions: [] },
|
||||||
|
bgp: {}, last_updated: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [filters, setFilters] = useState({
|
||||||
|
showPlanes: true, showSatellites: true, showShips: true, showWebcams: false,
|
||||||
|
showNews: true, showConflicts: true, showCyber: true, militaryOnlySats: false,
|
||||||
|
jammingOnlyPlanes: false, stationsVisible: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [selectedItem, setSelectedItem] = useState<any>(null);
|
||||||
|
const [showFilterPanel, setShowFilterPanel] = useState(false);
|
||||||
|
const [feedTab, setFeedTab] = useState<'brief' | 'live' | 'predictions'>('brief');
|
||||||
|
const [webcamViewer, setWebcamViewer] = useState<any>(null);
|
||||||
|
const [camPov, setCamPov] = useState<{ lat: number; lng: number; altitude: number } | null>(null);
|
||||||
|
const [showReticle, setShowReticle] = useState(false);
|
||||||
|
const historyModeRef = useRef(false);
|
||||||
|
|
||||||
|
const [utcTime, setUtcTime] = useState('');
|
||||||
|
useEffect(() => {
|
||||||
|
const update = () => setUtcTime(new Date().toISOString().replace('T', ' ').slice(0, 19) + 'Z');
|
||||||
|
update();
|
||||||
|
const t = setInterval(update, 1000);
|
||||||
|
return () => clearInterval(t);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const { lastJsonMessage, readyState, sendJsonMessage } = useWebSocket(wsUrl, { shouldReconnect: () => true, reconnectInterval: 3000 });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(window as any).__GODS_EYE_REQUEST_SECTOR__ = (box: any) => sendJsonMessage({ type: 'request_sector', box });
|
||||||
|
return () => { (window as any).__GODS_EYE_REQUEST_SECTOR__ = null; };
|
||||||
|
}, [sendJsonMessage]);
|
||||||
|
|
||||||
|
const [systemStatus, setSystemStatus] = useState('Initializing Sentinel...');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const msg = lastJsonMessage as any;
|
||||||
|
if (msg?.type === 'status') { setSystemStatus(msg.message); return; }
|
||||||
|
if (msg?.type === 'sector_update') { (window as any).__GODS_EYE_ON_SECTOR_DATA__?.(msg); return; }
|
||||||
|
if (msg?.type === 'update' && !historyModeRef.current) {
|
||||||
|
const updateSig = JSON.stringify(msg.last_updated || {});
|
||||||
|
if (updateSig === lastUpdateSigRef.current) return;
|
||||||
|
lastUpdateSigRef.current = updateSig;
|
||||||
|
setRawData((prev: any) => ({
|
||||||
|
...prev,
|
||||||
|
planes: msg.planes || prev.planes,
|
||||||
|
satellites: msg.satellites || prev.satellites,
|
||||||
|
news: msg.news || prev.news,
|
||||||
|
gpsInterference: msg.gps_interference || prev.gpsInterference,
|
||||||
|
webcams: msg.webcams || prev.webcams,
|
||||||
|
ships: msg.ships || prev.ships,
|
||||||
|
emergencySquawks: msg.emergency_squawks || prev.emergencySquawks,
|
||||||
|
conflicts: msg.conflicts || prev.conflicts,
|
||||||
|
cyber_attacks: msg.cyber_attacks || prev.cyber_attacks,
|
||||||
|
aiAnalysis: msg.ai_analysis || prev.aiAnalysis,
|
||||||
|
bgp: msg.bgp || prev.bgp,
|
||||||
|
last_updated: msg.last_updated || prev.last_updated,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, [lastJsonMessage]);
|
||||||
|
|
||||||
|
const isOnline = readyState === ReadyState.OPEN;
|
||||||
|
const threatLevel = rawData.aiAnalysis?.threat_level || 'UNKNOWN';
|
||||||
|
const threatConf = THREAT_CONFIG[threatLevel] || THREAT_CONFIG.UNKNOWN;
|
||||||
|
const criticalConflicts = useMemo(() => (rawData.conflicts || []).filter((c: any) => c.severity === 'CRITICAL'), [rawData.conflicts]);
|
||||||
|
const milPlanes = useMemo(() => (rawData.planes || []).filter((p: any) => p.military), [rawData.planes]);
|
||||||
|
|
||||||
|
const liveFeed = useMemo(() => {
|
||||||
|
return [...(rawData.news || []).map((n: any) => ({ ...n, _feedType: 'news' })), ...(rawData.conflicts || []).map((c: any) => ({ ...c, _feedType: 'conflict' }))]
|
||||||
|
.sort((a, b) => new Date(b.published_at || 0).getTime() - new Date(a.published_at || 0).getTime()).slice(0, 80);
|
||||||
|
}, [rawData.news, rawData.conflicts]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative w-screen h-screen overflow-hidden bg-[#020617] text-slate-200 font-mono select-none antialiased">
|
||||||
|
|
||||||
|
{/* GLOBE */}
|
||||||
|
<div className="absolute inset-0">
|
||||||
|
<Suspense fallback={
|
||||||
|
<div className="absolute inset-0 bg-[#020617] flex items-center justify-center z-50">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="w-12 h-12 border-2 border-sky-500/20 border-t-sky-400 rounded-full animate-spin mx-auto mb-4" />
|
||||||
|
<p className="text-[10px] text-sky-400 font-bold uppercase tracking-[0.4em] animate-pulse">{systemStatus}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}>
|
||||||
|
<GlobeView data={rawData} selectedItem={selectedItem} onSelectItem={(item) => item?.dataType === 'webcam' ? setWebcamViewer(item) : setSelectedItem(item)} onPovChange={setCamPov} />
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* TARGETING RETICLE */}
|
||||||
|
{showReticle && (
|
||||||
|
<div className="absolute inset-0 pointer-events-none z-10 flex items-center justify-center">
|
||||||
|
<div className="relative w-32 h-32">
|
||||||
|
{/* Corner brackets */}
|
||||||
|
<div className="absolute top-0 left-0 w-5 h-5 border-t-2 border-l-2 border-rose-400/70" />
|
||||||
|
<div className="absolute top-0 right-0 w-5 h-5 border-t-2 border-r-2 border-rose-400/70" />
|
||||||
|
<div className="absolute bottom-0 left-0 w-5 h-5 border-b-2 border-l-2 border-rose-400/70" />
|
||||||
|
<div className="absolute bottom-0 right-0 w-5 h-5 border-b-2 border-r-2 border-rose-400/70" />
|
||||||
|
{/* Center dot */}
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<div className="w-1.5 h-1.5 rounded-full bg-rose-400/80" />
|
||||||
|
</div>
|
||||||
|
{/* Crosshair lines */}
|
||||||
|
<div className="absolute top-1/2 left-0 right-0 h-px bg-rose-400/30" />
|
||||||
|
<div className="absolute left-1/2 top-0 bottom-0 w-px bg-rose-400/30" />
|
||||||
|
</div>
|
||||||
|
{camPov && (
|
||||||
|
<div className="absolute" style={{ top: 'calc(50% + 72px)', transform: 'translateX(-50%)', left: '50%' }}>
|
||||||
|
<span className="text-[9px] text-rose-400/70 font-mono tracking-widest bg-black/40 px-2 py-0.5 rounded">
|
||||||
|
{camPov.lat >= 0 ? 'N' : 'S'}{Math.abs(camPov.lat).toFixed(4)}° {camPov.lng >= 0 ? 'E' : 'W'}{Math.abs(camPov.lng).toFixed(4)}°
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* VIGNETTE */}
|
||||||
|
<div className="absolute inset-0 pointer-events-none z-5" style={{
|
||||||
|
background: 'radial-gradient(ellipse at center, transparent 55%, rgba(0,0,0,0.6) 100%)',
|
||||||
|
}} />
|
||||||
|
|
||||||
|
{/* TOP COMMAND BAR */}
|
||||||
|
<header className="absolute top-0 left-0 right-0 h-14 z-20 flex items-center justify-between px-4 bg-gradient-to-b from-[#020617]/95 to-transparent backdrop-blur-sm pointer-events-none border-b border-slate-800/40">
|
||||||
|
{/* LEFT: LOGO + STATUS */}
|
||||||
|
<div className="flex items-center gap-4 pointer-events-auto">
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<div className="relative flex items-center justify-center w-8 h-8 rounded-lg bg-sky-900/40 border border-sky-500/40 shadow-[0_0_15px_rgba(14,165,233,0.25)]">
|
||||||
|
<Shield size={15} className="text-sky-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-black tracking-[0.2em] text-white drop-shadow-[0_0_8px_rgba(255,255,255,0.4)]">GOD'S EYE</div>
|
||||||
|
<div className="text-[8px] text-sky-500 tracking-[0.35em] font-bold uppercase">Strategic Command</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="h-5 w-px bg-slate-700/60" />
|
||||||
|
|
||||||
|
<div className={`flex items-center gap-1.5 px-2.5 py-1 rounded border text-[9px] font-bold tracking-wider ${isOnline ? 'border-emerald-500/30 text-emerald-400' : 'border-red-500/30 text-red-400 animate-pulse'}`}>
|
||||||
|
{isOnline ? <Wifi size={10}/> : <WifiOff size={10}/>}
|
||||||
|
{isOnline ? 'DATALINK SECURE' : 'LINK LOST'}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1.5 bg-slate-900/60 border border-slate-700/50 px-2.5 py-1 rounded text-[9px] text-slate-400">
|
||||||
|
<Clock size={10} className="text-sky-500" /> {utcTime}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CENTER: THREAT LEVEL */}
|
||||||
|
<div className={`flex items-center gap-3 px-4 py-1.5 rounded-lg border ${threatConf.cls} ${threatConf.glow} backdrop-blur-sm`}>
|
||||||
|
<div className={`w-2 h-2 rounded-full ${threatConf.bar} animate-pulse`} />
|
||||||
|
<span className="text-[9px] text-slate-400 tracking-widest">THREAT</span>
|
||||||
|
<span className="text-sm font-black tracking-wider">{threatLevel}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* RIGHT: STATS + CONTROLS */}
|
||||||
|
<div className="flex items-center gap-3 pointer-events-auto">
|
||||||
|
{/* Entity counts */}
|
||||||
|
<div className="flex items-center gap-3 text-[10px] bg-slate-900/60 border border-slate-700/50 px-3 py-1.5 rounded backdrop-blur-sm">
|
||||||
|
<StatBadge icon={<Plane size={11} className="text-amber-400"/>} value={rawData.planes.length} label="AC" />
|
||||||
|
<div className="w-px h-4 bg-slate-700" />
|
||||||
|
<StatBadge icon={<Satellite size={11} className="text-sky-400"/>} value={rawData.satellites.length} label="SAT" />
|
||||||
|
<div className="w-px h-4 bg-slate-700" />
|
||||||
|
<StatBadge icon={<Anchor size={11} className="text-blue-400"/>} value={rawData.ships.length} label="VES" />
|
||||||
|
<div className="w-px h-4 bg-slate-700" />
|
||||||
|
<StatBadge icon={<AlertTriangle size={11} className="text-rose-400"/>} value={criticalConflicts.length} label="CRT" pulse={criticalConflicts.length > 0} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Reticle toggle */}
|
||||||
|
<button onClick={() => setShowReticle(r => !r)} title="Toggle targeting reticle"
|
||||||
|
className={`p-2 rounded border transition-all ${showReticle ? 'bg-rose-500/20 border-rose-400 text-rose-300' : 'bg-slate-900/60 border-slate-700 text-slate-400 hover:border-rose-500/50 hover:text-rose-300'} backdrop-blur-sm`}>
|
||||||
|
<Crosshair size={15} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Filter toggle */}
|
||||||
|
<button onClick={() => setShowFilterPanel(!showFilterPanel)}
|
||||||
|
className={`p-2 rounded border transition-all ${showFilterPanel ? 'bg-sky-500/20 border-sky-400 text-sky-300' : 'bg-slate-900/60 border-slate-700 text-slate-400 hover:border-sky-500/50 hover:text-sky-300'} backdrop-blur-sm`}>
|
||||||
|
<Layers size={15} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* LEFT SIDEBAR – INTELLIGENCE FEED */}
|
||||||
|
<aside className="absolute left-4 top-16 bottom-12 w-[340px] z-20 pointer-events-none flex flex-col">
|
||||||
|
<div className="pointer-events-auto h-full flex flex-col bg-slate-950/70 border border-slate-800/80 rounded-xl backdrop-blur-xl shadow-2xl shadow-black/60 overflow-hidden">
|
||||||
|
|
||||||
|
{/* Tab bar */}
|
||||||
|
<div className="flex border-b border-slate-800/80 bg-slate-900/40">
|
||||||
|
<FeedTab icon={<Radio size={10}/>} label="AI Brief" active={feedTab==='brief'} onClick={()=>setFeedTab('brief')} />
|
||||||
|
<FeedTab icon={<Activity size={10}/>} label="Live Intel" active={feedTab==='live'} onClick={()=>setFeedTab('live')} />
|
||||||
|
<FeedTab icon={<Crosshair size={10}/>} label="Predictions" active={feedTab==='predictions'} onClick={()=>setFeedTab('predictions')} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto scrollbar-hide p-3 space-y-3">
|
||||||
|
|
||||||
|
{/* ── AI BRIEF ── */}
|
||||||
|
{feedTab === 'brief' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Summary */}
|
||||||
|
<div className="p-3 rounded-lg bg-sky-950/20 border border-sky-500/20">
|
||||||
|
<div className="flex items-center gap-1.5 mb-2">
|
||||||
|
<Radio size={9} className="text-sky-400" />
|
||||||
|
<span className="text-[9px] font-bold text-sky-500 tracking-widest uppercase">Strategic Summary</span>
|
||||||
|
<span className="ml-auto text-[8px] text-slate-600">{rawData.aiAnalysis?.source || 'heuristic'}</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-[11px] leading-relaxed text-slate-300">{rawData.aiAnalysis?.summary}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Key Drivers */}
|
||||||
|
{rawData.aiAnalysis?.key_drivers?.length > 0 && (
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<SectionHeader icon={<Zap size={9} className="text-amber-400"/>} label="Signal Drivers" />
|
||||||
|
{rawData.aiAnalysis.key_drivers.map((d: any, i: number) => (
|
||||||
|
<div key={i} className="flex items-start gap-2.5 p-2.5 rounded-lg bg-slate-900/50 border border-slate-800/80 hover:border-slate-700 transition-colors">
|
||||||
|
<span className="text-sm mt-0.5">{d.icon}</span>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center justify-between mb-0.5">
|
||||||
|
<span className="text-[10px] font-bold text-slate-200">{d.signal}</span>
|
||||||
|
<WeightBadge weight={d.weight} />
|
||||||
|
</div>
|
||||||
|
<p className="text-[9px] text-slate-500 leading-tight">{d.detail}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Regional Theaters */}
|
||||||
|
{rawData.aiAnalysis?.regional_briefs?.length > 0 && (
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<SectionHeader icon={<MapPin size={9}/>} label="Regional Theaters" />
|
||||||
|
{rawData.aiAnalysis.regional_briefs.map((r: any, i: number) => (
|
||||||
|
<div key={i}
|
||||||
|
className="p-2.5 rounded-lg bg-slate-900/50 border border-slate-800/80 hover:border-sky-500/30 transition-all cursor-pointer group"
|
||||||
|
onClick={() => setSelectedItem({ lat: r.lat, lon: r.lon, name: r.region, summary: r.summary, dataType: 'news' })}>
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<span className="text-[10px] font-bold text-slate-200 group-hover:text-sky-400 transition-colors">{r.region}</span>
|
||||||
|
<WeightBadge weight={r.status} />
|
||||||
|
</div>
|
||||||
|
<p className="text-[9px] text-slate-500 line-clamp-2 leading-snug">{r.summary}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Intel Correlations */}
|
||||||
|
{rawData.aiAnalysis?.correlations?.length > 0 && (
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<SectionHeader icon={<Compass size={9}/>} label="Intel Correlations" />
|
||||||
|
{rawData.aiAnalysis.correlations.map((c: string, i: number) => (
|
||||||
|
<div key={i} className="flex gap-2 p-2.5 rounded-lg bg-sky-500/5 border border-sky-500/10">
|
||||||
|
<div className="w-1 h-1 rounded-full bg-sky-400 mt-1.5 flex-shrink-0" />
|
||||||
|
<p className="text-[9px] text-sky-200/70 italic leading-snug">{c}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Watch Items */}
|
||||||
|
{rawData.aiAnalysis?.watch_items?.length > 0 && (
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<SectionHeader icon={<Eye size={9} className="text-rose-400"/>} label="Watch List" />
|
||||||
|
{rawData.aiAnalysis.watch_items.map((w: string, i: number) => (
|
||||||
|
<div key={i} className="flex gap-2 p-2 rounded bg-rose-500/5 border border-rose-500/10">
|
||||||
|
<span className="text-rose-400 text-[9px] mt-0.5 flex-shrink-0">▶</span>
|
||||||
|
<p className="text-[9px] text-rose-200/70 leading-snug">{w}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ── LIVE INTEL ── */}
|
||||||
|
{feedTab === 'live' && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{liveFeed.map((item: any, idx: number) => (
|
||||||
|
<div key={idx} onClick={() => setSelectedItem({ ...item, dataType: item._feedType })}
|
||||||
|
className="p-2.5 rounded-lg bg-slate-900/40 border border-slate-800 hover:border-sky-500/40 hover:bg-slate-800/50 transition-all cursor-pointer group">
|
||||||
|
<div className="flex items-center gap-2 mb-1.5">
|
||||||
|
<span className={`text-[8px] px-1.5 py-0.5 rounded font-bold uppercase tracking-wider ${item._feedType === 'conflict' ? 'bg-rose-500/20 text-rose-400' : 'bg-sky-500/20 text-sky-400'}`}>
|
||||||
|
{item.category || item.event_type || 'INTEL'}
|
||||||
|
</span>
|
||||||
|
{item._feedType === 'conflict' && item.severity && (
|
||||||
|
<span className={`text-[7px] px-1 rounded font-black ${item.severity === 'CRITICAL' ? 'bg-red-500/20 text-red-400' : item.severity === 'HIGH' ? 'bg-orange-500/20 text-orange-400' : 'bg-slate-700 text-slate-400'}`}>{item.severity}</span>
|
||||||
|
)}
|
||||||
|
<span className="ml-auto text-[8px] text-slate-600">{timeAgo(item.published_at)}</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] font-medium text-slate-300 group-hover:text-white line-clamp-2 leading-snug">{item.title}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ── PREDICTIONS ── */}
|
||||||
|
{feedTab === 'predictions' && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="text-[9px] text-slate-500 leading-relaxed p-2 rounded bg-slate-900/30 border border-slate-800">
|
||||||
|
AI-synthesized threat predictions from movement, OSINT, and SIGINT correlation.
|
||||||
|
</div>
|
||||||
|
{(rawData.aiAnalysis?.predictions || []).length === 0 && (
|
||||||
|
<div className="text-center py-8 text-slate-600 text-[10px]">No predictions available yet.</div>
|
||||||
|
)}
|
||||||
|
{(rawData.aiAnalysis?.predictions || []).map((p: any, i: number) => (
|
||||||
|
<div key={i}
|
||||||
|
className="p-3 rounded-lg bg-slate-900/60 border border-amber-500/20 hover:border-amber-500/40 transition-all cursor-pointer group"
|
||||||
|
onClick={() => setSelectedItem({ ...p, name: p.location, dataType: 'prediction' })}>
|
||||||
|
<div className="flex items-start justify-between mb-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Crosshair size={12} className="text-amber-400 flex-shrink-0" />
|
||||||
|
<span className="text-[11px] font-bold text-amber-300 group-hover:text-amber-200">{p.location}</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] font-black text-amber-400 bg-amber-500/10 px-2 py-0.5 rounded border border-amber-500/20">{p.probability}</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-slate-300 font-medium mb-1.5 line-clamp-2">{p.event}</p>
|
||||||
|
{p.reason && <p className="text-[9px] text-slate-500 leading-snug line-clamp-3">{p.reason}</p>}
|
||||||
|
<div className="flex items-center gap-1.5 mt-2 text-[8px] text-slate-600">
|
||||||
|
<MapPin size={8}/>
|
||||||
|
<span>{p.lat?.toFixed(2)}°, {p.lon?.toFixed(2)}°</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* RIGHT SIDEBAR – TARGET DETAILS */}
|
||||||
|
{selectedItem && (
|
||||||
|
<aside className="absolute right-4 top-16 bottom-12 w-[320px] z-20 pointer-events-none">
|
||||||
|
<div className="pointer-events-auto h-full flex flex-col bg-slate-950/80 border border-sky-900/50 rounded-xl backdrop-blur-xl shadow-[0_0_30px_rgba(14,165,233,0.15)] overflow-hidden">
|
||||||
|
<div className="p-3.5 border-b border-sky-900/50 bg-sky-950/20 flex justify-between items-start flex-shrink-0">
|
||||||
|
<div>
|
||||||
|
<span className="text-[8px] text-sky-400 font-bold tracking-widest uppercase mb-1 block">Target Acquired</span>
|
||||||
|
<h3 className="text-[13px] font-black text-white uppercase tracking-wide">
|
||||||
|
{selectedItem.callsign || selectedItem.name || selectedItem.title?.slice(0, 32) || 'Unknown Entity'}
|
||||||
|
</h3>
|
||||||
|
{selectedItem.dataType && (
|
||||||
|
<span className="text-[8px] text-sky-600 tracking-widest uppercase">{selectedItem.dataType}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button onClick={() => setSelectedItem(null)} className="p-1 rounded hover:bg-rose-500/20 text-slate-500 hover:text-rose-400 transition-colors ml-2 flex-shrink-0"><X size={14}/></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 p-4 space-y-4 overflow-y-auto scrollbar-hide">
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<DetailBox label="Latitude" value={selectedItem.lat?.toFixed(5) ?? 'N/A'} />
|
||||||
|
<DetailBox label="Longitude" value={(selectedItem.lon ?? selectedItem.lng)?.toFixed(5) ?? 'N/A'} />
|
||||||
|
{selectedItem.alt != null && <DetailBox label="Altitude" value={`${Math.round(selectedItem.alt).toLocaleString()} m`} />}
|
||||||
|
{selectedItem.velocity != null && <DetailBox label="Velocity" value={`${Math.round(selectedItem.velocity)} kt`} />}
|
||||||
|
{selectedItem.heading != null && <DetailBox label="Heading" value={`${Math.round(selectedItem.heading)}°`} />}
|
||||||
|
{selectedItem.country && <DetailBox label="Origin" value={selectedItem.country} />}
|
||||||
|
{selectedItem.vessel_type && <DetailBox label="Class" value={selectedItem.vessel_type} />}
|
||||||
|
{selectedItem.type && <DetailBox label="Type" value={selectedItem.type} />}
|
||||||
|
{selectedItem.probability && <DetailBox label="Probability" value={selectedItem.probability} />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(selectedItem.summary || selectedItem.title || selectedItem.event || selectedItem.reason) && (
|
||||||
|
<div className="p-3 bg-slate-900/50 border border-slate-700/50 rounded-lg text-[10px] leading-relaxed text-slate-300 space-y-2">
|
||||||
|
{selectedItem.event && <p className="font-medium text-slate-200">{selectedItem.event}</p>}
|
||||||
|
{selectedItem.reason && <p className="text-slate-400 italic">{selectedItem.reason}</p>}
|
||||||
|
{!selectedItem.event && (selectedItem.summary || selectedItem.title) && <p>{selectedItem.summary || selectedItem.title}</p>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedItem.military && (
|
||||||
|
<div className="flex items-center gap-2 px-3 py-2 bg-rose-500/10 border border-rose-500/30 rounded text-[9px] text-rose-400 font-bold tracking-wider">
|
||||||
|
<AlertTriangle size={10}/> MILITARY ASSET
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedItem.image && (
|
||||||
|
<img src={selectedItem.image} alt="Target" className="w-full h-36 object-cover rounded-lg border border-slate-700/50" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-2.5 border-t border-slate-800/80 bg-slate-950/50 flex justify-between items-center flex-shrink-0">
|
||||||
|
<span className="text-[7px] text-slate-600 tracking-widest uppercase">ID: {selectedItem.id || selectedItem.icao || 'SYS-GEN'}</span>
|
||||||
|
<span className="text-[7px] text-slate-600 tracking-widest uppercase">{utcTime}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* LAYER FILTER PANEL */}
|
||||||
|
{showFilterPanel && (
|
||||||
|
<div className="absolute top-16 right-16 w-56 z-30 bg-slate-950/95 border border-slate-700/80 rounded-xl p-4 backdrop-blur-xl shadow-2xl pointer-events-auto">
|
||||||
|
<h3 className="text-[9px] font-bold text-slate-400 uppercase tracking-widest mb-3 border-b border-slate-800 pb-2 flex items-center gap-1.5"><Layers size={9}/> Display Layers</h3>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<ToggleRow label="Airspace / Aircraft" active={filters.showPlanes} onClick={() => setFilters(f => ({...f, showPlanes: !f.showPlanes}))} />
|
||||||
|
<ToggleRow label="Maritime / Vessels" active={filters.showShips} onClick={() => setFilters(f => ({...f, showShips: !f.showShips}))} />
|
||||||
|
<ToggleRow label="Satellites / Orbital" active={filters.showSatellites} onClick={() => setFilters(f => ({...f, showSatellites: !f.showSatellites}))} />
|
||||||
|
<ToggleRow label="Cyber Intel" active={filters.showCyber} onClick={() => setFilters(f => ({...f, showCyber: !f.showCyber}))} />
|
||||||
|
<ToggleRow label="GPS Jamming" active={true} onClick={() => {}} />
|
||||||
|
<ToggleRow label="Conflict Events" active={filters.showConflicts} onClick={() => setFilters(f => ({...f, showConflicts: !f.showConflicts}))} />
|
||||||
|
<ToggleRow label="News Rings" active={filters.showNews} onClick={() => setFilters(f => ({...f, showNews: !f.showNews}))} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* BOTTOM STATUS BAR */}
|
||||||
|
<footer className="absolute bottom-0 left-0 right-0 h-9 z-20 flex items-center justify-between px-4 bg-[#020617]/90 border-t border-slate-800/60 backdrop-blur-sm pointer-events-none">
|
||||||
|
<div className="flex items-center gap-5 text-[9px] text-slate-600">
|
||||||
|
<span className="flex items-center gap-1.5"><Globe size={9} className="text-sky-700"/> GOD'S EYE v2.1</span>
|
||||||
|
<span className="flex items-center gap-1.5"><Plane size={9} className="text-amber-700"/>{rawData.planes.length.toLocaleString()} AC · <span className="text-rose-700">{milPlanes.length} MIL</span></span>
|
||||||
|
<span className="flex items-center gap-1.5"><Anchor size={9} className="text-blue-700"/>{rawData.ships.length} VES</span>
|
||||||
|
<span className="flex items-center gap-1.5"><Satellite size={9} className="text-sky-700"/>{rawData.satellites.length} SAT</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4 text-[9px] text-slate-600">
|
||||||
|
{camPov && (
|
||||||
|
<span className="flex items-center gap-1 text-slate-700 font-mono">
|
||||||
|
{camPov.lat >= 0 ? 'N' : 'S'}{Math.abs(camPov.lat).toFixed(3)}°
|
||||||
|
{camPov.lng >= 0 ? 'E' : 'W'}{Math.abs(camPov.lng).toFixed(3)}°
|
||||||
|
ALT {camPov.altitude.toFixed(2)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{rawData.gpsInterference?.length > 0 && (
|
||||||
|
<span className="text-rose-600 animate-pulse flex items-center gap-1"><Radio size={9}/>{rawData.gpsInterference.length} GPS JAM</span>
|
||||||
|
)}
|
||||||
|
{rawData.cyber_attacks?.length > 0 && (
|
||||||
|
<span className="text-orange-700 flex items-center gap-1"><Zap size={9}/>{rawData.cyber_attacks.length} CYBER</span>
|
||||||
|
)}
|
||||||
|
<span className={`font-bold ${isOnline ? 'text-emerald-700' : 'text-red-700 animate-pulse'}`}>
|
||||||
|
{isOnline ? '● LIVE' : '○ OFFLINE'}
|
||||||
|
</span>
|
||||||
|
<span>{utcTime}</span>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
{/* ALTITUDE LEGEND */}
|
||||||
|
<div className="absolute bottom-11 left-4 z-20 pointer-events-none flex flex-col gap-0.5">
|
||||||
|
{[
|
||||||
|
{ color: '#ff2615', label: 'MIL' },
|
||||||
|
{ color: '#00d9ff', label: '>10k m' },
|
||||||
|
{ color: '#33ff80', label: '>5k m' },
|
||||||
|
{ color: '#ffb300', label: '>2k m' },
|
||||||
|
{ color: '#ff6600', label: '>500 m' },
|
||||||
|
{ color: '#ffffff', label: 'GND' },
|
||||||
|
].map(({ color, label }) => (
|
||||||
|
<div key={label} className="flex items-center gap-1.5">
|
||||||
|
<div className="w-2 h-2 rounded-full flex-shrink-0" style={{ backgroundColor: color, boxShadow: `0 0 4px ${color}88` }} />
|
||||||
|
<span className="text-[7px] text-slate-600 tracking-wider">{label}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* WEBCAM MODAL */}
|
||||||
|
{webcamViewer && (
|
||||||
|
<div className="absolute inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm pointer-events-auto">
|
||||||
|
<div className="w-[800px] h-[500px] bg-slate-950 border border-slate-700 rounded-xl overflow-hidden shadow-2xl flex flex-col">
|
||||||
|
<div className="h-10 border-b border-slate-800 flex items-center justify-between px-4 bg-slate-900/80">
|
||||||
|
<span className="text-xs font-bold text-slate-200 tracking-wider uppercase flex items-center gap-2"><Camera size={13}/> LIVE SURVEILLANCE FEED — {webcamViewer.name || webcamViewer.location}</span>
|
||||||
|
<button onClick={() => setWebcamViewer(null)} className="text-slate-400 hover:text-white transition-colors"><X size={16}/></button>
|
||||||
|
</div>
|
||||||
|
<iframe src={webcamViewer.embed_url} className="flex-1 w-full border-none" allow="autoplay; fullscreen" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── UI COMPONENTS ──
|
||||||
|
function StatBadge({ icon, value, label, pulse }: { icon: React.ReactNode; value: number; label: string; pulse?: boolean }) {
|
||||||
|
return (
|
||||||
|
<div className={`flex items-center gap-1 ${pulse ? 'animate-pulse' : ''}`}>
|
||||||
|
{icon}
|
||||||
|
<span className="font-bold text-white">{value.toLocaleString()}</span>
|
||||||
|
<span className="text-slate-600 text-[8px]">{label}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function FeedTab({ icon, label, active, onClick }: { icon: React.ReactNode; label: string; active: boolean; onClick: () => void }) {
|
||||||
|
return (
|
||||||
|
<button onClick={onClick} className={`flex-1 py-2.5 text-[9px] font-bold uppercase tracking-widest transition-all border-b-2 flex items-center justify-center gap-1 ${active ? 'border-sky-500 text-sky-400 bg-sky-900/10' : 'border-transparent text-slate-600 hover:text-slate-400 hover:bg-white/3'}`}>
|
||||||
|
{icon}{label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function SectionHeader({ icon, label }: { icon: React.ReactNode; label: string }) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-1.5 text-[9px] font-bold text-sky-500 tracking-widest uppercase mb-1">
|
||||||
|
{icon}{label}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function WeightBadge({ weight }: { weight: string }) {
|
||||||
|
const cls = weight === 'CRITICAL' ? 'bg-red-500/20 text-red-400' : weight === 'HIGH' ? 'bg-orange-500/20 text-orange-400' : weight === 'ELEVATED' ? 'bg-amber-500/20 text-amber-400' : 'bg-slate-800 text-slate-500';
|
||||||
|
return <span className={`text-[7px] px-1.5 py-0.5 rounded font-black ${cls}`}>{weight}</span>;
|
||||||
|
}
|
||||||
|
function DetailBox({ label, value }: { label: string; value: string | number }) {
|
||||||
|
return (
|
||||||
|
<div className="bg-slate-900/60 border border-slate-800/80 rounded-lg p-2.5">
|
||||||
|
<span className="block text-[8px] text-slate-600 tracking-widest uppercase mb-1">{label}</span>
|
||||||
|
<span className="text-[11px] font-bold text-slate-200">{value}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function ToggleRow({ label, active, onClick }: { label: string; active: boolean; onClick: () => void }) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-between items-center cursor-pointer p-1.5 rounded hover:bg-white/5 transition-colors" onClick={onClick}>
|
||||||
|
<span className="text-[10px] text-slate-400">{label}</span>
|
||||||
|
<div className={`w-7 h-3.5 rounded-full p-0.5 transition-colors ${active ? 'bg-sky-500' : 'bg-slate-700'}`}>
|
||||||
|
<div className={`w-2.5 h-2.5 bg-white rounded-full transition-transform shadow-sm ${active ? 'translate-x-3.5' : ''}`}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 4.0 KiB |
@@ -0,0 +1,255 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import Globe from 'globe.gl';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import maplibregl from 'maplibre-gl';
|
||||||
|
import 'maplibre-gl/dist/maplibre-gl.css';
|
||||||
|
import { TileTextureManager } from '../utils/tileRenderer';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: { planes: any[]; satellites: any[]; news?: any[]; gpsInterference?: any[]; webcams?: any[]; ships?: any[]; aiAnalysis?: any; emergencySquawks?: any[]; conflicts?: any[]; cyber_attacks?: any[]; };
|
||||||
|
selectedItem: any;
|
||||||
|
onSelectItem: (item: any) => void;
|
||||||
|
onPovChange?: (pov: { lat: number; lng: number; altitude: number }) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEG_TO_RAD = Math.PI / 180;
|
||||||
|
const GLOBE_RADIUS = 100;
|
||||||
|
const MAP_STYLE = 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json';
|
||||||
|
|
||||||
|
function predictPlanePos(p: any, s: number) {
|
||||||
|
if (!p.velocity || p.heading == null || s <= 0 || s > 120) return p;
|
||||||
|
const cLat = Math.cos(p.lat * DEG_TO_RAD); if (Math.abs(cLat) < 0.001) return p;
|
||||||
|
return { ...p, lat: p.lat + (p.velocity * Math.cos(p.heading * DEG_TO_RAD) * s) / 111320, lon: (p.lon ?? p.lng) + (p.velocity * Math.sin(p.heading * DEG_TO_RAD) * s) / (111320 * cLat) };
|
||||||
|
}
|
||||||
|
function predictShipPos(p: any, s: number) {
|
||||||
|
if (!p.velocity || p.heading == null || s <= 0 || s > 600) return p;
|
||||||
|
const cLat = Math.cos(p.lat * DEG_TO_RAD); if (Math.abs(cLat) < 0.001) return p;
|
||||||
|
return { ...p, lat: p.lat + (p.velocity * Math.cos(p.heading * DEG_TO_RAD) * s) / 111320, lon: (p.lon ?? p.lng) + (p.velocity * Math.sin(p.heading * DEG_TO_RAD) * s) / (111320 * cLat) };
|
||||||
|
}
|
||||||
|
function predictSatPos(p: any, s: number) {
|
||||||
|
if (!isFinite(p.lat) || !isFinite(p.lon ?? p.lng)) return p;
|
||||||
|
return { ...p, lon: ((p.lon ?? p.lng) + (0.067 * s) + 180) % 360 - 180 };
|
||||||
|
}
|
||||||
|
function polar2xyz(lat: number, lng: number, alt: number): [number, number, number] {
|
||||||
|
const phi = (90 - lat) * DEG_TO_RAD, theta = (90 - lng) * DEG_TO_RAD, r = GLOBE_RADIUS * (1 + alt);
|
||||||
|
return [r * Math.sin(phi) * Math.cos(theta), r * Math.cos(phi), r * Math.sin(phi) * Math.sin(theta)];
|
||||||
|
}
|
||||||
|
function validCoord(la: any, lo: any) { return typeof la === 'number' && typeof lo === 'number' && isFinite(la) && isFinite(lo); }
|
||||||
|
function hexGeoJson(lat: number, lon: number, radiusDeg: number) {
|
||||||
|
const cosLat = Math.cos((lat * Math.PI) / 180); const verts: [number, number][] = [];
|
||||||
|
for (let i = 0; i < 6; i++) { const ang = ((i * 60 - 90) * Math.PI) / 180; verts.push([lon + (radiusDeg / cosLat) * Math.sin(ang), lat + radiusDeg * Math.cos(ang)]); }
|
||||||
|
verts.push(verts[0]); return { type: 'Polygon' as const, coordinates: [verts] };
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPlaneGeo() { const geo = new THREE.ConeGeometry(0.55, 2.2, 3); geo.rotateX(Math.PI / 2); return geo; }
|
||||||
|
function createShipGeo() { return new THREE.BoxGeometry(0.5, 0.35, 1.8); }
|
||||||
|
function createSatGeo() { return new THREE.OctahedronGeometry(0.75); }
|
||||||
|
|
||||||
|
// Altitude-coded aircraft colors (WorldView style)
|
||||||
|
function getPlaneColor(item: any): [number, number, number] {
|
||||||
|
if (item.military) return [1.0, 0.15, 0.05]; // red — military
|
||||||
|
const alt = item.alt || 0;
|
||||||
|
if (alt > 10000) return [0.0, 0.85, 1.0]; // cyan — high altitude
|
||||||
|
if (alt > 5000) return [0.2, 1.0, 0.5]; // green — cruise
|
||||||
|
if (alt > 2000) return [1.0, 0.7, 0.0]; // amber — low
|
||||||
|
if (alt > 500) return [1.0, 0.4, 0.0]; // orange — very low
|
||||||
|
return [1.0, 1.0, 1.0]; // white — ground/unknown
|
||||||
|
}
|
||||||
|
function getShipColor(item: any): [number, number, number] {
|
||||||
|
const t = (item.type || item.vessel_type || '').toLowerCase();
|
||||||
|
if (item.military || t.includes('military') || t.includes('naval')) return [1.0, 0.2, 0.1];
|
||||||
|
if (t.includes('tanker') || t.includes('lng')) return [1.0, 0.65, 0.0];
|
||||||
|
return [0.0, 0.7, 1.0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const GlobeView: React.FC<Props> = ({ data, selectedItem, onSelectItem, onPovChange }) => {
|
||||||
|
const globeRef = useRef<HTMLDivElement>(null), globeInstance = useRef<any>(null), mapContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [isLoaded, setIsLoaded] = useState(false), [viewMode, setViewMode] = useState<'globe' | 'map'>('globe'), [mapReady, setMapReady] = useState(false);
|
||||||
|
const viewModeRef = useRef<'globe' | 'map'>('globe');
|
||||||
|
const MAX_PLANES = 20000, MAX_SHIPS = 10000, MAX_SATS = 3000;
|
||||||
|
const pRef = useRef<any[]>([]), sRef = useRef<any[]>([]), saRef = useRef<any[]>([]), spRef = useRef<any[]>([]), ssRef = useRef<any[]>([]);
|
||||||
|
const pT = useRef(Date.now()), sT = useRef(Date.now()), saT = useRef(Date.now());
|
||||||
|
const pM = useRef<THREE.InstancedMesh>(null!), sM = useRef<THREE.InstancedMesh>(null!), saM = useRef<THREE.InstancedMesh>(null!), selM = useRef<THREE.Mesh>(null!);
|
||||||
|
const currentPov = useRef({ lat: 25, lng: 20, altitude: 2.2 });
|
||||||
|
const mapLibre = useRef<maplibregl.Map | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => { pRef.current = data.planes || []; pT.current = Date.now(); }, [data.planes]);
|
||||||
|
useEffect(() => { sRef.current = data.ships || []; sT.current = Date.now(); }, [data.ships]);
|
||||||
|
useEffect(() => { saRef.current = data.satellites || []; saT.current = Date.now(); }, [data.satellites]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
viewModeRef.current = viewMode;
|
||||||
|
// When returning to globe from map, reset altitude so controls
|
||||||
|
// don't immediately re-trigger the map condition (altitude < 0.35)
|
||||||
|
if (viewMode === 'globe' && globeInstance.current) {
|
||||||
|
const safeAlt = 1.5;
|
||||||
|
currentPov.current = { ...currentPov.current, altitude: safeAlt };
|
||||||
|
globeInstance.current.pointOfView({ lat: currentPov.current.lat, lng: currentPov.current.lng, altitude: safeAlt }, 800);
|
||||||
|
}
|
||||||
|
}, [viewMode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(window as any).__GODS_EYE_ON_SECTOR_DATA__ = (msg: any) => { spRef.current = msg.planes || []; ssRef.current = msg.ships || []; };
|
||||||
|
return () => { (window as any).__GODS_EYE_ON_SECTOR_DATA__ = null; };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isLoaded || viewMode !== 'globe') return;
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
const pov = currentPov.current;
|
||||||
|
if (pov.altitude > 1.2) { spRef.current = []; ssRef.current = []; return; }
|
||||||
|
const span = pov.altitude * 45;
|
||||||
|
(window as any).__GODS_EYE_REQUEST_SECTOR__?.({ lamin: Math.max(-90, pov.lat-span), lamax: Math.min(90, pov.lat+span), lomin: pov.lng-span*1.5, lomax: pov.lng+span*1.5 });
|
||||||
|
}, 2500);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [isLoaded, viewMode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!globeRef.current || globeInstance.current) return;
|
||||||
|
const tileMgr = new TileTextureManager();
|
||||||
|
const globe = new Globe(globeRef.current).width(window.innerWidth).height(window.innerHeight).backgroundColor('#020617').showGlobe(true)
|
||||||
|
.globeMaterial(new THREE.MeshStandardMaterial({ map: tileMgr.createTexture(), roughness: 0.9, metalness: 0.2 }))
|
||||||
|
.showAtmosphere(true).atmosphereColor('#0ea5e9').atmosphereAltitude(0.15);
|
||||||
|
globe.pointOfView({ lat: 25, lng: 20, altitude: 2.2 }, 0); globeInstance.current = globe;
|
||||||
|
const scene = globe.scene(); scene.add(new THREE.AmbientLight(0xffffff, 0.6));
|
||||||
|
const sun = new THREE.DirectionalLight(0xffffff, 3.0); sun.position.set(500, 500, 500); scene.add(sun);
|
||||||
|
const matPlane = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.2, metalness: 0.9, emissive: 0xffffff, emissiveIntensity: 0.4 });
|
||||||
|
const matShip = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.3, metalness: 0.7, emissive: 0xffffff, emissiveIntensity: 0.35 });
|
||||||
|
const matSat = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.1, metalness: 1.0, emissive: 0xffffff, emissiveIntensity: 0.5 });
|
||||||
|
pM.current = new THREE.InstancedMesh(createPlaneGeo(), matPlane, MAX_PLANES);
|
||||||
|
sM.current = new THREE.InstancedMesh(createShipGeo(), matShip, MAX_SHIPS);
|
||||||
|
saM.current = new THREE.InstancedMesh(createSatGeo(), matSat, MAX_SATS);
|
||||||
|
[pM.current, sM.current, saM.current].forEach((m) => { m.instanceMatrix.setUsage(THREE.DynamicDrawUsage); m.frustumCulled = false; scene.add(m); });
|
||||||
|
selM.current = new THREE.Mesh(new THREE.RingGeometry(1.5, 1.8, 32), new THREE.MeshBasicMaterial({ color: 0x00ffff, side: THREE.DoubleSide, transparent: true, opacity: 0.9 }));
|
||||||
|
selM.current.visible = false; scene.add(selM.current);
|
||||||
|
globe.controls().addEventListener('change', () => {
|
||||||
|
const pov = globe.pointOfView(); currentPov.current = pov;
|
||||||
|
onPovChange?.(pov);
|
||||||
|
if (pov.altitude < 0.35 && viewModeRef.current === 'globe') setViewMode('map');
|
||||||
|
});
|
||||||
|
const getHits = (e: PointerEvent) => {
|
||||||
|
const rect = globeRef.current!.getBoundingClientRect(), mouse = new THREE.Vector2(((e.clientX - rect.left)/rect.width)*2-1, -((e.clientY-rect.top)/rect.height)*2+1);
|
||||||
|
const rc = new THREE.Raycaster(); rc.setFromCamera(mouse, globe.camera());
|
||||||
|
return rc.intersectObjects([pM.current, sM.current, saM.current]);
|
||||||
|
};
|
||||||
|
globeRef.current!.addEventListener('pointerup', (e) => {
|
||||||
|
const hits = getHits(e); if (hits.length > 0 && hits[0].instanceId != null) (window as any).__GODS_EYE_HIT__ = { obj: hits[0].object, idx: hits[0].instanceId };
|
||||||
|
});
|
||||||
|
tileMgr.loadBaseLayer().catch(()=>{}); setIsLoaded(true);
|
||||||
|
return () => { if (typeof globe._destructor === 'function') globe._destructor(); globeInstance.current = null; };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isLoaded || !globeInstance.current) return;
|
||||||
|
const dummy = new THREE.Object3D(), up = new THREE.Vector3(0, 1, 0), basis = new THREE.Matrix4();
|
||||||
|
const rendered: {p: any[], s: any[], sa: any[]} = { p: [], s: [], sa: [] };
|
||||||
|
function animate() {
|
||||||
|
if (viewModeRef.current !== 'globe') { requestAnimationFrame(animate); return; }
|
||||||
|
const t = Date.now(), pe = (t-pT.current)/1000, se = (t-sT.current)/1000, sae = (t-saT.current)/1000, alt = currentPov.current.altitude;
|
||||||
|
const planes = [...pRef.current]; if(spRef.current.length) { const ids = new Set(planes.map(x=>x.id)); spRef.current.forEach(x=>{if(!ids.has(x.id)) planes.push(x);}); }
|
||||||
|
const ships = [...sRef.current]; if(ssRef.current.length) { const ids = new Set(ships.map(x=>x.id)); ssRef.current.forEach(x=>{if(!ids.has(x.id)) ships.push(x);}); }
|
||||||
|
const sats = saRef.current;
|
||||||
|
const lod = alt > 1.8 ? 10 : alt > 1.0 ? 3 : 1, isZ = alt < 1.2, pLat = currentPov.current.lat, pLng = currentPov.current.lng;
|
||||||
|
[ { m: pM.current, d: planes, t: pe, lim: MAX_PLANES, type: 'p', f: predictPlanePos, r: rendered.p },
|
||||||
|
{ m: sM.current, d: ships, t: se, lim: MAX_SHIPS, type: 's', f: predictShipPos, r: rendered.s },
|
||||||
|
{ m: saM.current, d: sats, t: sae, lim: MAX_SATS, type: 'sa', f: predictSatPos, r: rendered.sa }
|
||||||
|
].forEach(l => {
|
||||||
|
if (!l.m) return;
|
||||||
|
let count = 0; l.r.length = 0;
|
||||||
|
for (let i = 0; i < l.d.length && count < l.lim; i++) {
|
||||||
|
const item = l.d[i], lat = item.lat, lon = item.lon ?? item.lng;
|
||||||
|
if (!validCoord(lat, lon)) continue;
|
||||||
|
if (isZ && l.type !== 'sa' && (Math.abs(lat - pLat) > 60 || Math.abs(((lon - pLng + 540) % 360) - 180) > 70)) continue;
|
||||||
|
if (l.type !== 'sa' && !(item.military || item.vessel_type === 'Military') && (i % lod !== 0)) continue;
|
||||||
|
const p = l.f(item, l.t), [x, y, z] = polar2xyz(p.lat, p.lon ?? p.lng, l.type === 'sa' ? 0.15 : 0.02);
|
||||||
|
if (y === -1000) continue;
|
||||||
|
const pos = new THREE.Vector3(x, y, z), norm = pos.clone().normalize();
|
||||||
|
basis.makeBasis(new THREE.Vector3().crossVectors(up, norm).normalize(), new THREE.Vector3().crossVectors(norm, new THREE.Vector3().crossVectors(up, norm).normalize()).normalize(), norm);
|
||||||
|
dummy.quaternion.setFromRotationMatrix(basis); dummy.position.copy(pos); dummy.rotateZ(-(item.heading || 0) * DEG_TO_RAD);
|
||||||
|
const s = l.type === 'sa' ? 1.4 : (alt > 2.0 ? 1.6 : alt > 1.0 ? 1.2 : alt < 0.4 ? 0.6 : 0.9); dummy.scale.set(s, s, s);
|
||||||
|
dummy.updateMatrix(); l.m.setMatrixAt(count, dummy.matrix);
|
||||||
|
const c = l.type === 'p' ? getPlaneColor(item) : l.type === 's' ? getShipColor(item) : [0, 1, 0.5];
|
||||||
|
l.m.setColorAt(count, new THREE.Color(c[0], c[1], c[2])); l.r[count] = item; count++;
|
||||||
|
}
|
||||||
|
l.m.instanceMatrix.needsUpdate = true; if (l.m.instanceColor) l.m.instanceColor.needsUpdate = true; l.m.count = count;
|
||||||
|
});
|
||||||
|
if (selectedItem && validCoord(selectedItem.lat, selectedItem.lon ?? selectedItem.lng)) {
|
||||||
|
const [x, y, z] = polar2xyz(selectedItem.lat, selectedItem.lon ?? selectedItem.lng, selectedItem.dataType === 'satellite' ? 0.16 : 0.03);
|
||||||
|
selM.current.position.set(x, y, z); selM.current.lookAt(0, 0, 0); selM.current.visible = true;
|
||||||
|
} else selM.current.visible = false;
|
||||||
|
const hit = (window as any).__GODS_EYE_HIT__;
|
||||||
|
if (hit) {
|
||||||
|
if (hit.obj === pM.current) onSelectItem({ ...rendered.p[hit.idx], dataType: 'plane' });
|
||||||
|
else if (hit.obj === sM.current) onSelectItem({ ...rendered.s[hit.idx], dataType: 'ship' });
|
||||||
|
else if (hit.obj === saM.current) onSelectItem({ ...rendered.sa[hit.idx], dataType: 'satellite' });
|
||||||
|
(window as any).__GODS_EYE_HIT__ = null;
|
||||||
|
}
|
||||||
|
requestAnimationFrame(animate);
|
||||||
|
}
|
||||||
|
animate();
|
||||||
|
}, [isLoaded]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!globeInstance.current || !isLoaded) return;
|
||||||
|
const globe = globeInstance.current;
|
||||||
|
globe.arcsData(data.cyber_attacks || []).arcStartLat('startLat').arcStartLng('startLng').arcEndLat('endLat').arcEndLng('endLng').arcColor((d: any) => d.color || '#ff003c').arcDashLength(0.5).arcDashGap(1).arcDashAnimateTime(1000).arcStroke(0.6).arcAltitudeAutoScale(0.3);
|
||||||
|
const rData = [...(data.news || []).filter(n => n.lat).map(n => ({ ...n, _ringType: 'news' })), ...(data.emergencySquawks || []).filter(e => e.lat).map(e => ({ ...e, _ringType: 'emergency' }))];
|
||||||
|
globe.ringsData(rData).ringLat('lat').ringLng((d: any) => d.lon ?? d.lng).ringColor((d: any) => d._ringType === 'emergency' ? (t: number) => `rgba(255,0,50,${1-t})` : (t: number) => `rgba(14,165,233,${0.4*(1-t)})`).ringMaxRadius(4);
|
||||||
|
globe.polygonsData((data.gpsInterference || []).map(z => ({ ...z, _geo: hexGeoJson(z.lat, z.lon ?? z.lng, 1.2) }))).polygonGeoJsonGeometry((d: any) => d._geo).polygonCapColor(() => 'rgba(255,0,60,0.15)').polygonStrokeColor(() => '#ff003c').polygonAltitude(0.001);
|
||||||
|
}, [data.news, data.gpsInterference, data.cyber_attacks, isLoaded]);
|
||||||
|
|
||||||
|
// --- MAPLIBRE (2D) ---
|
||||||
|
useEffect(() => {
|
||||||
|
if (viewMode !== 'map' || !mapContainerRef.current) {
|
||||||
|
setMapReady(false);
|
||||||
|
if (mapLibre.current) { mapLibre.current.remove(); mapLibre.current = null; }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const map = new maplibregl.Map({ container: mapContainerRef.current, style: MAP_STYLE, center: [currentPov.current.lng, currentPov.current.lat], zoom: 9 });
|
||||||
|
map.on('zoom', () => { if (map.getZoom() < 4) setViewMode('globe'); });
|
||||||
|
map.on('moveend', () => { const c = map.getCenter(); currentPov.current = { ...currentPov.current, lat: c.lat, lng: c.lng }; });
|
||||||
|
map.on('load', () => {
|
||||||
|
map.addSource('p', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } });
|
||||||
|
map.addLayer({ id: 'p-layer', type: 'circle', source: 'p', paint: { 'circle-radius': 6, 'circle-color': '#eab308', 'circle-stroke-width': 1, 'circle-stroke-color': '#000' } });
|
||||||
|
map.addSource('s', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } });
|
||||||
|
map.addLayer({ id: 's-layer', type: 'circle', source: 's', paint: { 'circle-radius': 5, 'circle-color': '#0ea5e9', 'circle-stroke-width': 1, 'circle-stroke-color': '#000' } });
|
||||||
|
map.addSource('sa', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } });
|
||||||
|
map.addLayer({ id: 'sa-layer', type: 'circle', source: 'sa', paint: { 'circle-radius': 4, 'circle-color': '#22c55e', 'circle-stroke-width': 1, 'circle-stroke-color': '#000' } });
|
||||||
|
map.on('click', 'p-layer', (e) => { if (e.features?.length) onSelectItem({ ...e.features[0].properties, dataType: 'plane' }); });
|
||||||
|
map.on('click', 's-layer', (e) => { if (e.features?.length) onSelectItem({ ...e.features[0].properties, dataType: 'ship' }); });
|
||||||
|
map.on('click', 'sa-layer', (e) => { if (e.features?.length) onSelectItem({ ...e.features[0].properties, dataType: 'satellite' }); });
|
||||||
|
setMapReady(true);
|
||||||
|
});
|
||||||
|
mapLibre.current = map;
|
||||||
|
return () => { setMapReady(false); if (mapLibre.current) { mapLibre.current.remove(); mapLibre.current = null; } };
|
||||||
|
}, [viewMode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const map = mapLibre.current;
|
||||||
|
if (!mapReady || !map) return;
|
||||||
|
const pGeo: any = { type: 'FeatureCollection', features: (data.planes || []).filter(x => validCoord(x.lat, x.lon)).map(x => ({ type: 'Feature', geometry: { type: 'Point', coordinates: [x.lon, x.lat] }, properties: { ...x } })) };
|
||||||
|
const sGeo: any = { type: 'FeatureCollection', features: (data.ships || []).filter(x => validCoord(x.lat, x.lon??x.lng)).map(x => ({ type: 'Feature', geometry: { type: 'Point', coordinates: [x.lon??x.lng, x.lat] }, properties: { ...x } })) };
|
||||||
|
const saGeo: any = { type: 'FeatureCollection', features: (data.satellites || []).filter(x => validCoord(x.lat, x.lon??x.lng)).map(x => ({ type: 'Feature', geometry: { type: 'Point', coordinates: [x.lon??x.lng, x.lat] }, properties: { ...x } })) };
|
||||||
|
(map.getSource('p') as any)?.setData(pGeo);
|
||||||
|
(map.getSource('s') as any)?.setData(sGeo);
|
||||||
|
(map.getSource('sa') as any)?.setData(saGeo);
|
||||||
|
}, [mapReady, data.planes, data.ships, data.satellites]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="absolute inset-0 bg-[#020617]">
|
||||||
|
<div ref={globeRef} style={{ width: '100%', height: '100%', position: 'absolute', top: 0, left: 0, visibility: viewMode === 'globe' ? 'visible' : 'hidden', pointerEvents: viewMode === 'globe' ? 'auto' : 'none' }} />
|
||||||
|
<div ref={mapContainerRef} style={{ width: '100%', height: '100%', position: 'absolute', top: 0, zIndex: 10, display: viewMode === 'map' ? 'block' : 'none' }} />
|
||||||
|
{viewMode === 'map' && (
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode('globe')}
|
||||||
|
style={{ position: 'absolute', bottom: '48px', right: '12px', zIndex: 20 }}
|
||||||
|
className="flex items-center gap-1.5 px-3 py-1.5 bg-slate-900/90 border border-sky-500/50 text-sky-400 text-[10px] font-bold tracking-wider rounded-lg backdrop-blur-sm hover:bg-sky-500/20 transition-all shadow-lg"
|
||||||
|
>
|
||||||
|
↩ 3D Globe
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(GlobeView);
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
:root {
|
||||||
|
font-family: 'JetBrains Mono', 'SF Mono', 'Fira Code', 'Cascadia Code', ui-monospace, monospace;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
color-scheme: dark;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
background-color: #000;
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backdrop-blur-md {
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.backdrop-blur-xl {
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.backdrop-blur-2xl {
|
||||||
|
backdrop-filter: blur(30px);
|
||||||
|
-webkit-backdrop-filter: blur(30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar styling */
|
||||||
|
.scrollbar-thin::-webkit-scrollbar {
|
||||||
|
width: 3px;
|
||||||
|
}
|
||||||
|
.scrollbar-thin::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.scrollbar-thin::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(100, 200, 255, 0.15);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(100, 200, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Globe HTML marker animations */
|
||||||
|
.globe-marker {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
.globe-marker:hover {
|
||||||
|
z-index: 100 !important;
|
||||||
|
}
|
||||||
|
.globe-marker:hover img {
|
||||||
|
transform: scale(1.3);
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.globe-marker-conflict {
|
||||||
|
animation: conflictPulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.globe-marker-launch {
|
||||||
|
animation: launchGlow 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes conflictPulse {
|
||||||
|
0%, 100% { opacity: 0.9; }
|
||||||
|
50% { opacity: 0.6; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes launchGlow {
|
||||||
|
0%, 100% { filter: brightness(1); }
|
||||||
|
50% { filter: brightness(1.3); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { Component, StrictMode, type ReactNode } from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import './index.css'
|
||||||
|
import App from './App.tsx'
|
||||||
|
|
||||||
|
class RootErrorBoundary extends Component<{ children: ReactNode }, { hasError: boolean }> {
|
||||||
|
constructor(props: { children: ReactNode }) {
|
||||||
|
super(props);
|
||||||
|
this.state = { hasError: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromError() {
|
||||||
|
return { hasError: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error: Error) {
|
||||||
|
console.error('[UI] Fatal render error:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
return (
|
||||||
|
<div style={{ height: '100vh', display: 'grid', placeItems: 'center', background: '#000', color: '#93c5fd', fontFamily: 'monospace' }}>
|
||||||
|
<div style={{ textAlign: 'center' }}>
|
||||||
|
<p style={{ margin: 0, fontSize: '12px', textTransform: 'uppercase', letterSpacing: '0.08em' }}>UI Recovery Mode</p>
|
||||||
|
<p style={{ margin: '8px 0 0', fontSize: '11px', color: '#9ca3af' }}>Open devtools console for the runtime stack trace.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<RootErrorBoundary>
|
||||||
|
<App />
|
||||||
|
</RootErrorBoundary>
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
import * as THREE from 'three';
|
||||||
|
|
||||||
|
// CartoDB Dark Matter — high-performance tactical dark tiles
|
||||||
|
const IMG = 'https://a.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png';
|
||||||
|
// OpenStreetMap Roads (monochrome overlay style)
|
||||||
|
const RDS = 'https://a.basemaps.cartocdn.com/dark_only_labels/{z}/{x}/{y}.png';
|
||||||
|
|
||||||
|
function tileYToLat(y: number, z: number): number {
|
||||||
|
const n = 1 << z;
|
||||||
|
return Math.atan(Math.sinh(Math.PI * (1 - (2 * y) / n))) * (180 / Math.PI);
|
||||||
|
}
|
||||||
|
|
||||||
|
function latToTileY(lat: number, z: number): number {
|
||||||
|
const n = 1 << z;
|
||||||
|
const r = lat * (Math.PI / 180);
|
||||||
|
return Math.floor(
|
||||||
|
((1 - Math.log(Math.tan(r) + 1 / Math.cos(r)) / Math.PI) / 2) * n,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple tile texture for the 3D globe.
|
||||||
|
* Uses ESRI satellite imagery with road overlay.
|
||||||
|
* Deep zoom detail is handled by switching to a 2D Leaflet map.
|
||||||
|
*/
|
||||||
|
export class TileTextureManager {
|
||||||
|
private canvas: HTMLCanvasElement;
|
||||||
|
private ctx: CanvasRenderingContext2D;
|
||||||
|
private loaded = new Set<string>();
|
||||||
|
private loading = new Set<string>();
|
||||||
|
private texture: THREE.CanvasTexture | null = null;
|
||||||
|
private disposed = false;
|
||||||
|
private dirty = false;
|
||||||
|
private rafId = 0;
|
||||||
|
private W: number;
|
||||||
|
private H: number;
|
||||||
|
private lastArea = { lat: 0, lon: 0, z: 0 };
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// 4k is the "sweet spot" for globe textures — 8k often hangs the main thread on GPU upload
|
||||||
|
this.W = 4096;
|
||||||
|
this.H = 2048;
|
||||||
|
this.canvas = document.createElement('canvas');
|
||||||
|
this.canvas.width = this.W;
|
||||||
|
this.canvas.height = this.H;
|
||||||
|
const ctx = this.canvas.getContext('2d', { alpha: false });
|
||||||
|
if (!ctx) throw new Error('Canvas 2D unavailable');
|
||||||
|
this.ctx = ctx;
|
||||||
|
this.ctx.fillStyle = '#050a15';
|
||||||
|
this.ctx.fillRect(0, 0, this.W, this.H);
|
||||||
|
}
|
||||||
|
|
||||||
|
createTexture(): THREE.CanvasTexture {
|
||||||
|
this.texture = new THREE.CanvasTexture(this.canvas);
|
||||||
|
this.texture.colorSpace = THREE.SRGBColorSpace;
|
||||||
|
this.texture.minFilter = THREE.LinearMipmapLinearFilter;
|
||||||
|
this.texture.magFilter = THREE.LinearFilter;
|
||||||
|
this.texture.anisotropy = 4;
|
||||||
|
this.startDirtyLoop();
|
||||||
|
return this.texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadBaseLayer(): Promise<void> {
|
||||||
|
const z = 3;
|
||||||
|
const n = 1 << z;
|
||||||
|
const b: Promise<void>[] = [];
|
||||||
|
for (let x = 0; x < n; x++)
|
||||||
|
for (let y = 0; y < n; y++) b.push(this.fetch(x, y, z, IMG));
|
||||||
|
await Promise.allSettled(b);
|
||||||
|
this.dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateForView(alt: number, lat: number, lon: number): Promise<void> {
|
||||||
|
let tz: number;
|
||||||
|
if (alt > 2.5) tz = 3;
|
||||||
|
else if (alt > 1.5) tz = 4;
|
||||||
|
else if (alt > 0.8) tz = 5;
|
||||||
|
else if (alt > 0.4) tz = 6;
|
||||||
|
else if (alt > 0.18) tz = 7;
|
||||||
|
else if (alt > 0.08) tz = 8;
|
||||||
|
else tz = 9;
|
||||||
|
|
||||||
|
const p = this.lastArea;
|
||||||
|
if (tz === p.z && Math.abs(lat - p.lat) < 2 && Math.abs(lon - p.lon) < 2)
|
||||||
|
return;
|
||||||
|
this.lastArea = { lat, lon, z: tz };
|
||||||
|
if (tz <= 3) return;
|
||||||
|
|
||||||
|
const r = Math.max(alt * 50, 15);
|
||||||
|
await this.loadArea(lat, lon, r, tz, IMG);
|
||||||
|
if (tz >= 5) await this.loadArea(lat, lon, r, Math.min(tz, 8), RDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadArea(
|
||||||
|
cLat: number,
|
||||||
|
cLon: number,
|
||||||
|
r: number,
|
||||||
|
z: number,
|
||||||
|
url: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const n = 1 << z;
|
||||||
|
const minLat = Math.max(-85, cLat - r);
|
||||||
|
const maxLat = Math.min(85, cLat + r);
|
||||||
|
const yMin = Math.max(0, latToTileY(maxLat, z));
|
||||||
|
const yMax = Math.min(n - 1, latToTileY(minLat, z));
|
||||||
|
const xMin = Math.floor(((cLon - r + 180) / 360) * n);
|
||||||
|
const xMax = Math.floor(((cLon + r + 180) / 360) * n);
|
||||||
|
const tiles: { x: number, y: number, dist: number }[] = [];
|
||||||
|
|
||||||
|
// Calculate center tile coords to sort by distance
|
||||||
|
const cx = ((Math.floor(((cLon + 180) / 360) * n) % n) + n) % n;
|
||||||
|
const cy = latToTileY(cLat, z);
|
||||||
|
|
||||||
|
for (let tx = xMin; tx <= xMax; tx++) {
|
||||||
|
const x = ((tx % n) + n) % n;
|
||||||
|
for (let y = yMin; y <= yMax; y++) {
|
||||||
|
const pre = url === IMG ? 'I' : 'R';
|
||||||
|
const key = `${pre}${z}/${x}/${y}`;
|
||||||
|
if (!this.loaded.has(key) && !this.loading.has(key)) {
|
||||||
|
// Manhattan distance for sorting
|
||||||
|
const dist = Math.abs(x - cx) + Math.abs(y - cy);
|
||||||
|
tiles.push({ x, y, dist });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort tiles by distance from center
|
||||||
|
tiles.sort((a, b) => a.dist - b.dist);
|
||||||
|
|
||||||
|
// Process in batches of 6 (slightly smaller for better prioritization)
|
||||||
|
for (let i = 0; i < tiles.length; i += 6) {
|
||||||
|
const batch = tiles.slice(i, i + 6).map(({ x, y }) => this.fetch(x, y, z, url));
|
||||||
|
await Promise.allSettled(batch);
|
||||||
|
this.dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetch(x: number, y: number, z: number, base: string): Promise<void> {
|
||||||
|
const pre = base === IMG ? 'I' : 'R';
|
||||||
|
const key = `${pre}${z}/${x}/${y}`;
|
||||||
|
if (this.loaded.has(key) || this.loading.has(key)) return Promise.resolve();
|
||||||
|
this.loading.add(key);
|
||||||
|
|
||||||
|
const url = base
|
||||||
|
.replace('{z}', String(z))
|
||||||
|
.replace('{y}', String(y))
|
||||||
|
.replace('{x}', String(x));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cache = await caches.open('tile-cache-v1');
|
||||||
|
let response = await cache.match(url);
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
response = await fetch(url);
|
||||||
|
if (response.ok) {
|
||||||
|
await cache.put(url, response.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error(`${response.status}`);
|
||||||
|
|
||||||
|
const blob = await response.blob();
|
||||||
|
const bmp = await createImageBitmap(blob);
|
||||||
|
|
||||||
|
if (!this.disposed) {
|
||||||
|
this.drawBitmap(bmp, x, y, z);
|
||||||
|
this.loaded.add(key);
|
||||||
|
}
|
||||||
|
bmp.close();
|
||||||
|
this.loading.delete(key);
|
||||||
|
} catch (err) {
|
||||||
|
this.loading.delete(key);
|
||||||
|
// Fallback: if cache fails, try fetching directly
|
||||||
|
if (!this.disposed) {
|
||||||
|
// Silently fail or retry without cache
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawBitmap(bmp: ImageBitmap, x: number, y: number, z: number): void {
|
||||||
|
const n = 1 << z;
|
||||||
|
const lonL = (x / n) * 360 - 180;
|
||||||
|
const lonR = ((x + 1) / n) * 360 - 180;
|
||||||
|
const latT = tileYToLat(y, z);
|
||||||
|
const latB = tileYToLat(y + 1, z);
|
||||||
|
const px0 = Math.round(((lonL + 180) / 360) * this.W);
|
||||||
|
const px1 = Math.round(((lonR + 180) / 360) * this.W);
|
||||||
|
const py0 = Math.round(((90 - latT) / 180) * this.H);
|
||||||
|
const py1 = Math.round(((90 - latB) / 180) * this.H);
|
||||||
|
this.ctx.drawImage(bmp, px0, py0, px1 - px0 + 1, py1 - py0 + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private startDirtyLoop(): void {
|
||||||
|
let lastCheck = 0;
|
||||||
|
const tick = (now: number) => {
|
||||||
|
if (this.disposed) return;
|
||||||
|
// Throttle texture uploads to 5fps max (every 200ms) — uploading an 8k texture is expensive
|
||||||
|
if (this.dirty && this.texture && now - lastCheck > 200) {
|
||||||
|
this.texture.needsUpdate = true;
|
||||||
|
this.dirty = false;
|
||||||
|
lastCheck = now;
|
||||||
|
}
|
||||||
|
this.rafId = requestAnimationFrame(tick);
|
||||||
|
};
|
||||||
|
this.rafId = requestAnimationFrame(tick);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose(): void {
|
||||||
|
this.disposed = true;
|
||||||
|
cancelAnimationFrame(this.rafId);
|
||||||
|
this.loaded.clear();
|
||||||
|
this.loading.clear();
|
||||||
|
if (this.texture) { this.texture.dispose(); this.texture = null; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"types": ["vite/client"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2023",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"types": ["node"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
tailwindcss(),
|
||||||
|
],
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks: {
|
||||||
|
three: ['three'],
|
||||||
|
'globe-gl': ['globe.gl'],
|
||||||
|
'maplibre-gl': ['maplibre-gl'],
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
Generated
+329
@@ -0,0 +1,329 @@
|
|||||||
|
{
|
||||||
|
"name": "gods-eye-root",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "gods-eye-root",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"concurrently": "^9.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ansi-regex": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ansi-styles": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-convert": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/chalk": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.1.0",
|
||||||
|
"supports-color": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/chalk/node_modules/supports-color": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"has-flag": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cliui": {
|
||||||
|
"version": "8.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||||
|
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"string-width": "^4.2.0",
|
||||||
|
"strip-ansi": "^6.0.1",
|
||||||
|
"wrap-ansi": "^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/color-convert": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-name": "~1.1.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/color-name": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/concurrently": {
|
||||||
|
"version": "9.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz",
|
||||||
|
"integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"chalk": "4.1.2",
|
||||||
|
"rxjs": "7.8.2",
|
||||||
|
"shell-quote": "1.8.3",
|
||||||
|
"supports-color": "8.1.1",
|
||||||
|
"tree-kill": "1.2.2",
|
||||||
|
"yargs": "17.7.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"conc": "dist/bin/concurrently.js",
|
||||||
|
"concurrently": "dist/bin/concurrently.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/emoji-regex": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/escalade": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-caller-file": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": "6.* || 8.* || >= 10.*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-flag": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-fullwidth-code-point": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/require-directory": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rxjs": {
|
||||||
|
"version": "7.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
||||||
|
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/shell-quote": {
|
||||||
|
"version": "1.8.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
|
||||||
|
"integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/string-width": {
|
||||||
|
"version": "4.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||||
|
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"emoji-regex": "^8.0.0",
|
||||||
|
"is-fullwidth-code-point": "^3.0.0",
|
||||||
|
"strip-ansi": "^6.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/strip-ansi": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/supports-color": {
|
||||||
|
"version": "8.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
||||||
|
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"has-flag": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tree-kill": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"tree-kill": "cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
|
"node_modules/wrap-ansi": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.0.0",
|
||||||
|
"string-width": "^4.1.0",
|
||||||
|
"strip-ansi": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/y18n": {
|
||||||
|
"version": "5.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||||
|
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs": {
|
||||||
|
"version": "17.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||||
|
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cliui": "^8.0.1",
|
||||||
|
"escalade": "^3.1.1",
|
||||||
|
"get-caller-file": "^2.0.5",
|
||||||
|
"require-directory": "^2.1.1",
|
||||||
|
"string-width": "^4.2.3",
|
||||||
|
"y18n": "^5.0.5",
|
||||||
|
"yargs-parser": "^21.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs-parser": {
|
||||||
|
"version": "21.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||||
|
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "gods-eye-root",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Root management for God's Eye project",
|
||||||
|
"scripts": {
|
||||||
|
"install-all": "npm install && cd frontend && npm install && cd ../backend && python3 -m venv venv && source venv/bin/activate && pip install -r requirements.txt",
|
||||||
|
"backend": "cd backend && source venv/bin/activate && python3 main.py",
|
||||||
|
"frontend": "cd frontend && npm run dev",
|
||||||
|
"start": "concurrently --kill-others \"npm run backend\" \"npm run frontend\""
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"concurrently": "^9.2.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user