* 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.
* 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)
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.
* 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.
* 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
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.
* 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>
* 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>
* 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
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.
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.
- 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
* 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.
* 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
* 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).
* 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
* 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
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
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.
* 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
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.
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)
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
* 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
* 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>
* 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%).
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.
- 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).
* 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>
- 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
* 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
* 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
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.
- 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