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:
anoracleofra-code
2026-03-10 09:01:35 -06:00
parent c4de39bb02
commit 7cb926e227
5 changed files with 266 additions and 4 deletions
+112 -1
View File
@@ -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]