From 2d1c95389a02932d00a4f0ec58c1e19f622ea58b Mon Sep 17 00:00:00 2001 From: Marc Shade Date: Tue, 24 Feb 2026 09:36:29 -0500 Subject: [PATCH] fix: wildfire data parsing + dashboard cascade failure protection Population exposure was showing 0 because exposure.py parsed wildfire data using wrong keys ("regions" instead of "fires_by_region", "detections" instead of "top_clusters"). Now correctly matches NASA FIRMS response format. Dashboard updateAll() now wraps each map/UI update in try/catch to prevent a single failed layer from blocking all subsequent layer renders. Co-Authored-By: Claude Opus 4.6 --- src/world_intel_mcp/analysis/exposure.py | 12 ++++----- src/world_intel_mcp/dashboard/index.html | 31 ++++++++++++++---------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/world_intel_mcp/analysis/exposure.py b/src/world_intel_mcp/analysis/exposure.py index 0871426..e6fd284 100644 --- a/src/world_intel_mcp/analysis/exposure.py +++ b/src/world_intel_mcp/analysis/exposure.py @@ -117,17 +117,17 @@ async def fetch_population_exposure( "detail": f"M{eq.get('magnitude', '?')} {eq.get('place', '')}", }) - # Wildfires - for region_data in results.get("wildfire", {}).get("regions", []): - for fire in region_data.get("detections", []): - lat = fire.get("latitude") or fire.get("lat") - lon = fire.get("longitude") or fire.get("lon") + # Wildfires (fires_by_region is a dict keyed by region name) + for region_name, region_data in results.get("wildfire", {}).get("fires_by_region", {}).items(): + for cluster in region_data.get("top_clusters", []): + lat = cluster.get("lat") + lon = cluster.get("lon") if lat is not None and lon is not None: events.append({ "lat": float(lat), "lon": float(lon), "type": "wildfire", - "detail": f"FRP {fire.get('frp', '?')} in {region_data.get('region', '?')}", + "detail": f"FRP {cluster.get('max_frp', '?')} ({cluster.get('fire_count', '?')} fires) in {region_name}", }) # Conflict diff --git a/src/world_intel_mcp/dashboard/index.html b/src/world_intel_mcp/dashboard/index.html index 8c980b6..5e893f6 100644 --- a/src/world_intel_mcp/dashboard/index.html +++ b/src/world_intel_mcp/dashboard/index.html @@ -1887,30 +1887,35 @@ function updateTicker(data) { function updateAll(data) { latestData = data; - // Map layers - updateMapQuakes(data.earthquakes); - updateMapMilitary(data.military_flights); + // Map layers (each wrapped to prevent cascade failures) var conflictData = (data.acled_events && !data.acled_events.error && data.acled_events.count > 0) ? data.acled_events : (data.ucdp_events && !data.ucdp_events.error && data.ucdp_events.count > 0) ? data.ucdp_events : data.conflict_zones; - updateMapConflict(conflictData); - updateMapFires(data.wildfires); - updateMapConvergence(data.signal_convergence); - updateMapNuclear(data.nuclear_monitor); - updateMapInfra(data); - updateMapExposure(data.population_exposure); + var mapUpdates = [ + ['quakes', function() { updateMapQuakes(data.earthquakes); }], + ['military', function() { updateMapMilitary(data.military_flights); }], + ['conflict', function() { updateMapConflict(conflictData); }], + ['fires', function() { updateMapFires(data.wildfires); }], + ['convergence', function() { updateMapConvergence(data.signal_convergence); }], + ['nuclear', function() { updateMapNuclear(data.nuclear_monitor); }], + ['infra', function() { updateMapInfra(data); }], + ['exposure', function() { updateMapExposure(data.population_exposure); }] + ]; + mapUpdates.forEach(function(pair) { + try { pair[1](); } catch(e) { console.warn('updateAll: ' + pair[0] + ' failed:', e); } + }); // Alert banner - updateAlertBanner(data.alert_digest); + try { updateAlertBanner(data.alert_digest); } catch(e) { console.warn('updateAll: alert_banner failed:', e); } // HUD - updateHudStats(data); + try { updateHudStats(data); } catch(e) { console.warn('updateAll: hud failed:', e); } // Drawer - updateDrawer(data); + try { updateDrawer(data); } catch(e) { console.warn('updateAll: drawer failed:', e); } // Ticker - updateTicker(data); + try { updateTicker(data); } catch(e) { console.warn('updateAll: ticker failed:', e); } // Feed health var feedKeys = Object.keys(data).filter(function(k) { return k !== 'source_health' && k !== 'cache_stats' && k !== 'timestamp'; });