* feat(forecast): add AI Forecasts prediction module (Pro-tier)
MiroFish-inspired prediction engine that generates structured forecasts
across 6 domains (conflict, market, supply chain, political, military,
infrastructure) using existing WorldMonitor data streams.
- Proto definitions for ForecastService with GetForecasts RPC
- Dedicated seed script (seed-forecasts.mjs) with 6 domain detectors,
cross-domain cascade resolver, prediction market calibration, and
trend detection via prior snapshot comparison
- Premium-gated RPC handler (PREMIUM_RPC_PATHS enforcement)
- Lazy-loaded ForecastPanel with domain filters, probability bars,
trend arrows, signal evidence, and cascade links
- Health monitoring integration (seed-meta freshness tracking)
- Refresh scheduler with API key guard
* test(forecast): add 47 unit tests for forecast detectors and utilities
Covers forecastId, normalize, resolveCascades, calibrateWithMarkets,
computeTrends, and smoke tests for all 6 domain detectors. Exports
testable functions from seed script with direct-run guard.
* fix(forecast): domain mismatch 'infra' vs 'infrastructure', add panel category
- Seed script used 'infra' but ForecastPanel filtered on 'infrastructure',
causing Infra tab to show zero results
- Added 'forecast' to intelligence category in PANEL_CATEGORY_MAP
* fix(forecast): move CSS to one-time injection, improve type safety
- P2: Move style block from setContent to one-time document.head injection
to prevent CSS accumulation on repeated renders
- P3: Replace +toFixed(3) with Math.round for readability in seed script
- P3: Use Forecast type instead of any[] in RPC handler filter
* fix(forecast): handle sebuf proto data shapes from Redis
Detectors now normalize CII scores from server-side proto format
(combinedScore, TREND_DIRECTION_RISING, region) to uniform shape.
Outage severity handles proto enum format (SEVERITY_LEVEL_HIGH).
Added confidence floor of 0.3 for single-source predictions.
Verified against live Redis: 2 predictions generated (Iran infra
shutdown, IL political instability).
* feat(forecast): unlock AI Forecasts on web, lock desktop only (trial)
- Remove forecast RPC from PREMIUM_RPC_PATHS (web access is free)
- Panel locked on desktop only (same as oref-sirens/telegram-intel)
- Remove API key guards from data-loader and refresh scheduler
- Web users get full access during trial period
* chore: regenerate proto types with make generate
Re-ran make generate after rebasing on main. Plugin v0.7.0 dropped
@ts-nocheck from output, added it back to all 50 generated files.
Fixed 4 type errors from proto codegen changes:
- MarketSource enum -> string union type
- TemporalAnomalyProto -> TemporalAnomaly rename
- webcam lastUpdated number -> string
* fix(forecast): use chokepoints v4 key, include ciiContribution in unrest
- P1: Switch chokepoints input from stale v2 to active v4 Redis key,
matching bootstrap.js and cache-keys.ts
- P2: Add ciiContribution to unrest component fallback chain in
normalizeCiiEntry so political detector reads the correct sebuf field
* feat(forecast): Phase 2 LLM scenario enrichment + confidence model
MiroFish-inspired enhancements:
- LLM scenario narratives via Groq/OpenRouter (narrative-only, no numeric
adjustment). Evidence-grounded prompts with mandatory signal citation
and few-shot examples from MiroFish's SECTION_SYSTEM_PROMPT_TEMPLATE.
- Top-4 predictions batched into single LLM call for cost efficiency.
- News context from newsInsights attached to all predictions for LLM
prompt grounding (NOT in signals, cannot affect confidence).
- Deterministic confidence model: source diversity via SIGNAL_TO_SOURCE
mapping (deduplicates cii+cii_delta, theater+indicators) + calibration
agreement from prediction market drift. Floor 0.2, ceiling 1.0.
- Output validation: rejects scenarios without signal references.
- Truncated JSON repair for small model output.
- Structured JSON logging for LLM calls.
- Redis cache for LLM scenarios (1h TTL).
- 23 new tests (70 total), all passing.
- Live-tested: OpenRouter gemini-2.5-flash produces evidence-grounded
scenario narratives from real WorldMonitor data.
* feat(forecast): Phase 3 multi-perspective scenarios, projections, data-driven cascades
MiroFish-inspired enhancements:
- Multi-perspective LLM analysis: top-2 predictions get strategic,
regional, and contrarian viewpoints via combined LLM call
- Probability projections: domain-specific decay curves (h24/d7/d30)
anchored to timeHorizon so probability equals projections[timeHorizon]
- Data-driven cascade rules: moved from hardcoded array to JSON config
(scripts/data/cascade-rules.json) with schema validation, named
predicate evaluators, unknown key rejection, and fallback to defaults
- 4 new cascade paths: infrastructure->supply_chain, infrastructure->market
(both requiresSeverity:total), conflict->political, political->market
- Proto: added Perspectives and Projections messages to Forecast
- ForecastPanel: renders projections row and conditional perspectives toggle
- 89 tests (19 new), all passing
- Live-tested: OpenRouter produces perspectives from real data
* feat(forecast): Phase 4 data utilization + entity graph
Fixes data gaps that prevented 4 of 6 detectors from firing:
- Input normalizers: chokepoint v4 shape + GPS hexes-to-zones mapping
- Chokepoint warm-ping (production-only, requires WM_API_BASE_URL)
- Lowered CII conflict threshold from 70 to 60, gated on level=high|critical
4 new standalone detectors:
- UCDP conflict zones (10+ events per country)
- Cyber threat concentration (5+ threats per country)
- GPS jamming in maritime shipping zones (5 regions)
- Prediction markets as signals (60-90% probability markets)
Entity-relationship graph (file-based, 38 nodes):
- Countries, theaters, commodities, chokepoints, alliances
- Alias table resolves both ISO codes and display names
- Graph cascade discovery links predictions across entities
Result: 51 predictions (up from 1-2), spanning conflict, infrastructure,
and supply chain domains. 112 tests, all passing.
* fix(forecast): redis cache format, signal source mapping, type safety
Fresh-eyes audit fixes:
- BUG: redisSet used wrong Upstash API format (POST body with {value,ex}
instead of command array ['SET',key,value,'EX',ttl]). LLM cache writes
were silently failing, causing fresh LLM calls every run.
- BUG: prediction_market signal type missing from SIGNAL_TO_SOURCE,
inflating confidence for market-derived predictions.
- CLEANUP: Remove unnecessary (f as any) casts in ForecastPanel since
generated Forecast type already has projections/perspectives fields.
- CLEANUP: Bump health maxStaleMin from 60 to 90 to avoid false STALE
alerts when LLM calls add latency to seed runs.
* feat(forecast): headline-entity matching with news corroboration signals
Uses entity graph aliases to match headlines to predictions by
country/theater (excludes commodity/infrastructure nodes to prevent
false positives). Predictions with matching headlines get a
news_corroboration signal visible in the panel.
Also fixes buildUserPrompt to merge unique headlines from ALL
predictions in the LLM batch (was only reading preds[0].newsContext).
Live-tested: 13 of 51 predictions now have corroborating headlines
(Iran, Israel, Syria, Ukraine, etc). 116 tests, all passing.
* feat(forecast): add country-codes.json for headline-entity matching
56 countries with ISO codes, full names, and scoring keywords (extracted
from src/config/countries.ts + UCDP-relevant additions). Used by
attachNewsContext for richer headline matching via getSearchTermsForRegion
which combines country-codes + entity graph + keyword aliases.
14/57 predictions now have news corroboration (limited by headline
coverage, not matching quality: only 8 headlines currently available).
* feat(forecast): read 300 headlines from news digest instead of 8
Read news:digest:v1:full:en (300 headlines across 16 categories) instead
of just news:insights:v1 topStories (8 headlines). Fallback to topStories
if digest is unavailable.
Result: news corroboration jumped from 25% to 64% (38/59 predictions).
* fix(forecast): handle parenthetical country names in headline matching
Strip suffixes like '(Zaire)', '(Burma)', '(Soviet Union)' from UCDP
region names before matching against country-codes.json. Also use
includes() for reverse name lookup to catch partial matches.
Corroboration: 64% -> 69% (41/59). Remaining 18 unmatched are countries
with no current English-language news coverage.
* fix(forecast): cache validated LLM output, add digest test, log cache errors
Fresh-eyes audit fixes:
- Combined LLM cache now stores only validated items (was caching raw
unvalidated output, serving potentially invalid scenarios on cache hit)
- redisSet logs warnings on failure (was silently swallowing all errors)
- Added digest-based test for attachNewsContext (primary path was untested)
- Fixed test arity: attachNewsContext(preds, news, digest) with 3 params
* fix(forecast): remove dead confidenceFromSources, reduce warm-ping timeout
- P2: Remove confidenceFromSources (dead code, computeConfidence overwrites
all initial confidence values). Inline the formula in original detectors.
- P3: Reduce warm-ping timeout from 30s to 15s (non-critical step)
- P3: Add trial status comment on forecast panel config
* fix(forecast): resolve ISO codes to country names, fix market detector, safe pre-push
P1 fixes from code review:
- CII ISO codes (IL, IR) now resolved to full country names (Israel, Iran)
via country-codes.json. Prevents substring false positives (IL matching
Chile) in event correlation. Uses word-boundary regex for matching.
- Market detector CII-to-theater mapping now uses entity graph traversal
instead of broken theater-name substring matching. Iran correctly maps
to Middle East theater via graph links.
- Pre-push hook no longer runs destructive git checkout on proto freshness
failure. Reports mismatch and exits without modifying worktree.
* feat: harness engineering P0 - linting, testing, architecture docs
Add foundational infrastructure for agent-first development:
- AGENTS.md: agent entry point with progressive disclosure to deeper docs
- ARCHITECTURE.md: 12-section system reference with source-file refs and ownership rule
- Biome 2.4.7 linter with project-tuned rules, CI workflow (lint-code.yml)
- Architectural boundary lint enforcing forward-only dependency direction (lint-boundaries.mjs)
- Unit test CI workflow (test.yml), all 1083 tests passing
- Fixed 9 pre-existing test failures (bootstrap sync, deploy-config headers, globe parity, redis mocks, geometry URL, import.meta.env null safety)
- Fixed 12 architectural boundary violations (types moved to proper layers)
- Added 3 missing cache tier entries in gateway.ts
- Synced cache-keys.ts with bootstrap.js
- Renamed docs/architecture.mdx to "Design Philosophy" with cross-references
- Deprecated legacy docs/Docs_To_Review/ARCHITECTURE.md
- Harness engineering roadmap tracking doc
* fix: address PR review feedback on harness-engineering-p0
- countries-geojson.test.mjs: skip gracefully when CDN unreachable
instead of failing CI on network issues
- country-geometry-overrides.test.mts: relax timing assertion
(250ms -> 2000ms) for constrained CI environments
- lint-boundaries.mjs: implement the documented api/ boundary check
(was documented but missing, causing false green)
* fix(lint): scan api/ .ts files in boundary check
The api/ boundary check only scanned .js/.mjs files, missing the 25
sebuf RPC .ts edge functions. Now scans .ts files with correct rules:
- Legacy .js: fully self-contained (no server/ or src/ imports)
- RPC .ts: may import server/ and src/generated/ (bundled at deploy),
but blocks imports from src/ application code
* fix(lint): detect import() type expressions in boundary lint
- Move AppContext back to app/app-context.ts (aggregate type that
references components/services/utils belongs at the top, not types/)
- Move HappyContentCategory and TechHQ to types/ (simple enums/interfaces)
- Boundary lint now catches import('@/layer') expressions, not just
from '@/layer' imports
- correlation-engine imports of AppContext marked boundary-ignore
(type-only imports of top-level aggregate)
* 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)
* 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
* style(docs): add OG image and SEO metatags for Mintlify
Sharing docs links showed generic meta from the main site.
Add seo.metatags to docs.json with OG image, site name,
and Twitter card configuration.
* feat(docs): add Mintlify changelog with Update components and RSS
Convert CHANGELOG.md into Mintlify-native changelog page using
<Update> components with tag filtering and RSS feed support.
All 27 versions converted with categorized tags for filtering.
Extract license content from contributing.mdx into its own first-class
docs/license.mdx page. Add prominent warnings about rebranding/renaming
being prohibited without a commercial license, an enforcement section,
and expanded commercial use restrictions.
Update README.md license section to reflect the dual-license model
(AGPL-3.0 for non-commercial, commercial license required for business
use). Previously it incorrectly stated commercial use was allowed under
AGPL alone.
Update cross-references in documentation.mdx and getting-started.mdx to
point to the new /license page.
The documentation.mdx page was just a table of contents duplicating the
sidebar navigation, with irrelevant variant demo links at the top. Replace
with a concise introduction that explains what World Monitor is, what
users can do with it, quick navigation links, and the license summary.
getting-started.mdx incorrectly stated "MIT" as the license.
The actual license is AGPL-3.0-only (per LICENSE file and package.json).
Adds a comprehensive license section to contributing.mdx explaining:
- What AGPL-3.0 means in plain language
- Rights and obligations table
- Common scenarios (self-hosting, public deployment, API usage, PRs)
- Why AGPL was chosen for this project
* 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
* docs: restructure documentation into focused, navigable pages (#docs-reorg)
Break the 4096-line documentation.mdx monolith into 13 focused pages
organized by feature area. Reorganize Mintlify navigation from 5 generic
groups into 7 feature-based groups. Move Orbital Surveillance from
Infrastructure to Map Layers where it belongs.
- Extract: signal-intelligence, features, overview, hotspots, CII,
geographic-convergence, strategic-risk, infrastructure-cascade,
military-tracking, maritime-intelligence, natural-disasters,
contributing, getting-started
- Append to: architecture.mdx (9 sections), ai-intelligence.mdx (3 sections)
- Fix legacy .md links in map-engine.mdx, maps-and-geocoding.mdx
- Slim documentation.mdx to an 80-line index/hub page
- Eliminate duplicate content that caused repeated headings
* fix(docs): remove duplicate H1 headings from all Mintlify pages
Mintlify auto-renders the frontmatter `title` as an H1, so having
`# Title` in the body creates a doubled heading on every page.
Remove the redundant H1 (and repeated description lines) from all
31 .mdx files.
Replace "WorldMonitor" with "World Monitor" in all user-facing display
text across blog posts, docs, layouts, structured data, footer, offline
page, and X-Title headers. Technical identifiers (User-Agent strings,
X-WorldMonitor-Key headers, @WorldMonitorApp handle, function names)
are preserved unchanged. Also adds anchors color to Mintlify docs config
to fix blue link color in dark mode.
* style(docs): align Mintlify header/footer with site design system
- Navbar: Blog, Dashboard, Pro, GitHub (matching site header)
- Primary CTA: "Get Early Access" green button linking to /pro#waitlist
- Colors: switch from blue (#3b82f6) to green (#4ade80) accent
- Footer: Dashboard, Pro, Blog + Community (GitHub, Discussions, X)
- Add X/Twitter social link
- Normalize all URLs to www.worldmonitor.app
- Name simplified to "World Monitor" (matching site branding)
* feat: add Docs link to dashboard, blog, and pro navigation
Add link to /docs across all site surfaces:
- Blog header nav and footer
- Dashboard footer nav
- Pro page footer (main + enterprise)
* 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.
* fix(docs): rename doc files to lowercase kebab-case for Mintlify
Mintlify serves pages at lowercase URLs. Uppercase filenames caused
404s on the Documentation tab. Renames all 18 doc files, updates
docs.json references, and fixes internal cross-links.
* fix(docs): rename .md to .mdx for Mintlify compatibility
Mintlify expects .mdx files. Plain .md files were not being found,
causing 404s on all documentation pages.
* feat(docs): add navbar links and footer with site variants
- Navbar: Live App, Tech, Finance, Blog links + GitHub CTA
- Footer: World Monitor variants + Resources columns
- Logo links back to worldmonitor.app
* fix(docs): add required theme field to Mintlify docs.json
Mintlify v2 requires a theme discriminator. Without it, deployment
fails with "Invalid discriminator value" error.
* fix(docs): migrate docs.json to Mintlify v2 schema
- navigation: object with tabs/groups instead of array
- Remove colors.background (unrecognized in v2)
- topbarLinks/topbarCtaButton → navbar
- footerSocials → footer.socials
- openapi: single string per group instead of array
- Split docs into Documentation + API Reference tabs
Set up Mintlify to serve docs at worldmonitor.app/docs via Vercel rewrites
proxying to worldmonitor.mintlify.dev.
- Add mint.json with navigation (5 doc groups + 22 OpenAPI API references)
- Add Vercel rewrites for /docs, exclude from SPA catch-all and no-cache rules
- Exclude /docs from CSP headers (Mintlify manages its own scripts/styles)
- Add frontmatter to all 18 navigation docs for proper Mintlify rendering
- Fix internal links from ./FILE.md to /FILE format for Mintlify routing
- Convert ../path links to GitHub URLs (source code references)
- Add .mintlifyignore for internal docs (Docs_To_Review, roadmap, etc.)
- Copy app icon as logo.png and favicon.png
Document /api/health and /api/seed-health with response schemas,
key classifications, staleness thresholds, and monitoring integration
examples. Linked from the main documentation index.
* feat(natural): add tropical cyclone tracking from NHC and GDACS
Integrate NHC ArcGIS REST API (15 storm slots across AT/EP/CP basins)
and GDACS TC field extraction to provide real-time tropical cyclone data
with forecast tracks, uncertainty cones, and historical track paths.
- Proto: add optional TC fields (storm_id, wind_kt, pressure_mb, etc.)
plus ForecastPoint, PastTrackPoint, CoordRing messages
- Server/seed: NHC two-pass query (forecast points then detail layers),
GDACS wind/pressure parsing, Saffir-Simpson classification, dedup
strategy (NHC > GDACS > EONET), pressureMb validation (850-1050),
advisory date with Number.isFinite guard
- Globe: dashed red forecast track, per-segment wind-colored past track,
semi-transparent orange forecast cone polygon
- Popup: TC details panel with color-coded category badge, wind/pressure
- Frontend mapper: forward all TC fields, convert CoordRing to number[][][]
* fix(natural): improve GDACS dedup, NHC classification, and TC popup i18n
- GDACS dedup now checks name + geographic proximity instead of name-only
- NHC classification uses stormtype field for subtropical/post-tropical
- TC popup labels use t() for localization instead of hardcoded English
* feat(map): add cyclone-specific deck.gl layers for 2D map
- Storm center ScatterplotLayer with Saffir-Simpson wind coloring
- Past track PathLayer with per-segment wind-speed color ramp
- Forecast track PathLayer with dashed line via PathStyleExtension
- Cone PolygonLayer for forecast uncertainty visualization
- Tooltip and click routing for all new storm layer IDs
* fix(map): remove click routing for synthetic storm track/cone layers
Track and cone layers carry lightweight objects without full NaturalEvent
fields. Clicking them would pass incomplete data to the popup renderer.
Only storm-centers-layer (which holds the full NaturalEvent) routes to
the natEvent popup. Tracks and cones remain tooltip-only.
* fix(map): attach parent NaturalEvent to synthetic storm layers for clicks
Synthetic track/cone objects now carry _event reference to the parent
NaturalEvent. Click handler unwraps _event before passing to popup,
so clicking any storm element opens the full TC popup.
Non-technical document for press and external audiences covering
what World Monitor does, where data comes from, key metrics,
scoring systems, privacy posture, roadmap highlights, and 12 FAQs.
* fix(railway): use npm install instead of npm ci for Railpack compat
Railpack runs the install step before source files are fully copied
into the build container, so package-lock.json isn't available when
npm ci executes. Switch to npm install --omit=dev which doesn't
require a pre-existing lockfile.
* fix(docs): add blank lines around lists in roadmap-pro.md
Auto-fix MD032 markdownlint violations (blanks-around-lists).
* 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
Track ~80-120 intelligence-relevant satellites on the 3D globe using CelesTrak
TLE data and client-side SGP4 propagation (satellite.js). Satellites render at
actual orbital altitude with country-coded colors, 15-min orbit trails, and
ground footprint projections.
Architecture: Railway seeds TLEs every 2h → Redis → Vercel CDN (1h cache) →
browser does SGP4 math every 3s (zero server cost for real-time movement).
- New relay seed loop (ais-relay.cjs) fetching military + resource groups
- New edge handler (api/satellites.js) with 10min cache + negative cache
- Frontend service with circuit breaker and propagation lifecycle
- GlobeMap integration: markers, trails (pathsData), footprints, tooltips
- Layer registry as globe-only "Orbital Surveillance" with i18n (21 locales)
- Full documentation at docs/ORBITAL_SURVEILLANCE.md with roadmap
- Fix pre-existing SearchModal TS error (non-null assertion)
* feat(pro): restructure landing page with hybrid draft/current layout
Reorganize the Pro landing page into a 13-section structure that combines
the best of the external copy draft with existing high-value components:
New sections added:
- Two-path split (Pro vs Enterprise) right after social proof
- "Why upgrade" value props (Less noise, Faster, Control, Deeper)
- Audience personas (Journalists, Investors, Researchers, Security, Teams)
- Final dual CTA ("Get Pro" + "Talk to Sales")
Kept from current page:
- Live dashboard iframe embed
- Source marquee (43 scrolling sources)
- Slack morning brief mock
- API section with code example (separate tier)
- Enterprise specifics (air-gapped, MCP, white-label, satellite)
Copy updates:
- Hero: "for serious users and organizations" + mission line
- FAQ: warmer tone, 8 questions including "Is this only for conflict monitoring?"
- Schema markup updated to match new FAQ
* fix(pro): CRO quick wins — unified CTA, benefit-first hero, invisible Turnstile
- Unify all CTAs to "Reserve Your Early Access"
- Hero subtitle rewritten benefit-first ("Understand global events faster")
- Add "Launching March 2026" timeline badge
- Make Turnstile CAPTCHA invisible (size: 'invisible')
- Replace Enterprise mailto with inline contact form
- Move audience personas higher, add Gov & Energy traders, remove Journalists
- Update FAQ structured data in source template
- Remove unused Newspaper import
* fix(pro): update README image to jpg + add iframe fallback image
- README: reference worldmonitor-7-mar-2026.jpg instead of png
- Pro landing: add fallback image behind iframe for loading state
* feat(pro): dedicated enterprise page with contact form via hash routing
- Move enterprise contact form to dedicated #enterprise page
- Enterprise page has: hero, feature grid, use cases, and contact form
- Pro page enterprise section now links to #enterprise instead of inline form
- Hash routing: #enterprise → EnterprisePage, everything else → Pro landing
- Re-render Turnstile widgets on page transitions
* fix(pro): use Vite asset import for iframe fallback image (cached path)
Import dashboard fallback image via Vite so it gets content-hashed
into /pro/assets/ — hits CF 1-month immutable cache rule.
Placeholder jpg included; replace with real screenshot before deploy.
* fix(pro): add real dashboard screenshot for iframe fallback + README
Replace placeholder with actual 326K screenshot. Vite content-hashes
it to /pro/assets/worldmonitor-7-mar-2026-[hash].jpg (CF 1mo cache).
Also added to docs/images/ for README reference.
Create docs/CORS.md with allowed origins, step-by-step pattern for
adding CORS to new edge functions, and guidance on adding new origins.
Link from docs/ARCHITECTURE.md under API & Data Pipeline.
* docs: add maps infrastructure and geocoding reference
New MAPS_AND_GEOCODING.md covers R2 CDN architecture
(maps.worldmonitor.app), the country geometry service,
boundary override mechanism (#1044), and common pitfalls.
Cross-referenced from MAP_ENGINE.md.
* docs: align with PR #1150 boundary override implementation
- Data flow: sequential load (base first, then override with 3s timeout)
instead of Promise.all parallel fetch
- Override URL: R2 CDN (maps.worldmonitor.app), base from /data/ (Vercel)
- Override lookup: Map-based O(1) matching, not find() O(n)
- Common mistakes: timeout guidance replaces stale parallel fetch advice
Add a Map Theme dropdown in Settings below Map Tile Provider that lets
users pick the basemap visual style. Each provider has different themes:
- PMTiles: Black (default), Dark, Grayscale, Light, White
- OpenFreeMap: Dark (default), Positron
- CARTO: Dark Matter (default), Voyager, Positron
Map theme is fully independent of app theme (Auto/Dark/Light) — app
theme only affects UI chrome. Theme selection is per-provider and
persisted independently in localStorage. Overlay paint colors adapt to
the map theme, not the app theme.
* feat(map): migrate basemap from CARTO to self-hosted PMTiles on Cloudflare R2
Replace CARTO tile provider (frequent 403 errors) with self-hosted PMTiles
served from Cloudflare R2. Uses @protomaps/basemaps for style generation
with OpenFreeMap as automatic fallback when VITE_PMTILES_URL is unset.
- Add pmtiles and @protomaps/basemaps dependencies
- Create src/config/basemap.ts for PMTiles protocol registration and style building
- Update DeckGLMap.ts to use PMTiles styles (non-happy variants)
- Fix fallback detection using data event instead of style.load
- Update SW cache rules: replace CARTO/MapTiler with PMTiles NetworkFirst
- Add Protomaps preconnect hints in index.html
- Bundle pmtiles + @protomaps/basemaps in maplibre chunk
- Upload 3.4GB world tiles (zoom 0-10) to R2 bucket worldmonitor-maps
* fix(map): use CDN custom domain maps.worldmonitor.app for PMTiles
Replace r2.dev URL with custom domain backed by Cloudflare CDN edge.
Update preconnect hint and .env.example with production URL.
* fix(map): harden PMTiles fallback detection to prevent false triggers
- Require 2+ network errors before triggering OpenFreeMap fallback
- Use persistent data listener instead of once (clears timeout on first tile load)
- Increase fallback timeout to 10s for PMTiles header + initial tile fetch
- Add console.warn for map errors to aid debugging
- Remove redundant style.load listener (fires immediately for inline styles)
* feat(settings): add Map Tile Provider selector in settings
Add dropdown in Settings → Map section to switch between:
- Auto (PMTiles → OpenFreeMap fallback)
- PMTiles (self-hosted)
- OpenFreeMap
- CARTO
Choice persists in localStorage and reloads basemap instantly.
* fix(map): make OSS-friendly — default to free OpenFreeMap, hide PMTiles when unconfigured
- Default to OpenFreeMap when VITE_PMTILES_URL is unset (zero config for OSS users)
- Hide PMTiles/Auto options from settings dropdown when no PMTiles URL configured
- If user previously selected PMTiles but env var is removed, gracefully fall back
- Remove production URL from .env.example to avoid exposing hosted tiles
- Add docs link for self-hosting PMTiles in .env.example
* docs: add map tile provider documentation to README and MAP_ENGINE.md
Document the tile provider system (OpenFreeMap, CARTO, PMTiles) in
MAP_ENGINE.md with self-hosting instructions, fallback behavior, and
OSS-friendly defaults. Update README to reference tile providers in
the feature list, tech stack, and environment variables table.
* fix: resolve rebase conflicts and fix markdown lint errors
- Restore OSS-friendly basemap defaults (MAP_PROVIDER_OPTIONS as IIFE,
getMapProvider with hasTilesUrl check)
- Fix markdown lint: add blank lines after ### headings in README
- Reconcile UnifiedSettings import with MAP_PROVIDER_OPTIONS constant
Stadia Maps tiles return HTTP 401 without an API key — style.json loads
but actual .pbf tile requests fail, leaving the map black with floating
data points (issue #1031).
Changes:
- Switch fallback to OpenFreeMap (free, no auth, CORS *, dark style)
- Replace overly broad 'style.json' error match with 'cartocdn.com'
- Store style load timeout on instance, clear in destroy()
- Update TODO-131 text to reference OpenFreeMap
CARTO basemap CORS errors weren't triggering the Stadia fallback because
the error message didn't always match 'Failed to fetch' or 'AJAXError'.
Add broader error pattern matching (CORS, NetworkError, style.json) and
a 5-second timeout that switches to fallback if style hasn't loaded.
Also adds TODO-131 for self-hosted Protomaps + CloudFront tiles to
eliminate third-party basemap dependency entirely.
* feat: move EONET/GDACS to server-side with Redis caching and bootstrap hydration
Browser-direct fetches to eonet.gsfc.nasa.gov and gdacs.org caused CORS
errors and had no server-side caching. This moves both to the standard
Vercel edge → cachedFetchJson → Redis → bootstrap hydration pattern.
- Add proto definitions for NaturalService with ListNaturalEvents RPC
- Create server handler merging EONET + GDACS with 30min Redis TTL
- Add Vercel edge function at /api/natural/v1/list-natural-events
- Register naturalEvents in bootstrap SLOW_KEYS for CDN hydration
- Replace browser-direct fetches with RPC client + circuit breaker
- Delete src/services/gdacs.ts (logic moved server-side)
* fix: restore @ts-nocheck on generated files stripped by buf generate
The promotion guide had stale figures from an earlier version that no
longer match the README and current state of the project:
- 150+ news feeds → 170+
- 35+ data layers → 40+
- 14 languages → 19
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* 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>
* 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>
* refactor(proto): consolidate SummarizeArticleResponse status fields
Replace redundant boolean/string status fields (cached, skipped, error,
error_type, reason) with a SummarizeStatus enum and a single
status_detail string. This addresses L-8 lint issue by reducing field
count and making the response status unambiguous.
- Add SummarizeStatus enum (UNSPECIFIED, SUCCESS, CACHED, SKIPPED, ERROR)
- Replace cached/skipped booleans with status enum field
- Merge error/error_type/reason into statusDetail string
- Reserve old field numbers (4, 7, 8, 9, 10) for wire compatibility
- Update server handlers and client code to use new fields
- Regenerate TypeScript types and OpenAPI docs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(proto): restore error and errorType fields for programmatic error handling
Keep the new SummarizeStatus enum and statusDetail for consolidated
status tracking, but restore the separate error and errorType fields
(proto field numbers 9, 10) to preserve structured error information
for downstream consumers that need to programmatically handle errors.
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Elie Habib <elie.habib@gmail.com>
* perf(api): convert classify-event to GET and add summarize-article cache endpoint for CDN caching
classify-event (7.9M calls/wk) was POST — bypassing all CDN caching.
Converting to GET with static cache tier (1hr) enables Cloudflare edge
caching. Degraded responses (no API key, empty title, errors) are marked
no-cache to prevent caching error states.
summarize-article has repeated headlines too large for URL params. Added
a new GetSummarizeArticleCache GET endpoint that looks up Redis by a
deterministic cache key. Client computes key via shared
buildSummaryCacheKey(), tries GET first (CDN-cacheable), falls back to
existing POST on miss. Shared module ensures client/server key parity.
* fix(types): wire missing DeductSituation and ListGulfQuotes RPCs, fix tsc errors
- Added DeductSituation RPC to intelligence/v1/service.proto (messages
existed, RPC declaration was missing)
- Added ListGulfQuotes proto + RPC to market/v1/service.proto (handler
existed, proto was missing)
- Fixed scrapedAt type mismatch in conflict/index.ts (int64 → string)
- Added @ts-nocheck to generated files with codegen type bugs
- Regenerated all sebuf client/server code
* fix(types): fix int64→string type mismatch in list-iran-events.ts