* feat: enhance support for HLS streams and update font styles
* chore: add .vercelignore to exclude large local build artifacts from Vercel deploys
* chore: include node types in tsconfig to fix server type errors on Vercel build
* fix(middleware): guard optional variant OG lookup to satisfy strict TS
* fix: desktop build and live channels handle null safety
- scripts/build-sidecar-sebuf.mjs: Skip building removed [domain]/v1/[rpc].ts (removed in #785)
- src/live-channels-window.ts: Add optional chaining for handle property to prevent null errors
- src-tauri/Cargo.lock: Bump version to 2.5.24
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix: address review issues on PR #1020
- Remove AGENTS.md (project guidelines belong to repo owner)
- Restore tracking script in index.html (accidentally removed)
- Revert tsconfig.json "node" types (leaks Node globals to frontend)
- Add protocol validation to isHlsUrl() (security: block non-http URIs)
- Revert Cargo.lock version bump (release management concern)
* fix: address P2/P3 review findings
- Preserve hlsUrl for HLS-only channels in refreshChannelInfo (was
incorrectly clearing the stream URL on every refresh cycle)
- Replace deprecated .substr() with .substring()
- Extract duplicated HLS display name logic into getChannelDisplayName()
---------
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Elie Habib <elie.habib@gmail.com>
Stadia Maps tiles return HTTP 401 without an API key — style.json loads
but actual .pbf tile requests fail, leaving the map black with floating
data points (issue #1031).
Changes:
- Switch fallback to OpenFreeMap (free, no auth, CORS *, dark style)
- Replace overly broad 'style.json' error match with 'cartocdn.com'
- Store style load timeout on instance, clear in destroy()
- Update TODO-131 text to reference OpenFreeMap
CARTO basemap CORS errors weren't triggering the Stadia fallback because
the error message didn't always match 'Failed to fetch' or 'AJAXError'.
Add broader error pattern matching (CORS, NetworkError, style.json) and
a 5-second timeout that switches to fallback if style hasn't loaded.
Also adds TODO-131 for self-hosted Protomaps + CloudFront tiles to
eliminate third-party basemap dependency entirely.
Reduce MAX_DISTANCE_KM from 600 to 300 so countries like Lebanon no
longer show distant foreign nuclear facilities (e.g. Dimona at ~320km).
Rename infra label from "Nuclear Facilities" to "Nearby Nuclear" across
all 21 locales to clarify these are proximity-based, not domestic.
- register-interest.js: coerce source/appVersion to string with a 100-char cap
before forwarding to Convex. Non-string values (objects, arrays) are truthy so
the previous || 'unknown' guard passed them through, causing Convex to throw
a type-validation error and surface a 500 to the caller. Also fixes unbounded
metadata strings filling the registrations table cheaply.
- rss-proxy.js: apply the same www-normalization used by the initial domain check
to the 301-redirect hostname check. The old bare ALLOWED_DOMAINS.includes(hostname)
call rejected canonical redirects (e.g. bbc.co.uk -> www.bbc.co.uk) even when
one form is allowlisted, breaking several feeds silently.
- _api-key.js: align BROWSER_ORIGIN_PATTERNS Vercel-preview regex with the
narrower pattern already enforced by _cors.js (worldmonitor-*-elie-*.vercel.app).
The broader worldmonitor-*.vercel.app pattern was dead code because _cors.js
rejects those origins before _api-key.js is reached.
The fires panel showed "NASA_FIRMS_API_KEY not configured" whenever
detections were empty, which is misleading — empty data can have
multiple causes (seed cron lag, Redis expiry, circuit breaker).
Use the existing i18n key `panels.satelliteFires.noData` instead,
which is already translated across all 21 supported languages.
* fix: allow zero fire detections in seed validation
FIRMS NRT data has a rolling window — at certain hours, all 9 monitored
regions can legitimately return 0 active fire detections. The strict
length > 0 validation caused CRASHED status on Railway cron runs
during these periods. Structure-only validation is sufficient.
* fix: add rate-limit-aware retry for CoinGecko 429s
The default withRetry (1s/2s/4s backoff) is too short for CoinGecko
rate limits. New fetchWithRateLimitRetry uses 10s/20s/30s/40s/50s
delays with up to 5 attempts specifically for 429 responses.
* fix: add 429 rate-limit retry to all Yahoo and CoinGecko seed scripts
Yahoo Finance and CoinGecko both return 429 when rate limited. The
default withRetry (1s/2s/4s) is too fast for rate limits. Added
per-request 429-specific retry with longer backoff:
- Yahoo: 5s/10s/15s/20s (4 attempts per symbol)
- CoinGecko: 10s/20s/30s/40s/50s (5 attempts)
Scripts updated: seed-etf-flows, seed-gulf-quotes, seed-commodity-quotes,
seed-market-quotes, seed-stablecoin-markets.
The panel was visible but never loaded content on mobile. Root cause:
constructor called hide() + set isHidden=true on mobile, but
applyPanelSettings() later called toggle(true) removing the hidden
class. Panel was visible but updateInsights() bailed on isHidden flag.
Server-side pre-computed insights (via bootstrap hydration from Railway)
need zero client-side ML — no reason to block mobile. Now allows server
insights through while still guarding against the heavy client-side
fallback pipeline on mobile devices.
The settings and signal modal overlays had z-index: 1000, which was
lower than the stacking context created by the globe's WebGL canvas
container. Bump both to z-index: 9999 to match other fullscreen overlays.
Globe mode switching created fresh map instances without re-pushing cached
data or re-wiring callbacks, losing all markers and event handlers.
- Cache all 30 data setter args and 6 callbacks in MapContainer
- Snapshot viewport state before switch, restore after
- Rehydrate cached data + callbacks on new map instance
- Disconnect ResizeObserver before switch to prevent duplicates
- Release all cached references on destroy()
- Handle severity "critical" in all Iran event renderers
- Fix full.ts DEFAULT_MAP_LAYERS iranAttacks: false → true
Root cause: any data update destroyed/recreated ALL DOM markers, arcs, paths,
and polygons. Eco mode only adjusted pixel ratio — no actual CPU reduction.
- Decouple flush channels so marker updates don't rebuild arcs/paths/polygons
- Set globe.gl accessors once at init; flush methods now only update data arrays
- Hybrid debounce (100ms trailing + 300ms maxWait) coalesces startup burst
- Eco mode disables dash animations (stops globe.gl RAF loop), pulse CSS, atmosphere
- Cache reversed GeoJSON rings to avoid recomputing on every flushPolygons()
- Remove duplicate ResizeObserver (MapContainer's observer handles it)
- Layer channel routing via static LAYER_CHANNELS map (single source of truth)
- Move conflict color maps to static readonly (no per-call allocation)
The GPS jamming data pipeline had no scheduled seed — fetch-gpsjam.mjs
existed as a standalone script but was never wired into the relay's
setInterval-based seed system. Redis key intelligence:gpsjam:v1 was
always empty, forcing the edge function to fall back to direct
gpsjam.org fetches (without lat/lon pre-computation).
Adds startGpsJamSeedLoop() that runs every 6 hours:
- Fetches manifest + CSV from gpsjam.org
- Parses H3 hex data with min-aircraft threshold
- Converts H3→lat/lon via h3-js (pre-computed for frontend)
- Classifies regions for conflict zone tagging
- Writes enriched data to Redis with 24h TTL
When CDN serves stale bootstrap responses with empty arrays (e.g.,
{anomalies:[]}), the hydration check `if (hydrated)` is truthy, causing
panels to skip the RPC fallback and show empty data. Fixed all 12
vulnerable getHydratedData consumers to verify the payload has actual
data before trusting it.
Also rewrote api/telegram-feed.js to inspect response body — empty
responses get short CDN TTL (15s) to prevent prolonged staleness.
Vercel edge runtime adds a default `Cache-Control: public` to responses.
The gateway's `!mergedHeaders.has('Cache-Control')` guard prevented
s-maxage tier headers from being applied, so Cloudflare had no TTL
guidance — caching was inconsistent across endpoints.
Remove the guard so the gateway always sets the correct tier
(fast/medium/slow/static/daily) with proper s-maxage values.
PR #1006 moved the emrldco analytics loader from <body> to <head>
but didn't add its sha256 hash to the Content-Security-Policy
script-src directive, causing the script to be blocked.
* feat: add seed-first pattern to 15 RPC handlers with Railway seed scripts
Migrate handlers from direct external API calls to seed-first pattern:
Railway cron seeds Redis → handlers read from Redis → fallback to live
fetch if seed stale and SEED_FALLBACK_* env enabled.
Handlers updated: earthquakes, fire-detections, internet-outages,
climate-anomalies, unrest-events, cyber-threats, market-quotes,
commodity-quotes, crypto-quotes, etf-flows, gulf-quotes,
stablecoin-markets, natural-events, displacement-summary, risk-scores.
Also adds:
- scripts/_seed-utils.mjs (shared seed framework with atomic publish,
distributed locks, retry, freshness metadata)
- 13 seed scripts for Railway cron
- api/seed-health.js monitoring endpoint
- scripts/validate-seed-migration.mjs post-deploy validation
- Restored multi-source CII in get-risk-scores (8 sources: ACLED,
UCDP, outages, climate, cyber, fires, GPS, Iran)
* feat: add seed scripts for market quotes, commodity quotes & airport delays
New seed scripts:
- seed-market-quotes.mjs: 28 symbols via Yahoo Finance + Finnhub
- seed-commodity-quotes.mjs: 6 commodity futures via Yahoo Finance
- seed-airport-delays.mjs: FAA + NOTAM airport closure data
Handler changes (seed-first pattern):
- list-market-quotes.ts: read from seed data before live fetch
- list-commodity-quotes.ts: read from seed data before live fetch
- list-airport-delays.ts: seed-first for FAA and NOTAM data
Other changes:
- ais-relay.cjs: add DISABLE_RELAY_MARKET_SEED guard for cutover
- _seed-utils.mjs: add sleep, parseYahooChart, writeExtraKey helpers
- seed-health.js: monitor 4 new seed domains
- validate-seed-migration.mjs: add new domains to validation
* fix: extract digest items from category buckets in seed-insights
The news digest Redis key stores items nested in category buckets
({ categories: { politics: { items: [...] }, ... } }), not as a
flat array. The script was checking for digest.items which is
undefined, causing "Digest has no items" errors on every run.
RSS feeds (CBS, CNN) update pubDate on article edits, making hours-old
stories appear "2m ago". Combined with the in-memory dedup map clearing
on every page load, the same article re-fires as "breaking" every app
open.
Two fixes:
- Persist dedup map to localStorage so seen alerts survive page reloads
- 10s startup grace period suppresses RSS alerts during initial feed
fetch (OREF siren alerts bypass this — real-time sirens never delayed)
* 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>
The geopoliticalBoundaries layer had only one entry (Korean DMZ) but
carried its own toggle, type, rendering code, popup, i18n strings,
and variant configs. Merge it into the existing conflicts layer to
reduce surface area.
- Add Korean DMZ as a ConflictZone (intensity: low) in CONFLICT_ZONES
- Remove GeopoliticalBoundary type, GEOPOLITICAL_BOUNDARIES export
- Remove dedicated layer rendering in DeckGLMap and GlobeMap
- Remove popup type and renderer in MapPopup
- Clean layer registry, variant configs, panels, E2E harnesses
- Remove i18n keys from all 21 locale files
Railway marks cron jobs as "failed" when the Node.js process doesn't
exit cleanly. The seed scripts relied on natural event loop drain,
but undici's connection pool keeps handles alive, causing Railway to
kill the process and mark it as failed.
Changes:
- Add process.exit(0) on success and lock-skip paths in runSeed()
- Fix recordCount for crypto (.quotes) and stablecoin (.stablecoins)
- Add writeExtraKey, sleep, parseYahooChart shared utilities
- Add extraKeys option to runSeed for bootstrap hydration keys
* fix(globe): add polygonGeoJsonGeometry accessor for polygon rendering
globe.gl's default polygonGeoJsonGeometry accessor looks for d.geometry,
but our GlobePolygon objects store coordinates in d.coords. Without this
explicit accessor, all polygon data (conflict zones, boundaries, CII
choropleth) was silently ignored — polygonsData was set but nothing
rendered.
* fix(globe): render conflict zone polygons correctly on 3D globe
Root cause: globe.gl's ConicPolygonGeometry + earcut renders CCW exterior
rings as their complement on the sphere (filling everything EXCEPT the
polygon). The simplified CONFLICT_ZONES coords also lacked enough vertices
for proper spherical tessellation.
Fix:
- Use real GeoJSON country geometries from countriesGeoData instead of
simplified CONFLICT_ZONES.coords (maps zone IDs to ISO-2 codes)
- Reverse winding order (CCW → CW) on all polygon rings so earcut fills
the polygon interior, not the complement
- Apply same winding fix to GEOPOLITICAL_BOUNDARIES polygons
- Zones without country mapping (Strait of Hormuz, South Lebanon, Red Sea)
are represented by center markers only
Tested: Iran, Ukraine, Gaza/Israel, Sudan, Myanmar all render as localized
red polygon overlays on the globe without envelope artifacts.
* fix(globe): guard against null geometry in GeoJSON features
Some GeoJSON features have null geometry which caused TypeError
in flushPolygons. Add null guards for both conflict zone and CII
choropleth polygon rendering paths.
The panel showed "Insufficient Data" even when Railway/Vercel had
pre-computed 8-source CII scores available, because the insufficient
check only looked at client-side GDELT/RSS freshness. Now fetches
cached server scores when overallStatus is insufficient (not just
during learning mode) and bypasses the insufficient view when those
scores are available.
- Add 56px accent-blue floating action button (bottom-right)
- Always visible regardless of scroll or horizontal overflow
- Hide subtitles and type badges on mobile results for cleaner look
- Wire FAB to same openSearch handler as header icon
The RSS_ALLOWED_DOMAINS refactor missed the redirect handler at line 4755,
causing ReferenceError: allowedDomains is not defined every time an RSS
feed returns a 301/302 redirect. This crashes the entire relay process.
flushPolygons() was only called from setCIIScores() and GeoJSON load,
never from setLayers()/enableLayer() or initial globe setup. Conflict
zone polygons were pushed to polygonsData but never rendered because
the flush never fired when the conflicts layer was active at startup.
* perf(rss): route RSS direct to Railway, skip Vercel middleman
Vercel /api/rss-proxy has 65% error rate (207K failed invocations/12h).
Route browser RSS requests directly to Railway (proxy.worldmonitor.app)
via Cloudflare CDN, eliminating Vercel as middleman.
- Add VITE_RSS_DIRECT_TO_RELAY feature flag (default off) for staged rollout
- Centralize RSS proxy URL in rssProxyUrl() with desktop/dev/prod routing
- Make Railway /rss public (skip auth, keep rate limiting with CF-Connecting-IP)
- Add wildcard *.worldmonitor.app CORS + always emit Vary: Origin on /rss
- Extract ~290 RSS domains to shared/rss-allowed-domains.cjs (single source of truth)
- Convert Railway domain check to Set for O(1) lookups
- Remove rss-proxy from KEYED_CLOUD_API_PATTERN (no longer needs API key header)
- Add edge function test for shared domain list import
* fix(edge): replace node:module with JSON import for edge-compatible RSS domains
api/_rss-allowed-domains.js used createRequire from node:module which is
unsupported in Vercel Edge Runtime, breaking all edge functions (including
api/gpsjam). Replaced with JSON import attribute syntax that works in both
esbuild (Vercel build) and Node.js 22+ (tests).
Also fixed middleware.ts TS18048 error where VARIANT_OG[variant] could be
undefined.
* test(edge): add guard against node: built-in imports in api/ files
Scans ALL api/*.js files (including _ helpers) for node: module imports
which are unsupported in Vercel Edge Runtime. This would have caught the
createRequire(node:module) bug before it reached Vercel.
* fix(edge): inline domain array and remove NextResponse reference
- Replace `import ... with { type: 'json' }` in _rss-allowed-domains.js
with inline array — Vercel esbuild doesn't support import attributes
- Replace `NextResponse.next()` with bare `return` in middleware.ts —
NextResponse was never imported
* ci(pre-push): add esbuild bundle check and edge function tests
The pre-push hook now catches Vercel build failures locally:
- esbuild bundles each api/*.js entrypoint (catches import attribute
syntax, missing modules, and other bundler errors)
- runs edge function test suite (node: imports, module isolation)
* Harden natural event category handling against class injection
* refactor: DRY up category allowlist, harden sanitizeClassToken
- Extract NATURAL_EVENT_CATEGORIES Set to src/types (single source of truth)
- eonet.ts imports shared Set instead of duplicating it
- sanitizeClassToken strips leading digits/hyphens for valid CSS class names
- Server copy kept separate (cannot import from src/types)
---------
Co-authored-by: Elie Habib <elie.habib@gmail.com>
The 3D globe only showed tiny circle markers at conflict zone centers.
Now renders full polygon overlays from CONFLICT_ZONES coords data with
intensity-based styling (color, altitude, stroke) and rich hover tooltips.
- Add 'conflict' kind to GlobePolygon with intensity/parties/casualties
- Fix layer semantics: boundaries use geopoliticalBoundaries toggle
- 3-way style branching for cii/conflict/boundary polygon rendering
- Ring normalization: close unclosed rings, skip degenerate polygons
- Shrink center marker (28→20px) since polygons carry visual weight
* feat: add mobile bottom sheet search with trigger bar
Replace the desktop-only Cmd+K search modal with a native-feeling
bottom sheet on mobile (<768px). Adds a fixed pill-shaped trigger
bar at the bottom of the screen, suggestion chips for quick access
to countries/commands, 48px touch targets, and slide-up animation.
Also removes the deprecated MobileWarningModal, adds a community
discussion link in settings, and improves mobile header layout.
* fix: improve mobile search bottom sheet UX
- Remove redundant trigger pill (header search icon is primary entry)
- Reduce sheet height from 75vh to 50vh for half-sheet feel
- Cap mobile results to 5 (2 per type) to avoid overwhelming small screens
- Add visualViewport listener for iOS keyboard-aware sheet resizing
- Replace clipped "Cancel" text with × icon + aria-label
- Hide chips on first keystroke to preserve vertical space
- Show 2 tips instead of 4 on mobile
- Revert panels-grid padding-bottom (no longer needed without pill)
* 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.
* fix(webcams): recover from blocked/stuck youtube embeds
---------
Co-authored-by: fayez bast <fayezbast15@gmail.com>
Replace the desktop-only Cmd+K search modal with a native-feeling
bottom sheet on mobile (<768px). Adds a fixed pill-shaped trigger
bar at the bottom of the screen, suggestion chips for quick access
to countries/commands, 48px touch targets, and slide-up animation.
Also removes the deprecated MobileWarningModal, adds a community
discussion link in settings, and improves mobile header layout.
* 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