mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-04-24 03:26:06 +02:00
feat: add FIRMS thermal, space weather, radiation, and internet outage layers
Add 4 new intelligence layers for v0.5: - NASA FIRMS VIIRS thermal anomaly tiles (frontend-only WMTS) - NOAA Space Weather Kp index badge in bottom bar - Safecast radiation monitoring with clustered markers - IODA internet outage alerts at country centroids All use free keyless APIs. All layers default to off. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+4
-1
@@ -93,7 +93,10 @@ async def live_data_slow(request: Request):
|
||||
"gdelt": d.get("gdelt", []),
|
||||
"airports": d.get("airports", []),
|
||||
"satellites": d.get("satellites", []),
|
||||
"kiwisdr": d.get("kiwisdr", [])
|
||||
"kiwisdr": d.get("kiwisdr", []),
|
||||
"space_weather": d.get("space_weather"),
|
||||
"radiation": d.get("radiation", []),
|
||||
"internet_outages": d.get("internet_outages", [])
|
||||
}
|
||||
# ETag based on last_updated + item counts
|
||||
last_updated = d.get("last_updated", "")
|
||||
|
||||
@@ -101,7 +101,10 @@ latest_data = {
|
||||
"frontlines": None,
|
||||
"gdelt": [],
|
||||
"liveuamap": [],
|
||||
"kiwisdr": []
|
||||
"kiwisdr": [],
|
||||
"space_weather": None,
|
||||
"radiation": [],
|
||||
"internet_outages": []
|
||||
}
|
||||
|
||||
# Thread lock for safe reads/writes to latest_data
|
||||
@@ -1269,6 +1272,111 @@ def fetch_kiwisdr():
|
||||
logger.error(f"Error fetching KiwiSDR nodes: {e}")
|
||||
latest_data["kiwisdr"] = []
|
||||
|
||||
def fetch_space_weather():
|
||||
"""Fetch NOAA SWPC Kp index and recent solar events."""
|
||||
try:
|
||||
kp_resp = fetch_with_curl("https://services.swpc.noaa.gov/json/planetary_k_index_1m.json", timeout=10)
|
||||
kp_value = None
|
||||
kp_text = "QUIET"
|
||||
if kp_resp.status_code == 200:
|
||||
kp_data = kp_resp.json()
|
||||
if kp_data:
|
||||
latest_kp = kp_data[-1]
|
||||
kp_value = float(latest_kp.get("kp_index", 0))
|
||||
if kp_value >= 7:
|
||||
kp_text = f"STORM G{min(int(kp_value) - 4, 5)}"
|
||||
elif kp_value >= 5:
|
||||
kp_text = f"STORM G{min(int(kp_value) - 4, 5)}"
|
||||
elif kp_value >= 4:
|
||||
kp_text = "ACTIVE"
|
||||
elif kp_value >= 3:
|
||||
kp_text = "UNSETTLED"
|
||||
|
||||
events = []
|
||||
ev_resp = fetch_with_curl("https://services.swpc.noaa.gov/json/edited_events.json", timeout=10)
|
||||
if ev_resp.status_code == 200:
|
||||
all_events = ev_resp.json()
|
||||
for ev in all_events[-10:]:
|
||||
events.append({
|
||||
"type": ev.get("type", ""),
|
||||
"begin": ev.get("begin", ""),
|
||||
"end": ev.get("end", ""),
|
||||
"classtype": ev.get("classtype", ""),
|
||||
})
|
||||
|
||||
latest_data["space_weather"] = {
|
||||
"kp_index": kp_value,
|
||||
"kp_text": kp_text,
|
||||
"events": events,
|
||||
}
|
||||
logger.info(f"Space weather: Kp={kp_value} ({kp_text}), {len(events)} events")
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching space weather: {e}")
|
||||
|
||||
def fetch_radiation():
|
||||
"""Fetch global radiation measurements from Safecast (CC0, no key)."""
|
||||
measurements = []
|
||||
try:
|
||||
url = "https://api.safecast.org/en-US/measurements.json?distance=10000&latitude=0&longitude=0"
|
||||
response = fetch_with_curl(url, timeout=15)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
for m in data:
|
||||
lat = m.get("latitude")
|
||||
lng = m.get("longitude")
|
||||
value = m.get("value")
|
||||
if lat is None or lng is None or value is None:
|
||||
continue
|
||||
measurements.append({
|
||||
"lat": lat,
|
||||
"lng": lng,
|
||||
"cpm": value,
|
||||
"captured_at": m.get("captured_at", ""),
|
||||
})
|
||||
measurements = measurements[:500]
|
||||
logger.info(f"Radiation: {len(measurements)} sensors")
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching radiation data: {e}")
|
||||
latest_data["radiation"] = measurements
|
||||
|
||||
def fetch_internet_outages():
|
||||
"""Fetch internet outage alerts from IODA (Georgia Tech)."""
|
||||
outages = []
|
||||
try:
|
||||
now = int(time.time())
|
||||
start = now - 86400
|
||||
url = f"https://api.ioda.inetintel.cc.gatech.edu/v2/outages/alerts?from={start}&until={now}"
|
||||
response = fetch_with_curl(url, timeout=15)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
alerts = data.get("data", [])
|
||||
for alert in alerts:
|
||||
entity = alert.get("entity", {})
|
||||
if entity.get("type") != "country":
|
||||
continue
|
||||
code = entity.get("code", "")
|
||||
name = entity.get("name", "")
|
||||
level = alert.get("level", "")
|
||||
score = alert.get("condition", alert.get("score", 0))
|
||||
if level == "normal":
|
||||
continue
|
||||
outages.append({
|
||||
"country_code": code,
|
||||
"country_name": name,
|
||||
"level": level,
|
||||
"score": score if isinstance(score, (int, float)) else 0,
|
||||
})
|
||||
seen = {}
|
||||
for o in outages:
|
||||
cc = o["country_code"]
|
||||
if cc not in seen or o["score"] > seen[cc]["score"]:
|
||||
seen[cc] = o
|
||||
outages = list(seen.values())[:100]
|
||||
logger.info(f"Internet outages: {len(outages)} countries affected")
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching internet outages: {e}")
|
||||
latest_data["internet_outages"] = outages
|
||||
|
||||
def fetch_bikeshare():
|
||||
bikes = []
|
||||
try:
|
||||
@@ -1825,6 +1933,9 @@ def update_slow_data():
|
||||
fetch_earthquakes,
|
||||
fetch_geopolitics,
|
||||
fetch_kiwisdr,
|
||||
fetch_space_weather,
|
||||
fetch_radiation,
|
||||
fetch_internet_outages,
|
||||
]
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=len(slow_funcs)) as executor:
|
||||
futures = [executor.submit(func) for func in slow_funcs]
|
||||
|
||||
@@ -146,6 +146,9 @@ export default function Dashboard() {
|
||||
gibs_imagery: false,
|
||||
highres_satellite: false,
|
||||
kiwisdr: false,
|
||||
firms: false,
|
||||
radiation: false,
|
||||
internet_outages: false,
|
||||
});
|
||||
|
||||
// NASA GIBS satellite imagery state
|
||||
@@ -511,6 +514,21 @@ export default function Dashboard() {
|
||||
<div className="text-[8px] text-[var(--text-muted)] font-mono tracking-[0.2em]">STYLE</div>
|
||||
<div className="text-[11px] text-cyan-400 font-mono font-bold">{activeStyle}</div>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="w-px h-8 bg-[var(--border-primary)]" />
|
||||
|
||||
{/* Space Weather */}
|
||||
<div className="flex flex-col items-center" title={`Kp Index: ${data?.space_weather?.kp_index ?? 'N/A'}`}>
|
||||
<div className="text-[8px] text-[var(--text-muted)] font-mono tracking-[0.2em]">SOLAR</div>
|
||||
<div className={`text-[11px] font-mono font-bold ${
|
||||
(data?.space_weather?.kp_index ?? 0) >= 5 ? 'text-red-400' :
|
||||
(data?.space_weather?.kp_index ?? 0) >= 4 ? 'text-yellow-400' :
|
||||
'text-green-400'
|
||||
}`}>
|
||||
{data?.space_weather?.kp_text || 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</>
|
||||
|
||||
@@ -411,6 +411,53 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
|
||||
};
|
||||
}, [activeLayers.kiwisdr, data?.kiwisdr, inView]);
|
||||
|
||||
// Radiation monitors — green/red dots based on CPM level
|
||||
const radiationGeoJSON = useMemo(() => {
|
||||
if (!activeLayers.radiation || !data?.radiation?.length) return null;
|
||||
return {
|
||||
type: 'FeatureCollection' as const,
|
||||
features: data.radiation.filter((r: any) => r.lat != null && r.lng != null).map((r: any, i: number) => ({
|
||||
type: 'Feature' as const,
|
||||
properties: { id: i, type: 'radiation', cpm: r.cpm || 0, captured_at: r.captured_at || '' },
|
||||
geometry: { type: 'Point' as const, coordinates: [r.lng, r.lat] }
|
||||
}))
|
||||
};
|
||||
}, [activeLayers.radiation, data?.radiation]);
|
||||
|
||||
// Internet outages — country centroids
|
||||
const COUNTRY_CENTROIDS: Record<string, [number, number]> = {
|
||||
'AF': [67.7, 33.9], 'AL': [20.2, 41.2], 'DZ': [1.7, 28.0], 'AO': [17.9, -11.2], 'AR': [-63.6, -38.4],
|
||||
'AM': [45.0, 40.1], 'AU': [133.8, -25.3], 'AZ': [47.6, 40.1], 'BD': [90.4, 23.7], 'BY': [27.9, 53.7],
|
||||
'BR': [-51.9, -14.2], 'MM': [96.0, 21.9], 'KH': [105.0, 12.6], 'CM': [12.4, 7.4], 'CA': [-106.3, 56.1],
|
||||
'CF': [20.9, 6.6], 'TD': [18.7, 15.5], 'CL': [-71.5, -35.7], 'CN': [104.2, 35.9], 'CO': [-74.3, 4.6],
|
||||
'CD': [21.8, -4.0], 'CU': [-77.8, 21.5], 'EG': [30.8, 26.8], 'ET': [40.5, 9.1], 'FR': [2.2, 46.2],
|
||||
'DE': [10.5, 51.2], 'GH': [-1.0, 7.9], 'GR': [21.8, 39.1], 'HT': [-72.3, 19.1], 'IN': [78.9, 20.6],
|
||||
'ID': [113.9, -0.8], 'IR': [53.7, 32.4], 'IQ': [43.7, 33.2], 'IL': [34.9, 31.0], 'IT': [12.6, 41.9],
|
||||
'JP': [138.3, 36.2], 'JO': [36.2, 30.6], 'KZ': [67.0, 48.0], 'KE': [37.9, -0.0], 'KP': [127.5, 40.3],
|
||||
'KR': [128.0, 35.9], 'KW': [47.5, 29.3], 'LB': [35.9, 33.9], 'LY': [17.2, 26.3], 'MX': [-102.6, 23.6],
|
||||
'MA': [-7.1, 31.8], 'MZ': [35.5, -18.7], 'NG': [8.7, 9.1], 'PK': [69.3, 30.4], 'PS': [35.2, 31.9],
|
||||
'PH': [122.0, 12.9], 'PL': [19.1, 51.9], 'RU': [105.3, 61.5], 'SA': [45.1, 23.9], 'SD': [30.2, 12.9],
|
||||
'SO': [46.2, 5.2], 'ZA': [22.9, -30.6], 'SS': [31.3, 6.9], 'SY': [38.0, 35.0], 'TW': [121.0, 23.7],
|
||||
'TZ': [34.9, -6.4], 'TH': [100.5, 15.9], 'TR': [35.2, 38.9], 'UA': [31.2, 48.4], 'AE': [53.8, 23.4],
|
||||
'GB': [-3.4, 55.4], 'US': [-98.5, 39.8], 'UZ': [64.6, 41.4], 'VE': [-66.6, 6.4], 'VN': [108.3, 14.1],
|
||||
'YE': [48.5, 15.6], 'ZW': [29.2, -19.0],
|
||||
};
|
||||
const internetOutagesGeoJSON = useMemo(() => {
|
||||
if (!activeLayers.internet_outages || !data?.internet_outages?.length) return null;
|
||||
return {
|
||||
type: 'FeatureCollection' as const,
|
||||
features: data.internet_outages.map((o: any) => {
|
||||
const coords = COUNTRY_CENTROIDS[o.country_code];
|
||||
if (!coords) return null;
|
||||
return {
|
||||
type: 'Feature' as const,
|
||||
properties: { country: o.country_name, level: o.level, score: o.score || 0 },
|
||||
geometry: { type: 'Point' as const, coordinates: coords }
|
||||
};
|
||||
}).filter(Boolean)
|
||||
};
|
||||
}, [activeLayers.internet_outages, data?.internet_outages]);
|
||||
|
||||
// Load Images into the Map Style once loaded
|
||||
const onMapLoad = useCallback((e: any) => {
|
||||
const map = e.target;
|
||||
@@ -1145,7 +1192,9 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
|
||||
earthquakesGeoJSON && 'earthquakes-layer',
|
||||
satellitesGeoJSON && 'satellites-layer',
|
||||
cctvGeoJSON && 'cctv-layer',
|
||||
kiwisdrGeoJSON && 'kiwisdr-layer'
|
||||
kiwisdrGeoJSON && 'kiwisdr-layer',
|
||||
radiationGeoJSON && 'radiation-layer',
|
||||
internetOutagesGeoJSON && 'internet-outages-layer'
|
||||
].filter(Boolean) as string[];
|
||||
|
||||
|
||||
@@ -1249,6 +1298,27 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
|
||||
</Source>
|
||||
)}
|
||||
|
||||
{/* NASA FIRMS VIIRS — thermal anomalies / wildfires overlay */}
|
||||
{activeLayers.firms && gibsDate && (
|
||||
<Source
|
||||
key={`firms-${gibsDate}`}
|
||||
id="firms-viirs"
|
||||
type="raster"
|
||||
tiles={[`https://gibs.earthdata.nasa.gov/wmts/epsg3857/best/VIIRS_NOAA20_Thermal_Anomalies_375m_All/default/${gibsDate}/GoogleMapsCompatible_Level9/{z}/{y}/{x}.png`]}
|
||||
tileSize={256}
|
||||
maxzoom={9}
|
||||
>
|
||||
<Layer
|
||||
id="firms-viirs-layer"
|
||||
type="raster"
|
||||
paint={{
|
||||
'raster-opacity': 0.9,
|
||||
'raster-fade-duration': 300
|
||||
}}
|
||||
/>
|
||||
</Source>
|
||||
)}
|
||||
|
||||
{/* SOLAR TERMINATOR — night overlay */}
|
||||
{activeLayers.day_night && nightGeoJSON && (
|
||||
<Source id="night-overlay" type="geojson" data={nightGeoJSON as any}>
|
||||
@@ -1906,6 +1976,63 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
|
||||
</Source>
|
||||
)}
|
||||
|
||||
{/* Radiation Monitors — green/red clustered dots */}
|
||||
{radiationGeoJSON && (
|
||||
<Source id="radiation" type="geojson" data={radiationGeoJSON as any} cluster={true} clusterRadius={50} clusterMaxZoom={12}>
|
||||
<Layer
|
||||
id="radiation-clusters"
|
||||
type="circle"
|
||||
filter={['has', 'point_count']}
|
||||
paint={{
|
||||
'circle-color': 'rgba(0, 255, 100, 0.7)',
|
||||
'circle-radius': ['step', ['get', 'point_count'], 10, 5, 14, 10, 18],
|
||||
'circle-stroke-width': 1,
|
||||
'circle-stroke-color': 'rgba(0, 255, 100, 1.0)'
|
||||
}}
|
||||
/>
|
||||
<Layer
|
||||
id="radiation-cluster-count"
|
||||
type="symbol"
|
||||
filter={['has', 'point_count']}
|
||||
layout={{ 'text-field': '{point_count_abbreviated}', 'text-size': 10, 'text-allow-overlap': true }}
|
||||
paint={{ 'text-color': '#ffffff', 'text-halo-color': '#000000', 'text-halo-width': 1 }}
|
||||
/>
|
||||
<Layer
|
||||
id="radiation-layer"
|
||||
type="circle"
|
||||
filter={['!', ['has', 'point_count']]}
|
||||
paint={{
|
||||
'circle-radius': 5,
|
||||
'circle-color': ['case', ['>', ['get', 'cpm'], 100], '#ff2222', '#00ff66'],
|
||||
'circle-stroke-width': 1,
|
||||
'circle-stroke-color': ['case', ['>', ['get', 'cpm'], 100], '#ff4444', '#00cc55'],
|
||||
'circle-opacity': 0.8
|
||||
}}
|
||||
/>
|
||||
</Source>
|
||||
)}
|
||||
|
||||
{/* Internet Outages — country-level markers */}
|
||||
{internetOutagesGeoJSON && (
|
||||
<Source id="internet-outages" type="geojson" data={internetOutagesGeoJSON as any}>
|
||||
<Layer
|
||||
id="internet-outages-layer"
|
||||
type="circle"
|
||||
paint={{
|
||||
'circle-radius': 12,
|
||||
'circle-color': ['case',
|
||||
['>=', ['get', 'score'], 80], '#ff0040',
|
||||
['>=', ['get', 'score'], 50], '#ff6600',
|
||||
'#888888'
|
||||
],
|
||||
'circle-stroke-width': 2,
|
||||
'circle-stroke-color': '#ffffff',
|
||||
'circle-opacity': 0.8
|
||||
}}
|
||||
/>
|
||||
</Source>
|
||||
)}
|
||||
|
||||
{/* Satellite positions — mission-type icons */}
|
||||
{satellitesGeoJSON && (
|
||||
<Source id="satellites" type="geojson" data={satellitesGeoJSON as any}>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Plane, AlertTriangle, Activity, Satellite, Cctv, ChevronDown, ChevronUp, Ship, Eye, Anchor, Settings, Sun, Moon, BookOpen, Radio, Play, Pause, Globe } from "lucide-react";
|
||||
import { Plane, AlertTriangle, Activity, Satellite, Cctv, ChevronDown, ChevronUp, Ship, Eye, Anchor, Settings, Sun, Moon, BookOpen, Radio, Play, Pause, Globe, Flame, Wifi } from "lucide-react";
|
||||
import { useTheme } from "@/lib/ThemeContext";
|
||||
|
||||
const WorldviewLeftPanel = React.memo(function WorldviewLeftPanel({ data, activeLayers, setActiveLayers, onSettingsClick, onLegendClick, gibsDate, setGibsDate, gibsOpacity, setGibsOpacity }: { data: any; activeLayers: any; setActiveLayers: any; onSettingsClick?: () => void; onLegendClick?: () => void; gibsDate?: string; setGibsDate?: (d: string) => void; gibsOpacity?: number; setGibsOpacity?: (o: number) => void }) {
|
||||
@@ -58,6 +58,9 @@ const WorldviewLeftPanel = React.memo(function WorldviewLeftPanel({ data, active
|
||||
{ id: "gibs_imagery", name: "MODIS Terra (Daily)", source: "NASA GIBS", count: null, icon: Globe },
|
||||
{ id: "highres_satellite", name: "High-Res Satellite", source: "Esri World Imagery", count: null, icon: Satellite },
|
||||
{ id: "kiwisdr", name: "KiwiSDR Receivers", source: "KiwiSDR.com", count: data?.kiwisdr?.length || 0, icon: Radio },
|
||||
{ id: "firms", name: "Thermal Anomalies", source: "NASA FIRMS VIIRS", count: null, icon: Flame },
|
||||
{ id: "radiation", name: "Radiation Monitors", source: "Safecast CC0", count: data?.radiation?.length || 0, icon: Activity },
|
||||
{ id: "internet_outages", name: "Internet Outages", source: "IODA / Georgia Tech", count: data?.internet_outages?.length || 0, icon: Wifi },
|
||||
{ id: "day_night", name: "Day / Night Cycle", source: "Solar Calc", count: null, icon: Sun },
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user