docs/climate-variant-full.md
| Component | Status |
|---|---|
| Proto RPCs | 1 — ListClimateAnomalies |
| Redis keys | 1 — climate:anomalies:v2 |
| Seed scripts | 1 — seed-climate-anomalies.mjs |
| MCP tool | get_climate_data — bundled with weather:alerts:v1 |
| Hostname variant | Not configured |
Critical flaw in existing seeder: seed-climate-anomalies.mjs uses a 30-day rolling window as its own baseline. It compares "last 7 days" vs "previous 23 days" — not against 30-year climate normals. This produces anomaly numbers that are internally consistent but climatologically meaningless (e.g., a heat wave during a hot month won't appear anomalous if the prior 3 weeks were equally hot).
Fix first: Replace 30-day rolling baseline with 30-year ERA5 climatological normals via Copernicus Climate Data Store or Open-Meteo's historical endpoint with proper reference period (1991–2020 WMO standard).
Correct approach using Open-Meteo:
// Reference period: same calendar month, 1991-2020 (30-year WMO normal)
// Step 1: Fetch current 7-day mean for zone
// Step 2: Fetch historical 30-year monthly mean for same month
// using open-meteo archive: start_date=1991-01-01 end_date=2020-12-31, aggregate monthly
// Step 3: anomaly = current - historical_mean
Expand zones: Current 15 zones are geopolitically focused. Add climate-specific zones:
Bump cache key to climate:anomalies:v2 to avoid stale %-based precipitation anomalies being misread as millimeters.
What: Real atmospheric CO2 concentration + trend + annual growth rate. The foundational number behind all climate change.
Sources:
https://gml.noaa.gov/webdata/ccgg/trends/co2/co2_daily_mlo.txthttps://gml.noaa.gov/webdata/ccgg/trends/co2/co2_weekly_mlo.txthttps://gml.noaa.gov/webdata/ccgg/trends/co2/co2_mm_mlo.txthttps://gml.noaa.gov/webdata/ccgg/trends/co2/co2_annmean_gl.txthttps://gml.noaa.gov/webdata/ccgg/trends/ch4/ch4_mm_gl.txthttps://gml.noaa.gov/webdata/ccgg/trends/n2o/n2o_mm_gl.txtWhat to compute:
Redis key: climate:co2-monitoring:v1
Seed script: seed-co2-monitoring.mjs
Cache TTL: 259200 (72h — 3x daily interval gold standard)
Proto RPC: GetCo2Monitoring
message Co2Monitoring {
double current_ppm = 1; // latest daily/weekly reading
double year_ago_ppm = 2;
double annual_growth_rate = 3; // ppm/year
double pre_industrial_baseline = 4; // 280.0 (hardcoded)
double monthly_average = 5;
repeated Co2DataPoint trend_12m = 6; // monthly readings, last 12 months
double methane_ppb = 7;
double nitrous_oxide_ppb = 8;
int64 measured_at = 9;
string station = 10; // "Mauna Loa, Hawaii"
}
message Co2DataPoint {
string month = 1; // "YYYY-MM"
double ppm = 2;
double anomaly = 3; // year-over-year delta vs same calendar month, in ppm
}
What: Real-time disaster events with severity scoring. GDACS already runs in the natural events seeder — expose it as a climate layer too, plus add additional disaster sources.
CRITICAL: Reuse natural:events:v1 Redis key data — don't re-seed. Just expose GDACS + wildfire + earthquake data through a climate-domain RPC that filters for climate-relevant events: floods, storms, droughts, wildfires, heat waves.
Additional sources for climate-specific disaster data:
https://api.reliefweb.int/v1/disasters?filter[field]=primary_type&filter[value]=FL (floods)
https://www.emdat.be/
https://www.spc.noaa.gov/products/outlook/day1otlk_cat.nolhtml (US only)Redis key: climate:disasters:v1
Seed script: seed-climate-disasters.mjs
Cache TTL: 21600 (6h — ReliefWeb updates multiple times daily)
Proto RPC: ListClimateDisasters
message ClimateDisaster {
string id = 1;
string type = 2; // "flood" | "cyclone" | "drought" | "wildfire" | "heatwave" | "earthquake"
string name = 3;
string country = 4;
string country_code = 5;
double lat = 6;
double lng = 7;
string severity = 8; // "green" | "orange" | "red" (GDACS scale) or "low"/"medium"/"high"
int64 started_at = 9;
string status = 10; // "alert" | "ongoing" | "past"
int32 affected_population = 11;
string source = 12; // "GDACS" | "ReliefWeb" | "NASA FIRMS"
string source_url = 13;
}
What: Global PM2.5, AQI, ozone, NO2 readings — direct output of fossil fuel combustion and climate feedback loops.
Note: This layer is SHARED with the Health variant (health:air-quality:v1). Climate domain gets the same data but a different RPC focused on pollution sources and trends rather than health risk.
Sources:
OPENAQ_API_KEY): https://api.openaq.org/v3/locations?limit=1000¶meters_id=2
WAQI_API_KEY): city aggregates + dominant pollutanthttps://ads.atmosphere.copernicus.eu/api/v2/ (requires CDS API key)https://atmosphere.copernicus.eu/charts/packages/nrta/https://www.airnowapi.org/aq/observation/zipCode/current/?format=application/json&zipCode={zip}&API_KEY={key}Redis key: climate:air-quality:v1 (mirrors health:air-quality:v1 — same seed, separate key)
Seed script: Shared with health: seed-health-air-quality.mjs writes both keys
Cache TTL: 3600 (1h)
Proto RPC: ListAirQualityData
What: The long-term physical indicators of climate change — sea level rise, Arctic sea ice, ocean heat.
Sources:
https://masie_web.apps.nsidc.org/pub/DATASETS/NOAA/G02135/north/daily/data/N_seaice_extent_daily_v3.0.csvhttps://nsidc.org/data/seaice_index/https://tidesandcurrents.noaa.gov/mdapi/latest/webapi/stations.json?type=waterlevelshttps://tidesandcurrents.noaa.gov/sltrends/sltrends.htmlhttps://www.cmar.csiro.au/sealevel/sl_data_cmar.htmlhttps://www.ncei.noaa.gov/data/oceans/woa/WOA23/DATA/https://cds.climate.copernicus.eu/api/v2/ (requires free CDS key)Redis key: climate:ocean-ice:v1
Seed script: seed-climate-ocean-ice.mjs
Cache TTL: 86400 (24h — daily/weekly satellite updates)
Proto RPC: GetOceanIceData
message OceanIceData {
// Arctic sea ice
double arctic_extent_mkm2 = 1; // million km²
double arctic_extent_anomaly_mkm2 = 2; // vs 1981-2010 median
string arctic_trend = 3; // "record_low" | "below_average" | "average" | "above_average"
// Global mean sea level
double sea_level_mm_above_1993 = 4; // mm above 1993 satellite era baseline
double sea_level_annual_rise_mm = 5; // mm/year current rate
// Ocean heat content
double ohc_0_700m_zj = 6; // zettajoules, 0-700m depth
double sst_anomaly_c = 7; // global SST anomaly vs 1971-2000
int64 measured_at = 8;
repeated IceTrendPoint ice_trend_12m = 9;
}
message IceTrendPoint {
string month = 1;
double extent_mkm2 = 2;
double anomaly_mkm2 = 3;
}
What: Aggregated news from authoritative climate sources with AI tagging for events, policies, records.
Sources (RSS, no keys):
https://www.carbonbrief.org/feedhttps://www.theguardian.com/environment/climate-crisis/rsshttps://reliefweb.int/updates/rss.xml?content=reports&country=0&theme=4590https://earthobservatory.nasa.gov/feeds/earth-observatory.rsshttps://www.noaa.gov/taxonomy/term/28/rsshttps://phys.org/rss-feed/earth-news/earth-sciences/https://atmosphere.copernicus.eu/rsshttps://insideclimatenews.org/feed/https://www.climatecentral.org/rssRedis key: climate:news-intelligence:v1
Seed script: seed-climate-news.mjs (or relay loop)
Cache TTL: 1800 (30min)
Proto RPC: ListClimateNews
| Script | Interval | Key | TTL |
|---|---|---|---|
seed-climate-anomalies.mjs | Every 3h (existing, fix baseline) | climate:anomalies:v2 | 3h |
seed-co2-monitoring.mjs | Daily 06:00 UTC | climate:co2-monitoring:v1 | 72h |
seed-climate-disasters.mjs | Every 6h | climate:disasters:v1 | 6h |
seed-health-air-quality.mjs | Every 1h (shared) | climate:air-quality:v1 | 1h |
seed-climate-ocean-ice.mjs | Daily 08:00 UTC | climate:ocean-ice:v1 | 24h |
seed-climate-news.mjs | Every 30min (or relay loop) | climate:news-intelligence:v1 | 1h |
service ClimateService {
rpc ListClimateAnomalies(...) // EXISTING (fix baseline)
rpc GetCo2Monitoring(GetCo2MonitoringRequest) returns (GetCo2MonitoringResponse) {
option (sebuf.http.config) = {path: "/get-co2-monitoring", method: HTTP_METHOD_GET};
}
rpc ListClimateDisasters(ListClimateDisastersRequest) returns (ListClimateDisastersResponse) {
option (sebuf.http.config) = {path: "/list-climate-disasters", method: HTTP_METHOD_GET};
}
rpc ListAirQualityData(ListAirQualityDataRequest) returns (ListAirQualityDataResponse) {
option (sebuf.http.config) = {path: "/list-air-quality-data", method: HTTP_METHOD_GET};
}
rpc GetOceanIceData(GetOceanIceDataRequest) returns (GetOceanIceDataResponse) {
option (sebuf.http.config) = {path: "/get-ocean-ice-data", method: HTTP_METHOD_GET};
}
rpc ListClimateNews(ListClimateNewsRequest) returns (ListClimateNewsResponse) {
option (sebuf.http.config) = {path: "/list-climate-news", method: HTTP_METHOD_GET};
}
}
Per AGENTS.md, adding a new seeded key requires changes in 4 files:
server/_shared/cache-keys.ts — add to BOOTSTRAP_CACHE_KEYS:co2Monitoring: 'climate:co2-monitoring:v1',
climateDisasters: 'climate:disasters:v1',
climateAirQuality: 'climate:air-quality:v1',
oceanIce: 'climate:ocean-ice:v1',
climateNews: 'climate:news-intelligence:v1',
climateZoneNormals: 'climate:zone-normals:v1',
api/health.js — add each data key (not zone-normals) to the BOOTSTRAP_KEYS array (startup hydration on deploy)
api/mcp.ts — add keys to the get_climate_data tool's _cacheKeys array (see MCP Tool section below)
Each seed script — must call runSeed() with the correct canonical key so it writes seed-meta:<domain>:<name> automatically. The seed-meta key is required for health monitoring (_seedMetaKey in the MCP tool).
get_climate_dataReplace current entry in api/mcp.ts:
{
name: 'get_climate_data',
description: 'Climate intelligence: temperature/precipitation anomalies (vs 30-year WMO normals), atmospheric CO2 trend (NOAA Mauna Loa), global disaster alerts (GDACS/ReliefWeb), air quality (OpenAQ/WAQI), sea level rise and Arctic ice extent (NSIDC/NOAA), and climate news.',
inputSchema: {
type: 'object',
properties: {
layer: { type: 'string', description: '"anomalies" | "co2" | "disasters" | "air-quality" | "ocean-ice" | "news" | empty for all' },
region: { type: 'string', description: 'Region or zone name filter' },
},
required: [],
},
_cacheKeys: [
'climate:anomalies:v2',
'climate:co2-monitoring:v1',
'climate:disasters:v1',
'climate:air-quality:v1',
'climate:ocean-ice:v1',
'climate:news-intelligence:v1',
],
_seedMetaKey: 'seed-meta:climate:co2-monitoring',
_maxStaleMin: 2880,
}
| Service | Key Name | Free Tier |
|---|---|---|
| WAQI (air quality) | WAQI_API_KEY | 1000 req/day |
| OpenAQ v3 (air quality) | OPENAQ_API_KEY | Required by current API docs |
| NOAA GML | None | Free |
| NSIDC | None | Free |
| ReliefWeb API | None | Free |
| RSS feeds (all) | None | Public |
| Copernicus CDS | CDS_API_KEY | Free (registration required) — only needed for CAMS/ERA5 advanced queries |
OpenAQ now requires OPENAQ_API_KEY. WAQI_API_KEY is still optional, and CDS_API_KEY is only needed for CAMS/ERA5 advanced queries.
Before any new layer work, fix seed-climate-anomalies.mjs:
// WRONG (current): compare last 7d vs previous 23d of same 30d window
// RIGHT: compare last 7d vs 30-year monthly mean (1991–2020 WMO standard)
// Implementation: fetch Open-Meteo archive with start_date=1991-01-01 end_date=2020-12-31,
// aggregate daily values by calendar month to get monthly mean per zone.
// IMPORTANT: use the full 1991-2020 period — do NOT use a shorter "proxy" window
// (e.g., 2014-2023) as a warm decade would systematically understate current anomalies.
const NORMALS_KEY = 'climate:zone-normals:v1';
const NORMALS_TTL = 30 * 86400; // 30 days — recalculate monthly
The 30-year normal fetch should run as a separate monthly seed and cache in climate:zone-normals:v1, then seed-climate-anomalies.mjs reads that as its baseline. This is one fetch per zone per year instead of per run.
climate.worldmonitor.appAdd to src/config/variant.ts:
climate: {
defaultPanels: ['climate-anomalies', 'co2-monitoring', 'climate-disasters', 'ocean-ice', 'air-quality', 'climate-news'],
mapLayers: ['climate-anomalies-heatmap', 'disasters', 'air-quality', 'wildfire'],
theme: { primaryColor: '#00AA55', accentColor: '#FF6600' },
refreshIntervals: { anomalies: 3 * 60, co2: 24 * 60, disasters: 6 * 60, news: 30 },
i18n: { title: 'Climate Intelligence', subtitle: 'Atmospheric, Ocean & Disaster Monitoring' },
}
seed-climate-anomalies.mjs baseline — high priority, improves existing data quality immediatelyseed-co2-monitoring.mjs — NOAA GML text file parsing, no key, 30min effort, high impact (single most important climate number)seed-climate-news.mjs — RSS aggregation, no key, fast winseed-climate-disasters.mjs — ReliefWeb API (no key) + reuse GDACS from natural seederseed-health-air-quality.mjs — OpenAQ (OPENAQ_API_KEY), writes both health:air-quality:v1 and climate:air-quality:v1seed-climate-ocean-ice.mjs — NSIDC CSV parsing (no key), daily dataseed-climate-zone-normals.mjs — one-time + monthly refresh, feeds anomaly baselineget_climate_data with new cache keysclimate.worldmonitor.app