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
Fed Total Assets (WALCL) display value was correctly divided by 1000
to show billions, but the change delta was left in raw millions.
A $17.45B weekly change showed as +17450$B. Now divides change by
1000 to match the display unit.
"0 warning(s), 0 AIS disruption(s)" adds no information and
contradicts the actual intelligence (e.g. 95% traffic drop at
Hormuz). Now only shows the warnings/disruptions line when at
least one is non-zero. Directions still display on their own.
Remove TradingView's lightweight-charts dependency (~60KB) and replace
with a self-contained Canvas 2D implementation. Fixes:
- TradingView logo/branding and redirect links removed
- Chart data no longer hidden by legend labels (Cargo/Tanker overlapping March data)
- Shows last 60 days instead of full 6-month range
- Uses app theme CSS variables (--text-dim, --border-subtle, --accent-primary)
- Hover tooltips showing date + both values
- Bottom legend with colored dots and current values
- Highlighted endpoint dots for latest data
- Responds to theme-changed events
- ResizeObserver for responsive width
* 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.
* 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.
* 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).
* 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.
* 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.
* feat(ui): add glowing border pulse effect for search highlights
Replace subtle background fade with a 3-pulse blue glow animation
(3s duration) on panels and map countries when navigating via search
or breaking news alerts. Helps users spot the target result.
* fix(map): reset country highlight opacity after pulse animation ends
Without this, fill-opacity decays to near-zero at animation end,
leaving the country nearly invisible until the brief panel closes.
* fix(ui): cancel stale highlight timers and clean up pulse RAF on destroy
- Cancel previous setTimeout before scheduling new highlight, preventing
premature class removal on rapid re-search (Codex P2)
- Extract shared applyHighlight() in SearchManager with WeakMap tracking
- Cancel countryPulseRaf in DeckGLMap.destroy() to prevent orphaned RAF
* fix(ui): theme-aware highlight opacity and per-element timer tracking
- Use getHighlightRestOpacity() to read current map theme instead of
hardcoded dark-theme values (0.12), fixing light-theme regression
- Pulse animation base values now scale from theme-correct resting opacity
- BreakingNewsBanner: switch from single timer slot to WeakMap per-element
tracking, preventing permanent highlight on rapid alert clicks
* 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>
Add "?" info tooltips explaining methodology to: Escalation Monitor,
Economic Warfare, Economic Indicators, Trade Policy, Supply Chain,
Markets, Sector Heatmap, Commodities, Market Radar, BTC ETF Tracker.
Remove the source attribution footer from the Supply Chain panel that
was listing all upstream data providers.
* fix(ui): add span-2 to content-heavy panels truncated at 380px
Panels with tabs + dense list content were capped at 380px by
grid-auto-rows: minmax(200px, 380px). Adding span-2 gives them
2 grid rows (~760px) so content is visible without scrolling.
Panels: SupplyChain, TelegramIntel, Economic, TradePolicy,
GdeltIntel, SecurityAdvisories, CII, UcdpEvents, Displacement,
StrategicPosture.
* fix(ui): use defaultRowSpan option to preserve user resize preferences
Move span-2 from post-constructor classList.add to Panel's
defaultRowSpan option, applied before saved span restore. This
ensures user resize-to-1-row preferences persist across reloads
instead of being overridden by subclass constructors.
Also fix Panel restore logic to handle savedSpan=1 (was skipping
when savedSpan <= 1).
* 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.
Two follow-up fixes for the server insights fallback (PR #1574):
1. onAiFlowChanged() checked lastClusters.length before re-rendering,
dropping back to "waiting" state instead of trying server insights.
Now delegates to updateInsights() which checks server path first.
2. renderServerInsights() rendered lastMissedStories from a previous
clustering run. When clusters are empty (fallback path), these
stale ML-detected stories are unrelated to the server stories.
Clear lastMissedStories when entering server path with no clusters.
The empty cluster guard (clusters.length === 0) returned early with
"UNAVAILABLE" before checking getServerInsights(). This caused the
AI Insights panel to show "Waiting for news data..." when clustering
failed or was slow, even though pre-computed server insights were
available via bootstrap.
Move the server insights check before the cluster guard so the panel
can render from Railway-seeded data regardless of client-side
clustering state.
* feat(telegram): add media support, LIVE indicator, and error state
- Display images and videos from Telegram messages with media grid
- Add LIVE indicator badge for messages < 10 minutes old
- Show error state when relay returns errors
- Add "View Source" action button per message
- Update TelegramItem type with optional mediaUrls field
Co-authored-by: lspassos1 <lspassos@icloud.com>
* fix(telegram): escape HTML entities in message text, add noopener to media links
- Escape &, <, >, " in item.text before safeHtml() to prevent
untrusted Telegram messages from injecting links or styling
- Add noopener,noreferrer to window.open() on image click to
prevent tabnabbing via window.opener reference
* fix(telegram): guard null text, add missing i18n keys
- Guard item.text with fallback to empty string for media-only messages
- Add missing viewSource and live i18n keys to all 21 locale files
- Replace hardcoded 'LIVE' string with t() call
* fix(telegram): surface relay errors to panel error state
data-loader catch block was swallowing fetch failures silently.
Now forwards error to TelegramIntelPanel.setData() so users see
the error state UI instead of a stale/empty panel.
* fix(telegram): remove hardcoded English error fallback
Use enabled:false without error field to trigger the existing
translated disabled message in TelegramIntelPanel.
---------
Co-authored-by: lspassos1 <lspassos@icloud.com>
* feat(correlation): server-side correlation engine seed + bootstrap hydration
Move correlation card computation from client-side (per-browser, 10-30s delay)
to server-side (Railway cron, instant via bootstrap). Seed script reads 8 Redis
keys, runs 4 adapter signal collectors (military, escalation, economic, disaster),
clusters/scores/generates cards, writes to Redis with 10min TTL.
- New: scripts/seed-correlation.mjs (pure JS port of correlation engine)
- bootstrap.js: add correlationCards to FAST_KEYS tier
- health.js + seed-health.js: register for monitoring (maxStaleMin: 15)
- CorrelationPanel: consume bootstrap on construction, show "Analyzing..." only
after live engine has run (not for bootstrap-only cards)
- _seed-utils.mjs: support opts.recordCount override (function or number)
* fix(correlation): stale timestamp fallback + coordinate-based country resolution
P1: news stories lacked per-story pubDate, causing Date.now() fallback on
every seed run. Now _clustering.mjs propagates pubDate through to
enrichedStories, and seed-correlation reads s.pubDate then generatedAt.
P2: normalizeToCode dropped signals with unparseable country names.
Added centroid-based coordinate fallback (haversine nearest-match within
800km) matching the live engine's getCountryAtCoordinates behavior.
* fix(correlation): add 11 missing country centroids to coordinate fallback
CI, CR, CV, CY, GA, IS, LA, SZ, TL, TT, XK were in the normalization
maps but missing from COUNTRY_CENTROIDS, causing coordinate-only signals
in those countries to be misclassified or dropped during bootstrap.
* fix(correlation): align protest/outage field names with actual Redis schema
Codex review P1 findings: seed-correlation read wrong field names from
Redis data.
Protests (unrest:events:v1): p.time -> p.occurredAt, p.lat/lon ->
p.location.latitude/longitude, severity enum SEVERITY_LEVEL_* mapping.
Outages (infra:outages:v1): o.pubDate -> o.detectedAt, o.lat/lon ->
o.location.latitude/longitude, severity enum OUTAGE_SEVERITY_* mapping.
Both escalation and disaster adapters updated. Old field names kept as
fallbacks for data shape compatibility.
* 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
* feat: seed GDELT intelligence topics to Redis with bootstrap hydration
Add standalone seed script that pre-populates all 6 Live Intelligence
topics (military, cyber, nuclear, sanctions, intelligence, maritime)
from the GDELT Doc API into Redis. Frontend consumes bootstrap data
lazily via the service layer, falling back to RPC if unavailable.
- scripts/seed-gdelt-intel.mjs: new seed script with per-topic 429 retry
- api/bootstrap.js: register gdeltIntel in FAST_KEYS
- api/health.js: register in BOOTSTRAP_KEYS + SEED_META + dataSize
- api/seed-health.js: register in SEED_DOMAINS
- scripts/_seed-utils.mjs: add topics to recordCount detection
- src/services/gdelt-intel.ts: lazy bootstrap consumption in service layer
* fix(seed): align staleness thresholds and strengthen GDELT validation
- seed-health intervalMin 30→60 so staleness (120min) matches health.js maxStaleMin
- validate requires ≥3/6 topics populated (not just military)
- recordCount sums articles across topics instead of reporting topic count
Gate LlmStatusIndicator behind isDesktopRuntime() so Vercel web
never fires a wasted 404 fetch to /api/llm-health (sidecar-only).
Add Kharg Island, Qom, Andisheh, and Ankara to the Iran events
geolocation map for accurate event placement.
- Implement GlobeMap setters for positive events, kindness, happiness
choropleth, species recovery, and renewable installations
- Add focusOnHappyData auto-zoom on first happy data load
- Remove TV mode (empty setupTvMode stub, button markup, toggleTvMode)
- Move add-panel block to bottom grid for happy variant
- Remove console.debug statements from hot paths
Co-authored-by: Rayan Habib <rayanhabib07@gmail.com>
* feat(panels): improve drag UX and add close buttons to live panels
* fix: address PR feedback from koala73 and SebastienMelki
* fix: remove unused variable in llm-health probe
---------
Co-authored-by: rayanhabib07 <rayanhabib07@gmail.com>
Extract LlmStatusIndicator UI from PR #1522 into a standalone PR.
Shows a green/red dot in the header indicating LLM provider reachability.
Polls /api/llm-health every 60s; hides itself on Vercel (404 fallback).
Wire up setupLlmStatusIndicator() in App.init(), which was missing in
the original PR (dead code fix).
Co-authored-by: Jon Torrez <jrtorrez31337@users.noreply.github.com>
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.
* Cache geolocation results in sessionStorage across variant switches
On mobile, navigating between variants re-initializes the app, which
called getCurrentPosition() and resolveUserRegion() fresh each time,
triggering the browser's "Allow Location" prompt repeatedly.
Now both precise coordinates and resolved region are cached in
sessionStorage so the permission is only requested once per browser
session. sessionStorage is used (not localStorage) so the location
stays fresh across sessions while persisting across variant navigations.
https://claude.ai/code/session_01YNLqb4wZZX5ewWmweMggjy
* fix(location): don't cache timezone fallback before geolocation settles
resolveUserRegion() was caching the timezone-derived region even when
geolocation permission was pending (prompt state). This froze the
fallback for the entire session, preventing subsequent variant switches
from using precise coordinates once the user granted permission.
Two fixes:
- Check cached coords (from parallel resolvePreciseUserCoordinates)
before falling back to timezone
- Don't cache the timezone fallback, so subsequent calls retry
geolocation and can upgrade to a coords-based region
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(correlation): produce distinct titles for economic cluster cards
Economic adapter generateTitle() was using only signal types (sanctions_news,
commodity_spike, market_move) to build titles, so all clusters with the same
signal types got identical labels like "Economic warfare: sanctions activity".
Pass entityKey/country context from the engine to generateTitle() and rewrite
the economic adapter to extract specific commodity names, percentage changes,
and country mentions from signal data. Cards now show distinct titles like
"Gold spike (+3.2%)", "Iran sanctions activity", "Market disruption: BTC-USD".
* fix(correlation): tighten US regex match, handle mixed commodity+sanctions titles
- P1: KNOWN_ENTITIES regex now uses negative lookahead to prevent
false positives on "USDA", "USB", etc. Also matches "USA"/"United States".
- P2: Mixed clusters with both commodity_spike and sanctions_news now
show combined titles like "Oil spike (+3.2%) + sanctions" instead of
dropping the sanctions context.
* fix: filter generic entity keys from sanctions qualifier, preserve market context in mixed clusters
P1: entityKey from clusterByEntity() can be generic tokens (sanctions,
trade, tariff, etc.) which produce tautological titles like "Sanctions
sanctions activity". displayEntity() filters these out.
P2: sanctions_news branch now checks for co-occurring market_move
signals and appends them instead of dropping the market context.
* 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.
* 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>
Railway deploys with rootDirectory=scripts/, so ../shared/ resolves to
/shared/ which doesn't exist. Move the canonical file to scripts/data/
and update all four consumers.
- Move GEOPOLITICAL_TAGS, TECH_TAGS, FINANCE_TAGS, and EXCLUDE_KEYWORDS
to shared/prediction-tags.json so seed, RPC handler, and client all
reference a single source of truth
- Remove open_interest proto field (always 0 for Polymarket, never
displayed in UI) and corresponding openInterest assignments
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Elie Habib <elie.habib@gmail.com>
* fix(csp): add commodity variant to CSP and fix iframe variant navigation
- Add commodity.worldmonitor.app to frame-src and frame-ancestors in
vercel.json and index.html CSP — was missing while all other variants
were listed
- Open variant links in new tab when app runs inside an iframe to prevent
sandbox navigation errors ("This content is blocked")
- Add allow-popups and allow-popups-to-escape-sandbox to pro page iframe
sandbox attribute
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(csp): add missing variant subdomains to tauri.conf.json frame-src
Sync tauri.conf.json CSP with index.html and vercel.json by adding
finance, commodity, and happy worldmonitor.app subdomains to frame-src.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add PR screenshots for CSP fix
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Elie Habib <elie.habib@gmail.com>
* feat(live-news): add 17 new HLS channels and enrich 8 existing with direct HLS
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.
* fix(ui): re-render channel grid on toggle for immediate visual feedback
The card click handler only called renderList() but not
renderAvailableChannels(), so the green border/checkmark didn't
appear until the window was reopened.