Commit Graph

286 Commits

Author SHA1 Message Date
Elie Habib
45f5e5a457 feat(forecast): AI Forecasts prediction module (#1579)
* feat(forecast): add AI Forecasts prediction module (Pro-tier)

MiroFish-inspired prediction engine that generates structured forecasts
across 6 domains (conflict, market, supply chain, political, military,
infrastructure) using existing WorldMonitor data streams.

- Proto definitions for ForecastService with GetForecasts RPC
- Dedicated seed script (seed-forecasts.mjs) with 6 domain detectors,
  cross-domain cascade resolver, prediction market calibration, and
  trend detection via prior snapshot comparison
- Premium-gated RPC handler (PREMIUM_RPC_PATHS enforcement)
- Lazy-loaded ForecastPanel with domain filters, probability bars,
  trend arrows, signal evidence, and cascade links
- Health monitoring integration (seed-meta freshness tracking)
- Refresh scheduler with API key guard

* test(forecast): add 47 unit tests for forecast detectors and utilities

Covers forecastId, normalize, resolveCascades, calibrateWithMarkets,
computeTrends, and smoke tests for all 6 domain detectors. Exports
testable functions from seed script with direct-run guard.

* fix(forecast): domain mismatch 'infra' vs 'infrastructure', add panel category

- Seed script used 'infra' but ForecastPanel filtered on 'infrastructure',
  causing Infra tab to show zero results
- Added 'forecast' to intelligence category in PANEL_CATEGORY_MAP

* fix(forecast): move CSS to one-time injection, improve type safety

- P2: Move style block from setContent to one-time document.head injection
  to prevent CSS accumulation on repeated renders
- P3: Replace +toFixed(3) with Math.round for readability in seed script
- P3: Use Forecast type instead of any[] in RPC handler filter

* fix(forecast): handle sebuf proto data shapes from Redis

Detectors now normalize CII scores from server-side proto format
(combinedScore, TREND_DIRECTION_RISING, region) to uniform shape.
Outage severity handles proto enum format (SEVERITY_LEVEL_HIGH).
Added confidence floor of 0.3 for single-source predictions.

Verified against live Redis: 2 predictions generated (Iran infra
shutdown, IL political instability).

* feat(forecast): unlock AI Forecasts on web, lock desktop only (trial)

- Remove forecast RPC from PREMIUM_RPC_PATHS (web access is free)
- Panel locked on desktop only (same as oref-sirens/telegram-intel)
- Remove API key guards from data-loader and refresh scheduler
- Web users get full access during trial period

* chore: regenerate proto types with make generate

Re-ran make generate after rebasing on main. Plugin v0.7.0 dropped
@ts-nocheck from output, added it back to all 50 generated files.
Fixed 4 type errors from proto codegen changes:
- MarketSource enum -> string union type
- TemporalAnomalyProto -> TemporalAnomaly rename
- webcam lastUpdated number -> string

* fix(forecast): use chokepoints v4 key, include ciiContribution in unrest

- P1: Switch chokepoints input from stale v2 to active v4 Redis key,
  matching bootstrap.js and cache-keys.ts
- P2: Add ciiContribution to unrest component fallback chain in
  normalizeCiiEntry so political detector reads the correct sebuf field

* feat(forecast): Phase 2 LLM scenario enrichment + confidence model

MiroFish-inspired enhancements:
- LLM scenario narratives via Groq/OpenRouter (narrative-only, no numeric
  adjustment). Evidence-grounded prompts with mandatory signal citation
  and few-shot examples from MiroFish's SECTION_SYSTEM_PROMPT_TEMPLATE.
- Top-4 predictions batched into single LLM call for cost efficiency.
- News context from newsInsights attached to all predictions for LLM
  prompt grounding (NOT in signals, cannot affect confidence).
- Deterministic confidence model: source diversity via SIGNAL_TO_SOURCE
  mapping (deduplicates cii+cii_delta, theater+indicators) + calibration
  agreement from prediction market drift. Floor 0.2, ceiling 1.0.
- Output validation: rejects scenarios without signal references.
- Truncated JSON repair for small model output.
- Structured JSON logging for LLM calls.
- Redis cache for LLM scenarios (1h TTL).
- 23 new tests (70 total), all passing.
- Live-tested: OpenRouter gemini-2.5-flash produces evidence-grounded
  scenario narratives from real WorldMonitor data.

* feat(forecast): Phase 3 multi-perspective scenarios, projections, data-driven cascades

MiroFish-inspired enhancements:
- Multi-perspective LLM analysis: top-2 predictions get strategic,
  regional, and contrarian viewpoints via combined LLM call
- Probability projections: domain-specific decay curves (h24/d7/d30)
  anchored to timeHorizon so probability equals projections[timeHorizon]
- Data-driven cascade rules: moved from hardcoded array to JSON config
  (scripts/data/cascade-rules.json) with schema validation, named
  predicate evaluators, unknown key rejection, and fallback to defaults
- 4 new cascade paths: infrastructure->supply_chain, infrastructure->market
  (both requiresSeverity:total), conflict->political, political->market
- Proto: added Perspectives and Projections messages to Forecast
- ForecastPanel: renders projections row and conditional perspectives toggle
- 89 tests (19 new), all passing
- Live-tested: OpenRouter produces perspectives from real data

* feat(forecast): Phase 4 data utilization + entity graph

Fixes data gaps that prevented 4 of 6 detectors from firing:
- Input normalizers: chokepoint v4 shape + GPS hexes-to-zones mapping
- Chokepoint warm-ping (production-only, requires WM_API_BASE_URL)
- Lowered CII conflict threshold from 70 to 60, gated on level=high|critical

4 new standalone detectors:
- UCDP conflict zones (10+ events per country)
- Cyber threat concentration (5+ threats per country)
- GPS jamming in maritime shipping zones (5 regions)
- Prediction markets as signals (60-90% probability markets)

Entity-relationship graph (file-based, 38 nodes):
- Countries, theaters, commodities, chokepoints, alliances
- Alias table resolves both ISO codes and display names
- Graph cascade discovery links predictions across entities

Result: 51 predictions (up from 1-2), spanning conflict, infrastructure,
and supply chain domains. 112 tests, all passing.

* fix(forecast): redis cache format, signal source mapping, type safety

Fresh-eyes audit fixes:
- BUG: redisSet used wrong Upstash API format (POST body with {value,ex}
  instead of command array ['SET',key,value,'EX',ttl]). LLM cache writes
  were silently failing, causing fresh LLM calls every run.
- BUG: prediction_market signal type missing from SIGNAL_TO_SOURCE,
  inflating confidence for market-derived predictions.
- CLEANUP: Remove unnecessary (f as any) casts in ForecastPanel since
  generated Forecast type already has projections/perspectives fields.
- CLEANUP: Bump health maxStaleMin from 60 to 90 to avoid false STALE
  alerts when LLM calls add latency to seed runs.

* feat(forecast): headline-entity matching with news corroboration signals

Uses entity graph aliases to match headlines to predictions by
country/theater (excludes commodity/infrastructure nodes to prevent
false positives). Predictions with matching headlines get a
news_corroboration signal visible in the panel.

Also fixes buildUserPrompt to merge unique headlines from ALL
predictions in the LLM batch (was only reading preds[0].newsContext).

Live-tested: 13 of 51 predictions now have corroborating headlines
(Iran, Israel, Syria, Ukraine, etc). 116 tests, all passing.

* feat(forecast): add country-codes.json for headline-entity matching

56 countries with ISO codes, full names, and scoring keywords (extracted
from src/config/countries.ts + UCDP-relevant additions). Used by
attachNewsContext for richer headline matching via getSearchTermsForRegion
which combines country-codes + entity graph + keyword aliases.

14/57 predictions now have news corroboration (limited by headline
coverage, not matching quality: only 8 headlines currently available).

* feat(forecast): read 300 headlines from news digest instead of 8

Read news:digest:v1:full:en (300 headlines across 16 categories) instead
of just news:insights:v1 topStories (8 headlines). Fallback to topStories
if digest is unavailable.

Result: news corroboration jumped from 25% to 64% (38/59 predictions).

* fix(forecast): handle parenthetical country names in headline matching

Strip suffixes like '(Zaire)', '(Burma)', '(Soviet Union)' from UCDP
region names before matching against country-codes.json. Also use
includes() for reverse name lookup to catch partial matches.

Corroboration: 64% -> 69% (41/59). Remaining 18 unmatched are countries
with no current English-language news coverage.

* fix(forecast): cache validated LLM output, add digest test, log cache errors

Fresh-eyes audit fixes:
- Combined LLM cache now stores only validated items (was caching raw
  unvalidated output, serving potentially invalid scenarios on cache hit)
- redisSet logs warnings on failure (was silently swallowing all errors)
- Added digest-based test for attachNewsContext (primary path was untested)
- Fixed test arity: attachNewsContext(preds, news, digest) with 3 params

* fix(forecast): remove dead confidenceFromSources, reduce warm-ping timeout

- P2: Remove confidenceFromSources (dead code, computeConfidence overwrites
  all initial confidence values). Inline the formula in original detectors.
- P3: Reduce warm-ping timeout from 30s to 15s (non-critical step)
- P3: Add trial status comment on forecast panel config

* fix(forecast): resolve ISO codes to country names, fix market detector, safe pre-push

P1 fixes from code review:
- CII ISO codes (IL, IR) now resolved to full country names (Israel, Iran)
  via country-codes.json. Prevents substring false positives (IL matching
  Chile) in event correlation. Uses word-boundary regex for matching.
- Market detector CII-to-theater mapping now uses entity graph traversal
  instead of broken theater-name substring matching. Iran correctly maps
  to Middle East theater via graph links.
- Pre-push hook no longer runs destructive git checkout on proto freshness
  failure. Reports mismatch and exits without modifying worktree.
2026-03-15 01:42:04 +04:00
Elie Habib
fe67111dc9 feat: harness engineering P0 - linting, testing, architecture docs (#1587)
* feat: harness engineering P0 - linting, testing, architecture docs

Add foundational infrastructure for agent-first development:

- AGENTS.md: agent entry point with progressive disclosure to deeper docs
- ARCHITECTURE.md: 12-section system reference with source-file refs and ownership rule
- Biome 2.4.7 linter with project-tuned rules, CI workflow (lint-code.yml)
- Architectural boundary lint enforcing forward-only dependency direction (lint-boundaries.mjs)
- Unit test CI workflow (test.yml), all 1083 tests passing
- Fixed 9 pre-existing test failures (bootstrap sync, deploy-config headers, globe parity, redis mocks, geometry URL, import.meta.env null safety)
- Fixed 12 architectural boundary violations (types moved to proper layers)
- Added 3 missing cache tier entries in gateway.ts
- Synced cache-keys.ts with bootstrap.js
- Renamed docs/architecture.mdx to "Design Philosophy" with cross-references
- Deprecated legacy docs/Docs_To_Review/ARCHITECTURE.md
- Harness engineering roadmap tracking doc

* fix: address PR review feedback on harness-engineering-p0

- countries-geojson.test.mjs: skip gracefully when CDN unreachable
  instead of failing CI on network issues
- country-geometry-overrides.test.mts: relax timing assertion
  (250ms -> 2000ms) for constrained CI environments
- lint-boundaries.mjs: implement the documented api/ boundary check
  (was documented but missing, causing false green)

* fix(lint): scan api/ .ts files in boundary check

The api/ boundary check only scanned .js/.mjs files, missing the 25
sebuf RPC .ts edge functions. Now scans .ts files with correct rules:
- Legacy .js: fully self-contained (no server/ or src/ imports)
- RPC .ts: may import server/ and src/generated/ (bundled at deploy),
  but blocks imports from src/ application code

* fix(lint): detect import() type expressions in boundary lint

- Move AppContext back to app/app-context.ts (aggregate type that
  references components/services/utils belongs at the top, not types/)
- Move HappyContentCategory and TechHQ to types/ (simple enums/interfaces)
- Boundary lint now catches import('@/layer') expressions, not just
  from '@/layer' imports
- correlation-engine imports of AppContext marked boundary-ignore
  (type-only imports of top-level aggregate)
2026-03-14 21:29:21 +04:00
Elie Habib
03bd3d0743 refactor: move shared types from services/config to types layer (#1597)
types/index.ts had backward imports via inline import() types
referencing @/services/ and @/config/ (higher layers). This
partially violated the architectural boundary rule from PR #1587.

Moved ThreatClassification, ThreatLevel, EventCategory,
HappyContentCategory, TechHQ, Port, and PortType definitions
into src/types/index.ts (lowest layer). Original files re-export
from @/types for backward compatibility.

Zero inline import() and zero backward from '@/...' imports remain
in src/types/index.ts.
2026-03-14 21:18:27 +04:00
Elie Habib
6e8f3037e9 fix(feeds): remove dead RSS feeds, fix broken URLs, drop oversized sat group (#1596)
- Remove 403-blocked feeds: Breaking Defense, My Modern Met, AEI
- Fix Infobae URL: /feeds/rss/ -> /arc/outboundfeeds/rss/ (both files)
- Fix CSIS URL: /feed -> /rss.xml (server _feeds.ts)
- Drop 'active' from CelesTrak SAT_GROUPS (>2MB, always rejected)
2026-03-14 21:14:46 +04:00
Elie Habib
9fe850586c fix(supply-chain): correct PortWatch ArcGIS API integration (#1577)
* fix(supply-chain): correct PortWatch ArcGIS service URL, field names, and chokepoint mappings

The PortWatch seed was returning no data because the ArcGIS service name,
WHERE clause fields, date field, and chokepoint names were all wrong.
Verified all 12 chokepoints return 175 days of data against the live API.
Added error logging to pwFetchAllPages for future debugging.

* fix(supply-chain): sync geofence names with relayName renames

CHOKEPOINT_GEOFENCES in ais-relay.cjs still used old names
('Strait of Malacca', 'Bab el-Mandeb', 'Strait of Gibraltar')
while _chokepoint-ids.ts relayName was updated. buildRelayLookup
does exact string match, so these 3 chokepoints had zero transit
counts despite relay data being present.

Rename geofence entries to match the new relayName values and
update corresponding test assertions.
2026-03-14 17:14:46 +04:00
Elie Habib
d4088fede5 fix(feeds): update dead RSS feed URLs (#1575)
- a16z: a16z.com/feed/ -> www.a16z.news/feed
- First Round Review: /feed.xml -> /articles/rss
- RAND: Google News proxy -> rand.org/pubs/articles.xml (direct)
- Add www.a16z.news to allowed domains
2026-03-14 16:00:35 +04:00
Elie Habib
0383253a59 feat(supply-chain): chokepoint transit intelligence with 3 data sources (#1560)
* feat(supply-chain): replace S&P Global with 3 free maritime data sources

Replace expensive S&P Global Maritime API with IMF PortWatch (vessel transit
counts), CorridorRisk (risk intelligence), and AISStream chokepoint crossing
counter. All external API calls run on Railway relay, Vercel reads Redis only.

- Add 4 new chokepoints (10 total): Cape of Good Hope, Gibraltar, Bosphorus, Dardanelles
- Add TransitSummary proto (field 14) with today counts, WoW%, 180d history, risk context
- Add D3 multi-line chart (tanker vs cargo) with expandable chokepoint cards
- Add crossing detection with enter+dwell+exit semantics, 30min cooldown, 5min min dwell
- Add PortWatch seed loop (6h), CorridorRisk seed loop (1h), transit seed loop (10min)
- Add canonical chokepoint ID map for cross-source name resolution
- 177 tests passing across 6 test files

* fix(supply-chain): address P2 review findings

- Discard partial PortWatch pagination results on mid-page failure (prevents
  truncated history with wrong WoW numbers cached for 6h)
- Rename "Transit today" to "24h" label (rolling 24h window, not calendar day)
- Fix chart label from "30d" to "180d" (matches actual PortWatch query range)
- Add 30s initial seed for chokepoint transits on relay cold start (prevents
  10min gap of zero transit data)

* feat(supply-chain): swap D3 chart for TradingView lightweight-charts

Replace hand-rolled D3 SVG transit chart with lightweight-charts v5 canvas
rendering for Bloomberg-quality time-series visualization.

- Add TransitChart helper class with mount/destroy lifecycle, theme listener,
  and autoSize support
- Use MutationObserver (not rAF) to mount chart after setContent debounce
- Clean up chart on tab switch, collapse, and re-render (no orphaned canvases)
- Respond to theme-changed events via chart.applyOptions()
- D3 stays for other 5 components (ProgressCharts, RenewableEnergy, etc.)

* feat(supply-chain): add geo coords and trade routes for 4 new chokepoints

Cherry-pick from PR #1511: Cape of Good Hope, Gibraltar, Bosphorus, and
Dardanelles map-layer coordinates and trade route definitions.

* fix(supply-chain): health.js v2->v4 key + double cache TTLs for missed seeds

- health.js chokepoints key was still v2, now v4 (matches handler + bootstrap)
- PortWatch TTL: 21600s (6h) -> 43200s (12h), seed interval stays 6h
- CorridorRisk TTL: 3600s (1h) -> 7200s (2h), seed interval stays 1h
- Ensures one missed seed run doesn't expire the key and cause empty data
2026-03-14 14:20:49 +04:00
Elie Habib
b312e0110f feat(panels): separate Windy webcam into its own panel, restore original Live Webcams (#1561) 2026-03-14 11:12:34 +04:00
Jon Torrez
987ed03f5d feat(webcams): add webcam map layer with Windy API integration (#1540) (#1540)
- Webcam markers on flat, globe, and DeckGL maps with category-based icons
- Server-side spatial queries via Redis GEOSEARCH with quantized bbox caching
- Pinned webcams panel with localStorage persistence
- Seed script for Windy API with regional bounding boxes and adaptive splitting
- Input validation (webcamId regex + encodeURIComponent) and NaN projection guards
- Bandwidth optimizations: zoom threshold, bbox overlap check, 1s cooldown
- Client-side image cache with 200-entry FIFO eviction
- Globe altitude-based viewport estimation for webcam loading
- CSP updates for webcam iframe sources
- Seed-meta key for health.js freshness tracking
2026-03-14 09:34:54 +04:00
Elie Habib
e3bb7b0c95 feat(panels): persist TV channel and webcam selection across reloads (#1535)
Active TV channel, webcam region filter, view mode, and active feed
reset to defaults on every page load. Store these preferences in
localStorage with validation and graceful fallback for removed or
invalid entries.
2026-03-13 15:31:09 +04:00
Jon Torrez
fb3c48e077 feat: multi-domain correlation engine (#1524)
* feat: correlation engine with 4-domain signal convergence analysis

Multi-domain convergence detection engine with adapter pattern for
military force posture, escalation monitoring, economic warfare, and
disaster cascade domains. Grid-based spatial clustering with union-find
and circular-mean centroid computation (antimeridian-safe). LLM-assisted
assessment via deductSituation RPC for high-scoring convergence cards.

Adapters collect signals from AppContext intelligence cache, cluster by
proximity/country/entity, score by weighted type diversity with 30pt cap,
and detect trend direction across 5-minute refresh cycles.

Fixes from review:
- Concurrent-run guard prevents overlapping engine executions
- Entity keyword matching uses compound patterns first to avoid false
  positives (removed ambiguous "bank", "reserve", "rate", etc.)
- LLM cache key includes score bucket to prevent collision between
  same-location clusters with different signal counts
- Outage signal dedup: disaster adapter excludes outages in countries
  with active conflict events (already captured by escalation adapter)

* fix: address review feedback on correlation engine PR #1524

- Call pruneLlmCache() at start of each run() to prevent unbounded growth
- Add LLM concurrency limit (max 3 in-flight) to prevent RPC storms
- Fix EconomicCorrelationPanel missing setMapNavigateHandler (View on map
  button was silently broken)
- Remove unsafe (m as any) timestamp cast in economic adapter; use `now`
  since MarketDataCore has no per-quote timestamp field
- Require minimum 2 signals for country/entity clusters (single signal
  is not convergence)
- Use toFixed(1) for geographic cluster keys to reduce ID collisions

* fix: prevent false convergence cards from sentinel coords and catch-all bucket

- disaster.ts: skip outages with 0/0 sentinel coordinates (infra mapping
  uses 0/0 for unknown locations, creating fake geographic hotspots)
- disaster.ts: use == null instead of truthy check for earthquake coords
  (truthy check drops legitimate events at latitude/longitude 0)
- engine.ts: drop unmatched entity labels instead of clustering into
  "general" bucket (unrelated sanctions news and market moves were merging
  into false convergence cards)

* fix: filter 0/0 sentinel coordinates in escalation outage signals

Same fix as disaster adapter: infrastructure/index.ts synthesizes 0/0
for missing outage locations, which corrupts centroid computation,
map navigation, and LLM geoContext.

---------

Co-authored-by: Elie Habib <elie.habib@gmail.com>
2026-03-13 10:46:53 +04:00
Jon Torrez
019b2a00a5 feat: USNI vessel rendering with homeport resolution (#1525)
* feat: USNI vessel rendering with homeport resolution and cluster markers

Enhanced vessel rendering on GlobeMap with USNI fleet data enrichment:
- Homeport resolution with fallback chain: parsed USNI homePort text →
  hull-number lookup table (HULL_HOMEPORT) → deployment theater coords
- Port-specific scatter offsets for in-port vessels (tighter 1-4km spread)
- USNI deployment status badges in vessel popups (escaped via esc())
- Vessel cluster markers with count + type composition indicators
- Data source badge in MapPopup for USNI-enriched vs AIS-only vessels
- Added usniHomePort field to MilitaryVessel type
- USNI_REGION_COORDINATES expanded with additional homeports/shipyards

* fix: address review findings on vessel rendering

- hideTooltip() now also hides MapPopup to prevent orphaned popups
  when switching between vessel clicks and non-vessel hovers
- Escape cluster activityType in tooltip HTML for defense-in-depth
- Add verification date to HULL_HOMEPORT lookup table

* fix: populate usniHomePort for AIS-matched vessels and localize badges

- Resolve homeport in both hull-number and name-match enrichment paths,
  not just synthetic USNI vessels. AIS vessels matched to USNI records
  now get usniHomePort populated via the same resolvePortCoords chain.
- Replace hard-coded "EST. POSITION" / "AIS LIVE" strings in MapPopup
  with i18n keys so non-English locales render correctly.

---------

Co-authored-by: Elie Habib <elie.habib@gmail.com>
2026-03-13 10:35:53 +04:00
Elie Habib
876c06455f fix: remove orphan Financial panel from commodity variant (#1471)
The `finance` key in COMMODITY_PANELS had no matching feed in
COMMODITY_FEEDS, causing the panel to render as "UNAVAILABLE".
2026-03-12 07:27:08 +04:00
Elie Habib
651cd3d08b feat(desktop): sidecar cloud proxy, domain handlers, and panel fixes (#1454)
* feat(desktop): compile domain handlers + add in-memory sidecar cache

The sidecar was broken for all 23 sebuf/RPC domain routes because
the build script (build-sidecar-handlers.mjs) never existed on main
while package.json already referenced it. This adds the missing script
and an in-memory TTL+LRU cache so the sidecar doesn't need Upstash Redis.

- Add scripts/build-sidecar-handlers.mjs (esbuild multi-entry, 23 domains)
- Add server/_shared/sidecar-cache.ts (500 entries, 50MB max, lazy sweep)
- Modify redis.ts getCachedJson/setCachedJson to use dynamic import for
  sidecar cache when LOCAL_API_MODE=tauri-sidecar (zero cost on Vercel Edge)
- Update tauri.conf.json beforeDevCommand to compile handlers
- Add gitignore pattern for compiled api/*/v1/[rpc].js

* fix(desktop): gate premium panel fetches and open footer links in browser

Skip oref-sirens and telegram-intel HTTP requests on desktop when
WORLDMONITOR_API_KEY is not present. Use absolute URLs for footer
links on desktop so the Tauri external link handler opens them in
the system browser instead of navigating within the webview.

* fix(desktop): cloud proxy, bootstrap timeouts, and panel data fixes

- Set Origin header on cloud proxy requests (fixes 401 from API key validator)
- Strip If-None-Match/If-Modified-Since headers (fixes stale 304 responses)
- Add cloud-preferred routing for market/economic/news/infrastructure/research
- Enable cloud fallback via LOCAL_API_CLOUD_FALLBACK env var in main.rs
- Increase bootstrap timeouts on desktop (8s/12s vs 3s/5s) for sidecar proxy hops
- Force per-feed RSS fallback on desktop (server digest has fewer categories)
- Add finance feeds to commodity variant (client + server)
- Remove desktop diagnostics from ServiceStatusPanel (show cloud statuses only)
- Restore DeductionPanel CSS from PR #1162
- Deduplicate repeated sidecar error logs
2026-03-12 06:50:30 +04:00
Elie Habib
2a7d7fc3fe fix: standardize brand name to "World Monitor" with space (#1463)
Replace "WorldMonitor" with "World Monitor" in all user-facing display
text across blog posts, docs, layouts, structured data, footer, offline
page, and X-Title headers. Technical identifiers (User-Agent strings,
X-WorldMonitor-Key headers, @WorldMonitorApp handle, function names)
are preserved unchanged. Also adds anchors color to Mintlify docs config
to fix blue link color in dark mode.
2026-03-12 01:28:16 +04:00
Elie Habib
dc740222f3 Reuse toFlagEmoji helper (#1418) 2026-03-11 15:37:08 +04:00
Elie Habib
678b3adf76 fix(map): merge NOTAM closures into Aviation layer (#1408)
The separate notamOverlay toggle was confusing for users since NOTAM
closures were already rendered under the flights toggle in both DeckGLMap
and GlobeMap. Remove the dead toggle from MapLayers type, layer registry,
all variant configs, panel defaults, and e2e harnesses. Fix CMD+K command
label ("military flights" -> "aviation layer") and update help description
to mention NOTAM closures.
2026-03-11 10:34:43 +04:00
Elie Habib
baacc4cf50 feat(header): show commodity & happy variants on all versions, remove theme toggle (#1407)
- Always render commodity and happy variant links in desktop and mobile nav
- Remove dark/light theme toggle button from header (available in settings)
- Add panel categories for commodity and happy variants
- Add i18n keys for new panel categories
2026-03-11 10:15:28 +04:00
Elie Habib
e880e739a9 feat(imagery): make satellite imagery globe-native, remove sidebar panel (#1381)
* feat(imagery): make satellite imagery globe-native, remove sidebar panel

Globe (3D) now owns its imagery fetching lifecycle with viewport refetch
on orbit controls end, version-guarded async fetches, and proper cleanup.
Data-loader provides initial seed only for 2D mode. Removes the
SatelliteImageryPanel sidebar panel and all callback plumbing, adds a
one-time migration to prune stale panel keys from localStorage.

- Remove SatelliteImageryPanel and all setOnImageryUpdate wiring
- Add globe viewport refetch with 800ms debounce, 2-degree skip, version counter
- Fix data-loader race with cancellable retry, globe-mode skip
- Enrich polygon + marker tooltips with resolution, mode, preview thumbnail
- Add isAllowedPreviewUrl allowlist (shared utility) for CSP-safe previews
- Clamp bbox to [-90,90]/[-180,180] for antimeridian safety
- Add destroyed guard in fetchImageryForViewport
- Panel prune migration cleans settings + 3 order keys for existing users

* fix(imagery): add initial fetch on globe init and clear stale data on disable

When satellites toggle ON (or are already on at init), immediately fetch
imagery for the current viewport instead of waiting for a controls end
event. When toggled OFF, clear stale imagery arrays so re-enabling does
not flash old footprints from a previous viewport.
2026-03-10 16:24:57 +04:00
Elie Habib
5e70c99b6b feat(orbital): enable satellite imagery panel in orbital surveillance layer (#1375) 2026-03-10 11:58:42 +04:00
Elie Habib
9e18b91f96 Fix type error: remove invalid 'satelliteImagery' from LAYER_SYNONYMS (#1370) 2026-03-10 09:05:34 +04:00
Elie Habib
7cce132574 fix(map): fix satellite imagery STAC backend, merge into Orbital Surveillance (#1364)
* fix(map): fix satellite imagery STAC backend and merge into Orbital Surveillance layer

The satellite imagery layer was broken because the backend fetched
catalog.json from Capella's S3 bucket which returns 404. Replaced with
Element 84's Earth Search STAC API (Sentinel-2 + Sentinel-1 data).

Also merged the separate Satellite Imagery layer into the existing
Orbital Surveillance layer since they are complementary features.
Adds bbox/datetime snapping for better cache hit rates.

* fix: address PR review findings for satellite imagery merge

P1: Decouple imagery fetch from satellite TLE loading. Imagery
footprints now load asynchronously (fire-and-forget) so toggling
Orbital Surveillance stays fast.

P2: Migrate old satelliteImagery URL param to satellites so existing
shared links/bookmarks preserve overlay state.

P2: Map legacy source values (e.g. "capella") to all collections
instead of returning empty results.

* fix: only refresh imagery on viewport move if scenes already loaded

Prevents imagery API calls on every pan/zoom for users who only want
orbital tracking. Viewport imagery refresh only triggers after the
initial load has already populated scenes.

* fix: restore notamOverlay entries lost during rebase conflict resolution
2026-03-10 08:44:47 +04:00
Elie Habib
3031c0ea34 feat(map): add layer search filter with synonym support (#1369)
* feat(map): add layer search filter with synonym support

Layer panel now has a search input that filters layers by label text,
layer key, and a synonym map (60+ aliases like "aviation" for flights).
Also fixes globe layer panel position to match flat map (bottom-left).
Includes i18n translations for all 21 locales.

* fix: respect hideLayerToggle in search + restore globe top position

P1: Search clear was re-showing layers hidden by hideLayerToggle().
Added data-layer-hidden attribute to mark permanently hidden toggles,
and bindLayerSearch now skips them.

P2: Restored globe panel top-left positioning (bottom=auto, top=10px).
2026-03-10 08:35:38 +04:00
Elie Habib
d3dcc53c85 feat(map): merge NOTAM closures into Aviation layer (#1363)
* feat(map): merge NOTAM closures into Aviation layer, fix click popup

Consolidate the separate "NOTAM Closures" toggle into the "Aviation"
layer so users get a single checkbox for flight delays, NOTAM rings,
and aircraft positions.

- Remove notamOverlay from MapLayers, all variants, URL state, registry
- Render NOTAM rings under flights toggle in both DeckGL and Globe maps
- Wire notam-overlay-layer click to flight popup (was missing entirely)
- Broaden NOTAM detection: restrictions (RA/RO, TFR, danger areas)
  render as major severity; closures remain severe
- Add restrictedIcaos to LoadedNotamResult for severity distinction

* fix(aviation): separate restriction NOTAMs from closures in all consumers

Restrictions (TFR, danger areas) were being added to closedIcaoCodes,
causing ops-summary to report them as full closures and CII scoring to
apply the closure penalty (+20 instead of +10).

- Keep closedIcaoCodes for real closures only, restrictedIcaoCodes separate
- Restrictions use delayType 'general' (not 'closure') so downstream code
  (popup labels, globe rings, CII scoring) treats them correctly
- ops-summary now shows RESTRICTED flag instead of CLOSED for restrictions
- buildNotamAlert/mergeNotamWithExistingAlert accept delayType param
2026-03-10 08:17:23 +04:00
Elie Habib
fc134647a5 feat(map): add NOTAM overlay + satellite imagery integration (#1356)
* feat(map): add NOTAM overlay + satellite imagery integration

NOTAM Overlay:
- Expand airport monitoring from MENA-only to 64 global airports
- Add ScatterplotLayer (55km red rings) on flat map for airspace closures
- Add CSS-pulsing ring markers on globe for closures
- Independent of flights layer toggle (works when flights OFF)
- Bump NOTAM cache key v1 to v2

Satellite Imagery:
- Add Capella SAR STAC catalog proxy at /api/imagery/v1
- SSRF protection via URL allowlist + bbox/datetime validation
- SatelliteImageryPanel with preview thumbnails and scene metadata
- PolygonLayer footprints on flat map with viewport-triggered search
- Polygon footprints on globe with "Search this area" button
- Full variant only, default disabled

Layer key propagation across all 23+ files including variants,
harnesses, registry, URL state, and renderer channels.

* fix(imagery): wire panel data flow, fix viewport race, add datetime filter

P1 fixes:
- Imagery scenes now flow through MapContainer.setOnImageryUpdate()
  callback, making data available to both renderers and panel
- Add version guard to fetchImageryForViewport() preventing stale
  responses from overwriting newer viewport data
- Wire SatelliteImageryPanel.update() and setOnSearchArea() in
  panel-layout.ts (panel was previously unhooked)
- Globe mode "Search this area" fetches via MapContainer.getBbox()

P2 fix:
- search-imagery.ts now filters STAC items by datetime range when
  the client provides the datetime parameter

Also:
- Add MapContainer.getBbox() for viewport-aware imagery fetching
- Add DeckGLMap.getBbox() public method
- Data-loader layer toggle triggers initial imagery fetch

* fix(imagery): complete source filter + fix date-only end bound

- Filter STAC items by constellation when source param is provided,
  making the API contract match actual behavior
- Date-only end bounds (YYYY-MM-DD without T) now include the full
  day (23:59:59.999Z) instead of only midnight
2026-03-10 07:19:02 +04:00
lspassos1
a3b120c13e fix(map): replace crude Yemen/Red Sea conflict zone with detailed coastline polygon (#1330)
Replace the 5-point bounding box with a 31-point polygon that traces
Yemen's actual Red Sea and Gulf of Aden coastline, including the
Bab el-Mandeb strait chokepoint where Houthi shipping attacks occur.

Before: rough rectangle covering parts of Djibouti and Eritrea
After: polygon follows Yemen's coast from Al Hudaydah through the
strait along the Gulf of Aden to Oman border

References: #1151
2026-03-09 23:25:07 +04:00
Elie Habib
1a3e8ecc4f fix(cii): add Cuba to risk scoring pipeline (#1321)
Cuba is experiencing a severe humanitarian crisis (grid collapse, 20h+
blackouts, protests, UN collapse warning) but was completely absent from
CII because it was not in TIER1_COUNTRIES, CURATED_COUNTRIES, or any
server-side scoring config. Added with baseline risk 45, multiplier 2.0.
2026-03-09 10:11:42 +04:00
Elie Habib
a433e6a8fe fix(globe): disable CII Instability layer in 3D mode (#1292)
The choropleth wraps the entire globe incorrectly. Restrict to flat
map only until the rendering is fixed.
2026-03-08 23:30:58 +04:00
Elie Habib
595e3dbb86 feat: premium finance stock analysis suite (#1268)
* Add premium finance stock analysis suite

* docs: link premium finance from README

Add Premium Stock Analysis entry to the Finance & Markets section
with a link to docs/PREMIUM_FINANCE.md.

* fix: address review feedback on premium finance suite

- Chunk Redis pipelines into batches of 200 (Upstash limit)
- Add try-catch around cachedFetchJson in backtest handler
- Log warnings on Redis pipeline HTTP failures
- Include name in analyze-stock cache key to avoid collisions
- Change analyze-stock and backtest-stock gateway cache to 'slow'
- Add dedup guard for concurrent ledger generation
- Add SerpAPI date pre-filter (tbs=qdr:d/w)
- Extract sanitizeSymbol to shared module
- Extract buildEmptyAnalysisResponse helper
- Fix RSI to use Wilder's smoothing (matches TradingView)
- Add console.warn for daily brief summarization errors
- Fall back to stale data in loadStockBacktest on error
- Make daily-market-brief premium on all platforms
- Use word boundaries for short token headline matching
- Add stock-analysis 15-min refresh interval
- Stagger stock-analysis and backtest requests (200ms)
- Rename signalTone to stockSignalTone
2026-03-08 22:54:40 +04:00
Elie Habib
3c3a374f81 feat(search): add 16 missing layer toggles to Cmd+K command palette (#1289)
Only 16 of 31 full-variant layers had individual toggle commands in the
command palette. Adds toggles for GPS jamming, orbital surveillance,
UCDP events, Iran attacks, irradiators, spaceports, datacenters,
military activity, natural events, waterways, economic centers,
critical minerals, CII instability, day/night, and sanctions.
2026-03-08 22:35:06 +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
4306322b67 fix(seo): comprehensive SEO improvements for /pro and main pages (#1271)
Pro page (/pro):
- Shorten title to ≤60 chars for SERP visibility
- Fix canonical + all URLs to www.worldmonitor.app
- Fix multiple H1 (Enterprise modal H1→H2)
- Fix heading hierarchy (H2→H4 jumps → proper H2→H3→H4)
- Sync FAQPage JSON-LD with all 8 visible FAQs
- Add twitter:title, twitter:description, og:locale, og:image:alt
- Add hreflang tags for all 21 supported languages
- Add <noscript> fallback with key SEO content
- Add SSG prerender script (injects text into built HTML)
- Self-host WIRED logo SVG (was loading from Wikipedia)
- Add aria-labels on footer links and CTAs
- Fix i18n to read ?lang= query parameter (was only localStorage + navigator)

Main page:
- Fix canonical + OG/Twitter URLs to www.worldmonitor.app
- Update twitter:site/creator to @worldmonitorai
- Add <noscript> with H1, description, features, and /pro link
- Add hreflang tags for all 21 languages
- Add og:image:alt meta tag
- Add @worldmonitorai to JSON-LD sameAs
- Align title with variant-meta.ts

Shared:
- Update sitemap.xml URLs to www.worldmonitor.app
- Update robots.txt sitemap reference to www
- Update variant-meta.ts full variant URL to www
2026-03-08 14:46:20 +04:00
Elie Habib
f2c83e59b1 fix(map): prevent ghost layers that render without a toggle (#1264)
* fix(map): prevent ghost layers that render without a toggle

Layers enabled in variant defaults but missing from VARIANT_LAYER_ORDER
rendered on the map with no UI toggle to turn them off. Commodity variant
had 6 ghost layers including undersea cables.

Add sanitizeLayersForVariant() guardrail that forces any layer not in
the variant's allowed list to false — applied at both fresh init and
localStorage load. Replace the fragile happy-only hardcoded blocklist
with this generic mechanism.

Add regression test covering all 10 variant×platform combinations.

* fix(map): sanctions renderers + sanitize URL-derived layers

- Fix sanctions layer having empty renderers[] — getLayersForVariant
  filtered it out so it had no toggle despite being in VARIANT_LAYER_ORDER
- Apply sanitizeLayersForVariant() after URL state merge, replacing
  fragile hardcoded tech/happy blocklists — deeplinks can no longer
  enable layers outside the variant's allowed set
- Add renderer coverage to guardrail test (11 cases)

* fix(map): remove no-op sanctions toggle + guard search-manager layer mutations

- sanctions has no DeckGL/Globe renderer (only SVG map country fills),
  so revert renderers to [] and remove from VARIANT_LAYER_ORDER — no
  more no-op toggle in desktop mode
- Set sanctions defaults to false across all variants (SVG map has its
  own toggle list independent of VARIANT_LAYER_ORDER)
- Guard search-manager layer commands (layers:all, presets, layer:*)
  against enabling off-variant layers via getAllowedLayerKeys()
- Add renderer-coverage assertion to guardrail test

* fix(map): make layer sanitizer renderer-aware for SVG-only layers

sanctions is implemented in SVG map (country fills) but not DeckGL/Globe.
Previous commit removed it from VARIANT_LAYER_ORDER, causing the sanitizer
to strip it on reload — breaking SVG map state persistence.

Add SVG_ONLY_LAYERS concept: layers allowed by the sanitizer but excluded
from DeckGL/Globe toggle UI. sanctions restored to defaults for full,
finance, and commodity variants where the SVG map exposes it.

- getAllowedLayerKeys() now includes SVG_ONLY_LAYERS
- VARIANT_LAYER_ORDER remains DeckGL/Globe-only (renderer test enforced)
- Guardrail test updated to check both sources
2026-03-08 14:14:16 +04:00
Elie Habib
d6c9176213 Revert "fix(scripts): sync package-lock.json with h3-js dependency (#1254)" (#1256)
This reverts commit 4816e27d3c.
2026-03-08 08:57:20 +04:00
Elie Habib
4816e27d3c fix(scripts): sync package-lock.json with h3-js dependency (#1254)
* Add premium stock analysis for finance variant

* fix(scripts): sync package-lock.json with h3-js dependency

Railway npm ci requires lock file in sync with package.json.

* fix(market): narrow undefined check for TS strict null safety
2026-03-08 08:45:12 +04:00
Alex
8a71b07271 feat: add data about tech HQs located in Ireland (#1244)
* Added dogpatch

* Added tech hqs
2026-03-08 08:19:12 +04:00
Elie Habib
bf7b03ab8f refactor: guard panel creation by variant config (#1221)
* fix: three panel issues — Tech Readiness toggle, Crypto top 10, FIRMS key check

1. #1132 — Add tech-readiness to FULL_PANELS so it appears in the
   Settings toggle list for Full/Geopolitical variant users.

2. #979 — Expand crypto panel from 4 coins to top 10 by market cap
   (BTC, ETH, USDT, BNB, SOL, XRP, USDC, ADA, DOGE, TRX) across
   client config, server metadata, CoinPaprika fallback map, and
   seed script.

3. #997 — Check isFeatureAvailable('nasaFirms') before loading FIRMS
   data. When the API key is missing, show a clear "not configured"
   message instead of the generic "No fire data available".

Closes #1132, closes #979, closes #997

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

* fix: replace stablecoins with AVAX/LINK, remove duplicate key, revert FIRMS change

- Replace USDT/USDC (stablecoins pegged ~$1) with AVAX and LINK
- Remove duplicate 'usd-coin' key in COINPAPRIKA_ID_MAP
- Add CoinPaprika fallback IDs for avalanche-2 and chainlink
- Revert FIRMS API key gating (handled differently now)
- Add sync comments across the 3 crypto config locations

* fix: update AIS relay + seed CoinPaprika fallback for all 10 coins

The AIS relay (primary seeder) still had the old 4-coin list.
The seed script's CoinPaprika fallback map was also missing the
new coins. Both now have all 10 entries.

* refactor: DRY crypto config into shared/crypto.json

Single source of truth for crypto IDs, metadata, and CoinPaprika
fallback mappings. All 4 consumers now import from shared/crypto.json:
- src/config/markets.ts (client)
- server/worldmonitor/market/v1/_shared.ts (server)
- scripts/seed-crypto-quotes.mjs (seed script)
- scripts/ais-relay.cjs (primary relay seeder)

Adding a new coin now requires editing only shared/crypto.json.

* chore: fix pre-existing markdown lint errors in README.md

Add blank lines between headings and lists per MD022/MD032 rules.

* fix: correct CoinPaprika XRP mapping and add crypto config test

- Fix xrp-ripple → xrp-xrp (current CoinPaprika id)
- Add tests/crypto-config.test.mjs: validates every coin has meta,
  coinpaprika mapping, unique symbols, no stablecoins, and valid
  id format — bad fallback ids now fail fast

* test: validate CoinPaprika ids against live API

The regex-only check wouldn't have caught the xrp-ripple typo.
New test fetches /v1/coins from CoinPaprika and asserts every
configured id exists. Gracefully skips if API is unreachable.

* fix(test): handle network failures in CoinPaprika API validation

Wrap fetch in try-catch so DNS failures, timeouts, and rate limits
skip gracefully instead of failing the test suite.

* refactor: guard panel creation by variant config

Only create panels listed in the active variant's DEFAULT_PANELS.
Previously, ~30 panels were created unconditionally for ALL variants,
wasting DOM nodes and memory (e.g., tech variant got 15+ geopolitical
panels it never uses).

Changes:
- Add shouldCreatePanel(), createNewsPanel(), createPanel() helpers
  that gate creation on DEFAULT_PANELS membership
- Add shouldCreatePanel guard inside lazyPanel() for lazy-loaded panels
- Replace 27 repetitive 4-line NewsPanel blocks with one-liner calls
- Replace variant-specific SITE_VARIANT blocks with per-panel guards
- Add null guards to all hard panel dereferences in data-loader.ts
  and event-handlers.ts (markets, heatmap, commodities, crypto,
  polymarket, monitors)
- Add commodity variant to trade-policy/supply-chain data loading
- Remove climate/satellite-fires from COMMODITY_PANELS (no data loader)
- Guard giving data fetch with DEFAULT_PANELS check
- Add FEEDS loop guard to skip panels not in config
- Add DEV-mode assertion warning about unconfigured panels
- Add panel-config-guardrails.test.mjs (static analysis test)

* fix: tech-readiness refresh and settings visibility for full variant

- Add full variant to tech-readiness data loading condition
- Add tech-readiness to full variant's dataTracking category map

---------

Co-authored-by: Nicolas Gomes Ferreira Dos Santos <ndossantos@ucsd.edu>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 22:40:05 +04:00
Elie Habib
cad6b9c4e0 feat(infrastructure): expand submarine cables to 86 via TeleGeography API (#1224)
* feat(infrastructure): expand submarine cables to 86 via TeleGeography API seed

- Add `seed-submarine-cables.mjs` Railway cron script fetching 86 strategic
  cables from TeleGeography API (was 19 hand-curated)
- Update `geo.ts` static baseline with full cable data (routes, landing points,
  owners, RFS year, regions)
- Update `get-cable-health.ts` cable name/landing mappings for new slug-based IDs
- Add `data?.cables?.length` to `_seed-utils.mjs` record count heuristic
- Update `map-harness.ts` cable ID references
- Remove GitHub Actions workflows for UCDP and WB indicators (Railway cron only)

* fix(infrastructure): cable route matching, name false positives, validation threshold

- Fix route geometry: only strip numeric suffix when result matches a known
  cable slug, preventing seamewe-6→seamewe, farice-1→farice, etc.
- Fix name matching: use word-boundary regex instead of substring includes;
  disambiguate short names (ACE→ACE CABLE, SAFE→SAFE CABLE, PEACE→PEACE CABLE,
  TEAMS→TEAMS CABLE) to prevent false matches on common NGA words
- Raise validation threshold from 50 to 75 (88% success required) to prevent
  heavily partial upstream results from overwriting good cached data

* fix(infrastructure): tie validation threshold to 90% of configured cable count

Dynamic threshold based on CABLE_REGIONS length instead of a hardcoded number.
Currently requires >= 78 of 86 cables (90%).
2026-03-07 22:24:58 +04:00
Elie Habib
dd127447c0 refactor: consolidate duplicated market data lists into shared JSON configs (#1212)
Adding a new item (crypto, ETF, stablecoin, gulf symbol, etc.) previously
required editing 2-4 files because the same list was hardcoded independently
in seed scripts, RPC handlers, and frontend config. Following the proven
shared/crypto.json pattern, extract 6 new shared JSON configs so each list
has a single source of truth.

New shared configs:
- shared/stablecoins.json (ids + coinpaprika mappings)
- shared/etfs.json (BTC spot ETF tickers + issuers)
- shared/gulf.json (GCC indices, currencies, oil benchmarks)
- shared/sectors.json (sector ETF symbols + names)
- shared/commodities.json (VIX, gold, oil, gas, silver, copper)
- shared/stocks.json (market symbols + yahoo-only set)

All seed scripts, RPC handlers, and frontend config now import from
these shared JSON files instead of maintaining independent copies.
2026-03-07 22:00:55 +04:00
Elie Habib
c385cd5be5 fix(geo): improve Sudan and Myanmar conflict zone polygons and data (#1216)
- Sudan: expand from 4-point rectangle to 24-point country outline,
  update casualties (150k+), displaced (14M+ IDPs, 3M+ refugees),
  add keywords (rsf, saf, el fasher, port sudan, wad madani, famine),
  expand key developments and parties
- Myanmar: expand from 4-point rectangle to 25-point country outline,
  upgrade intensity medium→high, update displaced (3M+), add keywords
  (shan, rakhine, arakan army, kachin, pdf, tatmadaw), name specific
  armed groups (AA, MNDAA, TNLA, KIA, PDF), detail key developments
2026-03-07 21:24:18 +04:00
Elie Habib
736426a8ed fix(aviation): correct cancellation rate calculation and add 12 airports (#1209)
- Use resolved-flights-only denominator (landed+active+cancelled+diverted)
  instead of all flights including scheduled/unknown. DXB was showing 15%
  cancelled (NORMAL) when the real rate among resolved flights is ~58% (MAJOR).
- Add flight_date=today filter to AviationStack API calls to avoid mixing
  historical/future flights into today's cancellation stats.
- Factor cancellation rate into ops summary table severity (was ignored,
  only delay minutes were considered). Uses shared severityFromCancelRate()
  to avoid threshold duplication.
- Add minimum resolved threshold (>=10) before using resolved denominator
  to prevent extreme percentages from tiny samples.
- Add 12 major airports to AviationStack monitoring: YVR, SCL, DUB, LIS,
  ATH, WAW, CAN, TPE, MNL, AMM, KWI, CMN (40→52 airports).
2026-03-07 19:55:16 +04:00
Nicolas Dos Santos
7b9426299d fix: Tech Readiness toggle, Crypto top 10, FIRMS API key check (#1132, #979, #997) (#1135)
* fix: three panel issues — Tech Readiness toggle, Crypto top 10, FIRMS key check

1. #1132 — Add tech-readiness to FULL_PANELS so it appears in the
   Settings toggle list for Full/Geopolitical variant users.

2. #979 — Expand crypto panel from 4 coins to top 10 by market cap
   (BTC, ETH, USDT, BNB, SOL, XRP, USDC, ADA, DOGE, TRX) across
   client config, server metadata, CoinPaprika fallback map, and
   seed script.

3. #997 — Check isFeatureAvailable('nasaFirms') before loading FIRMS
   data. When the API key is missing, show a clear "not configured"
   message instead of the generic "No fire data available".

Closes #1132, closes #979, closes #997

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

* fix: replace stablecoins with AVAX/LINK, remove duplicate key, revert FIRMS change

- Replace USDT/USDC (stablecoins pegged ~$1) with AVAX and LINK
- Remove duplicate 'usd-coin' key in COINPAPRIKA_ID_MAP
- Add CoinPaprika fallback IDs for avalanche-2 and chainlink
- Revert FIRMS API key gating (handled differently now)
- Add sync comments across the 3 crypto config locations

* fix: update AIS relay + seed CoinPaprika fallback for all 10 coins

The AIS relay (primary seeder) still had the old 4-coin list.
The seed script's CoinPaprika fallback map was also missing the
new coins. Both now have all 10 entries.

* refactor: DRY crypto config into shared/crypto.json

Single source of truth for crypto IDs, metadata, and CoinPaprika
fallback mappings. All 4 consumers now import from shared/crypto.json:
- src/config/markets.ts (client)
- server/worldmonitor/market/v1/_shared.ts (server)
- scripts/seed-crypto-quotes.mjs (seed script)
- scripts/ais-relay.cjs (primary relay seeder)

Adding a new coin now requires editing only shared/crypto.json.

* chore: fix pre-existing markdown lint errors in README.md

Add blank lines between headings and lists per MD022/MD032 rules.

* fix: correct CoinPaprika XRP mapping and add crypto config test

- Fix xrp-ripple → xrp-xrp (current CoinPaprika id)
- Add tests/crypto-config.test.mjs: validates every coin has meta,
  coinpaprika mapping, unique symbols, no stablecoins, and valid
  id format — bad fallback ids now fail fast

* test: validate CoinPaprika ids against live API

The regex-only check wouldn't have caught the xrp-ripple typo.
New test fetches /v1/coins from CoinPaprika and asserts every
configured id exists. Gracefully skips if API is unreachable.

* fix(test): handle network failures in CoinPaprika API validation

Wrap fetch in try-catch so DNS failures, timeouts, and rate limits
skip gracefully instead of failing the test suite.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Elie Habib <elie.habib@gmail.com>
2026-03-07 18:23:32 +04:00
Elie Habib
6e07ace424 fix(panels): green home row in World Clock + show Tech Readiness on all variants (#1202)
- Change wc-home background from accent to semantic-positive (green)
  with left border highlight, matching original design
- Add tech-readiness panel to dataTracking category (full variant)
  so it loads on all site variants, not just tech
2026-03-07 15:52:38 +04:00
Elie Habib
c0339d8233 fix(web): remove premium restrictions from web and fix map header layout (#1178)
- Gate all premium panel restrictions (locked/enhanced) behind
  isDesktopRuntime() — web users see no locks, PRO badges, or paywalls
- Affected panels: cii, strategic-risk, gdelt-intel, supply-chain,
  oref-sirens, telegram-intel
- Affected map layers: iranAttacks, gpsJamming, ciiChoropleth
- Add missing .map-header-actions CSS (display:flex) — fixes 2D/3D
  toggle, fullscreen, and pin buttons stacking vertically instead of
  horizontal row (regression from 8d83aa02 replacing inline styles)
2026-03-07 12:13:01 +04:00
Elie Habib
8d83aa02eb fix(economic): guard against undefined BIS and spending data (#1162)
* feat: premium panel gating, code cleanup, and backend simplifications

Recovered stranded changes from fix/desktop-premium-error-unification.

Premium gating:
- Add premium field ('locked'|'enhanced') to PanelConfig and LayerDefinition
- Panel.showLocked() with lock icon, CTA button, and _locked guard
- PRO badge for enhanced panels when no WM API key
- Exponential backoff auto-retry on showError() (15s→30s→60s→180s cap)
- Gate oref-sirens and telegram-intel panels behind WM API key
- Lock gpsJamming and iranAttacks layer toggles, badge ciiChoropleth
- Add tauri-titlebar drag region for custom titlebar

Code cleanup:
- Extract inline CSS from AirlineIntelPanel, WorldClockPanel to panels.css
- Remove unused showGeoError() from CountryBriefPage
- Remove dead geocodeFailed/retryBtn/closeBtn locale keys (20 files)
- Clean up var names and inline styles across 6 components

Backend:
- Remove seed-meta throttle from redis.ts (unnecessary complexity)
- Risk scores: call handler functions directly instead of raw Redis reads
- Update OpenRouter model to gpt-oss-safeguard-20b:nitro
- Add direct UCDP API fetching with version probing

Config:
- Remove titleBarStyle: Overlay from tauri.conf.json
- Add build:pro and build-sidecar-handlers to build:desktop
- Remove DXB/RUH from default aviation watchlist
- Simplify reverse-geocode (remove AbortController wrapper)

* fix: cast handler requests to any for API tsconfig compat

* fix: revert stale changes that conflict with merged PRs

Reverts files to main versions where old branch changes would
overwrite intentional fixes from PRs #1134, #1138, #1144, #1154:

- news/_shared.ts: keep gemini-2.5-flash model (not stale gpt-oss)
- redis.ts: keep seed-meta throttle from PR #1138
- reverse-geocode.ts: keep AbortController timeout from PR #1134
- CountryBriefPage.ts: keep showGeoError() from PR #1134
- country-intel.ts: keep showGeoError usage from PR #1134
- get-risk-scores.ts: revert non-existent imports
- watchlist.ts: keep DXB/RUH airports from PR #1144
- locales: restore geocodeFailed/retryBtn/closeBtn keys

* fix: neutralize language, parallel override loading, fetch timeout

- Rename conflict zone from "War" to "Border Conflict", intensity high→medium
- Rewrite description to factual language (no "open war" claim)
- Load country boundary overrides in parallel with main GeoJSON
- Neutralize comments/docs: reference Natural Earth source, remove political terms
- Add 60s timeout to Natural Earth fetch script (~24MB download)
- Add trailing newline to GeoJSON override file

* fix: restore caller messages in Panel errors and vessel expansion in popups

- Move UCDP direct-fetch cooldown after successful fetch to avoid
  suppressing all data for 10 minutes on a single failure
- Use caller-provided messages in showError/showRetrying instead of
  discarding them; respect autoRetrySeconds parameter
- Restore cluster-toggle click handler and expandable vessel list
  in military cluster popups
2026-03-07 09:43:27 +04:00
Elie Habib
a61ec28ba3 chore: reduce default map layers, add user-requests doc (#1141)
* fix(desktop): settings UI redesign, IPC security hardening, release profile

Settings window:
- Add titlebar drag region (macOS traffic light clearance)
- Move Export/Import from Overview to Debug & Logs section
- Category cards grid changed to 3-column layout

Security (IPC trust boundary):
- Add require_trusted_window() to get_desktop_runtime_info, open_url,
  open_live_channels_window_command, open_youtube_login
- Validate base_url in open_live_channels_window_command (localhost-only http)

Performance:
- Add [profile.release] with fat LTO, codegen-units=1, strip, panic=abort
- Reuse reqwest::Client via app state with connection pooling
- Debounce window resize handler (150ms) in EventHandlerManager

* feat(pro): add Pro waitlist landing page with referral system

- React 19 + Vite 6 + Tailwind v4 landing page at /pro
- Cloudflare Turnstile + honeypot bot protection
- Resend transactional confirmation emails with branded template
- Viral referral system: unique codes, position tracking, social share
- Convex schema: referralCode, referredBy, referralCount fields + counters table
- O(1) position counter pattern instead of O(n) collection scan
- SEO: structured data, sitemap, scrolling source marquee
- Vercel routing: /pro rewrite + cache headers + SPA exclusion
- XSS-safe DOM rendering (no innerHTML with user data)

* chore: disable natural & economic map layers by default, add user-requests doc

Reduce default-on map layers from 11 to 9 for better first-load
performance. Natural disasters and economic overlays remain available
via the layer toggle — just no longer enabled on first visit.

Also adds docs/user-requests.md compiling all feature requests from
GitHub issues (55+) and discussions (40+ threads, 391 comments).
2026-03-06 23:50:34 +04:00
Elie Habib
f56f84d6f9 fix(webcams): MTV Lebanon as live stream, not RSS (reverts #1121) (#1122)
* Revert "feat(feeds): add MTV Lebanon News YouTube feed to Middle East region (#1121)"

This reverts commit 5bee51ab79.

* feat(webcams): add MTV Lebanon News live stream to Middle East region
2026-03-06 14:44:43 +04:00
Elie Habib
5bee51ab79 feat(feeds): add MTV Lebanon News YouTube feed to Middle East region (#1121) 2026-03-06 14:18:56 +04:00
Elie Habib
9a6ae69124 fix(map): use CORS-enabled R2 URL for PMTiles in Tauri desktop (#1119)
The CF proxy custom domain (maps.worldmonitor.app) doesn't forward R2
CORS headers, so PMTiles fails silently in Tauri where the origin is
https://tauri.localhost. Web works because it's same-origin via CF proxy.

Detect Tauri via __TAURI__ and swap to VITE_PMTILES_URL_PUBLIC (direct
R2 public URL with CORS). Web continues using the CF proxy URL for
edge caching benefits.

Requires adding https://tauri.localhost to R2 CORS allowed origins.
2026-03-06 13:53:55 +04:00
Elie Habib
cfede93516 feat(channels): add Rudaw TV live stream and RSS feed (#1117)
- Add Rudaw (Kurdish Iraqi) as HLS live channel in Middle East region
- HLS stream: svs.itworkscdn.net/rudawlive/rudawlive.smil/playlist.m3u8
- Add Rudaw RSS feed via Google News (not enabled by default)
- Relax DEV check for HLS-only channels without fallbackVideoId
2026-03-06 13:03:49 +04:00