Commit Graph

56 Commits

Author SHA1 Message Date
lspassos1
88282cc45f fix(deps): resolve all npm audit vulnerabilities (#1328)
- Add serialize-javascript >=7.0.4 override to fix RCE via RegExp.flags
  (workbox-build -> @rollup/plugin-terser -> serialize-javascript chain)
- Bump markdownlint-cli2 ^0.20.0 -> ^0.21.0 to fix markdown-it ReDoS
- Run npm audit fix to resolve ajv, dompurify, minimatch, rollup, and
  fast-xml-parser advisories via lockfile updates

npm audit: 0 vulnerabilities
2026-03-10 06:28:48 +04:00
Elie Habib
9772548d83 feat: add orbital surveillance layer with real-time satellite tracking (#1278)
Track ~80-120 intelligence-relevant satellites on the 3D globe using CelesTrak
TLE data and client-side SGP4 propagation (satellite.js). Satellites render at
actual orbital altitude with country-coded colors, 15-min orbit trails, and
ground footprint projections.

Architecture: Railway seeds TLEs every 2h → Redis → Vercel CDN (1h cache) →
browser does SGP4 math every 3s (zero server cost for real-time movement).

- New relay seed loop (ais-relay.cjs) fetching military + resource groups
- New edge handler (api/satellites.js) with 10min cache + negative cache
- Frontend service with circuit breaker and propagation lifecycle
- GlobeMap integration: markers, trails (pathsData), footprints, tooltips
- Layer registry as globe-only "Orbital Surveillance" with i18n (21 locales)
- Full documentation at docs/ORBITAL_SURVEILLANCE.md with roadmap
- Fix pre-existing SearchModal TS error (non-null assertion)
2026-03-08 21:55:46 +04:00
Elie Habib
fce836039b feat(map): migrate basemap from CARTO to self-hosted PMTiles on R2 (#1064)
* feat(map): migrate basemap from CARTO to self-hosted PMTiles on Cloudflare R2

Replace CARTO tile provider (frequent 403 errors) with self-hosted PMTiles
served from Cloudflare R2. Uses @protomaps/basemaps for style generation
with OpenFreeMap as automatic fallback when VITE_PMTILES_URL is unset.

- Add pmtiles and @protomaps/basemaps dependencies
- Create src/config/basemap.ts for PMTiles protocol registration and style building
- Update DeckGLMap.ts to use PMTiles styles (non-happy variants)
- Fix fallback detection using data event instead of style.load
- Update SW cache rules: replace CARTO/MapTiler with PMTiles NetworkFirst
- Add Protomaps preconnect hints in index.html
- Bundle pmtiles + @protomaps/basemaps in maplibre chunk
- Upload 3.4GB world tiles (zoom 0-10) to R2 bucket worldmonitor-maps

* fix(map): use CDN custom domain maps.worldmonitor.app for PMTiles

Replace r2.dev URL with custom domain backed by Cloudflare CDN edge.
Update preconnect hint and .env.example with production URL.

* fix(map): harden PMTiles fallback detection to prevent false triggers

- Require 2+ network errors before triggering OpenFreeMap fallback
- Use persistent data listener instead of once (clears timeout on first tile load)
- Increase fallback timeout to 10s for PMTiles header + initial tile fetch
- Add console.warn for map errors to aid debugging
- Remove redundant style.load listener (fires immediately for inline styles)

* feat(settings): add Map Tile Provider selector in settings

Add dropdown in Settings → Map section to switch between:
- Auto (PMTiles → OpenFreeMap fallback)
- PMTiles (self-hosted)
- OpenFreeMap
- CARTO

Choice persists in localStorage and reloads basemap instantly.

* fix(map): make OSS-friendly — default to free OpenFreeMap, hide PMTiles when unconfigured

- Default to OpenFreeMap when VITE_PMTILES_URL is unset (zero config for OSS users)
- Hide PMTiles/Auto options from settings dropdown when no PMTiles URL configured
- If user previously selected PMTiles but env var is removed, gracefully fall back
- Remove production URL from .env.example to avoid exposing hosted tiles
- Add docs link for self-hosting PMTiles in .env.example

* docs: add map tile provider documentation to README and MAP_ENGINE.md

Document the tile provider system (OpenFreeMap, CARTO, PMTiles) in
MAP_ENGINE.md with self-hosting instructions, fallback behavior, and
OSS-friendly defaults. Update README to reference tile providers in
the feature list, tech stack, and environment variables table.

* fix: resolve rebase conflicts and fix markdown lint errors

- Restore OSS-friendly basemap defaults (MAP_PROVIDER_OPTIONS as IIFE,
  getMapProvider with hasTilesUrl check)
- Fix markdown lint: add blank lines after ### headings in README
- Reconcile UnifiedSettings import with MAP_PROVIDER_OPTIONS constant
2026-03-06 08:40:14 +04:00
Elie Habib
29ef8eae2f docs: update README with accurate counts and 9 new feature sections (#1071)
- Fix stale counts: 170+ feeds → 435+, 15 bootstrap keys → 38,
  28+ data sources → 31, 20+ search types → 24, panel counts
- Add Aviation Intelligence Panel documentation
- Add Customizable Market Watchlist section
- Add News Importance Scoring algorithm details
- Add Railway Seed Data Pipeline table (21 cron jobs)
- Add SmartPollLoop adaptive polling documentation
- Expand Prediction Markets with 4-tier fetch strategy
- Add Iran conflict monitoring layer details
- Add Mobile search sheet and FAB section
- Expand Regression Testing section (30 files, 554 tests)
- Expand Bootstrap Hydration with full 38-key tier listing
- Bump version 2.5.24 → 2.5.25
2026-03-05 23:40:37 +04:00
Mert Efe Şensoy
f771114522 feat: aviation monitoring layer with flight tracking, airline intel panel, and news feeds (#907)
* 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>
2026-03-04 21:09:37 +04:00
Elie Habib
034ab9916f feat(globe): add interactive 3D globe view with 28 live data layers (#926)
* feat(globe): add 3D globe view powered by globe.gl

Replicate the Sentinel.axonia.us globe locally and expose it via Settings.

- Add GlobeMap.ts: new globe.gl v2 component with night-sky starfield,
  earth topobathy texture, specular water map, atmosphere glow, auto-rotate
  (pauses on interaction, resumes after 60 s), and HTML marker layer for
  conflict zones, intel hotspots, and other data categories
- Update MapContainer with switchToGlobe() / switchToFlat() runtime
  methods and isGlobeMode() query; constructor accepts preferGlobe param
- Wire globe toggle in UnifiedSettings General tab (MAP section);
  persisted to worldmonitor-map-mode via loadFromStorage/saveToStorage
- Add mapMode storage key to STORAGE_KEYS
- Download earth textures to public/textures/ (topo-bathy, night-sky,
  water specular, day)
- Add globe.gl ^2.45.0 and @types/three dependencies
- Add globe CSS + @keyframes globe-pulse for pulsing conflict markers

* feat(globe): wire region selector & CMD+K navigation to 3D globe

* feat(globe): add zoom controls, layer panel, marker tooltips; fix Vercel build

* feat(globe): expand to all 28 world-variant layers with live data rendering

* refactor(globe): use proper keyof MapLayers types

* fix(globe): route AIS/flight data to globe, implement ship traffic markers, hide dayNight toggle

- MapContainer: add globe guard to setAisData and setFlightDelays (data was silently dropped)
- GlobeMap: implement setAisData with AisDisruptionMarker (typed, no any casts); renders
  disruption events with severity-colored ship icons and full tooltip (name/type/severity)
- GlobeMap: three-point dayNight suppression — disabled in initGlobe(), overridden in
  setLayers(), ignored in enableLayer(); toggle removed from layer panel UI
- MapContainer: add globe guards to 5 happy-variant setters (P3: keep no-op stubs in globe)
- Add tests/globe-2d-3d-parity.test.mjs: 13 static-analysis tests covering routing,
  AIS marker fields, and dayNight suppression (all passing)
2026-03-03 21:08:06 +04:00
Nicolas Dos Santos
7373135978 fix: resolve build errors and fullscreen overlay bugs (#857, #859, #860, #829)
Add missing supercluster, preact, @types/geojson, and @types/supercluster dependencies. Remove DOM reparenting in LiveNewsPanel fullscreen toggle that caused iframe reloads and channel switching. Hide sibling panels and map overlays when a panel is in fullscreen mode.
2026-03-03 09:56:25 +04:00
Elie Habib
078a239ceb feat(live-news): add CNN & CNBC HLS streams via sidecar proxy (#682)
* feat(live-news): add CNN & CNBC HLS streams via sidecar proxy (desktop only)

Add /api/hls-proxy route to sidecar that proxies HLS manifests and
segments from allowlisted CDN hosts, injecting the required Referer
header that browsers cannot set. Rewrites m3u8 URLs so all segments
and encryption keys also route through the proxy.

Desktop gets native <video> HLS playback for CNN and CNBC; web falls
through to YouTube as before (no bandwidth cost on Vercel).

* fix(types): add missing @types/dompurify dev dependency
2026-03-01 21:06:18 +04:00
Chris Chen
a7efa7dda8 feat: implement deduct situation feature (#636) (#642)
* Add Security Advisories panel with government travel alerts (#460)

* feat: add Security Advisories panel with government travel advisory feeds

Adds a new panel aggregating travel/security advisories from official
government foreign affairs agencies (US State Dept, AU DFAT Smartraveller,
UK FCDO, NZ MFAT). Advisories are categorized by severity level
(Do Not Travel, Reconsider, Caution, Normal) with filter tabs by
source country. Includes summary counts, auto-refresh, and persistent
caching via the existing data-freshness system.


* chore: update package-lock.json


* fix: event delegation, localization, and cleanup for SecurityAdvisories panel

P1 fixes:
- Use event delegation on this.content (bound once in constructor) instead
  of direct addEventListener after each innerHTML replacement — prevents
  memory leaks and stale listener issues on re-render
- Use setContent() consistently instead of mixing with this.content.innerHTML
- Add securityAdvisories translations to all 16 non-English locale files
  (panels name, component strings, common.all key)
- Revert unrelated package-lock.json version bump

P2 fixes:
- Deduplicate loadSecurityAdvisories — loadIntelligenceData now calls the
  shared method instead of inlining duplicate fetch+set logic
- Add Accept header to fetch calls for better content negotiation

* feat(advisories): add US embassy alerts, CDC, ECDC, and WHO health feeds

Adds 21 new advisory RSS feeds:
- 13 US Embassy per-country security alerts (TH, AE, DE, UA, MX, IN, PK, CO, PL, BD, IT, DO, MM)
- CDC Travel Notices
- 5 ECDC feeds (epidemiological, threats, risk assessments, avian flu, publications)
- 2 WHO feeds (global news, Africa emergencies)

Panel gains a Health filter tab for CDC/ECDC/WHO sources.
All new domains added to RSS proxy allowlist.
i18n "health" key added across all 17 locales.

* feat(cache): add negative-result caching to cachedFetchJson (#466)

When upstream APIs return errors (HTTP 403, 429, timeout), fetchers
return null. Previously null results were not cached, causing repeated
request storms against broken APIs every refresh cycle.

Now caches a sentinel value ('__WM_NEG__') with a short 2-minute TTL
on null results. Subsequent requests within that window get null
immediately without hitting upstream. Thrown errors (transient) skip
sentinel caching and retry immediately.

Also filters sentinels from getCachedJsonBatch pipeline reads and fixes
theater posture coalescing test (expected 2 OpenSky fetches for 2
theater query regions, not 1).

* feat: convert 52 API endpoints from POST to GET for edge caching (#468)

* feat: convert 52 API endpoints from POST to GET for edge caching

Convert all cacheable sebuf RPC endpoints to HTTP GET with query/path
parameters, enabling CDN edge caching to reduce costs. Flatten nested
request types (TimeRange, PaginationRequest, BoundingBox) into scalar
query params. Add path params for resource lookups (GetFredSeries,
GetHumanitarianSummary, GetCountryStockIndex, GetCountryIntelBrief,
GetAircraftDetails). Rewrite router with hybrid static/dynamic matching
for path param support.

Kept as POST: SummarizeArticle, ClassifyEvent, RecordBaselineSnapshot,
GetAircraftDetailsBatch, RegisterInterest.

Generated with sebuf v0.9.0 (protoc-gen-ts-client, protoc-gen-ts-server).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add rate_limited field to market response protos

The rateLimited field was hand-patched into generated files on main but
never declared in the proto definitions. Regenerating wiped it out,
breaking the build. Now properly defined in both ListEtfFlowsResponse
and ListMarketQuotesResponse protos.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: remove accidentally committed .planning files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add Cloudflare edge caching infrastructure for api.worldmonitor.app (#471)

Route web production RPC traffic through api.worldmonitor.app via fetch
interceptor (installWebApiRedirect). Add default Cache-Control headers
(s-maxage=300, stale-while-revalidate=60) on GET 200 responses, with
no-store override for real-time endpoints (vessel snapshot). Update CORS
to allow GET method. Skip Vercel bot middleware for API subdomain using
hostname check (non-spoofable, replacing CF-Ray header approach). Update
desktop cloud fallback to route through api.worldmonitor.app.

* fix(beta): eagerly load T5-small model when beta mode is enabled

BETA_MODE now couples the badge AND model loading — the summarization-beta
model starts loading on startup instead of waiting for the first summarization call.

* fix: move 5 path-param endpoints to query params for Vercel routing (#472)

Vercel's `api/[domain]/v1/[rpc].ts` captures one dynamic segment.
Path params like `/get-humanitarian-summary/SA` add an extra segment
that has no matching route file, causing 404 on both OPTIONS preflight
and direct requests. These endpoints were broken in production.

Changes:
- Remove `{param}` from 5 service.proto HTTP paths
- Add `(sebuf.http.query)` annotations to request message fields
- Update generated client/server code to use URLSearchParams
- Update OpenAPI specs (YAML + JSON) to declare query params
- Add early-return guards in 4 handlers for missing required params
- Add happy.worldmonitor.app to runtime.ts redirect hosts

Affected endpoints:
- GET /api/conflict/v1/get-humanitarian-summary?country_code=SA
- GET /api/economic/v1/get-fred-series?series_id=T10Y2Y&limit=120
- GET /api/market/v1/get-country-stock-index?country_code=US
- GET /api/intelligence/v1/get-country-intel-brief?country_code=US
- GET /api/military/v1/get-aircraft-details?icao24=a12345

* fix(security-advisories): route feeds through RSS proxy to avoid CORS blocks (#473)

- Advisory feeds were fetched directly from the browser, hitting CORS
  on all 21 feeds (US State Dept, AU Smartraveller, US Embassies, ECDC,
  CDC, WHO). Route through /api/rss-proxy on web, keep proxyUrl for desktop.
- Fix double slash in ECDC Avian Influenza URL (323//feed → 323/feed)
- Add feeds.news24.com to RSS proxy allowlist (was returning 403)

* feat(cache): tiered edge Cache-Control aligned to upstream TTLs (#474)

* fix: move 5 path-param endpoints to query params for Vercel routing

Vercel's `api/[domain]/v1/[rpc].ts` captures one dynamic segment.
Path params like `/get-humanitarian-summary/SA` add an extra segment
that has no matching route file, causing 404 on both OPTIONS preflight
and direct requests. These endpoints were broken in production.

Changes:
- Remove `{param}` from 5 service.proto HTTP paths
- Add `(sebuf.http.query)` annotations to request message fields
- Update generated client/server code to use URLSearchParams
- Update OpenAPI specs (YAML + JSON) to declare query params
- Add early-return guards in 4 handlers for missing required params
- Add happy.worldmonitor.app to runtime.ts redirect hosts

Affected endpoints:
- GET /api/conflict/v1/get-humanitarian-summary?country_code=SA
- GET /api/economic/v1/get-fred-series?series_id=T10Y2Y&limit=120
- GET /api/market/v1/get-country-stock-index?country_code=US
- GET /api/intelligence/v1/get-country-intel-brief?country_code=US
- GET /api/military/v1/get-aircraft-details?icao24=a12345

* feat(cache): add tiered edge Cache-Control aligned to upstream TTLs

Replace flat s-maxage=300 with 5 tiers (fast/medium/slow/static/no-store)
mapped per-endpoint to respect upstream Redis TTLs. Adds stale-if-error
resilience headers and X-No-Cache plumbing for future degraded responses.
X-Cache-Tier debug header gated behind ?_debug query param.

* fix(tech): use rss() for CISA feed, drop build from pre-push hook (#475)

- CISA Advisories used dead rss.worldmonitor.app domain (404), switch to rss() helper
- Remove Vite build from pre-push hook (tsc already catches errors)

* fix(desktop): enable click-to-play YouTube embeds + CISA feed fixes (#476)

* fix(tech): use rss() for CISA feed, drop build from pre-push hook

- CISA Advisories used dead rss.worldmonitor.app domain (404), switch to rss() helper
- Remove Vite build from pre-push hook (tsc already catches errors)

* fix(desktop): enable click-to-play for YouTube embeds in WKWebView

WKWebView blocks programmatic autoplay in cross-origin iframes regardless
of allow attributes, Permissions-Policy, mute-first retries, or secure
context. Documented all 10 approaches tested in docs/internal/.

Changes:
- Switch sidecar embed origin from 127.0.0.1 to localhost (secure context)
- Add MutationObserver + retry chain as best-effort autoplay attempts
- Use postMessage('*') to fix tauri://localhost cross-origin messaging
- Make sidecar play overlay non-interactive (pointer-events:none)
- Fix .webcam-iframe pointer-events:none blocking clicks in grid view
- Add expand button to grid cells for switching to single view on desktop
- Add http://localhost:* to CSP frame-src in index.html and tauri.conf.json

* fix(gateway): convert stale POST requests to GET for backwards compat (#477)

Stale cached client bundles still send POST to endpoints converted to
GET in PR #468, causing 404s. The gateway now parses the POST JSON body
into query params and retries the match as GET.

* feat(proxy): add Cloudflare edge caching for proxy.worldmonitor.app (#478)

Add CDN-Cache-Control headers to all proxy endpoints so Cloudflare can
cache responses at the edge independently of browser Cache-Control:

- RSS: 600s edge + stale-while-revalidate=300 (browser: 300s)
- UCDP: 3600s edge (matches browser)
- OpenSky: 15s edge (browser: 30s) for fresher flight data
- WorldBank: 1800s/86400s edge (matches browser)
- Polymarket: 120s edge (matches browser)
- Telegram: 10s edge (matches browser)
- AIS snapshot: 2s edge (matches browser)

Also fixes:
- Vary header merging: sendCompressed/sendPreGzipped now merge existing
  Vary: Origin instead of overwriting, preventing cross-origin cache
  poisoning at the edge
- Stale fallback responses (OpenSky, WorldBank, Polymarket, RSS) now
  set Cache-Control: no-store + CDN-Cache-Control: no-store to prevent
  edge caching of degraded responses
- All no-cache branches get CDN-Cache-Control: no-store
- /opensky-reset gets no-store (state-changing endpoint)

* fix(sentry): add noise filters for 4 unresolved issues (#479)

- Tighten AbortError filter to match "AbortError: The operation was aborted"
- Filter "The user aborted a request" (normal navigation cancellation)
- Filter UltraViewer service worker injection errors (/uv/service/)
- Filter Huawei WebView __isInQueue__ injection

* feat: configurable VITE_WS_API_URL + harden POST→GET shim (#480)

* fix(gateway): harden POST→GET shim with scalar guard and size limit

- Only convert string/number/boolean values to query params (skip objects,
  nested arrays, __proto__ etc.) to prevent prototype pollution vectors
- Skip body parsing for Content-Length > 1MB to avoid memory pressure

* feat: make API base URL configurable via VITE_WS_API_URL

Replace hardcoded api.worldmonitor.app with VITE_WS_API_URL env var.
When empty, installWebApiRedirect() is skipped entirely — relative
/api/* calls stay on the same domain (local installs). When set,
browser fetch is redirected to that URL.

Also adds VITE_WS_API_URL and VITE_WS_RELAY_URL hostnames to
APP_HOSTS allowlist dynamically.

* fix(analytics): use greedy regex in PostHog ingest rewrites (#481)

Vercel's :path* wildcard doesn't match trailing slashes that
PostHog SDK appends (e.g. /ingest/s/?compression=...), causing 404s.
Switch to :path(.*) which matches all path segments including
trailing slashes. Ref: PostHog/posthog#17596

* perf(proxy): increase AIS snapshot edge TTL from 2s to 10s (#482)

With 20k requests/30min (60% of proxy traffic) and per-PoP caching,
a 2s edge TTL expires before the next request from the same PoP arrives,
resulting in near-zero cache hits. 10s allows same-PoP dedup while
keeping browser TTL at 2s for fresh vessel positions.

* fix(markets): commodities panel showing stocks instead of commodities (#483)

The shared circuit breaker (cacheTtlMs: 0) cached the stocks response,
then the stale-while-revalidate path returned that cached stocks data
for the subsequent commodities fetch. Skip SWR when caching is disabled.

* feat(gateway): complete edge cache tier coverage + degraded-response policy (#484)

- Add 11 missing GET routes to RPC_CACHE_TIER map (8 slow, 3 medium)
- Add response-headers side-channel (WeakMap) so handlers can signal
  X-No-Cache without codegen changes; wire into military-flights and
  positive-geo-events handlers on upstream failure
- Add env-controlled per-endpoint tier override (CACHE_TIER_OVERRIDE_*)
  for incident response rollback
- Add VITE_WS_API_URL hostname allowlist (*.worldmonitor.app + localhost)
- Fix fetch.bind(globalThis) in positive-events-geo.ts (deferred lambda)
- Add CI test asserting every generated GET route has an explicit cache
  tier entry (prevents silent default-tier drift)

* chore: bump version to 2.5.20 + changelog

Covers PRs #452–#484: Cloudflare edge caching, commodities SWR fix,
security advisories panel, settings redesign, 52 POST→GET migrations.

* fix(rss): remove stale indianewsnetwork.com from proxy allowlist (#486)

Feed has no <pubDate> fields and latest content is from April 2022.
Not referenced in any feed config — only in the proxy domain allowlist.

* feat(i18n): add Korean (한국어) localization (#487)

- Add ko.json with all 1606 translation keys matching en.json structure
- Register 'ko' in SUPPORTED_LANGUAGES, LANGUAGES display array, and locale map
- Korean appears as 🇰🇷 한국어 in the language dropdown

* feat: add Polish tv livestreams (#488)

* feat(rss): add Axios (api.axios.com/feed) as US news source (#494)

Add api.axios.com to proxy allowlist and CSP connect-src, register
Axios feed under US category as Tier 2 mainstream source.

* perf: bootstrap endpoint + polling optimization (#495)

* 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

* feat(military): server-side military bases 125K + rate limiting (#496)

* feat(military): server-side military bases with 125K entries + rate limiting (#485)

Migrate military bases from 224 static client-side entries to 125,380
server-side entries stored in Redis GEO sorted sets, served via
bbox-filtered GEOSEARCH endpoint with server-side clustering.

Data pipeline:
- Pizzint/Polyglobe: 79,156 entries (Supabase extraction)
- OpenStreetMap: 45,185 entries
- MIRTA: 821 entries
- Curated strategic: 218 entries
- 277 proximity duplicates removed

Server:
- ListMilitaryBases RPC with GEOSEARCH + HMGET + tier/filter/clustering
- Antimeridian handling (split bbox queries)
- Blue-green Redis deployment with atomic version pointer switch
- geoSearchByBox() + getHashFieldsBatch() helpers in redis.ts

Security:
- @upstash/ratelimit: 60 req/min sliding window per IP
- IP spoofing fix: prioritize x-real-ip (Vercel-injected) over x-forwarded-for
- Require API key for non-browser requests (blocks unauthenticated curl/scripts)
- Input validation: allowlisted types/kinds, regex country, clamped bbox/zoom

Frontend:
- Viewport-driven loading with bbox quantization + debounce
- Server-side grid clustering at low zoom levels
- Enriched popup with kind, category badges (airforce/naval/nuclear/space)
- Static 224 bases kept as search fallback + initial render

* fix(military): fallback to production Redis keys in preview deployments

Preview deployments prefix Redis keys with `preview:{sha}:` but military
bases data is seeded to unprefixed (production) keys. When the prefixed
`military:bases:active` key is missing, fall back to the unprefixed key
and use raw (unprefixed) keys for geo/meta lookups.

* fix: remove unused 'remaining' destructure in rate-limit (TS6133)

* ci: add typecheck:api to pre-push hook to catch server-side TS errors

* debug(military): add X-Bases-Debug response header for preview diagnostics

* fix(bases): trigger initial server fetch on map load

fetchServerBases() was only called on moveend — if the user
never panned/zoomed, the API was never called and only the 224
static fallback bases showed.

* perf(military): debounce base fetches + upgrade edge cache to static tier (#497)

- Add 300ms debounce on moveend to prevent rapid pan flooding
- Fixes stale-bbox bug where pendingFetch returns old viewport data
- Upgrade edge cache tier from medium (5min) to static (1hr) — bases are
  static infrastructure, aligned with server-side cachedFetchJson TTL
- Keep error logging in catch blocks for production diagnostics

* fix(cyber): make GeoIP centroid fallback jitter deterministic (#498)

Replace Math.random() jitter with DJB2 hash seeded by the threat
indicator (IP/URL), so the same threat always maps to the same
coordinates across requests while different threats from the same
country still spread out.

Closes #203

Co-authored-by: Chris Chen <fuleinist@users.noreply.github.com>

* fix: use cross-env for Windows-compatible npm scripts (#499)

Replace direct `VAR=value command` syntax with cross-env/cross-env-shell
so dev, build, test, and desktop scripts work on Windows PowerShell/CMD.

Co-authored-by: facusturla <facusturla@users.noreply.github.com>

* feat(live-news): add CBC News to optional North America channels (#502)

YouTube handle @CBCNews with fallback video ID 5vfaDsMhCF4.

* fix(bootstrap): harden hydration cache + polling review fixes (#504)

- Filter null/undefined values before storing in hydration cache to
  prevent future consumers using !== undefined from misinterpreting
  null as valid data
- Debounce wm:intelligence-updated event handler via requestAnimationFrame
  to coalesce rapid alert generation into a single render pass
- Include alert IDs in StrategicRiskPanel change fingerprint so content
  changes are detected even when alert count stays the same
- Replace JSON.stringify change detection in ServiceStatusPanel with
  lightweight name:status fingerprint
- Document max effective refresh interval (40x base) in scheduler

* fix(geo): tokenization-based keyword matching to prevent false positives (#503)

* fix(geo): tokenization-based keyword matching to prevent false positives

Replace String.includes() with tokenization-based Set.has() matching
across the geo-tagging pipeline. Prevents false positives like "assad"
matching inside "ambassador" and "hts" matching inside "rights".

- Add src/utils/keyword-match.ts as single source of truth
- Decompose possessives/hyphens ("Assad's" → includes "assad")
- Support multi-word phrase matching ("white house" as contiguous)
- Remove false-positive-prone DC keywords ('house', 'us ')
- Update 9 consumer files across geo-hub, map, CII, and asset systems
- Add 44 tests covering false positives, true positives, edge cases

Co-authored-by: karim <mirakijka@gmail.com>
Fixes #324

* fix(geo): add inflection suffix matching + fix test imports

Address code review feedback:

P1a: Add suffix-aware matching for plurals and demonyms so existing
keyword lists don't regress (houthi→houthis, ukraine→ukrainian,
iran→iranian, israel→israeli, russia→russian, taiwan→taiwanese).
Uses curated suffix list + e-dropping rule to avoid false positives.

P1b: Expand conflictTopics arrays in DeckGLMap and Map with demonym
forms so "Iranian senate..." correctly registers as conflict topic.

P2: Replace inline test functions with real module import via tsx.
Tests now exercise the production keyword-match.ts directly.

* fix: wire geo-keyword tests into test:data command

The .mts test file wasn't covered by `node --test tests/*.test.mjs`.
Add `npx tsx --test tests/*.test.mts` so test:data runs both suites.

* fix: cross-platform test:data + pin tsx in devDependencies

- Use tsx as test runner for both .mjs and .mts (single invocation)
- Removes ; separator which breaks on Windows cmd.exe
- Add tsx to devDependencies so it works in offline/CI environments

* fix(geo): multi-word demonym matching + short-keyword suffix guard

- Add wordMatches() for suffix-aware phrase matching so "South Korean"
  matches keyword "south korea" and "North Korean" matches "north korea"
- Add MIN_SUFFIX_KEYWORD_LEN=4 guard so short keywords like "ai", "us",
  "hts" only do exact-match (prevents "ais"→"ai", "uses"→"us" false positives)
- Add 5 new tests covering both fixes (58 total, all passing)

* fix(geo): support plural demonyms in keyword matching

Add compound suffixes (ians, eans, ans, ns, is) to handle plural
demonym forms like "Iranians"→"iran", "Ukrainians"→"ukraine",
"Russians"→"russia", "Israelis"→"israel". Adds 5 new tests (63 total).

---------

Co-authored-by: karim <mirakijka@gmail.com>

* chore: strip 61 debug console.log calls from 20 service files (#501)

* chore: strip 61 debug console.log calls from services

Remove development/tracing console.log statements from 20 files.
These add noise to production browser consoles and increase bundle size.

Preserved: all console.error (error handling) and console.warn (warnings).
Preserved: debug-gated logs in runtime.ts (controlled by verbose flag).

Removed: debugInjectTestEvents() from geo-convergence.ts (test-only code).
Removed: logSummary()/logReport() methods that were pure console.log wrappers.

* fix: remove orphaned stubs and remaining debug logs from stripped services

- Remove empty logReport() method and unused startTime variable (parallel-analysis.ts)
- Remove orphaned console.group/console.groupEnd pair (parallel-analysis.ts)
- Remove empty logSignalSummary() export (signal-aggregator.ts)
- Remove logSignalSummary import/call and 3 remaining console.logs (InsightsPanel.ts)
- Remove no-op logDirectFetchBlockedOnce() and dead infrastructure (prediction/index.ts)

* fix: generalize Vercel preview origin regex + include filters in bases cache key (#506)

- api/_api-key.js: preview URL pattern was user-specific (-elie-),
  rejecting other collaborators' Vercel preview deployments.
  Generalized to match any worldmonitor-*.vercel.app origin.

- military-bases.ts: client cache key only checked bbox/zoom, ignoring
  type/kind/country filters. Switching filters without panning returned
  stale results. Unified into single cacheKey string.

* fix(prediction): filter stale/expired markets from Polymarket panel (#507)

Prediction panel was showing expired markets (e.g. "Will US strike Iran
on Feb 9" at 0%). Root causes: no active/archived API filters, no
end_date_min param, no client-side expiry guard, and sub-market selection
picking highest volume before filtering expired ones.

- Add active=true, archived=false, end_date_min API params to all 3
  Gamma API call sites (events, markets, probe)
- Pre-filter sub-markets by closed/expired BEFORE volume selection in
  both fetchPredictions() and fetchCountryMarkets()
- Add defense-in-depth isExpired() client-side filter on final results
- Propagate endDate through all market object paths including sebuf
  fallback
- Show expiry date in PredictionPanel UI with new .prediction-meta
  layout
- Add "closes" i18n key to all 18 locale files
- Add endDate to server handler GammaMarket/GammaEvent interfaces and
  map to proto closesAt field

* fix(relay): guard proxy handlers against ERR_HTTP_HEADERS_SENT crash (#509)

Polymarket and World Bank proxy handlers had unguarded res.writeHead()
calls in error/timeout callbacks that race with the response callback.
When upstream partially responds then times out, both paths write
headers → process crash. Replace 5 raw writeHead+end calls with
safeEnd() which checks res.headersSent before writing.

* feat(breaking-news): add active alert banner with audio for critical/high RSS items (#508)

RSS items classified as critical/high threat now trigger a full-width
breaking news banner with audio alert, auto-dismiss (60s/30s by severity),
visibility-aware timer pause, dedup, and a toggle in the Intelligence
Findings dropdown.

* fix(sentry): filter Android OEM WebView bridge injection errors (#510)

Add ignoreErrors pattern for LIDNotifyId, onWebViewAppeared, and
onGetWiFiBSSID — native bridge functions injected by Lenovo/Huawei
device SDKs into Chrome Mobile WebView. No stack frames in our code.

* chore: add validated telegram channels list (global + ME + Iran + cyber) (#249)

* feat(conflict): add Iran Attacks map layer + strip debug logs (#511)

* chore: strip 61 debug console.log calls from services

Remove development/tracing console.log statements from 20 files.
These add noise to production browser consoles and increase bundle size.

Preserved: all console.error (error handling) and console.warn (warnings).
Preserved: debug-gated logs in runtime.ts (controlled by verbose flag).

Removed: debugInjectTestEvents() from geo-convergence.ts (test-only code).
Removed: logSummary()/logReport() methods that were pure console.log wrappers.

* fix: remove orphaned stubs and remaining debug logs from stripped services

- Remove empty logReport() method and unused startTime variable (parallel-analysis.ts)
- Remove orphaned console.group/console.groupEnd pair (parallel-analysis.ts)
- Remove empty logSignalSummary() export (signal-aggregator.ts)
- Remove logSignalSummary import/call and 3 remaining console.logs (InsightsPanel.ts)
- Remove no-op logDirectFetchBlockedOnce() and dead infrastructure (prediction/index.ts)

* feat(conflict): add Iran Attacks map layer

Adds a new Iran-focused conflict events layer that aggregates real-time
events, geocodes via 40-city lookup table, caches 15min in Redis, and
renders as a toggleable DeckGL ScatterplotLayer with severity coloring.

- New proto + codegen for ListIranEvents RPC
- Server handler with HTML parsing, city geocoding, category mapping
- Frontend service with circuit breaker
- DeckGL ScatterplotLayer with severity-based color/size
- MapPopup with sanitized source links
- iranAttacks toggle across all variants, harnesses, and URL state

* fix: resolve bootstrap 401 and 429 rate limiting on page init (#512)

Same-origin browser requests don't send Origin header (per CORS spec),
causing validateApiKey to reject them. Extract origin from Referer as
fallback. Increase rate limit from 60 to 200 req/min to accommodate
the ~50 requests fired during page initialization.

* fix(relay): prevent Polymarket OOM via request deduplication (#513)

Concurrent Polymarket requests for the same cache key each fired
independent https.get() calls. With 12 categories × multiple clients,
740 requests piled up in 10s, all buffering response bodies → 4.1GB
heap → OOM crash on Railway.

Fix: in-flight promise map deduplicates concurrent requests to the
same cache key. 429/error responses are negative-cached for 30s to
prevent retry storms.

* fix(threat-classifier): add military/conflict keyword gaps and news-to-conflict bridge (#514)

Breaking news headlines like "Israel's strike on Iran" were classified as
info level because the keyword classifier lacked standalone conflict phrases.
Additionally, the conflict instability score depended solely on ACLED data
(1-7 day lag) with no bridge from real-time breaking news.

- Add 3 critical + 18 high contextual military/conflict keywords
- Preserve threat classification on semantically merged clusters
- Add news-derived conflict floor when ACLED/HAPI report zero signal
- Upsert news events by cluster ID to prevent duplicates
- Extract newsEventIndex to module-level Map for serialization safety

* fix(breaking-news): let critical alerts bypass global cooldown and replace HIGH alerts (#516)

Global cooldown (60s) was blocking critical alerts when a less important
HIGH alert fired from an earlier RSS batch. Added priority-aware cooldown
so critical alerts always break through. Banner now auto-dismisses HIGH
alerts when a CRITICAL arrives. Added Iran/strikes keywords to classifier.

* fix(rate-limit): increase sliding window to 300 req/min (#515)

App init fires many concurrent classify-event, summarize-article, and
record-baseline-snapshot calls, exhausting the 200/min limit and causing
429s. Bump to 300 as a temporary measure while client-side batching is
implemented.

* fix(breaking-news): fix fake pubDate fallback and filter noisy think-tank alerts (#517)

Two bugs causing stale CrisisWatch article to fire as breaking alert:
1. Non-standard pubDate format ("Friday, February 27, 2026 - 12:38")
   failed to parse → fallback was `new Date()` (NOW) → day-old articles
   appeared as "just now" and passed recency gate on every fetch
2. Tier 3+ sources (think tanks) firing alerts on keyword-only matches
   like "War" in policy analysis titles — too noisy for breaking alerts

Fix: parsePubDate() handles non-standard formats and falls back to
epoch (not now). Tier 3+ sources require LLM classification to fire.

* fix: make iran-events handler read-only from Redis (#518)

Remove server-side LiveUAMap scraper (blocked by Cloudflare 403 on
Vercel IPs). Handler now reads pre-populated Redis cache pushed from
local browser scraping. Change cache tier from slow to fast to prevent
CDN from serving stale empty responses for 30+ minutes.

* fix(relay): Polymarket circuit breaker + concurrency limiter (OOM fix) (#519)

* fix(rate-limit): increase sliding window to 300 req/min

App init fires many concurrent classify-event, summarize-article, and
record-baseline-snapshot calls, exhausting the 200/min limit and causing
429s. Bump to 300 as a temporary measure while client-side batching is
implemented.

* fix(relay): add Polymarket circuit breaker + concurrency limiter to prevent OOM

Railway relay OOM crash: 280 Polymarket 429 errors in 8s, heap hit 3.7GB.
Multiple unique cache keys bypassed per-key dedup, flooding upstream.

- Circuit breaker: trips after 5 consecutive failures, 60s cooldown
- Concurrent upstream limiter: max 3 simultaneous requests
- Negative cache TTL: 30s → 60s to reduce retry frequency
- Upstream slot freed on response.on('end'), not headers, preventing
  body buffer accumulation past the concurrency cap

* fix(relay): guard against double-finalization on Polymarket timeout

request.destroy() in timeout handler also fires request.on('error'),
causing double decrement of polymarketActiveUpstream (counter goes
negative, disabling concurrency cap) and double circuit breaker trip.

Add finalized guard so decrement + failure accounting happens exactly
once per request regardless of which error path fires first.

* fix(threat-classifier): stagger AI classification requests to avoid Groq 429 (#520)

flushBatch() fired up to 20 classifyEvent RPCs simultaneously via
Promise.all, instantly hitting Groq's ~30 req/min rate limit.

- Sequential execution with 2s min-gap between requests (~28 req/min)
- waitForGap() enforces hard floor + jitter across batch boundaries
- batchInFlight guard prevents concurrent flush loops
- 429/5xx: requeue failed job (with retry cap) + remaining untouched jobs
- Queue cap at 100 items with warn on overflow

* fix(relay): regenerate package-lock.json with telegram dependency

The lockfile was missing resolved entries for the telegram package,
causing Railway to skip installation despite it being in package.json.

* chore: trigger deploy to flush CDN cache for iran-events endpoint

* Revert "fix(relay): regenerate package-lock.json with telegram dependency"

This reverts commit a8d5e1dbbd.

* fix(relay): add POLYMARKET_ENABLED env flag kill switch (#523)

Set POLYMARKET_ENABLED=false on Railway to disable all Polymarket
upstream requests. Returns 503 immediately, preventing OOM crashes.

* fix(breaking-news): fill keyword gaps missing real Iran attack headlines (#521)

* fix(breaking-news): fill keyword gaps that miss real Iran attack headlines

Three root causes for zero alerts during the Iran war:

1. Keyword gaps — top Iran headlines failed classification:
   - "US and Israel attack Iran" → info (no "attack iran" keyword)
   - "attacked Iran" → info (only "attacks iran" existed, plural)
   - "Explosions heard in Tehran" → info (no "explosions" keyword)
   Added: attack iran, attacked iran, attack on iran, attack against iran,
   bombing/bombed iran, war against iran (CRITICAL); explosions,
   launched/launches attacks, retaliatory/preemptive/preventive attack (HIGH)

2. 5-item RSS limit — Al Jazeera's CRITICAL "major combat operations"
   headline was item #7 and never reached the classifier. Increased
   per-feed limit from 5 to 10.

3. False positive — "OpenAI strikes deal with Pentagon" matched HIGH
   keyword "strikes". Added "strikes deal/agreement/partnership" to
   exclusions.

* fix(threat-classifier): prevent Iran compound keyword false positives

"attack iran" as plain substring matched "Iran-backed" and "Iranian"
in headlines about proxy groups, not direct attacks on Iran.

Added TRAILING_BOUNDARY_KEYWORDS set with negative lookahead (?![\w-])
for all Iran compound keywords. This rejects "Iran-backed militias"
and "Iranian targets" while still matching "attack Iran:" and
"attack Iran" at end of string.

Addresses Codex review comment on PR #521.

* fix(relay): regenerate package-lock.json with telegram dependency (#522)

The lockfile was missing resolved entries for the telegram package,
causing Railway to skip installation despite it being in package.json.

* fix(iran): bypass stale CDN cache for iran-events endpoint (#524)

The CDN cached empty {events:[],scrapedAt:0} from the pre-Redis
deployment and Vercel deploy didn't purge all edge nodes. Add ?_v=2
query param to force cache miss until CDN naturally expires the
stale slow-tier entry.

* fix(focal-points): attribute theater military activity to target nations (#525)

The signal aggregator attributed military flights/vessels to the country
they're physically over (point-in-polygon). Aircraft attacking Iran from
the Persian Gulf got attributed to XX/IQ/SA, not IR — so Iran showed
ELEVATED in Focal Points despite being under active attack (CRIT in
Strategic Posture).

Feed theater-level posture data back into the signal aggregator for
target nations (Iran, Taiwan, North Korea, Gaza, Yemen) so they get
credited for military activity in their theater bounding box. Includes
double-count guard to skip if the nation already has signals.

Also fixes stale "sebuf" comment in threat-classifier.

* fix(relay): block rsshub.app requests with 410 Gone (#526)

Stale clients still send RSS requests to rsshub.app (NHK, MOFCOM, MIIT).
These feeds were migrated to Google News RSS but cached PWA clients keep
hitting the relay, which forwards to rsshub.app and gets 403.

- Add explicit blocklist returning 410 Gone before allowlist check
- Remove rsshub.app from all allowlists (relay, edge proxy, vite)
- Remove dead AP News dev proxy target

* feat(map): prioritize Iran Attacks layer (#527)

* feat(map): move Iran Attacks layer to first position and enable by default

Move iranAttacks to the top of the layer toggle list in the full
(geopolitical) variant so it appears first. Enable it by default on
both desktop and mobile during the active conflict.

* feat(map): add Iran Attacks layer support to SVG/mobile map

- Implement setIranEvents() in SVG Map (was no-op)
- Render severity-colored circle markers matching DeckGL layer
- Add iranAttacks to mobile layer toggles (first position)
- Forward setIranEvents to SVG map in MapContainer
- Add IranEventPopupData to PopupData union for click popups
- Add .iran-event-marker CSS with pulse animation
- Add data-layer-hidden-iranAttacks CSS toggle

* fix(geo): expand geo hub index with 60+ missing world locations (#528)

The geo hub index only had ~30 entries, missing all Gulf states (UAE,
Qatar, Bahrain, Kuwait, Oman), Iraq cities, and many world capitals.
News mentioning Abu Dhabi, Dubai, Baghdad, etc. had no lat/lon assigned
so they never appeared on the map.

Added: Gulf capitals (Abu Dhabi, Dubai, Doha, Manama, Kuwait, Muscat),
Iraq (Baghdad, Erbil, Basra), Jordan, Istanbul, Haifa, Dimona, Isfahan,
Kabul, Mumbai, Shanghai, Hong Kong, Singapore, Manila, Jakarta, Bangkok,
Hanoi, Canberra, all major European capitals (Rome, Madrid, Warsaw,
Bucharest, Helsinki, Stockholm, Oslo, Baltics, Athens, Belgrade, Minsk,
Tbilisi, Chisinau, Yerevan, Baku), Americas (Ottawa, Mexico City,
Brasilia, Buenos Aires, Caracas, Bogota, Havana), Africa (Nairobi,
Pretoria, Lagos, Kinshasa, Mogadishu, Tripoli, Tunis, Algiers, Rabat),
conflict zones (Iraq, Kashmir, Golan), chokepoints (Malacca, Panama,
Gibraltar), and US military bases (Ramstein, Incirlik, Diego Garcia,
Guam, Okinawa).

* fix(iran): bust CDN cache to serve updated Gulf-geocoded events (#532)

CDN edge cache was still serving stale 93-event response without
Gulf state coordinates (UAE, Bahrain, Qatar, Kuwait). Bump cache
key from ?_v=2 to ?_v=3 so browsers fetch fresh 100-event data.
Also gitignore internal/ for private tooling scripts.

* fix(alerts): remove SESSION_START gate that blocked pre-existing breaking news (#533)

The isRecent() function used Math.max(now - 15min, SESSION_START) as
recency cutoff. Since SESSION_START = Date.now() at module load, items
published before page load could never trigger alerts — they failed the
SESSION_START gate in the first 15 min, then aged past the 15-min window.

Now uses only the 15-minute recency window. Spam prevention remains via
per-event dedup (30 min), global cooldown (60s), and source tier filter.

* fix(relay): Telegram + OOM + memory cleanup (#531)

* fix(relay): resolve Telegram missing package, OOM crashes, and memory cleanup

- Add `telegram` and `ws` to root dependencies so Railway's `npm install` installs them
- Log V8 heap limit at startup to confirm NODE_OPTIONS is active
- Make MAX_VESSELS/MAX_VESSEL_HISTORY env-configurable (default 20k, down from 50k)
- Add permanent latch to skip Telegram import retries when package is missing
- Raise memory cleanup threshold from 450MB to 2GB (env-configurable)
- Clear all caches (RSS, Polymarket, WorldBank) during emergency cleanup

* fix(relay): treat blank env vars as unset in safeInt

Number('') === 0 passes isFinite, silently clamping caps to 1000
instead of using the 20000 default. Guard empty/null before parsing.

* fix(live-news): replace 7 stale YouTube fallback video IDs (#535)

Validated all 23 YouTube fallbackVideoIds via oEmbed API and all 9
HLS URLs. Found 5 broken IDs (403 embed-restricted or 404 deleted)
plus 2 previously identified stale IDs:

- Fox News: QaftgYkG-ek → ZvdiJUYGBis
- Sky News Arabia: MN50dHFHMKE → U--OjmpjF5o
- RTVE 24H: 7_srED6k0bE → -7GEFgUKilA
- CNN Brasil: 1kWRw-DA6Ns → 6ZkOlaGfxq4
- C5N: NdQSOItOQ5Y → SF06Qy1Ct6Y
- TBS NEWS DIG: ohI356mwBp8 → Anr15FA9OCI
- TRT World: CV5Fooi8WDI → ABfFhWzWs0s

All 9 HLS URLs validated OK. 16 remaining YouTube IDs validated OK.

* fix(relay): fix telegram ESM import path and broaden latch regex

- `import('telegram/sessions')` fails with "Directory import is not
  supported resolving ES modules" — use explicit `telegram/sessions/index.js`
- Broaden permanent-disable latch to also catch "Directory import" errors

* fix(ui): move download banner to bottom-right (#536)

* fix(alerts): remove SESSION_START gate that blocked pre-existing breaking news

The isRecent() function used Math.max(now - 15min, SESSION_START) as
recency cutoff. Since SESSION_START = Date.now() at module load, items
published before page load could never trigger alerts — they failed the
SESSION_START gate in the first 15 min, then aged past the 15-min window.

Now uses only the 15-minute recency window. Spam prevention remains via
per-event dedup (30 min), global cooldown (60s), and source tier filter.

* fix(ui): move download banner to bottom-right of screen

Repositioned from top-right (overlapping content) to bottom-right.
Dismissal already persists via localStorage. Added TODO for header
download link.

* Revert "fix(relay): fix telegram ESM import path and broaden latch regex"

This reverts commit 1f2f0175ab.

* Revert "Revert "fix(relay): fix telegram ESM import path and broaden latch regex"" (#537)

This reverts commit ad41a2e2d245850fd4b699af2adbe53acca80325.

* feat: add day/night solar terminator overlay to map (#529)

* Trigger redeploy with preview env vars

* Trigger deployment

* chore: trigger redeploy for PR #41

* chore: trigger Vercel redeploy (edge function transient failure)

* chore: retrigger Vercel deploy

* feat: add Nigeria feeds and Greek locale feeds (#271)

- Add 5 Nigeria news sources to Africa section (Premium Times, Vanguard,
  Channels TV, Daily Trust, ThisDay)
- Add 5 Greek feeds with lang: 'el' for locale-aware filtering
  (Kathimerini, Naftemporiki, in.gr, iefimerida, Proto Thema)
- Add source tiers for all new outlets
- Allowlist 8 new domains in RSS proxy

* fix: enforce military bbox filtering and add behavioral cache tests (#284)

* fix: add request coalescing to Redis cache layer

Concurrent cache misses for the same key now share a single upstream
fetch instead of each triggering redundant API calls. This eliminates
duplicate work within Edge Function invocations under burst traffic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: reduce AIS polling frequency from 10s to 30s

Vessel positions do not change meaningfully in 10 seconds at sea.
Reduces Railway relay requests by 66% with negligible UX impact.
Stale threshold bumped to 45s to match the new interval.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: quantize military flights bbox cache keys to 1-degree grid

Precise bounding box coordinates caused near-zero cache hit rate since
every map pan/zoom produced a unique key. Snapping to a 1-degree grid
lets nearby viewports share cache entries, dramatically reducing
redundant OpenSky API calls.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: parallelize ETF chart fetches instead of sequential await loop

The loop awaited each ETF chart fetch individually, blocking on every
Yahoo gate delay. Using Promise.allSettled lets all 10 fetches queue
concurrently through the Yahoo gate, cutting wall time from ~12s to ~6s.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add Redis pipeline batch GET to reduce round-trips

Add getCachedJsonBatch() using the Upstash pipeline API to fetch
multiple keys in a single HTTP call. Refactor aircraft details batch
handler from 20 sequential GETs to 1 pipelined request.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: add structural tests for Redis caching optimizations

18 tests covering: cachedFetchJson request coalescing (in-flight dedup,
cache-before-fetch ordering, cleanup), getCachedJsonBatch pipeline API,
aircraft batch handler pipeline usage, bbox grid quantization (1-degree
step, expanded fetch bbox), and ETF parallel fetch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: enforce military bbox contract and add behavioral cache tests

---------

Co-authored-by: Elias El Khoury <efk@anghami.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add User-Agent and Cloudflare 403 detection to all secret validation probes (#296)

Sidecar validation probes were missing User-Agent headers, causing
Cloudflare-fronted APIs (e.g. Wingbits) to return 403 which was
incorrectly treated as an auth rejection. Added CHROME_UA to all 13
probes and isCloudflare403() helper to soft-pass CDN blocks.

* fix: open external links in system browser on Tauri desktop (#297)

Tauri WKWebView/WebView2 traps target="_blank" navigation, so news
links and other external URLs silently fail to open. Added a global
capture-phase click interceptor that routes cross-origin links through
the existing open_url Tauri command, falling back to window.open.

* fix: add Greek flag mapping to language selector (#307)

* fix: add missing country brief i18n keys and export PDF option (#308)

- Add levels, trends, fallback keys to top-level countryBrief in en/el/th/vi
  locales (fixes raw key display in intelligence brief and header badge)
- Add Export PDF option to country brief dropdown using scoped print dialog
- Add exportPdf i18n key to all 17 locale files

* feat: add day/night solar terminator overlay to map

Add a real-time day/night overlay layer using deck.gl PolygonLayer that
renders the solar terminator (boundary between day and night zones).
The overlay uses astronomical formulas (Meeus) to compute the subsolar
point and trace the terminator line at 1° resolution.

- New toggleable "Day/Night" layer in all 3 variants (full/tech/finance)
- Theme-aware styling (lighter fill on light theme, darker on dark)
- Auto-refresh every 5 minutes with conditional timer (only runs when
  layer is enabled, pauses when render is paused)
- Cached polygon computation to avoid recomputing on every render
- i18n translations for all 17 locales
- Updated documentation with new layer entry

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address review feedback — equinox terminator + locale indentation

- Replace safeTanDecl epsilon clamp with proper equinox handling:
  when |tanDecl| < 1e-6, draw terminator as vertical great circle
  through the poles (subsolar meridian ±90°) instead of clamping
- Fix JSON indentation in all 17 locale files: dayNight and
  tradeRoutes keys were left-aligned instead of matching 8-space
  indentation of surrounding keys

---------

Co-authored-by: Elie Habib <elie.habib@gmail.com>
Co-authored-by: Elias El Khoury <efk@anghami.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* fix(relay): auto-reconnect on Telegram AUTH_KEY_DUPLICATED and fix IranIntl handle (#539)

- On AUTH_KEY_DUPLICATED (406), disconnect client and set to null so
  next poll cycle reconnects fresh — self-heals after competing client dies
- Fix IranIntl → iranintltv (correct Telegram channel handle)

* fix(live-news): add fallback video ID for LiveNOW from FOX channel (#538)

The livenow-fox channel had no fallbackVideoId, relying solely on
YouTube handle lookup which fails intermittently. Added ZvdiJUYGBis
(confirmed live stream) as fallback.

* fix(iran): bump CDN cache-bust to v4 for fresh event data (#544)

100 new events pushed to Redis covering active Iran-Israel-US
conflict theater including Gulf states (UAE, Bahrain, Qatar,
Kuwait, Jordan). Bump ?_v=3 to ?_v=4 to bypass stale CDN.

* fix(telegram): fix ESM import path in session-auth script (#542)

telegram/sessions → telegram/sessions/index.js (same fix as relay)

* fix(telegram): latch AUTH_KEY_DUPLICATED to stop retry spam (#543)

AUTH_KEY_DUPLICATED is permanent — the session string is invalidated
and no amount of retrying will fix it. Previously the relay retried
every 60s, spamming logs. Now it logs a clear error message with
instructions to regenerate the session and stops retrying.

Renamed telegramImportFailed → telegramPermanentlyDisabled to cover
both import failures and auth failures under one latch.

* fix(live-news): fix broken Europe channel handles + add fallback video IDs (#541)

* fix(live-news): fix broken Europe channel handles + add fallback video IDs

- Fix France 24 English handle: @FRANCE24English (404) → @France24_en
- Fix WELT handle: @WELTNachrichtensender (hijacked to "Movie Glow") → @WELTVideoTV
- Add fallbackVideoId for BBC News, France 24 EN, TRT Haber, NTV Turkey,
  CNN TURK, TVP Info, Telewizja Republika (verified via Playwright)
- Update stale fallback IDs for Fox News, RTVE, CNN Brasil, C5N, TBS News,
  Sky News Arabia, TRT World

* fix(live-news): update CBS News fallback video ID

* fix(live-news): update Newsmax fallback video ID

* fix(live-news): add NBC News fallback video ID

* fix(live-news): full channel audit — fix 10 broken handles + update 8 stale fallbacks

Broken handles fixed:
- Bloomberg: @Bloomberg (404) → @markets
- WION: @WIONews (wrong channel "Write It Out") → @WION
- CTI News: @CtiTv (404) → @中天新聞CtiNews
- VTC NOW: @VTCNOW (404) → @VTCNowOfficial
- Record News: @recordnewsoficial (404) → @RecordNews
- T13: @T13 (404) → @Teletrece
- Channels TV: @channelstv (404) → @ChannelsTelevision
- KTN News: @KTNNewsKE (404) → @ktnnews_kenya
- eNCA: @enewschannel (404) → @eNCA
- SABC News: @SABCNews (404) → @SABCDigitalNews

Stale fallback video IDs refreshed:
- Sky News, NASA, CBC News, CNN Brasil, C5N, TBS NEWS DIG,
  Sky News Arabia, TRT World

* feat(oref): add OREF sirens panel with Hebrew-to-English translation (#545)

Add real-time Israel Home Front Command (OREF) siren alerts panel:
- Edge Function proxy at api/oref-alerts.js
- OrefSirensPanel component with live/history views
- oref-alerts service with 10s polling and update callbacks
- Hebrew→English translation via existing translateText() LLM chain
  with 3-layer caching (in-memory Map → server Redis → circuit breaker)
- i18n strings for all 23 locales
- Panel registration, data-loader integration, CSS styles

* fix(relay): use execFileSync for OREF curl to avoid shell injection (#546)

Proxy credentials with special characters (semicolons, dollar signs)
were interpolated into a shell command via execSync. Switch to
execFileSync which passes args directly without shell parsing.

* gave the user freedom to resize panels "fixes issue #426" (#489)

* gave the user freedom to resize panles

* feat(panels): add horizontal resize with col-span persistence

* feat(cii): integrate Iran strike events into CII scoring, country brief & timeline (#547)

Iran had ~100 geolocated strike events but the CII was unaware of them:
conflict score stuck at 70 (ACLED only), no strike chip in Active Signals,
timeline conflict lane empty, intelligence brief silent on strikes.

Changes:
- Add strikes[] to CountryData and ingestStrikesForCII() with geo-lookup
  fallback (bounding boxes when GeoJSON not yet loaded)
- Boost CII conflict score with 7-day freshness window
  (min(50, count*3 + highSev*5))
- Cache iranEvents in IntelligenceCache, preserve across refresh cycles
- Wire data loading: always load Iran events (not gated by map layer),
  ingest into CII, trigger panel refresh
- Add activeStrikes to CountryBriefSignals with geo-lookup counting
- Render strike chip in Active Signals and include in fallback brief
- Feed strike events into 7-day timeline (conflict lane)
- Add structured strikeCount/highSeverityStrikeCount fields to GeoSignal
  (replaces fragile regex parsing in focal-point-detector)
- Add active_strike signal type to InsightsPanel focal points
- Add bounding-box fallback to signal aggregator for conflict events
- Add i18n keys for activeStrikes

* fix(alerts): add compound escalation for military action + geopolitical target (#548)

Keyword matching was too rigid — "attacks on iran" didn't match CRITICAL
because only "attack on iran" (singular) existed. Headlines like
"strikes by US and Israel on Iran" also missed because words weren't
adjacent.

Added compound escalation: if a HIGH military/conflict keyword matches
AND the headline mentions a critical geopolitical target (Iran, Russia,
China, Taiwan, NATO, US forces), escalate to CRITICAL. Also added
missing Iran keyword variants (plural forms, "Iran retaliates/strikes").

* feat(conflict): enhance Iran events popup with severity badge and related events (#549)

Rewrite the Iran events popup to follow the established popup pattern
(conflict/protest) with severity-colored header, badge, close button,
stat rows, and source link using CSS classes instead of inline styles.

- Add normalizeSeverity helper (clamps unknown values to 'low')
- Show related events from same location (normalized matching, max 5)
- Add IranEventPopupData to PopupData union (removes unsafe double cast)
- Add iranEvent header CSS with severity border-left colors
- Add i18n keys for en/ar/fr

* feat(telegram): add Telegram Intel panel (#550)

* feat(telegram): add Telegram Intel panel consuming relay feed

- Service layer: fetchTelegramFeed() with 30s cache, types matching relay shape
- Panel component: topic filter tabs, safe DOM rendering via h()+replaceChildren()
- DataLoader + RefreshScheduler pattern (60s interval, hidden-tab aware)
- Handles enabled=false and empty states from relay
- CSS following existing gdelt-intel pattern
- Panel title localized across all 18 locales

* fix(i18n): add components.telegramIntel translations to 10 remaining locales

* feat(live-news): add residential proxy + gzip decompression for YouTube detection (#551)

YouTube blocks Vercel datacenter IPs — returns HTML without videoDetails/isLive
data. Switch from edge runtime to Node.js serverless to enable HTTP CONNECT
tunnel proxy via YOUTUBE_PROXY_URL env var. Add zlib decompression for gzip
responses (YouTube returns empty body without Accept-Encoding header).

Also adds missing fallback video IDs for WELT, KTN News, CNA NewsAsia,
and updates TBS NEWS DIG fallback.

* debug(live-news): add debug param to diagnose proxy env var on Vercel

* fix(live-news): set explicit runtime: 'nodejs' for proxy support

Vercel defaults to edge runtime when not specified. node:http/https/zlib
imports are unavailable in edge — causing FUNCTION_INVOCATION_FAILED.
Remove debug param added in previous commit.

* fix(live-news): lazy-load node modules + proxy fallback to direct fetch

Top-level import of node:http/https/zlib crashes if Vercel bundles
for edge despite runtime: 'nodejs' config. Use dynamic import() to
lazy-load at call time. Also add try/catch around proxy so it falls
back to direct fetch if proxy connection fails.

* feat(aviation): integrate AviationStack API for non-US airport delays (#552)

Replace 100% simulated delay data for international airports with real
flight data from AviationStack API. Add 28 Middle East/conflict-zone
airports (Iran, Iraq, Lebanon, Syria, Yemen, Pakistan, Libya, Sudan).

Key changes:
- AviationStack integration with bounded concurrency (5 parallel),
  rotating batch (20 airports/cycle), and 20s deadline
- Redis SETNX lock prevents cross-isolate cache stampede on expiry
- Split FAA/intl caches (both 30min TTL) with isolated error handling
- Fix severity colors (was checking 'GS'/'GDP', now minor/moderate/major/severe)
- Fix tooltip (was obj.airport, now obj.name + obj.iata)
- Add FLIGHT_DELAY_TYPE_CLOSURE for airport/airspace closures
- Add closure i18n key across all 18 locales
- Graceful fallback: no API key → simulation; API failure → simulation

* feat(live-news): move YouTube proxy scraping to Railway relay

Vercel serverless cannot use node:http/https for HTTP CONNECT proxy
tunnels. Move the residential proxy YouTube scraping to the Railway
relay (ais-relay.cjs) which has full Node.js access.

- Add /youtube-live route to relay with proxy + direct fetch fallback
- Add 5-min in-memory cache for channel lookups, 1hr for oembed
- Revert Vercel api/youtube/live.js to edge runtime — now proxies to
  Railway first, falls back to direct scrape

* feat(settings): add AVIATIONSTACK_API to desktop settings page (#553)

Register the key in Rust keychain (SUPPORTED_SECRET_KEYS), frontend
RuntimeSecretKey type, feature toggle, and settings UI under
"Tracking & Sensing" category.

* fix(live-news): use correct relay auth header for YouTube proxy (#554)

Edge function was sending X-Relay-Auth header with RELAY_AUTH_TOKEN env
var, but the Railway relay expects x-relay-key header validated against
RELAY_SHARED_SECRET. This mismatch caused the relay to reject requests
from Vercel, falling back to direct YouTube scrape (which fails from
datacenter IPs for many channels).

* fix(live-news): align YouTube edge function with relay auth pattern (#555)

Use same getRelayBaseUrl/getRelayHeaders as other edge functions:
- WS_RELAY_URL env var instead of VITE_WS_API_URL
- RELAY_SHARED_SECRET + RELAY_AUTH_HEADER for flexible auth
- Dual x-relay-key + Authorization headers

* fix(i18n): rename OREF Sirens panel to Israel Sirens (#556)

Remove internal implementation references (OREF, proxy relay, oref.org.il)
from all user-facing strings across 18 locales and panel config.

* fix(live-news): annotate empty catches and sanitize error output (#560)

- Add context comments to empty catch blocks for debuggability
- Replace error.message leak with generic client-safe message

* fix(sentry): add noise filters and fix beforeSend null-filename leak (#561)

- Add 8 new ignoreErrors patterns: signal timeout, premium gate,
  hybridExecute/mag/webkit bridge injections, postMessage null,
  NotSupportedError, appendChild injection, luma assignment
- Fix LIDNotify regex to match both LIDNotify and LIDNotifyId
- Fix beforeSend: strip null/anonymous filename frames so deck.gl
  TypeErrors (28 events, 8 users) are properly suppressed

* feat(cii): wire OREF sirens into CII score & country brief (#559)

* feat(cii): wire OREF sirens into CII score and country brief

Active OREF sirens now boost Israel's CII score through two channels:
- Conflict component: +25 base + min(25, alertCount*5) for active sirens
- Blended score: +15 for active sirens, +5/+10 for 24h history thresholds

Country brief for Israel shows a siren signal chip when alerts are active.

* refactor(cii): extract getOrefBlendBoost helper to DRY scoring paths

* fix(relay): add graceful shutdown + poll concurrency guard for Telegram (#562)

- SIGTERM/SIGINT handler disconnects Telegram client before container dies
- telegramPollInFlight guard prevents overlapping poll cycles
- Mid-poll AUTH_KEY_DUPLICATED now permanently disables (was reconnect loop)

* fix(aviation): query all airports instead of rotating batch (#557)

* fix(aviation): query all airports instead of rotating batch of 20

The rotating batch (20 airports/cycle) caused major airports like DXB
(52% cancellations) to be missed entirely for multiple cache cycles.
With a paid AviationStack plan, query all ~90 non-US airports per
refresh with concurrency 10 and 50s deadline (~9 chunks × 5s = 45s).

* feat(cii): feed airport disruptions into CII and country brief

Major/severe airport delays and closures now boost the CII security
score and appear as signal chips in country briefs. Only major+
severity alerts are ingested to avoid noise from minor delays.

- Add aviationDisruptions to CountryData and ingestAviationForCII()
- Boost security score: closure +20, severe +15, major +10, moderate +5
- Store flight delays in intelligenceCache for country brief access
- Add aviation disruptions chip in country brief signals grid

* fix(relay): replace smart quotes crashing relay on startup (#563)

* fix(relay): replace Unicode smart quotes crashing Node.js CJS parser

* fix(relay): await Telegram disconnect + guard startup poll

* fix(cii): resolve Gulf country strike misattribution via multi-match bbox disambiguation (#564)

Dubai/Doha/Bahrain/Kuwait coordinates matched Iran's bounding box first
due to iteration order. Now collects ALL matching bboxes, disambiguates
via isCoordinateInCountry() geometry, and falls back to smallest-area bbox.

- Add BH, QA, KW, JO, OM to bounds tables (previously missing entirely)
- Extract ME_STRIKE_BOUNDS + resolveCountryFromBounds() into country-geometry.ts
- All 4 consumer files use shared constant (single source of truth)
- Bump CDN cache-bust param for iran-events endpoint

* fix(relay): upstreamWs → upstreamSocket in graceful shutdown (#565)

* fix(relay): install curl in Railway container for OREF polling (#567)

* fix(relay): increase Polymarket cache TTL to 10 minutes (#568)

* fix(relay): increase Polymarket cache TTL to 10 minutes

All requests were MISS with 2-min TTL under concurrent load.
Bump to 10-min cache and 5-min negative cache to reduce upstream pressure.

* fix(relay): normalize Polymarket cache key from canonical params

Raw url.search as cache key meant ?tag=fed&endpoint=events and
?endpoint=events&tag_slug=fed produced different keys for the same
upstream request — defeating both cache and inflight dedup, causing
121 MISS entries in 3 seconds.

Build cache key from parsed canonical params (endpoint + sorted
query string) so all equivalent requests share one cache entry.

* feat(webcams): add Iran tab to live webcams panel (#569)

Add dedicated Iran region tab as the first/default tab with 4 feeds:
Tehran, Middle East overview, Tehran (alt angle), and Jerusalem.

* fix(relay): replace nixpacks.toml with railpack.json for curl (#571)

Railway uses Railpack (not Nixpacks). nixpacks.toml in scripts/ was
silently skipped. Use railpack.json at repo root with deploy.aptPackages
to install curl at runtime for OREF polling.

* fix(webcams): replace duplicate Tehran feed with Tel Aviv, rename Iran tab (#572)

- Remove duplicate iran-tehran2 feed (same channel/video as iran-tehran)
- Remove iran-mideast feed
- Add Tel Aviv feed (-VLcYT5QBrY) to Iran Attacks tab
- Rename tab label from "IRAN" to "IRAN ATTACKS" across all 18 locales

* feat(scripts): add Iran events seed script and latest data (#575)

Add seed-iran-events.mjs for importing Iran conflict events into Redis
(conflict:iran-events:v1). Includes geocoding by location keywords and
category-to-severity mapping. Data file contains 100 events from
2026-02-28.

* fix(relay): add timeouts and logging to Telegram poll loop (#578)

GramJS getEntity/getMessages have no built-in timeout. When the first
channel hangs (FLOOD_WAIT, MTProto stall), telegramPollInFlight stays
true forever, blocking all future polls — zero messages collected, zero
errors logged, frontend shows "No messages available".

- Add 15s per-channel timeout on getEntity + getMessages calls
- Add 3-min overall poll cycle timeout
- Force-clear stuck in-flight flag after 3.5 minutes
- Detect FLOOD_WAIT errors and break loop early
- Log per-cycle summary: channels polled, new msgs, errors, duration
- Track media-only messages separately (no text → not a bug)
- Expose lastError, pollInFlight, pollInFlightSince on /status endpoint

* feat(cii): hook security advisories into CII scoring & country briefs (#579)

Travel advisories (Do Not Travel, Reconsider, Caution) from US, AU, UK,
NZ now act as a floor and boost on CII scores. Do Not Travel guarantees
a minimum score of 60 (elevated), Reconsider floors at 50. Multi-source
corroboration (3+ govts) adds +5 bonus.

Advisory chips appear in country brief signal grid with level-appropriate
styling, and advisory context is passed to AI brief generation.

- Extract target country from advisory titles via embassy feed tags and
  country name matching
- Add advisoryMaxLevel/advisoryCount/advisorySources to CII CountryData
- Wire ingestAdvisoriesForCII into data loader pipeline
- Add travelAdvisories/travelAdvisoryMaxLevel to CountryBriefSignals
- Render advisory signal chips in CountryBriefPage

* fix(sentry): guard setView against invalid preset + filter translateNotifyError (#580)

- DeckGLMap.setView(): early-return if VIEW_PRESETS[view] is undefined,
  preventing TypeError on 'longitude' when select value is invalid
- Add ignoreErrors pattern for Google Translate widget crash

* feat(relay): bootstrap OREF 24h history on startup (#582)

* fix(relay): improve OREF curl error logging with stderr capture

-s flag silenced curl errors. Add -S to show errors, capture stderr
via stdio pipes, and log curl's actual error message instead of
generic "Command failed" from execFileSync.

* feat(relay): bootstrap OREF 24h history on startup and add missing headers

- Fetch AlertsHistory.json once on startup to populate orefState.history
  immediately instead of starting empty
- Add X-Requested-With: XMLHttpRequest header required by Akamai WAF
- Add IST→UTC date converter handling DST ambiguity
- Redact proxy credentials from error logs and client responses
- Fix historyCount24h to count individual alert records, not snapshots
- Guard historyCount24h reducer for both array and string data shapes
- Add input validation to orefDateToUTC for malformed dates

* feat(aviation): add comprehensive logging to flight delay pipeline (#581)

Adds structured [Aviation] logging throughout the handler chain:
- Handler: timing, FAA/intl alert counts
- fetchIntlWithLock: cache hit/miss, lock status, fallback triggers
- fetchAviationStackDelays: airport count, deadline hits, success/fail stats
- fetchSingleAirport: per-airport flight stats, API errors, severity
- Annotate empty catches with error context

* feat: add GPS/GNSS jamming map layer + CII integration (#570)

* feat: add GPS/GNSS jamming data ingestion from gpsjam.org

- scripts/fetch-gpsjam.mjs: standalone fetcher that downloads daily H3
  hex data, filters medium/high interference, converts to lat/lon via
  h3-js, and writes JSON. Can be run on cron.
- api/gpsjam.js: Vercel Edge Function that proxies gpsjam.org data with
  1hr cache, returns medium/high hexes for frontend consumption.
- src/services/gps-interference.ts: frontend service that fetches from
  the Edge API, converts H3→lat/lon, and classifies by conflict region.
- h3-js added as dependency for hex→coordinate conversion.

* feat: add GPS jamming map layer, CII integration, and country brief signals

Wire gpsjam.org data into map visualization, instability scoring, and
country intelligence. ScatterplotLayer renders high (red) and medium
(orange) interference hexes. CII security score incorporates jamming
counts per country via h3→country geocoding with cache. Country briefs
show jamming zone chip. Full i18n across 18 locales including popup
labels. Data loads with intelligence signals cycle (15min), gated by
1hr client-side cache.

* feat(aviation): add NOTAM closure detection via ICAO API (#583)

* feat(aviation): add NOTAM closure detection via ICAO API

Adds international airport closure detection via ICAO NOTAMs:
- New fetchNotamClosures() queries ICAO realtime-notams endpoint
- Detects closures via Q-codes (FA/AH/AL/AW/AC/AM) and text patterns
- Batches airports in groups of 20 per API call
- 4-hour cache TTL via cachedFetchJson (stampede-safe)
- NOTAM closures override existing AviationStack alerts for same airport
- Graceful: no ICAO_API_KEY env var = silently skipped

To activate: set ICAO_API_KEY env var (register at dataservices.icao.int)

* feat(settings): add ICAO_API_KEY to desktop app settings

Adds ICAO NOTAM API key to the desktop settings UI:
- Rust: SUPPORTED_SECRET_KEYS [23→24]
- TypeScript: RuntimeSecretKey + RuntimeFeatureId unions
- Feature definition: 'icaoNotams' in Tracking & Sensing category
- Settings UI: label, signup URL, analytics name

* feat(aviation): limit NOTAM queries to MENA airports only

Per user request, ICAO NOTAM closure detection is scoped to
Middle East airports only (region='mena', ~35 airports).
This reduces API calls (2 batches vs 5) and focuses on the
region where closures are most relevant.

* fix(aviation): align NOTAM cache TTL to 30 min (matching FAA/intl)

* feat(risk): wire theater posture + breaking news into strategic risk score (#584)

The composite score ignored theater military buildup and breaking news
alerts, causing misleadingly low scores during active military events.

- Add theater boost from raw asset counts + strike capability (capped +25)
- Add breaking news severity boost (critical=15, high=8, capped +15)
- Listen for wm:breaking-news events with 30-min TTL and auto-expiry
- Read cached theater postures via getCachedPosture() with stale discount
- Lower trend threshold from ±5 to ±3 for faster escalation detection
- Cleanup listeners and timers in destroy()

* fix(relay): delay Telegram connect 60s on startup to prevent AUTH_KEY_DUPLICATED (#587)

Railway zero-downtime deploys start the new container before the old one
receives SIGTERM. Both containers connect with the same session string
simultaneously, triggering Telegram's AUTH_KEY_DUPLICATED which permanently
invalidates the session. A 60s startup delay gives the old container time
to disconnect gracefully. Configurable via TELEGRAM_STARTUP_DELAY_MS env.

* feat(feeds): add RT (Russia Today) RSS feeds (#585)

- Add RT main feed (rt.com/rss/) and RT Russia desk (rt.com/rss/russia/)
- Add to SOURCE_TIERS (tier 3), SOURCE_TYPES (wire), SOURCE_CREDIBILITY
- Add rt.com to rss-proxy ALLOWED_DOMAINS

* feat(live-news): add RT channel via HLS + enable HLS on web (#586)

- Add RT (rt.com) as optional channel in Europe region
- HLS stream: rt-glb.rttv.com/dvr/rtnews/playlist.m3u8 (CORS: *)
- Enable native <video> HLS playback on web (was desktop-only)
- Channels in DIRECT_HLS_MAP with CORS headers now work everywhere

* fix(telegram): add missing relay auth headers to telegram-feed edge function (#590)

The edge function was the only relay proxy missing RELAY_SHARED_SECRET
auth headers. The relay returns 401 for all non-public routes, so the
panel always received an error response → "No messages available".

* fix(aviation): replace broken lock mechanism with direct cache, add cancellation tiers (#591)

The Redis SETNX lock for AviationStack had a 3s wait but the API takes
~25s for 50 airports. Every lock-loser fell back to simulation — meaning
AviationStack data was never served despite the API key being configured.

Changes:
- Remove fetchIntlWithLock/tryAcquireLock, use getCachedJson/setCachedJson
  with conditional TTL (30min real data, 5min simulation fallback)
- Add cancellation severity tiers: ≥20% moderate, ≥10% minor (DXB at 32%
  cancelled was previously dropped as null)
- Bump cache key to v2 to invalidate stale simulation data
- Add HTML content-type detection for NOTAM API (ICAO CloudFront returns
  HTML challenge pages from Vercel datacenter IPs)

* fix(relay): stop Polymarket cache stampede from concurrent limit + CDN bypass (#592)

Three issues caused continuous MISS every 5 seconds:

1. Concurrent limit rejection poisoned cache: 11 tags fire via Promise.all
   but POLYMARKET_MAX_CONCURRENT=3, so 8 tags got negative-cached with
   empty [] (5 min TTL). Those 8 tags NEVER got positive cache because
   they were always throttled. Fix: replace reject-with-negative-cache
   with a proper queue — excess requests wait for a slot instead of
   being silently rejected.

2. Cache key fragmentation: fetchPredictions(limit=20) and
   fetchCountryMarkets(limit=30) created separate cache entries for the
   same tag. Fix: normalize to canonical limit=50 upstream, cache key
   is shared regardless of caller's requested limit.

3. CDN bypass: end_date_min timestamp in query string made every URL
   unique, preventing Vercel CDN caching entirely. Fix: strip
   end_date_min, active, archived from proxy params (relay ignores them
   anyway).

* fix(polymarket): add queue backpressure and response limit slicing (#593)

- Add POLYMARKET_MAX_QUEUED=20 cap to prevent unbounded queue growth
  under sustained load (rejects with negative cache when full)
- Use requestedLimit to slice cached responses — callers requesting
  limit=20 now get 20 items instead of the full 50-item upstream payload
- Hoist PROXY_STRIP_KEYS Set to module level (avoids per-call allocation)

* fix(webcams): add 4th Iran Attacks feed to fill 2x2 grid (#601)

Add Middle East multi-cam (4E-iFtUM2kk) as 4th Iran region feed.
Previously only 3 feeds for a 4-cell grid, leaving one cell black.

* fix(aviation): route NOTAM through relay + improve intl logging (#599)

Root cause: ICAO NOTAM API times out from Vercel edge (>10s).
AviationStack alerts indistinguishable from simulation in logs.

Changes:
- Add /notam proxy endpoint to Railway relay (25s timeout, 30min cache)
- Route fetchNotamClosures through relay when WS_RELAY_URL is set
- Fall back to direct ICAO call (20s timeout) when no relay
- Log cache hits with real vs simulated alert counts
- Send all MENA airports in single NOTAM request (was batched by 20)

Requires: ICAO_API_KEY env var on Railway relay

* chore(telegram): update channel list — remove nexta_live, air_alert_ua; add wfwitness (#600)

* fix(sentry): guard YT player methods + filter GM/InvalidState noise (#602)

- Guard loadVideoById/cueVideoById with typeof check in LiveNewsPanel
  (race condition: YT.Player object exists before onReady fires)
- Add ignoreErrors for GM_getValue (Greasemonkey extension) and
  InvalidStateError (IndexedDB/DOM state errors from browser internals)

* fix(aviation): always show all monitored airports on flight delays map (#603)

* fix(aviation): always show MENA airports on map regardless of delay status

MENA airports only appeared when they had active delays/closures.
GCC airports like DOH, AUH, RUH were invisible during normal operations.
Now fills in "normal operations" entries for all MENA airports without
alerts so they always render as gray dots on the flight delays layer.

* fix(aviation): show all monitored airports globally, not just MENA

Extend normal-operations fill to all 128 monitored airports worldwide,
not just the 35 MENA airports. Any airport without active delays now
appears as a gray dot on the flight delays map layer.

* fix(webcams): fix broken live news channels — eNCA handle, remove VTC NOW, fix CTI News (#604)

- eNCA: fix YouTube handle from @eNCA to @encanews
- VTC NOW: remove — VTC shut down Jan 2025 (Vietnam govt restructuring)
- CTI News: remove stale fallbackVideoId and useFallbackOnly, let auto-detect work

* docs(readme): comprehensive update for Telegram, OREF, GPS jamming, airports, and more (#606)

Reflects 30+ recent commits with updates across the README:
- Update data layer counts (36 → 40+), webcam counts (19 → 22), data sources (16 → 28+)
- Add 6 new How It Works sections: Telegram OSINT, OREF Rocket Alerts, GPS/GNSS
  Interference, Security Advisories, Airport Delays/NOTAMs, Strategic Risk Score
- Expand Railway relay section with service table (AIS, OpenSky, Telegram, OREF,
  Polymarket, NOTAM) and update architecture ASCII diagram
- Add Iran/Attacks webcam tab, HLS native streaming details, RT coverage
- Add Algorithmic Design Decisions section (log vs linear scoring, Welford's,
  H3 hex grids, cosine-lat correction, negative caching)
- Add 2 new architecture principles (graceful degradation, multi-source corroboration)
- Add 8 new roadmap items for recently shipped features
- Update Tech Stack table with Telegram, OREF, gpsjam.org in Geopolitical APIs

* chore: bump version to 2.5.21 (#605)

* fix(aviation): invalidate stale IndexedDB cache + reduce CDN TTL (#607)

Bump circuit breaker name from 'FAA Flight Delays' to 'Flight Delays v2'
to force all clients to discard stale IndexedDB entries that predate
PR #603 (which added normal-ops airport fill). Also downgrade CDN cache
tier from 'slow' (15 min) to 'medium' (5 min) since airport status
changes more frequently than other slow-tier endpoints.

* fix(relay): increase OREF curl maxBuffer to prevent ENOBUFS (#609)

* fix(relay): increase OREF curl maxBuffer to 10MB to prevent ENOBUFS

AlertsHistory.json response exceeds execFileSync default 1MB buffer,
causing spawnSync ENOBUFS on Railway container at startup.

* fix(relay): use curl -o tmpfile for OREF history instead of stdout buffer

Large AlertsHistory.json overflows execFileSync stdout buffer (ENOBUFS).
Now writes to temp file via curl -o, reads with fs.readFileSync, cleans up.
Live alerts (tiny payload) still use stdout path.

* fix: RT channel HLS-only recovery, test shim, and LiveNOW fallback (#610)

- Remove useFallbackOnly from RT channel — RT is HLS-only (banned from
  YouTube), so the flag was causing undefined videoId on HLS failure
  instead of graceful offline state
- Add response-headers shim to redis-caching test so military flights
  bbox tests can import list-military-flights.ts
- Restore LiveNOW from FOX fallbackVideoId (removed in channel audit)

* fix(docs): add blank lines after CHANGELOG headings for markdownlint (#608)

* Expand country brief and CII signal coverage (#611)

* perf(rss): raise news refresh interval to 10min and cache TTL to 20min (#612)

Reduces upstream RSS polling frequency and extends client-side cache
lifetime to lower API load and bandwidth usage.

* fix(api): harden cache-control headers for polymarket and rss-proxy (#613)

* docs(changelog): add v2.5.21 entry covering 86 merged PRs (#616)

Comprehensive changelog for 2026-03-01 release including Iran Attacks
layer, Telegram Intel panel, OREF sirens, GPS jamming, AviationStack,
breaking news alerts, and strategic risk score.

* fix(aviation): increase cache TTL from 30min to 2h to reduce API quota usage (#617)

* feat(oref): show history waves timeline with translation and NaN fix (#618)

- Fetch and display alert history waves in OrefSirensPanel (cap 50 most recent)
- Last-hour waves highlighted with amber border and RECENT badge
- Translate Hebrew history alerts via existing translateAlerts pipeline
- Guard formatAlertTime/formatWaveTime against NaN from unparseable OREF dates
- Cap relay history bootstrap to 500 records
- Add 3-minute TTL to prevent re-fetching history on every 10s poll
- Remove dead .oref-footer/.oref-history CSS; add i18n key for history summary

* feat(map): native mobile map experience with location detection and full feature parity (#619)

- Fix URL restore: lat/lon now override view center when explicitly provided
- Fix touch scroll: 8px threshold before drag activation, preventDefault once active
- Add location bootstrap: timezone-first detection, optional geolocation upgrade
- Enable DeckGL on mobile with deviceMemory capability guard
- Add DeckGL state sync on moveend/zoomend for URL param updates
- Fix breakpoint off-by-one: JS now uses <= to match CSS max-width: 768px
- Add country-click on SVG fallback with CSS transform inversion
- Add fitCountry() to both map engines (DeckGL uses fitBounds, SVG uses projection)
- Add SVG inertial touch animation with exponential velocity decay
- Add mobile map e2e tests for timezone, URL restore, touch, and breakpoint

* fix(sentry): guard pauseVideo optional chaining + add 4 noise filters (#624)

- Fix: `this.player.pauseVideo()` → `this.player.pauseVideo?.()` at
  LiveNewsPanel line 1301 (6 Sentry issues, 33 events)
- Noise: Chrome extension "Could not establish connection"
- Noise: Safari "webkitCurrentPlaybackTargetIsWireless" internal
- Noise: Sentry SDK crash on iOS "a.theme"
- Noise: Broaden Can't find variable to include EmptyRanges
- Noise: Catch stale cached builds with /this.player.\w+ is not a function/

* fix(aviation): prevent AviationStack API quota blowout (#623)

- Replace manual getCachedJson/setCachedJson with cachedFetchJson for
  intl delays — prevents thundering herd (concurrent misses each firing
  93 API calls independently)
- Bump Redis cache TTL from 30min to 2h
- Bump frontend polling from 10min to 2h to match server cache
- Bump circuit breaker browser cache from 5min to 2h
- Bump CDN edge tier from medium (5min) to static (1h)
- Bump cache key to v3 to force fresh entry with new TTL

* feat(news): server-side feed aggregation to reduce edge invocations by ~95% (#622)

Phase 1: Force CDN caching on rss-proxy (s-maxage=300 for 2xx, short
TTL for errors) — fixes bug where upstream no-cache headers were passed
through verbatim, defeating Vercel CDN.

Phase 2: Add ListFeedDigest RPC that aggregates all feeds server-side
into a single Redis-cached response. Client makes 1 request instead of
~90 per cycle. Includes circuit breaker with persistent cache fallback,
per-feed AI reclassification, and headline ingestion parity.

Phase 3: Increase polling interval from 5min to 7min to offset CDN
cache alignment.

New files:
- proto/worldmonitor/news/v1/list_feed_digest.proto
- server/worldmonitor/news/v1/{_feeds,_classifier,list-feed-digest}.ts
- src/services/ai-classify-queue.ts (extracted from rss.ts)

* feat(rss): add conditional GET (ETag/If-Modified-Since) to Railway relay (#625)

When RSS cache expires, send If-None-Match/If-Modified-Since headers
on revalidation. Upstream 304 responses refresh the cache timestamp
and serve cached body with zero bandwidth, cutting egress ~80-95%
for feeds that support conditional GET.

* fix(map): sync layer toggles to URL for shareable links (#576) (#621)

Layer toggles were not updating the browser URL, so shared links
would not carry the user's current layer state. Extracted the
debounced URL sync to a reusable class method and call it on
every layer change.

* chore(cache): bump all sub-5min cache TTLs and polling intervals (#626)

Audit and raise every cache/poll under 5 min to reduce upstream API
pressure and Vercel function invocations across 31 files:

Frontend polling: markets 4→8min, crypto 4→8min, predictions 5→10min,
natural 5→1hr, cableHealth 5→2hr, oref 10s→2min, maritime 30s→5min,
IntelligenceGapBadge 1→3min.

Circuit breakers: PizzINT 2→30min, aviation 5→20min, seismology/weather/
outages/statuses/wildfires 5→30min, military-vessels/flights/GDACS/
maritime/polymarket/GDELT 5→10min, chokepoint 5→20min.

Server Redis: market-quotes 2→8min, stablecoins 2→8min/5→10min,
vessel-snapshot 10s→5min, earthquakes 5→30min, sector 5→10min,
predictions/crypto/commodities 5→10min, outages/statuses 5→30min,
macro-signals/chokepoints 5→15min, aviation-sim 5→15min.

CDN edge: market RPCs fast→medium, infra/seismology fast→slow.

* fix(military): narrow ICAO hex ranges to stop civilian false positives (#627)

* fix(military): narrow hex ranges and callsign regex to stop civilian false positives (#462)

MILITARY_HEX_RANGES used entire country ICAO allocations instead of
military-specific sub-ranges (sourced from tar1090-db/ranges.json).
This flagged ALL commercial aircraft from Italy, Spain, Japan, India,
South Korea, etc. as military activity.

Key changes:
- Remove A00000-A3FFFF (US civilian N-numbers) — military starts at ADF7C8
- Italy 300000-33FFFF → 33FF00-33FFFF (top 256 codes only)
- Spain 340000-37FFFF → 350000-37FFFF (upper 3/4 confirmed military)
- Japan 840000-87FFFF removed (no confirmed JASDF sub-range)
- France narrowed to 3AA000-3AFFFF + 3B7000-3BFFFF
- Germany narrowed to 3EA000-3EBFFF + 3F4000-3FBFFF
- India 800000-83FFFF → 800200-8002FF (256 codes)
- Canada C00000-C0FFFF → C20000-C3FFFF (upper half)
- Remove unconfirmed: South Korea, Sweden, Singapore, Pakistan
- Add confirmed: Austria, Belgium, Switzerland, Brazil
- Drop overly broad /^[A-Z]{4,}\d{1,3}$/ callsign regex from server

* fix(military): constrain short prefixes + add classification tests

Move ambiguous 2-letter prefixes (AE, RF, TF, PAT, SAM, OPS, CTF,
IRG, TAF) to SHORT_MILITARY_PREFIXES — these now only match when
followed by a digit (e.g. AE1234=military, AEE123=Aegean Airlines).

Add 97-case test suite covering:
- Military callsign detection (19 known patterns)
- Short prefix digit gating (6 cases)
- Civilian airline non-detection (26 airlines)
- Short prefix letter rejection (6 cases)
- Military hex range boundaries (7 confirmed ranges)
- Civilian hex non-detection (19 codes)
- Boundary precision (ADF7C8 start, 33FF00 start, etc.)
- No-full-allocation guard (10 countries)

* fix: use charAt() instead of bracket indexing for strict TS

* fix(sidecar): add AVIATIONSTACK_API and ICAO_API_KEY to env allowlist (#632)

Both keys were added to Rust SUPPORTED_SECRET_KEYS and runtime-config.ts
but the sidecar's own ALLOWED_ENV_KEYS was never updated. This caused
"key not in allowlist" 403 when saving/verifying these keys from the
desktop settings UI.

Also adds AviationStack API validation in validateSecretAgainstProvider.

* fix(linux): sanitize env for xdg-open in AppImage (#631)

* fix(desktop): backoff on errors to stop CPU abuse + shrink settings window (#633)

Three bugs combine to burn 130% CPU when sidecar auth fails:

1. RefreshScheduler resets backoff multiplier to 1 (fastest) on error,
   causing failed endpoints to poll at base interval instead of backing off.
   Fix: exponential backoff on errors, same as unchanged-data path.

2. classify-event batch system ignores 401 (auth failure) — only pauses
   on 429/5xx. Hundreds of classify calls fire every 2s, each wasted.
   Fix: pause 120s on 401, matching the 429/5xx pattern.

3. Fetch patch retries every 401 (refresh token + retry), doubling all
   requests to the sidecar even when token refresh consistently fails.
   Fix: 60s cooldown after a retry-401 still returns 401.

Also shrinks settings window from 760→600px (min 620→480) to reduce
the empty whitespace below content on all tabs.

* feat: implement deduct situation feature (#636)

* feat: Add new intelligence panels for situation deduction, strategic posture, and tech events with associated backend services and E2E tests.

* fix(deduction): desktop-only gating, XSS fix, and code review fixes

- Gate DeductionPanel creation behind isDesktopApp check
- Gate deduce buttons in StrategicPosturePanel and TechEventsPanel behind isDesktopRuntime()
- Remove deduction from FULL_PANELS (runtime-only on desktop)
- Add DOMPurify to sanitize marked.parse() output before innerHTML
- Revert shared _shared.ts: restore UPSTREAM_TIMEOUT_MS=30_000, hardcoded GROQ_API_URL/MODEL
- Define local DEDUCT_TIMEOUT_MS=120_000, DEDUCT_API_URL, DEDUCT_MODEL in handler
- Fix dead API key guard: remove 'ollama-local' fallback, skip if no key
- Strip all console.log from deduct-situation handler (keep console.error)
- Bump max_tokens 500→1500 for meaningful analysis
- Fix cache key: deduct:sebuf→deduct:situation
- Add 5s client-side rate limit cooldown on submit button
- Move @types/marked to devDependencies, add dompurify + @types/dompurify
- Add DeductContextDetail shared interface in types/index.ts
- Extract buildNewsContext helper to src/utils/news-context.ts
- Pass getLatestNews to StrategicPosturePanel constructor
- Fix E2E test: .deduction-markdown-content→.deduction-result
- Revert emoji escapes in commands.ts to unicode format
- Revert variant-switcher whitespace in panel-layout.ts

* fix: resolve IranEvent timestamp type mismatches (string vs number)

Generated IranEvent.timestamp is string but consumers expected number.
- Coerce timestamps via Number() in data-loader before passing to aggregators
- Accept string|number in MapPopup IranEventPopupData interface
- Fix scrapedAt fallback type in conflict/index.ts
- Coerce timestamp in country-intel.ts and DeckGLMap.ts sort

* fix: restore generated code from main, keep intelligence deductSituation RPC

Reverts all src/generated/ files to origin/main except the intelligence
service client/server which retains the new deductSituation RPC added by
this PR. Fixes scrapedAt type to match generated number type.

* fix: add missing listGulfQuotes handler from main

The Gulf Economies panel (PR #667) added this RPC after the feature
branch was forked. Bring handler and registration from main.

* fix: address code review findings (P1+P2+P3)

- P1: Fix event listener leak — store handler ref, remove in destroy()
- P1: Prevent cooldown bypass via requestSubmit when button disabled
- P2: Read process.env inside function body for runtime secret updates
- P3: Use \u{1F9E0} unicode escape in StrategicPosturePanel (consistency)

---------

Co-authored-by: Elie Habib <elie.habib@gmail.com>
Co-authored-by: Sebastien Melki <sebastien@anghami.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Konstanty Szumigaj <kostekszumigaj@gmail.com>
Co-authored-by: Chris Chen <fuleinist@users.noreply.github.com>
Co-authored-by: facusturla <facusturla@users.noreply.github.com>
Co-authored-by: karim <mirakijka@gmail.com>
Co-authored-by: maxime.io <maxime.de.visscher@gmail.com>
Co-authored-by: Elias El Khoury <efk@anghami.com>
Co-authored-by: Fayez Bast <96446332+FayezBast@users.noreply.github.com>
2026-03-01 20:49:55 +04:00
Elie Habib
45b00e9d7d feat(settings): badge pulse animation with settings toggle (#676)
* feat: animate panel count badge on new data arrival

* feat(settings): gate badge pulse animation behind toggle (off by default)

PR #671 adds CSS pulse on panel count badges. This commit gates it
behind a new `badgeAnimation` setting in UnifiedSettings so users
opt-in. Adds i18n keys for all 18 locales.

---------

Co-authored-by: jeronlxj <jeronliaw@u.nus.edu>
2026-03-01 19:21:36 +04:00
Elie Habib
36e36d8b57 Cost/traffic hardening, runtime fallback controls, and PostHog removal (#638)
- Remove PostHog analytics runtime and configuration
- Add API rate limiting (api/_rate-limit.js)
- Harden traffic controls across edge functions
- Add runtime fallback controls and data-loader improvements
- Add military base data scripts (fetch-mirta-bases, fetch-osm-bases)
- Gitignore large raw data files
- Settings playground prototypes
2026-03-01 11:53:20 +04:00
Elie Habib
cac2a4f5af fix(desktop): route register-interest to cloud when sidecar lacks CONVEX_URL (#639)
* fix(desktop): route register-interest to cloud when sidecar lacks CONVEX_URL

The waitlist registration endpoint needs Convex (cloud-only dependency).
The sidecar handler returned 503 without cloud fallback, and
getRemoteApiBaseUrl() returned '' on desktop (VITE_WS_API_URL unset),
so the settings window fetch resolved to tauri://localhost → 404.

Three-layer fix:
1. Sidecar: tryCloudFallback() when CONVEX_URL missing (proxies to
   https://worldmonitor.app via remoteBase)
2. runtime.ts: getRemoteApiBaseUrl() defaults to https://worldmonitor.app
   on desktop when VITE_WS_API_URL is unset
3. CI: add VITE_WS_API_URL=https://worldmonitor.app to all 4 desktop
   build steps

* chore(deps): bump posthog-js to fix pre-push typecheck
2026-03-01 11:46:31 +04:00
Elie Habib
ff98e3eac7 feat: add GPS/GNSS jamming map layer + CII integration (#570)
* feat: add GPS/GNSS jamming data ingestion from gpsjam.org

- scripts/fetch-gpsjam.mjs: standalone fetcher that downloads daily H3
  hex data, filters medium/high interference, converts to lat/lon via
  h3-js, and writes JSON. Can be run on cron.
- api/gpsjam.js: Vercel Edge Function that proxies gpsjam.org data with
  1hr cache, returns medium/high hexes for frontend consumption.
- src/services/gps-interference.ts: frontend service that fetches from
  the Edge API, converts H3→lat/lon, and classifies by conflict region.
- h3-js added as dependency for hex→coordinate conversion.

* feat: add GPS jamming map layer, CII integration, and country brief signals

Wire gpsjam.org data into map visualization, instability scoring, and
country intelligence. ScatterplotLayer renders high (red) and medium
(orange) interference hexes. CII security score incorporates jamming
counts per country via h3→country geocoding with cache. Country briefs
show jamming zone chip. Full i18n across 18 locales including popup
labels. Data loads with intelligence signals cycle (15min), gated by
1hr client-side cache.
2026-02-28 23:10:15 +04:00
Elie Habib
ddff426c51 fix(relay): Telegram + OOM + memory cleanup (#531)
* fix(relay): resolve Telegram missing package, OOM crashes, and memory cleanup

- Add `telegram` and `ws` to root dependencies so Railway's `npm install` installs them
- Log V8 heap limit at startup to confirm NODE_OPTIONS is active
- Make MAX_VESSELS/MAX_VESSEL_HISTORY env-configurable (default 20k, down from 50k)
- Add permanent latch to skip Telegram import retries when package is missing
- Raise memory cleanup threshold from 450MB to 2GB (env-configurable)
- Clear all caches (RSS, Polymarket, WorldBank) during emergency cleanup

* fix(relay): treat blank env vars as unset in safeInt

Number('') === 0 passes isFinite, silently clamping caps to 1000
instead of using the 20000 default. Guard empty/null before parsing.
2026-02-28 16:04:57 +04:00
Elie Habib
9a0b0ccef8 fix(geo): tokenization-based keyword matching to prevent false positives (#503)
* fix(geo): tokenization-based keyword matching to prevent false positives

Replace String.includes() with tokenization-based Set.has() matching
across the geo-tagging pipeline. Prevents false positives like "assad"
matching inside "ambassador" and "hts" matching inside "rights".

- Add src/utils/keyword-match.ts as single source of truth
- Decompose possessives/hyphens ("Assad's" → includes "assad")
- Support multi-word phrase matching ("white house" as contiguous)
- Remove false-positive-prone DC keywords ('house', 'us ')
- Update 9 consumer files across geo-hub, map, CII, and asset systems
- Add 44 tests covering false positives, true positives, edge cases

Co-authored-by: karim <mirakijka@gmail.com>
Fixes #324

* fix(geo): add inflection suffix matching + fix test imports

Address code review feedback:

P1a: Add suffix-aware matching for plurals and demonyms so existing
keyword lists don't regress (houthi→houthis, ukraine→ukrainian,
iran→iranian, israel→israeli, russia→russian, taiwan→taiwanese).
Uses curated suffix list + e-dropping rule to avoid false positives.

P1b: Expand conflictTopics arrays in DeckGLMap and Map with demonym
forms so "Iranian senate..." correctly registers as conflict topic.

P2: Replace inline test functions with real module import via tsx.
Tests now exercise the production keyword-match.ts directly.

* fix: wire geo-keyword tests into test:data command

The .mts test file wasn't covered by `node --test tests/*.test.mjs`.
Add `npx tsx --test tests/*.test.mts` so test:data runs both suites.

* fix: cross-platform test:data + pin tsx in devDependencies

- Use tsx as test runner for both .mjs and .mts (single invocation)
- Removes ; separator which breaks on Windows cmd.exe
- Add tsx to devDependencies so it works in offline/CI environments

* fix(geo): multi-word demonym matching + short-keyword suffix guard

- Add wordMatches() for suffix-aware phrase matching so "South Korean"
  matches keyword "south korea" and "North Korean" matches "north korea"
- Add MIN_SUFFIX_KEYWORD_LEN=4 guard so short keywords like "ai", "us",
  "hts" only do exact-match (prevents "ais"→"ai", "uses"→"us" false positives)
- Add 5 new tests covering both fixes (58 total, all passing)

* fix(geo): support plural demonyms in keyword matching

Add compound suffixes (ians, eans, ans, ns, is) to handle plural
demonym forms like "Iranians"→"iran", "Ukrainians"→"ukraine",
"Russians"→"russia", "Israelis"→"israel". Adds 5 new tests (63 total).

---------

Co-authored-by: karim <mirakijka@gmail.com>
2026-02-28 10:58:53 +04:00
Elie Habib
22b6e31c61 fix: use cross-env for Windows-compatible npm scripts (#499)
Replace direct `VAR=value command` syntax with cross-env/cross-env-shell
so dev, build, test, and desktop scripts work on Windows PowerShell/CMD.

Co-authored-by: facusturla <facusturla@users.noreply.github.com>
2026-02-28 09:42:22 +04:00
Elie Habib
3d2c638a72 feat(military): server-side military bases 125K + rate limiting (#496)
* feat(military): server-side military bases with 125K entries + rate limiting (#485)

Migrate military bases from 224 static client-side entries to 125,380
server-side entries stored in Redis GEO sorted sets, served via
bbox-filtered GEOSEARCH endpoint with server-side clustering.

Data pipeline:
- Pizzint/Polyglobe: 79,156 entries (Supabase extraction)
- OpenStreetMap: 45,185 entries
- MIRTA: 821 entries
- Curated strategic: 218 entries
- 277 proximity duplicates removed

Server:
- ListMilitaryBases RPC with GEOSEARCH + HMGET + tier/filter/clustering
- Antimeridian handling (split bbox queries)
- Blue-green Redis deployment with atomic version pointer switch
- geoSearchByBox() + getHashFieldsBatch() helpers in redis.ts

Security:
- @upstash/ratelimit: 60 req/min sliding window per IP
- IP spoofing fix: prioritize x-real-ip (Vercel-injected) over x-forwarded-for
- Require API key for non-browser requests (blocks unauthenticated curl/scripts)
- Input validation: allowlisted types/kinds, regex country, clamped bbox/zoom

Frontend:
- Viewport-driven loading with bbox quantization + debounce
- Server-side grid clustering at low zoom levels
- Enriched popup with kind, category badges (airforce/naval/nuclear/space)
- Static 224 bases kept as search fallback + initial render

* fix(military): fallback to production Redis keys in preview deployments

Preview deployments prefix Redis keys with `preview:{sha}:` but military
bases data is seeded to unprefixed (production) keys. When the prefixed
`military:bases:active` key is missing, fall back to the unprefixed key
and use raw (unprefixed) keys for geo/meta lookups.

* fix: remove unused 'remaining' destructure in rate-limit (TS6133)

* ci: add typecheck:api to pre-push hook to catch server-side TS errors

* debug(military): add X-Bases-Debug response header for preview diagnostics

* fix(bases): trigger initial server fetch on map load

fetchServerBases() was only called on moveend — if the user
never panned/zoomed, the API was never called and only the 224
static fallback bases showed.
2026-02-28 09:16:59 +04:00
Elie Habib
30bd84abb2 fix(linux): append host GStreamer plugins to AppImage search path (#424)
* chore: bump v2.5.12

## Changelog

- fix(linux): enable keyring persistence via Secret Service + keyutils (#419)
- fix(ci): use weston+XWayland for Linux smoke test (#417)
- ci: add standalone Test Linux App workflow (#414)
- ci: skip Typecheck and Lint on fork PRs (#415)
- perf: optimize Wingbits API usage and reduce unnecessary polling (#416)

* fix(linux): append host GStreamer plugins to AppImage search path

The linuxdeploy GStreamer hook force-overrides GST_PLUGIN_PATH_1_0 and
GST_PLUGIN_SYSTEM_PATH_1_0 to only contain bundled plugins from the CI
build system (Ubuntu 24.04, GStreamer 1.24).  On hosts with newer
GStreamer (e.g. Arch 1.28), codec plugins like gst-libav and
fakevideosink from gst-plugins-bad are invisible — WebKit can't play
video.

Append common host GStreamer plugin directories as fallback so the
system's codec plugins are discoverable while bundled plugins retain
priority.

Also fixes:
- tauri.conf.json devUrl port mismatch (5173 → 3000) breaking desktop:dev
- live-channels-window YouTube validation allowing add on non-OK responses
2026-02-26 20:24:50 +04:00
Elie Habib
b238618e13 Add BIS central bank data integration (policy rates, exchange rates, credit) (#363)
* feat: add BIS Central Bank Policy Tracker to EconomicPanel

Add a new "Central Banks" tab to the Economic panel powered by the BIS
(Bank for International Settlements) free SDMX REST API. Displays policy
rates, effective exchange rates, and credit-to-GDP ratios for 12 major
central banks (Fed, ECB, BOE, BOJ, SNB, MAS, RBI, RBA, PBOC, BOC, BOK,
BCB).

- Proto: 4 new files (bis_data, get_bis_policy_rates, get_bis_exchange_rates,
  get_bis_credit) + 3 RPCs added to EconomicService
- Server: shared CSV fetch/parse helper using papaparse, 3 handlers with
  Redis caching (6h/12h TTL), batched single-request per dataset
- Frontend: 3 separate circuit breakers, fetchBisData() with Promise.all
- UI: Central Banks tab with policy rates (sorted by rate, colored by
  cut/hike/hold), real EER indices, and credit-to-GDP ratios
- Integration: 'bis' DataSourceId, BIS in StatusPanel WORLD_APIS, 60min
  refresh interval, data loader wiring
- i18n: BIS keys added to all 17 locale files

No API key required — BIS is free/public. All BIS-derived text rendered
via escapeHtml() to prevent XSS.

https://claude.ai/code/session_01N73WokR4NPuSg8JpPz7nYZ

* chore: update package-lock.json

https://claude.ai/code/session_01N73WokR4NPuSg8JpPz7nYZ

* fix: BIS policy tracker - URL bug, error logging, UX, and data cleanup

- Fix double '?' in fetchBisCSV URL construction (format=csv was silently ignored)
- Add error logging to all 3 BIS server handlers (previously silent catch blocks)
- Fix misleading "BIS data loading..." empty state to "temporarily unavailable - will retry"
- Remove unused nominal EER fetch to save bandwidth (only real EER is displayed)
- Fix previousRatio rounding inconsistency in credit-to-GDP fallback path

https://claude.ai/code/session_01N73WokR4NPuSg8JpPz7nYZ

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-25 14:46:27 +04:00
Sebastien Melki
b1d835b69f feat: HappyMonitor — positive news dashboard (happy.worldmonitor.app) (#229)
* chore: add project config

* docs: add domain research (stack, features, architecture, pitfalls)

* docs: define v1 requirements

* docs: create roadmap (9 phases)

* docs(01): capture phase context

* docs(state): record phase 1 context session

* docs(01): research phase domain

* docs(01): create phase plan

* fix(01): revise plans based on checker feedback

* feat(01-01): register happy variant in config system and build tooling

- Add 'happy' to allowed stored variants in variant.ts
- Create variants/happy.ts with panels, map layers, and VariantConfig
- Add HAPPY_PANELS, HAPPY_MAP_LAYERS, HAPPY_MOBILE_MAP_LAYERS inline in panels.ts
- Update ternary export chains to select happy config when SITE_VARIANT === 'happy'
- Add happy entry to VARIANT_META in vite.config.ts
- Add dev:happy and build:happy scripts to package.json

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(01-01): update index.html for variant detection, CSP, and Google Fonts

- Add happy.worldmonitor.app to CSP frame-src directive
- Extend inline script to detect variant from hostname (happy/tech/finance) and localStorage
- Set data-variant attribute on html element before first paint to prevent FOUC
- Add Google Fonts preconnect and Nunito stylesheet links
- Add favicon variant path replacement in htmlVariantPlugin for non-full variants

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(01-01): create happy variant favicon assets

- Create SVG globe favicon in sage green (#6B8F5E) and warm gold (#C4A35A)
- Generate PNG favicons at all required sizes (16, 32, 180, 192, 512)
- Generate favicon.ico with PNG-in-ICO wrapper
- Create branded OG image (1200x630) with cream background, sage/gold scheme

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(01-01): complete variant registration plan

- Create 01-01-SUMMARY.md documenting variant registration
- Update STATE.md with plan 1 completion, metrics, decisions
- Update ROADMAP.md with phase 01 progress (1/3 plans)
- Mark INFRA-01, INFRA-02, INFRA-03 requirements complete

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(01-02): create happy variant CSS theme with warm palette and semantic overrides

- Complete happy-theme.css with light mode (cream/sage), dark mode (navy/warm), and semantic colors
- 179 lines covering all CSS custom properties: backgrounds, text, borders, overlays, map, panels
- Nunito typography and 14px panel border radius for soft rounded aesthetic
- Semantic colors remapped: gold (critical), sage (growth), blue (hope), pink (kindness)
- Dark mode uses warm navy/sage tones, never pure black
- Import added to main.css after panels.css

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(01-02): add happy variant skeleton shell overrides and theme-color meta

- Inline skeleton styles for happy variant light mode (cream bg, Nunito font, sage dot, warm shimmer)
- Inline skeleton styles for happy variant dark mode (navy bg, warm borders, sage tones)
- Rounded corners (14px) on skeleton panels and map for soft aesthetic
- Softer pill border-radius (8px) in happy variant
- htmlVariantPlugin: theme-color meta updated to #FAFAF5 for happy variant mobile chrome

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(01-02): complete happy theme CSS plan

- SUMMARY.md with execution results and self-check
- STATE.md advanced to plan 2/3, decisions logged
- ROADMAP.md progress updated (2/3 plans complete)
- REQUIREMENTS.md: THEME-01, THEME-03, THEME-04 marked complete

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(01-03): create warm basemap styles and wire variant-aware map selection

- Add happy-light.json: sage land, cream background, light blue ocean (forked from CARTO Voyager)
- Add happy-dark.json: dark sage land, navy background, dark navy ocean (forked from CARTO Dark Matter)
- Both styles preserve CARTO CDN source/sprite/glyph URLs for tile loading
- DeckGLMap.ts selects happy basemap URLs when SITE_VARIANT is 'happy'

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(01-03): style panel chrome, empty states, and loading for happy variant

- Panels get 14px rounded corners with subtle warm shadows
- Panel titles use normal casing (no uppercase) for friendlier feel
- Empty states (.panel-empty, .empty-state) show nature-themed sprout SVG icon
- Loading radar animation softened to 3s rotation with sage-green glow
- Status dots use gentle happy-pulse animation (2.5s ease-in-out)
- Error states use warm gold tones instead of harsh red
- Map controls, tabs, badges all get rounded corners
- Severity badges use warm semantic colors
- Download banner and posture radar adapted to warm theme

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(01-03): bridge SITE_VARIANT to data-variant attribute on <html>

The CSS theme overrides rely on [data-variant="happy"] on the document root,
but the inline script only detects variant from hostname/localStorage. This
leaves local dev (VITE_VARIANT=happy) and Vercel deployments without the
attribute set. Two fixes:

1. main.ts sets document.documentElement.dataset.variant from SITE_VARIANT
2. Vite htmlVariantPlugin injects build-time variant fallback into inline script

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(01-03): boost CSS specificity so happy theme wins over :root

The happy-theme.css was imported before :root in main.css, and both
[data-variant="happy"] and :root have equal specificity (0-1-0), so
:root variables won after in the cascade. Fix by using :root[data-variant="happy"]
(specificity 0-2-0) which always beats :root (0-1-0).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(01): fix CSS cascade — import happy-theme after main.css in main.ts

The root cause: happy-theme.css was @imported inside main.css (line 4),
which meant Vite loaded it BEFORE the :root block (line 9+). With equal
specificity, the later :root variables always won.

Fix: remove @import from main.css, import happy-theme.css directly in
main.ts after main.css. This ensures cascade order is correct — happy
theme variables come last and win. No !important needed.

Also consolidated semantic color variables into the same selector blocks
to reduce redundancy.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(01): fix CSS cascade with @layer base and theme toggle for happy variant

- Wrap main.css in @layer base via base-layer.css so happy-theme.css
  (unlayered) always wins the cascade for custom properties
- Remove duplicate <link> stylesheet from index.html (was double-loading)
- Default happy variant to light theme (data-theme="light") so the
  theme toggle works on first click instead of requiring two clicks
- Force build-time variant in inline script — stale localStorage can no
  longer override the deployment variant
- Prioritize VITE_VARIANT env over localStorage in variant.ts so
  variant-specific builds are deterministic

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(01-03): complete map basemap & panel chrome plan — Phase 1 done

- Add 01-03-SUMMARY.md with task commits, deviations, and self-check
- Update STATE.md: Phase 1 complete, advance to ready for Phase 2
- Update ROADMAP.md: mark Phase 1 plans 3/3 complete
- Update REQUIREMENTS.md: mark THEME-02 and THEME-05 complete

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(phase-01): complete phase execution

* docs(phase-02): research curated content pipeline

* docs(02): create phase plan — curated content pipeline

* feat(02-01): add positive RSS feeds for happy variant

- Add HAPPY_FEEDS record with 8 feeds across 5 categories (positive, science, nature, health, inspiring)
- Update FEEDS export ternary to route happy variant to HAPPY_FEEDS
- Add happy source tiers to SOURCE_TIERS (Tier 2 for main sources, Tier 3 for category feeds)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(02-01): extend GDELT with tone filtering and positive topic queries

- Add tone_filter (field 4) and sort (field 5) to SearchGdeltDocumentsRequest proto
- Regenerate TypeScript client/server types via buf generate
- Handler appends toneFilter to GDELT query string, uses req.sort for sort param
- Add POSITIVE_GDELT_TOPICS array with 5 positive topic queries
- Add fetchPositiveGdeltArticles() with tone>5 and ToneDesc defaults
- Add fetchPositiveTopicIntelligence() and fetchAllPositiveTopicIntelligence() helpers
- Existing fetchGdeltArticles() backward compatible (empty toneFilter/sort = no change)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(02-01): complete positive feeds & GDELT tone filtering plan

- Create 02-01-SUMMARY.md with execution results
- Update STATE.md: phase 2, plan 1 of 2, decisions, metrics
- Update ROADMAP.md: phase 02 progress (1/2 plans)
- Mark FEED-01 and FEED-03 requirements complete

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(02-02): add positive content classifier and extend NewsItem type

- Create positive-classifier.ts with 6 content categories (science-health, nature-wildlife, humanity-kindness, innovation-tech, climate-wins, culture-community)
- Source-based pre-mapping for GNN category feeds (fast path)
- Priority-ordered keyword classification for general positive feeds (slow path)
- Add happyCategory optional field to NewsItem interface
- Export HAPPY_CATEGORY_LABELS and HAPPY_CATEGORY_ALL for downstream UI use

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore(02-02): clean up happy variant config and verify feed wiring

- Remove dead FEEDS placeholder from happy.ts (now handled by HAPPY_FEEDS in feeds.ts)
- Remove unused Feed type import
- Verified SOURCE_TIERS has all 8 happy feed entries (Tier 2: GNN/Positive.News/RTBC/Optimist, Tier 3: GNN category feeds)
- Verified FEEDS export routes to HAPPY_FEEDS when SITE_VARIANT=happy
- Verified App.ts loadNews() dynamically iterates FEEDS keys
- Happy variant builds successfully

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(02-02): complete content category classifier plan

- SUMMARY.md documenting classifier implementation and feed wiring cleanup
- STATE.md updated: Phase 2 complete, 5 total plans done, 56% progress
- ROADMAP.md updated: Phase 02 marked complete (2/2 plans)
- REQUIREMENTS.md: FEED-04 marked complete

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(02-03): create gap closure plan for classifier wiring

* feat(02-03): wire classifyNewsItem into happy variant news ingestion

- Import classifyNewsItem from positive-classifier service
- Add classification step in loadNewsCategory() after fetchCategoryFeeds
- Guard with SITE_VARIANT === 'happy' to avoid impact on other variants
- In-place mutation via for..of loop sets happyCategory on every NewsItem

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(02-03): complete classifier wiring gap closure plan

- Add 02-03-SUMMARY.md documenting classifier wiring completion
- Update STATE.md with plan 3/3 position and decisions
- Update ROADMAP.md with completed plan checkboxes
- Include 02-VERIFICATION.md phase verification document

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(phase-2): complete phase execution

* test(02): complete UAT - 1 passed, 1 blocker diagnosed

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(phase-3): research positive news feed & quality pipeline

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(03): create phase plan for positive news feed and quality pipeline

* fix(03): revise plans based on checker feedback

* feat(03-02): add imageUrl to NewsItem and extract images from RSS

- Add optional imageUrl field to NewsItem interface
- Add extractImageUrl() helper to rss.ts with 4-strategy image extraction
  (media:content, media:thumbnail, enclosure, img-in-description)
- Wire image extraction into fetchFeed() for happy variant only

* feat(03-01): add happy variant guards to all App.ts code paths

- Skip DEFCON/PizzInt indicator for happy variant
- Add happy variant link (sun icon) to variant switcher header
- Show 'Good News Map' title for happy variant map section
- Skip LiveNewsPanel, LiveWebcams, TechEvents, ServiceStatus, TechReadiness, MacroSignals, ETFFlows, Stablecoin panels for happy
- Gate live-news first-position logic with happy exclusion
- Only load 'news' data for happy variant (skip markets, predictions, pizzint, fred, oil, spending, intelligence, military layers)
- Only schedule 'news' refresh interval for happy (skip all geopolitical/financial refreshes)
- Add happy-specific search modal with positive placeholder and no military/geopolitical sources

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(03-02): create PositiveNewsFeedPanel with filter bar and card rendering

- New PositiveNewsFeedPanel component extending Panel with:
  - Category filter bar (All + 6 positive categories)
  - Rich card rendering with image, title, source, category badge, time
  - Filter state preserved across data refreshes
  - Proper cleanup in destroy()
- Add CSS styles to happy-theme.css for cards and filter bar
  - Category-specific badge colors using theme variables
  - Scoped under [data-variant="happy"] to avoid affecting other variants

* feat(03-01): return empty channels for happy variant in LiveNewsPanel

- Defense-in-depth: LIVE_CHANNELS returns empty array for happy variant
- Ensures zero Bloomberg/war streams even if panel is somehow instantiated
- Combined with createPanels() guard from Task 1 for belt-and-suspenders safety

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(03-02): complete positive news feed panel plan

- Created 03-02-SUMMARY.md with execution results
- Updated STATE.md with position, decisions, and metrics
- Updated ROADMAP.md with phase 03 progress (2/3 plans)
- Marked NEWS-01, NEWS-02 requirements as complete

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(03-01): complete Happy Variant App.ts Integration plan

- SUMMARY.md with execution results and decisions
- STATE.md updated with 03-01 decisions and session info
- ROADMAP.md progress updated (2/3 phase 3 plans)
- NEWS-03 requirement marked complete

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(03-03): create sentiment gate service for ML-based filtering

- Exports filterBySentiment() wrapping mlWorker.classifySentiment()
- Default threshold 0.85 with localStorage override for tuning
- Graceful degradation: returns all items if ML unavailable
- Batches titles at 20 items per call (ML_THRESHOLDS.maxTextsPerBatch)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(03-03): wire multi-stage quality pipeline and positive-feed panel into App.ts

- Register 'positive-feed' in HAPPY_PANELS replacing 'live-news'
- Import PositiveNewsFeedPanel, filterBySentiment, fetchAllPositiveTopicIntelligence
- Add positivePanel + happyAllItems class properties
- Create PositiveNewsFeedPanel in createPanels() for happy variant
- Accumulate curated items in loadNewsCategory() for happy variant
- Implement loadHappySupplementaryAndRender() 4-stage pipeline:
  1. Curated feeds render immediately (non-blocking UX)
  2. GDELT positive articles fetched as supplementary
  3. Sentiment-filtered via DistilBERT-SST2 (filterBySentiment)
  4. Merged + sorted by date, re-rendered
- Auto-refresh on REFRESH_INTERVALS.feeds re-runs full pipeline
- ML failure degrades gracefully to curated-only display

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(03-03): complete quality pipeline plan - phase 3 done

- Summary: multi-stage positive news pipeline with ML sentiment gate
- STATE.md: phase 3 complete (3/3), 89% progress
- ROADMAP.md: phase 03 marked complete
- REQUIREMENTS.md: FEED-02, FEED-05 marked complete

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(03): wire positive-feed panel key in panels.ts and add happy map layer/legend config

The executor updated happy.ts but the actual HAPPY_PANELS export comes from
panels.ts — it still had 'live-news' instead of 'positive-feed', so the panel
never rendered. Also adds happyLayers (natural only) and happy legend to Map.ts
to hide military layer toggles and geopolitical legend items.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(phase-3): complete phase execution

* docs(phase-4): research global map & positive events

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(04): create phase plan — global map & positive events

* fix(04): revise plans based on checker feedback

* feat(04-01): add positiveEvents and kindness keys to MapLayers interface and all variant configs

- Add positiveEvents and kindness boolean keys to MapLayers interface
- Update all 10 variant layer configs (8 in panels.ts + 2 in happy.ts)
- Happy variant: positiveEvents=true, kindness=true; all others: false
- Fix variant config files (full, tech, finance) and e2e harnesses for compilation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(04-01): add happy variant layer toggles and legend in DeckGLMap

- Add happy branch to createLayerToggles with 3 toggles: Positive Events, Acts of Kindness, Natural Events
- Add happy branch to createLegend with 4 items: Positive Event (green), Breakthrough (gold), Act of Kindness (light green), Natural Event (orange)
- Non-happy variants unchanged

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(04-01): complete map layer config & happy variant toggles plan

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(04-02): add positive events geocoding pipeline and map layer

- Proto service PositiveEventsService with ListPositiveGeoEvents RPC
- Server-side GDELT GEO fetch with positive topic queries, dedup, classification
- Client-side service calling server RPC + RSS geocoding via inferGeoHubsFromTitle
- DeckGLMap green/gold ScatterplotLayer with pulse animation for significant events
- Tooltip shows event name, category, and report count
- Routes registered in api gateway and vite dev server

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(04-02): wire positive events loading into App.ts happy variant pipeline

- Import fetchPositiveGeoEvents and geocodePositiveNewsItems
- Load positive events in loadAllData() for happy variant with positiveEvents toggle
- loadPositiveEvents() merges GDELT GEO RPC + geocoded RSS items, deduplicates by name
- loadDataForLayer switch case for toggling positiveEvents layer on/off
- MapContainer.setPositiveEvents() delegates to DeckGLMap

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(04-02): complete positive events geocoding pipeline plan

- SUMMARY.md with task commits, decisions, deviations
- STATE.md updated with position, metrics, decisions
- ROADMAP.md and REQUIREMENTS.md updated

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(04-03): create kindness-data service with baseline generator and curated events

- Add KindnessPoint interface for map visualization data
- Add MAJOR_CITIES constant with ~60 cities worldwide (population-weighted)
- Implement generateBaselineKindness() producing 50-80 synthetic points per cycle
- Implement extractKindnessEvents() for real kindness items from curated news
- Export fetchKindnessData() merging baseline + real events

* feat(04-03): add kindness layer to DeckGLMap and wire into App.ts pipeline

- Add createKindnessLayers() with solid green fill + gentle pulse ring for real events
- Add kindness-layer tooltip showing city name and description
- Add setKindnessData() setter in DeckGLMap and MapContainer
- Wire loadKindnessData() into App.ts loadAllData and loadDataForLayer
- Kindness layer gated by mapLayers.kindness toggle (happy variant only)
- Pulse animation triggers when real kindness events are present

* docs(04-03): complete kindness data pipeline & map layer plan

- Create 04-03-SUMMARY.md documenting kindness layer implementation
- Update STATE.md: phase 04 complete (3/3 plans), advance position
- Update ROADMAP.md: phase 04 marked complete
- Mark KIND-01 and KIND-02 requirements as complete

* docs(phase-4): complete phase execution

* docs(phase-5): research humanity data panels domain

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(05-humanity-data-panels): create phase plan

* feat(05-01): create humanity counters service with metric definitions and rate calculations

- Define 6 positive global metrics with annual totals from UN/WHO/World Bank/UNESCO
- Calculate per-second rates from annual totals / 31,536,000 seconds
- Absolute-time getCounterValue() avoids drift across tabs/throttling
- Locale-aware formatCounterValue() using Intl.NumberFormat

* feat(05-02): install papaparse and create progress data service

- Install papaparse + @types/papaparse for potential OWID CSV fallback
- Create src/services/progress-data.ts with 4 World Bank indicators
- Export PROGRESS_INDICATORS (life expectancy, literacy, child mortality, poverty)
- Export fetchProgressData() using existing getIndicatorData() RPC
- Null value filtering, year sorting, invertTrend-aware change calculation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(05-01): create CountersPanel component with 60fps animated ticking numbers

- Extend Panel base class with counters-grid of 6 counter cards
- requestAnimationFrame loop updates all values at 60fps
- Absolute-time calculation via getCounterValue() prevents drift
- textContent updates (not innerHTML) avoid layout thrashing
- startTicking() / destroy() lifecycle methods for App.ts integration

* feat(05-02): create ProgressChartsPanel with D3.js area charts

- Extend Panel base class with id 'progress', title 'Human Progress'
- Render 4 stacked D3 area charts (life expectancy, literacy, child mortality, poverty)
- Warm happy-theme colors: sage green, soft blue, warm gold, muted rose
- d3.area() with curveMonotoneX for smooth filled curves
- Header with label, change badge (e.g., "+58.0% since 1960"), and unit
- Hover tooltip with bisector-based nearest data point detection
- ResizeObserver with 200ms debounce for responsive re-rendering
- Clean destroy() lifecycle with observer disconnection

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(05-01): complete ticking counters service & panel plan

- SUMMARY.md with execution results and self-check
- STATE.md updated to phase 5, plan 1/3
- ROADMAP.md progress updated
- Requirements COUNT-01, COUNT-02, COUNT-03 marked complete

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(05-02): complete progress charts panel plan

- Create 05-02-SUMMARY.md with execution results
- Update STATE.md: plan 2/3, decisions, metrics
- Update ROADMAP.md: phase 05 progress (2/3 plans)
- Mark PROG-01, PROG-02, PROG-03 complete in REQUIREMENTS.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(05-03): wire CountersPanel and ProgressChartsPanel into App.ts lifecycle

- Import CountersPanel, ProgressChartsPanel, and fetchProgressData
- Add class properties for both new panels
- Instantiate both panels in createPanels() gated by SITE_VARIANT === 'happy'
- Add progress data loading task in refreshAll() for happy variant
- Add loadProgressData() private method calling fetchProgressData + setData
- Add destroy() cleanup for both panels (stops rAF loop and ResizeObserver)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(05-03): add counter and progress chart CSS styles to happy-theme.css

- Counters grid: responsive 3-column layout (3/2/1 at 900px/500px breakpoints)
- Counter cards: hover lift, tabular-nums for jitter-free 60fps updates
- Counter icon/value/label/source typography hierarchy
- Progress chart containers: stacked with border dividers
- Chart header with label, badge, and unit display
- D3 SVG axis styling (tick text fill, domain stroke)
- Hover tooltip with absolute positioning and shadow
- Dark mode adjustments for card hover shadow and tooltip shadow
- All selectors scoped under [data-variant='happy']

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(05-03): complete panel wiring & CSS plan

- Create 05-03-SUMMARY.md with execution results
- Update STATE.md: phase 5 complete (3/3 plans), decisions, metrics
- Update ROADMAP.md: phase 05 progress (3/3 summaries, Complete)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(phase-5): complete phase execution

* docs(06): research phase 6 content spotlight panels

* docs(phase-6): create phase plan

* feat(06-01): add science RSS feeds and BreakthroughsTickerPanel

- Expand HAPPY_FEEDS.science from 1 to 5 feeds (ScienceDaily, Nature News, Live Science, New Scientist)
- Create BreakthroughsTickerPanel extending Panel with horizontal scrolling ticker
- Doubled content rendering for seamless infinite CSS scroll animation
- Sanitized HTML output using escapeHtml/sanitizeUrl

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(06-01): create HeroSpotlightPanel with photo, map location, and hero card

- Create HeroSpotlightPanel extending Panel for daily hero spotlight
- Render hero card with image, source, title, time, and optional map button
- Conditionally show "Show on map" button only when both lat and lon exist
- Expose onLocationRequest callback for App.ts map integration wiring
- Sanitized HTML output using escapeHtml/sanitizeUrl

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(06-02): add GoodThingsDigestPanel with progressive AI summarization

- Panel extends Panel base class with id 'digest', title '5 Good Things'
- Renders numbered story cards with titles immediately (progressive rendering)
- Summarizes each story in parallel via generateSummary() with Promise.allSettled
- AbortController cancels in-flight summaries on re-render or destroy
- Graceful fallback to truncated title on summarization failure
- Passes [title, source] to satisfy generateSummary's 2-headline minimum

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(06-02): complete Good Things Digest Panel plan

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(06-01): complete content spotlight panels plan

- Add 06-01-SUMMARY.md with execution results
- Update STATE.md with position, decisions, metrics
- Update ROADMAP.md and REQUIREMENTS.md progress

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(06-03): wire Phase 6 panels into App.ts lifecycle and update happy.ts config

- Import and instantiate BreakthroughsTickerPanel, HeroSpotlightPanel, GoodThingsDigestPanel in createPanels()
- Wire heroPanel.onLocationRequest callback to map.setCenter + map.flashLocation
- Distribute data to all three panels after content pipeline in loadHappySupplementaryAndRender()
- Add destroy calls for all three panels in App.destroy()
- Add digest key to DEFAULT_PANELS in happy.ts config

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(06-03): add CSS styles for ticker, hero card, and digest panels

- Add happy-ticker-scroll keyframe animation for infinite horizontal scroll
- Add breakthroughs ticker styles (wrapper, track, items with hover pause)
- Add hero spotlight card styles (image, body, source, title, location button)
- Add digest list styles (numbered cards, titles, sources, progressive summaries)
- Add dark mode overrides for all three panel types
- All selectors scoped under [data-variant="happy"]

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(06-03): complete panel wiring & CSS plan

- Create 06-03-SUMMARY.md with execution results
- Update STATE.md: phase 6 complete, 18 plans done, 78% progress
- Update ROADMAP.md: phase 06 marked complete (3/3 plans)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(phase-6): complete phase execution

* docs(07): research conservation & energy trackers phase

* docs(07-conservation-energy-trackers): create phase plan

* feat(07-02): add renewable energy data service

- Fetch World Bank EG.ELC.RNEW.ZS indicator (IEA-sourced) for global + 7 regions
- Return global percentage, historical time-series, and regional breakdown
- Graceful degradation: individual region failures skipped, complete failure returns zeroed data
- Follow proven progress-data.ts pattern for getIndicatorData() RPC usage

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(07-01): add conservation wins dataset and data service

- Create conservation-wins.json with 10 species recovery stories and population timelines
- Create conservation-data.ts with SpeciesRecovery interface and fetchConservationWins() loader
- Species data sourced from USFWS, IUCN, NOAA, WWF, and other published reports

* feat(07-02): add RenewableEnergyPanel with D3 arc gauge and regional breakdown

- Animated D3 arc gauge showing global renewable electricity % with 1.5s easeCubicOut
- Historical trend sparkline using d3.area() + curveMonotoneX below gauge
- Regional breakdown with horizontal bars sorted by percentage descending
- All colors use getCSSColor() for theme-aware rendering
- Empty state handling when no data available

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(07-01): add SpeciesComebackPanel with D3 sparklines and species cards

- Create SpeciesComebackPanel extending Panel base class
- Render species cards with photo (lazy loading + error fallback), info badges, D3 sparkline, and summary
- D3 sparklines use area + line with curveMonotoneX and viewBox for responsive sizing
- Recovery status badges (recovered/recovering/stabilized) and IUCN category badges
- Population values formatted with Intl.NumberFormat for readability

* docs(07-02): complete renewable energy panel plan

- SUMMARY.md with task commits, decisions, self-check
- STATE.md updated to phase 7 plan 2, 83% progress
- ROADMAP.md phase 07 progress updated
- REQUIREMENTS.md: ENERGY-01, ENERGY-02, ENERGY-03 marked complete

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(07-01): complete species comeback panel plan

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(07-03): wire species and renewable panels into App.ts lifecycle

- Add imports for SpeciesComebackPanel, RenewableEnergyPanel, and data services
- Add class properties for speciesPanel and renewablePanel
- Instantiate both panels in createPanels() gated by SITE_VARIANT === 'happy'
- Add loadSpeciesData() and loadRenewableData() tasks in refreshAll()
- Add destroy cleanup for both panels before map cleanup
- Add species and renewable entries to happy.ts DEFAULT_PANELS config

* feat(07-03): add CSS styles for species cards and renewable energy gauge

- Species card grid layout with 2-column responsive grid
- Photo, info, badges (recovered/recovering/stabilized/IUCN), sparkline, summary styles
- Renewable energy gauge section, historical sparkline, and regional bar chart styles
- Dark mode overrides for species card hover shadow and IUCN badge background
- All styles scoped with [data-variant='happy'] using existing CSS variables

* docs(07-03): complete panel wiring & CSS plan

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(happy): add missing panel entries and RSS proxy for dev mode

HAPPY_PANELS in panels.ts was missing digest, species, and renewable
entries — panels were constructed but never appended to the grid because
the panelOrder loop only iterated the 6 original keys.

Also adds RSS proxy middleware for Vite dev server, fixes sebuf route
regex to match hyphenated domains (positive-events), and adds happy
feed domains to the rss-proxy allowlist.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: progress data lookup, ticker speed, ultrawide layout gap

1. Progress/renewable data: World Bank API returns countryiso3code "WLD"
   for world aggregate, but services were looking up by request code "1W".
   Changed lookups to use "WLD".

2. Breakthroughs ticker: slowed animation from 30s to 60s duration.

3. Ultrawide layout (>2000px): replaced float-based layout with CSS grid.
   Map stays in left column (60%), panels grid in right column (40%).
   Eliminates dead space under the map where panels used to wrap below.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: UI polish — counter overflow, ticker speed, monitors panel, filter tabs

- Counter values: responsive font-size with clamp(), overflow protection,
  tighter card padding to prevent large numbers from overflowing
- Breakthroughs ticker: slowed from 60s to 120s animation duration
- My Monitors panel: gate monitors from panel order in happy variant
  (was unconditionally pushed into panelOrder regardless of variant)
- Filter tabs: smaller padding/font, flex-shrink:0, fade mask on right
  edge to hint at scrollable overflow

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(happy): exclude APT groups layer from happy variant map

The APT groups layer (cyber threat actors like Fancy Bear, Cozy Bear)
was only excluded for the tech variant. Now also excluded for happy,
since cyber threat data has no place on a Good News Map.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(happy-map): labeled markers, remove fake baseline, fix APT leak

- Positive events now show category emoji + location name as colored
  text labels (TextLayer) instead of bare dots. Labels filter by zoom
  level to avoid clutter at global view.
- Removed synthetic kindness baseline (50-80 fake "Volunteers at work"
  dots in random cities). Only real kindness events from news remain.
- Kindness events also get labeled dots with headlines.
- Improved tooltips with proper category names and source counts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(happy-map): disable earthquakes, fix GDELT query syntax

- Disable natural events layer (earthquakes) for happy variant —
  not positive news
- Fix GDELT GEO positive queries: OR terms require parentheses
  per GDELT API syntax, added third query for charity/volunteer news
- Updated both desktop and mobile happy map layer configs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(happy): ultrawide grid overflow, panel text polish

Ultrawide: set min-height:0 on map/panels grid children so they
respect 1fr row constraint and scroll independently instead of
pushing content below the viewport.

Panel CSS: softer word-break on counters, line-clamp on digest
and species summaries, ticker title max-width, consistent
text-dim color instead of opacity hacks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(08-map-data-overlays): research phase domain

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(08-map-data-overlays): create phase plan

* Add Global Giving Activity Index with multi-platform aggregation (#255)

* feat(08-01): add static data for happiness scores, renewable installations, and recovery zones

- Create world-happiness.json with 152 country scores from WHR 2025
- Create renewable-installations.json with 92 global entries (solar/wind/hydro/geothermal)
- Extend conservation-wins.json with recoveryZone lat/lon for all 10 species

* feat(08-01): add service loaders, extend MapLayers with happiness/species/energy keys

- Create happiness-data.ts with fetchHappinessScores() returning Map<ISO2, score>
- Create renewable-installations.ts with fetchRenewableInstallations() returning typed array
- Extend SpeciesRecovery interface with optional recoveryZone field
- Add happiness, speciesRecovery, renewableInstallations to MapLayers interface
- Update all 8 variant MapLayers configs (happiness=true in happy, false elsewhere)
- Update e2e harness files with new layer keys

* docs(08-01): complete data foundation plan summary and state updates

- Create 08-01-SUMMARY.md with execution results
- Update STATE.md to phase 8, plan 1/2
- Update ROADMAP.md progress for phase 08
- Mark requirements MAP-03, MAP-04, MAP-05 complete

* feat(08-02): add happiness choropleth, species recovery, and renewable installation overlay layers

- Add three Deck.gl layer creation methods with color-coded rendering
- Add public data setters for happiness scores, species recovery zones, and renewable installations
- Wire layers into buildLayers() gated by MapLayers keys
- Add tooltip cases for all three new layer types
- Extend happy variant layer toggles (World Happiness, Species Recovery, Clean Energy)
- Extend happy variant legend with choropleth, species, and renewable entries
- Cache country GeoJSON reference in loadCountryBoundaries() for choropleth reuse

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(08-02): wire MapContainer delegation and App.ts data loading for map overlays

- Add MapContainer delegation methods for happiness, species recovery, and renewable installations
- Add happiness scores and renewable installations map data loading in App.ts refreshAll()
- Chain species recovery zone data to map from existing loadSpeciesData()
- All three overlay datasets flow from App.ts through MapContainer to DeckGLMap

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(08-02): complete map overlay layers plan

- Create 08-02-SUMMARY.md with execution results
- Update STATE.md: phase 8 complete (2/2 plans), 22 total plans, decisions logged
- Update ROADMAP.md: phase 08 marked complete

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(phase-8): complete phase execution

* docs(roadmap): add Phase 7.1 gap closure for renewable energy installation & coal data

Addresses Phase 7 verification gaps (ENERGY-01, ENERGY-03): renewable panel
lacks solar/wind installation growth and coal plant closure visualizations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(7.1): research renewable energy installation & coal retirement data

* docs(71): create phase plans for renewable energy installation & coal retirement data

* feat(71-01): add GetEnergyCapacity RPC proto and server handler

- Create get_energy_capacity.proto with request/response messages
- Add GetEnergyCapacity RPC to EconomicService in service.proto
- Implement server handler with EIA capability API integration
- Coal code fallback (COL -> BIT/SUB/LIG/RC) for sub-type support
- Redis cache with 24h TTL for annual capacity data
- Register handler in economic service handler

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(71-01): add client-side fetchEnergyCapacity with circuit breaker

- Add GetEnergyCapacityResponse import and capacityBreaker to economic service
- Export fetchEnergyCapacityRpc() with energyEia feature gating
- Add CapacitySeries/CapacityDataPoint types to renewable-energy-data.ts
- Export fetchEnergyCapacity() that transforms proto types to domain types

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(71-01): complete EIA energy capacity data pipeline plan

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(71-02): add setCapacityData() with D3 stacked area chart to RenewableEnergyPanel

- setCapacityData() renders D3 stacked area (solar yellow + wind blue) with coal decline (red)
- Chart labeled 'US Installed Capacity (EIA)' with compact inline legend
- Appends below existing gauge/sparkline/regions without replacing content
- CSS styles for capacity section, header, legend in happy-theme.css

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(71-02): wire EIA capacity data loading in App.ts loadRenewableData()

- Import fetchEnergyCapacity from renewable-energy-data service
- Call fetchEnergyCapacity() after World Bank gauge data, pass to setCapacityData()
- Wrapped in try/catch so EIA failure does not break existing gauge

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(71-02): complete EIA capacity visualization plan

- SUMMARY.md documenting D3 stacked area chart implementation
- STATE.md updated: Phase 7.1 complete (2/2 plans), progress 100%
- ROADMAP.md updated with plan progress

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(phase-71): complete phase execution

* docs(phase-09): research sharing, TV mode & polish domain

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(09): create phase plan for sharing, TV mode & polish

* docs(phase-09): plan Sharing, TV Mode & Polish

3 plans in 2 waves covering share cards (Canvas 2D renderer),
TV/ambient mode (fullscreen panel cycling + CSS particles),
and celebration animations (canvas-confetti milestones).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(09-01): create Canvas 2D renderer for happy share cards

- 1080x1080 branded PNG with warm gradient per category
- Category badge, headline word-wrap, source, date, HappyMonitor branding
- shareHappyCard() with Web Share API -> clipboard -> download fallback
- wrapText() helper for Canvas 2D manual line breaking

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(09-02): create TvModeController and TV mode CSS

- TvModeController class manages fullscreen, panel cycling with configurable 30s-2min interval
- CSS [data-tv-mode] attribute drives larger typography, hidden interactive elements, smooth panel transitions
- Ambient floating particles (CSS-only, opacity 0.04) with reduced motion support
- TV exit button appears on hover, hidden by default outside TV mode

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(09-02): wire TV mode into App.ts header and lifecycle

- TV mode button with monitor icon in happy variant header
- TV exit button at page level, visible on hover in TV mode
- Shift+T keyboard shortcut toggles TV mode
- TvModeController instantiated lazily on first toggle
- Proper cleanup in destroy() method

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(09-01): add share button to positive news cards with handler

- Share button (SVG upload icon) appears on card hover, top-right
- Delegated click handler prevents link navigation, calls shareHappyCard
- Brief .shared visual feedback (green, scale) for 1.5s on click
- Dark mode support for share button background
- Fix: tv-mode.ts panelKeys index guard (pre-existing build blocker)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(09-02): complete TV Mode plan

- SUMMARY.md with task commits, deviations, decisions
- STATE.md updated: position, metrics, decisions, session
- ROADMAP.md updated: phase 09 progress (2/3 plans)
- REQUIREMENTS.md updated: TV-01, TV-02, TV-03 marked complete

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(09-01): complete positive news share cards plan

- SUMMARY.md with Canvas 2D renderer and share button accomplishments
- STATE.md updated with decisions and session continuity
- ROADMAP.md progress updated (2/3 plans in phase 09)
- REQUIREMENTS.md: SHARE-01, SHARE-02, SHARE-03 marked complete

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(09-03): add celebration service with canvas-confetti

- Install canvas-confetti + @types/canvas-confetti
- Create src/services/celebration.ts with warm nature-inspired palette
- Session-level dedup (Set<string>) prevents repeat celebrations
- Respects prefers-reduced-motion media query
- Milestone detection for species recovery + renewable energy records
- Moderate particle counts (40-80) for "warm, not birthday party" feel

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(09-03): wire milestone celebrations into App.ts data pipelines

- Import checkMilestones in App.ts
- Call checkMilestones after species data loads with recovery statuses
- Call checkMilestones after renewable energy data loads with global percentage
- All celebration calls gated behind SITE_VARIANT === 'happy'
- Placed after panel setData() so data is visible before confetti fires

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(09-03): complete celebration animations plan

- 09-03-SUMMARY.md with execution results
- STATE.md updated: phase 09 complete, 26 plans total, 100% progress
- ROADMAP.md updated with phase 09 completion
- REQUIREMENTS.md: THEME-06 marked complete

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(phase-09): complete phase execution

* fix(happy): remove natural events layer from happy variant

Natural events (earthquakes, volcanoes, storms) were leaking into the
happy variant through stale localStorage and the layer toggle UI. Force
all non-happy layers off regardless of localStorage state, and remove
the natural events toggle from both DeckGL and SVG map layer configs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(phase-7.1): complete phase execution — mark all phases done

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(v1): complete milestone audit — 49/49 requirements satisfied

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(happy): close audit tech debt — map layer defaults, theme-color meta

- Enable speciesRecovery and renewableInstallations layers by default
  in HAPPY_MAP_LAYERS (panels.ts + happy.ts) so MAP-04/MAP-05 are
  visible on first load
- Use happy-specific theme-color meta values (#FAFAF5 light, #1A2332
  dark) in setTheme() and applyStoredTheme() instead of generic colors

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: add checkpoint for giving integration handoff

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(giving): integrate Global Giving Activity Index from PR #254

Cherry-pick the giving feature that was left behind when PR #255
batch-merged without including #254's proto/handler/panel files.

Adds:
- Proto definitions (GivingService, GivingSummary, PlatformGiving, etc.)
- Server handler: GoFundMe/GlobalGiving/JustGiving/crypto/OECD aggregation
- Client service with circuit breaker
- GivingPanel with tabs (platforms, categories, crypto, institutional)
- Full wiring: API routes, vite dev server, data freshness, panel config
- Happy variant panel config entry

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(giving): move panel init and data fetch out of full-variant-only blocks

The GivingPanel was instantiated inside `if (SITE_VARIANT === 'full')` and
the data fetch was inside `loadIntelligenceSignals()` (also full-only).
Moved both to variant-agnostic scope so the panel works on happy variant.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(giving): bypass debounced setContent so tab buttons are clickable

Panel.setContent() is debounced (150ms), so event listeners attached
immediately after it were binding to DOM elements that got replaced by
the deferred innerHTML write. Write directly to this.content.innerHTML
like other interactive panels do.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: remove .planning/ from repo and gitignore it

Planning files served their purpose during happy monitor development.
They remain on disk for reference but no longer tracked.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: merge new panels into saved panelSettings so they aren't hidden

When panelSettings is loaded from localStorage, any panels added since
the user last saved settings would be missing from the config. The
applyPanelSettings loop wouldn't touch them, but without a config entry
they also wouldn't appear in the settings toggle UI correctly.

Now merges DEFAULT_PANELS entries into loaded settings for any keys
that don't exist yet, so new panels are visible by default.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: giving data baselines, theme toggle persistence, and client caching

- Replace broken GoFundMe (301→404) and GlobalGiving (401) API calls
  with hardcoded baselines from published annual reports. Activity index
  rises from 42 to 56 as all 3 platforms now report non-zero volumes.
- Fix happy variant theme toggle not persisting across page reloads:
  applyStoredTheme() couldn't distinguish "no preference" from "user
  chose dark" — both returned DEFAULT_THEME. Now checks raw localStorage.
- Fix inline script in index.html not setting data-theme="dark" for
  happy variant, causing CSS :root[data-variant="happy"] (light) to
  win over :root[data-variant="happy"][data-theme="dark"].
- Add client-side caching to giving service: persistCache on circuit
  breaker, 30min in-memory TTL, and request deduplication.
- Add Playwright E2E tests for theme toggle (8 tests, all passing).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* perf: add persistent cache to all 29 circuit breakers across 19 services

Enable persistCache and set appropriate cacheTtlMs on every circuit
breaker that lacked them. Data survives page reloads via IndexedDB
fallback and reduces redundant API calls on navigation.

TTLs matched to data freshness: 5min for real-time feeds (weather,
earthquakes, wildfires, aviation), 10min for event data (conflict,
cyber, unrest, climate, research), 15-30min for slow-moving data
(economic indicators, energy capacity, population exposure).

Market quotes breaker intentionally left at cacheTtlMs: 0 (real-time).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: expand map labels progressively as user zooms in

Labels now show more text at higher zoom levels instead of always
truncating at 30 chars. Zoom <3: 20 chars, <5: 35, <7: 60, 7+: full.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: keep 30-char baseline for map labels, expand to full text at zoom 6+

Previous change was too aggressive with low-zoom truncation (20 chars).
Now keeps original 30-char limit at global view, progressively expands
to 50/80/200 chars as user zooms in. Also scales font size with zoom.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Revert "fix: keep 30-char baseline for map labels, expand to full text at zoom 6+"

This reverts commit 33b8a8accc2d48acd45f3dcea97a083b8bcebbf0.

* Revert "feat: expand map labels progressively as user zooms in"

This reverts commit 285f91fe471925ca445243ae5d8ac37723f2eda7.

* perf: stale-while-revalidate for instant page load

Circuit breaker now returns stale cached data immediately and refreshes
in the background, instead of blocking on API calls when cache exceeds
TTL. Also persists happyAllItems to IndexedDB so Hero, Digest, and
Breakthroughs panels render instantly from cache on page reload.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address PR #229 review — 4 issues from koala

1. P1: Fix duplicate event listeners in PositiveNewsFeedPanel.renderCards()
   — remove listener before re-adding to prevent stacking on re-renders

2. P1: Fix TV mode cycling hidden panels causing blank screen
   — filter out user-disabled panels from cycle list, rebuild keys on toggle

3. P2: Fix positive classifier false positives for short keywords
   — "ai" and "art" now use space-delimited matching to avoid substring hits
     (e.g. "aid", "rain", "said", "start", "part")

4. P3: Fix CSP blocking Google Fonts stylesheet for Nunito
   — add https://fonts.googleapis.com to style-src directive

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: decompose App.ts into focused modules under src/app/

Break the 4,597-line monolithic App class into 7 focused modules plus a
~460-line thin orchestrator. Each module implements the AppModule lifecycle
(init/destroy) and communicates via a shared AppContext state object with
narrow callback interfaces — no circular dependencies.

Modules extracted:
- app-context.ts: shared state types (AppContext, AppModule, etc.)
- desktop-updater.ts: desktop version checking + update badge
- country-intel.ts: country briefs, timeline, CII signals
- search-manager.ts: search modal, result routing, index updates
- refresh-scheduler.ts: periodic data refresh with jitter/backoff
- panel-layout.ts: panel creation, grid layout, drag-drop
- data-loader.ts: all 36 data loading methods
- event-handlers.ts: DOM events, shortcuts, idle detection, URL sync

Verified: tsc --noEmit (zero errors), all 3 variant builds pass
(full, tech, finance), runtime smoke test confirms no regressions.

* fix: resolve test failures and missing CSS token from PR review

1. flushStaleRefreshes test now reads from refresh-scheduler.ts (moved
   during App.ts modularization)
2. e2e runtime tests updated to import DesktopUpdater and DataLoaderManager
   instead of App.prototype for resolveUpdateDownloadUrl and loadMarkets
3. Add --semantic-positive CSS variable to main.css and happy-theme.css
   (both light and dark variants)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: hide happy variant button from other variants

The button is only visible when already on the happy variant. This
allows merging the modularized App.ts without exposing the unfinished
happy layout to users — layout work continues in a follow-up PR.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Elie Habib <elie.habib@gmail.com>
2026-02-25 10:05:26 +04:00
toasterbook88
b667b189ff Build/runtime hardening and dependency security updates (#286)
* Simplify RSS freshness update to static import

* Refine vendor chunking for map stack in Vite build

* Patch transitive XML parser vulnerability via npm override

* Shim Node child_process for browser bundle warnings

* Filter known onnxruntime eval warning in Vite build

* test: add loaders XML/WMS parser regression coverage

* chore: align fast-xml-parser override with merged dependency set

---------

Co-authored-by: Elie Habib <elie.habib@gmail.com>
2026-02-24 08:21:03 +00:00
Elie Habib
740e3513ef Fix deployment build: make WORLDMONITOR_API_KEY optional (#217) 2026-02-21 13:55:21 +00:00
Elie Habib
1922a781cd Add PostHog analytics with privacy-first design (#216) 2026-02-21 13:37:36 +00:00
Elie Habib
a388afe400 feat: API key gating for desktop cloud fallback + registration (#215)
* feat: API key gating for desktop cloud fallback + registration system

Gate desktop cloud fallback behind WORLDMONITOR_API_KEY — desktop users
need a valid key for cloud access, otherwise operate local-only (sidecar).
Add email registration system via Convex DB for future key distribution.

Client-side: installRuntimeFetchPatch() checks key presence before
allowing cloud fallback, with secretsReady promise + 2s timeout.
Server-side: origin-aware validation in sebuf gateway — desktop origins
require key, web origins pass through.

- Add WORLDMONITOR_API_KEY to 3-place secret system (Rust, TS, sidecar)
- New "World Monitor" settings tab with key input + registration form
- New api/_api-key.js server-side validation (origin-aware)
- New api/register-interest.js edge function with rate limiting
- Convex DB schema + mutation for email registration storage
- CORS headers updated for X-WorldMonitor-Key + Authorization
- E2E tests for key gate (blocked without key, allowed with key)
- Deployment docs (API_KEY_DEPLOYMENT.md) + updated desktop config docs

* fix: harden worldmonitor key + registration input handling

* fix: show invalid WorldMonitor API key status

* fix: simplify key validation, trim registration checks, add env example vars

- Inline getValidKeys() in _api-key.js
- Remove redundant type checks in register-interest.js
- Simplify WorldMonitorTab status to present/missing
- Add WORLDMONITOR_VALID_KEYS and CONVEX_URL to .env.example

* feat(sidecar): integrate proto gateway bundle into desktop build

The sidecar's buildRouteTable() only discovers .js files, so the proto
gateway at api/[domain]/v1/[rpc].ts was invisible — all 45 sebuf RPCs
returned 404 in the desktop app. Wire the existing build script into
Tauri's build commands and add esbuild as an explicit devDependency.
2026-02-21 10:36:23 +00:00
Sebastien Melki
c939cc6296 Proto-first API rebuild: sebuf contracts, handlers, gateway, and generated docs (#106)
* docs: initialize sebuf integration project with codebase map

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: add project config

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: complete project research

* docs: define v1 requirements (34 requirements, 8 categories)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: create roadmap (8 phases)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(01): capture phase context

* docs(state): record phase 1 context session

* docs(01): research phase domain - buf toolchain, sebuf codegen, proto patterns

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(01-proto-foundation): create phase plan

* chore(01-01): configure buf toolchain with buf.yaml, buf.gen.yaml, buf.lock

- buf.yaml v2 with STANDARD+COMMENTS lint, FILE+PACKAGE+WIRE_JSON breaking, deps on protovalidate and sebuf
- buf.gen.yaml configures protoc-gen-ts-client, protoc-gen-ts-server, protoc-gen-openapiv3 plugins
- buf.lock generated with resolved dependency versions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(01-01): add shared core proto type definitions

- geo.proto: GeoCoordinates with lat/lng validation, BoundingBox for spatial queries
- time.proto: TimeRange with google.protobuf.Timestamp start/end
- pagination.proto: cursor-based PaginationRequest (1-100 page_size) and PaginationResponse
- i18n.proto: LocalizableString for pre-localized upstream API strings
- identifiers.proto: typed ID wrappers (HotspotID, EventID, ProviderID) for cross-domain refs
- general_error.proto: GeneralError with RateLimited, UpstreamDown, GeoBlocked, MaintenanceMode

All files pass buf lint (STANDARD+COMMENTS) and buf build with zero errors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(01-01): complete buf toolchain and core proto types plan

- SUMMARY.md documents 2 tasks, 9 files created, 2 deviations auto-fixed
- STATE.md updated: plan 1/2 in phase 1, decisions recorded
- ROADMAP.md updated: phase 01 in progress (1/2 plans)
- REQUIREMENTS.md updated: PROTO-01, PROTO-02, PROTO-03 marked complete

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(01-01): use int64 epoch millis instead of google.protobuf.Timestamp

User preference: all time fields use int64 (Unix epoch milliseconds)
instead of google.protobuf.Timestamp for simpler serialization and
JS interop. Applied to TimeRange and MaintenanceMode.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(01-02): create test domain proto files with core type imports

- Add test_item.proto with GeoCoordinates import and int64 timestamps
- Add get_test_items.proto with TimeRange and Pagination imports
- Add service.proto with HTTP annotations for TestService
- All proto files pass buf lint and buf build

* feat(01-02): run buf generate and create Makefile for code generation pipeline

- Add Makefile with generate, lint, clean, install, check, format, breaking targets
- Update buf.gen.yaml with managed mode and paths=source_relative for correct output paths
- Generate TypeScript client (TestServiceClient class) at src/generated/client/
- Generate TypeScript server (TestServiceHandler interface) at src/generated/server/
- Generate OpenAPI 3.1.0 specs (JSON + YAML) at docs/api/
- Core type imports (GeoCoordinates, TimeRange, Pagination) flow through to generated output

* docs(01-02): complete test domain code generation pipeline plan

- Create 01-02-SUMMARY.md with pipeline validation results
- Update STATE.md: phase 1 complete, 2/2 plans done, new decisions recorded
- Update ROADMAP.md: phase 1 marked complete (2/2)
- Update REQUIREMENTS.md: mark PROTO-04 and PROTO-05 complete

* docs(phase-01): complete phase execution and verification

* test(01): complete UAT - 6 passed, 0 issues

* feat(2A): define all 17 domain proto packages with generated clients, servers, and OpenAPI specs

Remove test domain protos (Phase 1 scaffolding). Add core enhancements
(severity.proto, country.proto, expanded identifiers.proto). Define all
17 domain services: seismology, wildfire, climate, conflict, displacement,
unrest, military, aviation, maritime, cyber, market, prediction, economic,
news, research, infrastructure, intelligence. 79 proto files producing
34 TypeScript files and 34 OpenAPI specs. buf lint clean, tsc clean.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(2B): add server runtime phase context and handoff checkpoint

Prepare Phase 2B with full context file covering deliverables,
key reference files, generated code patterns, and constraints.
Update STATE.md with resume pointer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(2B): research phase domain

* docs(2B): create phase plan

* feat(02-01): add shared server infrastructure (router, CORS, error mapper)

- router.ts: Map-based route matcher from RouteDescriptor[] arrays
- cors.ts: TypeScript port of api/_cors.js with POST/OPTIONS methods
- error-mapper.ts: onError callback handling ApiError, network, and unknown errors

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(02-01): implement seismology handler as first end-to-end proof

- Implements SeismologyServiceHandler from generated server types
- Fetches USGS M4.5+ earthquake GeoJSON feed and transforms to proto-shaped Earthquake[]
- Maps all fields: id, place, magnitude, depthKm, location, occurredAt (String), sourceUrl

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(02-01): complete server infrastructure plan

- SUMMARY.md with task commits, decisions, and self-check
- STATE.md updated: position, decisions, session info
- REQUIREMENTS.md: SERVER-01, SERVER-02, SERVER-06 marked complete

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(02-02): create Vercel catch-all gateway, tsconfig.api.json, and typecheck:api script

- api/[[...path]].ts mounts seismology routes via catch-all with CORS on every response path
- tsconfig.api.json extends base config without vite/client types for edge runtime
- package.json adds typecheck:api script

* feat(02-02): add Vite dev server plugin for sebuf API routes

- sebufApiPlugin() intercepts /api/{domain}/v1/* in dev mode
- Uses dynamic imports to lazily load handler modules inside configureServer
- Converts Connect IncomingMessage to Web Standard Request
- CORS headers applied to all plugin responses (200, 204, 403, 404)
- Falls through to existing proxy rules for non-sebuf /api/* paths

* docs(02-02): complete gateway integration plan

- SUMMARY.md documenting catch-all gateway + Vite plugin implementation
- STATE.md updated: Phase 2B complete, decisions recorded
- ROADMAP.md updated: Phase 02 marked complete (2/2 plans)
- REQUIREMENTS.md: SERVER-03, SERVER-04, SERVER-05 marked complete

* docs(02-server-runtime): create gap closure plan for SERVER-05 Tauri sidecar

* feat(02-03): add esbuild compilation step for sebuf sidecar gateway bundle

- Create scripts/build-sidecar-sebuf.mjs that bundles api/[[...path]].ts into a single ESM .js file
- Add build:sidecar-sebuf npm script and chain it into the main build command
- Install esbuild as explicit devDependency
- Gitignore the compiled api/[[...path]].js build artifact

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(02-03): verify sidecar discovery and annotate SERVER-05 gap closure

- Confirm compiled bundle handler returns status 200 for POST requests
- Add gap closure note to SERVER-05 in REQUIREMENTS.md
- Verify typecheck:api and full build pipeline pass without regressions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(02-03): complete sidecar sebuf bundle plan

- Create 02-03-SUMMARY.md documenting esbuild bundle compilation
- Update STATE.md with plan 03 position, decisions, and metrics
- Update ROADMAP.md plan progress (3/3 plans complete)
- Annotate SERVER-05 gap closure in REQUIREMENTS.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(phase-02): complete phase execution

* docs(2C): capture seismology migration phase context

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(state): record phase 2C context session

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(2C): research seismology migration phase

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(2C): create seismology migration phase plan

* feat(2C-01): annotate all int64 time fields with INT64_ENCODING_NUMBER

- Vendor sebuf/http/annotations.proto locally with Int64Encoding extension (50010)
- Remove buf.build/sebmelki/sebuf BSR dep, use local vendored proto instead
- Add INT64_ENCODING_NUMBER annotation to 34 time fields across 20 proto files
- Regenerate all TypeScript client and server code (time fields now `number` not `string`)
- Fix seismology handler: occurredAt returns number directly (no String() wrapper)
- All non-time int64 fields (displacement counts, population) left as string
- buf lint, buf generate, tsc, and sidecar build all pass with zero errors

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(2C-01): complete INT64_ENCODING_NUMBER plan

- Create 2C-01-SUMMARY.md with execution results and deviations
- Update STATE.md: plan 01 complete, int64 blocker resolved, new decisions
- Update ROADMAP.md: mark 2C-01 plan complete
- Update REQUIREMENTS.md: mark CLIENT-01 complete

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(lint): exclude .planning/ from markdownlint

GSD planning docs use formatting that triggers MD032 -- these are
machine-generated and not user-facing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(2C-02): rewrite earthquake adapter to use SeismologyServiceClient and adapt all consumers to proto types

- Replace legacy fetch/circuit-breaker adapter with port/adapter wrapping SeismologyServiceClient
- Update 7 consuming files to import Earthquake from @/services/earthquakes (the port)
- Adapt all field accesses: lat/lon -> location?.latitude/longitude, depth -> depthKm, time -> occurredAt, url -> sourceUrl
- Remove unused filterByTime from Map.ts (only called for earthquakes, replaced with inline filter)
- Update e2e test data to proto Earthquake shape

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore(2C-02): delete legacy earthquake endpoint, remove Vite proxy, clean API_URLS config

- Delete api/earthquakes.js (legacy Vercel edge function proxying USGS)
- Remove /api/earthquake Vite dev proxy (sebufApiPlugin handles seismology now)
- Remove API_URLS.earthquakes entry from base config (no longer referenced)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(2C-02): complete seismology client wiring plan

- Create 2C-02-SUMMARY.md with execution results
- Update STATE.md: phase 2C complete, decisions, metrics
- Update ROADMAP.md: mark 2C-02 and phase 2C complete
- Mark requirements CLIENT-02, CLIENT-04, CLEAN-01, CLEAN-02 complete

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(phase-2C): complete phase execution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(2D): create wildfire migration phase plan

* feat(2D-01): enhance FireDetection proto and implement wildfire handler

- Add region (field 8) and day_night (field 9) to FireDetection proto
- Regenerate TypeScript client and server types
- Implement WildfireServiceHandler with NASA FIRMS CSV proxy
- Fetch all 9 monitored regions in parallel via Promise.allSettled
- Graceful degradation to empty list when API key is missing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(2D-01): wire wildfire routes into gateway and rebuild sidecar

- Import createWildfireServiceRoutes and wildfireHandler in catch-all
- Mount wildfire routes alongside seismology in allRoutes array
- Rebuild sidecar-sebuf bundle with wildfire endpoint included

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(2D-01): complete wildfire handler plan

- Create 2D-01-SUMMARY.md with execution results
- Update STATE.md position to 2D plan 01 complete
- Update ROADMAP.md with 2D progress (1/2 plans)
- Mark DOMAIN-01 requirement complete

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(2D-02): create wildfires service module and rewire all consumers

- Add src/services/wildfires/index.ts with fetchAllFires, computeRegionStats, flattenFires, toMapFires
- Rewire App.ts to import from @/services/wildfires with proto field mappings
- Rewire SatelliteFiresPanel.ts to import FireRegionStats from @/services/wildfires
- Update signal-aggregator.ts source comment

* chore(2D-02): delete legacy wildfire endpoint and service module

- Remove api/firms-fires.js (replaced by api/server/worldmonitor/wildfire/v1/handler.ts)
- Remove src/services/firms-satellite.ts (replaced by src/services/wildfires/index.ts)
- Zero dangling references confirmed
- Full build passes (tsc, vite, sidecar)

* docs(2D-02): complete wildfire consumer wiring plan

- Create 2D-02-SUMMARY.md with execution results
- Update STATE.md: phase 2D complete, progress ~52%
- Update ROADMAP.md: phase 2D plan progress

* docs(phase-2D): complete phase execution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(phase-2E): research climate migration domain

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(2E): create phase plan

* feat(2E-01): implement climate handler with 15-zone monitoring and baseline comparison

- Create ClimateServiceHandler with 15 hardcoded monitored zones matching legacy
- Parallel fetch from Open-Meteo Archive API via Promise.allSettled
- 30-day baseline comparison: last 7 days vs preceding baseline
- Null filtering with paired data points, minimum 14-point threshold
- Severity classification (normal/moderate/extreme) and type (warm/cold/wet/dry/mixed)
- 1-decimal rounding for tempDelta and precipDelta
- Proto ClimateAnomaly mapping with GeoCoordinates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(2E-01): wire climate routes into gateway and rebuild sidecar

- Import createClimateServiceRoutes and climateHandler in catch-all gateway
- Mount climate routes alongside seismology and wildfire
- Rebuild sidecar-sebuf bundle with climate routes included

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(2E-01): complete climate handler plan

- Create 2E-01-SUMMARY.md with execution results
- Update STATE.md: position to 2E plan 01, add decisions
- Update ROADMAP.md: mark 2E-01 complete, update progress table

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(2E-02): rewrite climate service module and rewire all consumers

- Replace src/services/climate.ts with src/services/climate/index.ts directory module
- Port/adapter pattern: ClimateServiceClient maps proto shapes to legacy consumer shapes
- Rewire ClimateAnomalyPanel, DeckGLMap, MapContainer, country-instability, conflict-impact
- All 6 consumers import ClimateAnomaly from @/services/climate instead of @/types
- Drop dead getSeverityColor function, keep getSeverityIcon and formatDelta
- Fix minSeverity required param in listClimateAnomalies call

* chore(2E-02): delete legacy climate endpoint and remove dead types

- Delete api/climate-anomalies.js (replaced by sebuf climate handler)
- Remove ClimateAnomaly and AnomalySeverity from src/types/index.ts
- Full build passes with zero errors

* docs(2E-02): complete climate client wiring plan

- Create 2E-02-SUMMARY.md with execution results
- Update STATE.md: phase 2E complete, decisions, session continuity
- Update ROADMAP.md: phase 2E progress

* docs(phase-2E): complete phase execution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(2F): research prediction migration domain

* docs(2F): create prediction migration phase plans

* feat(2F-01): implement prediction handler with Gamma API proxy

- PredictionServiceHandler proxying Gamma API with 8s timeout
- Maps events/markets to proto PredictionMarket with 0-1 yesPrice scale
- Graceful degradation: returns empty markets on any failure (Cloudflare expected)
- Supports category-based events endpoint and default markets endpoint

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(2F-01): wire prediction routes into gateway

- Import createPredictionServiceRoutes and predictionHandler
- Mount prediction routes in allRoutes alongside seismology, wildfire, climate
- Sidecar bundle rebuilt successfully (21.2 KB)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(2F-01): complete prediction handler plan

- SUMMARY.md with handler implementation details and deviation log
- STATE.md updated to 2F in-progress position with decisions
- ROADMAP.md updated to 1/2 plans complete for phase 2F
- REQUIREMENTS.md marked DOMAIN-02 complete

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(2F-02): create prediction service module and rewire all consumers

- Create src/services/prediction/index.ts preserving all business logic from polymarket.ts
- Replace strategy 4 (Vercel edge) with PredictionServiceClient in polyFetch
- Update barrel export from polymarket to prediction in services/index.ts
- Rewire 7 consumers to import PredictionMarket from @/services/prediction
- Fix 3 yesPrice bugs: CountryIntelModal (*100), App.ts search (*100), App.ts snapshot (1-y)
- Drop dead code getPolymarketStatus()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore(2F-02): delete legacy endpoint and remove dead types

- Delete api/polymarket.js (replaced by sebuf handler)
- Delete src/services/polymarket.ts (replaced by src/services/prediction/index.ts)
- Remove PredictionMarket interface from src/types/index.ts (now in prediction module)
- Type check and sidecar build both pass

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(2F-02): complete prediction consumer wiring plan

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(phase-2F): complete phase execution

* docs(phase-2F): fix roadmap plan counts and completion status

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(2G): research displacement migration phase

* docs(2G): create displacement migration phase plans

* feat(2G-01): implement displacement handler with UNHCR API pagination and aggregation

- 40-entry COUNTRY_CENTROIDS map for geographic coordinates
- UNHCR Population API pagination (10,000/page, 25-page guard)
- Year fallback: current year to current-2 until data found
- Per-country origin + asylum aggregation with unified merge
- Global totals computation across all raw records
- Flow corridor building sorted by refugees, capped by flowLimit
- All int64 fields returned as String() per proto types
- Graceful empty response on any failure

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(2G-01): wire displacement routes into gateway and rebuild sidecar

- Import createDisplacementServiceRoutes and displacementHandler
- Mount displacement routes alongside seismology, wildfire, climate, prediction
- Sidecar bundle rebuilt with displacement included (31.0 KB)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(2G-01): complete displacement handler plan

- SUMMARY.md with execution metrics and decisions
- STATE.md updated to 2G phase position
- ROADMAP.md updated with 2G-01 plan progress

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(2G-02): create displacement service module and rewire all consumers

- Create src/services/displacement/index.ts as port/adapter using DisplacementServiceClient
- Map proto int64 strings to numbers and GeoCoordinates to flat lat/lon
- Preserve circuit breaker, presentation helpers (getDisplacementColor, formatPopulation, etc.)
- Rewire App.ts, DisplacementPanel, MapContainer, DeckGLMap, conflict-impact, country-instability
- Delete legacy src/services/unhcr.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore(2G-02): delete legacy endpoint and remove dead displacement types

- Delete api/unhcr-population.js (replaced by displacement handler from 2G-01)
- Remove DisplacementFlow, CountryDisplacement, UnhcrSummary from src/types/index.ts
- All consumers now import from @/services/displacement
- Sidecar rebuild, tsc, and full Vite build pass clean

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(2G-02): complete displacement consumer wiring plan

- SUMMARY.md with 2 task commits, decisions, deviation documentation
- STATE.md updated: phase 2G complete, 02/02 plans done
- ROADMAP.md updated with plan progress

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(phase-2G): complete phase execution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(2H): research aviation migration phase

* docs(2H): create aviation migration phase plans

* feat(2H-01): implement aviation handler with FAA XML parsing and simulated delays

- Install fast-xml-parser for server-side XML parsing (edge-compatible)
- Create AviationServiceHandler with FAA NASSTATUS XML fetch and parse
- Enrich US airports with MONITORED_AIRPORTS metadata (lat, lon, name, icao)
- Generate simulated delays for non-US airports with rush-hour weighting
- Map short-form strings to proto enums (FlightDelayType, FlightDelaySeverity, etc.)
- Wrap flat lat/lon into GeoCoordinates for proto response
- Graceful empty alerts on any upstream failure

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(2H-01): wire aviation routes into gateway and rebuild sidecar

- Mount createAviationServiceRoutes in catch-all gateway alongside 5 existing domains
- Import aviationHandler for route wiring
- Rebuild sidecar-sebuf bundle with aviation routes included

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(2H-01): complete aviation handler plan

- Create 2H-01-SUMMARY.md with execution results
- Update STATE.md position to 2H-01 with aviation decisions
- Update ROADMAP.md progress for phase 2H (1/2 plans)
- Mark DOMAIN-08 requirement as complete

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(2H-02): create aviation service module and rewire all consumers

- Create src/services/aviation/index.ts as port/adapter wrapping AviationServiceClient
- Map proto enum strings to short-form (severity, delayType, region, source)
- Unwrap GeoCoordinates to flat lat/lon, convert epoch-ms updatedAt to Date
- Preserve circuit breaker with identical name string
- Rewire Map, DeckGLMap, MapContainer, MapPopup, map-harness to import from @/services/aviation
- Update barrel export: flights -> aviation
- Delete legacy src/services/flights.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore(2H-02): delete legacy endpoint and remove dead aviation types

- Delete api/faa-status.js (replaced by aviation handler in 2H-01)
- Remove FlightDelaySource, FlightDelaySeverity, FlightDelayType, AirportRegion, AirportDelayAlert from src/types/index.ts
- Preserve MonitoredAirport with inlined region type union
- Full build (tsc + vite + sidecar) passes with zero errors

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(2H-02): complete aviation consumer wiring plan

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(phase-2H): complete phase execution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(2I): research phase domain

* docs(2I): create phase plan

* feat(2I-01): implement ResearchServiceHandler with 3 RPCs

- arXiv XML parsing with fast-xml-parser (ignoreAttributes: false for attributes)
- GitHub trending repos with primary + fallback API URLs
- Hacker News Firebase API with 2-step fetch and bounded concurrency (10)
- All RPCs return empty arrays on failure (graceful degradation)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(2I-01): mount research routes in gateway and rebuild sidecar

- Import createResearchServiceRoutes and researchHandler in catch-all gateway
- Add research routes to allRoutes array (after aviation)
- Sidecar bundle rebuilt (116.6 KB)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(2I-01): complete research handler plan

- SUMMARY.md with self-check passed
- STATE.md updated to phase 2I, plan 01 of 02
- ROADMAP.md updated with plan 2I-01 complete
- REQUIREMENTS.md: DOMAIN-05 marked complete

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(2I-02): create research service module and delete legacy code

- Add src/services/research/index.ts with fetchArxivPapers, fetchTrendingRepos, fetchHackernewsItems backed by ResearchServiceClient
- Re-export proto types ArxivPaper, GithubRepo, HackernewsItem (no enum mapping needed)
- Circuit breakers wrap all 3 client calls with empty-array fallback
- Delete legacy API endpoints: api/arxiv.js, api/github-trending.js, api/hackernews.js
- Delete legacy service files: src/services/arxiv.ts, src/services/github-trending.ts, src/services/hackernews.ts
- Remove arxiv, githubTrending, hackernews entries from API_URLS and REFRESH_INTERVALS in config

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(2I-02): complete research consumer wiring plan

- SUMMARY.md documenting service module creation and 6 legacy file deletions
- STATE.md updated: phase 2I complete, decisions recorded
- ROADMAP.md updated: phase 2I marked complete

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(phase-2I): complete phase execution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(2J): complete unrest migration research

* docs(2J): create unrest migration phase plans

* feat(2J-01): implement UnrestServiceHandler with ACLED + GDELT dual-fetch

- Create handler with listUnrestEvents RPC proxying ACLED API and GDELT GEO API
- ACLED fetch uses Bearer auth from ACLED_ACCESS_TOKEN env var, returns empty on missing token
- GDELT fetch returns GeoJSON protest events with no auth required
- Deduplication uses 0.5-degree grid + date key, preferring ACLED over GDELT on collision
- Severity classification and event type mapping ported from legacy protests.ts
- Sort by severity (high first) then recency (newest first)
- Graceful degradation: returns empty events on any upstream failure

* feat(2J-01): mount unrest routes in gateway and rebuild sidecar

- Import createUnrestServiceRoutes and unrestHandler in catch-all gateway
- Add unrest service routes to allRoutes array
- Sidecar bundle rebuilt to include unrest endpoint
- RPC routable at POST /api/unrest/v1/list-unrest-events

* docs(2J-01): complete unrest handler plan

- Create 2J-01-SUMMARY.md with execution results and self-check
- Update STATE.md with phase 2J position, decisions, session continuity
- Update ROADMAP.md with plan 01 completion status

* feat(2J-02): create unrest service module with proto-to-legacy type mapping

- Full adapter maps proto UnrestEvent to legacy SocialUnrestEvent shape
- 4 enum mappers: severity, eventType, sourceType, confidence
- fetchProtestEvents returns ProtestData with events, byCountry, highSeverityCount, sources
- getProtestStatus infers ACLED configuration from response event sources
- Circuit breaker wraps client call with empty fallback

* feat(2J-02): update services barrel, remove vite proxies, delete legacy files

- Services barrel: protests -> unrest re-export
- Vite proxy entries removed: /api/acled, /api/gdelt-geo
- Legacy files deleted: api/acled.js, api/gdelt-geo.js, src/services/protests.ts
- Preserved: api/acled-conflict.js (conflict domain), SocialUnrestEvent type

* docs(2J-02): complete unrest service module plan

- SUMMARY.md created with full adapter pattern documentation
- STATE.md updated: 2J-02 complete, decisions recorded
- ROADMAP.md updated: Phase 2J marked complete

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(phase-2J): complete phase execution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(phase-2K): complete conflict migration research

* docs(2K): create phase plan

* feat(2K-01): implement ConflictServiceHandler with 3 RPCs

- listAcledEvents proxies ACLED API for battles/explosions/violence with Bearer auth
- listUcdpEvents discovers UCDP GED API version dynamically, fetches backward with 365-day trailing window
- getHumanitarianSummary proxies HAPI API with ISO-2 to ISO-3 country mapping
- All RPCs have graceful degradation returning empty on failure

* feat(2K-01): mount conflict routes in gateway and rebuild sidecar

- Add createConflictServiceRoutes and conflictHandler imports to catch-all gateway
- Spread conflict routes into allRoutes array (3 RPC endpoints)
- Rebuild sidecar bundle with conflict endpoints included

* docs(2K-01): complete conflict handler plan

- Create 2K-01-SUMMARY.md with execution details and self-check
- Update STATE.md: position to 2K-01, add 5 decisions
- Update ROADMAP.md: mark 2K-01 complete (1/2 plans done)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(2K-02): create conflict service module with 4-shape proto-to-legacy type mapping

- Port/adapter mapping AcledConflictEvent -> ConflictEvent, UcdpViolenceEvent -> UcdpGeoEvent, HumanitarianCountrySummary -> HapiConflictSummary
- UCDP classifications derived heuristically from GED events (deaths/events thresholds -> war/minor/none)
- deduplicateAgainstAcled ported exactly with haversine + date + fatality matching
- 3 circuit breakers for 3 RPCs, exports 5 functions + 2 group helpers + all legacy types

* feat(2K-02): rewire consumer imports and delete 9 legacy conflict files

- App.ts consolidated from 4 direct imports to single @/services/conflict import
- country-instability.ts consolidated from 3 type imports to single ./conflict import
- Deleted 4 API endpoints: acled-conflict.js, ucdp-events.js, ucdp.js, hapi.js
- Deleted 4 service files: conflicts.ts, ucdp.ts, ucdp-events.ts, hapi.ts
- Deleted 1 dead code file: conflict-impact.ts
- UcdpGeoEvent preserved in src/types/index.ts (scope guard for map components)

* docs(2K-02): complete conflict service module plan

- SUMMARY.md with 4-shape proto adapter, consumer consolidation, 9 legacy deletions
- STATE.md updated: Phase 2K complete (2/2 plans), progress ~100%
- ROADMAP.md updated: Phase 2K marked complete

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(phase-2K): complete conflict migration phase execution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(2L): research maritime migration phase domain

* docs(2L): create maritime migration phase plans

* feat(2L-01): implement MaritimeServiceHandler with 2 RPCs

- getVesselSnapshot proxies WS relay with wss->https URL conversion
- Maps density/disruptions to proto shape with GeoCoordinates nesting
- Disruption type/severity mapped from lowercase to proto enums
- listNavigationalWarnings proxies NGA MSI broadcast warnings API
- NGA military date parsing (081653Z MAY 2024) to epoch ms
- Both RPCs gracefully degrade to empty on upstream failure
- No caching (client-side polling manages refresh intervals)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(2L-01): mount maritime routes in gateway and rebuild sidecar

- Import createMaritimeServiceRoutes and maritimeHandler
- Add maritime routes to allRoutes array in catch-all gateway
- Sidecar bundle rebuilt (148.0 KB) with maritime endpoints
- RPCs routable at /api/maritime/v1/get-vessel-snapshot and /api/maritime/v1/list-navigational-warnings

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(2L-01): complete maritime handler plan

- SUMMARY.md with 2 task commits documented
- STATE.md updated to 2L phase, plan 01/02 complete
- ROADMAP.md progress updated for phase 2L
- REQUIREMENTS.md: DOMAIN-06 marked complete

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(2L-02): create maritime service module with hybrid fetch and polling/callback preservation

- Port/adapter wrapping MaritimeServiceClient for proto RPC path
- Full polling/callback architecture preserved from legacy ais.ts
- Hybrid fetch: proto RPC for snapshot-only, raw WS relay for candidates
- Proto-to-legacy type mapping for AisDisruptionEvent and AisDensityZone
- Exports fetchAisSignals, initAisStream, disconnectAisStream, getAisStatus, isAisConfigured, registerAisCallback, unregisterAisCallback, AisPositionData

* feat(2L-02): rewire consumer imports and delete 3 legacy maritime files

- cable-activity.ts: fetch NGA warnings via MaritimeServiceClient.listNavigationalWarnings() with NgaWarning shape reconstruction from proto fields
- military-vessels.ts: imports updated from './ais' to './maritime'
- Services barrel: updated from './ais' to './maritime'
- desktop-readiness.ts: service/api references updated to maritime handler paths
- Deleted: api/ais-snapshot.js, api/nga-warnings.js, src/services/ais.ts
- AisDisruptionEvent/AisDensityZone/AisDisruptionType preserved in src/types/index.ts

* docs(2L-02): complete maritime service module plan

- SUMMARY.md with hybrid fetch pattern, polling/callback preservation, 3 legacy files deleted
- STATE.md updated: phase 2L complete, 5 decisions recorded
- ROADMAP.md updated: 2L plans marked complete

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: bind globalThis.fetch in all sebuf service clients

Generated sebuf clients store globalThis.fetch as a class property,
then call it as this.fetchFn(). This loses the window binding and
throws "Illegal invocation" in browsers. Pass { fetch: fetch.bind(globalThis) }
to all 11 client constructors.

Also includes vite.config.ts with all 10 migrated domain handlers
registered in the sebuf dev server plugin.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: migrate cyber + economic domains to sebuf (12/17)

Cyber (Phase 2M):
- Create handler aggregating 5 upstream sources (Feodo, URLhaus, C2Intel, OTX, AbuseIPDB)
  with dedup, GeoIP hydration, country centroid fallback
- Create service module with CyberServiceClient + circuit breaker
- Delete api/cyber-threats.js, api/cyber-threats.test.mjs, src/services/cyber-threats.ts

Economic (Phase 2N) — consolidates 3 legacy services:
- Create handler with 3 RPCs: getFredSeries (FRED API), listWorldBankIndicators
  (World Bank API), getEnergyPrices (EIA API)
- Create unified service module replacing fred.ts, oil-analytics.ts, worldbank.ts
- Preserve all exported functions/types for EconomicPanel and TechReadinessPanel
- Delete api/fred-data.js, api/worldbank.js, src/services/fred.ts,
  src/services/oil-analytics.ts, src/services/worldbank.ts

Both domains registered in vite.config.ts and api/[[...path]].ts.
TypeScript check and vite build pass cleanly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: migrate infrastructure domain to sebuf (13/17)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: migrate market domain to sebuf (14/17)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: migrate news domain to sebuf (15/17)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: migrate intelligence domain to sebuf (16/17)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: migrate military domain to sebuf (17/17)

All 17 domains now have sebuf handlers registered in the gateway.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: migrate intelligence services to sebuf client

Rewire pizzint.ts, cached-risk-scores.ts, and threat-classifier.ts
to use IntelligenceServiceClient instead of legacy /api/ fetch calls.
Handler now preserves raw threat level in subcategory field.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: migrate military theater posture to sebuf client

Rewire cached-theater-posture.ts to use MilitaryServiceClient instead
of legacy /api/theater-posture fetch. Adds theater metadata map for
proto→legacy TheaterPostureSummary adapter. UI gracefully falls back
to total counts when per-type breakdowns aren't available.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: rewire country-intel to sebuf client

Replace legacy fetch('/api/country-intel') with typed
IntelligenceServiceClient.getCountryIntelBrief() RPC call in App.ts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: migrate stablecoin-markets to sebuf (market domain)

Add ListStablecoinMarkets RPC to market service. Port CoinGecko
stablecoin peg-health logic from api/stablecoin-markets.js into
the market handler. Rewire StablecoinPanel to use typed sebuf client.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: migrate etf-flows to sebuf (market domain)

Add ListEtfFlows RPC to market service. Port Yahoo Finance BTC spot
ETF flow estimation logic from api/etf-flows.js into the market handler.
Rewire ETFFlowsPanel to use typed sebuf client.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: migrate worldpop-exposure to sebuf (displacement domain)

Add GetPopulationExposure RPC to displacement service. Port country
population data and radius-based exposure estimation from
api/worldpop-exposure.js into the displacement handler. Rewire
population-exposure.ts to use typed sebuf client.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: remove superseded legacy edge functions

Delete 4 legacy api/*.js files that are now fully replaced by
sebuf handlers:
- api/stablecoin-markets.js -> market/ListStablecoinMarkets
- api/etf-flows.js -> market/ListEtfFlows
- api/worldpop-exposure.js -> displacement/GetPopulationExposure
- api/classify-batch.js -> intelligence/ClassifyEvent

Remaining legacy files are still actively used by client code
(stock-index, opensky, gdelt-doc, rss-proxy, summarize endpoints,
macro-signals, tech-events) or are shared utilities.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: delete dead legacy files and unused API_URLS config

Remove coingecko.js, debug-env.js, cache-telemetry.js, _cache-telemetry.js
(all zero active consumers). Delete unused API_URLS export from base config.
Update desktop-readiness market-panel metadata to reference sebuf paths.
Remove dead CoinGecko dev proxy from vite.config.ts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: migrate stock-index and opensky to sebuf

- Add GetCountryStockIndex RPC to market domain (Yahoo Finance + cache)
- Fill ListMilitaryFlights stub in military handler (OpenSky with bounding box)
- Rewire App.ts stock-index fetch to MarketServiceClient.getCountryStockIndex()
- Delete api/stock-index.js and api/opensky.js edge functions
- OpenSky client path unchanged (relay primary, vite proxy for dev)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* wip: sebuf legacy migration paused at phase 3/10

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(03): capture phase context

* docs(state): record phase 3 context session

* docs(03): research phase domain

* docs(03): create phase plan — 5 plans in 2 waves

* feat(03-01): commit wingbits migration (step 3) -- 3 RPCs added to military domain

- Add GetAircraftDetails, GetAircraftDetailsBatch, GetWingbitsStatus RPCs
- Rewire src/services/wingbits.ts to use MilitaryServiceClient
- Update desktop-readiness.ts routes to match new RPC paths
- Delete legacy api/wingbits/ edge functions (3 files)
- Regenerate military service client/server TypeScript + OpenAPI docs

* feat(03-02): add SummarizeArticle proto and implement handler

- Create summarize_article.proto with request/response messages
- Add SummarizeArticle RPC to NewsService proto
- Implement full handler with provider dispatch (ollama/groq/openrouter)
- Port cache key builder, deduplication, prompt builder, think-token stripping
- Inline Upstash Redis helpers for edge-compatible caching

* feat(03-01): migrate gdelt-doc to intelligence RPC + delete _ip-rate-limit.js

- Add SearchGdeltDocuments RPC to IntelligenceService proto
- Implement searchGdeltDocuments handler (port from api/gdelt-doc.js)
- Rewire src/services/gdelt-intel.ts to use IntelligenceServiceClient
- Delete legacy api/gdelt-doc.js edge function
- Delete dead api/_ip-rate-limit.js (zero importers)
- Regenerate intelligence service client/server TypeScript + OpenAPI docs

* feat(03-02): rewire summarization client to NewsService RPC, delete 4 legacy files

- Replace direct fetch to /api/{provider}-summarize with NewsServiceClient.summarizeArticle()
- Preserve identical fallback chain: ollama -> groq -> openrouter -> browser T5
- Delete api/groq-summarize.js, api/ollama-summarize.js, api/openrouter-summarize.js
- Delete api/_summarize-handler.js and api/_summarize-handler.test.mjs
- Update desktop-readiness.ts to reference new sebuf route

* feat(03-03): rewire MacroSignalsPanel to EconomicServiceClient + delete legacy

- Replace fetch('/api/macro-signals') with EconomicServiceClient.getMacroSignals()
- Add mapProtoToData() to convert proto optional fields to null for rendering
- Delete legacy api/macro-signals.js edge function

* feat(03-04): add ListTechEvents proto, city-coords data, and handler

- Create list_tech_events.proto with TechEvent, TechEventCoords messages
- Add ListTechEvents RPC to ResearchService proto
- Extract 360-city geocoding table to api/data/city-coords.ts
- Implement listTechEvents handler with ICS+RSS parsing, curated events, dedup, filtering
- Regenerate TypeScript client/server from proto

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(03-01): complete wingbits + GDELT doc migration plan

- Create 03-01-SUMMARY.md with execution results
- Update STATE.md with plan 01 completion, steps 3-4 done
- Update ROADMAP.md plan progress (2/5 plans complete)
- Mark DOMAIN-10 requirement complete

* docs(03-02): complete summarization migration plan

- Create 03-02-SUMMARY.md with execution results
- Update STATE.md position to step 6/10
- Update ROADMAP.md plan progress
- Mark DOMAIN-09 requirement complete

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(03-04): rewire TechEventsPanel and App to ResearchServiceClient, delete legacy

- Replace fetch('/api/tech-events') with ResearchServiceClient.listTechEvents() in TechEventsPanel
- Replace fetch('/api/tech-events') with ResearchServiceClient.listTechEvents() in App.loadTechEvents()
- Delete legacy api/tech-events.js (737 lines)
- TypeScript compiles cleanly with no references to legacy endpoint

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(03-03): complete macro-signals migration plan

- Create 03-03-SUMMARY.md with execution results
- Mark DOMAIN-04 requirement complete in REQUIREMENTS.md

* docs(03-04): complete tech-events migration plan

- Add 03-04-SUMMARY.md with execution results
- Update STATE.md: advance to plan 5/step 8, add decisions
- Update ROADMAP.md: 4/5 plans complete for phase 03

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(03-05): add temporal baseline protos + handler with Welford's algorithm

- GetTemporalBaseline RPC: anomaly detection with z-score thresholds
- RecordBaselineSnapshot RPC: batch update via Welford's online algorithm
- Inline mgetJson helper for Redis batch reads
- Inline getCachedJson/setCachedJson Redis helpers
- Generated TypeScript client/server + OpenAPI docs

* feat(03-05): migrate temporal-baseline + tag non-JSON + final cleanup

- Rewire temporal-baseline.ts to InfrastructureServiceClient RPCs
- Delete api/temporal-baseline.js (migrated to sebuf handler)
- Delete api/_upstash-cache.js (no importers remain)
- Tag 6 non-JSON edge functions with // Non-sebuf: comment header
- Update desktop-readiness.ts: fix stale cloudflare-outages reference

* docs(03-05): complete temporal-baseline + non-JSON tagging + final cleanup plan

- SUMMARY.md with Welford algorithm migration details
- STATE.md updated: Phase 3 complete (100%)
- ROADMAP.md updated: 5/5 plans complete

* chore(03): delete orphaned ollama-summarize test after RPC migration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(phase-3): complete phase execution

* docs(v1): create milestone audit report

Audits all 13 phases of the v1 sebuf integration milestone.
12/13 phases verified (2L maritime missing VERIFICATION.md).
25/34 requirements satisfied, 6 superseded, 2 partial, 1 unsatisfied (CLEAN-03).
All 17 domains wired end-to-end. Integration check passes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(v1): update audit — mark CLEAN-03/04 + MIGRATE-* as superseded

CLEAN-03 superseded by port/adapter architecture (internal types
intentionally decoupled from proto wire types). MIGRATE-01-05
superseded by direct cutover approach. DOMAIN-03 checkbox updated.
Milestone status: tech_debt (no unsatisfied requirements).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(roadmap): add gap closure phase 4 — v1 milestone cleanup

Closes all audit gaps: CLIENT-03 circuit breaker coverage, DOMAIN-03/06
verification gaps, documentation staleness, orphaned code cleanup.
Fixes traceability table phase assignments to match actual roadmap phases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore(03): commit generated NewsService OpenAPI specs + checkpoint update

SummarizeArticle RPC was added during Phase 3 plan 02 but generated
OpenAPI specs were not staged with that commit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(04): research phase domain

* docs(04): create phase plan

* docs(04-01): fix ROADMAP.md Phase 3 staleness and delete .continue-here.md

- Change Phase 3 heading from IN PROGRESS to COMPLETE
- Check off plans 03-03, 03-04, 03-05 (all were already complete)
- Delete stale .continue-here.md (showed task 3/10 in_progress but all 10 done)

* feat(04-02): add circuit breakers to seismology, wildfire, climate, maritime

- Seismology: wrap listEarthquakes in breaker.execute with empty-array fallback
- Wildfire: replace manual try/catch with breaker.execute for listFireDetections
- Climate: replace manual try/catch with breaker.execute for listClimateAnomalies
- Maritime: wrap proto getVesselSnapshot RPC in snapshotBreaker.execute, preserve raw relay fallback

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(04-02): add circuit breakers to news summarization and GDELT intelligence

- Summarization: wrap newsClient.summarizeArticle in summaryBreaker.execute for both tryApiProvider and translateText
- GDELT: wrap client.searchGdeltDocuments in gdeltBreaker.execute, replace manual try/catch
- Fix: include all required fields (tokens, reason, error, errorType, query) in fallback objects
- CLIENT-03 fully satisfied: all 17 domains have circuit breaker coverage

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(04-01): create 2L-VERIFICATION.md, fix desktop-readiness.ts, complete service barrel

- Create retroactive 2L-VERIFICATION.md with 12/12 must-haves verified
- Fix map-layers-core and market-panel stale file refs in desktop-readiness.ts
- Fix opensky-relay-cloud stale refs (api/opensky.js deleted)
- Add missing barrel re-exports: conflict, displacement, research, wildfires, climate
- Skip military/intelligence/news barrels (would cause duplicate exports)
- TypeScript compiles cleanly with zero errors

* docs(04-02): complete circuit breaker coverage plan

- SUMMARY.md: 6 domains covered, CLIENT-03 satisfied, 1 deviation (fallback type fix)
- STATE.md: Phase 4 plan 02 complete, position and decisions updated
- ROADMAP.md: Phase 04 marked complete (2/2 plans)
- REQUIREMENTS.md: CLIENT-03 marked complete

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(04-01): complete documentation fixes plan

- Create 04-01-SUMMARY.md with execution results
- Update STATE.md with Plan 01 completion and decisions
- Update ROADMAP.md: Plan 04-01 checked, progress 1/2

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(phase-04): complete phase verification and fix tracking gaps

ROADMAP.md Phase 4 status updated to Complete, 04-02 checkbox checked,
progress table finalized. REQUIREMENTS.md coverage summary updated
(27 complete, 0 partial/pending). STATE.md reflects verified phase.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(military): migrate USNI fleet tracker to sebuf RPC

Port all USNI Fleet Tracker parsing logic from api/usni-fleet.js into
MilitaryService.GetUSNIFleetReport RPC with proto definitions, inline
Upstash caching (6h fresh / 7d stale), and client adapter mapping.
Deletes legacy edge function and _upstash-cache.js dependency.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: add proto validation annotations and split all 17 handler files into per-RPC modules

Phase A: Added buf.validate constraints to ~25 proto files (~130 field annotations
including required IDs, score ranges, coordinate bounds, page size limits).

Phase B: Split all 17 domain handler.ts files into per-RPC modules with thin
re-export handler.ts files. Extracted shared Redis cache helpers to
api/server/_shared/redis.ts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: add ADDING_ENDPOINTS guide and update Contributing section

All JSON endpoints must use sebuf — document the complete workflow for
adding RPCs to existing services and creating new services, including
proto conventions, validation annotations, and generated OpenAPI docs.

Update DOCUMENTATION.md Contributing section to reference the new guide
and remove the deprecated "Adding a New API Proxy" pattern.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add blank lines before lists to pass markdown lint (MD032)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: disambiguate duplicate city keys in city-coords

Vercel's TypeScript check treats duplicate object keys as errors (TS1117).
Rename 'san jose' (Costa Rica) -> 'san jose cr' and
'cambridge' (UK) -> 'cambridge uk' to avoid collision.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: resolve Vercel deployment errors — relocate hex-db + fix TS strict-null

- Move military-hex-db.js next to handler (fixes Edge Function unsupported module)
- Fix strict-null TS errors across 12 handler files (displacement, economic,
  infrastructure, intelligence, market, military, research, wildfire)
- Add process declare to wildfire handler, prefix unused vars, cast types

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: convert military-hex-db to .ts for Edge Function compatibility

Vercel Edge bundler can't resolve .js data modules from .ts handlers.
Also remove unused _region variable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: inline military hex db as packed string to avoid Edge Function module error

Vercel Edge bundler can't resolve separate data modules. Inline 20K hex
IDs as a single concatenated string, split into Set at runtime.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: remove broken export { process } from 4 _shared files

declare const is stripped in JS output, making export { process }
reference nothing. No consumers import it — each handler file has
its own declare.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: move server/ out of api/ to fix Vercel catch-all routing

Vercel's file-based routing was treating api/server/**/*.ts as individual
API routes, overriding the api/[[...path]].ts catch-all for multi-segment
paths like /api/infrastructure/v1/list-service-statuses (3 segments).
Moving to server/ at repo root removes the ambiguity.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: rename catch-all to [...path] — [[...path]] is Next.js-only syntax

Vercel's native edge function routing only supports [...path] for
multi-segment catch-all matching. The [[...path]] double-bracket syntax
is a Next.js feature and was only matching single-segment paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add dynamic segment route for multi-segment API paths

Vercel's native file-based routing for non-Next.js projects doesn't
support [...path] catch-all matching multiple segments. Use explicit
api/[domain]/v1/[rpc].ts which matches /api/{domain}/v1/{rpc} via
standard single-segment dynamic routing that Vercel fully supports.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: remove conflicting [...path] catch-all — replaced by [domain]/v1/[rpc]

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: widen CORS pattern to match all Vercel preview URL formats

Preview URLs use elie-ab2dce63 not elie-habib-projects as the team slug.
Broaden pattern to elie-[a-z0-9]+ to cover both.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: update sidecar build script for new api/[domain]/v1/[rpc] path

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: trigger Vercel rebuild for all variants

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address PR #106 review — critical bugs, hardening, and cleanup

Fixes from @koala73's code review:

Critical:
- C-1: Add max-size eviction (2048 cap) to GeoIP in-memory cache
- C-2: Move type/source/severity filters BEFORE .slice(pageSize) in cyber handler
- C-3: Atomic SET with EX in Redis helper (single Upstash REST call)
- C-4: Add AbortSignal.timeout(30s) to LLM fetch in summarize-article

High:
- H-1: Add top-level try/catch in gateway with CORS-aware 500 response
- H-3: Sanitize error messages — generic text for 5xx, passthrough for 4xx only
- H-4: Add timeout (10s) + Redis cache (5min) to seismology handler
- H-5: Add null guards (optional chaining) in seismology USGS feature mapping
- H-6: Race OpenSky + Wingbits with Promise.allSettled instead of sequential fallback
- H-8: Add Redis cache (5min TTL) to infrastructure service-status handler

Medium:
- M-12/M-13: Fix HAPI summary field mappings (iso3 from countryCode, internallyDisplaced)

Infrastructure:
- R-1: Remove .planning/ from git tracking, add to .gitignore
- Port UCDP parallel page fetching from main branch (#198)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address PR #106 review issues — hash, CORS, router, cache, LLM, GeoIP

Fixes/improvements from PR #106 code review tracking issues:

- #180: Replace 32-bit hash (Java hashCode/DJB2) with unified FNV-1a 52-bit
  hash in server/_shared/hash.ts, greatly reducing collision probability
- #182: Cache router construction in Vite dev plugin — build once, invalidate
  on HMR changes to server/ files
- #194: Add input length limits for LLM prompt injection (headlines 500 chars,
  title 500 chars, geoContext 2000 chars, max 10 headlines)
- #195/#196: GeoIP AbortController — cancel orphaned background workers on
  timeout instead of letting them fire after response is sent
- #198: Port UCDP partial-result caching from main — 10min TTL for partial
  results vs 6hr for complete, with in-memory fallback cache

Proto codegen regenerated for displacement + conflict int64_encoding changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: restore fast-xml-parser dependency needed by sebuf handlers

Main branch removed fast-xml-parser in v2.5.1 (legacy edge functions
no longer needed it), but sebuf handlers in aviation/_shared.ts and
research/list-arxiv-papers.ts still import it for XML API parsing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: fix stale paths, version badge, and local-backend-audit for sebuf

- ADDING_ENDPOINTS.md: fix handler paths from api/server/ to server/,
  fix import depth (4 levels not 5), fix gateway extension (.js not .ts)
- DOCUMENTATION.md: update version badge 2.1.4 -> 2.5.1, fix broken
  ROADMAP.md links to .planning/ROADMAP.md, fix handler path reference
- COMMUNITY-PROMOTION-GUIDE.md: add missing v2.5.1 to version table
- local-backend-audit.md: rewrite for sebuf architecture — replace all
  stale api/*.js references with sebuf domain handler paths

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: use make commands, add generation-before-push warning, bump sebuf to v0.7.0

- ADDING_ENDPOINTS.md: replace raw `cd proto && buf ...` with `make check`,
  `make generate`, `make install`; add warning that `make generate` must run
  before pushing proto changes (links to #200)
- Makefile: bump sebuf plugin versions from v0.6.0 to v0.7.0
- PR description also updated to use make commands

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: make install installs everything, document setup

- Makefile: `make install` now installs buf, sebuf plugins, npm deps,
  and proto deps in one command; pin buf and sebuf versions as variables
- ADDING_ENDPOINTS.md: updated prerequisites to show `make install`
- DOCUMENTATION.md: updated Installation section with `make install`
  and generation reminder

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address PR #106 re-review issues — timeouts, iso3, pagination, CORS, dedup

- NEW-1: HAPI handler now returns ISO-3 code (via ISO2_TO_ISO3 lookup) instead of ISO-2
- NEW-3: Aviation FAA fetch now has AbortSignal.timeout(15s)
- NEW-4: Climate Open-Meteo fetch now has AbortSignal.timeout(20s)
- NEW-5: Wildfire FIRMS fetch now has AbortSignal.timeout(15s)
- NEW-6: Seismology now respects pagination.pageSize (default 500)
- NEW-9: Gateway wraps getCorsHeaders() in try/catch with safe fallback
- NEW-10: Tech events dedup key now includes start year to avoid dropping yearly variants

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address PR #106 round 2 — seismology crash, tsconfig, type contracts

- Fix _req undefined crash in seismology handler (renamed to req)
- Cache full earthquake set, slice on read (avoids cache pollution)
- Add server/ to tsconfig.api.json includes (catches type errors at build)
- Remove String() wrappers on numeric proto fields in displacement/HAPI
- Fix hashString re-export not available locally in news/_shared.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: loadMarkets heatmap regression, stale test, Makefile playwright

- Add finnhub_skipped + skip_reason to ListMarketQuotesResponse proto
- Wire skipped signal through handler → adapter → App.loadMarkets
- Fix circuit breaker cache conflating different symbol queries (cacheTtlMs: 0)
- Use dynamic fetch wrapper so e2e test mocks intercept correctly
- Update e2e test mocks from old endpoints to sebuf proto endpoints
- Delete stale summarization-chain.test.mjs (imports deleted files)
- Add install-playwright target to Makefile

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add missing proto fields to emptyStockFallback (build fix)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(quick-1): fix country fallback, ISO-2 contract, and proto field semantics

- BLOCKING-1: Return undefined when country has no ISO3 mapping instead of wrong country data
- BLOCKING-2: country_code field now returns ISO-2 per proto contract
- MEDIUM-1: Rename proto fields from humanitarian to conflict-event semantics (populationAffected -> conflictEventsTotal, etc.)
- Update client service adapter to use new field names
- Regenerate TypeScript types from updated proto

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(quick-1): add in-memory cache + in-flight dedup to AIS vessel snapshot

- HIGH-1: 10-second TTL cache matching client poll interval
- Concurrent requests share single upstream fetch (in-flight dedup)
- Follows same pattern as get-macro-signals.ts cache
- No change to RPC response shape

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(quick-1): stub RPCs throw UNIMPLEMENTED, remove hardcoded politics, add tests

- HIGH-2: listNewsItems, summarizeHeadlines, listMilitaryVessels now throw UNIMPLEMENTED
- LOW-1: Replace hardcoded "Donald Trump" with date-based dynamic LLM context
- LOW-1 extended: Also fix same issue in intelligence/get-country-intel-brief.ts (Rule 2)
- MEDIUM-2: Add tests/server-handlers.test.mjs with 20 tests covering all review items

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(quick-2): remove 3 dead stub RPCs (ListNewsItems, SummarizeHeadlines, ListMilitaryVessels)

- Delete proto definitions, handler stubs, and generated code for dead RPCs
- Clean _shared.ts: remove tryGroq, tryOpenRouter, buildPrompt, dead constants
- Remove 3 UNIMPLEMENTED stub tests from server-handlers.test.mjs
- Regenerate proto codegen (buf generate) and OpenAPI docs
- SummarizeArticle and all other RPCs remain intact

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address PR #106 review findings (stale snapshot, iso naming, scoring, test import)

- Serve stale AIS snapshot on relay failure instead of returning undefined
- Rename HapiConflictSummary.iso3 → iso2 to match actual ISO-2 content
- Fix HAPI fallback scoring: use weight 3 for combined political violence
  (civilian targeting is folded in, was being underweighted at 0)
- Extract deduplicateHeadlines to shared .mjs so tests import production code

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: add coverage for PR106 iso2 and fallback regressions

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Elie Habib <elie.habib@gmail.com>
2026-02-21 03:39:56 +04:00
Claude
6c02bd2eb6 chore: sync package-lock.json with package.json version
npm install regenerated the lockfile to reflect the current
version (2.5.0) and license field from package.json.

https://claude.ai/code/session_01684qa7XvS7sf9CShqU8zNg
2026-02-19 21:34:39 +00:00
Elie Habib
941475eb79 feat: integrate Sentry browser error tracking
Initializes @sentry/browser early in main.ts with environment
detection (production/preview/development). Disabled on localhost
and Tauri desktop. Traces sampled at 10%.
2026-02-18 13:56:31 +04:00
Elie Habib
30be26e779 merge(main): update PR #86 and resolve merge conflicts 2026-02-17 20:58:31 +04:00
Elie Habib
9a31eeba09 chore(release): finalize 2.3.8 changelog and version 2026-02-17 09:53:03 +04:00
Elie Habib
88ad25cb93 release: v2.3.7
Full light mode theme, header dark/light toggle, desktop update checker,
bundled Node.js in installer, CORS fixes, and panel defaults update.
2026-02-16 23:56:28 +04:00
Lib-LOCALE
00925011dc feat(i18n): Implement full internationalization support for 9 languages and add Linux AppImage config 2026-02-16 12:32:55 +01:00
Elie Habib
700132adad fix: hide node.exe console window on Windows & bump v2.3.6
Add CREATE_NO_WINDOW (0x08000000) creation flag to the sidecar
Command::new() spawn on Windows. Without this, node.exe inherits
a visible console window that overlays the Tauri GUI.
2026-02-16 09:00:16 +04:00
Elie Habib
46010c3911 feat: differentiated panel error messages & auto-hide desktop config (v2.3.5)
- Add Panel.showConfigError() with amber styling and desktop Settings link
- Propagate `skipped` flag from Finnhub and FIRMS API responses
- Show "API key not configured" on Markets/Heatmap/Commodities/FIRMS panels
  when sidecar returns skipped (missing API key)
- ETF, Stablecoin, MacroSignals panels detect upstream API unavailability
  and show retry message instead of generic "Failed to fetch"
- RuntimeConfigPanel auto-hides when all features are configured
- Bump version to 2.3.5
2026-02-16 08:51:47 +04:00
Elie Habib
939ac3a864 fix: sync package-lock.json with markdownlint-cli2 devDependency
The lock file was out of sync causing npm ci to fail in CI for v2.3.3
and v2.3.4 builds.
2026-02-16 00:50:43 +04:00
Elie Habib
a9b3582ae3 fix: harden sidecar verification, dedupe spikes, and bump v2.3.1 2026-02-15 22:57:09 +04:00
Elie Habib
fb51b5bf40 fix: desktop settings UX overhaul & IPv4-safe fetch for sidecar
- Show "Staged" status/pill for buffered secrets instead of "Missing"
- Add macOS Edit menu (Cmd+C/V/X/Z) for WKWebView clipboard support
- Raise settings window when main gains focus (prevent hide-behind)
- Fix Cloudflare verification to probe Radar API (not token/verify)
- Fix EIA verification URL to valid v2 endpoint
- Force IPv4 globally: monkey-patch fetch() to avoid IPv6 ETIMEDOUT
  on government APIs (EIA, NASA FIRMS) with broken AAAA records
- Soft-pass on network errors during secret verification (don't block save)
- Add desktopRequiredSecrets to skip relay URLs on desktop
- Cross-window sync for secrets and feature toggles via localStorage events
- Add @tauri-apps/cli devDependency
2026-02-15 22:35:21 +04:00
Elie Habib
c353cf2070 Reduce egress costs, add PWA support, fix Polymarket and Railway relay
Egress optimization:
- Add s-maxage + stale-while-revalidate to all API endpoints for Vercel CDN caching
- Add vercel.json with immutable caching for hashed assets
- Add gzip compression to sidecar responses >1KB
- Add gzip to Railway RSS responses (4 paths previously uncompressed)
- Increase polling intervals: markets/crypto 60s→120s, ETF/macro/stablecoins 60s→180s
- Remove hardcoded Railway URL from theater-posture.js (now env-var only)

PWA / Service Worker:
- Add vite-plugin-pwa with autoUpdate strategy
- Cache map tiles (CacheFirst), fonts (StaleWhileRevalidate), static assets
- NetworkOnly for all /api/* routes (real-time data must be fresh)
- Manual SW registration (web only, skip Tauri)
- Add offline fallback page
- Replace manual manifest with plugin-generated manifest

Polymarket fix:
- Route dev proxy through production Vercel (bypasses JA3 blocking)
- Add 4th fallback tier: production URL as absolute fallback

Desktop/Sidecar:
- Dual-backend cache (_upstash-cache.js): Redis cloud + in-memory+file desktop
- Settings window OK/Cancel redesign
- Runtime config and secret injection improvements
2026-02-14 19:53:04 +04:00
Elie Habib
19754716c6 feat: add intelligence layers and harden data ingestion 2026-02-13 08:14:53 +04:00
Elie Habib
a8a648bfd9 Add Playwright map harness smoke tests for layer regressions 2026-02-12 12:34:42 +04:00
Elie Habib
c5b683212f Switch story rendering to client-side Canvas (WASM not allowed in Vercel Edge) 2026-01-29 23:49:48 +04:00
Elie Habib
5adefb1d76 Add World Stories: shareable vertical country intelligence snapshots via @vercel/og 2026-01-29 23:42:03 +04:00
Elie Habib
fcdf94de62 Add server-side Redis caching for AI summaries + improve story ranking
- Add Upstash Redis caching to groq-summarize.js and openrouter-summarize.js
- Switch to llama-3.1-8b-instant (14.4K/day vs 1K for 70b)
- Cross-user cache deduplication with 24h TTL
- Remove client-side cache (server handles all caching now)

Improve AI Insights story selection:
- Composite importance score: sources × velocity × recency + alert bonus
- Source diversity cap: max 3 stories from same source
- Recency decay: newer stories rank higher (12h half-life)

Scalability: ~144x headroom (was hitting 1K/day limit)
2026-01-25 12:14:16 +04:00
Elie Habib
5c01ff41e6 Add client-side ML features with ONNX Runtime
- Add ML worker infrastructure (@xenova/transformers)
- Implement semantic news clustering (hybrid Jaccard + embeddings)
- Add InsightsPanel with themes, entities, sentiment analysis
- Add click-to-summarize feature in NewsPanel
- Wire up ML-enhanced velocity/sentiment scoring
- Desktop-only activation (mobile excluded)
- Fix division by zero in cosineSimilarity
- Fix dead Promise code in ml-worker.ts
2026-01-25 10:48:55 +04:00
Claude
f1c4cd8950 Add @deck.gl/mapbox dependency
Required for MapboxOverlay integration with MapLibre.

https://claude.ai/code/session_01GTanC7R6aSQNsnijqJRUFz
2026-01-23 14:10:53 +00:00
Claude
bfe31675ed Add deck.gl WebGL visualization for desktop
Implement Palantir-like interactive map experience using deck.gl with
MapLibre GL as the base map. Key changes:

- Add deck.gl (@deck.gl/core, @deck.gl/layers, @deck.gl/geo-layers) and
  maplibre-gl dependencies for GPU-accelerated rendering
- Create DeckGLMap component with WebGL layers for:
  - Undersea cables and pipelines (PathLayer)
  - Military bases, nuclear facilities, datacenters (ScatterplotLayer)
  - Conflict zones (GeoJsonLayer with polygons)
  - Hotspots, earthquakes, weather, outages, protests
  - AIS density, military vessels and flights
  - Strategic waterways, economic centers
  - Tech variant: startup hubs, tech HQs, accelerators, cloud regions
- Create MapContainer wrapper that conditionally renders:
  - DeckGLMap (WebGL) on desktop with WebGL support
  - Existing D3/SVG MapComponent on mobile for graceful degradation
- Add dark theme CSS styles for deck.gl controls, legend, layer toggles
- Import maplibre-gl CSS in main.ts

Desktop users now get smooth 60fps interactions with large datasets
while mobile users retain the optimized SVG experience.

https://claude.ai/code/session_01GTanC7R6aSQNsnijqJRUFz
2026-01-23 11:48:20 +00:00
Elie Habib
5d037c4132 Add tech variant with expanded global tech ecosystem data
- Add variant system (full/tech) with VITE_VARIANT env var
- Create tech-geo.ts with 465 entries:
  - 295 TECH_HQS (FAANG, unicorns across US, Europe, MENA, India, SEA, China, LATAM, Africa)
  - 112 ACCELERATORS (YC, Techstars, 500 Global, regional accelerators)
  - 38 STARTUP_HUBS (mega/major/emerging tiers)
  - 20 CLOUD_REGIONS (AWS, GCP, Azure, Cloudflare)
- Add map layers: startupHubs, cloudRegions, accelerators, techHQs
- Add tech-specific RSS feeds and panels
- Fix YouTube channel fallback IDs (Yahoo Finance, NASA TV, TBPN)
- MENA expansion: 50+ companies (UAE, Saudi, Egypt, Jordan)
- India: 40+ unicorns (Flipkart, PhonePe, Razorpay, etc)
- SEA: 25+ companies (Grab, GoTo, J&T Express, etc)
- LATAM: 35+ companies (Nubank, MercadoLibre, Bitso, etc)
2026-01-22 23:18:32 +04:00
Elie Habib
d5cd174516 Bump version to 1.5.0 2026-01-20 11:36:08 +04:00
Elie Habib
e7a01b7b09 Bump version to 1.4.2 2026-01-18 13:42:46 +04:00
Elie Habib
36845cebac Bump version to 1.4.1 2026-01-18 13:29:16 +04:00
Elie Habib
2532cd2106 Add data freshness tracking for Oil/Spending + update README
- Add 'oil' and 'spending' as separate DataSourceIds in data-freshness
- Update oil-analytics.ts to report to 'oil' source
- Update usa-spending.ts to report to 'spending' source
- Add EIA and USASpending to StatusPanel API list
- Update README: version 1.3.8, new API dependencies, project structure
- Document EIA_API_KEY in optional API keys section
2026-01-16 13:15:43 +04:00