diff --git a/backend/data/plane_alert_db.json.REMOVED.git-id b/backend/data/plane_alert_db.json.REMOVED.git-id index a71d364..b1272cf 100644 --- a/backend/data/plane_alert_db.json.REMOVED.git-id +++ b/backend/data/plane_alert_db.json.REMOVED.git-id @@ -1 +1 @@ -eb32e2f229fad1d5a14fe81e071e71591f875673 \ No newline at end of file +38a18cbbf1acbec5eb9266b809c28d31e2941c53 \ No newline at end of file diff --git a/backend/services/data_fetcher.py b/backend/services/data_fetcher.py index c61960c..e1b0316 100644 --- a/backend/services/data_fetcher.py +++ b/backend/services/data_fetcher.py @@ -337,10 +337,22 @@ def enrich_with_tracked_names(flight: dict) -> dict: match = _TRACKED_NAMES_DB[callsign] if match: + name = match["name"] # Let Excel take precedence as it has cleaner individual names (e.g. Elon Musk instead of FALCON LANDING LLC). - flight["alert_operator"] = match["name"] + flight["alert_operator"] = name flight["alert_category"] = match["category"] - if "alert_color" not in flight: + + # Override pink default if the name implies a specific function + name_lower = name.lower() + is_gov = any(w in name_lower for w in ['state of ', 'government', 'republic', 'ministry', 'department', 'federal', 'cia']) + is_law = any(w in name_lower for w in ['police', 'marshal', 'sheriff', 'douane', 'customs', 'patrol', 'gendarmerie', 'guardia', 'law enforcement']) + is_med = any(w in name_lower for w in ['fire', 'bomberos', 'ambulance', 'paramedic', 'medevac', 'rescue', 'hospital', 'medical', 'lifeflight']) + + if is_gov or is_law: + flight["alert_color"] = "blue" + elif is_med: + flight["alert_color"] = "#32cd32" # lime + elif "alert_color" not in flight: flight["alert_color"] = "pink" return flight diff --git a/backend/services/region_dossier.py b/backend/services/region_dossier.py index 6cd12d7..48474c9 100644 --- a/backend/services/region_dossier.py +++ b/backend/services/region_dossier.py @@ -1,6 +1,8 @@ import logging +import time import concurrent.futures from urllib.parse import quote +import requests as _requests from cachetools import TTLCache from services.network_utils import fetch_with_curl @@ -10,26 +12,46 @@ logger = logging.getLogger(__name__) # Key: rounded lat/lng grid (0.1 degree ≈ 11km) dossier_cache = TTLCache(maxsize=500, ttl=86400) +# Nominatim requires max 1 req/sec — track last call time +_nominatim_last_call = 0.0 + def _reverse_geocode(lat: float, lng: float) -> dict: + global _nominatim_last_call url = ( f"https://nominatim.openstreetmap.org/reverse?" f"lat={lat}&lon={lng}&format=json&zoom=10&addressdetails=1&accept-language=en" ) - try: - res = fetch_with_curl(url, timeout=10) - if res.status_code == 200: - data = res.json() - addr = data.get("address", {}) - return { - "city": addr.get("city") or addr.get("town") or addr.get("village") or addr.get("county") or "", - "state": addr.get("state") or addr.get("region") or "", - "country": addr.get("country") or "", - "country_code": (addr.get("country_code") or "").upper(), - "display_name": data.get("display_name", ""), - } - except Exception as e: - logger.warning(f"Reverse geocode failed: {e}") + headers = {"User-Agent": "ShadowBroker-OSINT/1.0 (live-risk-dashboard; contact@shadowbroker.app)"} + + for attempt in range(2): + # Enforce Nominatim's 1 req/sec policy + elapsed = time.time() - _nominatim_last_call + if elapsed < 1.1: + time.sleep(1.1 - elapsed) + _nominatim_last_call = time.time() + + try: + # Use requests directly — fetch_with_curl raises on non-200 which breaks 429 handling + res = _requests.get(url, timeout=10, headers=headers) + if res.status_code == 200: + data = res.json() + addr = data.get("address", {}) + return { + "city": addr.get("city") or addr.get("town") or addr.get("village") or addr.get("county") or "", + "state": addr.get("state") or addr.get("region") or "", + "country": addr.get("country") or "", + "country_code": (addr.get("country_code") or "").upper(), + "display_name": data.get("display_name", ""), + } + elif res.status_code == 429: + logger.warning(f"Nominatim 429 rate-limited, retrying after 2s (attempt {attempt+1})") + time.sleep(2) + continue + else: + logger.warning(f"Nominatim returned {res.status_code}") + except Exception as e: + logger.warning(f"Reverse geocode failed: {e}") return {} diff --git a/frontend/src/components/CesiumViewer.tsx b/frontend/src/components/CesiumViewer.tsx index 9765e46..f565164 100644 --- a/frontend/src/components/CesiumViewer.tsx +++ b/frontend/src/components/CesiumViewer.tsx @@ -656,13 +656,11 @@ export default function CesiumViewer({ data, activeLayers, activeFilters, effect } if (filters.tracked_owner?.length) { const op = (f.alert_operator || '').toLowerCase(); - const t1 = (f.alert_tag1 || '').toLowerCase(); - const t2 = (f.alert_tag2 || '').toLowerCase(); - const t3 = (f.alert_tag3 || '').toLowerCase(); + const tags = (f.alert_tags || '').toLowerCase(); const cs = (f.callsign || '').toLowerCase(); if (!filters.tracked_owner.some(sv => { const q = sv.toLowerCase(); - return op.includes(q) || t1.includes(q) || t2.includes(q) || t3.includes(q) || cs.includes(q); + return op.includes(q) || tags.includes(q) || cs.includes(q); })) return false; } return true; diff --git a/frontend/src/components/FilterPanel.tsx b/frontend/src/components/FilterPanel.tsx index 0efd21a..75c2fa5 100644 --- a/frontend/src/components/FilterPanel.tsx +++ b/frontend/src/components/FilterPanel.tsx @@ -106,8 +106,7 @@ export default function FilterPanel({ data, activeFilters, setActiveFilters }: F const ops = new Set(trackedOperators); for (const f of data?.tracked_flights || []) { if (f.alert_operator) ops.add(f.alert_operator); - if (f.alert_tag1) ops.add(f.alert_tag1); - if (f.alert_tag2) ops.add(f.alert_tag2); + if (f.alert_tags) ops.add(f.alert_tags); } return Array.from(ops).sort(); }, [data?.tracked_flights]); diff --git a/frontend/src/components/MapLegend.tsx b/frontend/src/components/MapLegend.tsx index bd4f801..803ea21 100644 --- a/frontend/src/components/MapLegend.tsx +++ b/frontend/src/components/MapLegend.tsx @@ -101,10 +101,23 @@ const LEGEND: LegendCategory[] = [ name: "TRACKED AIRCRAFT (ALERT)", color: "text-pink-400 border-pink-500/30", items: [ - { svg: airliner("#FF1493"), label: "Alert — Low Priority (pink)" }, - { svg: airliner("#FF2020"), label: "Alert — High Priority (red)" }, - { svg: airliner("#1A3A8A"), label: "Alert — Government (navy)" }, - { svg: airliner("white"), label: "Alert — General (white)" }, + { svg: airliner("#FF1493"), label: "VIP / Celebrity / Bizjet (hot pink)" }, + { svg: airliner("#FF2020"), label: "Dictator / Oligarch (red)" }, + { svg: airliner("#3b82f6"), label: "Government / Police / Customs (blue)" }, + { svg: heli("#32CD32"), label: "Medical / Fire / Rescue (lime)" }, + { svg: airliner("yellow"), label: "Military / Intelligence (yellow)" }, + { svg: airliner("#222"), label: "PIA — Privacy / Stealth (black)" }, + { svg: airliner("#FF8C00"), label: "Private Flights / Joe Cool (orange)" }, + { svg: airliner("white"), label: "Climate Crisis (white)" }, + { svg: airliner("#9B59B6"), label: "Private Jets / Historic / Other (purple)" }, + ], + }, + { + name: "POTUS FLEET", + color: "text-yellow-400 border-yellow-500/30", + items: [ + { svg: ``, label: "Air Force One / Two (gold ring)" }, + { svg: ``, label: "Marine One (gold ring + heli)" }, ], }, { @@ -141,6 +154,14 @@ const LEGEND: LegendCategory[] = [ { svg: circle("#ff6600"), label: "Earthquake (size = magnitude)" }, ], }, + { + name: "WILDFIRES", + color: "text-red-400 border-red-500/30", + items: [ + { svg: ``, label: "Active wildfire / hotspot" }, + { svg: clusterCircle("#cc0000", "#ff3300"), label: "Fire cluster (grouped hotspots)" }, + ], + }, { name: "INCIDENTS & INTELLIGENCE", color: "text-red-400 border-red-500/30", @@ -166,6 +187,14 @@ const LEGEND: LegendCategory[] = [ { svg: ``, label: "Low severity (25-50% degraded)" }, ], }, + { + name: "INFRASTRUCTURE", + color: "text-purple-400 border-purple-500/30", + items: [ + { svg: ``, label: "Data Center" }, + { svg: circle("#ef4444"), label: "Internet Outage Zone" }, + ], + }, { name: "SURVEILLANCE / CCTV", color: "text-green-400 border-green-500/30", diff --git a/frontend/src/components/MaplibreViewer.tsx b/frontend/src/components/MaplibreViewer.tsx index 3ba9b59..206d802 100644 --- a/frontend/src/components/MaplibreViewer.tsx +++ b/frontend/src/components/MaplibreViewer.tsx @@ -2723,64 +2723,209 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele )} - {/* SENTINEL-2 IMAGERY — floating intel card on map near right-click */} - {selectedEntity?.type === 'region_dossier' && selectedEntity.extra && regionDossier?.sentinel2 && !regionDossierLoading && ( - -
- {/* Header bar */} -
-
-
- SENTINEL-2 IMAGERY + {/* SENTINEL-2 IMAGERY — fullscreen overlay modal */} + {selectedEntity?.type === 'region_dossier' && selectedEntity.extra && regionDossier?.sentinel2 && !regionDossierLoading && (() => { + const s2 = regionDossier.sentinel2; + const imgUrl = s2.fullres_url || s2.thumbnail_url; + return ( +
{ if (e.target === e.currentTarget) onEntityClick(null); }} + onKeyDown={(e: any) => { if (e.key === 'Escape') onEntityClick(null); }} + tabIndex={-1} + ref={(el) => el?.focus()} + > +
+ {/* Header bar */} +
+
+
+ + SENTINEL-2 IMAGERY + +
+
+ + {selectedEntity.extra.lat.toFixed(4)}, {selectedEntity.extra.lng.toFixed(4)} + + +
- {selectedEntity.extra.lat.toFixed(3)}, {selectedEntity.extra.lng.toFixed(3)} + + {s2.found ? ( + <> + {/* Metadata row */} +
+ {s2.platform} + {s2.datetime?.slice(0, 10)} + {s2.cloud_cover?.toFixed(0)}% cloud +
+ + {/* Image */} + {imgUrl ? ( +
+ Sentinel-2 scene +
+ ) : ( +
+ Scene found — no preview available +
+ )} + + {/* Action buttons */} + {imgUrl && ( +
+ + ⬇ DOWNLOAD + + + + ↗ OPEN FULL RES + +
+ )} + + ) : ( +
+ No clear imagery in last 30 days +
+ )}
- - {regionDossier.sentinel2.found ? ( - <> - {/* Metadata row */} -
- {regionDossier.sentinel2.platform} - {regionDossier.sentinel2.datetime?.slice(0, 10)} - {regionDossier.sentinel2.cloud_cover?.toFixed(0)}% cloud -
- - {/* Thumbnail */} - {regionDossier.sentinel2.thumbnail_url ? ( - - Sentinel-2 scene - - ) : ( -
Scene found — no preview available
- )} - - {/* Footer */} -
- CLICK IMAGE TO OPEN FULL RESOLUTION -
- - ) : ( -
- No clear imagery in last 30 days -
- )}
- - )} + ); + })()} {/* MEASUREMENT LINES */} {measurePoints && measurePoints.length >= 2 && (