* feat(correlation): server-side correlation engine seed + bootstrap hydration
Move correlation card computation from client-side (per-browser, 10-30s delay)
to server-side (Railway cron, instant via bootstrap). Seed script reads 8 Redis
keys, runs 4 adapter signal collectors (military, escalation, economic, disaster),
clusters/scores/generates cards, writes to Redis with 10min TTL.
- New: scripts/seed-correlation.mjs (pure JS port of correlation engine)
- bootstrap.js: add correlationCards to FAST_KEYS tier
- health.js + seed-health.js: register for monitoring (maxStaleMin: 15)
- CorrelationPanel: consume bootstrap on construction, show "Analyzing..." only
after live engine has run (not for bootstrap-only cards)
- _seed-utils.mjs: support opts.recordCount override (function or number)
* fix(correlation): stale timestamp fallback + coordinate-based country resolution
P1: news stories lacked per-story pubDate, causing Date.now() fallback on
every seed run. Now _clustering.mjs propagates pubDate through to
enrichedStories, and seed-correlation reads s.pubDate then generatedAt.
P2: normalizeToCode dropped signals with unparseable country names.
Added centroid-based coordinate fallback (haversine nearest-match within
800km) matching the live engine's getCountryAtCoordinates behavior.
* fix(correlation): add 11 missing country centroids to coordinate fallback
CI, CR, CV, CY, GA, IS, LA, SZ, TL, TT, XK were in the normalization
maps but missing from COUNTRY_CENTROIDS, causing coordinate-only signals
in those countries to be misclassified or dropped during bootstrap.
* fix(correlation): align protest/outage field names with actual Redis schema
Codex review P1 findings: seed-correlation read wrong field names from
Redis data.
Protests (unrest:events:v1): p.time -> p.occurredAt, p.lat/lon ->
p.location.latitude/longitude, severity enum SEVERITY_LEVEL_* mapping.
Outages (infra:outages:v1): o.pubDate -> o.detectedAt, o.lat/lon ->
o.location.latitude/longitude, severity enum OUTAGE_SEVERITY_* mapping.
Both escalation and disaster adapters updated. Old field names kept as
fallbacks for data shape compatibility.
* feat(supply-chain): replace S&P Global with 3 free maritime data sources
Replace expensive S&P Global Maritime API with IMF PortWatch (vessel transit
counts), CorridorRisk (risk intelligence), and AISStream chokepoint crossing
counter. All external API calls run on Railway relay, Vercel reads Redis only.
- Add 4 new chokepoints (10 total): Cape of Good Hope, Gibraltar, Bosphorus, Dardanelles
- Add TransitSummary proto (field 14) with today counts, WoW%, 180d history, risk context
- Add D3 multi-line chart (tanker vs cargo) with expandable chokepoint cards
- Add crossing detection with enter+dwell+exit semantics, 30min cooldown, 5min min dwell
- Add PortWatch seed loop (6h), CorridorRisk seed loop (1h), transit seed loop (10min)
- Add canonical chokepoint ID map for cross-source name resolution
- 177 tests passing across 6 test files
* fix(supply-chain): address P2 review findings
- Discard partial PortWatch pagination results on mid-page failure (prevents
truncated history with wrong WoW numbers cached for 6h)
- Rename "Transit today" to "24h" label (rolling 24h window, not calendar day)
- Fix chart label from "30d" to "180d" (matches actual PortWatch query range)
- Add 30s initial seed for chokepoint transits on relay cold start (prevents
10min gap of zero transit data)
* feat(supply-chain): swap D3 chart for TradingView lightweight-charts
Replace hand-rolled D3 SVG transit chart with lightweight-charts v5 canvas
rendering for Bloomberg-quality time-series visualization.
- Add TransitChart helper class with mount/destroy lifecycle, theme listener,
and autoSize support
- Use MutationObserver (not rAF) to mount chart after setContent debounce
- Clean up chart on tab switch, collapse, and re-render (no orphaned canvases)
- Respond to theme-changed events via chart.applyOptions()
- D3 stays for other 5 components (ProgressCharts, RenewableEnergy, etc.)
* feat(supply-chain): add geo coords and trade routes for 4 new chokepoints
Cherry-pick from PR #1511: Cape of Good Hope, Gibraltar, Bosphorus, and
Dardanelles map-layer coordinates and trade route definitions.
* fix(supply-chain): health.js v2->v4 key + double cache TTLs for missed seeds
- health.js chokepoints key was still v2, now v4 (matches handler + bootstrap)
- PortWatch TTL: 21600s (6h) -> 43200s (12h), seed interval stays 6h
- CorridorRisk TTL: 3600s (1h) -> 7200s (2h), seed interval stays 1h
- Ensures one missed seed run doesn't expire the key and cause empty data
* feat: seed GDELT intelligence topics to Redis with bootstrap hydration
Add standalone seed script that pre-populates all 6 Live Intelligence
topics (military, cyber, nuclear, sanctions, intelligence, maritime)
from the GDELT Doc API into Redis. Frontend consumes bootstrap data
lazily via the service layer, falling back to RPC if unavailable.
- scripts/seed-gdelt-intel.mjs: new seed script with per-topic 429 retry
- api/bootstrap.js: register gdeltIntel in FAST_KEYS
- api/health.js: register in BOOTSTRAP_KEYS + SEED_META + dataSize
- api/seed-health.js: register in SEED_DOMAINS
- scripts/_seed-utils.mjs: add topics to recordCount detection
- src/services/gdelt-intel.ts: lazy bootstrap consumption in service layer
* fix(seed): align staleness thresholds and strengthen GDELT validation
- seed-health intervalMin 30→60 so staleness (120min) matches health.js maxStaleMin
- validate requires ≥3/6 topics populated (not just military)
- recordCount sums articles across topics instead of reporting topic count
* fix(tech-events): prevent partial fetch results from being cached
Techmeme ICS and dev.events RSS fetches on Vercel edge can partially
fail (timeout, truncation), returning only 1 event instead of 20+.
The handler cached this partial result for 6 hours, causing the Tech
Events panel to show empty.
- Add 8s AbortSignal.timeout on both external fetches
- Require minimum 5 events before caching (at least curated count)
* fix(tech-events): remove MIN_EVENTS threshold and add diagnostic logging
The MIN_EVENTS=5 threshold caused empty results when both external
sources fail on Vercel edge (only 4 curated events available). Now
any events > 0 are cached. Added detailed logging to diagnose why
Techmeme ICS and dev.events RSS fetches fail on Vercel edge.
Also removed past STEP Dubai 2026 event.
* fix(tech-events): route fetches through Railway relay when direct fails
Vercel edge functions cannot reliably reach Techmeme ICS and dev.events
RSS (datacenter IP blocking). Added fetchTextWithRelay() that tries
direct fetch first, then falls back to Railway relay proxy (/rss endpoint)
which fetches from a different IP. Same pattern used by news feed digest
and other handlers that hit blocked external sources.
* feat(tech-events): gold standard pipeline with Railway seed + bootstrap hydration
Full data pipeline overhaul to match project conventions:
- Add tech events seed loop to ais-relay.cjs: fetches Techmeme ICS +
dev.events RSS every 6h from Railway (avoids Vercel IP blocking),
parses both sources, merges with curated fallback events, writes to
Redis (data key + bootstrap key + seed-meta)
- Register in api/bootstrap.js BOOTSTRAP_CACHE_KEYS (SLOW tier)
- Register in api/health.js BOOTSTRAP_KEYS + SEED_META (420min stale)
- Restructure RPC handler: reads from single broad Redis key (populated
by seed), applies geocoding + filtering in-memory per request params.
Fallback fetcher only runs on cold start before first seed
- TechEventsPanel: check getHydratedData('techEvents') from bootstrap
before falling back to RPC call
- data-loader: use hydrated bootstrap data for map layer, RPC fallback
Move theaterPosture from SLOW (2h CDN) to FAST tier (20min/10min after
PR #1314) so military posture data stays fresh. Increase risk scores
breaker TTL to 30min to match health.js maxStaleMin, and reduce
localStorage staleness from 24h to 1h to prevent stale risk data in UI.
CDN-Cache-Control for fast tier was 20min + 5min stale-while-revalidate,
but insights-loader rejects data older than 15min. This caused frequent
fallback to the slow 4-step client-side AI pipeline. Align CDN TTL with
origin Cache-Control (10min + 2min SWR) so CF revalidates before the
freshness threshold.
* fix: eliminate frontend external API calls, enforce gold standard pattern
- Polymarket: remove browser fan-out (536→105 lines), bootstrap → RPC only
- USASpending: remove direct API calls, read from bootstrap hydration
- NWS Weather: remove direct API calls, read from bootstrap hydration
- Nominatim: proxy through api/reverse-geocode.js with Redis cache + SSRF clamping
- Add seed scripts for weather alerts (15min) and spending (60min)
- Wire both seed loops into ais-relay.cjs
- Register weatherAlerts + spending in bootstrap.js and health.js
- Add 4 missing standalone keys to health.js (cyberThreatsRpc, militaryBases, temporalAnomalies, displacement)
* fix: resolve reload regressions and null-cache poisoning from #1217
- Weather/Spending: fall back to `/api/bootstrap?keys=` on scheduled
reloads after the one-shot `getHydratedData()` is consumed
- Prediction: add client-side bootstrap filter for country markets
when RPC fails (server skips bootstrap for query-based requests)
- Reverse-geocode: restore abort/timeout guard so transient network
errors don't permanently poison the in-memory cache
* perf(baseline): move temporal baseline for news+fires to server-side
Every browser client was calling record-baseline-snapshot (POST) +
get-temporal-baseline (GET) on every data refresh from 5 call sites.
With N concurrent users this created N identical writes and ~5N reads
per cycle — causing 429 rate limiting and statistically biased baselines.
Phase 1 moves news and satellite_fires to server-side computation:
- New ListTemporalAnomalies RPC reads counts from existing Redis keys
(news:insights:v1, wildfire:fires:v1), computes anomalies against v2
baselines, applies Welford update (1 sample/cycle), caches 15min
- Bootstrap hydration delivers anomalies on page load (zero extra calls)
- Client refreshes via RPC every 10min (1 cached call vs 5N before)
- Remaining 3 types (military_flights, vessels, ais_gaps) stay client-side
- Owner-guarded distributed lock prevents concurrent computation
- All reads/writes use prefix-aware getCachedJson/setCachedJson
Expected ~60% reduction in baseline-related Vercel invocations.
* fix(temporal): per-invocation lock owner and immediate refresh on cold cache
P1: When bootstrap has no temporal anomaly data (cold cache, expired
snapshot, fresh deploy), fire refreshTemporalBaseline() immediately
instead of waiting up to 10 minutes for the scheduled refresh.
P2: Generate lockOwner per invocation via makeLockOwner() instead of
once at module load. Prevents warm edge isolates from cross-releasing
each other's locks when one invocation outlives the 30s TTL.
* fix(temporal): use TTL-only lock instead of TOCTOU GET-then-DEL release
The non-atomic GET→check-owner→DEL release was vulnerable to a race
where the TTL expires between GET and DEL, allowing a new lock holder
to be evicted. Simplify by relying solely on the 30s TTL for lock
expiry — the computation completes well within that window.
* fix(economic): seed all WB indicators on Railway, never call WB API from frontend
Extends seed-wb-indicators.mjs to pre-compute progress data (4 indicators)
and renewable energy data (EG.ELC.RNEW.ZS) alongside tech readiness rankings.
Frontend callers (progress-data.ts, renewable-energy-data.ts, getTechReadinessRankings,
getCountryComparison) now read exclusively from bootstrap/Redis seed data.
Zero Vercel Edge → World Bank API calls remain.
* fix: address code review findings (P1+P2)
- Fix triple JSON.parse in seed verification (P1)
- Graceful fallback for renewable data fetch failure (P2)
- Use Map lookup instead of Array.find in progress-data (P2)
- Update regression test for bootstrap-only getTechReadinessRankings (P2)
Register 6 new seeds in bootstrap.js (crypto, gulf, stablecoin, unrest,
iran, ucdp) and wire getHydratedData() in 7 service files. Also adds
hydration for 2 previously-registered keys (cyberThreats, flightDelays)
that had no frontend consumer. Syncs cache-keys.ts with bootstrap.js
for test parity.
Cyber hydration correctly maps through toCyberThreat() to convert proto
enum strings to friendly types.
* perf: reduce Vercel data transfer costs with CDN optimization
- Increase polling intervals (markets 8→12min, feeds 15→20min, crypto 8→12min)
- Increase background tab hiddenMultiplier from 10→30 (polls 3x less when hidden)
- Double CDN s-maxage TTLs across all cache tiers in gateway
- Add CDN-Cache-Control header for Cloudflare-specific longer edge caching
- Add ETag generation + 304 Not Modified support in gateway (zero-byte revalidation)
- Add CDN-Cache-Control to bootstrap endpoint
- Add explicit SPA rewrite rule in vercel.json for CF proxy compatibility
- Add Cache-Control headers for /map-styles/, /data/, /textures/ static paths
* fix: prevent CF from caching SPA HTML + reduce Polymarket bandwidth 95%
- vercel.json: apply no-cache headers to ALL SPA routes (same regex as
rewrite rule), not just / and /index.html — prevents CF proxy from
caching stale HTML that references old content-hashed bundle filenames
- Polymarket: add server-side aggregation via Railway seed script that
fetches all tags once and writes to Redis, eliminating 11-request
fan-out per user per poll cycle
- Bootstrap: add predictions to hydration keys for zero-cost page load
- RPC handler: read Railway-seeded bootstrap key before falling back to
live Gamma API fetch
- Client: 3-strategy waterfall (bootstrap → RPC → fan-out fallback)
* feat: Implement comprehensive aviation monitoring service with flight search, status, news, and tracking.
* feat: Introduce Airline Intelligence Panel with aviation data tabs, map components, and localization.
* feat: Implement DeckGL-based map for advanced visualization, D3/SVG fallback, i18n support, and aircraft tracking.
* Update server/worldmonitor/aviation/v1/get-carrier-ops.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update server/worldmonitor/aviation/v1/search-flight-prices.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update server/worldmonitor/aviation/v1/track-aircraft.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update server/worldmonitor/aviation/v1/get-airport-ops-summary.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update proto/worldmonitor/aviation/v1/position_sample.proto
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update server/worldmonitor/aviation/v1/list-airport-flights.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update proto/worldmonitor/aviation/v1/price_quote.proto
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* feat: Add server-side endpoints for aviation news and aircraft tracking, and introduce a new DeckGLMap component for map visualization.
* Update server/worldmonitor/aviation/v1/list-airport-flights.ts
The cache key for listAirportFlights excludes limit, but the upstream fetch/simulated generator uses limit to determine how many flights to return. If the first request within TTL uses a small limit, larger subsequent requests will be incorrectly capped until cache expiry. Include limit (or a normalized bucket/max) in cacheKey, or always fetch/cache a fixed max then slice per request.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update server/worldmonitor/aviation/v1/get-flight-status.ts
getFlightStatus accepts origin, but cacheKey does not include it. This can serve cached results from an origin-less query to an origin-filtered query (or vice versa). Add origin (normalized) to the cache key or apply filtering after fetch to ensure cache correctness.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* feat: Implement DeckGL map for advanced visualization and new aviation data services.
* fix(aviation): prevent cache poisoning and keyboard shortcut in inputs
- get-carrier-ops: move minFlights filter post-cache to avoid cache
fragmentation (different callers sharing cached full result)
- AviationCommandBar: guard Ctrl+J shortcut so it does not fire when
focus is inside an INPUT or TEXTAREA element
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: introduce AviationCommandBar component for parsing user commands, fetching aviation data, and displaying results.
* feat: Implement aircraft tracking service with OpenSky and simulated data sources.
* feat: introduce DeckGLMap component for WebGL-accelerated map visualizations using deck.gl and maplibre-gl.
* fix(aviation): address code review findings for PR #907
Proto: add missing (sebuf.http.query) annotations on all GET request
fields across 6 proto files; add currency/market fields to
SearchFlightPricesRequest.
Server: add parseStringArray to aviation _shared.ts and apply to
get-airport-ops-summary, get-carrier-ops, list-aviation-news handlers
to prevent crash on comma-separated query params; remove leaked API
token from URL params in travelpayouts_data; fix identical simulated
flight statuses in list-airport-flights; remove unused endDate var;
normalize cache key entity casing in list-aviation-news.
Client: refactor AirlineIntelPanel to extend Panel base class and
register in DEFAULT_PANELS for full/tech/finance variants; fix
AviationCommandBar reference leak with proper destroy() cleanup in
panel-layout; rename priceUsd→priceAmount in display type and all
usages; change auto-refresh to call refresh() instead of loadOps().
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: introduce aviation command bar component with aircraft tracking and flight information services.
* feat: Add `AirlineIntelPanel` component for displaying airline operations, flights, carriers, tracking, news, and prices in a tabbed interface.
* feat: Add endpoints for listing airport flights and fetching aviation news.
* Update proto/worldmonitor/aviation/v1/search_flight_prices.proto
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* feat: Add server endpoint for listing airport flights and client-side MapPopup types and utilities.
* feat: Introduce MapPopup component with support for various data types and responsive positioning for map features.
* feat: Add initial English localization file (en.json).
* fix(aviation): address PR review findings across aviation stack
- Add User-Agent header to Travelpayouts provider (server convention)
- Use URLSearchParams for API keys instead of raw URL interpolation
- Add input length validation on flightNumber (max 10 chars)
- Replace regex XML parsing with fast-xml-parser in aviation news
- Fix (f as any)._airport type escape with typed Map<FI, string>
- Extract DEFAULT_WATCHED_AIRPORTS constant from hardcoded arrays
- Use event delegation for AirlineIntelPanel price search listener
- Add bootstrap hydration key for flight delays
- Bump OpenSky cache TTL to 120s (anonymous tier rate limit)
- Match DeckGLMap aircraft poll interval to server cache (120s)
- Fix GeoJSON polygon winding order (shoelace check + auto-reversal)
* docs: add aviation env vars to .env.example
AVIATIONSTACK_API, ICAO_API_KEY, TRAVELPAYOUTS_API_TOKEN
* feat: Add aviation news listing API and introduce shared RSS allowed domains.
* fix: add trailing newline to rss-allowed-domains.json, remove unused ringIsClockwise
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Elie Habib <elie.habib@gmail.com>
* fix: add circuit breaker + bootstrap to CII risk scores
Same pattern as theater posture (#948): replace fragile in-memory cache
+ manual persistent-cache with circuit breaker (SWR, IndexedDB, cooldown)
and bootstrap hydration. Eliminates learning-mode delay on cold start
and survives RPC failures without blanking the panel.
* fix: add localStorage sync prime for CII risk scores
getCachedScores() is called synchronously by country-intel.ts as a
fallback during learning mode. Without localStorage priming, the
breaker's async IndexedDB hydration hasn't run yet and returns null.
- Add shape validator (isValidCiiEntry) for untrusted localStorage data
- Add loadFromStorage/saveToStorage with 24h staleness ceiling
- Prime breaker synchronously at module load from localStorage
- Skip priming for empty cii arrays to avoid cached-empty trap
- Save to localStorage on both bootstrap and RPC success paths
* feat: Railway CII seed + bootstrap hydration for instant panel render
- Add 8-source CII seed to Railway (ACLED, UCDP, outages, climate, cyber, fires, GPS, Iran strikes)
- Neuter Vercel handler to read-only (returns Railway-seeded cache, never recomputes)
- Register riskScores in bootstrap FAST tier for CDN-cached delivery
- Add early CII hydration in data-loader before intelligence signals
- Add CIIPanel.renderFromCached() for instant render from bootstrap data
- Refactor cached-risk-scores.ts: circuit breaker + localStorage sync prime + bootstrap hydration
- Progressive enhancement: cached render → full 18-source local recompute (no spinner)
* fix: remove duplicate riskScores key in BOOTSTRAP_TIERS after merge
* feat: move EONET/GDACS to server-side with Redis caching and bootstrap hydration
Browser-direct fetches to eonet.gsfc.nasa.gov and gdacs.org caused CORS
errors and had no server-side caching. This moves both to the standard
Vercel edge → cachedFetchJson → Redis → bootstrap hydration pattern.
- Add proto definitions for NaturalService with ListNaturalEvents RPC
- Create server handler merging EONET + GDACS with 30min Redis TTL
- Add Vercel edge function at /api/natural/v1/list-natural-events
- Register naturalEvents in bootstrap SLOW_KEYS for CDN hydration
- Replace browser-direct fetches with RPC client + circuit breaker
- Delete src/services/gdacs.ts (logic moved server-side)
* fix: restore @ts-nocheck on generated files stripped by buf generate
* fix: add circuit breaker + bootstrap to CII risk scores
Same pattern as theater posture (#948): replace fragile in-memory cache
+ manual persistent-cache with circuit breaker (SWR, IndexedDB, cooldown)
and bootstrap hydration. Eliminates learning-mode delay on cold start
and survives RPC failures without blanking the panel.
* fix: add localStorage sync prime for CII risk scores
getCachedScores() is called synchronously by country-intel.ts as a
fallback during learning mode. Without localStorage priming, the
breaker's async IndexedDB hydration hasn't run yet and returns null.
- Add shape validator (isValidCiiEntry) for untrusted localStorage data
- Add loadFromStorage/saveToStorage with 24h staleness ceiling
- Prime breaker synchronously at module load from localStorage
- Skip priming for empty cii arrays to avoid cached-empty trap
- Save to localStorage on both bootstrap and RPC success paths
Add circuit breaker + IndexedDB persistence + bootstrap hydration to
theater posture fetching — the only major panel without these resilience
layers. Replaces fragile in-memory cache (15-min TTL) and destructive
localStorage (30-min hard-delete) with the standard 3-tier pattern used
by all other panels.
* enhance supply chain panel
* fix(supply-chain): resolve P1 threat zeroing and P2 geo-first misclassification
P1: threat baseline is now always applied regardless of config
staleness — stale config only adds a review-recommended note,
never zeros the score.
P2: resolveChokepointId now checks text evidence first and only
falls back to proximity when text has no confident match.
Adds regression test: text "Bab el-Mandeb" with location near
Suez correctly resolves to bab_el_mandeb.
---------
Co-authored-by: fayez bast <fayezbast15@gmail.com>
GDELT GEO API had 99.9% timeout rate on Vercel Edge (746 invocations, ~31s
sequential calls vs 25s edge limit). Move fetching to Railway cron (15min),
write to Redis, have Vercel serve read-only from cache with bootstrap hydration.
- Add startPositiveEventsSeedLoop() to ais-relay.cjs (3 queries, dedup, classify)
- Rewrite handler to cache-read-only pattern (matches UCDP)
- Register bootstrap key in FAST_KEYS for instant first render
- Wire getHydratedData() in data-loader before RPC fallback
* feat(tech-readiness): bootstrap hydration via Railway seed + bootstrap key
Add pre-computed TechReadiness rankings to the bootstrap payload so the
panel renders immediately on first load instead of waiting for 4 slow
World Bank RPC calls (which can trip circuit breakers on cold starts,
causing persistent "No data available" until the 5-min cooldown expires).
- scripts/seed-wb-indicators.mjs: new Railway seed script that fetches
IT.NET.USER.ZS / IT.CEL.SETS.P2 / IT.NET.BBND.P2 / GB.XPD.RSDV.GD.ZS
for all countries, computes rankings (same weights as the frontend
getTechReadinessRankings), and writes economic:worldbank-techreadiness:v1
to Redis with a 7-day TTL
- api/bootstrap.js: register techReadiness key in BOOTSTRAP_CACHE_KEYS
and SLOW_KEYS (s-maxage=3600, appropriate for annual WB data)
- src/services/economic/index.ts: fast-path in getTechReadinessRankings()
returns getHydratedData('techReadiness') immediately on first page load;
country-specific comparison requests still use live RPCs
* ci: add weekly GHA workflow for WB tech readiness seed
Markets and commodities panels showed "Failed to load" because they
relied entirely on the listMarketQuotes RPC while sectors worked via
bootstrap hydration. Both also shared a single circuit breaker — 2
transient failures across both calls triggered a 5-minute cooldown.
- Add bootstrap Redis keys (market:stocks-bootstrap:v1 and
market:commodities-bootstrap:v1) to Railway seed and bootstrap API
- Hydrate markets/commodities from bootstrap on page load (same
pattern as sectors)
- Split circuit breaker: separate stockBreaker and commodityBreaker
so commodity failures don't kill market retries and vice versa
Split bootstrap endpoint into slow-changing (1h TTL: BIS rates,
minerals, sectors, etc.) and fast-changing (10min TTL: earthquakes,
outages, macro signals, etc.) tiers via ?tier=slow|fast query param.
Frontend fetches both tiers in parallel with shared 800ms timeout.
Partial failure is graceful — panels fall through to individual RPCs.
Backward compatible: no ?tier= param returns all keys at s-maxage=600.
Also removes orphaned ucdpEvents key (no getHydratedData consumer).
* feat(conflict): wire UCDP API access token across full stack
UCDP API now requires an `x-ucdp-access-token` header. Renames the
stub `UC_DP_KEY` to `UCDP_ACCESS_TOKEN` (matching ACLED convention)
and wires it through Rust keychain, sidecar allowlist + verification,
handler fetch headers, feature toggles, and desktop settings UI.
- Rename UC_DP_KEY → UCDP_ACCESS_TOKEN in type system and labels
- Add ucdpConflicts feature toggle with required secret
- Add UCDP_ACCESS_TOKEN to Rust SUPPORTED_SECRET_KEYS (24→25)
- Add sidecar ALLOWED_ENV_KEYS entry + validation with dynamic GED version probing
- Handler sends x-ucdp-access-token header when token is present
- UC_DP_KEY fallback in handler for one-release migration window
- Update .env.example, desktop-readiness, and docs
* feat(conflict): pre-fetch UCDP events via Railway cron + Redis cache
Replace the 228-line edge handler that fetched UCDP GED API on every
request with a thin Redis reader. The heavy fetch logic (version
discovery, paginated backward fetch, 1-year trailing window filter)
now runs as a setInterval loop in the Railway relay (ais-relay.cjs)
every 6 hours, writing to Redis key conflict:ucdp-events:v1.
Changes:
- Add UCDP seed loop to ais-relay.cjs (6h interval, 6 pages, 2K cap)
- Rewrite list-ucdp-events.ts as thin Redis reader (35 lines)
- Add conflict:ucdp-events:v1 to bootstrap batch keys
- Protect key from cache-purge via durable data prefix
- Add manual-only seed-ucdp-events workflow + standalone script
- Rename panel "UCDP Events" → "Armed Conflict Events" in locale
- Add 24h TTL + 25h staleness check as safety nets
* perf: bootstrap endpoint + polling optimization (phases 3-4)
Replace 15+ individual RPC calls on startup with a single /api/bootstrap
batch call that fetches pre-cached data from Redis. Consolidate 6 panel
setInterval timers into the central RefreshScheduler for hidden-tab
awareness (10x multiplier) and adaptive backoff (up to 4x for unchanged
data). Convert IntelligenceGapBadge from 10s polling to event-driven
updates with 60s safety fallback.
* fix(bootstrap): inline Redis + cache keys in edge function
Vercel Edge Functions cannot resolve cross-directory TypeScript imports
from server/_shared/. Inline getCachedJsonBatch and BOOTSTRAP_CACHE_KEYS
directly in api/bootstrap.js. Add sync test to ensure inlined keys stay
in sync with the canonical server/_shared/cache-keys.ts registry.
* test: add Edge Function module isolation guard for all api/*.js files
Prevents any Edge Function from importing from ../server/ or ../src/
which breaks Vercel builds. Scans all 12 non-helper Edge Functions.
* fix(bootstrap): read unprefixed cache keys on all environments
Preview deploys set VERCEL_ENV=preview which caused getKeyPrefix() to
prefix Redis keys with preview:<sha>:, but handlers only write to
unprefixed keys on production. Bootstrap is a read-only consumer of
production cache — always read unprefixed keys.
* fix(bootstrap): wire sectors hydration + add coverage guard
- Wire getHydratedData('sectors') in data-loader to skip Yahoo Finance
fetch when bootstrap provides sector data
- Add test ensuring every bootstrap key has a getHydratedData consumer
— prevents adding keys without wiring them
* fix(server): resolve 25 TypeScript errors + add server typecheck to CI
- _shared.ts: remove unused `delay` variable
- list-etf-flows.ts: add missing `rateLimited` field to 3 return literals
- list-market-quotes.ts: add missing `rateLimited` field to 4 return literals
- get-cable-health.ts: add non-null assertions for regex groups and array access
- list-positive-geo-events.ts: add non-null assertion for array index
- get-chokepoint-status.ts: add required fields to request objects
- CI: run `typecheck:api` (tsconfig.api.json) alongside `typecheck` to catch
server/ TS errors before merge