Commit Graph

87 Commits

Author SHA1 Message Date
Elie Habib
31b9643583 feat(supply-chain): surface CorridorRisk intelligence in chokepoint panel (#1616)
The CorridorRisk API provides rich intelligence that we were storing
but not displaying. Now surfaced in the panel:

- risk_summary: live intelligence narrative shown in the description
  area (e.g. "Armed confrontations are active across the Persian Gulf
  with 52% of events classified as armed clashes")
- risk_report.action: routing recommendation shown when card is
  expanded (e.g. "Recommend REROUTING via Cape of Good Hope for all
  non-essential Gulf cargo")

Changes:
- Proto: add risk_summary and risk_report_action to TransitSummary
- Relay: extract risk_report.action in seedCorridorRisk, pass both
  fields through seedTransitSummaries
- Handler: pass through to API response + include in description
- UI: riskSummary in risk row, riskReportAction in expanded view
2026-03-15 02:40:33 +04:00
Elie Habib
3cbf1169a8 fix(supply-chain): add CorridorRisk diagnostic logging and HTML detection (#1611)
corridorrisk: EMPTY in health despite relay running new code. The seed
produces zero log output, making it impossible to diagnose. Added:
- Log fetch start ("Fetching...") and in-flight skip
- Log HTTP error with response body and content-type
- Detect HTML responses (Cloudflare challenge) before JSON.parse
- Increase timeout from 10s to 15s for slow Railway regions
2026-03-15 02:09:17 +04:00
Elie Habib
ca451c732d fix: add forecast RPC_CACHE_TIER entry, fix transitSummary test regex (#1610)
- Add /api/forecast/v1/get-forecasts to RPC_CACHE_TIER as 'medium'
  (route-cache-tier test requires every GET route has explicit entry)
- Fix transitSummary test regex to accept optional field syntax (?:)
  from proto codegen v0.7.0
2026-03-15 02:02:37 +04:00
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
39cf56dd4d perf: reduce ~14M uncached API calls/day via client caches + workbox fix + USNI Railway migration (#1605)
* perf: reduce uncached API calls via client-side circuit breaker caches

Add client-side circuit breaker caches with IndexedDB persistence to the
top 3 uncached API endpoints (CF analytics: 10.5M uncached requests/day):

- classify-events (5.37M/day): 6hr cache per normalized title, shouldCache
  guards against caching null/transient failures
- get-population-exposure (3.45M/day): 6hr cache per coordinate key
  (toFixed(4) for ~11m precision), 64-entry LRU
- summarize-article (1.68M/day): 2hr cache per headline-set hash via
  buildSummaryCacheKey, eliminates both cache-check and summarize RPCs

Fix workbox-*.js getting no-cache headers (3.62M/day): exclude from SPA
catch-all regex in vercel.json, add explicit immutable cache rule for
content-hashed workbox files.

Migrate USNI fleet fetch from Vercel edge to Railway relay (gold standard):
- Add seedUSNIFleet() loop to ais-relay.cjs (6hr interval, gzip support)
- Make server handler Redis-read-only (435 lines reduced to 38)
- Move usniFleet from ON_DEMAND to BOOTSTRAP_KEYS in health.js
- Add persistCache + shouldCache to client breaker

Estimated reduction: ~14.3M uncached requests/day.

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

P1: Include SummarizeOptions in summary cache key to prevent cross-option
cache pollution (e.g. cloud summary replayed after user disables cloud LLMs).

P2: Document that forceRefresh is intentionally ignored now that USNI
fetching moved to Railway relay (Vercel is Redis-read-only).

* fix: reject forceRefresh explicitly instead of silently ignoring it

Return an error response with explanation when forceRefresh=true is sent,
rather than silently returning cached data. Makes the behavior regression
visible to any caller instead of masking it.

* fix(build): set worker.format to 'es' for Vite 6 compatibility

Vite 6 defaults worker.format to 'iife' which fails with code-splitting
workers (analysis.worker.ts uses dynamic imports). Setting 'es' fixes
the Vercel production build.

* fix(test): update deploy-config test for workbox regex exclusion

The SPA catch-all regex test hard-coded the old pattern without the
workbox exclusion. Update to match the new vercel.json source pattern.
2026-03-15 00:52:10 +04:00
Elie Habib
48c86842ac fix(test): update CorridorRisk tests for open beta API (no auth) (#1604)
PR #1598 switched CorridorRisk to the open beta API at
corridorrisk.io/api/corridors (no API key required). The old tests
expected CORRIDOR_RISK_API_KEY and Bearer token auth, causing 4 CI
failures. Updated to match the new endpoint and score-based risk
level derivation.
2026-03-14 23:41:28 +04:00
Elie Habib
13bb3ef080 fix(supply-chain): increase Redis timeout for PortWatch and remove content height cap (#1598)
* fix(supply-chain): increase Redis timeout for PortWatch and remove content height cap

Root cause: getCachedJson has a 1500ms timeout, but the PortWatch
payload (~149KB for 13 chokepoints x 175 days) exceeds this on
high-latency Edge regions. The fetch silently times out and returns
null, so the handler builds responses with empty transit summaries.

Fix: add optional timeoutMs param to getCachedJson, use 5000ms for
the PortWatch fetch. Also remove the 300px max-height on
.economic-content so the Supply Chain panel fills available height.

* refactor(supply-chain): move transit summary assembly to Railway relay

Vercel Edge was reading 3 large Redis keys (PortWatch 149KB, transit
counts, CorridorRisk) and assembling transit summaries on every request.
The 1500ms Redis timeout caused the 149KB PortWatch fetch to silently
fail on high-latency Edge regions (Mumbai bom1), leaving all transit
data empty.

Now Railway builds the pre-assembled transit summaries (including
anomaly detection) and writes them to a single key. Vercel reads
ONE small pre-built key instead of 3 raw keys.

Flow: Railway seeds PortWatch + transit counts -> builds summaries ->
writes supply_chain:transit-summaries:v1 -> Vercel reads it.

This follows the gold standard: "Vercel reads Redis ONLY; Railway
makes ALL external API calls and data assembly."

* test(supply-chain): add sync tests for relay threat levels and name mappings

detectTrafficAnomalyRelay and CHOKEPOINT_THREAT_LEVELS in the relay are
duplicated from _scoring.mjs and get-chokepoint-status.ts because
ais-relay.cjs is CJS. Added sync tests that validate:
- Every canonical chokepoint has a relay threat level
- Relay threat levels match handler CHOKEPOINTS config
- RELAY_NAME_TO_ID covers all canonical chokepoints

This catches drift between the two source-of-truth files.

* fix(ui): restore bounded scroll on economic-content with flex layout

The previous fix replaced max-height: 300px with flex: 1 1 auto, but
.panel-content was not a flex container so the flex rule was ignored.
This caused tabs to scroll away with the content.

Fix: use :has(.economic-content) to make .panel-content a flex column
only for panels with tabbed economic content. Tabs stay pinned, content
area scrolls independently.

* feat(supply-chain): fix CorridorRisk API integration (open beta, no key needed)

The CorridorRisk API is in open beta at corridorrisk.io/api/corridors
(not api.corridorrisk.io/v1/corridors). No API key required during beta.

Changes:
- Fix URL to corridorrisk.io/api/corridors
- Remove API key requirement (open beta)
- Update name matching for actual API names (e.g. "Persian Gulf &
  Strait of Hormuz" -> hormuz_strait)
- Derive riskLevel from score (>=70 critical, >=50 high, etc.)
- Store riskScore, vesselCount, eventCount7d, riskSummary
- Feed CorridorRisk data into transit summaries

* test(supply-chain): comprehensive transit summary integration tests

75 tests across 10 suites covering:
- Relay seedTransitSummaries assembly (Redis key, fields, triggers)
- CorridorRisk name mapping and risk level derivation from score
- Handler reads pre-built summaries (not raw upstream keys)
- Handler isolation: no PortWatchData/CorridorRiskData/CANONICAL_CHOKEPOINTS imports
- detectTrafficAnomalyRelay sync with _scoring.mjs (side-by-side execution)
- detectTrafficAnomaly edge cases (boundaries, threat levels, unsorted history)
- CHOKEPOINT_THREAT_LEVELS relay-handler sync validation

* fix(supply-chain): hydrate transit summaries from Redis on relay restart

After relay restart, latestPortwatchData and latestCorridorRiskData are
null. The initial seedTransitSummaries call (35s after boot) would
return early with no data, leaving the transit-summaries:v1 key stale
until the next PortWatch seed completes (6+ seconds later).

Fix: seedTransitSummaries now reads persisted PortWatch and CorridorRisk
data from Redis when in-memory state is empty. This covers the cold-start
gap so Vercel always has fresh transit summaries.

Also adds 5 tests validating the hydration path order and assignment.

* fix(supply-chain): add fallback to raw Redis keys when pre-built summaries are empty

P1: If supply_chain:transit-summaries:v1 is absent (relay not deployed,
restart in progress, or transient PortWatch failure), the handler now
falls back to reading the raw portwatch, corridorrisk, and transit count
keys directly and assembling summaries on the fly.

This ensures corridor risk data (riskLevel, incidentCount7d, disruptionPct)
is never silently zeroed out, and users keep history/counts even during
the 6-hour PortWatch re-seed window.

Strategy: pre-built summaries (fast path) -> raw keys fallback (slow path)
-> all-zero defaults (last resort).
2026-03-14 23:27:27 +04:00
Elie Habib
519ae55980 feat(supply-chain): detect AIS dark-transit anomalies in war zones (#1595)
* feat(supply-chain): detect AIS dark-transit anomalies in war zones

When PortWatch history shows >50% traffic drop in war_zone or critical
chokepoints, surface it as intelligence: "Traffic down X% vs 30-day
baseline — vessels may be transiting dark (AIS off)".

The absence of AIS signals in conflict zones like Hormuz is itself a
signal (vessels disabling transponders to avoid targeting).

Changes:
- Add detectTrafficAnomaly() comparing 7-day vs 30-day baseline
- Boost disruption score by 10 when traffic anomaly detected
- Show WoW% from PortWatch even when real-time AIS counts are 0
- 6 new tests for anomaly detection edge cases

* fix(supply-chain): clamp disruptionScore to 100 and dedupe anomaly function

P1: disruptionScore could exceed 100 when anomalyBonus was added on top
of a max-score base, rendering "110/100" in the UI. Now clamped before
assignment, not just for status.

P2: detectTrafficAnomaly was duplicated in test file, so regressions in
the real code path would go undetected. Moved function into _scoring.mjs
(pure, no server deps). Both handler and tests import the same function.

* fix(supply-chain): require 37 days for traffic anomaly detection

detectTrafficAnomaly needs 7 recent + 30 baseline days. The threshold
was 30, which would use a partial baseline (23 days). Now correctly
requires 37 rows before signaling.
2026-03-14 21:29:48 +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
259cbd17d4 fix(supply-chain): use ArcGIS timestamp syntax for PortWatch date filter (#1593)
* fix(supply-chain): use ArcGIS timestamp syntax for PortWatch date filter

The PortWatch seed loop was silently failing on every cycle because
`date >= <epoch_ms>` is not valid ArcGIS Feature Service syntax.
ArcGIS requires `date >= timestamp 'YYYY-MM-DD HH:MM:SS'`.

This caused: no chart history, no transit counts, no WoW% in the
Supply Chain panel chokepoints tab (all added in PRs #1560/#1572/#1577).

Verified: 13/13 chokepoints return 175 days of history with the fix.

Also adds chokepointTransits to health.js STANDALONE_KEYS and SEED_META
so the transit counter seed freshness is monitored.

* fix(supply-chain): preserve full UTC time in PortWatch timestamp filter

pwEpochToTimestamp() was hardcoding 00:00:00, expanding the 180-day
window to the start of that UTC date. Now preserves HH:MM:SS from the
original epoch to match the intended query boundary exactly.
2026-03-14 20:36:05 +04:00
Frank
9d88aff739 fix: re-sync globe map on fullscreen transitions (#1510)
* fix: re-sync globe map on fullscreen transitions

* fix: call resize() instead of render() and anchor test regex

resize() propagates to MapLibre canvas resize in DeckGL mode,
fixing stale canvas dimensions after fullscreen transitions.
Anchor test regex to setupMapFullscreen to avoid matching the
wrong toggle block.

---------

Co-authored-by: Elie Habib <elie.habib@gmail.com>
2026-03-14 18:11:11 +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
14bc59e543 fix(supply-chain): correct PortWatch ArcGIS URL and field mappings (#1572)
* fix(supply-chain): correct PortWatch ArcGIS URL, field names, and chokepoint mappings

The PortWatch seed was failing silently because:
1. Wrong service name: portal_chokepoint_daily -> Daily_Chokepoints_Data
2. Wrong query fields: chokepoint/observation_date -> portname/date (epoch)
3. Wrong data model: expected one row per vessel type, actual schema has
   all counts as columns (n_tanker, n_cargo, n_total) per row
4. Wrong chokepoint names: e.g. "Strait of Malacca" -> "Malacca Strait",
   "Bab el-Mandeb" -> "Bab el-Mandeb Strait", "Bosphorus" -> "Bosporus Strait"
5. Removed Dardanelles (not in PortWatch dataset)

Discovered via IMF PortWatch ArcGIS service directory and returnDistinctValues
query on the portname field.

* feat(supply-chain): add Korea, Dover, Kerch, Lombok chokepoints

Extend from 10 to 14 monitored chokepoints using PortWatch data
availability. All 4 new straits have IMF PortWatch coverage.

- Korea Strait: Japan-Korea trade, busiest East Asia corridor
- Dover Strait: world's busiest shipping lane
- Kerch Strait: war_zone (Russia controls, Ukraine grain restricted)
- Lombok Strait: Malacca bypass for VLCCs

Added to: handler config, canonical ID map, PortWatch seed names,
AIS relay transit counter, tests.

* docs: update maritime docs and changelog for 14 chokepoints + transit intelligence

- maritime-intelligence.mdx: 9 -> 14 chokepoints, add data source descriptions,
  add chart rendering note
- changelog.mdx + CHANGELOG.md: add [Unreleased] section for #1560 and #1572

* fix(tests): update portwatch test for pre-aggregated column model

pwClassifyVesselType was removed when switching to pre-aggregated
n_tanker/n_cargo/n_total columns. Update test to verify the new
field names instead.

* fix(supply-chain): sync canonical PortWatch names with actual ArcGIS feed

P1: Dardanelles has no PortWatch data (0 rows). Set portwatchName to empty
    string so it won't attempt fetch or show phantom zero history.
P2: portwatchNameToId() returned undefined for Malacca Strait, Bab el-Mandeb
    Strait, Gibraltar Strait, Bosporus Strait because canonical map used
    old names instead of actual ArcGIS portname values.

Fixed mappings:
  Strait of Malacca -> Malacca Strait
  Bab el-Mandeb -> Bab el-Mandeb Strait
  Strait of Gibraltar -> Gibraltar Strait
  Bosphorus -> Bosporus Strait
  Dardanelles -> '' (not in PortWatch)

* refactor(supply-chain): merge Dardanelles into Turkish Straits

IMF PortWatch tracks Bosphorus+Dardanelles as a single corridor
(Bosporus Strait). Keeping them separate caused double-counting in
AIS transit data and left Dardanelles with permanently empty history.

- Merge into single "Turkish Straits" entry (id stays 'bosphorus')
- Absorb all Dardanelles keywords (canakkale, gallipoli, aegean)
- Single wider AIS geofence (lat 40.70, lon 28.0, radius 1.5)
- 14 -> 13 chokepoints
- Update docs, changelog, tests

* fix: rename Turkish Straits to Bosporus Strait (match PortWatch naming)
2026-03-14 16:00:07 +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
11a0323fdc fix(correlation): normalize escalation signals to ISO2 country codes (#1529)
* fix(correlation): normalize escalation signals to ISO2 country codes

Protests used full names ("Iran") while news clusters used ISO2 ("IR"),
causing clusterByCountry() to produce duplicate rows. All signal sources
now normalize via normalizeToCode() before clustering. generateTitle()
resolves codes back to full names for display.

* test(correlation): add behavioral tests for escalation country normalization

Suite B: mocked geometry, tests collectSignals normalizes "Iran" to "IR",
generateTitle shows full names, and mixed-format signals share same code.

* fix(correlation): resolve aliases before 2-char fast path in normalizeToCode

nameToCountryCode() now runs first so two-letter aliases like UK resolve
to their canonical ISO2 code (GB) instead of being returned verbatim.
Adds static test for resolution order and behavioral test for UK->GB.
2026-03-13 12:32:53 +04:00
Elie Habib
6d91f7071c fix(live-news): restore fallback integrity checks (#1517) 2026-03-13 08:50:10 +04:00
Fayez Bast
36d0954720 feat(cache): key market quote breakers by symbol set (#1379)
* feat(cache): key market quote breakers by symbol set

* feat(cache): key market quote breakers by symbol set

* feat(cache): key market quote breakers by symbol set

---------

Co-authored-by: Elie Habib <elie.habib@gmail.com>
2026-03-13 00:21:08 +04:00
Elie Habib
91215ee4b9 feat(live-news): add 17 new HLS channels and enrich 8 existing with direct HLS (#1504)
Enriched 8 YouTube-only channels with direct HLS streams: bloomberg, cnn,
abc-news, nbc-news, aljazeera, ndtv, i24-news, cgtn-arabic.

Added 17 new HLS-only channels across regions: cgtn, gb-news, reuters-tv,
the-guardian, phoenix, rtp3, ctv-news, aljazeera-mubasher, alarabiya-business,
al-qahera-news, press-tv, dw-arabic, dw-espanol, rt-arabic, rt-espanol,
cgtn-espanol.

Updated tests to accept hlsUrl as valid alternative to fallbackVideoId,
fixing 2 pre-existing test failures.
2026-03-12 23:51:06 +04:00
Elie Habib
7d72c6844b fix(predictions): composite scoring, finance variant, region ranking (#1486)
* fix(predictions): replace volume-only sort with composite scoring, add finance variant and region ranking

The prediction panel was surfacing irrelevant near-certain markets (1%/99% meme
markets like celebrity presidential bids) because the discrepancy filter was
inverted and sorting was by volume alone.

- Replace broken discrepancy filter with composite scoring (60% uncertainty +
  40% log-scaled volume) in seed script
- Add meme candidate detection and sports/entertainment keyword exclusion
- Add finance variant with dedicated tags for economy/trade/rates topics
- Add region-aware soft ranking outside circuit breaker cache
- Add input validation (category max 50, query max 100) in RPC handler
- Skip events without markets instead of defaulting to yesPrice=50
- Per-bucket relaxation safety valve when <15 markets pass strict filters

* fix(predictions): apply region sort before truncation, add RPC fallback scoring, validate finance seed

- Keep 25 candidates from bootstrap/RPC, apply region sort, then slice to 15
  (previously sliced to 15 first, making region boost ineffective for markets
  ranked 16-25)
- Add client-side uncertainty scoring + near-certain filter (10-90%) for RPC
  fallback path (previously fell back to Gamma's volume-only ordering)
- Include finance array in seed validation (previously only checked
  geopolitical/tech, allowing broken finance data to ship silently)

* test(predictions): add 54 unit tests for scoring, filtering, and region tagging

Extract pure prediction scoring functions into shared module
(_prediction-scoring.mjs) for testability. Tests cover parseYesPrice,
isExcluded, isMemeCandidate, tagRegions, shouldInclude, scoreMarket,
filterAndScore, isExpired, plus regression tests for the meme market
surfacing bug that motivated this fix.
2026-03-12 14:02:58 +04:00
Elie Habib
a286580463 fix(docs): add .mintignore and enhance MDX lint for Mintlify compatibility (#1474)
* fix(docs): add .mintignore to exclude non-MDX-safe files

roadmap-pro.md contains curly braces ({hash}, {userId}) that Mintlify's
MDX parser interprets as JSX expressions, causing deploy failures.
Exclude it along with PRESS_KIT.md and Docs_To_Review/ (internal files
not in navigation).

* fix(docs): enhance MDX lint to catch curly braces and .md files

Mintlify parses all docs/ files as MDX, treating {expr} as JSX
expressions. The existing lint only checked .mdx files for bare angle
brackets. Now also checks:
- .md files (Mintlify processes these too)
- Bare curly braces {word} outside code fences/spans
- Respects docs/.mintignore for excluded files
2026-03-12 08:01:32 +04:00
Elie Habib
8766b45a57 fix(docs): comprehensive MDX angle bracket escaping (#1453)
* fix(docs): comprehensive MDX angle bracket escaping

Escape all bare < patterns that MDX interprets as JSX across
documentation.mdx, algorithms.mdx, ai-intelligence.mdx,
data-sources.mdx, finance-data.mdx, relay-parameters.mdx,
and maps-and-geocoding.mdx.

* feat(lint): add MDX bare angle bracket lint to pre-push

Adds tests/mdx-lint.test.mjs that scans all docs/*.mdx files for
bare <digit and <hyphen patterns outside code fences. These break
Mintlify's MDX parser. Wired into .husky/pre-push so issues are
caught before reaching Mintlify.
2026-03-12 00:45:52 +04:00
lspassos1
1886161587 fix: resilient assertions and test stability (#1389)
Co-authored-by: Elie Habib <elie.habib@gmail.com>
2026-03-12 00:19:16 +04:00
Elie Habib
bbe6a828f1 feat(pro): harden enterprise form with mandatory fields and lead qualification (#1382)
* feat(pro): harden enterprise contact form with mandatory fields and lead qualification

- Add mandatory phone number and company fields (client + server validation)
- Block free email domains (gmail, yahoo, hotmail, etc.) with 422 response and inline error
- Include phone (clickable tel: link) and email domain (clickable company link) in sales notification
- Add i18n translations for phone placeholder and work email error across all 21 locales
- Tighten phone regex to require start/end with digit, rejecting junk input

* fix(pro): rebuild static assets and fix contact handler tests

- Rebuild public/pro/ bundle to include new phone/company/email validation fields
- Add phone field to test validBody() fixture
- Add tests for free email domain rejection (422), missing org, missing/invalid phone
2026-03-10 17:25:09 +04:00
Elie Habib
3d51a2619e fix(contact): add enterprise contact form endpoint (#1365)
* fix(contact): add dedicated enterprise contact endpoint with storage and email notifications

The enterprise contact form was posting to /api/register-interest which
only stored email for the waitlist. Name, organization, and message fields
were silently dropped and no notification was sent.

- Add api/contact.js endpoint with Turnstile, rate limiting, honeypot
- Add Convex contactMessages table and submit mutation
- Send notification email to sales@worldmonitor.app via Resend
- Sanitize email subject (strip newlines, truncate length)
- Fix cf-connecting-ip priority in register-interest.js IP detection

* chore: add contact.js to legacy endpoint allowlist

* fix(contact): harden Turnstile enforcement, surface email status, add tests

- Turnstile rejects in production when TURNSTILE_SECRET_KEY is unset
  (only allows skip in development)
- sendNotificationEmail returns boolean, response includes emailSent field
- Log error (not warn) when RESEND_API_KEY is missing in production
- Add 15 endpoint tests covering validation, Turnstile, notifications, Convex
2026-03-10 08:26:05 +04:00
Elie Habib
b8ead5280c fix(scoring): CII scoring followups from PR #1351 review (#1360)
- Sort bboxes by area (smallest first) so AE matches before SA for Dubai coords
- Explicit outage severity matching (no catch-all inflating unknown severities)
- Add 'united arab emirates' to AE keywords for ACLED/UCDP normalization
- Add CU/MX/BR/AE to client-side TIER1_NAMES (was showing raw ISO codes)
- Add UAE geo attribution test verifying bbox overlap resolution
2026-03-10 06:52:48 +04:00
Elie Habib
c5d196f29e feat(scoring): port frontend CII scoring formulas to server (#1351)
* feat(scoring): port frontend CII scoring formulas to server

Port the frontend's proven scoring formulas (log2/sqrt scaling, fatality
splits, outage/GPS severity tiers, OREF integration, advisory floors and
boosts) to the server-side CII computation so scores are data-driven and
self-correcting.

- Add MX, BR, AE to TIER1_COUNTRIES (21 to 24)
- Disable relay CII seed loop (RPC computes on-demand)
- Add activeAlertCount to OREF Redis payload
- Expand CountrySignals with fatality split, outage tiers, GPS severity,
  OREF fields, advisory level, and high severity strikes
- Port calcUnrestScore (log2 dampening, protest fatality boost, outage
  severity tiers)
- Port calcConflictScore (weighted ACLED events, sqrt fatalities, OREF
  boost, strike severity)
- Port calcSecurityScore (GPS high/medium weighting, cap 35)
- Add advisory floors (do-not-travel 60, reconsider 50) and boosts
- Add OREF blend boost for IL
- Fix fires fallback (empty array is truthy) and climate severity
  nullish coalescing
- Add 14 fixture tests covering floors, boosts, scaling, and edge cases

* docs: update CII scoring documentation to match server-side formulas

Update ALGORITHMS.md and DOCUMENTATION.md to reflect the ported scoring
formulas: 24 tier-1 countries, log2/sqrt scaling, outage/GPS severity
tiers, OREF integration, advisory floors/boosts, and data source table.
2026-03-09 23:36:56 +04:00
Elie Habib
4c0459291c test: add legacy api/*.js endpoint allowlist guardrail (#1312)
Prevents new flat api/*.js endpoints from being added without
deliberate opt-in. New data endpoints should use the sebuf protobuf
RPC pattern (proto → buf generate → handler → handler.ts).

Refs #1311
2026-03-09 08:39:17 +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
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
Frank
e53a43e494 fix(map): stop deckgl layer toggles from getting stuck (#1248)
* fix(map): isolate deckgl layer state from external mutations

* fix(map): improve state isolation and replace brittle regex test

- Also shallow-copy pan in constructor for full isolation
- Simplify setLayers by removing intermediate nextLayers variable
- Replace regex source-scan test with negative-match assertions
  that survive refactors while still catching direct assignment

* fix(map): complete state isolation — getState and onStateChange

- getState() now deep-copies layers and pan, not just shallow spread
- onStateChange callbacks pass this.getState() instead of this.state
- Expanded test to cover getState copy and onStateChange isolation

* test(map): replace source-scan test with behavioral isolation tests

Source-text regex tests are brittle and don't prove runtime behavior.
New tests replicate the exact copy logic from DeckGLMap's constructor,
setLayers, getState, and onStateChange, then verify mutations to
caller-owned objects never affect internal state and vice versa.

---------

Co-authored-by: Elie Habib <elie.habib@gmail.com>
2026-03-08 09:33:49 +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
Elie Habib
1324f7ee58 fix(scripts): commit shared configs for Railway deploy (#1234)
Railway rootDirectory isolates build context — postinstall cp from
../shared/ fails because parent dirs aren't in the Nixpacks image.
Commit JSON/CJS configs directly into scripts/shared/.

- Remove useless postinstall from scripts/package.json
- Remove scripts/shared/ from .gitignore
- Commit all shared config files into scripts/shared/
- Add sync test to catch drift between shared/ and scripts/shared/
2026-03-08 00:24:33 +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
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
Fasih M.
6f9af63bad feat(geo): add Pak-Afghan conflict zone and country boundary override system (#1150)
* Add Pakistan–Afghanistan hotspot and conflict zone

Introduce a new INTEL_HOTSPOTS entry (pak_afghan) to track Pakistan–Afghanistan border tensions, including location, keywords, agencies, status, escalation indicators, and humanitarian significance. Also add a CONFLICT_ZONES polygon for 'Pakistan–Afghanistan War' with center, intensity, parties, startDate (Feb 21, 2026), key developments, and displacement/casualty notes to enable monitoring of cross-border strikes, TTP activity, and regional instability.

* Update conflict zone center coordinates

Adjust the center coordinates for the specified conflict zone in src/config/geo.ts from [50, 30] to [69, 31.8] to better reflect the actual Pakistan/Afghanistan border region and improve map centering/visualization accuracy.

* Add country boundary overrides (Pakistan)

Support optional country boundary overrides by loading public/data/country-boundary-overrides.geojson and replacing main country geometries when ISO codes match. Add a script (scripts/fetch-pakistan-boundary-override.mjs) to fetch Pakistan's de facto boundary from Natural Earth and write the override file, and document the override workflow in CONTRIBUTING.md. The country-geometry service now attempts to apply overrides and updates cached polygons/bboxes; failures are ignored since overrides are optional.

* 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

* refactor: serve country boundary overrides from R2 CDN

Move country-boundary-overrides.geojson from public/data/ to R2 bucket
(worldmonitor-maps) to avoid serving large static files through Vercel.
Update fetch URL, docs, and script with rclone upload instructions.

* fix: use maps.worldmonitor.app for R2 override URL (CF-proxied)

* fix(geo): bound optional country override fetch

---------

Co-authored-by: Elie Habib <elie.habib@gmail.com>
2026-03-07 10:00:03 +04:00
Elie Habib
a6b7c771ac fix(economic): seed all WB indicators on Railway, never call WB API from frontend (#1159)
* fix(economic): seed all WB indicators on Railway, never call WB API from frontend

Extends seed-wb-indicators.mjs to pre-compute progress data (4 indicators)
and renewable energy data (EG.ELC.RNEW.ZS) alongside tech readiness rankings.

Frontend callers (progress-data.ts, renewable-energy-data.ts, getTechReadinessRankings,
getCountryComparison) now read exclusively from bootstrap/Redis seed data.
Zero Vercel Edge → World Bank API calls remain.

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

- Fix triple JSON.parse in seed verification (P1)
- Graceful fallback for renewable data fetch failure (P2)
- Use Map lookup instead of Array.find in progress-data (P2)
- Update regression test for bootstrap-only getTechReadinessRankings (P2)
2026-03-07 08:00:28 +04:00
Fayez Bast
ca131bb075 feat/localize map(#1017) (#1032)
* feat/localize map(#1017)

* feat :map localization

* fix: address map-localizatio
2026-03-05 09:28:07 +04:00
Elie Habib
80b8071356 feat: server-side AI insights via Railway cron + bootstrap hydration (#1003)
Move the heavy AI insights pipeline (clustering, scoring, LLM brief)
from client-side (15-40s per user) to a 5-min Railway cron job. The
frontend reads pre-computed insights instantly via bootstrap hydration,
with graceful fallback to the existing client-side pipeline.

- Add _clustering.mjs: Jaccard clustering + importance scoring (pure JS)
- Add seed-insights.mjs: Railway cron reads digest, clusters, calls
  Groq/OpenRouter for brief, writes to Redis with LKG preservation
- Register insights key in bootstrap.js FAST_KEYS tier
- Add insights-loader.ts: module-level cached bootstrap reader
- Modify InsightsPanel.ts: server-first path (2-step progress) with
  client fallback (4-step, unchanged behavior)
- Add unit tests for clustering (12) and insights-loader (7)
2026-03-04 20:42:51 +04:00
Elie Habib
898ac7b1c4 perf(rss): route RSS direct to Railway, skip Vercel middleman (#961)
* perf(rss): route RSS direct to Railway, skip Vercel middleman

Vercel /api/rss-proxy has 65% error rate (207K failed invocations/12h).
Route browser RSS requests directly to Railway (proxy.worldmonitor.app)
via Cloudflare CDN, eliminating Vercel as middleman.

- Add VITE_RSS_DIRECT_TO_RELAY feature flag (default off) for staged rollout
- Centralize RSS proxy URL in rssProxyUrl() with desktop/dev/prod routing
- Make Railway /rss public (skip auth, keep rate limiting with CF-Connecting-IP)
- Add wildcard *.worldmonitor.app CORS + always emit Vary: Origin on /rss
- Extract ~290 RSS domains to shared/rss-allowed-domains.cjs (single source of truth)
- Convert Railway domain check to Set for O(1) lookups
- Remove rss-proxy from KEYED_CLOUD_API_PATTERN (no longer needs API key header)
- Add edge function test for shared domain list import

* fix(edge): replace node:module with JSON import for edge-compatible RSS domains

api/_rss-allowed-domains.js used createRequire from node:module which is
unsupported in Vercel Edge Runtime, breaking all edge functions (including
api/gpsjam). Replaced with JSON import attribute syntax that works in both
esbuild (Vercel build) and Node.js 22+ (tests).

Also fixed middleware.ts TS18048 error where VARIANT_OG[variant] could be
undefined.

* test(edge): add guard against node: built-in imports in api/ files

Scans ALL api/*.js files (including _ helpers) for node: module imports
which are unsupported in Vercel Edge Runtime. This would have caught the
createRequire(node:module) bug before it reached Vercel.

* fix(edge): inline domain array and remove NextResponse reference

- Replace `import ... with { type: 'json' }` in _rss-allowed-domains.js
  with inline array — Vercel esbuild doesn't support import attributes
- Replace `NextResponse.next()` with bare `return` in middleware.ts —
  NextResponse was never imported

* ci(pre-push): add esbuild bundle check and edge function tests

The pre-push hook now catches Vercel build failures locally:
- esbuild bundles each api/*.js entrypoint (catches import attribute
  syntax, missing modules, and other bundler errors)
- runs edge function test suite (node: imports, module isolation)
2026-03-04 18:42:00 +04:00
Elie Habib
c2f17dec45 fix(supply-chain): resolve P1 threat zeroing and P2 geo-first misclassification (#964)
* enhance supply chain panel

* fix(supply-chain): resolve P1 threat zeroing and P2 geo-first misclassification

P1: threat baseline is now always applied regardless of config
staleness — stale config only adds a review-recommended note,
never zeros the score.

P2: resolveChokepointId now checks text evidence first and only
falls back to proximity when text has no confident match.

Adds regression test: text "Bab el-Mandeb" with location near
Suez correctly resolves to bab_el_mandeb.

---------

Co-authored-by: fayez bast <fayezbast15@gmail.com>
2026-03-04 08:47:21 +04:00
Elie Habib
034ab9916f feat(globe): add interactive 3D globe view with 28 live data layers (#926)
* feat(globe): add 3D globe view powered by globe.gl

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

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

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

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

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

* refactor(globe): use proper keyof MapLayers types

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

- MapContainer: add globe guard to setAisData and setFlightDelays (data was silently dropped)
- GlobeMap: implement setAisData with AisDisruptionMarker (typed, no any casts); renders
  disruption events with severity-colored ship icons and full tooltip (name/type/severity)
- GlobeMap: three-point dayNight suppression — disabled in initGlobe(), overridden in
  setLayers(), ignored in enableLayer(); toggle removed from layer panel UI
- MapContainer: add globe guards to 5 happy-variant setters (P3: keep no-op stubs in globe)
- Add tests/globe-2d-3d-parity.test.mjs: 13 static-analysis tests covering routing,
  AIS marker fields, and dayNight suppression (all passing)
2026-03-03 21:08:06 +04:00
Elie Habib
e0a3ca5b62 Revert "feat: widget picker, layout tabs & panel close buttons (#882) (#890)" (#918)
This reverts commit c99314ae09.
2026-03-03 18:26:16 +04:00
Elie Habib
6ec076c8d3 test(circuit-breakers): harden regression tests with try/finally and existence guards (#911)
- Wrap all 4 behavioral it() blocks in try/finally so clearAllCircuitBreakers()
  always runs on assertion failure (P2 — leaked breaker state between tests)
- Add assert.ok(fnStart !== -1) guards for fetchHapiSummary, fetchPositiveGdeltArticles,
  and fetchGdeltArticles so renames produce a clear diagnostic (P2 — silent false-positives)
- Fix misleading comment in seed-wb-indicators.mjs: WLD/EAS are 3-char codes and
  aren't filtered by iso3.length !== 3 (P3)
- Add timeout-minutes: 10 and permissions: contents: read to seed GHA workflow (P3)
2026-03-03 15:13:29 +04:00
Nicolas Dos Santos
c99314ae09 feat: widget picker, layout tabs & panel close buttons (#882) (#890)
* docs: add widget picker & layout presets design for #882

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

* docs: add widget picker implementation plan (#882)

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

* feat: add layout presets config (#882)

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

* feat: add i18n keys for layout presets (#882)

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

* feat: add LayoutTabs component (#882)

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

* feat: add WidgetPicker popover component (#882)

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

* feat: add close button to panel headers (#882)

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

* feat: wire LayoutTabs and WidgetPicker into header (#882)

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

* refactor: remove Settings > Panels tab, replaced by widget picker (#882)

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

* test: add layout presets validation tests (#882)

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 14:12:23 +04:00
Elie Habib
a5b2af8e11 feat(tech-readiness): bootstrap hydration via Railway seed + bootstrap key (#889)
* feat(tech-readiness): bootstrap hydration via Railway seed + bootstrap key

Add pre-computed TechReadiness rankings to the bootstrap payload so the
panel renders immediately on first load instead of waiting for 4 slow
World Bank RPC calls (which can trip circuit breakers on cold starts,
causing persistent "No data available" until the 5-min cooldown expires).

- scripts/seed-wb-indicators.mjs: new Railway seed script that fetches
  IT.NET.USER.ZS / IT.CEL.SETS.P2 / IT.NET.BBND.P2 / GB.XPD.RSDV.GD.ZS
  for all countries, computes rankings (same weights as the frontend
  getTechReadinessRankings), and writes economic:worldbank-techreadiness:v1
  to Redis with a 7-day TTL
- api/bootstrap.js: register techReadiness key in BOOTSTRAP_CACHE_KEYS
  and SLOW_KEYS (s-maxage=3600, appropriate for annual WB data)
- src/services/economic/index.ts: fast-path in getTechReadinessRankings()
  returns getHydratedData('techReadiness') immediately on first page load;
  country-specific comparison requests still use live RPCs

* ci: add weekly GHA workflow for WB tech readiness seed
2026-03-03 13:30:42 +04:00
Elie Habib
3dadf74ded test(circuit-breakers): harden HAPI/GDELT regression tests (P2 fixes) (#886) 2026-03-03 11:45:48 +04:00
Elie Habib
dba8b18aad fix(circuit-breakers): per-instance breakers for HAPI (20 countries) and GDELT (#879)
* fix(tech-readiness): per-indicator circuit breakers for World Bank RPC

* test(tech-readiness): regression tests for per-indicator circuit breakers

* fix(circuit-breakers): per-instance breakers for HAPI and GDELT

* test(circuit-breakers): regression tests for HAPI per-country and GDELT split breakers
2026-03-03 10:52:43 +04:00
Elie Habib
286eb0358f fix(tech-readiness): per-indicator circuit breakers for World Bank RPC (#877)
* fix(tech-readiness): per-indicator circuit breakers for World Bank RPC

* test(tech-readiness): regression tests for per-indicator circuit breakers
2026-03-03 10:30:16 +04:00
Fayez Bast
9a3707cc93 feat/Smart Polling De-escalation (#865)
* feat/Smart Polling De-escalation

* fix(polling): review fixes for smart poll de-escalation (#865)

- Remove dual-poll bug in GulfEconomiesPanel (stale setInterval alongside startSmartPollLoop)
- Add 'startup' reason to SmartPollReason for runImmediately semantics
- Remove comments from refresh-scheduler.ts and SmartPollOptions per project convention
- Add 20 unit tests for startSmartPollLoop covering scheduling, jitter,
  backoff, visibility, abort, and lifecycle

---------

Co-authored-by: Elie Habib <elie.habib@gmail.com>
2026-03-03 09:29:37 +04:00