Repository: koala73/worldmonitor Branch: main Commit: b3644f701195 Files: 1349 Total size: 15.0 MB Directory structure: gitextract_xk6l94h3/ ├── .dockerignore ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ ├── config.yml │ │ ├── feature_request.yml │ │ └── new_data_source.yml │ ├── pull_request_template.md │ └── workflows/ │ ├── build-desktop.yml │ ├── contributor-trust.yml │ ├── docker-publish.yml │ ├── lint-code.yml │ ├── lint.yml │ ├── proto-check.yml │ ├── test-linux-app.yml │ ├── test.yml │ └── typecheck.yml ├── .gitignore ├── .husky/ │ ├── pre-commit │ └── pre-push ├── .markdownlint-cli2.jsonc ├── .npmrc ├── .nvmrc ├── .vercelignore ├── AGENTS.md ├── ARCHITECTURE.md ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── Dockerfile.relay ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── SELF_HOSTING.md ├── api/ │ ├── _api-key.js │ ├── _cors.js │ ├── _cors.test.mjs │ ├── _github-release.js │ ├── _ip-rate-limit.js │ ├── _json-response.js │ ├── _rate-limit.js │ ├── _relay.js │ ├── _rss-allowed-domains.js │ ├── _turnstile.js │ ├── _turnstile.test.mjs │ ├── _upstash-json.js │ ├── ais-snapshot.js │ ├── aviation/ │ │ └── v1/ │ │ └── [rpc].ts │ ├── bootstrap.js │ ├── cache-purge.js │ ├── climate/ │ │ └── v1/ │ │ └── [rpc].ts │ ├── conflict/ │ │ └── v1/ │ │ └── [rpc].ts │ ├── contact.js │ ├── cyber/ │ │ └── v1/ │ │ └── [rpc].ts │ ├── data/ │ │ └── city-coords.ts │ ├── displacement/ │ │ └── v1/ │ │ └── [rpc].ts │ ├── download.js │ ├── economic/ │ │ └── v1/ │ │ └── [rpc].ts │ ├── eia/ │ │ └── [[...path]].js │ ├── enrichment/ │ │ ├── _domain.js │ │ ├── company.js │ │ └── signals.js │ ├── forecast/ │ │ └── v1/ │ │ └── [rpc].ts │ ├── fwdstart.js │ ├── geo.js │ ├── giving/ │ │ └── v1/ │ │ └── [rpc].ts │ ├── gpsjam.js │ ├── health.js │ ├── imagery/ │ │ └── v1/ │ │ └── [rpc].ts │ ├── infrastructure/ │ │ └── v1/ │ │ └── [rpc].ts │ ├── intelligence/ │ │ └── v1/ │ │ └── [rpc].ts │ ├── loaders-xml-wms-regression.test.mjs │ ├── maritime/ │ │ └── v1/ │ │ └── [rpc].ts │ ├── market/ │ │ └── v1/ │ │ └── [rpc].ts │ ├── mcp-proxy.js │ ├── military/ │ │ └── v1/ │ │ └── [rpc].ts │ ├── military-flights.js │ ├── natural/ │ │ └── v1/ │ │ └── [rpc].ts │ ├── news/ │ │ └── v1/ │ │ └── [rpc].ts │ ├── og-story.js │ ├── og-story.test.mjs │ ├── opensky.js │ ├── oref-alerts.js │ ├── polymarket.js │ ├── positive-events/ │ │ └── v1/ │ │ └── [rpc].ts │ ├── prediction/ │ │ └── v1/ │ │ └── [rpc].ts │ ├── radiation/ │ │ └── v1/ │ │ └── [rpc].ts │ ├── register-interest.js │ ├── research/ │ │ └── v1/ │ │ └── [rpc].ts │ ├── reverse-geocode.js │ ├── rss-proxy.js │ ├── sanctions/ │ │ └── v1/ │ │ └── [rpc].ts │ ├── satellites.js │ ├── seed-health.js │ ├── seismology/ │ │ └── v1/ │ │ └── [rpc].ts │ ├── story.js │ ├── supply-chain/ │ │ └── v1/ │ │ └── [rpc].ts │ ├── telegram-feed.js │ ├── thermal/ │ │ └── v1/ │ │ └── [rpc].ts │ ├── trade/ │ │ └── v1/ │ │ └── [rpc].ts │ ├── unrest/ │ │ └── v1/ │ │ └── [rpc].ts │ ├── version.js │ ├── webcam/ │ │ └── v1/ │ │ └── [rpc].ts │ ├── wildfire/ │ │ └── v1/ │ │ └── [rpc].ts │ └── youtube/ │ ├── embed.js │ ├── embed.test.mjs │ └── live.js ├── biome.json ├── blog-site/ │ ├── .gitignore │ ├── .vscode/ │ │ ├── extensions.json │ │ └── launch.json │ ├── README.md │ ├── astro.config.mjs │ ├── package.json │ ├── public/ │ │ └── robots.txt │ ├── scripts/ │ │ └── generate-og-images.mjs │ ├── src/ │ │ ├── content/ │ │ │ └── blog/ │ │ │ ├── ai-powered-intelligence-without-the-cloud.md │ │ │ ├── build-on-worldmonitor-developer-api-open-source.md │ │ │ ├── command-palette-search-everything-instantly.md │ │ │ ├── cyber-threat-intelligence-for-security-teams.md │ │ │ ├── five-dashboards-one-platform-worldmonitor-variants.md │ │ │ ├── live-webcams-from-geopolitical-hotspots.md │ │ │ ├── monitor-global-supply-chains-and-commodity-disruptions.md │ │ │ ├── natural-disaster-monitoring-earthquakes-fires-volcanoes.md │ │ │ ├── osint-for-everyone-open-source-intelligence-democratized.md │ │ │ ├── prediction-markets-ai-forecasting-geopolitics.md │ │ │ ├── real-time-market-intelligence-for-traders-and-analysts.md │ │ │ ├── satellite-imagery-orbital-surveillance.md │ │ │ ├── track-global-conflicts-in-real-time.md │ │ │ ├── tracking-global-trade-routes-chokepoints-freight-costs.md │ │ │ ├── what-is-worldmonitor-real-time-global-intelligence.md │ │ │ ├── worldmonitor-in-21-languages-global-intelligence-for-everyone.md │ │ │ └── worldmonitor-vs-traditional-intelligence-tools.md │ │ ├── content.config.ts │ │ ├── layouts/ │ │ │ ├── Base.astro │ │ │ └── BlogPost.astro │ │ ├── pages/ │ │ │ ├── index.astro │ │ │ ├── posts/ │ │ │ │ └── [...id].astro │ │ │ └── rss.xml.ts │ │ └── styles/ │ │ └── global.css │ └── tsconfig.json ├── convex/ │ ├── _generated/ │ │ ├── api.d.ts │ │ ├── api.js │ │ ├── dataModel.d.ts │ │ ├── registerInterest.js │ │ ├── schema.js │ │ ├── server.d.ts │ │ └── server.js │ ├── contactMessages.ts │ ├── registerInterest.ts │ ├── schema.ts │ └── tsconfig.json ├── data/ │ ├── gamma-irradiators-raw.json │ ├── gamma-irradiators.json │ └── telegram-channels.json ├── deploy/ │ └── nginx/ │ └── brotli-api-proxy.conf ├── docker/ │ ├── .dockerignore │ ├── Dockerfile │ ├── Dockerfile.redis-rest │ ├── build-handlers.mjs │ ├── docker-entrypoint.sh │ ├── entrypoint.sh │ ├── nginx-security-headers.conf │ ├── nginx.conf │ ├── nginx.conf.template │ ├── redis-rest-proxy.mjs │ └── supervisord.conf ├── docker-compose.yml ├── docs/ │ ├── .mintignore │ ├── .mintlifyignore │ ├── COMMUNITY-PROMOTION-GUIDE.md │ ├── Docs_To_Review/ │ │ ├── API_REFERENCE.md │ │ ├── ARCHITECTURE.md │ │ ├── COMPONENTS.md │ │ ├── DATA_MODEL.md │ │ ├── DESKTOP_CONFIGURATION.md │ │ ├── DOCUMENTATION.md │ │ ├── EXTERNAL_APIS.md │ │ ├── NEWS_TRANSLATION_ANALYSIS.md │ │ ├── PANELS.md │ │ ├── RELEASE_PACKAGING.md │ │ ├── STATE_MANAGEMENT.md │ │ ├── TAURI_VALIDATION_REPORT.md │ │ ├── TODO_Performance.md │ │ ├── bugs.md │ │ ├── local-backend-audit.md │ │ ├── todo.md │ │ └── todo_docs.md │ ├── PRESS_KIT.md │ ├── TAURI_VALIDATION_REPORT.md │ ├── adding-endpoints.mdx │ ├── ai-intelligence.mdx │ ├── algorithms.mdx │ ├── api/ │ │ ├── AviationService.openapi.json │ │ ├── AviationService.openapi.yaml │ │ ├── ClimateService.openapi.json │ │ ├── ClimateService.openapi.yaml │ │ ├── ConflictService.openapi.json │ │ ├── ConflictService.openapi.yaml │ │ ├── CyberService.openapi.json │ │ ├── CyberService.openapi.yaml │ │ ├── DisplacementService.openapi.json │ │ ├── DisplacementService.openapi.yaml │ │ ├── EconomicService.openapi.json │ │ ├── EconomicService.openapi.yaml │ │ ├── ForecastService.openapi.json │ │ ├── ForecastService.openapi.yaml │ │ ├── GivingService.openapi.json │ │ ├── GivingService.openapi.yaml │ │ ├── ImageryService.openapi.json │ │ ├── ImageryService.openapi.yaml │ │ ├── InfrastructureService.openapi.json │ │ ├── InfrastructureService.openapi.yaml │ │ ├── IntelligenceService.openapi.json │ │ ├── IntelligenceService.openapi.yaml │ │ ├── MaritimeService.openapi.json │ │ ├── MaritimeService.openapi.yaml │ │ ├── MarketService.openapi.json │ │ ├── MarketService.openapi.yaml │ │ ├── MilitaryService.openapi.json │ │ ├── MilitaryService.openapi.yaml │ │ ├── NaturalService.openapi.json │ │ ├── NaturalService.openapi.yaml │ │ ├── NewsService.openapi.json │ │ ├── NewsService.openapi.yaml │ │ ├── PositiveEventsService.openapi.json │ │ ├── PositiveEventsService.openapi.yaml │ │ ├── PredictionService.openapi.json │ │ ├── PredictionService.openapi.yaml │ │ ├── RadiationService.openapi.json │ │ ├── RadiationService.openapi.yaml │ │ ├── ResearchService.openapi.json │ │ ├── ResearchService.openapi.yaml │ │ ├── SanctionsService.openapi.json │ │ ├── SanctionsService.openapi.yaml │ │ ├── SeismologyService.openapi.json │ │ ├── SeismologyService.openapi.yaml │ │ ├── SupplyChainService.openapi.json │ │ ├── SupplyChainService.openapi.yaml │ │ ├── ThermalService.openapi.json │ │ ├── ThermalService.openapi.yaml │ │ ├── TradeService.openapi.json │ │ ├── TradeService.openapi.yaml │ │ ├── UnrestService.openapi.json │ │ ├── UnrestService.openapi.yaml │ │ ├── WebcamService.openapi.json │ │ ├── WebcamService.openapi.yaml │ │ ├── WildfireService.openapi.json │ │ └── WildfireService.openapi.yaml │ ├── api-key-deployment.mdx │ ├── architecture.mdx │ ├── changelog.mdx │ ├── contributing.mdx │ ├── cors.mdx │ ├── country-instability-index.mdx │ ├── data-sources.mdx │ ├── desktop-app.mdx │ ├── docs.json │ ├── documentation.mdx │ ├── features.mdx │ ├── finance-data.mdx │ ├── geographic-convergence.mdx │ ├── getting-started.mdx │ ├── harness-engineering-roadmap.md │ ├── health-endpoints.mdx │ ├── hotspots.mdx │ ├── infrastructure-cascade.mdx │ ├── license.mdx │ ├── local-backend-audit.md │ ├── map-engine.mdx │ ├── maps-and-geocoding.mdx │ ├── maritime-intelligence.mdx │ ├── military-tracking.mdx │ ├── natural-disasters.mdx │ ├── orbital-surveillance.mdx │ ├── overview.mdx │ ├── premium-finance-search.mdx │ ├── premium-finance.mdx │ ├── relay-parameters.mdx │ ├── release-packaging.mdx │ ├── roadmap-pro.md │ ├── signal-intelligence.mdx │ ├── strategic-risk.mdx │ ├── user-requests.md │ └── webcam-layer.mdx ├── e2e/ │ ├── circuit-breaker-persistence.spec.ts │ ├── deduct-situation.spec.ts │ ├── investments-panel.spec.ts │ ├── keyword-spike-flow.spec.ts │ ├── map-harness.spec.ts │ ├── mobile-map-native.spec.ts │ ├── mobile-map-popup.spec.ts │ ├── rag-vector-store.spec.ts │ ├── runtime-fetch.spec.ts │ ├── theme-toggle.spec.ts │ ├── tsconfig.json │ └── widget-builder.spec.ts ├── index.html ├── live-channels.html ├── middleware.ts ├── nixpacks.toml ├── package.json ├── playwright.config.ts ├── pro-test/ │ ├── .env.example │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── metadata.json │ ├── package.json │ ├── prerender.mjs │ ├── src/ │ │ ├── App.tsx │ │ ├── i18n.ts │ │ ├── index.css │ │ ├── locales/ │ │ │ ├── ar.json │ │ │ ├── bg.json │ │ │ ├── cs.json │ │ │ ├── de.json │ │ │ ├── el.json │ │ │ ├── en.json │ │ │ ├── es.json │ │ │ ├── fr.json │ │ │ ├── it.json │ │ │ ├── ja.json │ │ │ ├── ko.json │ │ │ ├── nl.json │ │ │ ├── pl.json │ │ │ ├── pt.json │ │ │ ├── ro.json │ │ │ ├── ru.json │ │ │ ├── sv.json │ │ │ ├── th.json │ │ │ ├── tr.json │ │ │ ├── vi.json │ │ │ └── zh.json │ │ └── main.tsx │ ├── tsconfig.json │ └── vite.config.ts ├── proto/ │ ├── buf.gen.yaml │ ├── buf.yaml │ ├── sebuf/ │ │ └── http/ │ │ └── annotations.proto │ └── worldmonitor/ │ ├── aviation/ │ │ └── v1/ │ │ ├── airport_delay.proto │ │ ├── aviation_news_item.proto │ │ ├── carrier.proto │ │ ├── flight_instance.proto │ │ ├── get_airport_ops_summary.proto │ │ ├── get_carrier_ops.proto │ │ ├── get_flight_status.proto │ │ ├── list_airport_delays.proto │ │ ├── list_airport_flights.proto │ │ ├── list_aviation_news.proto │ │ ├── position_sample.proto │ │ ├── price_quote.proto │ │ ├── search_flight_prices.proto │ │ ├── service.proto │ │ └── track_aircraft.proto │ ├── climate/ │ │ └── v1/ │ │ ├── climate_anomaly.proto │ │ ├── list_climate_anomalies.proto │ │ └── service.proto │ ├── conflict/ │ │ └── v1/ │ │ ├── acled_event.proto │ │ ├── get_humanitarian_summary.proto │ │ ├── get_humanitarian_summary_batch.proto │ │ ├── humanitarian_summary.proto │ │ ├── list_acled_events.proto │ │ ├── list_iran_events.proto │ │ ├── list_ucdp_events.proto │ │ ├── service.proto │ │ └── ucdp_event.proto │ ├── core/ │ │ └── v1/ │ │ ├── country.proto │ │ ├── general_error.proto │ │ ├── geo.proto │ │ ├── i18n.proto │ │ ├── identifiers.proto │ │ ├── pagination.proto │ │ ├── severity.proto │ │ └── time.proto │ ├── cyber/ │ │ └── v1/ │ │ ├── cyber_threat.proto │ │ ├── list_cyber_threats.proto │ │ └── service.proto │ ├── displacement/ │ │ └── v1/ │ │ ├── displacement.proto │ │ ├── get_displacement_summary.proto │ │ ├── get_population_exposure.proto │ │ └── service.proto │ ├── economic/ │ │ └── v1/ │ │ ├── bis_data.proto │ │ ├── economic_data.proto │ │ ├── get_bis_credit.proto │ │ ├── get_bis_exchange_rates.proto │ │ ├── get_bis_policy_rates.proto │ │ ├── get_energy_capacity.proto │ │ ├── get_energy_prices.proto │ │ ├── get_fred_series.proto │ │ ├── get_fred_series_batch.proto │ │ ├── get_macro_signals.proto │ │ ├── list_world_bank_indicators.proto │ │ └── service.proto │ ├── forecast/ │ │ └── v1/ │ │ ├── forecast.proto │ │ ├── get_forecasts.proto │ │ └── service.proto │ ├── giving/ │ │ └── v1/ │ │ ├── get_giving_summary.proto │ │ ├── giving.proto │ │ └── service.proto │ ├── imagery/ │ │ └── v1/ │ │ ├── search_imagery.proto │ │ └── service.proto │ ├── infrastructure/ │ │ └── v1/ │ │ ├── get_cable_health.proto │ │ ├── get_temporal_baseline.proto │ │ ├── infrastructure.proto │ │ ├── list_internet_outages.proto │ │ ├── list_service_statuses.proto │ │ ├── list_temporal_anomalies.proto │ │ ├── record_baseline_snapshot.proto │ │ └── service.proto │ ├── intelligence/ │ │ └── v1/ │ │ ├── classify_event.proto │ │ ├── deduct_situation.proto │ │ ├── get_country_facts.proto │ │ ├── get_country_intel_brief.proto │ │ ├── get_pizzint_status.proto │ │ ├── get_risk_scores.proto │ │ ├── intelligence.proto │ │ ├── list_security_advisories.proto │ │ ├── search_gdelt_documents.proto │ │ └── service.proto │ ├── maritime/ │ │ └── v1/ │ │ ├── get_vessel_snapshot.proto │ │ ├── list_navigational_warnings.proto │ │ ├── service.proto │ │ └── vessel_snapshot.proto │ ├── market/ │ │ └── v1/ │ │ ├── analyze_stock.proto │ │ ├── backtest_stock.proto │ │ ├── get_country_stock_index.proto │ │ ├── get_sector_summary.proto │ │ ├── get_stock_analysis_history.proto │ │ ├── list_commodity_quotes.proto │ │ ├── list_crypto_quotes.proto │ │ ├── list_etf_flows.proto │ │ ├── list_gulf_quotes.proto │ │ ├── list_market_quotes.proto │ │ ├── list_stablecoin_markets.proto │ │ ├── list_stored_stock_backtests.proto │ │ ├── market_quote.proto │ │ └── service.proto │ ├── military/ │ │ └── v1/ │ │ ├── get_aircraft_details.proto │ │ ├── get_aircraft_details_batch.proto │ │ ├── get_theater_posture.proto │ │ ├── get_usni_fleet_report.proto │ │ ├── get_wingbits_live_flight.proto │ │ ├── get_wingbits_status.proto │ │ ├── list_military_bases.proto │ │ ├── list_military_flights.proto │ │ ├── military_flight.proto │ │ ├── military_vessel.proto │ │ ├── service.proto │ │ └── usni_fleet.proto │ ├── natural/ │ │ └── v1/ │ │ ├── list_natural_events.proto │ │ └── service.proto │ ├── news/ │ │ └── v1/ │ │ ├── get_summarize_article_cache.proto │ │ ├── list_feed_digest.proto │ │ ├── news_item.proto │ │ ├── service.proto │ │ └── summarize_article.proto │ ├── positive_events/ │ │ └── v1/ │ │ ├── list_positive_geo_events.proto │ │ └── service.proto │ ├── prediction/ │ │ └── v1/ │ │ ├── list_prediction_markets.proto │ │ ├── prediction_market.proto │ │ └── service.proto │ ├── radiation/ │ │ └── v1/ │ │ ├── list_radiation_observations.proto │ │ ├── radiation_observation.proto │ │ └── service.proto │ ├── research/ │ │ └── v1/ │ │ ├── list_arxiv_papers.proto │ │ ├── list_hackernews_items.proto │ │ ├── list_tech_events.proto │ │ ├── list_trending_repos.proto │ │ ├── research_item.proto │ │ └── service.proto │ ├── sanctions/ │ │ └── v1/ │ │ ├── country_sanctions_pressure.proto │ │ ├── list_sanctions_pressure.proto │ │ ├── program_sanctions_pressure.proto │ │ ├── sanctions_entry.proto │ │ └── service.proto │ ├── seismology/ │ │ └── v1/ │ │ ├── earthquake.proto │ │ ├── list_earthquakes.proto │ │ └── service.proto │ ├── supply_chain/ │ │ └── v1/ │ │ ├── get_chokepoint_status.proto │ │ ├── get_critical_minerals.proto │ │ ├── get_shipping_rates.proto │ │ ├── service.proto │ │ └── supply_chain_data.proto │ ├── thermal/ │ │ └── v1/ │ │ ├── list_thermal_escalations.proto │ │ ├── service.proto │ │ └── thermal_escalation_cluster.proto │ ├── trade/ │ │ └── v1/ │ │ ├── get_customs_revenue.proto │ │ ├── get_tariff_trends.proto │ │ ├── get_trade_barriers.proto │ │ ├── get_trade_flows.proto │ │ ├── get_trade_restrictions.proto │ │ ├── service.proto │ │ └── trade_data.proto │ ├── unrest/ │ │ └── v1/ │ │ ├── list_unrest_events.proto │ │ ├── service.proto │ │ └── unrest_event.proto │ ├── webcam/ │ │ └── v1/ │ │ ├── get_webcam_image.proto │ │ ├── list_webcams.proto │ │ └── service.proto │ └── wildfire/ │ └── v1/ │ ├── fire_detection.proto │ ├── list_fire_detections.proto │ └── service.proto ├── public/ │ ├── .well-known/ │ │ └── security.txt │ ├── a7f3e9d1b2c44e8f9a0b1c2d3e4f5a6b.txt │ ├── data/ │ │ ├── countries.geojson │ │ └── country-boundary-overrides.geojson │ ├── llms-full.txt │ ├── llms.txt │ ├── map-styles/ │ │ ├── happy-dark.json │ │ └── happy-light.json │ ├── offline.html │ ├── pro/ │ │ ├── assets/ │ │ │ ├── ar-BHa0nEOe.js │ │ │ ├── bg-Ci69To5a.js │ │ │ ├── cs-CqKhwIlR.js │ │ │ ├── de-B71p-f-t.js │ │ │ ├── el-DJwjBufy.js │ │ │ ├── es-aR_qLKIk.js │ │ │ ├── fr-BrtwTv_R.js │ │ │ ├── index-DQXUpmjr.css │ │ │ ├── index-k66dEz6-.js │ │ │ ├── it-DHbGtQXZ.js │ │ │ ├── ja-D8-35S3Y.js │ │ │ ├── ko-otMG-p7A.js │ │ │ ├── nl-B3DRC8p4.js │ │ │ ├── pl-DqoCbf3Z.js │ │ │ ├── pt-CqDblfWm.js │ │ │ ├── ro-DaIMP80d.js │ │ │ ├── ru-DN0TfVz-.js │ │ │ ├── sv-B8YGwHj7.js │ │ │ ├── th-Dx5iTAoX.js │ │ │ ├── tr-DqKzKEKV.js │ │ │ ├── vi-ByRwBJoF.js │ │ │ └── zh-Cf0ddDO-.js │ │ └── index.html │ ├── robots.txt │ └── sitemap.xml ├── scripts/ │ ├── _clustering.mjs │ ├── _military-surges.mjs │ ├── _prediction-scoring.mjs │ ├── _r2-storage.mjs │ ├── _seed-utils.mjs │ ├── _trade-parse-utils.mjs │ ├── ais-relay-rss.test.cjs │ ├── ais-relay.cjs │ ├── build-military-bases-final.mjs │ ├── build-sidecar-handlers.mjs │ ├── build-sidecar-sebuf.mjs │ ├── check-unicode-safety.mjs │ ├── data/ │ │ ├── cascade-rules.json │ │ ├── country-codes.json │ │ ├── curated-bases.json │ │ ├── entity-graph.json │ │ ├── forecast-evaluation-benchmark.json │ │ ├── forecast-historical-benchmark.json │ │ ├── mirta-processed.json │ │ └── prediction-tags.json │ ├── desktop-package.mjs │ ├── download-node.sh │ ├── evaluate-forecast-benchmark.mjs │ ├── extract-forecast-benchmark-candidates.mjs │ ├── fetch-country-boundary-overrides.mjs │ ├── fetch-gpsjam.mjs │ ├── fetch-mirta-bases.mjs │ ├── fetch-osm-bases.mjs │ ├── fetch-pizzint-bases.mjs │ ├── generate-oref-locations.mjs │ ├── lib/ │ │ └── thermal-escalation.mjs │ ├── lint-boundaries.mjs │ ├── need-work.csv │ ├── nixpacks.toml │ ├── package.json │ ├── promote-forecast-benchmark-candidate.mjs │ ├── railway-set-watch-paths.mjs │ ├── rss-feeds-report.csv │ ├── run-seeders.sh │ ├── seed-airport-delays.mjs │ ├── seed-aviation.mjs │ ├── seed-bis-data.mjs │ ├── seed-climate-anomalies.mjs │ ├── seed-commodity-quotes.mjs │ ├── seed-conflict-intel.mjs │ ├── seed-correlation.mjs │ ├── seed-crypto-quotes.mjs │ ├── seed-cyber-threats.mjs │ ├── seed-displacement-summary.mjs │ ├── seed-earthquakes.mjs │ ├── seed-economy.mjs │ ├── seed-etf-flows.mjs │ ├── seed-fire-detections.mjs │ ├── seed-forecasts.mjs │ ├── seed-gdelt-intel.mjs │ ├── seed-gulf-quotes.mjs │ ├── seed-infra.mjs │ ├── seed-insights.mjs │ ├── seed-internet-outages.mjs │ ├── seed-iran-events.mjs │ ├── seed-market-quotes.mjs │ ├── seed-military-bases.mjs │ ├── seed-military-flights.mjs │ ├── seed-military-maritime-news.mjs │ ├── seed-natural-events.mjs │ ├── seed-prediction-markets.mjs │ ├── seed-radiation-watch.mjs │ ├── seed-research.mjs │ ├── seed-sanctions-pressure.mjs │ ├── seed-security-advisories.mjs │ ├── seed-service-statuses.mjs │ ├── seed-stablecoin-markets.mjs │ ├── seed-submarine-cables.mjs │ ├── seed-supply-chain-trade.mjs │ ├── seed-thermal-escalation.mjs │ ├── seed-ucdp-events.mjs │ ├── seed-unrest-events.mjs │ ├── seed-usa-spending.mjs │ ├── seed-wb-indicators.mjs │ ├── seed-weather-alerts.mjs │ ├── seed-webcams.mjs │ ├── seo-indexnow-submit.mjs │ ├── shared/ │ │ ├── acled-oauth.mjs │ │ ├── commodities.json │ │ ├── country-names.json │ │ ├── crypto.json │ │ ├── etfs.json │ │ ├── gulf.json │ │ ├── rss-allowed-domains.cjs │ │ ├── rss-allowed-domains.json │ │ ├── sectors.json │ │ ├── stablecoins.json │ │ └── stocks.json │ ├── sync-desktop-version.mjs │ ├── telegram/ │ │ └── session-auth.mjs │ ├── validate-rss-feeds.mjs │ ├── validate-seed-migration.mjs │ └── vercel-ignore.sh ├── server/ │ ├── _shared/ │ │ ├── acled-auth.ts │ │ ├── acled.ts │ │ ├── cache-keys.ts │ │ ├── constants.ts │ │ ├── hash.ts │ │ ├── llm-health.ts │ │ ├── llm-sanitize.d.ts │ │ ├── llm-sanitize.js │ │ ├── llm.ts │ │ ├── normalize-list.ts │ │ ├── parse-string-array.ts │ │ ├── rate-limit.ts │ │ ├── redis.ts │ │ ├── response-headers.ts │ │ └── sidecar-cache.ts │ ├── cors.ts │ ├── env.d.ts │ ├── error-mapper.ts │ ├── gateway.ts │ ├── router.ts │ └── worldmonitor/ │ ├── _bootstrap-cache-key-refs.ts │ ├── aviation/ │ │ └── v1/ │ │ ├── _providers/ │ │ │ ├── demo_prices.ts │ │ │ └── travelpayouts_data.ts │ │ ├── _shared.ts │ │ ├── get-airport-ops-summary.ts │ │ ├── get-carrier-ops.ts │ │ ├── get-flight-status.ts │ │ ├── handler.ts │ │ ├── list-airport-delays.ts │ │ ├── list-airport-flights.ts │ │ ├── list-aviation-news.ts │ │ ├── search-flight-prices.ts │ │ └── track-aircraft.ts │ ├── climate/ │ │ └── v1/ │ │ ├── handler.ts │ │ └── list-climate-anomalies.ts │ ├── conflict/ │ │ └── v1/ │ │ ├── _shared.ts │ │ ├── get-humanitarian-summary-batch.ts │ │ ├── get-humanitarian-summary.ts │ │ ├── handler.ts │ │ ├── list-acled-events.ts │ │ ├── list-iran-events.ts │ │ └── list-ucdp-events.ts │ ├── cyber/ │ │ └── v1/ │ │ ├── _shared.ts │ │ ├── handler.ts │ │ └── list-cyber-threats.ts │ ├── displacement/ │ │ └── v1/ │ │ ├── get-displacement-summary.ts │ │ ├── get-population-exposure.ts │ │ └── handler.ts │ ├── economic/ │ │ └── v1/ │ │ ├── _bis-shared.ts │ │ ├── _fetch-with-timeout.ts │ │ ├── _fred-shared.ts │ │ ├── _shared.ts │ │ ├── get-bis-credit.ts │ │ ├── get-bis-exchange-rates.ts │ │ ├── get-bis-policy-rates.ts │ │ ├── get-energy-capacity.ts │ │ ├── get-energy-prices.ts │ │ ├── get-fred-series-batch.ts │ │ ├── get-fred-series.ts │ │ ├── get-macro-signals.ts │ │ ├── handler.ts │ │ └── list-world-bank-indicators.ts │ ├── forecast/ │ │ └── v1/ │ │ ├── get-forecasts.ts │ │ └── handler.ts │ ├── giving/ │ │ └── v1/ │ │ ├── get-giving-summary.ts │ │ └── handler.ts │ ├── imagery/ │ │ └── v1/ │ │ ├── handler.ts │ │ └── search-imagery.ts │ ├── infrastructure/ │ │ └── v1/ │ │ ├── _shared.ts │ │ ├── get-cable-health.ts │ │ ├── get-temporal-baseline.ts │ │ ├── handler.ts │ │ ├── list-internet-outages.ts │ │ ├── list-service-statuses.ts │ │ ├── list-temporal-anomalies.ts │ │ └── record-baseline-snapshot.ts │ ├── intelligence/ │ │ └── v1/ │ │ ├── _batch-classify.ts │ │ ├── _shared.ts │ │ ├── classify-event.ts │ │ ├── deduct-situation.ts │ │ ├── deduction-prompt.ts │ │ ├── get-country-facts.ts │ │ ├── get-country-intel-brief.ts │ │ ├── get-pizzint-status.ts │ │ ├── get-risk-scores.ts │ │ ├── handler.ts │ │ ├── list-security-advisories.ts │ │ └── search-gdelt-documents.ts │ ├── maritime/ │ │ └── v1/ │ │ ├── get-vessel-snapshot.ts │ │ ├── handler.ts │ │ └── list-navigational-warnings.ts │ ├── market/ │ │ └── v1/ │ │ ├── _shared.ts │ │ ├── analyze-stock.ts │ │ ├── backtest-stock.ts │ │ ├── get-country-stock-index.ts │ │ ├── get-sector-summary.ts │ │ ├── get-stock-analysis-history.ts │ │ ├── handler.ts │ │ ├── list-commodity-quotes.ts │ │ ├── list-crypto-quotes.ts │ │ ├── list-etf-flows.ts │ │ ├── list-gulf-quotes.ts │ │ ├── list-market-quotes.ts │ │ ├── list-stablecoin-markets.ts │ │ ├── list-stored-stock-backtests.ts │ │ ├── premium-stock-store.ts │ │ └── stock-news-search.ts │ ├── military/ │ │ └── v1/ │ │ ├── _shared.ts │ │ ├── _wingbits-aircraft-details.ts │ │ ├── get-aircraft-details-batch.ts │ │ ├── get-aircraft-details.ts │ │ ├── get-theater-posture.ts │ │ ├── get-usni-fleet-report.ts │ │ ├── get-wingbits-live-flight.ts │ │ ├── get-wingbits-status.ts │ │ ├── handler.ts │ │ ├── list-military-bases.ts │ │ └── list-military-flights.ts │ ├── natural/ │ │ └── v1/ │ │ ├── handler.ts │ │ └── list-natural-events.ts │ ├── news/ │ │ └── v1/ │ │ ├── _classifier.ts │ │ ├── _feeds.ts │ │ ├── _shared.ts │ │ ├── dedup.mjs │ │ ├── get-summarize-article-cache.ts │ │ ├── handler.ts │ │ ├── list-feed-digest.ts │ │ └── summarize-article.ts │ ├── positive-events/ │ │ └── v1/ │ │ ├── handler.ts │ │ └── list-positive-geo-events.ts │ ├── prediction/ │ │ └── v1/ │ │ ├── handler.ts │ │ └── list-prediction-markets.ts │ ├── radiation/ │ │ └── v1/ │ │ ├── handler.ts │ │ └── list-radiation-observations.ts │ ├── research/ │ │ └── v1/ │ │ ├── handler.ts │ │ ├── list-arxiv-papers.ts │ │ ├── list-hackernews-items.ts │ │ ├── list-tech-events.ts │ │ └── list-trending-repos.ts │ ├── sanctions/ │ │ └── v1/ │ │ ├── handler.ts │ │ └── list-sanctions-pressure.ts │ ├── seismology/ │ │ └── v1/ │ │ ├── handler.ts │ │ └── list-earthquakes.ts │ ├── supply-chain/ │ │ └── v1/ │ │ ├── _chokepoint-ids.ts │ │ ├── _corridorrisk-upstream.ts │ │ ├── _minerals-data.ts │ │ ├── _portwatch-upstream.ts │ │ ├── _scoring.mjs │ │ ├── get-chokepoint-status.ts │ │ ├── get-critical-minerals.ts │ │ ├── get-shipping-rates.ts │ │ └── handler.ts │ ├── thermal/ │ │ └── v1/ │ │ ├── handler.ts │ │ └── list-thermal-escalations.ts │ ├── trade/ │ │ └── v1/ │ │ ├── _shared.ts │ │ ├── get-customs-revenue.ts │ │ ├── get-tariff-trends.ts │ │ ├── get-trade-barriers.ts │ │ ├── get-trade-flows.ts │ │ ├── get-trade-restrictions.ts │ │ └── handler.ts │ ├── unrest/ │ │ └── v1/ │ │ ├── _shared.ts │ │ ├── handler.ts │ │ └── list-unrest-events.ts │ ├── webcam/ │ │ └── v1/ │ │ ├── get-webcam-image.ts │ │ ├── handler.ts │ │ └── list-webcams.ts │ └── wildfire/ │ └── v1/ │ ├── handler.ts │ └── list-fire-detections.ts ├── settings.html ├── shared/ │ ├── commodities.json │ ├── country-names.json │ ├── crypto.json │ ├── etfs.json │ ├── gulf.json │ ├── rss-allowed-domains.cjs │ ├── rss-allowed-domains.json │ ├── sectors.json │ ├── stablecoins.json │ └── stocks.json ├── src/ │ ├── App.ts │ ├── app/ │ │ ├── app-context.ts │ │ ├── country-intel.ts │ │ ├── data-loader.ts │ │ ├── desktop-updater.ts │ │ ├── event-handlers.ts │ │ ├── index.ts │ │ ├── panel-layout.ts │ │ ├── pending-panel-data.ts │ │ ├── refresh-scheduler.ts │ │ └── search-manager.ts │ ├── bootstrap/ │ │ └── chunk-reload.ts │ ├── components/ │ │ ├── AirlineIntelPanel.ts │ │ ├── AviationCommandBar.ts │ │ ├── BreakingNewsBanner.ts │ │ ├── BreakthroughsTickerPanel.ts │ │ ├── CIIPanel.ts │ │ ├── CascadePanel.ts │ │ ├── ClimateAnomalyPanel.ts │ │ ├── CommunityWidget.ts │ │ ├── CorrelationPanel.ts │ │ ├── CountersPanel.ts │ │ ├── CountryBriefPage.ts │ │ ├── CountryBriefPanel.ts │ │ ├── CountryDeepDivePanel.ts │ │ ├── CountryIntelModal.ts │ │ ├── CountryTimeline.ts │ │ ├── CustomWidgetPanel.ts │ │ ├── DailyMarketBriefPanel.ts │ │ ├── DeckGLMap.ts │ │ ├── DeductionPanel.ts │ │ ├── DisasterCorrelationPanel.ts │ │ ├── DisplacementPanel.ts │ │ ├── DownloadBanner.ts │ │ ├── ETFFlowsPanel.ts │ │ ├── EconomicCorrelationPanel.ts │ │ ├── EconomicPanel.ts │ │ ├── EnergyComplexPanel.ts │ │ ├── EscalationCorrelationPanel.ts │ │ ├── ForecastPanel.ts │ │ ├── GdeltIntelPanel.ts │ │ ├── GeoHubsPanel.ts │ │ ├── GivingPanel.ts │ │ ├── GlobeMap.ts │ │ ├── GoodThingsDigestPanel.ts │ │ ├── GulfEconomiesPanel.ts │ │ ├── HeroSpotlightPanel.ts │ │ ├── InsightsPanel.ts │ │ ├── IntelligenceGapBadge.ts │ │ ├── InvestmentsPanel.ts │ │ ├── LiveNewsPanel.ts │ │ ├── LiveWebcamsPanel.ts │ │ ├── LlmStatusIndicator.ts │ │ ├── MacroSignalsPanel.ts │ │ ├── Map.ts │ │ ├── MapContainer.ts │ │ ├── MapContextMenu.ts │ │ ├── MapPopup.ts │ │ ├── MarketPanel.ts │ │ ├── McpConnectModal.ts │ │ ├── McpDataPanel.ts │ │ ├── MilitaryCorrelationPanel.ts │ │ ├── MobileWarningModal.ts │ │ ├── MonitorPanel.ts │ │ ├── NewsPanel.ts │ │ ├── OrefSirensPanel.ts │ │ ├── Panel.ts │ │ ├── PinnedWebcamsPanel.ts │ │ ├── PizzIntIndicator.ts │ │ ├── PlaybackControl.ts │ │ ├── PopulationExposurePanel.ts │ │ ├── PositiveNewsFeedPanel.ts │ │ ├── PredictionPanel.ts │ │ ├── ProBanner.ts │ │ ├── ProgressChartsPanel.ts │ │ ├── RadiationWatchPanel.ts │ │ ├── RegulationPanel.ts │ │ ├── RenewableEnergyPanel.ts │ │ ├── RuntimeConfigPanel.ts │ │ ├── SanctionsPressurePanel.ts │ │ ├── SatelliteFiresPanel.ts │ │ ├── SearchModal.ts │ │ ├── SecurityAdvisoriesPanel.ts │ │ ├── ServiceStatusPanel.ts │ │ ├── SignalModal.ts │ │ ├── SpeciesComebackPanel.ts │ │ ├── StablecoinPanel.ts │ │ ├── StatusPanel.ts │ │ ├── StockAnalysisPanel.ts │ │ ├── StockBacktestPanel.ts │ │ ├── StoryModal.ts │ │ ├── StrategicPosturePanel.ts │ │ ├── StrategicRiskPanel.ts │ │ ├── SupplyChainPanel.ts │ │ ├── TechEventsPanel.ts │ │ ├── TechHubsPanel.ts │ │ ├── TechReadinessPanel.ts │ │ ├── TelegramIntelPanel.ts │ │ ├── ThermalEscalationPanel.ts │ │ ├── TradePolicyPanel.ts │ │ ├── UcdpEventsPanel.ts │ │ ├── UnifiedSettings.ts │ │ ├── VerificationChecklist.ts │ │ ├── VirtualList.ts │ │ ├── WidgetChatModal.ts │ │ ├── WorldClockPanel.ts │ │ └── index.ts │ ├── config/ │ │ ├── ai-datacenters.ts │ │ ├── ai-regulations.ts │ │ ├── ai-research-labs.ts │ │ ├── airports.ts │ │ ├── basemap.ts │ │ ├── bases-expanded.ts │ │ ├── beta.ts │ │ ├── commands.ts │ │ ├── commodity-geo.ts │ │ ├── commodity-markets.ts │ │ ├── commodity-miners.ts │ │ ├── countries.ts │ │ ├── entities.ts │ │ ├── feeds.ts │ │ ├── finance-geo.ts │ │ ├── geo.ts │ │ ├── gulf-fdi.ts │ │ ├── index.ts │ │ ├── irradiators.ts │ │ ├── map-layer-definitions.ts │ │ ├── markets.ts │ │ ├── military.ts │ │ ├── ml-config.ts │ │ ├── panels.ts │ │ ├── pipelines.ts │ │ ├── ports.ts │ │ ├── startup-ecosystems.ts │ │ ├── tech-companies.ts │ │ ├── tech-geo.ts │ │ ├── trade-routes.ts │ │ ├── variant-meta.ts │ │ ├── variant.ts │ │ └── variants/ │ │ ├── base.ts │ │ ├── commodity.ts │ │ ├── finance.ts │ │ ├── full.ts │ │ ├── happy.ts │ │ └── tech.ts │ ├── data/ │ │ ├── conservation-wins.json │ │ ├── renewable-installations.json │ │ └── world-happiness.json │ ├── e2e/ │ │ ├── map-harness.ts │ │ ├── mobile-map-harness.ts │ │ └── mobile-map-integration-harness.ts │ ├── generated/ │ │ ├── client/ │ │ │ └── worldmonitor/ │ │ │ ├── aviation/ │ │ │ │ └── v1/ │ │ │ │ └── service_client.ts │ │ │ ├── climate/ │ │ │ │ └── v1/ │ │ │ │ └── service_client.ts │ │ │ ├── conflict/ │ │ │ │ └── v1/ │ │ │ │ └── service_client.ts │ │ │ ├── cyber/ │ │ │ │ └── v1/ │ │ │ │ └── service_client.ts │ │ │ ├── displacement/ │ │ │ │ └── v1/ │ │ │ │ └── service_client.ts │ │ │ ├── economic/ │ │ │ │ └── v1/ │ │ │ │ └── service_client.ts │ │ │ ├── forecast/ │ │ │ │ └── v1/ │ │ │ │ └── service_client.ts │ │ │ ├── giving/ │ │ │ │ └── v1/ │ │ │ │ └── service_client.ts │ │ │ ├── imagery/ │ │ │ │ └── v1/ │ │ │ │ └── service_client.ts │ │ │ ├── infrastructure/ │ │ │ │ └── v1/ │ │ │ │ └── service_client.ts │ │ │ ├── intelligence/ │ │ │ │ └── v1/ │ │ │ │ └── service_client.ts │ │ │ ├── maritime/ │ │ │ │ └── v1/ │ │ │ │ └── service_client.ts │ │ │ ├── market/ │ │ │ │ └── v1/ │ │ │ │ └── service_client.ts │ │ │ ├── military/ │ │ │ │ └── v1/ │ │ │ │ └── service_client.ts │ │ │ ├── natural/ │ │ │ │ └── v1/ │ │ │ │ └── service_client.ts │ │ │ ├── news/ │ │ │ │ └── v1/ │ │ │ │ └── service_client.ts │ │ │ ├── positive_events/ │ │ │ │ └── v1/ │ │ │ │ └── service_client.ts │ │ │ ├── prediction/ │ │ │ │ └── v1/ │ │ │ │ └── service_client.ts │ │ │ ├── radiation/ │ │ │ │ └── v1/ │ │ │ │ └── service_client.ts │ │ │ ├── research/ │ │ │ │ └── v1/ │ │ │ │ └── service_client.ts │ │ │ ├── sanctions/ │ │ │ │ └── v1/ │ │ │ │ └── service_client.ts │ │ │ ├── seismology/ │ │ │ │ └── v1/ │ │ │ │ └── service_client.ts │ │ │ ├── supply_chain/ │ │ │ │ └── v1/ │ │ │ │ └── service_client.ts │ │ │ ├── thermal/ │ │ │ │ └── v1/ │ │ │ │ └── service_client.ts │ │ │ ├── trade/ │ │ │ │ └── v1/ │ │ │ │ └── service_client.ts │ │ │ ├── unrest/ │ │ │ │ └── v1/ │ │ │ │ └── service_client.ts │ │ │ ├── webcam/ │ │ │ │ └── v1/ │ │ │ │ └── service_client.ts │ │ │ └── wildfire/ │ │ │ └── v1/ │ │ │ └── service_client.ts │ │ └── server/ │ │ └── worldmonitor/ │ │ ├── aviation/ │ │ │ └── v1/ │ │ │ └── service_server.ts │ │ ├── climate/ │ │ │ └── v1/ │ │ │ └── service_server.ts │ │ ├── conflict/ │ │ │ └── v1/ │ │ │ └── service_server.ts │ │ ├── cyber/ │ │ │ └── v1/ │ │ │ └── service_server.ts │ │ ├── displacement/ │ │ │ └── v1/ │ │ │ └── service_server.ts │ │ ├── economic/ │ │ │ └── v1/ │ │ │ └── service_server.ts │ │ ├── forecast/ │ │ │ └── v1/ │ │ │ └── service_server.ts │ │ ├── giving/ │ │ │ └── v1/ │ │ │ └── service_server.ts │ │ ├── imagery/ │ │ │ └── v1/ │ │ │ └── service_server.ts │ │ ├── infrastructure/ │ │ │ └── v1/ │ │ │ └── service_server.ts │ │ ├── intelligence/ │ │ │ └── v1/ │ │ │ └── service_server.ts │ │ ├── maritime/ │ │ │ └── v1/ │ │ │ └── service_server.ts │ │ ├── market/ │ │ │ └── v1/ │ │ │ └── service_server.ts │ │ ├── military/ │ │ │ └── v1/ │ │ │ └── service_server.ts │ │ ├── natural/ │ │ │ └── v1/ │ │ │ └── service_server.ts │ │ ├── news/ │ │ │ └── v1/ │ │ │ └── service_server.ts │ │ ├── positive_events/ │ │ │ └── v1/ │ │ │ └── service_server.ts │ │ ├── prediction/ │ │ │ └── v1/ │ │ │ └── service_server.ts │ │ ├── radiation/ │ │ │ └── v1/ │ │ │ └── service_server.ts │ │ ├── research/ │ │ │ └── v1/ │ │ │ └── service_server.ts │ │ ├── sanctions/ │ │ │ └── v1/ │ │ │ └── service_server.ts │ │ ├── seismology/ │ │ │ └── v1/ │ │ │ └── service_server.ts │ │ ├── supply_chain/ │ │ │ └── v1/ │ │ │ └── service_server.ts │ │ ├── thermal/ │ │ │ └── v1/ │ │ │ └── service_server.ts │ │ ├── trade/ │ │ │ └── v1/ │ │ │ └── service_server.ts │ │ ├── unrest/ │ │ │ └── v1/ │ │ │ └── service_server.ts │ │ ├── webcam/ │ │ │ └── v1/ │ │ │ └── service_server.ts │ │ └── wildfire/ │ │ └── v1/ │ │ └── service_server.ts │ ├── live-channels-main.ts │ ├── live-channels-window.ts │ ├── locales/ │ │ ├── ar.d.ts │ │ ├── ar.json │ │ ├── bg.json │ │ ├── cs.json │ │ ├── de.json │ │ ├── el.json │ │ ├── en.json │ │ ├── es.d.ts │ │ ├── es.json │ │ ├── fr.json │ │ ├── it.d.ts │ │ ├── it.json │ │ ├── ja.json │ │ ├── ko.json │ │ ├── nl.d.ts │ │ ├── nl.json │ │ ├── pl.d.ts │ │ ├── pl.json │ │ ├── pt.d.ts │ │ ├── pt.json │ │ ├── ro.json │ │ ├── ru.d.ts │ │ ├── ru.json │ │ ├── sv.d.ts │ │ ├── sv.json │ │ ├── th.d.ts │ │ ├── th.json │ │ ├── tr.d.ts │ │ ├── tr.json │ │ ├── vi.d.ts │ │ ├── vi.json │ │ ├── zh.d.ts │ │ └── zh.json │ ├── main.ts │ ├── pwa.d.ts │ ├── services/ │ │ ├── activity-tracker.ts │ │ ├── ai-classify-queue.ts │ │ ├── ai-flow-settings.ts │ │ ├── analysis-core.ts │ │ ├── analysis-worker.ts │ │ ├── analytics.ts │ │ ├── aviation/ │ │ │ ├── index.ts │ │ │ └── watchlist.ts │ │ ├── bootstrap.ts │ │ ├── breaking-news-alerts.ts │ │ ├── cable-activity.ts │ │ ├── cable-health.ts │ │ ├── cached-risk-scores.ts │ │ ├── cached-theater-posture.ts │ │ ├── celebration.ts │ │ ├── climate/ │ │ │ └── index.ts │ │ ├── clustering.ts │ │ ├── conflict/ │ │ │ └── index.ts │ │ ├── conservation-data.ts │ │ ├── correlation-engine/ │ │ │ ├── adapters/ │ │ │ │ ├── disaster.ts │ │ │ │ ├── economic.ts │ │ │ │ ├── escalation.ts │ │ │ │ └── military.ts │ │ │ ├── engine.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── correlation.ts │ │ ├── country-geometry.ts │ │ ├── country-instability.ts │ │ ├── cross-module-integration.ts │ │ ├── cyber/ │ │ │ └── index.ts │ │ ├── daily-market-brief.ts │ │ ├── data-freshness.ts │ │ ├── desktop-readiness.ts │ │ ├── displacement/ │ │ │ └── index.ts │ │ ├── earthquakes.ts │ │ ├── economic/ │ │ │ └── index.ts │ │ ├── entity-extraction.ts │ │ ├── entity-index.ts │ │ ├── eonet.ts │ │ ├── feed-date.ts │ │ ├── focal-point-detector.ts │ │ ├── font-settings.ts │ │ ├── forecast.ts │ │ ├── gdelt-intel.ts │ │ ├── geo-activity.ts │ │ ├── geo-convergence.ts │ │ ├── geo-hub-index.ts │ │ ├── giving/ │ │ │ └── index.ts │ │ ├── globe-render-settings.ts │ │ ├── gps-interference.ts │ │ ├── happiness-data.ts │ │ ├── happy-share-renderer.ts │ │ ├── hotspot-escalation.ts │ │ ├── hub-activity-scoring.ts │ │ ├── humanity-counters.ts │ │ ├── i18n.ts │ │ ├── imagery.ts │ │ ├── index.ts │ │ ├── infrastructure/ │ │ │ └── index.ts │ │ ├── infrastructure-cascade.ts │ │ ├── insights-loader.ts │ │ ├── intelligence/ │ │ │ └── index.ts │ │ ├── investments-focus.ts │ │ ├── kindness-data.ts │ │ ├── live-news.ts │ │ ├── live-stream-settings.ts │ │ ├── maritime/ │ │ │ └── index.ts │ │ ├── market/ │ │ │ └── index.ts │ │ ├── market-watchlist.ts │ │ ├── mcp-store.ts │ │ ├── meta-tags.ts │ │ ├── military/ │ │ │ └── index.ts │ │ ├── military-bases.ts │ │ ├── military-flights.ts │ │ ├── military-surge.ts │ │ ├── military-vessels.ts │ │ ├── ml-capabilities.ts │ │ ├── ml-worker.ts │ │ ├── news/ │ │ │ └── index.ts │ │ ├── ollama-models.ts │ │ ├── oref-alerts.ts │ │ ├── oref-locations.ts │ │ ├── parallel-analysis.ts │ │ ├── persistent-cache.ts │ │ ├── pizzint.ts │ │ ├── population-exposure.ts │ │ ├── positive-classifier.ts │ │ ├── positive-events-geo.ts │ │ ├── prediction/ │ │ │ └── index.ts │ │ ├── preferences-content.ts │ │ ├── progress-data.ts │ │ ├── radiation.ts │ │ ├── related-assets.ts │ │ ├── renewable-energy-data.ts │ │ ├── renewable-installations.ts │ │ ├── research/ │ │ │ └── index.ts │ │ ├── rpc-client.ts │ │ ├── rss.ts │ │ ├── runtime-config.ts │ │ ├── runtime.ts │ │ ├── sanctions-pressure.ts │ │ ├── satellites.ts │ │ ├── security-advisories.ts │ │ ├── sentiment-gate.ts │ │ ├── settings-constants.ts │ │ ├── settings-manager.ts │ │ ├── signal-aggregator.ts │ │ ├── stock-analysis-history.ts │ │ ├── stock-analysis.ts │ │ ├── stock-backtest.ts │ │ ├── storage.ts │ │ ├── story-data.ts │ │ ├── story-renderer.ts │ │ ├── story-share.ts │ │ ├── summarization.ts │ │ ├── supply-chain/ │ │ │ └── index.ts │ │ ├── tauri-bridge.ts │ │ ├── tech-activity.ts │ │ ├── tech-hub-index.ts │ │ ├── telegram-intel.ts │ │ ├── temporal-baseline.ts │ │ ├── thermal-escalation.ts │ │ ├── threat-classifier.ts │ │ ├── throttled-target-requests.ts │ │ ├── trade/ │ │ │ └── index.ts │ │ ├── trending-keywords.ts │ │ ├── tv-mode.ts │ │ ├── unrest/ │ │ │ └── index.ts │ │ ├── usa-spending.ts │ │ ├── usni-fleet.ts │ │ ├── velocity.ts │ │ ├── weather.ts │ │ ├── webcams/ │ │ │ ├── index.ts │ │ │ └── pinned-store.ts │ │ ├── widget-store.ts │ │ ├── wildfires/ │ │ │ └── index.ts │ │ └── wingbits.ts │ ├── settings-main.ts │ ├── settings-window.ts │ ├── shims/ │ │ ├── child-process-proxy.ts │ │ └── child-process.ts │ ├── styles/ │ │ ├── base-layer.css │ │ ├── country-deep-dive.css │ │ ├── happy-theme.css │ │ ├── main.css │ │ ├── map-context-menu.css │ │ ├── panels.css │ │ ├── rtl-overrides.css │ │ └── settings-window.css │ ├── types/ │ │ └── index.ts │ ├── utils/ │ │ ├── analysis-constants.ts │ │ ├── circuit-breaker.ts │ │ ├── country-flag.ts │ │ ├── cross-domain-storage.ts │ │ ├── distance.ts │ │ ├── dom-utils.ts │ │ ├── export.ts │ │ ├── hash.ts │ │ ├── imagery-preview.ts │ │ ├── index.ts │ │ ├── keyword-match.ts │ │ ├── layer-warning.ts │ │ ├── map-locale.ts │ │ ├── news-context.ts │ │ ├── proxy.ts │ │ ├── reverse-geocode.ts │ │ ├── sanitize.ts │ │ ├── settings-persistence.ts │ │ ├── sparkline.ts │ │ ├── storage-quota.ts │ │ ├── summary-cache-key.ts │ │ ├── theme-colors.ts │ │ ├── theme-manager.ts │ │ ├── transit-chart.ts │ │ ├── urlState.ts │ │ ├── user-location.ts │ │ ├── utm.ts │ │ └── widget-sanitizer.ts │ ├── vite-env.d.ts │ └── workers/ │ ├── analysis.worker.ts │ ├── ml.worker.ts │ └── vector-db.ts ├── src-tauri/ │ ├── .cargo/ │ │ ├── config.local.toml.example │ │ └── config.toml │ ├── .gitignore │ ├── Cargo.toml │ ├── build.rs │ ├── capabilities/ │ │ ├── default.json │ │ └── youtube-login.json │ ├── icons/ │ │ └── icon.icns │ ├── nsis/ │ │ └── installer-hooks.nsh │ ├── sidecar/ │ │ ├── local-api-server.mjs │ │ ├── local-api-server.test.mjs │ │ ├── node/ │ │ │ └── .gitkeep │ │ └── package.json │ ├── src/ │ │ └── main.rs │ ├── tauri.conf.json │ ├── tauri.finance.conf.json │ └── tauri.tech.conf.json ├── tests/ │ ├── bootstrap.test.mjs │ ├── chokepoint-id-mapping.test.mjs │ ├── chokepoint-transit-counter.test.mjs │ ├── cii-scoring.test.mts │ ├── circuit-breaker-persistent-stale-ceiling.test.mts │ ├── clustering.test.mjs │ ├── contact-handler.test.mjs │ ├── corridorrisk-upstream.test.mjs │ ├── countries-geojson.test.mjs │ ├── country-geometry-overrides.test.mts │ ├── crypto-config.test.mjs │ ├── customs-revenue.test.mjs │ ├── daily-market-brief.test.mts │ ├── deckgl-layer-state-aliasing.test.mjs │ ├── deduction-prompt.test.mjs │ ├── deploy-config.test.mjs │ ├── digest-no-reclassify.test.mjs │ ├── download-handler.test.mjs │ ├── edge-functions.test.mjs │ ├── escalation-country-merge.test.mts │ ├── flush-stale-refreshes.test.mjs │ ├── forecast-detectors.test.mjs │ ├── forecast-history.test.mjs │ ├── forecast-trace-export.test.mjs │ ├── freight-indices.test.mjs │ ├── geo-keyword-matching.test.mts │ ├── globe-2d-3d-parity.test.mjs │ ├── globe-tooltip-enrichment.test.mjs │ ├── gulf-fdi-data.test.mjs │ ├── handlers.test.mts │ ├── hapi-gdelt-circuit-breakers.test.mjs │ ├── helpers/ │ │ ├── llm-health-stub.ts │ │ └── runtime-config-panel-harness.mjs │ ├── insights-loader.test.mjs │ ├── lint-md-script-scope.test.mjs │ ├── live-news-hls.test.mjs │ ├── llm-sanitize.test.mjs │ ├── map-fullscreen-resize.test.mjs │ ├── map-harness.html │ ├── map-locale.test.mts │ ├── market-quote-cache-keying.test.mjs │ ├── market-service-symbol-casing.test.mjs │ ├── mdx-lint.test.mjs │ ├── military-classification.test.mjs │ ├── military-flight-classification.test.mjs │ ├── military-surges.test.mjs │ ├── mobile-map-harness.html │ ├── mobile-map-integration-harness.html │ ├── oref-breaking.test.mjs │ ├── oref-locations.test.mjs │ ├── oref-proxy.test.mjs │ ├── panel-config-guardrails.test.mjs │ ├── portwatch-upstream.test.mjs │ ├── prediction-scoring.test.mjs │ ├── premium-stock-gateway.test.mts │ ├── redis-caching.test.mjs │ ├── relay-helper.test.mjs │ ├── route-cache-tier.test.mjs │ ├── runtime-config-panel-visibility.test.mjs │ ├── runtime-env-guards.test.mjs │ ├── runtime-harness.html │ ├── sanctions-pressure.test.mjs │ ├── sanctions-seed-unit.test.mjs │ ├── seed-utils.test.mjs │ ├── seed-warm-ping-origin.test.mjs │ ├── server-handlers.test.mjs │ ├── shared-llm.test.mts │ ├── smart-poll-loop.test.mjs │ ├── stock-analysis-history.test.mts │ ├── stock-analysis.test.mts │ ├── stock-backtest.test.mts │ ├── stock-news-search.test.mts │ ├── summarize-reasoning.test.mjs │ ├── supply-chain-handlers.test.mjs │ ├── supply-chain-panel-transit-chart.test.mjs │ ├── supply-chain-v2.test.mjs │ ├── tech-readiness-circuit-breakers.test.mjs │ ├── thermal-escalation-handler-guardrail.test.mjs │ ├── thermal-escalation-model.test.mjs │ ├── trade-policy-tariffs.test.mjs │ ├── transit-summaries.test.mjs │ ├── ttl-acled-ais-guards.test.mjs │ ├── ucdp-seed-resilience.test.mjs │ ├── urlState.test.mts │ ├── variant-layer-guardrail.test.mjs │ └── widget-builder.test.mjs ├── tsconfig.api.json ├── tsconfig.json ├── vercel.json └── vite.config.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ node_modules dist .git .github .windsurf .agent .agents .claude .factory .planning e2e src-tauri/target src-tauri/sidecar/node *.log *.md !README.md docs/internal docs/Docs_To_Review tests ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: Bug Report description: Report a bug in World Monitor labels: ["bug"] body: - type: markdown attributes: value: | Thanks for taking the time to report a bug! Please fill out the sections below so we can reproduce and fix it. - type: dropdown id: variant attributes: label: Variant description: Which variant are you using? options: - worldmonitor.app (Full / Geopolitical) - tech.worldmonitor.app (Tech / Startup) - finance.worldmonitor.app (Finance) - Desktop app (Windows) - Desktop app (macOS) - Desktop app (Linux) validations: required: true - type: dropdown id: area attributes: label: Affected area description: Which part of the app is affected? options: - Map / Globe - News panels / RSS feeds - AI Insights / World Brief - Market Radar / Crypto - Service Status - Trending Keywords - Country Brief pages - Live video streams - Desktop app (Tauri) - Settings / API keys - Settings / LLMs (Ollama, Groq, OpenRouter) - Live webcams - Other validations: required: true - type: textarea id: description attributes: label: Bug description description: A clear description of what the bug is. placeholder: Describe the bug... validations: required: true - type: textarea id: steps attributes: label: Steps to reproduce description: Steps to reproduce the behavior. placeholder: | 1. Go to '...' 2. Click on '...' 3. Scroll down to '...' 4. See error validations: required: true - type: textarea id: expected attributes: label: Expected behavior description: What you expected to happen. validations: required: true - type: textarea id: screenshots attributes: label: Screenshots / Console errors description: If applicable, add screenshots or paste browser console errors. - type: input id: browser attributes: label: Browser & OS description: e.g. Chrome 120 on Windows 11, Safari 17 on macOS Sonoma placeholder: Chrome 120 on Windows 11 ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Documentation url: https://github.com/koala73/worldmonitor/blob/main/docs/DOCUMENTATION.md about: Read the full documentation before opening an issue - name: Discussions url: https://github.com/koala73/worldmonitor/discussions about: Ask questions and share ideas in Discussions ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yml ================================================ name: Feature Request description: Suggest a new feature or improvement labels: ["enhancement"] body: - type: markdown attributes: value: | Have an idea for World Monitor? We'd love to hear it! - type: dropdown id: area attributes: label: Feature area description: Which area does this feature relate to? options: - Map / Globe / Data layers - News panels / RSS feeds - AI / Intelligence analysis - Market data / Crypto - Desktop app - UI / UX - API / Backend - Other validations: required: true - type: textarea id: description attributes: label: Description description: A clear description of the feature you'd like. placeholder: I'd like to see... validations: required: true - type: textarea id: problem attributes: label: Problem it solves description: What problem does this feature address? What's the use case? placeholder: This would help with... validations: required: true - type: textarea id: alternatives attributes: label: Alternatives considered description: Have you considered any alternative solutions or workarounds? - type: textarea id: context attributes: label: Additional context description: Any mockups, screenshots, links, or references that help illustrate the idea. ================================================ FILE: .github/ISSUE_TEMPLATE/new_data_source.yml ================================================ name: New Data Source description: Suggest a new RSS feed, API, or map layer labels: ["data-source"] body: - type: markdown attributes: value: | World Monitor aggregates 100+ feeds and data layers. Suggest a new one! - type: dropdown id: type attributes: label: Source type description: What kind of data source is this? options: - RSS / News feed - API integration - Map layer (geospatial data) - Live video stream - Status page - Other validations: required: true - type: dropdown id: variant attributes: label: Target variant description: Which variant should this appear in? options: - Full (Geopolitical) - Tech (Startup) - Finance - All variants validations: required: true - type: input id: source-name attributes: label: Source name description: Name of the source or organization. placeholder: e.g. RAND Corporation, CoinDesk, USGS validations: required: true - type: input id: url attributes: label: Feed / API URL description: Direct URL to the RSS feed, API endpoint, or data source. placeholder: https://example.com/rss validations: required: true - type: textarea id: description attributes: label: Why add this source? description: What value does this source bring? What does it cover that existing sources don't? placeholder: This source provides coverage of... validations: required: true - type: textarea id: notes attributes: label: Additional notes description: Any details about rate limits, authentication requirements, data format, or category placement. ================================================ FILE: .github/pull_request_template.md ================================================ ## Summary ## Type of change - [ ] Bug fix - [ ] New feature - [ ] New data source / feed - [ ] New map layer - [ ] Refactor / code cleanup - [ ] Documentation - [ ] CI / Build / Infrastructure ## Affected areas - [ ] Map / Globe - [ ] News panels / RSS feeds - [ ] AI Insights / World Brief - [ ] Market Radar / Crypto - [ ] Desktop app (Tauri) - [ ] API endpoints (`/api/*`) - [ ] Config / Settings - [ ] Other: ## Checklist - [ ] Tested on [worldmonitor.app](https://worldmonitor.app) variant - [ ] Tested on [tech.worldmonitor.app](https://tech.worldmonitor.app) variant (if applicable) - [ ] New RSS feed domains added to `api/rss-proxy.js` allowlist (if adding feeds) - [ ] No API keys or secrets committed - [ ] TypeScript compiles without errors (`npm run typecheck`) ## Screenshots ================================================ FILE: .github/workflows/build-desktop.yml ================================================ name: 'Build Desktop App' on: workflow_dispatch: inputs: variant: description: 'App variant' required: true default: 'full' type: choice options: - full - tech draft: description: 'Create as draft release' required: false default: true type: boolean push: tags: - 'v*' concurrency: group: desktop-build-${{ github.ref }} cancel-in-progress: true env: CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse jobs: build-tauri: permissions: contents: write strategy: fail-fast: false matrix: include: - platform: 'macos-14' args: '--target aarch64-apple-darwin' node_target: 'aarch64-apple-darwin' label: 'macOS-ARM64' timeout: 180 - platform: 'macos-latest' args: '--target x86_64-apple-darwin' node_target: 'x86_64-apple-darwin' label: 'macOS-x64' timeout: 180 - platform: 'windows-latest' args: '' node_target: 'x86_64-pc-windows-msvc' label: 'Windows-x64' timeout: 120 - platform: 'ubuntu-24.04' args: '' node_target: 'x86_64-unknown-linux-gnu' label: 'Linux-x64' timeout: 120 - platform: 'ubuntu-24.04-arm' args: '--target aarch64-unknown-linux-gnu' node_target: 'aarch64-unknown-linux-gnu' label: 'Linux-ARM64' timeout: 120 runs-on: ${{ matrix.platform }} name: Build (${{ matrix.label }}) timeout-minutes: ${{ matrix.timeout }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Start job timer shell: bash run: echo "JOB_START_EPOCH=$(date +%s)" >> "$GITHUB_ENV" - name: Setup Node.js uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 with: node-version: '22' cache: 'npm' - name: Install Rust stable uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 with: toolchain: stable targets: ${{ contains(matrix.platform, 'macos') && 'aarch64-apple-darwin,x86_64-apple-darwin' || (matrix.label == 'Linux-ARM64' && 'aarch64-unknown-linux-gnu' || '') }} - name: Rust cache uses: swatinem/rust-cache@ad397744b0d591a723ab90405b7247fac0e6b8db with: workspaces: './src-tauri -> target' cache-on-failure: true - name: Install Linux system dependencies if: contains(matrix.platform, 'ubuntu') run: | sudo apt-get update sudo apt-get install -y \ libwebkit2gtk-4.1-dev \ libappindicator3-dev \ librsvg2-dev \ patchelf \ gstreamer1.0-plugins-base \ gstreamer1.0-plugins-good \ gstreamer1.0-plugins-bad \ gstreamer1.0-plugins-ugly \ gstreamer1.0-libav \ gstreamer1.0-gl - name: Install frontend dependencies run: npm ci - name: Check version consistency run: npm run version:check - name: Bundle Node.js runtime shell: bash env: NODE_VERSION: '22.14.0' NODE_TARGET: ${{ matrix.node_target }} run: bash scripts/download-node.sh --target "$NODE_TARGET" - name: Verify bundled Node.js payload shell: bash run: | if [ "${{ matrix.node_target }}" = "x86_64-pc-windows-msvc" ]; then test -f src-tauri/sidecar/node/node.exe ls -lh src-tauri/sidecar/node/node.exe else test -f src-tauri/sidecar/node/node test -x src-tauri/sidecar/node/node ls -lh src-tauri/sidecar/node/node fi # ── Detect whether Apple signing secrets are configured ── - name: Check Apple signing secrets if: contains(matrix.platform, 'macos') id: apple-signing shell: bash run: | if [ -n "${{ secrets.APPLE_CERTIFICATE }}" ] && [ -n "${{ secrets.APPLE_CERTIFICATE_PASSWORD }}" ] && [ -n "${{ secrets.KEYCHAIN_PASSWORD }}" ]; then echo "available=true" >> $GITHUB_OUTPUT echo "Apple signing secrets detected" else echo "available=false" >> $GITHUB_OUTPUT echo "No Apple signing secrets — building unsigned" fi # ── macOS Code Signing (only when secrets are valid) ── - name: Import Apple Developer Certificate if: contains(matrix.platform, 'macos') && steps.apple-signing.outputs.available == 'true' env: APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} run: | printf '%s' "$APPLE_CERTIFICATE" | base64 --decode > certificate.p12 CERT_SIZE=$(wc -c < certificate.p12 | tr -d ' ') if [ "$CERT_SIZE" -lt 100 ]; then echo "::warning::Certificate file too small ($CERT_SIZE bytes) — likely invalid. Skipping signing." echo "SKIP_SIGNING=true" >> $GITHUB_ENV exit 0 fi security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain security default-keychain -s build.keychain security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain security set-keychain-settings -t 3600 -u build.keychain security import certificate.p12 -k build.keychain \ -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign || { echo "::warning::Certificate import failed — building unsigned" echo "SKIP_SIGNING=true" >> $GITHUB_ENV exit 0 } security set-key-partition-list -S apple-tool:,apple:,codesign: \ -s -k "$KEYCHAIN_PASSWORD" build.keychain CERT_INFO=$(security find-identity -v -p codesigning build.keychain \ | grep "Developer ID Application" || true) if [ -n "$CERT_INFO" ]; then CERT_ID=$(echo "$CERT_INFO" | head -1 | awk -F'"' '{print $2}') echo "APPLE_SIGNING_IDENTITY=$CERT_ID" >> $GITHUB_ENV echo "Certificate imported: $CERT_ID" else echo "::warning::No Developer ID certificate found in keychain — building unsigned" echo "SKIP_SIGNING=true" >> $GITHUB_ENV fi # ── Determine variant ── - name: Set build variant shell: bash run: | if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then echo "BUILD_VARIANT=${{ github.event.inputs.variant }}" >> $GITHUB_ENV else echo "BUILD_VARIANT=full" >> $GITHUB_ENV fi # ── Build with tauri-action ── # Signed builds: only when Apple signing secrets are valid and imported # Unsigned builds: fallback when no signing (Windows always uses this path) # ── Build: Full variant (signed) ── - name: Build Tauri app (full, signed) if: env.BUILD_VARIANT == 'full' && steps.apple-signing.outputs.available == 'true' && env.SKIP_SIGNING != 'true' uses: tauri-apps/tauri-action@79c624843491f12ae9d63592534ed49df3bc4adb env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} VITE_VARIANT: full VITE_DESKTOP_RUNTIME: '1' VITE_WS_API_URL: https://worldmonitor.app CONVEX_URL: ${{ secrets.CONVEX_URL }} APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} APPLE_SIGNING_IDENTITY: ${{ env.APPLE_SIGNING_IDENTITY }} APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} with: tagName: v__VERSION__ releaseName: 'World Monitor v__VERSION__' releaseBody: 'See changelog below.' releaseDraft: ${{ github.event_name == 'workflow_dispatch' && fromJSON(github.event.inputs.draft) }} prerelease: false args: ${{ matrix.args }} retryAttempts: 1 # ── Build: Full variant (unsigned — no Apple certs) ── - name: Build Tauri app (full, unsigned) if: env.BUILD_VARIANT == 'full' && (steps.apple-signing.outputs.available != 'true' || env.SKIP_SIGNING == 'true') uses: tauri-apps/tauri-action@79c624843491f12ae9d63592534ed49df3bc4adb env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} VITE_VARIANT: full VITE_DESKTOP_RUNTIME: '1' VITE_WS_API_URL: https://worldmonitor.app CONVEX_URL: ${{ secrets.CONVEX_URL }} with: tagName: v__VERSION__ releaseName: 'World Monitor v__VERSION__' releaseBody: 'See changelog below.' releaseDraft: ${{ github.event_name == 'workflow_dispatch' && fromJSON(github.event.inputs.draft) }} prerelease: false args: ${{ matrix.args }} retryAttempts: 1 # ── Build: Tech variant (signed) ── - name: Build Tauri app (tech, signed) if: env.BUILD_VARIANT == 'tech' && steps.apple-signing.outputs.available == 'true' && env.SKIP_SIGNING != 'true' uses: tauri-apps/tauri-action@79c624843491f12ae9d63592534ed49df3bc4adb env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} VITE_VARIANT: tech VITE_DESKTOP_RUNTIME: '1' VITE_WS_API_URL: https://worldmonitor.app CONVEX_URL: ${{ secrets.CONVEX_URL }} APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} APPLE_SIGNING_IDENTITY: ${{ env.APPLE_SIGNING_IDENTITY }} APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} with: tagName: v__VERSION__-tech releaseName: 'Tech Monitor v__VERSION__' releaseBody: 'See changelog below.' releaseDraft: ${{ github.event_name == 'workflow_dispatch' && fromJSON(github.event.inputs.draft) }} prerelease: false tauriScript: npx tauri args: --config src-tauri/tauri.tech.conf.json ${{ matrix.args }} retryAttempts: 1 # ── Build: Tech variant (unsigned — no Apple certs) ── - name: Build Tauri app (tech, unsigned) if: env.BUILD_VARIANT == 'tech' && (steps.apple-signing.outputs.available != 'true' || env.SKIP_SIGNING == 'true') uses: tauri-apps/tauri-action@79c624843491f12ae9d63592534ed49df3bc4adb env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} VITE_VARIANT: tech VITE_DESKTOP_RUNTIME: '1' VITE_WS_API_URL: https://worldmonitor.app CONVEX_URL: ${{ secrets.CONVEX_URL }} with: tagName: v__VERSION__-tech releaseName: 'Tech Monitor v__VERSION__' releaseBody: 'See changelog below.' releaseDraft: ${{ github.event_name == 'workflow_dispatch' && fromJSON(github.event.inputs.draft) }} prerelease: false tauriScript: npx tauri args: --config src-tauri/tauri.tech.conf.json ${{ matrix.args }} retryAttempts: 1 - name: Verify signed macOS bundle + embedded runtime if: contains(matrix.platform, 'macos') && steps.apple-signing.outputs.available == 'true' && env.SKIP_SIGNING != 'true' shell: bash run: | APP_PATH=$(find src-tauri/target -type d -path '*/bundle/macos/*.app' | head -1) if [ -z "$APP_PATH" ]; then echo "::error::No macOS .app bundle found after build." exit 1 fi codesign --verify --deep --strict --verbose=2 "$APP_PATH" NODE_PATH=$(find "$APP_PATH/Contents/Resources" -type f -path '*/sidecar/node/node' | head -1) if [ -z "$NODE_PATH" ]; then echo "::error::Bundled Node runtime missing from app resources." exit 1 fi echo "Verified signed app bundle and embedded Node runtime: $NODE_PATH" - name: Strip GPU libraries from AppImage if: contains(matrix.platform, 'ubuntu') shell: bash env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} APPIMAGETOOL_VERSION: '1.9.1' APPIMAGETOOL_SHA256_X86_64: 'ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3cb384eb0' APPIMAGETOOL_SHA256_AARCH64: 'f0837e7448a0c1e4e650a93bb3e85802546e60654ef287576f46c71c126a9158' run: | # --- Deterministic artifact selection --- mapfile -t IMAGES < <(find src-tauri/target -path '*/bundle/appimage/*.AppImage') if [ ${#IMAGES[@]} -eq 0 ]; then echo "No AppImage found, skipping GPU lib strip" exit 0 fi if [ ${#IMAGES[@]} -gt 1 ]; then echo "::error::Found ${#IMAGES[@]} AppImage files — expected exactly 1" printf ' %s\n' "${IMAGES[@]}" exit 1 fi APPIMAGE="${IMAGES[0]}" echo "Stripping bundled GPU/Wayland libraries from: $APPIMAGE" chmod +x "$APPIMAGE" # --- Clean extraction --- rm -rf squashfs-root "$APPIMAGE" --appimage-extract # --- Strip GPU/Wayland libs (14 named patterns + DRI drivers) --- GPU_LIBS=( 'libEGL.so*' 'libEGL_mesa.so*' 'libGLX.so*' 'libGLX_mesa.so*' 'libGLdispatch.so*' 'libGLESv2.so*' 'libGL.so*' 'libOpenGL.so*' 'libglapi.so*' 'libgbm.so*' 'libwayland-client.so*' 'libwayland-server.so*' 'libwayland-cursor.so*' 'libwayland-egl.so*' ) REMOVED=0 for pattern in "${GPU_LIBS[@]}"; do while IFS= read -r -d '' f; do rm -f "$f" echo " Removed: ${f#squashfs-root/}" ((REMOVED++)) || true done < <(find squashfs-root -name "$pattern" -print0) done # Mesa DRI drivers while IFS= read -r -d '' f; do rm -f "$f" echo " Removed DRI: ${f#squashfs-root/}" ((REMOVED++)) || true done < <(find squashfs-root -path '*/dri/*_dri.so' -print0) echo "Stripped $REMOVED GPU/Wayland library files" if [ "$REMOVED" -eq 0 ]; then echo "::error::No GPU libraries found to strip — build may have changed" exit 1 fi # --- Download and verify appimagetool (pinned to 1.9.1) --- TOOL_ARCH=${{ matrix.label == 'Linux-ARM64' && 'aarch64' || 'x86_64' }} TOOL_URL="https://github.com/AppImage/appimagetool/releases/download/${APPIMAGETOOL_VERSION}/appimagetool-${TOOL_ARCH}.AppImage" wget -q "$TOOL_URL" -O /tmp/appimagetool EXPECTED_SHA=$([ "$TOOL_ARCH" = "x86_64" ] && echo "$APPIMAGETOOL_SHA256_X86_64" || echo "$APPIMAGETOOL_SHA256_AARCH64") ACTUAL_SHA=$(sha256sum /tmp/appimagetool | awk '{print $1}') if [ "$ACTUAL_SHA" != "$EXPECTED_SHA" ]; then echo "::error::appimagetool SHA256 mismatch! Expected: $EXPECTED_SHA Got: $ACTUAL_SHA" exit 1 fi echo "appimagetool SHA256 verified: $ACTUAL_SHA" chmod +x /tmp/appimagetool # --- Repackage to temp path, then atomic mv --- APPIMAGE_TMP="${APPIMAGE}.stripped.tmp" ARCH=$TOOL_ARCH /tmp/appimagetool --appimage-extract-and-run squashfs-root "$APPIMAGE_TMP" mv -f "$APPIMAGE_TMP" "$APPIMAGE" # --- Post-repack verification: ALL banned patterns --- rm -rf squashfs-root "$APPIMAGE" --appimage-extract BANNED="" for pattern in "${GPU_LIBS[@]}"; do found=$(find squashfs-root -name "$pattern" -print 2>/dev/null) if [ -n "$found" ]; then BANNED+="$found"$'\n'; fi done found=$(find squashfs-root -path '*/dri/*_dri.so' -print 2>/dev/null) if [ -n "$found" ]; then BANNED+="$found"$'\n'; fi rm -rf squashfs-root if [ -n "$BANNED" ]; then echo "::error::Banned GPU libs still present after repack:" echo "$BANNED" exit 1 fi echo "Post-repack verification passed — no banned GPU libs" # --- Re-upload stripped AppImage to GitHub Release --- VERSION=$(node -p "require('./package.json').version") if [ "$BUILD_VARIANT" = "tech" ]; then TAG_NAME="v${VERSION}-tech" else TAG_NAME="v${VERSION}" fi echo "Computed release tag: $TAG_NAME" if gh release view "$TAG_NAME" &>/dev/null; then echo "Re-uploading stripped AppImage to release $TAG_NAME" gh release upload "$TAG_NAME" "$APPIMAGE" --clobber echo "Replaced release asset: $(basename "$APPIMAGE")" else echo "::warning::Release $TAG_NAME not found — skipping re-upload" fi rm -f /tmp/appimagetool - name: Smoke-test AppImage (Linux) if: contains(matrix.platform, 'ubuntu') shell: bash run: | sudo apt-get install -y xvfb imagemagick APPIMAGE=$(find src-tauri/target -path '*/bundle/appimage/*.AppImage' | head -1) if [ -z "$APPIMAGE" ]; then echo "::error::No AppImage found after build" exit 1 fi chmod +x "$APPIMAGE" # Start Xvfb with known display number Xvfb :99 -screen 0 1440x900x24 & export DISPLAY=:99 sleep 2 # Launch AppImage under virtual framebuffer "$APPIMAGE" --no-sandbox & APP_PID=$! # Wait for app to render sleep 15 # Screenshot the virtual display import -window root screenshot.png || true # Verify app is still running (didn't crash) if kill -0 $APP_PID 2>/dev/null; then echo "✅ AppImage launched successfully" kill $APP_PID || true else echo "❌ AppImage crashed during startup" exit 1 fi - name: Upload smoke test screenshot if: contains(matrix.platform, 'ubuntu') uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 with: name: linux-smoke-test-screenshot-${{ matrix.label }} path: screenshot.png if-no-files-found: warn - name: Cleanup Apple signing materials if: always() && contains(matrix.platform, 'macos') shell: bash run: | rm -f certificate.p12 security delete-keychain build.keychain || true - name: Report build duration if: always() shell: bash run: | if [ -z "${JOB_START_EPOCH:-}" ]; then echo "::warning::JOB_START_EPOCH missing; duration unavailable." exit 0 fi END_EPOCH=$(date +%s) ELAPSED=$((END_EPOCH - JOB_START_EPOCH)) MINUTES=$((ELAPSED / 60)) SECONDS=$((ELAPSED % 60)) echo "Build duration for ${{ matrix.label }}: ${MINUTES}m ${SECONDS}s" # ── Update release notes with changelog after all builds complete ── update-release-notes: needs: build-tauri if: always() && contains(needs.build-tauri.result, 'success') runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: fetch-depth: 0 - name: Generate and update release notes env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} shell: bash run: | VERSION=$(jq -r .version src-tauri/tauri.conf.json) TAG="v${VERSION}" PREV_TAG=$(git describe --tags --abbrev=0 "${TAG}^" 2>/dev/null || echo "") if [ -z "$PREV_TAG" ]; then COMMITS="Initial release" else COMMITS=$(git log "${PREV_TAG}..${TAG}" --oneline --no-merges | sed 's/^[a-f0-9]*//' | sed 's/^ /- /') fi BODY=$(cat </dev/null || true) if [ -z "$RESPONSE" ]; then echo "verdict=unavailable" >> "$GITHUB_OUTPUT" else VERDICT=$(echo "$RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); v=d.get('verdict',''); print(v if v in ('safe','caution','suspicious','dangerous') else 'unavailable')" 2>/dev/null || echo "unavailable") echo "verdict=$VERDICT" >> "$GITHUB_OUTPUT" fi - name: Apply trust label if: | steps.brin.outputs.verdict == 'safe' || steps.brin.outputs.verdict == 'caution' || steps.brin.outputs.verdict == 'suspicious' || steps.brin.outputs.verdict == 'dangerous' uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 env: BRIN_VERDICT: ${{ steps.brin.outputs.verdict }} with: script: | await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.pull_request.number, labels: [`trust:${process.env.BRIN_VERDICT}`], }); ================================================ FILE: .github/workflows/docker-publish.yml ================================================ name: Publish Docker image on: release: types: [published] workflow_dispatch: jobs: docker: runs-on: ubuntu-latest permissions: contents: read packages: write steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4 - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6 id: meta with: images: ghcr.io/koala73/worldmonitor tags: | type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=raw,value=latest,enable=${{ github.event_name == 'release' }} type=sha,prefix=sha- - uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7 with: context: . file: docker/Dockerfile push: true platforms: linux/amd64,linux/arm64 tags: ${{ steps.meta.outputs.tags }} cache-from: type=gha cache-to: type=gha,mode=max ================================================ FILE: .github/workflows/lint-code.yml ================================================ name: Lint Code on: pull_request: push: branches: [main] jobs: biome: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '22' cache: 'npm' - run: npm ci - run: npm run lint:unicode - run: npm run lint - run: npm run lint:boundaries ================================================ FILE: .github/workflows/lint.yml ================================================ name: Lint on: pull_request: paths: - '**/*.md' - '.markdownlint-cli2.jsonc' jobs: markdown: # No secrets needed — run for all PRs including forks runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 with: node-version: '22' cache: 'npm' - run: npm ci - run: npm run lint:md ================================================ FILE: .github/workflows/proto-check.yml ================================================ name: Proto Generation Check on: pull_request: paths: - 'proto/**' - 'src/generated/**' - 'docs/api/**' - 'Makefile' - '.github/workflows/proto-check.yml' jobs: proto-freshness: if: github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 with: go-version: '1.23' cache: false - name: Cache Go binaries (buf, protoc plugins) uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5 with: path: ~/go/bin key: go-bin-${{ runner.os }}-${{ hashFiles('Makefile') }} - name: Install buf and protoc plugins run: make install-buf install-plugins env: GOPROXY: direct GOPRIVATE: github.com/SebastienMelki - name: Run proto generation run: make generate - name: Verify generated code is up to date run: | if ! git diff --exit-code src/generated/ docs/api/; then echo "" echo "============================================================" echo "ERROR: Proto-generated code is out of date." echo "Run 'make generate' locally and commit the updated files." echo "============================================================" exit 1 fi UNTRACKED=$(git ls-files --others --exclude-standard src/generated/ docs/api/) if [ -n "$UNTRACKED" ]; then echo "" echo "============================================================" echo "ERROR: Untracked generated files found:" echo "$UNTRACKED" echo "Run 'make generate' locally and commit the new files." echo "============================================================" exit 1 fi echo "Proto-generated code is up to date." ================================================ FILE: .github/workflows/test-linux-app.yml ================================================ name: 'Test Linux App' on: workflow_dispatch: concurrency: group: test-linux-app-${{ github.ref }} cancel-in-progress: true env: CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse jobs: test-linux-app: runs-on: ubuntu-24.04 timeout-minutes: 120 permissions: contents: read steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Setup Node.js uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 with: node-version: '22' cache: 'npm' - name: Install Rust stable uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 with: toolchain: stable - name: Rust cache uses: swatinem/rust-cache@ad397744b0d591a723ab90405b7247fac0e6b8db with: workspaces: './src-tauri -> target' cache-on-failure: true - name: Install Linux system dependencies run: | sudo apt-get update sudo apt-get install -y \ libwebkit2gtk-4.1-dev \ libappindicator3-dev \ librsvg2-dev \ patchelf \ gstreamer1.0-plugins-base \ gstreamer1.0-plugins-good \ xwayland-run \ xvfb \ imagemagick \ xdotool - name: Install frontend dependencies run: npm ci - name: Bundle Node.js runtime shell: bash env: NODE_VERSION: '22.14.0' NODE_TARGET: 'x86_64-unknown-linux-gnu' run: bash scripts/download-node.sh --target "$NODE_TARGET" - name: Build Tauri app uses: tauri-apps/tauri-action@79c624843491f12ae9d63592534ed49df3bc4adb env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} VITE_VARIANT: full VITE_DESKTOP_RUNTIME: '1' CONVEX_URL: ${{ secrets.CONVEX_URL }} with: args: '' retryAttempts: 1 - name: Smoke-test AppImage shell: bash run: | APPIMAGE=$(find src-tauri/target/release/bundle/appimage -name '*.AppImage' | head -1) if [ -z "$APPIMAGE" ]; then echo "::error::No AppImage found after build" exit 1 fi chmod +x "$APPIMAGE" APPIMAGE_ABS=$(realpath "$APPIMAGE") # Write the inner test script (runs inside the display server) cat > /tmp/smoke-test.sh <<'SCRIPT' #!/bin/bash set -x echo "DISPLAY=$DISPLAY WAYLAND_DISPLAY=${WAYLAND_DISPLAY:-unset}" GDK_BACKEND=x11 "$APPIMAGE_ABS" --no-sandbox 2>&1 | tee /tmp/app.log & APP_PID=$! sleep 20 # Screenshot via X11 import -window root /tmp/screenshot.png 2>/dev/null || true # Verify app is still running if kill -0 $APP_PID 2>/dev/null; then echo "APP_STATUS=running" else echo "APP_STATUS=crashed" echo "--- App log ---" tail -50 /tmp/app.log || true fi # Window info xdotool search --name "" getwindowname 2>/dev/null | head -5 || true kill $APP_PID 2>/dev/null || true SCRIPT chmod +x /tmp/smoke-test.sh export APPIMAGE_ABS RESULT=0 # --- Try 1: xwfb-run (Xwayland on headless Wayland compositor) --- if command -v xwfb-run &>/dev/null; then echo "=== Using xwfb-run (Xwayland + headless compositor) ===" timeout 90 xwfb-run -- bash /tmp/smoke-test.sh 2>&1 | tee /tmp/display-server.log || RESULT=$? else echo "xwfb-run not found, skipping" RESULT=1 fi # --- Fallback: plain Xvfb --- if [ $RESULT -ne 0 ] || [ ! -f /tmp/screenshot.png ]; then echo "=== Falling back to Xvfb ===" Xvfb :99 -screen 0 1440x900x24 & XVFB_PID=$! export DISPLAY=:99 sleep 2 bash /tmp/smoke-test.sh 2>&1 | tee /tmp/display-server.log kill $XVFB_PID 2>/dev/null || true fi # --- Copy screenshot to workspace --- cp /tmp/screenshot.png screenshot.png 2>/dev/null || true # --- Check results --- if grep -q "APP_STATUS=crashed" /tmp/display-server.log 2>/dev/null; then echo "❌ AppImage crashed during startup" exit 1 fi if grep -q "APP_STATUS=running" /tmp/display-server.log 2>/dev/null; then echo "✅ AppImage launched successfully" else echo "⚠️ Could not determine app status" fi # --- Check screenshot has non-black content --- if [ -f screenshot.png ]; then COLORS=$(identify -verbose screenshot.png 2>/dev/null | grep "Colors:" | awk '{print $2}') echo "Screenshot unique colors: ${COLORS:-unknown}" if [ "${COLORS:-0}" -le 5 ]; then echo "⚠️ Screenshot appears blank (only $COLORS colors). App may not have rendered." else echo "✅ Screenshot has content ($COLORS unique colors)" fi fi - name: Upload smoke test screenshot if: always() uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 with: name: linux-smoke-test-screenshot path: screenshot.png if-no-files-found: warn - name: Upload logs if: always() uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 with: name: linux-smoke-test-logs path: | /tmp/display-server.log /tmp/app.log if-no-files-found: warn ================================================ FILE: .github/workflows/test.yml ================================================ name: Test on: pull_request: push: branches: [main] jobs: unit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '22' cache: 'npm' - run: npm ci - run: npm run test:data ================================================ FILE: .github/workflows/typecheck.yml ================================================ name: Typecheck on: pull_request: paths-ignore: - 'scripts/**' push: branches: [main] paths-ignore: - 'scripts/**' jobs: typecheck: # No secrets needed — run for all PRs including forks runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 with: node-version: '22' cache: 'npm' - run: npm ci - run: npm run typecheck - run: npm run typecheck:api ================================================ FILE: .gitignore ================================================ node_modules/ .idea/ dist/ public/blog/ .DS_Store *.log .env .env.local .playwright-mcp/ .vercel api/\[domain\]/v1/\[rpc\].js api/\[\[...path\]\].js .claude/ .cursor/ CLAUDE.md .env.vercel-backup .env.vercel-export .agent/ .factory/ .windsurf/ skills/ ideas/ docs/internal/ internal/ test-results/ src-tauri/sidecar/node/* !src-tauri/sidecar/node/.gitkeep # AI planning session state .planning/ # Compiled sebuf gateway bundle (built by scripts/build-sidecar-sebuf.mjs) api/[[][[].*.js # Compiled sidecar domain handler bundles (built by scripts/build-sidecar-handlers.mjs) api/*/v1/\[rpc\].js .claudedocs/ # Large generated data files (reproduced by scripts/) scripts/data/pizzint-processed.json scripts/data/osm-military-processed.json scripts/data/military-bases-final.json scripts/data/dedup-dropped-pairs.json scripts/data/pizzint-partial.json scripts/data/gpsjam-latest.json scripts/data/mirta-raw.geojson scripts/data/osm-military-raw.json # Iran events data (sensitive, not for public repo) scripts/data/iran-events-latest.json # Military bases rebuild script (references external Supabase URLs) scripts/rebuild-military-bases.mjs .wrangler # Build artifacts (generated by esbuild/tsc, not source code) api/data/city-coords.js # Runtime artifacts (generated by sidecar/tools, not source code) api-cache.json verbose-mode.json skills-lock.json tmp/ ================================================ FILE: .husky/pre-commit ================================================ echo "Running Unicode safety check (staged files)..." node scripts/check-unicode-safety.mjs --staged || exit 1 ================================================ FILE: .husky/pre-push ================================================ # Ensure dependencies are installed (worktrees start with no node_modules) if [ ! -d node_modules ]; then echo "node_modules missing, running npm install..." npm install --prefer-offline || exit 1 fi echo "Running type check..." npm run typecheck || exit 1 echo "Running API type check..." npm run typecheck:api || exit 1 echo "Running CJS syntax check..." for f in scripts/*.cjs; do [ -f "$f" ] && node -c "$f" || exit 1 done echo "Running Unicode safety check..." node scripts/check-unicode-safety.mjs || exit 1 echo "Running edge function bundle check..." for f in api/*.js; do case "$(basename "$f")" in _*) continue;; esac npx esbuild "$f" --bundle --format=esm --platform=browser --outfile=/dev/null 2>/dev/null || { echo "ERROR: esbuild failed to bundle $f — this will break Vercel deployment" npx esbuild "$f" --bundle --format=esm --platform=browser --outfile=/dev/null exit 1 } done echo "Running unit tests..." npm run test:data || exit 1 echo "Running edge function tests..." node --test tests/edge-functions.test.mjs || exit 1 echo "Running markdown lint..." npm run lint:md || exit 1 echo "Running MDX lint (Mintlify compatibility)..." node --test tests/mdx-lint.test.mjs || exit 1 echo "Running proto freshness check..." if git diff --name-only origin/main -- proto/ src/generated/ docs/api/ Makefile | grep -q .; then if command -v buf >/dev/null 2>&1 || [ -x "$HOME/go/bin/buf" ]; then export PATH="$HOME/go/bin:$PATH" fi if command -v buf &>/dev/null && command -v protoc-gen-ts-client &>/dev/null; then make generate if ! git diff --exit-code src/generated/ docs/api/; then echo "" echo "============================================================" echo "ERROR: Proto-generated code is out of date." echo "Run 'make generate' locally and commit the updated files." echo "============================================================" exit 1 fi UNTRACKED=$(git ls-files --others --exclude-standard src/generated/ docs/api/) if [ -n "$UNTRACKED" ]; then echo "" echo "============================================================" echo "ERROR: Untracked generated files found:" echo "$UNTRACKED" echo "Run 'make generate' locally and commit the new files." echo "============================================================" exit 1 fi echo "Proto-generated code is up to date." else echo "WARNING: buf or protoc plugins not installed, skipping proto freshness check." echo " Install with: make install-buf install-plugins" fi else echo "No proto-related changes, skipping." fi echo "Running version sync check..." npm run version:check || exit 1 ================================================ FILE: .markdownlint-cli2.jsonc ================================================ { // Only enforce the 3 rules from PR #72. Everything else is off. "config": { "default": false, "MD012": true, "MD022": true, "MD032": true }, "ignores": ["node_modules/**", "dist/**", "src-tauri/target/**", ".planning/**", "DMCA-TAKEDOWN-NOTICE.md"] } ================================================ FILE: .npmrc ================================================ loglevel=error ================================================ FILE: .nvmrc ================================================ 22 ================================================ FILE: .vercelignore ================================================ # Exclude local desktop build artifacts and sidecar binaries from deployments # These files are large and not needed by the Vercel-hosted frontend/API # Tauri build outputs src-tauri/target/ src-tauri/bundle/ # Sidecar and bundled node binaries src-tauri/sidecar/ src-tauri/**/node # macOS disk images and app bundles **/*.dmg **/*.app # Rust/Cargo build artifacts (safety) target/ # Common local artifacts .DS_Store ================================================ FILE: AGENTS.md ================================================ # AGENTS.md Agent entry point for WorldMonitor. Read this first, then follow links for depth. ## What This Project Is Real-time global intelligence dashboard. TypeScript SPA (Vite + Preact) with 86 panel components, 60+ Vercel Edge API endpoints, a Tauri desktop app with Node.js sidecar, and a Railway relay service. Aggregates 30+ external data sources (geopolitics, military, finance, climate, cyber, maritime, aviation). ## Repository Map ``` . ├── src/ # Browser SPA (TypeScript, class-based components) │ ├── app/ # App orchestration (data-loader, refresh-scheduler, panel-layout) │ ├── components/ # 86 UI panels + map components (Panel subclasses) │ ├── config/ # Variant configs, panel/layer definitions, market symbols │ ├── services/ # Business logic (120+ service files, organized by domain) │ ├── types/ # TypeScript type definitions │ ├── utils/ # Shared utilities (circuit-breaker, theme, URL state, DOM) │ ├── workers/ # Web Workers (analysis, ML/ONNX, vector DB) │ ├── generated/ # Proto-generated client/server stubs (DO NOT EDIT) │ ├── locales/ # i18n translation files │ └── App.ts # Main application entry ├── api/ # Vercel Edge Functions (plain JS, self-contained) │ ├── _*.js # Shared helpers (CORS, rate-limit, API key, relay) │ ├── health.js # Health check endpoint │ ├── bootstrap.js # Bulk data hydration endpoint │ └── / # Domain-specific endpoints (aviation/, climate/, etc.) ├── server/ # Server-side shared code (used by Edge Functions) │ ├── _shared/ # Redis, rate-limit, LLM, caching, response headers │ ├── gateway.ts # Domain gateway factory (CORS, auth, cache tiers) │ ├── router.ts # Route matching │ └── worldmonitor/ # Domain handlers (mirrors proto service structure) ├── proto/ # Protobuf definitions (sebuf framework) │ ├── buf.yaml # Buf configuration │ └── worldmonitor/ # Service definitions with HTTP annotations ├── shared/ # Cross-platform data (JSON configs for markets, RSS domains) ├── scripts/ # Seed scripts, build helpers, data fetchers ├── src-tauri/ # Tauri desktop shell (Rust + Node.js sidecar) │ └── sidecar/ # Node.js sidecar API server ├── tests/ # Unit/integration tests (node:test runner) ├── e2e/ # Playwright E2E specs ├── docs/ # Mintlify documentation site ├── docker/ # Docker build for Railway services ├── deploy/ # Deployment configs └── blog-site/ # Static blog (built into public/blog/) ``` ## How to Run ```bash npm install # Install deps (also runs blog-site postinstall) npm run dev # Start Vite dev server (full variant) npm run dev:tech # Start tech-only variant npm run typecheck # tsc --noEmit (strict mode) npm run typecheck:api # Typecheck API layer separately npm run test:data # Run unit/integration tests npm run test:sidecar # Run sidecar + API handler tests npm run test:e2e # Run all Playwright E2E tests make generate # Regenerate proto stubs (requires buf + sebuf plugins) ``` ## Architecture Rules ### Dependency Direction ``` types -> config -> services -> components -> app -> App.ts ``` - `types/` has zero internal imports - `config/` imports only from `types/` - `services/` imports from `types/` and `config/` - `components/` imports from all above - `app/` orchestrates components and services ### API Layer Constraints - `api/*.js` are Vercel Edge Functions: **self-contained JS only** - They CANNOT import from `../src/` or `../server/` (different runtime) - Only same-directory `_*.js` helpers and npm packages - Enforced by `tests/edge-functions.test.mjs` and pre-push hook esbuild check ### Server Layer - `server/` code is bundled INTO Edge Functions at deploy time via gateway - `server/_shared/` contains Redis client, rate limiting, LLM helpers - `server/worldmonitor//` has RPC handlers matching proto services - All handlers use `cachedFetchJson()` for Redis caching with stampede protection ### Proto Contract Flow ``` proto/ definitions -> buf generate -> src/generated/{client,server}/ -> handlers wire up ``` - GET fields need `(sebuf.http.query)` annotation - `repeated string` fields need `parseStringArray()` in handler - `int64` maps to `string` in TypeScript - CI checks proto freshness via `.github/workflows/proto-check.yml` ## Variant System The app ships multiple variants with different panel/layer configurations: - `full` (default): All features - `tech`: Technology-focused subset - `finance`: Financial markets focus - `commodity`: Commodity markets focus - `happy`: Positive news only Variant is set via `VITE_VARIANT` env var. Config lives in `src/config/variants/`. ## Key Patterns ### Adding a New API Endpoint 1. Define proto message in `proto/worldmonitor//` 2. Add RPC with `(sebuf.http.config)` annotation 3. Run `make generate` 4. Create handler in `server/worldmonitor//` 5. Wire handler in domain's `handler.ts` 6. Use `cachedFetchJson()` for caching, include request params in cache key ### Adding a New Panel 1. Create `src/components/MyPanel.ts` extending `Panel` 2. Register in `src/config/panels.ts` 3. Add to variant configs in `src/config/variants/` 4. Wire data loading in `src/app/data-loader.ts` ### Circuit Breakers - `src/utils/circuit-breaker.ts` for client-side - Used in data loaders to prevent cascade failures - Separate breaker per data domain ### Caching - Redis (Upstash) via `server/_shared/redis.ts` - `cachedFetchJson()` coalesces concurrent cache misses - Cache tiers: fast (5m), medium (10m), slow (30m), static (2h), daily (24h) - Cache key MUST include request-varying params ## Testing - **Unit/Integration**: `tests/*.test.{mjs,mts}` using `node:test` runner - **Sidecar tests**: `api/*.test.mjs`, `src-tauri/sidecar/*.test.mjs` - **E2E**: `e2e/*.spec.ts` using Playwright - **Visual regression**: Golden screenshot comparison per variant ## CI Checks (GitHub Actions) | Workflow | Trigger | What it checks | |---|---|---| | `typecheck.yml` | PR + push to main | `tsc --noEmit` for src and API | | `lint.yml` | PR (markdown changes) | markdownlint-cli2 | | `proto-check.yml` | PR (proto changes) | Generated code freshness | | `build-desktop.yml` | Manual | Tauri desktop build | | `test-linux-app.yml` | Manual | Linux AppImage smoke test | ## Pre-Push Hook Runs automatically before `git push`: 1. TypeScript check (src + API) 2. CJS syntax validation 3. Edge function esbuild bundle check 4. Edge function import guardrail test 5. Markdown lint 6. MDX lint (Mintlify compatibility) 7. Version sync check ## Deployment - **Web**: Vercel (auto-deploy on push to main) - **Relay/Seeds**: Railway (Docker, cron services) - **Desktop**: Tauri builds via GitHub Actions - **Docs**: Mintlify (proxied through Vercel at `/docs`) ## Critical Conventions - `fetch.bind(globalThis)` is BANNED. Use `(...args) => globalThis.fetch(...args)` instead - Edge Functions cannot use `node:http`, `node:https`, `node:zlib` - Always include `User-Agent` header in server-side fetch calls - Yahoo Finance requests must be staggered (150ms delays) - New data sources MUST have bootstrap hydration wired in `api/bootstrap.js` - Redis seed scripts MUST write `seed-meta:` for health monitoring ## External References - [Architecture (system reference)](ARCHITECTURE.md) - [Design Philosophy (why decisions were made)](docs/architecture.mdx) - [Contributing guide](CONTRIBUTING.md) - [Data sources catalog](docs/data-sources.mdx) - [Health endpoints](docs/health-endpoints.mdx) - [Adding endpoints guide](docs/adding-endpoints.mdx) - [API reference (OpenAPI)](docs/api/) ================================================ FILE: ARCHITECTURE.md ================================================ # Architecture > **Last verified**: 2026-03-14 against commit `24b502d0` > > **Ownership rule**: When deployment topology, API surface, desktop runtime, or bootstrap keys change, this document must be updated in the same PR. > **Design philosophy**: For the "why" behind architectural decisions, intelligence tradecraft, and algorithmic choices, see [Design Philosophy](docs/architecture.mdx). World Monitor is a real-time global intelligence dashboard built as a TypeScript single-page application. It aggregates data from dozens of external sources covering geopolitics, military activity, financial markets, cyber threats, climate events, maritime tracking, and aviation into a unified operational picture rendered through an interactive map and a grid of specialized panels. --- ## 1. System Overview ``` ┌─────────────────────────────────────────────────────────────────┐ │ Browser / Desktop │ │ ┌──────────┐ ┌──────────┐ ┌────────────┐ ┌──────────────┐ │ │ │ DeckGLMap│ │ GlobeMap │ │ Panels │ │ Workers │ │ │ │(deck.gl) │ │(globe.gl)│ │(86 classes)│ │(ML, analysis)│ │ │ └────┬─────┘ └────┬─────┘ └─────┬──────┘ └──────────────┘ │ │ └──────────────┴──────────────┘ │ │ │ fetch /api/* │ └─────────────────────────┼───────────────────────────────────────┘ │ ┌──────────────┼──────────────┐ │ │ │ ┌──────▼──────┐ ┌─────▼─────┐ ┌─────▼──────┐ │ Vercel │ │ Railway │ │ Tauri │ │ Edge Funcs │ │ AIS Relay │ │ Sidecar │ │ + Middleware│ │ + Seeds │ │ (Node.js) │ └──────┬──────┘ └─────┬─────┘ └─────┬──────┘ │ │ │ └──────────────┼──────────────┘ │ ┌──────▼──────┐ │ Upstash │ │ Redis │ └──────┬──────┘ │ ┌───────────┼───────────┐ │ │ │ ┌─────▼───┐ ┌─────▼───┐ ┌────▼────┐ │ Finnhub │ │ Yahoo │ │ ACLED │ │ OpenSky │ │ GDELT │ │ UCDP │ │ CoinGeck│ │ FRED │ │ FIRMS │ │ ... │ │ ... │ │ ... │ └─────────┘ └─────────┘ └─────────┘ 30+ upstream data sources ``` **Source files**: `package.json`, `vercel.json` --- ## 2. Deployment Topology | Service | Platform | Role | |---------|----------|------| | SPA + Edge Functions | Vercel | Static files, API endpoints, middleware (bot filtering, social OG) | | AIS Relay | Railway | WebSocket proxy (AIS stream), seed loops (market, aviation, GPSJAM, risk scores, UCDP, positive events), RSS proxy, OREF polling | | Redis | Upstash | Cache layer with stampede protection, seed-meta freshness tracking, rate limiting | | Convex | Convex Cloud | Contact form submissions, waitlist registrations | | Documentation | Mintlify | Public docs, proxied through Vercel at `/docs` | | Desktop App | Tauri 2.x | macOS (ARM64, x64), Windows (x64), Linux (x64, ARM64) with bundled Node.js sidecar | | Container Image | GHCR | Multi-arch Docker image (nginx serving built SPA, proxies API to upstream) | **Source files**: `vercel.json`, `docker/Dockerfile`, `scripts/ais-relay.cjs`, `convex/schema.ts`, `src-tauri/tauri.conf.json` --- ## 3. Frontend Architecture ### Entry and Initialization `src/main.ts` initializes Sentry error tracking, Vercel analytics, dynamic meta tags, runtime fetch patches (desktop sidecar redirection), theme application, and creates the `App` instance. `App.init()` runs in 8 phases: 1. **Storage + i18n**: IndexedDB, language detection, locale loading 2. **ML Worker**: ONNX model prep (embeddings, sentiment, summarization) 3. **Sidecar**: Wait for desktop sidecar readiness (desktop only) 4. **Bootstrap**: Two-tier concurrent hydration from `/api/bootstrap` (fast 3s + slow 5s timeouts) 5. **Layout**: PanelLayoutManager renders map and panels 6. **UI**: SignalModal, IntelligenceGapBadge, BreakingNewsBanner, correlation engine 7. **Data**: Parallel `loadAllData()` + viewport-conditional `primeVisiblePanelData()` 8. **Refresh**: Variant-specific polling intervals via `startSmartPollLoop()` ### Component Model All panels extend the `Panel` base class. Panels render via `setContent(html)` (debounced 150ms) and use event delegation on a stable `this.content` element. Panels support resizable row/col spans persisted to localStorage. ### Dual Map System - **DeckGLMap**: WebGL rendering via deck.gl + maplibre-gl. Supports ScatterplotLayer, GeoJsonLayer, PathLayer, IconLayer, PolygonLayer, ArcLayer, HeatmapLayer, H3HexagonLayer. PMTiles protocol for self-hosted basemap tiles. Supercluster for marker clustering. - **GlobeMap**: 3D interactive globe via globe.gl. Single merged `htmlElementsData` array with `_kind` discriminator. Earth texture, atmosphere shader, auto-rotate after idle. Layer definitions live in `src/config/map-layer-definitions.ts`, each specifying renderer support (flat/globe), premium status, variant filtering, and i18n keys. ### State Management No external state library. `AppContext` is a central mutable object holding: map references, panel instances, panel/layer settings, all cached data (news, markets, predictions, clusters, intelligence caches), in-flight request tracking, and UI component references. URL state syncs bidirectionally via `src/utils/urlState.ts` (debounced 250ms). ### Web Workers - **analysis.worker.ts**: News clustering (Jaccard similarity), cross-domain correlation detection - **ml.worker.ts**: ONNX inference via `@xenova/transformers` (MiniLM-L6 embeddings, sentiment, summarization, NER), in-worker vector store for headline memory - **vector-db.ts**: IndexedDB-backed vector store for semantic search ### Variant System Detected by hostname (`tech.worldmonitor.app` → tech, `finance.worldmonitor.app` → finance, etc.) or localStorage on desktop. Controls: default panels, map layers, refresh intervals, theme, UI text. Variant change resets all settings to defaults. **Source files**: `src/main.ts`, `src/App.ts`, `src/app/`, `src/components/Panel.ts`, `src/components/DeckGLMap.ts`, `src/components/GlobeMap.ts`, `src/config/variant.ts`, `src/workers/` --- ## 4. API Layer ### Edge Functions All API endpoints live in `api/` as self-contained JavaScript files deployed as Vercel Edge Functions. They cannot import from `../src/` or `../server/` (different runtime). Only same-directory `_*.js` helpers and npm packages are allowed. This constraint is enforced by `tests/edge-functions.test.mjs` and the pre-push esbuild bundle check. ### Shared Helpers | File | Purpose | |------|---------| | `_cors.js` | Origin allowlist (worldmonitor.app, Vercel previews, tauri://localhost, localhost) | | `_rate-limit.js` | Upstash sliding window rate limiting, IP extraction | | `_api-key.js` | Origin-aware API key validation (desktop requires key, trusted browser exempt) | | `_relay.js` | Factory for proxying requests to Railway relay service | ### Gateway Factory `server/gateway.ts` provides `createDomainGateway(routes)` for per-domain Edge Function bundles. Pipeline: 1. Origin check (403 if disallowed) 2. CORS headers 3. OPTIONS preflight 4. API key validation 5. Rate limiting (endpoint-specific, then global fallback) 6. Route matching (static Map lookup, then dynamic `{param}` scan) 7. POST-to-GET compatibility (for stale clients) 8. Handler execution with error boundary 9. ETag generation (FNV-1a hash) + 304 Not Modified 10. Cache header application ### Cache Tiers | Tier | s-maxage | Use case | |------|----------|----------| | fast | 300s | Live event streams, flight status | | medium | 600s | Market quotes, stock analysis | | slow | 1800s | ACLED events, cyber threats | | static | 7200s | Humanitarian summaries, ETF flows | | daily | 86400s | Critical minerals, static reference data | | no-store | 0 | Vessel snapshots, aircraft tracking | ### Domain Handlers `server/worldmonitor//v1/handler.ts` exports handler objects with per-RPC functions. Each RPC function uses `cachedFetchJson()` from `server/_shared/redis.ts` for cache-miss coalescing: concurrent requests for the same key share a single upstream fetch and Redis write. **Source files**: `api/`, `server/gateway.ts`, `server/router.ts`, `server/_shared/redis.ts`, `server/worldmonitor/` --- ## 5. Proto/RPC Contract System The project uses the **sebuf** framework built on Protocol Buffers: ``` proto/ definitions ↓ buf generate src/generated/client/ (TypeScript RPC client stubs) src/generated/server/ (TypeScript server message types) docs/api/ (OpenAPI v3 specs) ``` Service definitions use `(sebuf.http.config)` annotations to map RPCs to HTTP verbs and paths. GET fields require `(sebuf.http.query)` annotation. `repeated string` fields need `parseStringArray()` in the handler. `int64` maps to `string` in TypeScript. CI enforces generated code freshness via `.github/workflows/proto-check.yml`: runs `make generate` and fails if output differs from committed files. **Source files**: `proto/`, `Makefile`, `src/generated/`, `.github/workflows/proto-check.yml` --- ## 6. Data Pipeline ### Bootstrap Hydration `/api/bootstrap` reads cached keys from Redis in a single batch call. The SPA fetches two tiers concurrently (fast + slow) with separate abort controllers and timeouts. Hydrated data is consumed on-demand by panels via `getHydratedData(key)`. ### Seed Scripts `scripts/seed-*.mjs` fetch upstream data, transform it, and write to Redis via `atomicPublish()` from `scripts/_seed-utils.mjs`. Atomic publish acquires a Redis lock (SET NX), validates data, writes the cache key, writes `seed-meta:` with `{ fetchedAt, recordCount }`, and releases the lock. ### AIS Relay Seed Loops The Railway relay service (`scripts/ais-relay.cjs`) runs continuous seed loops: - Market data (stocks, commodities, crypto, stablecoins, sectors, ETF flows, gulf quotes) - Aviation (international delays) - Positive events - GPSJAM (GPS interference) - Risk scores (CII) - UCDP events These are the primary seeders. Standalone `seed-*.mjs` scripts on Railway cron are secondary/backup. ### Refresh Scheduling `startSmartPollLoop()` supports: exponential backoff (max 4x), viewport-conditional refresh (only if panel is near viewport), tab-pause (suspend when hidden), and staggered flush on tab visibility (150ms delays). ### Health Monitoring `api/health.js` checks every bootstrap and standalone key. For each key it reads `seed-meta:` and compares `fetchedAt` against `maxStaleMin`. Cascade groups handle fallback chains (e.g., theater-posture: live, stale, backup). Returns per-key status: OK, STALE, WARN, EMPTY. **Source files**: `api/bootstrap.js`, `api/health.js`, `scripts/_seed-utils.mjs`, `scripts/seed-*.mjs`, `scripts/ais-relay.cjs`, `src/services/bootstrap.ts`, `src/app/refresh-scheduler.ts` --- ## 7. Desktop Architecture ### Tauri Shell Tauri 2.x (Rust) manages the app lifecycle, system tray, and IPC commands: - **Secret management**: Read/write platform keyring (macOS Keychain, Windows Credential Manager, Linux keyring) - **Sidecar control**: Spawn Node.js process, probe port, inject environment variables - **Window management**: Three trusted windows (main, settings, live-channels) with Edit menu for macOS clipboard shortcuts ### Node.js Sidecar `src-tauri/sidecar/local-api-server.mjs` runs on a dynamic port. It dynamically loads Edge Function handler modules from `api/`, injects secrets from the keyring via environment variables, and monkey-patches `globalThis.fetch` to force IPv4 (Node.js tries IPv6 first, but many government APIs have broken IPv6). ### Fetch Patching `installRuntimeFetchPatch()` in `src/services/runtime.ts` replaces `window.fetch` on the desktop renderer. All `/api/*` requests route to the sidecar with `Authorization: Bearer ` (5-min TTL from Tauri IPC). If the sidecar fails, requests fall back to the cloud API. **Source files**: `src-tauri/src/main.rs`, `src-tauri/sidecar/local-api-server.mjs`, `src/services/runtime.ts`, `src/services/tauri-bridge.ts` --- ## 8. Security Model ### Trust Boundaries ``` Browser ↔ Vercel Edge ↔ Upstream APIs Desktop ↔ Sidecar ↔ Cloud API / Upstream APIs ``` ### Content Security Policy Three CSP sources that must stay in sync: 1. `index.html` `` tag (development, Tauri fallback) 2. `vercel.json` HTTP header (production, overrides meta) 3. `src-tauri/tauri.conf.json` (desktop) ### Authentication API keys are required for non-browser origins. Trusted browser origins (production domains, Vercel preview deployments, localhost) are exempt. Premium RPC paths always require a key. ### Bot Protection `middleware.ts` filters automated traffic: blocks known crawler user-agents on API and asset paths, allows social preview bots (Twitter, Facebook, LinkedIn, Telegram, Discord) on story and OG endpoints. ### Rate Limiting Per-IP sliding window via Upstash with per-endpoint overrides for high-traffic paths. ### Desktop Secret Storage Secrets are stored in the platform keyring (never plaintext), injected into the sidecar via Tauri IPC, and scoped to an allowlist of environment variable keys. **Source files**: `middleware.ts`, `vercel.json`, `index.html`, `src-tauri/tauri.conf.json`, `api/_api-key.js`, `server/_shared/rate-limit.ts` --- ## 9. Caching Architecture ### Four-Layer Hierarchy ``` Bootstrap seed (Railway writes to Redis on schedule) ↓ miss In-memory cache (per Vercel instance, short TTL) ↓ miss Redis (Upstash, cross-instance, cachedFetchJson coalesces concurrent misses) ↓ miss Upstream API fetch (result cached back to Redis + seed-meta written) ``` ### Cache Key Rules Every RPC handler with shared cache MUST include request-varying parameters in the cache key. Failure to do so causes cross-request data leakage. ### ETag / Conditional Requests `server/gateway.ts` computes an FNV-1a hash of each response body and returns it as an `ETag`. Clients send `If-None-Match` and receive `304 Not Modified` when content is unchanged. ### CDN Integration `CDN-Cache-Control` headers give Cloudflare edge (when enabled) longer TTLs than `Cache-Control`, since CF can revalidate via ETag without full payload transfer. ### Seed Metadata Every cache write also writes `seed-meta:` with `{ fetchedAt, recordCount }`. The health endpoint reads these to determine data freshness and raise staleness alerts. **Source files**: `server/_shared/redis.ts`, `server/gateway.ts`, `api/health.js` --- ## 10. Testing ### Unit and Integration `node:test` runner. Test files in `tests/*.test.{mjs,mts}` cover: server handlers, cache keying, circuit breakers, edge function constraints, data validation, market quote dedup, health checks, panel config guardrails, and variant layer filtering. ### Sidecar and API Tests `api/*.test.mjs` and `src-tauri/sidecar/*.test.mjs` test CORS handling, YouTube embed proxying, and local API server behavior. ### End-to-End Playwright specs in `e2e/*.spec.ts` test theme toggling, circuit breaker persistence, keyword spike flows, mobile map interactions, runtime fetch patching, and visual regression via golden screenshot comparison per variant. ### Edge Function Guardrails `tests/edge-functions.test.mjs` validates that all non-helper `api/*.js` files are self-contained: no `node:` built-in imports, no cross-directory `../server/` or `../src/` imports. The pre-push hook also runs an esbuild bundle check on each endpoint. ### Pre-Push Hook Runs before every `git push`: 1. TypeScript check (`tsc --noEmit` for src and API) 2. CJS syntax validation 3. Edge function esbuild bundle check 4. Edge function import guardrail test 5. Markdown lint 6. MDX lint (Mintlify compatibility) 7. Version sync check **Source files**: `tests/`, `e2e/`, `playwright.config.ts`, `.husky/pre-push` --- ## 11. CI/CD | Workflow | Trigger | Checks | |----------|---------|--------| | `typecheck.yml` | PR, push to main | `tsc --noEmit` for src and API tsconfigs | | `lint.yml` | PR (markdown changes) | markdownlint-cli2 | | `proto-check.yml` | PR (proto changes) | Generated code matches committed output | | `build-desktop.yml` | Release tag, manual | 5-platform matrix build, code signing (macOS), AppImage library stripping (Linux), smoke test | | `docker-publish.yml` | Release, manual | Multi-arch image (amd64, arm64) pushed to GHCR | | `test-linux-app.yml` | Manual | Linux AppImage build + headless smoke test with screenshot verification | **Source files**: `.github/workflows/`, `.husky/pre-push` --- ## 12. Directory Reference ``` . ├── api/ Vercel Edge Functions (self-contained JS) │ ├── _*.js Shared helpers (CORS, rate-limit, API key, relay) │ └── / Domain endpoints (aviation/, climate/, conflict/, ...) ├── blog-site/ Static blog (built into public/blog/) ├── convex/ Convex backend (contact form, waitlist) ├── data/ Static data files (conservation, renewable, happiness) ├── deploy/ Deployment configs ├── docker/ Dockerfile + nginx config for Railway ├── docs/ Mintlify documentation site ├── e2e/ Playwright E2E specs ├── proto/ Protobuf service definitions (sebuf framework) ├── scripts/ Seed scripts, build helpers, relay service ├── server/ Server-side code (bundled into Edge Functions) │ ├── _shared/ Redis, rate-limit, LLM, caching utilities │ ├── gateway.ts Domain gateway factory │ ├── router.ts Route matching │ └── worldmonitor/ Domain handlers (mirrors proto structure) ├── shared/ Cross-platform JSON configs (markets, RSS domains) ├── src/ Browser SPA (TypeScript) │ ├── app/ App orchestration managers │ ├── bootstrap/ Chunk reload recovery │ ├── components/ Panel subclasses + map components │ ├── config/ Variant, panel, layer, market configurations │ ├── generated/ Proto-generated client/server stubs (DO NOT EDIT) │ ├── locales/ i18n translation files │ ├── services/ Business logic organized by domain │ ├── types/ TypeScript type definitions │ ├── utils/ Shared utilities (circuit-breaker, theme, URL state) │ └── workers/ Web Workers (analysis, ML, vector DB) ├── src-tauri/ Tauri desktop shell (Rust) │ └── sidecar/ Node.js sidecar API server └── tests/ Unit/integration tests (node:test) ``` ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to World Monitor are documented here. ## [Unreleased] ### Added - US Treasury customs revenue in Trade Policy panel with monthly data, FYTD year-over-year comparison, and revenue spike highlighting (#1663) - Security advisories gold standard migration: Railway cron seed fetches 24 government RSS feeds hourly, Vercel reads Redis only (#1637) - CMD+K full panel coverage: all 55 panels now searchable (was 31), including AI forecasts, correlation panels, webcams, displacement, security advisories (#1656) - Chokepoint transit intelligence with 3 free data sources: IMF PortWatch (vessel transit counts), CorridorRisk (risk intelligence), AISStream (24h crossing counter) (#1560) - 13 monitored chokepoints (was 6): added Cape of Good Hope, Gibraltar, Bosporus Strait (absorbs Dardanelles), Korea, Dover, Kerch, Lombok (#1560, #1572) - Expandable chokepoint cards with TradingView lightweight-charts 180-day time-series (tanker vs cargo) (#1560) - Real-time transit counting with enter+dwell+exit crossing detection, 30min cooldown (#1560) - PortWatch, CorridorRisk, and transit seed loops on Railway relay (#1560) - R2 trace storage for forecast debugging with Cloudflare API upload (#1655) ### Fixed - Trade Policy panel WTO gate changed from panel-wide to per-tab, so Revenue tab works on desktop without WTO API key (#1663) - Conflict-intel seed succeeds without ACLED credentials by accepting empty events when humanitarian/PizzINT data is available (#1651) - Seed-forecasts crash from top-level @aws-sdk/client-s3 import resolved with lazy dynamic import (#1654) - Bootstrap desktop timeouts restored (5s/8s) while keeping aggressive web timeouts (1.2s/1.8s) (#1653) - Service worker navigation reverted to NetworkOnly to prevent stale HTML caching on deploy (#1653) - Railway seed watch paths fixed for 5 services (seed-insights, seed-unrest-events, seed-prediction-markets, seed-infra, seed-gpsjam) - PortWatch ArcGIS URL, field names, and chokepoint name mappings (#1572) ## [2.6.1] - 2026-03-11 ### Highlights - **Blog Platform** — Astro-powered blog at /blog with 16 SEO-optimized posts, OG images, and site footer (#1401, #1405, #1409) - **Country Intelligence** — country facts section with right-click context menu (#1400) - **Satellite Imagery Overhaul** — globe-native rendering, outline-only polygons, CSP fixes (#1381, #1385, #1376) ### Added - Astro blog at /blog with 16 SEO posts and build integration (#1401, #1403) - Blog redesign to match /pro page design system (#1405) - Blog SEO, OG images, favicon fix, and site footer (#1409) - Country facts section and right-click context menu for intel panel (#1400) - Satellite imagery panel enabled in orbital surveillance layer (#1375) - Globe-native satellite imagery, removed sidebar panel (#1381) - Layer search filter with synonym support (#1369) - Close buttons on panels and Add Panel block (#1354) - Enterprise contact form endpoint (#1365) - Commodity and happy variants shown on all header versions (#1407) ### Fixed - NOTAM closures merged into Aviation layer (#1408) - Intel deep dive layout reordered, duplicate headlines removed (#1404) - Satellite imagery outline-only polygons to eliminate alpha stacking blue tint (#1385) - Enterprise form hardened with mandatory fields and lead qualification (#1382) - Country intel silently dismisses when geocode cannot identify a country (#1383) - Globe hit targets enlarged for small marker types (#1378) - Imagery panel hidden for existing users and viewport refetch deadlock (#1377) - CSP violations for satellite preview images (#1376) - Safari TypeError filtering and Sentry noise patterns (#1380) - Swedish locale 'avbruten' TypeError variant filtered (#1402) - Satellite imagery STAC backend fix, merged into Orbital Surveillance (#1364) - Aviation "Computed" source replaced with specific labels, reduced cache TTLs (#1374) - Close button and hover-pause on all marker tooltips (#1371) - Invalid 'satelliteImagery' removed from LAYER_SYNONYMS (#1370) - Risk scores seeding gap and seed-meta key mismatch (#1366) - Consistent LIVE header pattern across news and webcams panels (#1367) - Globe null guards in path accessor callbacks (#1372) - Node_modules guard in pre-push hook, pinned Node 22 (#1368) - Typecheck CI workflow: removed paths-ignore, added push trigger (#1373) - Theme toggle removed from header (#1407) ## [2.6.0] - 2026-03-09 ### Highlights - **Orbital Surveillance** — real-time satellite tracking layer with TLE propagation (#1278) - **Premium Finance Suite** — stock analysis tools for Pro tier (#1268) - **Self-hosted Basemap** — migrated from CARTO to PMTiles on Cloudflare R2 (#1064) - **GPS Jamming v2** — migrated from gpsjam.org to Wingbits API with H3 hexagons (#1240) - **Military Flights Overhaul** — centralized via Redis seed + edge handler with OpenSky/Wingbits fallbacks (#1263, #1274, #1275, #1276) - **Pro Waitlist & Landing Page** — referral system, Turnstile CAPTCHA, 21-language localization (#1140, #1187) - **Server-side AI Classification** — batch headline classification moves from client to server (#1195) - **Commodity Variant** — new app variant focused on commodities with relevant panels & layers (#1040, #1100) - **Health Check System** — comprehensive health endpoint with auto seed-meta freshness tracking (#1091, #1127, #1128) ### Added - Orbital surveillance layer with real-time satellite tracking via satellite.js (#1278, #1281) - Premium finance stock analysis suite for Pro tier (#1268) - GPS jamming migration to Wingbits API with H3 hex grid (#1240) - Commodity app variant with dedicated panels and map layers (#1040, #1100) - Pro waitlist landing page with referral system and Turnstile CAPTCHA (#1140) - Pro landing page localization — 21 languages (#1187) - Pro page repositioning toward markets, macro & geopolitics (#1261) - Referral invite banner when visiting via `?ref=` link (#1232) - Server-side batch AI classification for news headlines (#1195) - Self-hosted PMTiles basemap on Cloudflare R2, replacing CARTO (#1064) - Per-provider map theme selector (#1101) - Globe visual preset setting (Earth / Cosmos) with texture selection (#1090, #1076) - Comprehensive health check endpoint for UptimeRobot (#1091) - Auto seed-meta freshness tracking for all RPC handlers (#1127) - Submarine cables expanded to 86 via TeleGeography API (#1224) - Pak-Afghan conflict zone and country boundary override system (#1150) - Sudan and Myanmar conflict zone polygon improvements (#1216) - Iran events: 28 new location coords, 48h TTL (#1251) - Tech HQs in Ireland data (#1244) - BIS data seed job (#1131) - CoinPaprika fallback for crypto/stablecoin data (#1092) - Rudaw TV live stream and RSS feed (#1117) - Dubai and Riyadh added to default airport watchlist (#1144) - Cmd+K: 16 missing layer toggles (#1289), "See all commands" link with category list (#1270) - UTM attribution tags on all outbound links (#1233) - Performance warning dialog replaces hard layer limit (#1088) - Unified error/retry UX with muted styling and countdown (#1115) - Settings reorganized into collapsible groups (#1110) - Reset Layout button with tooltip (#1267, #1250) - Markdown lint in pre-push hook (#1166) ### Changed - Military flights centralized via Redis seed + edge handler pattern (#1263) - Military flights seed with OpenSky anonymous fallback + Wingbits fallback (#1274, #1275) - Theater posture computed directly in relay instead of pinging Vercel RPC (#1259) - Countries GeoJSON served from R2 CDN (#1164) - Consolidated duplicated market data lists into shared JSON configs (#1212) - Eliminate all frontend external API calls — enforce gold standard pattern (#1217) - WB indicators seeded on Railway, never called from frontend (#1159, #1157) - Temporal baseline for news + fires moved to server-side (#1194) - Panel creation guarded by variant config (#1221) - Panel tab styles unified to underline pattern across all panels (#1106, #1182, #1190, #1192) - Reduce default map layers (#1141) - Share dialog dismissals persist across subdomains via cookies (#1286) - Country-wide conflict zones use actual country geometry (#1245) - Aviation seed interval reduced to 1h (#1258) - Replace curl with native Node.js HTTP CONNECT tunnel in seeds (#1287) - Seed scripts use `_seed-utils.mjs` shared configs from `scripts/shared/` (#1231, #1234) ### Fixed - **Rate Limiting**: prioritize `cf-connecting-ip` over `x-real-ip` for correct per-user rate limiting behind CF proxy (#1241) - **Security**: harden cache keys against injection and hash collision (#1103), per-endpoint rate limits for summarize endpoints (#1161) - **Map**: prevent ghost layers rendering without a toggle (#1264), DeckGL layer toggles getting stuck (#1248), auto-fallback to OpenFreeMap on basemap failure (#1109), CORS fallback for Carto basemap (#1142), use CORS-enabled R2 URL for PMTiles in Tauri (#1119), CII Instability layer disabled in 3D mode (#1292) - **Layout**: reconcile ultrawide zones when map is hidden (#1246), keep settings button visible on scaled desktop widths (#1249), exit fullscreen before switching variants (#1253), apply map-hidden layout class on initial load (#1087), preserve panel column position across refresh (#1170, #1108, #1112) - **Panels**: event delegation to survive setContent debounce (#1203), guard RPC response array access with optional chaining (#1174), clear stuck error headers and sanitize error messages (#1175), lazy panel race conditions + server feed gaps (#1113), Tech Readiness panel loading on full variant (#1208), Strategic Risk panel button listeners (#1214), World Clock green home row (#1202), Airline Intelligence CSS grid layouts (#1197) - **Pro/Turnstile**: explicit rendering to fix widget race condition (#1189), invisible widget support (#1215), CSP allow Turnstile (#1155), handle `already_registered` state (#1183), reset on enterprise form error (#1222), registration feedback and referral code gen (#1229, #1228), no-cache header for /pro (#1179), correct API endpoint path (#1177), www redirect loop fix (#1198, #1201) - **SEO**: comprehensive improvements for /pro and main pages (#1271) - **Railway**: remove custom railpack.json install step causing ENOENT builds (#1296, #1290, #1288) - **Aviation**: correct cancellation rate calculation and add 12 airports (#1209), unify NOTAM status logic (#1225) - **Sentry**: triage 26 issues, fix 3 bugs, add 29 noise filters (#1173, #1098) - **Health**: treat missing seed-meta as stale (#1128), resolve BIS credit and theater posture warnings (#1124), add WB seed loop (#1239), UCDP auth handling (#1252) - **Country Brief**: formatting, duplication, and news cap fixes (#1219), prevent modal stuck on geocode failure (#1134) - **Economic**: guard BIS and spending data against undefined (#1162, #1169) - **Webcams**: detect blocked YouTube embeds on web (#1107), use iframe load event fallback (#1123), MTV Lebanon as live stream (#1122) - **Desktop**: recover stranded routing fixes and unified error UX (#1160), DRY debounce, error handling, retry cap (#1084), debounce cache writes, batch secret push, lazy panels (#1077) - **PWA**: bump SW nuke key to v2 for CF-cached 404s (#1081), one-time SW nuke on first visit (#1079) - **Performance**: only show layer warning when adding layers, not removing (#1265), reduce unnecessary Vercel edge invocations (#1176) - **i18n**: sync all 20 locales to en.json — zero drift (#1104), correct indentation for geocode error keys (#1147) - **Insights**: graceful exit, LKG fallback, swap to Gemini 2.5 Flash (#1153, #1154) - **Seeds**: prevent API quota burn and respect rate limits (#1167), gracefully skip write when validation fails (#1089), seed-meta tracking for all bootstrap keys (#1163, #1138) ## [2.5.25] - 2026-03-04 ### Changed - **Supply Chain v2** — bump chokepoints & minerals cache keys to v2; add `aisDisruptions` field to `ChokepointInfo` (proto, OpenAPI, generated types, handler, UI panel); rename Malacca Strait → Strait of Malacca; reduce chokepoint Redis TTL from 15 min to 5 min; expand description to always show warning + AIS disruption counts; remove Nickel & Copper from critical minerals data (focus on export-controlled minerals); slice top producers to 3; use full FRED series names for shipping indices; add `daily` cache tier (86400s) and move minerals route to it; align client-side circuit breaker TTLs with server TTLs; fix upstream-unavailable banner to only show when no data is present; register supply-chain routes in Vite dev server plugin - **Cache migration**: old `supply_chain:chokepoints:v1` and `supply_chain:minerals:v1` Redis keys are no longer read by any consumer — they will expire via TTL with no action required ## [2.5.24] - 2026-03-03 ### Highlights - **UCDP conflict data** — integrated Uppsala Conflict Data Program for historical & ongoing armed conflict tracking (#760) - **Country brief sharing** — maximize mode, shareable URLs, native browser share button, expanded sections (#743, #854) - **Unified Vercel deployment** — consolidated 4 separate deployments into 1 via runtime variant detection (#756) - **CDN performance overhaul** — POST→GET conversion, per-domain edge functions, tiered bootstrap for ~46% egress reduction (#753, #795, #838) - **Security hardening** — CSP script hashes replace unsafe-inline, crypto.randomUUID() for IDs, XSS-safe i18n, Finnhub token header (#781, #844, #861, #744) - **i18n expansion** — French support with Live TV channels, hardcoded English strings replaced with translation keys (#794, #851, #839) ### Added - UCDP (Uppsala Conflict Data Program) integration for armed conflict tracking (#760) - Iran & Strait of Hormuz conflict zones, upgraded Ukraine polygon (#731) - 100 Iran war events seeded with expanded geocoder (#792) - Country brief maximize mode, shareable URLs, expanded sections & i18n (#743) - Native browser share button for country briefs (#854) - French i18n support with French Live TV channels (#851) - Geo-restricted live channel support, restored WELT (#765) - Manage Channels UX — toggle from grid + show all channels (#745) - Command palette: disambiguate Map vs Panel commands, split country into map/brief (#736) - Command palette: rotating contextual tips replace static empty state (#737) - Download App button for web users with dropdown (#734, #735) - Reset layout button to restore default panel sizes and order (#801) - System status moved into settings (#735) - Vercel cron to pre-warm AviationStack cache (#776) - Runtime variant detection — consolidate 4 Vercel deployments into 1 (#756) - CJS syntax check in pre-push hook (#769) ### Fixed - **Security**: XSS — wrap `t()` calls in `escapeHtml()` (#861), use `crypto.randomUUID()` instead of `Math.random()` for ID generation (#844), move Finnhub API key from query string to `X-Finnhub-Token` header (#744) - **i18n**: replace hardcoded English strings with translation keys (#839), i18n improvements (#794) - **Market**: parse comma-separated query params and align Railway cache keys (#856), Railway market data cron + complete missing tech feed categories (#850), Yahoo relay fallback + RSS digest relay for blocked feeds (#835), tech UNAVAILABLE feeds + Yahoo batch early-exit + sector heatmap gate (#810) - **Aviation**: move AviationStack fetching to Railway relay, reduce to 40 airports (#858) - **UI**: cancel pending debounced calls on component destroy (#848), guard async operations against stale DOM references (#843) - **Sentry**: guard stale DOM refs, audio.play() compat, add 16 noise filters (#855) - **Relay**: exponential backoff for failing RSS feeds (#853), deduplicate UCDP constants crashing Railway container (#766) - **API**: remove `[domain]` catch-all that intercepted all RPC routes (#753 regression) (#785), pageSize bounds validation on research handlers (#819), return 405 for wrong HTTP method (#757), pagination cursor for cyber threats (#754) - **Conflict**: bump Iran events cache-bust to v7 (#724) - **OREF**: prevent LLM translation cache from poisoning Hebrew→English pipeline (#733), strip translation labels from World Brief input (#768) - **Military**: harden USNI fleet report ship name regex (#805) - **Sidecar**: add required params to ACLED API key validation probe (#804) - **Macro**: replace hardcoded BTC mining thresholds with Mayer Multiple (#750) - **Cyber**: reduce GeoIP per-IP timeout from 3s to 1.5s (#748) - **CSP**: restore unsafe-inline for Vercel bot-challenge pages (#788), add missing script hash and finance variant (#798) - **Runtime**: route all /api/* calls through CDN edge instead of direct Vercel (#780) - **Desktop**: detect Linux node target from host arch (#742), harden Windows installer update path + map resize (#739), close update toast after clicking download (#738), only open valid http(s) links externally (#723) - **Webcams**: replace dead Tel Aviv live stream (#732), replace stale Jerusalem feed (#849) - Story header uses full domain WORLDMONITOR.APP (#799) - Open variant nav links in same window instead of new tab (#721) - Suppress map renders during resize drag (#728) - Append deduction panel to DOM after async import resolves (#764) - Deduplicate stale-while-revalidate background fetches in CircuitBreaker (#793) - CORS fallback, rate-limit bump, RSS proxy allowlist (#814) - Unavailable stream error messages updated (#759) ### Performance - Tier slow/fast bootstrap data for ~46% CDN egress reduction (#838) - Convert POST RPCs to GET for CDN caching (#795) - Split monolithic edge function into per-domain functions (#753) - Increase CDN cache TTLs + add stale-if-error across edge functions (#777) - Bump CDN cache TTLs for oref-alerts and youtube/live (#791) - Skip wasted direct fetch for Vercel-blocked domains in RSS proxy (#815) ### Security - Replace CSP unsafe-inline with script hashes and add trust signals (#781) - Expand Permissions-Policy and tighten CSP connect-src (#779) ### Changed - Extend support for larger screens (#740) - Green download button + retire sliding popup (#747) - Extract shared relay helper into `_relay.js` (#782) - Consolidate `SummarizeArticleResponse` status fields (#813) - Consolidate `declare const process` into shared `env.d.ts` (#752) - Deduplicate `clampInt` into `server/_shared/constants` - Add error logging for network errors in error mapper (#746) - Redis error logging + reduced timeouts for edge functions (#749) --- ## [2.5.21] - 2026-03-01 ### Highlights - **Iran Attacks map layer** — conflict events with severity badges, related event popups, and CII integration (#511, #527, #547, #549) - **Telegram Intel panel** — 27 curated OSINT channels via MTProto relay (#550) - **OREF Israel Sirens** — real-time alerts with Hebrew→English translation and 24h history bootstrap (#545, #556, #582) - **GPS/GNSS jamming layer** — detection overlay with CII integration (#570) - **Day/night terminator** — solar terminator overlay on map (#529) - **Breaking news alert banner** — audio alerts for critical/high RSS items with cooldown bypass (#508, #516, #533) - **AviationStack integration** — global airport delays for 128 airports with NOTAM closure detection (#552, #581, #583) - **Strategic risk score** — theater posture + breaking news wired into scoring algorithm (#584) ### Added - Iran Attacks map layer with conflict event popups, severity badges, and priority rendering (#511, #527, #549) - Telegram Intel panel with curated OSINT channel list (#550, #600) - OREF Israel Sirens panel with Hebrew-to-English translation (#545, #556) - OREF 24h history bootstrap on relay startup (#582) - GPS/GNSS jamming detection map layer + CII integration (#570) - Day/night solar terminator overlay (#529) - Breaking news active alert banner with audio for critical/high items (#508) - AviationStack integration for non-US airports + NOTAM closure detection (#552, #581, #583) - RT (Russia Today) HLS livestream + RSS feeds (#585, #586) - Iran webcams tab with 4 feeds (#569, #572, #601) - CBC News optional live channel (#502) - Strategic risk score wired to theater posture + breaking news (#584) - CII scoring: security advisories, Iran strikes, OREF sirens, GPS jamming (#547, #559, #570, #579) - Country brief + CII signal coverage expansion (#611) - Server-side military bases with 125K+ entries + rate limiting (#496) - AVIATIONSTACK_API key in desktop settings (#553) - Iran events seed script and latest data (#575) ### Fixed - **Aviation**: stale IndexedDB cache invalidation + reduced CDN TTL (#607), broken lock replaced with direct cache + cancellation tiers (#591), query all airports instead of rotating batch (#557), NOTAM routing through Railway relay (#599), always show all monitored airports (#603) - **Telegram**: AUTH_KEY_DUPLICATED fixes — latch to stop retry spam (#543), 60s startup delay (#587), graceful shutdown + poll guard (#562), ESM import path fixes (#537, #542), missing relay auth headers (#590) - **Relay**: Polymarket OOM prevention — circuit breaker + concurrency limiter (#519), request deduplication (#513), queue backpressure + response slicing (#593), cache stampede fix (#592), kill switch (#523); smart quotes crash (#563); graceful shutdown (#562, #565); curl for OREF (#546, #567, #571); maxBuffer ENOBUFS (#609); rsshub.app blocked (#526); ERR_HTTP_HEADERS_SENT guard (#509); Telegram memory cleanup (#531) - **Live news**: 7 stale YouTube fallback IDs replaced (#535, #538), broken Europe channel handles (#541), eNCA handle + VTC NOW removal + CTI News (#604), RT HLS recovery (#610), YouTube proxy auth alignment (#554, #555), residential proxy + gzip for detection (#551) - **Breaking news**: critical alerts bypass cooldown (#516), keyword gaps filled (#517, #521), fake pubDate filter (#517), SESSION_START gate removed (#533) - **Threat classifier**: military/conflict keyword gaps + news-to-conflict bridge (#514), Groq 429 stagger (#520) - **Geo**: tokenization-based matching to prevent false positives (#503), 60+ missing locations in hub index (#528) - **Iran**: CDN cache-bust pipeline v4 (#524, #532, #544), read-only handler (#518), Gulf misattribution via bbox disambiguation (#532) - **CII**: Gulf country strike misattribution (#564), compound escalation for military action (#548) - **Bootstrap**: 401/429 rate limiting fix (#512), hydration cache + polling hardening (#504) - **Sentry**: guard YT player methods + GM/InvalidState noise (#602), Android OEM WebView bridge injection (#510), setView invalid preset (#580), beforeSend null-filename leak (#561) - Rate limiting raised to 300 req/min sliding window (#515) - Vercel preview origin regex generalized + bases cache key (#506) - Cross-env for Windows-compatible npm scripts (#499) - Download banner repositioned to bottom-right (#536) - Stale/expired Polymarket markets filtered (#507) - Cyber GeoIP centroid fallback jitter made deterministic (#498) - Cache-control headers hardened for polymarket and rss-proxy (#613) ### Performance - Server-side military base fetches: debounce + static edge cache tier (#497) - RSS: refresh interval raised to 10min, cache TTL to 20min (#612) - Polymarket cache TTL raised to 10 minutes (#568) ### Changed - Stripped 61 debug console.log calls from 20 service files (#501) - Bumped version to 2.5.21 (#605) --- ## [2.5.20] - 2026-02-27 ### Added - **Edge caching**: Complete Cloudflare edge cache tier coverage with degraded-response policy (#484) - **Edge caching**: Cloudflare edge caching for proxy.worldmonitor.app (#478) and api.worldmonitor.app (#471) - **Edge caching**: Tiered edge Cache-Control aligned to upstream TTLs (#474) - **API migration**: Convert 52 API endpoints from POST to GET for edge caching (#468) - **Gateway**: Configurable VITE_WS_API_URL + harden POST-to-GET shim (#480) - **Cache**: Negative-result caching for cachedFetchJson (#466) - **Security advisories**: New panel with government travel alerts (#460) - **Settings**: Redesign settings window with VS Code-style sidebar layout (#461) ### Fixed - **Commodities panel**: Was showing stocks instead of commodities — circuit breaker SWR returned stale data from a different call when cacheTtlMs=0 (#483) - **Analytics**: Use greedy regex in PostHog ingest rewrites (#481) - **Sentry**: Add noise filters for 4 unresolved issues (#479) - **Gateway**: Convert stale POST requests to GET for backwards compat (#477) - **Desktop**: Enable click-to-play YouTube embeds + CISA feed fixes (#476) - **Tech variant**: Use rss() for CISA feed, drop build from pre-push hook (#475) - **Security advisories**: Route feeds through RSS proxy to avoid CORS blocks (#473) - **API routing**: Move 5 path-param endpoints to query params for Vercel routing (#472) - **Beta**: Eagerly load T5-small model when beta mode is enabled - **Scripts**: Handle escaped apostrophes in feed name regex (#455) - **Wingbits**: Add 5-minute backoff on /v1/flights failures (#459) - **Ollama**: Strip thinking tokens, raise max_tokens, fix panel summary cache (#456) - **RSS/HLS**: RSS feed repairs, HLS native playback, summarization cache fix (#452) ### Performance - **AIS proxy**: Increase AIS snapshot edge TTL from 2s to 10s (#482) --- ## [2.5.10] - 2026-02-26 ### Fixed - **Yahoo Finance rate-limit UX**: Show "rate limited — retrying shortly" instead of generic "Failed to load" on Markets, ETF, Commodities, and Sector panels when Yahoo returns 429 (#407) - **Sequential Yahoo calls**: Replace `Promise.all` with staggered batching in commodity quotes, ETF flows, and macro signals to prevent 429 rate limiting (#406) - **Sector heatmap Yahoo fallback**: Sector data now loads via Yahoo Finance when `FINNHUB_API_KEY` is missing (#406) - **Finnhub-to-Yahoo fallback**: Market quotes route Finnhub symbols through Yahoo when API key is not configured (#407) - **ETF early-exit on rate limit**: Skip retry loop and show rate-limit message immediately instead of waiting 60s (#407) - **Sidecar auth resilience**: 401-retry with token refresh for stale sidecar tokens after restart; `diagFetch` auth helper for settings window diagnostics (#407) - **Verbose toggle persistence**: Write verbose state to writable data directory instead of read-only app bundle on macOS (#407) - **AI summary verbosity**: Tighten prompts to 2 sentences / 60 words max with `max_tokens` reduced from 150 to 100 (#404) - **Settings modal title**: Rename from "PANELS" to "SETTINGS" across all 17 locales (#403) - **Sentry noise filters**: CSS.escape() for news ID selectors, player.destroy guard, 11 new ignoreErrors patterns, blob: URL extension frame filter (#402) --- ## [2.5.6] - 2026-02-23 ### Added - **Greek (Ελληνικά) locale** — full translation of all 1,397 i18n keys (#256) - **Nigeria RSS feeds** — 5 new sources: Premium Times, Vanguard, Channels TV, Daily Trust, ThisDay Live - **Greek locale feeds** — Naftemporiki, in.gr, iefimerida.gr for Greek-language news coverage - **Brasil Paralelo source** — Brazilian news with RSS feed and source tier (#260) ### Performance - **AIS relay optimization** — backpressure queue with configurable watermarks, spatial indexing for chokepoint detection (O(chokepoints) vs O(chokepoints × vessels)), pre-serialized + pre-gzipped snapshot cache eliminating per-request JSON.stringify + gzip CPU (#266) ### Fixed - **Vietnam flag country code** — corrected flag emoji in language selector (#245) - **Sentry noise filters** — added patterns for SW FetchEvent, PostHog ingest; enabled SW POST method for PostHog analytics (#246) - **Service Worker same-origin routing** — restricted SW route patterns to same-origin only, preventing cross-origin fetch interception (#247, #251) - **Social preview bot allowlisting** — whitelisted Twitterbot, facebookexternalhit, and other crawlers on OG image assets (#251) - **Windows CORS for Tauri** — allow `http://` origin from `tauri.localhost` for Windows desktop builds (#262) - **Linux AppImage GLib crash** — fix GLib symbol mismatch on newer distros by bundling compatible libraries (#263) --- ## [2.5.2] - 2026-02-21 ### Fixed - **QuotaExceededError handling** — detect storage quota exhaustion and stop further writes to localStorage/IndexedDB instead of silently failing; shared `markStorageQuotaExceeded()` flag across persistent-cache and utility storage - **deck.gl null.getProjection crash** — wrap `setProps()` calls in try/catch to survive map mid-teardown races in debounced/RAF callbacks - **MapLibre "Style is not done loading"** — guard `setFilter()` in mousemove/mouseout handlers during theme switches - **YouTube invalid video ID** — validate video ID format (`/^[\w-]{10,12}$/`) before passing to IFrame Player constructor - **Vercel build skip on empty SHA** — guard `ignoreCommand` against unset `VERCEL_GIT_PREVIOUS_SHA` (first deploy, force deploy) which caused `git diff` to fail and cancel builds - **Sentry noise filters** — added 7 patterns: iOS readonly property, SW FetchEvent, toLowerCase/trim/indexOf injections, QuotaExceededError --- ## [2.5.1] - 2026-02-20 ### Performance - **Batch FRED API requests** — frontend now sends a single request with comma-separated series IDs instead of 7 parallel edge function invocations, eliminating Vercel 25s timeouts - **Parallel UCDP page fetches** — replaced sequential loop with Promise.all for up to 12 pages, cutting fetch time from ~96s worst-case to ~8s - **Bot protection middleware** — blocks known social-media crawlers from hitting API routes, reducing unnecessary edge function invocations - **Extended API cache TTLs** — country-intel 12h→24h, GDELT 2h→4h, nuclear 12h→24h; Vercel ignoreCommand skips non-code deploys ### Fixed - **Partial UCDP cache poisoning** — failed page fetches no longer silently produce incomplete results cached for 6h; partial results get 10-min TTL in both Redis and memory, with `partial: true` flag propagated to CDN cache headers - **FRED upstream error masking** — single-series failures now return 502 instead of empty 200; batch mode surfaces per-series errors and returns 502 when all fail - **Sentry `Load failed` filter** — widened regex from `^TypeError: Load failed$` to `^TypeError: Load failed( \(.*\))?$` to catch host-suffixed variants (e.g., gamma-api.polymarket.com) - **Tooltip XSS hardening** — replaced `rawHtml()` with `safeHtml()` allowlist sanitizer for panel info tooltips - **UCDP country endpoint** — added missing HTTP method guards (OPTIONS/GET) - **Middleware exact path matching** — social preview bot allowlist uses `Set.has()` instead of `startsWith()` prefix matching ### Changed - FRED batch API supports up to 15 comma-separated series IDs with deduplication - Missing FRED API key returns 200 with `X-Data-Status: skipped-no-api-key` header instead of silent empty response - LAYER_TO_SOURCE config extracted from duplicate inline mappings into shared constant --- ## [2.5.0] - 2026-02-20 ### Highlights **Local LLM Support (Ollama / LM Studio)** — Run AI summarization entirely on your own hardware with zero cloud dependency. The desktop app auto-discovers models from any OpenAI-compatible local inference server (Ollama, LM Studio, llama.cpp, vLLM) and populates a selection dropdown. A 4-tier fallback chain ensures summaries always generate: Local LLM → Groq → OpenRouter → browser-side T5. Combined with the Tauri desktop app, this enables fully air-gapped intelligence analysis where no data leaves your machine. ### Added - **Ollama / LM Studio integration** — local AI summarization via OpenAI-compatible `/v1/chat/completions` endpoint with automatic model discovery, embedding model filtering, and fallback to manual text input - **4-tier summarization fallback chain** — Ollama (local) → Groq (cloud) → OpenRouter (cloud) → Transformers.js T5 (browser), each with 5-second timeout before silently advancing to the next - **Shared summarization handler factory** — all three API tiers use identical logic for headline deduplication (Jaccard >0.6), variant-aware prompting, language-aware output, and Redis caching (`summary:v3:{mode}:{variant}:{lang}:{hash}`) - **Settings window with 3 tabs** — dedicated **LLMs** tab (Ollama endpoint/model, Groq, OpenRouter), **API Keys** tab (12+ data source credentials), and **Debug & Logs** tab (traffic log, verbose mode, log file access). Each tab runs an independent verification pipeline - **Consolidated keychain vault** — all desktop secrets stored as a single JSON blob in one OS keychain entry (`secrets-vault`), reducing macOS Keychain authorization prompts from 20+ to exactly 1 on app startup. One-time auto-migration from individual entries with cleanup - **Cross-window secret synchronization** — saving credentials in the Settings window immediately syncs to the main dashboard via `localStorage` broadcast, with no app restart needed - **API key verification pipeline** — each credential is validated against its provider's actual API endpoint. Network errors (timeouts, DNS failures) soft-pass to prevent transient failures from blocking key storage; only explicit 401/403 marks a key invalid - **Plaintext URL inputs** — endpoint URLs (Ollama API, relay URLs, model names) display as readable text instead of masked password dots in Settings - **5 new defense/intel RSS feeds** — Military Times, Task & Purpose, USNI News, Oryx OSINT, UK Ministry of Defence - **Koeberg nuclear power plant** — added to the nuclear facilities map layer (the only commercial reactor in Africa, Cape Town, South Africa) - **Privacy & Offline Architecture** documentation — README now details the three privacy levels: full cloud, desktop with cloud APIs, and air-gapped local with Ollama - **AI Summarization Chain** documentation — README includes provider fallback flow diagram and detailed explanation of headline deduplication, variant-aware prompting, and cross-user cache deduplication ### Changed - AI fallback chain now starts with Ollama (local) before cloud providers - Feature toggles increased from 14 to 15 (added AI/Ollama) - Desktop architecture uses consolidated vault instead of per-key keychain entries - README expanded with ~85 lines of new content covering local LLM support, privacy architecture, summarization chain internals, and desktop readiness framework ### Fixed - URL and model fields in Settings display as plaintext instead of masked password dots - OpenAI-compatible endpoint flow hardened for Ollama/LM Studio response format differences (thinking tokens, missing `choices` array edge cases) - Sentry null guard for `getProjection()` crash with 6 additional noise filters - PathLayer cache cleared on layer toggle-off to prevent stale WebGL buffer rendering --- ## [2.4.1] - 2026-02-19 ### Fixed - **Map PathLayer cache**: Clear PathLayer on toggle-off to prevent stale WebGL buffers - **Sentry noise**: Null guard for `getProjection()` crash and 6 additional noise filters - **Markdown docs**: Resolve lint errors in documentation files --- ## [2.4.0] - 2026-02-19 ### Added - **Live Webcams Panel**: 2x2 grid of live YouTube webcam feeds from global hotspots with region filters (Middle East, Europe, Asia-Pacific, Americas), grid/single view toggle, idle detection, and full i18n support (#111) - **Linux download**: added `.AppImage` option to download banner ### Changed - **Mobile detection**: use viewport width only for mobile detection; touch-capable notebooks (e.g. ROG Flow X13) now get desktop layout (#113) - **Webcam feeds**: curated Tel Aviv, Mecca, LA, Miami; replaced dead Tokyo feed; diverse ALL grid with Jerusalem, Tehran, Kyiv, Washington ### Fixed - **Le Monde RSS**: English feed URL updated (`/en/rss/full.xml` → `/en/rss/une.xml`) to fix 404 - **Workbox precache**: added `html` to `globPatterns` so `navigateFallback` works for offline PWA - **Panel ordering**: one-time migration ensures Live Webcams follows Live News for existing users - **Mobile popups**: improved sheet/touch/controls layout (#109) - **Intelligence alerts**: disabled on mobile to reduce noise (#110) - **RSS proxy**: added 8 missing domains to allowlist - **HTML tags**: repaired malformed tags in panel template literals - **ML worker**: wrapped `unloadModel()` in try/catch to prevent unhandled timeout rejections - **YouTube player**: optional chaining on `playVideo?.()` / `pauseVideo?.()` for initialization race - **Panel drag**: guarded `.closest()` on non-Element event targets - **Beta mode**: resolved race condition and timeout failures - **Sentry noise**: added filters for Firefox `too much recursion`, maplibre `_layers`/`id`/`type` null crashes ## [2.3.9] - 2026-02-18 ### Added - **Full internationalization (14 locales)**: English, French, German, Spanish, Italian, Polish, Portuguese, Dutch, Swedish, Russian, Arabic, Chinese Simplified, Japanese — each with 1100+ translated keys - **RTL support**: Arabic locale with `dir="rtl"`, dedicated RTL CSS overrides, regional language code normalization (e.g. `ar-SA` correctly triggers RTL) - **Language switcher**: in-app locale picker with flag icons, persists to localStorage - **i18n infrastructure**: i18next with browser language detection and English fallback - **Community discussion widget**: floating pill linking to GitHub Discussions with delayed appearance and permanent dismiss - **Linux AppImage**: added `ubuntu-22.04` to CI build matrix with webkit2gtk/appindicator dependencies - **NHK World and Nikkei Asia**: added RSS feeds for Japan news coverage - **Intelligence Findings badge toggle**: option to disable the findings badge in the UI ### Changed - **Zero hardcoded English**: all UI text routed through `t()` — panels, modals, tooltips, popups, map legends, alert templates, signal descriptions - **Trending proper-noun detection**: improved mid-sentence capitalization heuristic with all-caps fallback when ML classifier is unavailable - **Stopword suppression**: added missing English stopwords to trending keyword filter ### Fixed - **Dead UTC clock**: removed `#timeDisplay` element that permanently displayed `--:--:-- UTC` - **Community widget duplicates**: added DOM idempotency guard preventing duplicate widgets on repeated news refresh cycles - **Settings help text**: suppressed raw i18n key paths rendering when translation is missing - **Intelligence Findings badge**: fixed toggle state and listener lifecycle - **Context menu styles**: restored intel-findings context menu styles - **CSS theme variables**: defined missing `--panel-bg` and `--panel-border` variables ## [2.3.8] - 2026-02-17 ### Added - **Finance variant**: Added a dedicated market-first variant (`finance.worldmonitor.app`) with finance/trading-focused feeds, panels, and map defaults - **Finance desktop profile**: Added finance-specific desktop config and build profile for Tauri packaging ### Changed - **Variant feed loading**: `loadNews` now enumerates categories dynamically and stages category fetches with bounded concurrency across variants - **Feed resilience**: Replaced direct MarketWatch RSS usage in finance/full/tech paths with Google News-backed fallback queries - **Classification pressure controls**: Tightened AI classification budgets for tech/full and tuned per-feed caps to reduce startup burst pressure - **Timeline behavior**: Wired timeline filtering consistently across map and news panels - **AI summarization defaults**: Switched OpenRouter summarization to auto-routed free-tier model selection ### Fixed - **Finance panel parity**: Kept data-rich panels while adding news panels for finance instead of removing core data surfaces - **Desktop finance map parity**: Finance variant now runs first-class Deck.GL map/layer behavior on desktop runtime - **Polymarket fallback**: Added one-time direct connectivity probe and memoized fallback to prevent repeated `ERR_CONNECTION_RESET` storms - **FRED fallback behavior**: Missing `FRED_API_KEY` now returns graceful empty payloads instead of repeated hard 500s - **Preview CSP tooling**: Allowed `https://vercel.live` script in CSP so Vercel preview feedback injection is not blocked - **Trending quality**: Suppressed noisy generic finance terms in keyword spike detection - **Mobile UX**: Hidden desktop download prompt on mobile devices ## [2.3.7] - 2026-02-16 ### Added - **Full light mode theme**: Complete light/dark theme system with CSS custom properties, ThemeManager module, FOUC prevention, and `getCSSColor()` utility for theme-aware inline styles - **Theme-aware maps and charts**: Deck.GL basemap, overlay layers, and CountryTimeline charts respond to theme changes in real time - **Dark/light mode header toggle**: Sun/moon icon in the header bar for quick theme switching, replacing the duplicate UTC clock - **Desktop update checker**: Architecture-aware download links for macOS (ARM/Intel) and Windows - **Node.js bundled in Tauri installer**: Sidecar no longer requires system Node.js - **Markdown linting**: Added markdownlint config and CI workflow ### Changed - **Panels modal**: Reverted from "Settings" back to "Panels" — removed redundant Appearance section now that header has theme toggle - **Default panels**: Enabled UCDP Conflict Events, UNHCR Displacement, Climate Anomalies, and Population Exposure panels by default ### Fixed - **CORS for Tauri desktop**: Fixed CORS issues for desktop app requests - **Markets panel**: Keep Yahoo-backed data visible when Finnhub API key is skipped - **Windows UNC paths**: Preserve extended-length path prefix when sanitizing sidecar script path - **Light mode readability**: Darkened neon semantic colors and overlay backgrounds for light mode contrast ## [2.3.6] - 2026-02-16 ### Fixed - **Windows console window**: Hide the `node.exe` console window that appeared alongside the desktop app on Windows ## [2.3.5] - 2026-02-16 ### Changed - **Panel error messages**: Differentiated error messages per panel so users see context-specific guidance instead of generic failures - **Desktop config auto-hide**: Desktop configuration panel automatically hides on web deployments where it is not relevant ## [2.3.4] - 2026-02-16 ### Fixed - **Windows sidecar crash**: Strip `\\?\` UNC extended-length prefix from paths before passing to Node.js — Tauri `resource_dir()` on Windows returns UNC-prefixed paths that cause `EISDIR: lstat 'C:'` in Node.js module resolution - **Windows sidecar CWD**: Set explicit `current_dir` on the Node.js Command to prevent bare drive-letter working directory issues from NSIS shortcut launcher - **Sidecar package scope**: Add `package.json` with `"type": "module"` to sidecar directory, preventing Node.js from walking up the entire directory tree during ESM scope resolution ## [2.3.3] - 2026-02-16 ### Fixed - **Keychain persistence**: Enable `apple-native` (macOS) and `windows-native` (Windows) features for the `keyring` crate — v3 ships with no default platform backends, so API keys were stored in-memory only and lost on restart - **Settings key verification**: Soft-pass network errors during API key verification so transient sidecar failures don't block saving - **Resilient keychain reads**: Use `Promise.allSettled` in `loadDesktopSecrets` so a single key failure doesn't discard all loaded secrets - **Settings window capabilities**: Add `"settings"` to Tauri capabilities window list for core plugin permissions - **Input preservation**: Capture unsaved input values before DOM re-render in settings panel ## [2.3.0] - 2026-02-15 ### Security - **CORS hardening**: Tighten Vercel preview deployment regex to block origin spoofing (`worldmonitorEVIL.vercel.app`) - **Sidecar auth bypass**: Move `/api/local-env-update` behind `LOCAL_API_TOKEN` auth check - **Env key allowlist**: Restrict sidecar env mutations to 18 known secret keys (matching `SUPPORTED_SECRET_KEYS`) - **postMessage validation**: Add `origin` and `source` checks on incoming messages in LiveNewsPanel - **postMessage targetOrigin**: Replace wildcard `'*'` with specific embed origin - **CORS enforcement**: Add `isDisallowedOrigin()` check to 25+ API endpoints that were missing it - **Custom CORS migration**: Migrate `gdelt-geo` and `eia` from custom CORS to shared `_cors.js` module - **New CORS coverage**: Add CORS headers + origin check to `firms-fires`, `stock-index`, `youtube/live` - **YouTube embed origins**: Tighten `ALLOWED_ORIGINS` regex in `youtube/embed.js` - **CSP hardening**: Remove `'unsafe-inline'` from `script-src` in both `index.html` and `tauri.conf.json` - **iframe sandbox**: Add `sandbox="allow-scripts allow-same-origin allow-presentation"` to YouTube embed iframe - **Meta tag validation**: Validate URL query params with regex allowlist in `parseStoryParams()` ### Fixed - **Service worker stale assets**: Add `skipWaiting`, `clientsClaim`, and `cleanupOutdatedCaches` to workbox config — fixes `NS_ERROR_CORRUPTED_CONTENT` / MIME type errors when users have a cached SW serving old HTML after redeployment ## [2.2.6] - 2026-02-14 ### Fixed - Filter trending noise and fix sidecar auth - Restore tech variant panels - Remove Market Radar and Economic Data panels from tech variant ### Docs - Add developer X/Twitter link to Support section - Add cyber threat API keys to `.env.example` ## [2.2.5] - 2026-02-13 ### Security - Migrate all Vercel edge functions to CORS allowlist - Restrict Railway relay CORS to allowed origins only ### Fixed - Hide desktop config panel on web - Route World Bank & Polymarket via Railway relay ## [2.2.3] - 2026-02-12 ### Added - Cyber threat intelligence map layer (Feodo Tracker, URLhaus, C2IntelFeeds, OTX, AbuseIPDB) - Trending keyword spike detection with end-to-end flow - Download desktop app slide-in banner for web visitors - Country briefs in Cmd+K search ### Changed - Redesign 4 panels with table layouts and scoped styles - Redesign population exposure panel and reorder UCDP columns - Dramatically increase cyber threat map density ### Fixed - Resolve z-index conflict between pinned map and panels grid - Cap geo enrichment at 12s timeout, prevent duplicate download banners - Replace ipwho.is/ipapi.co with ipinfo.io/freeipapi.com for geo enrichment - Harden trending spike processing and optimize hot paths - Improve cyber threat tooltip/popup UX and dot visibility ## [2.2.2] - 2026-02-10 ### Added - Full-page Country Brief Page replacing modal overlay - Download redirect API for platform-specific installers ### Fixed - Normalize country name from GeoJSON to canonical TIER1 name - Tighten headline relevance, add Top News section, compact markets - Hide desktop config panel on web, fix irrelevant prediction markets - Tone down climate anomalies heatmap to stop obscuring other layers - macOS: hide window on close instead of quitting ### Performance - Reduce idle CPU from pulse animation loop - Harden regression guardrails in CI, cache, and map clustering ## [2.2.1] - 2026-02-08 ### Fixed - Consolidate variant naming and fix PWA tile caching - Windows settings window: async command, no menu bar, no white flash - Constrain layers menu height in DeckGLMap - Allow Cloudflare Insights script in CSP - macOS build failures when Apple signing secrets are missing ## [2.2.0] - 2026-02-07 Initial v2.2 release with multi-variant support (World + Tech), desktop app (Tauri), and comprehensive geopolitical intelligence features. ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in the World Monitor community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Scope This Code of Conduct applies within all community spaces (GitHub issues, pull requests, discussions, and any associated communication channels) and also applies when an individual is officially representing the community in public spaces. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the project maintainer at **[GitHub Issues](https://github.com/koala73/worldmonitor/issues)** or by contacting the repository owner directly through GitHub. All complaints will be reviewed and investigated promptly and fairly. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. [homepage]: https://www.contributor-covenant.org [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html [Mozilla CoC]: https://github.com/mozilla/diversity [FAQ]: https://www.contributor-covenant.org/faq [translations]: https://www.contributor-covenant.org/translations ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to World Monitor Thank you for your interest in contributing to World Monitor! This project thrives on community contributions — whether it's code, data sources, documentation, or bug reports. ## Table of Contents - [Architecture Overview](#architecture-overview) - [Getting Started](#getting-started) - [Development Setup](#development-setup) - [How to Contribute](#how-to-contribute) - [Pull Request Process](#pull-request-process) - [AI-Assisted Development](#ai-assisted-development) - [Coding Standards](#coding-standards) - [Working with Sebuf (RPC Framework)](#working-with-sebuf-rpc-framework) - [Adding Data Sources](#adding-data-sources) - [Adding RSS Feeds](#adding-rss-feeds) - [Reporting Bugs](#reporting-bugs) - [Feature Requests](#feature-requests) - [Code of Conduct](#code-of-conduct) ## Architecture Overview World Monitor is a real-time OSINT dashboard built with **Vanilla TypeScript** (no UI framework), **MapLibre GL + deck.gl** for map rendering, and a custom Proto-first RPC framework called **Sebuf** for all API communication. ### Key Technologies | Technology | Purpose | |---|---| | **TypeScript** | All code — frontend, edge functions, and handlers | | **Vite** | Build tool and dev server | | **Sebuf** | Proto-first HTTP RPC framework for typed API contracts | | **Protobuf / Buf** | Service and message definitions across 22 domains | | **MapLibre GL** | Base map rendering (tiles, globe mode, camera) | | **deck.gl** | WebGL overlay layers (scatterplot, geojson, arcs, heatmaps) | | **d3** | Charts, sparklines, and data visualization | | **Vercel Edge Functions** | Serverless API gateway | | **Tauri v2** | Desktop app (Windows, macOS, Linux) | | **Convex** | Minimal backend (beta interest registration only) | | **Playwright** | End-to-end and visual regression testing | ### Variant System The codebase produces three app variants from the same source, each targeting a different audience: | Variant | Command | Focus | |---|---|---| | `full` | `npm run dev` | Geopolitics, military, conflicts, infrastructure | | `tech` | `npm run dev:tech` | Startups, AI/ML, cloud, cybersecurity | | `finance` | `npm run dev:finance` | Markets, trading, central banks, commodities | Variants share all code but differ in default panels, map layers, and RSS feeds. Variant configs live in `src/config/variants/`. ### Directory Structure | Directory | Purpose | |---|---| | `src/components/` | UI components — Panel subclasses, map, modals (~50 panels) | | `src/services/` | Data fetching modules — sebuf client wrappers, AI, signal analysis | | `src/config/` | Static data and variant configs (feeds, geo, military, pipelines, ports) | | `src/generated/` | Auto-generated sebuf client + server stubs (**do not edit by hand**) | | `src/types/` | TypeScript type definitions | | `src/locales/` | i18n JSON files (14 languages) | | `src/workers/` | Web Workers for analysis | | `server/` | Sebuf handler implementations for all 17 domain services | | `api/` | Vercel Edge Functions (sebuf gateway + legacy endpoints) | | `proto/` | Protobuf service and message definitions | | `data/` | Static JSON datasets | | `docs/` | Documentation + generated OpenAPI specs | | `src-tauri/` | Tauri v2 Rust app + Node.js sidecar for desktop builds | | `e2e/` | Playwright end-to-end tests | | `scripts/` | Build and packaging scripts | ## Getting Started 1. **Fork** the repository on GitHub 2. **Clone** your fork locally: ```bash git clone https://github.com//worldmonitor.git cd worldmonitor ``` 3. **Create a branch** for your work: ```bash git checkout -b feature/your-feature-name ``` ## Development Setup ```bash # Install everything (buf CLI, sebuf plugins, npm deps, Playwright browsers) make install # Start the development server (full variant, default) npm run dev # Start other variants npm run dev:tech npm run dev:finance # Run type checking npm run typecheck # Run tests npm run test:data # Data integrity tests npm run test:e2e # Playwright end-to-end tests # Production build (per variant) npm run build # full npm run build:tech npm run build:finance ``` The dev server runs at `http://localhost:3000`. Run `make help` to see all available make targets. ### Environment Variables (Optional) For full functionality, copy `.env.example` to `.env.local` and fill in the API keys you need. The app runs without any API keys — external data sources will simply be unavailable. See [API Dependencies](docs/DOCUMENTATION.md#api-dependencies) for the full list. ## How to Contribute ### Types of Contributions We Welcome - **Bug fixes** — found something broken? Fix it! - **New data layers** — add new geospatial data sources to the map - **RSS feeds** — expand our 100+ feed collection with quality sources - **UI/UX improvements** — make the dashboard more intuitive - **Performance optimizations** — faster loading, better caching - **Documentation** — improve docs, add examples, fix typos - **Accessibility** — make the dashboard usable by everyone - **Internationalization** — help make World Monitor available in more languages - **Tests** — add unit or integration tests ### What We're Especially Looking For - New data layers (see [Adding Data Sources](#adding-data-sources)) - Feed quality improvements and new RSS sources - Mobile responsiveness improvements - Performance optimizations for the map rendering pipeline - Better anomaly detection algorithms ## Pull Request Process 1. **Update documentation** if your change affects the public API or user-facing behavior 2. **Run type checking** before submitting: `npm run typecheck` 3. **Test your changes** locally with at least the `full` variant, and any other variant your change affects 4. **Keep PRs focused** — one feature or fix per pull request 5. **Write a clear description** explaining what your PR does and why 6. **Link related issues** if applicable ### PR Title Convention Use a descriptive title that summarizes the change: - `feat: add earthquake magnitude filtering to map layer` - `fix: resolve RSS feed timeout for Al Jazeera` - `docs: update API dependencies section` - `perf: optimize marker clustering at low zoom levels` - `refactor: extract threat classifier into separate module` ### Review Process - All PRs require review from a maintainer before merging - Maintainers may request changes — this is normal and collaborative - Once approved, a maintainer will merge your PR ## AI-Assisted Development We fully embrace AI-assisted development. Many of our own PRs are labeled with the LLM that helped produce them (e.g., `claude`, `codex`, `cursor`), and contributors are welcome to use any AI tools they find helpful. That said, **all code is held to the same quality bar regardless of how it was written**. AI-generated code will be reviewed with the same scrutiny as human-written code. Contributors are responsible for understanding and being able to explain every line they submit. Blindly pasting LLM output without review is discouraged — treat AI as a collaborator, not a replacement for your own judgement. ## Coding Standards ### TypeScript - Use TypeScript for all new code - Avoid `any` types — use proper typing or `unknown` with type guards - Export interfaces/types for public APIs - Use meaningful variable and function names ### Code Style - Follow the existing code style in the repository - Use `const` by default, `let` when reassignment is needed - Prefer functional patterns (map, filter, reduce) over imperative loops - Keep functions focused — one responsibility per function - Add JSDoc comments for exported functions and complex logic ### File Organization - Static layer/geo data and variant configs go in `src/config/` - Sebuf handler implementations go in `server/worldmonitor/{domain}/v1/` - Edge function gateway and legacy endpoints go in `api/` - UI components (panels, map, modals) go in `src/components/` - Service modules (data fetching, client wrappers) go in `src/services/` - Proto definitions go in `proto/worldmonitor/{domain}/v1/` ## Working with Sebuf (RPC Framework) Sebuf is the project's custom Proto-first HTTP RPC framework — a lightweight alternative to gRPC-Web. All API communication between client and server uses Sebuf. ### How It Works 1. **Proto definitions** in `proto/worldmonitor/{domain}/v1/` define services and messages 2. **Code generation** (`make generate`) produces: - TypeScript clients in `src/generated/client/` (e.g., `MarketServiceClient`) - Server route factories in `src/generated/server/` (e.g., `createMarketServiceRoutes`) 3. **Handlers** in `server/worldmonitor/{domain}/v1/handler.ts` implement the service interface 4. **Gateway** in `api/[domain]/v1/[rpc].ts` registers all handlers and routes requests 5. **Clients** in `src/services/{domain}/index.ts` wrap the generated client for app use ### Adding a New RPC Method 1. Add the method to the `.proto` service definition 2. Run `make generate` to regenerate client/server stubs 3. Implement the handler method in the domain's `handler.ts` 4. The client stub is auto-generated — use it from `src/services/{domain}/` Use `make lint` to lint proto files and `make breaking` to check for breaking changes against main. ### Proto Conventions - **Time fields**: Use `int64` (Unix epoch milliseconds), not `google.protobuf.Timestamp` - **int64 encoding**: Apply `[(sebuf.http.int64_encoding) = INT64_ENCODING_NUMBER]` on time fields so TypeScript receives `number` instead of `string` - **HTTP annotations**: Every RPC method needs `option (sebuf.http.config) = { path: "...", method: POST }` ### Proto Codegen Requirements Run `make install` to install everything automatically, or install individually: ```bash make install-buf # Install buf CLI (requires Go) make install-plugins # Install sebuf protoc-gen plugins (requires Go) ``` ## Adding Data Sources To add a new data layer to the map: 1. **Define the data source** — identify the API or dataset you want to integrate 2. **Add the proto service** (if the data needs a backend proxy) — define messages and RPC methods in `proto/worldmonitor/{domain}/v1/` 3. **Generate stubs** — run `make generate` 4. **Implement the handler** in `server/worldmonitor/{domain}/v1/` 5. **Register the handler** in `api/[domain]/v1/[rpc].ts` and `vite.config.ts` (for local dev) 6. **Create the service module** in `src/services/{domain}/` wrapping the generated client 7. **Add the layer config** and implement the map renderer following existing layer patterns 8. **Add to layer toggles** — make it toggleable in the UI 9. **Document the source** — add it to `docs/DOCUMENTATION.md` For endpoints that deal with non-JSON payloads (XML feeds, binary data, HTML embeds), you can add a standalone Edge Function in `api/` instead of Sebuf. For anything returning JSON, prefer Sebuf — the typed contracts are always worth it. ### Data Source Requirements - Must be freely accessible (no paid-only APIs for core functionality) - Must have a permissive license or be public government data - Should update at least daily for real-time relevance - Must include geographic coordinates or be geo-locatable ### Country boundary overrides Country outlines are loaded from `public/data/countries.geojson`. Optional higher-resolution overrides (sourced from [Natural Earth](https://www.naturalearthdata.com/)) are served from R2 CDN. The app loads overrides after the main file and replaces geometry for any country whose `ISO3166-1-Alpha-2` (or `ISO_A2`) matches. To refresh boundary overrides from Natural Earth, run: ```bash node scripts/fetch-country-boundary-overrides.mjs rclone copy public/data/country-boundary-overrides.geojson r2:worldmonitor-maps/ ``` ## Adding RSS Feeds To add new RSS feeds: 1. Verify the feed is reliable and actively maintained 2. Assign a **source tier** (1-4) based on editorial reliability 3. Flag any **state affiliation** or **propaganda risk** 4. Categorize the feed (geopolitics, defense, energy, tech, etc.) 5. Test that the feed parses correctly through the RSS proxy ## Reporting Bugs When filing a bug report, please include: - **Description** — clear description of the issue - **Steps to reproduce** — how to trigger the bug - **Expected behavior** — what should happen - **Actual behavior** — what actually happens - **Screenshots** — if applicable - **Browser/OS** — your environment details - **Console errors** — any relevant browser console output Use the [Bug Report issue template](https://github.com/koala73/worldmonitor/issues/new/choose) when available. ## Feature Requests We welcome feature ideas! When suggesting a feature: - **Describe the problem** it solves - **Propose a solution** with as much detail as possible - **Consider alternatives** you've thought about - **Provide context** — who would benefit from this feature? Use the [Feature Request issue template](https://github.com/koala73/worldmonitor/issues/new/choose) when available. ## Code of Conduct This project follows the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior through GitHub issues or by contacting the repository owner. --- Thank you for helping make World Monitor better! 🌍 ================================================ FILE: Dockerfile ================================================ # ============================================================================= # World Monitor — Docker Image # ============================================================================= # Multi-stage build: # builder — installs deps, compiles TS handlers, builds Vite frontend # final — nginx (static) + node (API) under supervisord # ============================================================================= # ── Stage 1: Builder ───────────────────────────────────────────────────────── FROM node:22-alpine AS builder WORKDIR /app # Install root dependencies (layer-cached until package.json changes) COPY package.json package-lock.json ./ RUN npm ci --ignore-scripts # Copy full source COPY . . # Compile TypeScript API handlers → self-contained ESM bundles # Output is api/**/*.js alongside the source .ts files RUN node docker/build-handlers.mjs # Build Vite frontend (outputs to dist/) # Skip blog build — blog-site has its own deps not installed here RUN npx tsc && npx vite build # ── Stage 2: Runtime ───────────────────────────────────────────────────────── FROM node:22-alpine AS final # nginx + supervisord RUN apk add --no-cache nginx supervisor gettext && \ mkdir -p /tmp/nginx-client-body /tmp/nginx-proxy /tmp/nginx-fastcgi \ /tmp/nginx-uwsgi /tmp/nginx-scgi /var/log/supervisor && \ addgroup -S appgroup && adduser -S appuser -G appgroup WORKDIR /app # API server COPY --from=builder /app/src-tauri/sidecar/local-api-server.mjs ./local-api-server.mjs COPY --from=builder /app/src-tauri/sidecar/package.json ./package.json # API handler modules (JS originals + compiled TS bundles) COPY --from=builder /app/api ./api # Static data files used by handlers at runtime COPY --from=builder /app/data ./data # Built frontend static files COPY --from=builder /app/dist /usr/share/nginx/html # Nginx + supervisord configs COPY docker/nginx.conf /etc/nginx/nginx.conf.template COPY docker/supervisord.conf /etc/supervisor/conf.d/worldmonitor.conf COPY docker/entrypoint.sh /app/entrypoint.sh RUN chmod +x /app/entrypoint.sh # Ensure writable dirs for non-root RUN chown -R appuser:appgroup /app /tmp/nginx-client-body /tmp/nginx-proxy \ /tmp/nginx-fastcgi /tmp/nginx-uwsgi /tmp/nginx-scgi /var/log/supervisor \ /var/lib/nginx /var/log/nginx USER appuser EXPOSE 8080 # Healthcheck via nginx HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \ CMD wget -qO- http://localhost:8080/api/health || exit 1 CMD ["/app/entrypoint.sh"] ================================================ FILE: Dockerfile.relay ================================================ # ============================================================================= # AIS Relay Sidecar # ============================================================================= # Runs scripts/ais-relay.cjs as a standalone container. # Only dependency beyond Node stdlib is the 'ws' WebSocket library. # Set AISSTREAM_API_KEY in docker-compose.yml. # ============================================================================= FROM node:22-alpine WORKDIR /app # Install only the ws package (everything else is Node stdlib) RUN npm install --omit=dev ws@8.19.0 # Relay script COPY scripts/ais-relay.cjs ./scripts/ais-relay.cjs # Shared helper required by the relay (rss-allowed-domains.cjs) COPY shared/ ./shared/ EXPOSE 3004 HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ CMD wget -qO- http://localhost:3004/health || exit 1 CMD ["node", "scripts/ais-relay.cjs"] ================================================ FILE: LICENSE ================================================ World Monitor — Real-time global intelligence dashboard Copyright (C) 2024-2026 Elie Habib This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Remote Network Interaction; Use with the GNU General Public License. Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . ================================================ FILE: Makefile ================================================ .PHONY: help lint generate breaking format check clean deps install install-buf install-plugins install-npm install-playwright .DEFAULT_GOAL := help # Variables PROTO_DIR := proto GEN_CLIENT_DIR := src/generated/client GEN_SERVER_DIR := src/generated/server DOCS_API_DIR := docs/api # Go install settings GO_PROXY := GOPROXY=direct GO_PRIVATE := GOPRIVATE=github.com/SebastienMelki GO_INSTALL := $(GO_PROXY) $(GO_PRIVATE) go install # Required tool versions BUF_VERSION := v1.64.0 SEBUF_VERSION := v0.7.0 help: ## Show this help message @echo 'Usage: make [target]' @echo '' @echo 'Targets:' @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " %-20s %s\n", $$1, $$2}' $(MAKEFILE_LIST) install: install-buf install-plugins install-npm install-playwright deps ## Install everything (buf, sebuf plugins, npm deps, proto deps, browsers) install-buf: ## Install buf CLI @if command -v buf >/dev/null 2>&1; then \ echo "buf already installed: $$(buf --version)"; \ else \ echo "Installing buf..."; \ $(GO_INSTALL) github.com/bufbuild/buf/cmd/buf@$(BUF_VERSION); \ echo "buf installed!"; \ fi install-plugins: ## Install sebuf protoc plugins (requires Go) @echo "Installing sebuf protoc plugins $(SEBUF_VERSION)..." @$(GO_INSTALL) github.com/SebastienMelki/sebuf/cmd/protoc-gen-ts-client@$(SEBUF_VERSION) @$(GO_INSTALL) github.com/SebastienMelki/sebuf/cmd/protoc-gen-ts-server@$(SEBUF_VERSION) @$(GO_INSTALL) github.com/SebastienMelki/sebuf/cmd/protoc-gen-openapiv3@$(SEBUF_VERSION) @echo "Plugins installed!" install-npm: ## Install npm dependencies npm install install-playwright: ## Install Playwright browsers for e2e tests npx playwright install chromium deps: ## Install/update buf proto dependencies cd $(PROTO_DIR) && buf dep update lint: ## Lint protobuf files cd $(PROTO_DIR) && buf lint generate: clean ## Generate code from proto definitions @mkdir -p $(GEN_CLIENT_DIR) $(GEN_SERVER_DIR) $(DOCS_API_DIR) cd $(PROTO_DIR) && buf generate @find $(GEN_CLIENT_DIR) $(GEN_SERVER_DIR) -name '*.ts' -exec sed -i.bak '1s;^;// @ts-nocheck\n;' {} \; -exec rm -f {}.bak \; @echo "Code generation complete!" breaking: ## Check for breaking changes against main cd $(PROTO_DIR) && buf breaking --against '.git#branch=main,subdir=proto' format: ## Format protobuf files cd $(PROTO_DIR) && buf format -w check: lint generate ## Run all checks (lint + generate) clean: ## Clean generated files @rm -rf $(GEN_CLIENT_DIR) @rm -rf $(GEN_SERVER_DIR) @rm -rf $(DOCS_API_DIR) @echo "Clean complete!" ================================================ FILE: README.md ================================================ # World Monitor **Real-time global intelligence dashboard** — AI-powered news aggregation, geopolitical monitoring, and infrastructure tracking in a unified situational awareness interface. [![GitHub stars](https://img.shields.io/github/stars/koala73/worldmonitor?style=social)](https://github.com/koala73/worldmonitor/stargazers) [![GitHub forks](https://img.shields.io/github/forks/koala73/worldmonitor?style=social)](https://github.com/koala73/worldmonitor/network/members) [![Discord](https://img.shields.io/badge/Discord-Join-5865F2?style=flat&logo=discord&logoColor=white)](https://discord.gg/re63kWKxaz) [![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0) [![TypeScript](https://img.shields.io/badge/TypeScript-007ACC?style=flat&logo=typescript&logoColor=white)](https://www.typescriptlang.org/) [![Last commit](https://img.shields.io/github/last-commit/koala73/worldmonitor)](https://github.com/koala73/worldmonitor/commits/main) [![Latest release](https://img.shields.io/github/v/release/koala73/worldmonitor?style=flat)](https://github.com/koala73/worldmonitor/releases/latest)

Web App  Tech Variant  Finance Variant  Commodity Variant  Happy Variant

Download Windows  Download macOS ARM  Download macOS Intel  Download Linux

Documentation  ·  Releases  ·  Contributing

![World Monitor Dashboard](docs/images/worldmonitor-7-mar-2026.jpg) --- ## What It Does - **435+ curated news feeds** across 15 categories, AI-synthesized into briefs - **Dual map engine** — 3D globe (globe.gl) and WebGL flat map (deck.gl) with 45 data layers - **Cross-stream correlation** — military, economic, disaster, and escalation signal convergence - **Country Intelligence Index** — composite risk scoring across 12 signal categories - **Finance radar** — 92 stock exchanges, commodities, crypto, and 7-signal market composite - **Local AI** — run everything with Ollama, no API keys required - **5 site variants** from a single codebase (world, tech, finance, commodity, happy) - **Native desktop app** (Tauri 2) for macOS, Windows, and Linux - **21 languages** with native-language feeds and RTL support For the full feature list, architecture, data sources, and algorithms, see the **[documentation](https://docs.worldmonitor.app)**. --- ## Quick Start ```bash git clone https://github.com/koala73/worldmonitor.git cd worldmonitor npm install npm run dev ``` Open [localhost:5173](http://localhost:5173). No environment variables required for basic operation. For variant-specific development: ```bash npm run dev:tech # tech.worldmonitor.app npm run dev:finance # finance.worldmonitor.app npm run dev:commodity # commodity.worldmonitor.app npm run dev:happy # happy.worldmonitor.app ``` See the **[self-hosting guide](https://docs.worldmonitor.app/getting-started)** for deployment options (Vercel, Docker, static). --- ## Tech Stack | Category | Technologies | |----------|-------------| | **Frontend** | Vanilla TypeScript, Vite, globe.gl + Three.js, deck.gl + MapLibre GL | | **Desktop** | Tauri 2 (Rust) with Node.js sidecar | | **AI/ML** | Ollama / Groq / OpenRouter, Transformers.js (browser-side) | | **API Contracts** | Protocol Buffers (92 protos, 22 services), sebuf HTTP annotations | | **Deployment** | Vercel Edge Functions (60+), Railway relay, Tauri, PWA | | **Caching** | Redis (Upstash), 3-tier cache, CDN, service worker | Full stack details in the **[architecture docs](https://docs.worldmonitor.app/architecture)**. --- ## Contributing Contributions welcome! See [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines. ```bash npm run typecheck # Type checking npm run build:full # Production build ``` --- ## License **AGPL-3.0** for non-commercial use. **Commercial license** required for any commercial use. | Use Case | Allowed? | |----------|----------| | Personal / research / educational | Yes | | Self-hosted (non-commercial) | Yes, with attribution | | Fork and modify (non-commercial) | Yes, share source under AGPL-3.0 | | Commercial use / SaaS / rebranding | Requires commercial license | See [LICENSE](LICENSE) for full terms. For commercial licensing, contact the maintainer. Copyright (C) 2024-2026 Elie Habib. All rights reserved. --- ## Author **Elie Habib** — [GitHub](https://github.com/koala73) ## Contributors ## Security Acknowledgments We thank the following researchers for responsibly disclosing security issues: - **Cody Richard** — Disclosed three security findings covering IPC command exposure, renderer-to-sidecar trust boundary analysis, and fetch patch credential injection architecture (2026) See our [Security Policy](./SECURITY.md) for responsible disclosure guidelines. ---

worldmonitor.app  ·  docs.worldmonitor.app  ·  finance.worldmonitor.app  ·  commodity.worldmonitor.app

## Star History Star History Chart ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Supported Versions | Version | Supported | | ------- | ------------------ | | main | :white_check_mark: | Only the latest version on the `main` branch is actively maintained and receives security updates. ## Reporting a Vulnerability **Please do NOT report security vulnerabilities through public GitHub issues.** If you discover a security vulnerability in World Monitor, please report it responsibly: 1. **GitHub Private Vulnerability Reporting**: Use [GitHub's private vulnerability reporting](https://github.com/koala73/worldmonitor/security/advisories/new) to submit your report directly through the repository. 2. **Direct Contact**: Alternatively, reach out to the repository owner [@koala73](https://github.com/koala73) directly through GitHub. ### What to Include - A description of the vulnerability and its potential impact - Steps to reproduce the issue - Affected components (edge functions, client-side code, data layers, etc.) - Any potential fixes or mitigations you've identified ### Response Timeline - **Acknowledgment**: Within 48 hours of your report - **Initial Assessment**: Within 1 week - **Fix/Patch**: Depending on severity, critical issues will be prioritized ### What to Expect - You will receive an acknowledgment of your report - We will work with you to understand and validate the issue - We will keep you informed of progress toward a fix - Credit will be given to reporters in the fix commit (unless you prefer anonymity) ## Security Considerations World Monitor is a client-side intelligence dashboard that aggregates publicly available data. Here are the key security areas: ### API Keys & Secrets - **Web deployment**: API keys are stored server-side in Vercel Edge Functions - **Desktop runtime**: API keys are stored in the OS keychain (macOS Keychain / Windows Credential Manager) via a consolidated vault entry, never on disk in plaintext - No API keys should ever be committed to the repository - Environment variables (`.env.local`) are gitignored - The RSS proxy uses domain allowlisting to prevent SSRF ### Edge Functions & Sebuf Handlers - All 17 domain APIs are served through Sebuf (a Proto-first RPC framework) via Vercel Edge Functions - Edge functions and handlers should validate/sanitize all input - CORS headers are configured per-function - Rate limiting and circuit breakers protect against abuse ### Client-Side Security - No sensitive data is stored in localStorage or sessionStorage - External content (RSS feeds, news) is sanitized before rendering - Map data layers use trusted, vetted data sources - Content Security Policy restricts script-src to `'self'` (no unsafe-inline/eval) ### Desktop Runtime Security (Tauri) - **IPC origin validation**: Sensitive Tauri commands (secrets, cache, token) are gated to trusted windows only; external-origin windows (e.g., YouTube login) are blocked - **DevTools**: Disabled in production builds; gated behind an opt-in Cargo feature for development - **Sidecar authentication**: A per-session CSPRNG token (`LOCAL_API_TOKEN`) authenticates all renderer-to-sidecar requests, preventing other local processes from accessing the API - **Capability isolation**: The YouTube login window runs under a restricted capability with no access to secret or cache IPC commands - **Fetch patch trust boundary**: The global fetch interceptor injects the sidecar token with a 5-minute TTL; the renderer is the intended client — if renderer integrity is compromised, Tauri IPC provides strictly more access than the fetch patch ### Data Sources - World Monitor aggregates publicly available OSINT data - No classified or restricted data sources are used - State-affiliated sources are flagged with propaganda risk ratings - All data is consumed read-only — the platform does not modify upstream sources ## Scope The following are **in scope** for security reports: - Vulnerabilities in the World Monitor codebase - Edge function security issues (SSRF, injection, auth bypass) - XSS or content injection through RSS feeds or external data - API key exposure or secret leakage - Tauri IPC command privilege escalation or capability bypass - Sidecar authentication bypass or token leakage - Dependency vulnerabilities with a viable attack vector The following are **out of scope**: - Vulnerabilities in third-party services we consume (report to the upstream provider) - Social engineering attacks - Denial of service attacks - Issues in forked copies of the repository - Security issues in user-provided environment configurations ## Best Practices for Contributors - Never commit API keys, tokens, or secrets - Use environment variables for all sensitive configuration - Sanitize external input in edge functions - Keep dependencies updated — run `npm audit` regularly - Follow the principle of least privilege for API access --- Thank you for helping keep World Monitor and its users safe! 🔒 ================================================ FILE: SELF_HOSTING.md ================================================ # 🌍 Self-Hosting World Monitor Run the full World Monitor stack locally with Docker/Podman. ## 📋 Prerequisites - **Docker** or **Podman** (rootless works fine) - **Docker Compose** or **podman-compose** (`pip install podman-compose` or `uvx podman-compose`) - **Node.js 22+** (for running seed scripts on the host) ## 🚀 Quick Start ```bash # 1. Clone and enter the repo git clone https://github.com/koala73/worldmonitor.git cd worldmonitor npm install # 2. Start the stack docker compose up -d # or: uvx podman-compose up -d # 3. Seed data into Redis ./scripts/run-seeders.sh # 4. Open the dashboard open http://localhost:3000 ``` The dashboard works out of the box with public data sources (earthquakes, weather, conflicts, etc.). API keys unlock additional data feeds. ## 🔑 API Keys Create a `docker-compose.override.yml` to inject your keys. This file is **gitignored** — your secrets stay local. ```yaml services: worldmonitor: environment: # 🤖 LLM — pick one or both (used for intelligence assessments) GROQ_API_KEY: "" # https://console.groq.com (free, 14.4K req/day) OPENROUTER_API_KEY: "" # https://openrouter.ai (free, 50 req/day) # 📊 Markets & Economics FINNHUB_API_KEY: "" # https://finnhub.io (free tier) FRED_API_KEY: "" # https://fred.stlouisfed.org/docs/api/api_key.html (free) EIA_API_KEY: "" # https://www.eia.gov/opendata/ (free) # ⚔️ Conflict & Unrest ACLED_ACCESS_TOKEN: "" # https://acleddata.com (free for researchers) # 🛰️ Earth Observation NASA_FIRMS_API_KEY: "" # https://firms.modaps.eosdis.nasa.gov (free) # ✈️ Aviation AVIATIONSTACK_API: "" # https://aviationstack.com (free tier) # 🚢 Maritime AISSTREAM_API_KEY: "" # https://aisstream.io (free) # 🌐 Internet Outages (paid) CLOUDFLARE_API_TOKEN: "" # https://dash.cloudflare.com (requires Radar access) # 🔌 Self-hosted LLM (optional — any OpenAI-compatible endpoint) LLM_API_URL: "" # e.g. http://localhost:11434/v1/chat/completions LLM_API_KEY: "" LLM_MODEL: "" ais-relay: environment: AISSTREAM_API_KEY: "" # same key as above — relay needs it too ``` ### 💰 Free vs Paid | Status | Keys | |--------|------| | 🟢 No key needed | Earthquakes, weather, natural events, UNHCR displacement, prediction markets, stablecoins, crypto, spending, climate anomalies, submarine cables, BIS data, cyber threats | | 🟢 Free signup | GROQ, FRED, EIA, NASA FIRMS, AISSTREAM, Finnhub, AviationStack, ACLED, OpenRouter | | 🟡 Free (limited) | OpenSky (higher rate limits with account) | | 🔴 Paid | Cloudflare Radar (internet outages) | ## 🌱 Seeding Data The seed scripts fetch upstream data and write it to Redis. They run **on the host** (not inside the container) and need the Redis REST proxy to be running. ```bash # Run all seeders (auto-sources API keys from docker-compose.override.yml) ./scripts/run-seeders.sh ``` **⚠️ Important:** Redis data persists across container restarts via the `redis-data` volume, but is lost on `docker compose down -v`. Re-run the seeders if you remove volumes or see stale data. To automate, add a cron job: ```bash # Re-seed every 30 minutes */30 * * * * cd /path/to/worldmonitor && ./scripts/run-seeders.sh >> /tmp/wm-seeders.log 2>&1 ``` ### 🔧 Manual seeder invocation If you prefer to run seeders individually: ```bash export UPSTASH_REDIS_REST_URL=http://localhost:8079 export UPSTASH_REDIS_REST_TOKEN=wm-local-token node scripts/seed-earthquakes.mjs node scripts/seed-military-flights.mjs # ... etc ``` ## 🏗️ Architecture ``` ┌─────────────────────────────────────────────┐ │ localhost:3000 │ │ (nginx) │ ├──────────────┬──────────────────────────────┤ │ Static Files │ /api/* proxy │ │ (Vite SPA) │ │ │ │ │ Node.js API (:46123) │ │ │ 50+ route handlers │ │ │ │ │ │ │ Redis REST proxy (:8079) │ │ │ │ │ │ │ Redis (:6379) │ └──────────────┴──────────────────────────────┘ AIS Relay (WebSocket → AISStream) ``` | Container | Purpose | Port | |-----------|---------|------| | `worldmonitor` | nginx + Node.js API (supervisord) | 3000 → 8080 | | `worldmonitor-redis` | Data store | 6379 (internal) | | `worldmonitor-redis-rest` | Upstash-compatible REST proxy | 8079 | | `worldmonitor-ais-relay` | Live vessel tracking WebSocket | 3004 (internal) | ## 🔨 Building from Source ```bash # Frontend only (for development) npx vite build # Full Docker image docker build -t worldmonitor:latest -f Dockerfile . # Rebuild and restart docker compose down && docker compose up -d ./scripts/run-seeders.sh ``` ### ⚠️ Build Notes - The Docker image uses **Node.js 22 Alpine** for both builder and runtime stages - Blog site build is skipped in Docker (separate dependencies) - The runtime stage needs `gettext` (Alpine package) for `envsubst` in the nginx config - If you hit `npm ci` sync errors in Docker, regenerate the lockfile with the container's npm version: ```bash docker run --rm -v "$(pwd)":/app -w /app node:22-alpine npm install --package-lock-only ``` ## 🌐 Connecting to External Infrastructure ### Shared Redis (optional) If you run other stacks that share a Redis instance, connect via an external network: ```yaml # docker-compose.override.yml services: redis: networks: - infra_default networks: infra_default: external: true ``` ### Self-Hosted LLM Any OpenAI-compatible endpoint works (Ollama, vLLM, llama.cpp server, etc.): ```yaml # docker-compose.override.yml services: worldmonitor: environment: LLM_API_URL: "http://your-host:8000/v1/chat/completions" LLM_API_KEY: "your-key" LLM_MODEL: "your-model-name" extra_hosts: - "your-host:192.168.1.100" # if not DNS-resolvable ``` ## 🐛 Troubleshooting | Issue | Fix | |-------|-----| | 📡 `0/55 OK` on health check | Seeders haven't run — `./scripts/run-seeders.sh` | | 🔴 nginx won't start | Check `podman logs worldmonitor` — likely missing `gettext` package | | 🔑 Seeders say "Missing UPSTASH_REDIS_REST_URL" | Stack isn't running, or run via `./scripts/run-seeders.sh` (auto-sets env vars) | | 📦 `npm ci` fails in Docker build | Lockfile mismatch — regenerate with `docker run --rm -v $(pwd):/app -w /app node:22-alpine npm install --package-lock-only` | | 🚢 No vessel data | Set `AISSTREAM_API_KEY` in both `worldmonitor` and `ais-relay` services | | 🔥 No wildfire data | Set `NASA_FIRMS_API_KEY` | | 🌐 No outage data | Requires `CLOUDFLARE_API_TOKEN` (paid Radar access) | ================================================ FILE: api/_api-key.js ================================================ const DESKTOP_ORIGIN_PATTERNS = [ /^https?:\/\/tauri\.localhost(:\d+)?$/, /^https?:\/\/[a-z0-9-]+\.tauri\.localhost(:\d+)?$/i, /^tauri:\/\/localhost$/, /^asset:\/\/localhost$/, ]; const BROWSER_ORIGIN_PATTERNS = [ /^https:\/\/(.*\.)?worldmonitor\.app$/, /^https:\/\/worldmonitor-[a-z0-9-]+-elie-[a-z0-9]+\.vercel\.app$/, ...(process.env.NODE_ENV === 'production' ? [] : [ /^https?:\/\/localhost(:\d+)?$/, /^https?:\/\/127\.0\.0\.1(:\d+)?$/, ]), ]; function isDesktopOrigin(origin) { return Boolean(origin) && DESKTOP_ORIGIN_PATTERNS.some(p => p.test(origin)); } function isTrustedBrowserOrigin(origin) { return Boolean(origin) && BROWSER_ORIGIN_PATTERNS.some(p => p.test(origin)); } function extractOriginFromReferer(referer) { if (!referer) return ''; try { return new URL(referer).origin; } catch { return ''; } } export function validateApiKey(req, options = {}) { const forceKey = options.forceKey === true; const key = req.headers.get('X-WorldMonitor-Key'); // Same-origin browser requests don't send Origin (per CORS spec). // Fall back to Referer to identify trusted same-origin callers. const origin = req.headers.get('Origin') || extractOriginFromReferer(req.headers.get('Referer')) || ''; // Desktop app — always require API key if (isDesktopOrigin(origin)) { if (!key) return { valid: false, required: true, error: 'API key required for desktop access' }; const validKeys = (process.env.WORLDMONITOR_VALID_KEYS || '').split(',').filter(Boolean); if (!validKeys.includes(key)) return { valid: false, required: true, error: 'Invalid API key' }; return { valid: true, required: true }; } // Trusted browser origin (worldmonitor.app, Vercel previews, localhost dev) — no key needed if (isTrustedBrowserOrigin(origin)) { if (forceKey && !key) { return { valid: false, required: true, error: 'API key required' }; } if (key) { const validKeys = (process.env.WORLDMONITOR_VALID_KEYS || '').split(',').filter(Boolean); if (!validKeys.includes(key)) return { valid: false, required: true, error: 'Invalid API key' }; } return { valid: true, required: forceKey }; } // Explicit key provided from unknown origin — validate it if (key) { const validKeys = (process.env.WORLDMONITOR_VALID_KEYS || '').split(',').filter(Boolean); if (!validKeys.includes(key)) return { valid: false, required: true, error: 'Invalid API key' }; return { valid: true, required: true }; } // No origin, no key — require API key (blocks unauthenticated curl/scripts) return { valid: false, required: true, error: 'API key required' }; } ================================================ FILE: api/_cors.js ================================================ const ALLOWED_ORIGIN_PATTERNS = [ /^https:\/\/(.*\.)?worldmonitor\.app$/, /^https:\/\/worldmonitor-[a-z0-9-]+-elie-[a-z0-9]+\.vercel\.app$/, /^https?:\/\/localhost(:\d+)?$/, /^https?:\/\/127\.0\.0\.1(:\d+)?$/, /^https?:\/\/tauri\.localhost(:\d+)?$/, /^https?:\/\/[a-z0-9-]+\.tauri\.localhost(:\d+)?$/i, /^tauri:\/\/localhost$/, /^asset:\/\/localhost$/, ]; function isAllowedOrigin(origin) { return Boolean(origin) && ALLOWED_ORIGIN_PATTERNS.some((pattern) => pattern.test(origin)); } export function getCorsHeaders(req, methods = 'GET, OPTIONS') { const origin = req.headers.get('origin') || ''; const allowOrigin = isAllowedOrigin(origin) ? origin : 'https://worldmonitor.app'; return { 'Access-Control-Allow-Origin': allowOrigin, 'Access-Control-Allow-Methods': methods, 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-WorldMonitor-Key', 'Access-Control-Max-Age': '86400', 'Vary': 'Origin', }; } /** * CORS headers for public cacheable responses (seeded data, no per-user variation). * Uses ACAO: * so Vercel edge stores ONE cache entry per URL instead of one per * unique Origin. Eliminates Vary: Origin cache fragmentation that multiplies * origin hits by the number of distinct client origins. * * Safe to use when isDisallowedOrigin() has already blocked unauthorized origins. */ export function getPublicCorsHeaders(methods = 'GET, OPTIONS') { return { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': methods, 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-WorldMonitor-Key', 'Access-Control-Max-Age': '86400', }; } export function isDisallowedOrigin(req) { const origin = req.headers.get('origin'); if (!origin) return false; return !isAllowedOrigin(origin); } ================================================ FILE: api/_cors.test.mjs ================================================ import { strict as assert } from 'node:assert'; import test from 'node:test'; import { getCorsHeaders, isDisallowedOrigin } from './_cors.js'; function makeRequest(origin) { const headers = new Headers(); if (origin !== null) { headers.set('origin', origin); } return new Request('https://worldmonitor.app/api/test', { headers }); } test('allows desktop Tauri origins', () => { const origins = [ 'https://tauri.localhost', 'https://abc123.tauri.localhost', 'tauri://localhost', 'asset://localhost', 'http://127.0.0.1:46123', ]; for (const origin of origins) { const req = makeRequest(origin); assert.equal(isDisallowedOrigin(req), false, `origin should be allowed: ${origin}`); const cors = getCorsHeaders(req); assert.equal(cors['Access-Control-Allow-Origin'], origin); } }); test('rejects unrelated external origins', () => { const req = makeRequest('https://evil.example.com'); assert.equal(isDisallowedOrigin(req), true); const cors = getCorsHeaders(req); assert.equal(cors['Access-Control-Allow-Origin'], 'https://worldmonitor.app'); }); test('requests without origin remain allowed', () => { const req = makeRequest(null); assert.equal(isDisallowedOrigin(req), false); }); ================================================ FILE: api/_github-release.js ================================================ const RELEASES_URL = 'https://api.github.com/repos/koala73/worldmonitor/releases/latest'; export async function fetchLatestRelease(userAgent) { const res = await fetch(RELEASES_URL, { headers: { 'Accept': 'application/vnd.github+json', 'User-Agent': userAgent, }, }); if (!res.ok) return null; return res.json(); } ================================================ FILE: api/_ip-rate-limit.js ================================================ export function createIpRateLimiter({ limit, windowMs }) { const rateLimitMap = new Map(); function getEntry(ip) { return rateLimitMap.get(ip) || null; } function isRateLimited(ip) { const now = Date.now(); const entry = getEntry(ip); if (!entry || now - entry.windowStart > windowMs) { rateLimitMap.set(ip, { windowStart: now, count: 1 }); return false; } entry.count += 1; return entry.count > limit; } return { isRateLimited, getEntry }; } ================================================ FILE: api/_json-response.js ================================================ export function jsonResponse(body, status, headers = {}) { return new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json', ...headers, }, }); } ================================================ FILE: api/_rate-limit.js ================================================ import { Ratelimit } from '@upstash/ratelimit'; import { Redis } from '@upstash/redis'; import { jsonResponse } from './_json-response.js'; let ratelimit = null; function getRatelimit() { if (ratelimit) return ratelimit; const url = process.env.UPSTASH_REDIS_REST_URL; const token = process.env.UPSTASH_REDIS_REST_TOKEN; if (!url || !token) return null; ratelimit = new Ratelimit({ redis: new Redis({ url, token }), limiter: Ratelimit.slidingWindow(600, '60 s'), prefix: 'rl', analytics: false, }); return ratelimit; } function getClientIp(request) { return ( request.headers.get('x-real-ip') || request.headers.get('cf-connecting-ip') || request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || '0.0.0.0' ); } export async function checkRateLimit(request, corsHeaders) { const rl = getRatelimit(); if (!rl) return null; const ip = getClientIp(request); try { const { success, limit, reset } = await rl.limit(ip); if (!success) { return jsonResponse({ error: 'Too many requests' }, 429, { 'X-RateLimit-Limit': String(limit), 'X-RateLimit-Remaining': '0', 'X-RateLimit-Reset': String(reset), 'Retry-After': String(Math.ceil((reset - Date.now()) / 1000)), ...corsHeaders, }); } return null; } catch { return null; } } ================================================ FILE: api/_relay.js ================================================ import { getCorsHeaders, isDisallowedOrigin } from './_cors.js'; import { validateApiKey } from './_api-key.js'; import { checkRateLimit } from './_rate-limit.js'; import { jsonResponse } from './_json-response.js'; export function getRelayBaseUrl() { const relayUrl = process.env.WS_RELAY_URL; if (!relayUrl) return null; return relayUrl.replace('wss://', 'https://').replace('ws://', 'http://').replace(/\/$/, ''); } export function getRelayHeaders(baseHeaders = {}) { const headers = { ...baseHeaders }; const relaySecret = process.env.RELAY_SHARED_SECRET || ''; if (relaySecret) { const relayHeader = (process.env.RELAY_AUTH_HEADER || 'x-relay-key').toLowerCase(); headers[relayHeader] = relaySecret; headers.Authorization = `Bearer ${relaySecret}`; } return headers; } export async function fetchWithTimeout(url, options, timeoutMs = 15000) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), timeoutMs); try { return await fetch(url, { ...options, signal: controller.signal }); } finally { clearTimeout(timeout); } } /** Build the final relay response — wraps non-JSON errors in a JSON envelope * so the client can always parse the body (guards against Cloudflare HTML 502s). * Exported so that standalone handlers (e.g. telegram-feed.js) can reuse it. */ export function buildRelayResponse(response, body, headers) { const ct = (response.headers.get('content-type') || '').toLowerCase(); // Treat any JSON-compatible type as JSON: application/json, application/problem+json, // application/vnd.api+json, application/ld+json, etc. const isNonJsonError = !response.ok && !ct.includes('/json') && !ct.includes('+json'); if (isNonJsonError) { console.warn(`[relay] Wrapping non-JSON ${response.status} upstream error (ct: ${ct || 'none'}); body preview: ${String(body).slice(0, 120)}`); } return new Response( isNonJsonError ? JSON.stringify({ error: `Upstream error: HTTP ${response.status}`, status: response.status }) : body, { status: response.status, headers: { 'Content-Type': isNonJsonError ? 'application/json' : (response.headers.get('content-type') || 'application/json'), ...headers, }, }, ); } export function createRelayHandler(cfg) { return async function handler(req) { const corsHeaders = getCorsHeaders(req, 'GET, OPTIONS'); if (isDisallowedOrigin(req)) { return jsonResponse({ error: 'Origin not allowed' }, 403, corsHeaders); } if (req.method === 'OPTIONS') { return new Response(null, { status: 204, headers: corsHeaders }); } if (req.method !== 'GET') { return jsonResponse({ error: 'Method not allowed' }, 405, corsHeaders); } if (cfg.requireApiKey) { const keyCheck = validateApiKey(req); if (keyCheck.required && !keyCheck.valid) { return jsonResponse({ error: keyCheck.error }, 401, corsHeaders); } } if (cfg.requireRateLimit) { const rateLimitResponse = await checkRateLimit(req, corsHeaders); if (rateLimitResponse) return rateLimitResponse; } const relayBaseUrl = getRelayBaseUrl(); if (!relayBaseUrl) { if (cfg.fallback) return cfg.fallback(req, corsHeaders); return jsonResponse({ error: 'WS_RELAY_URL is not configured' }, 503, corsHeaders); } try { const requestUrl = new URL(req.url); const path = typeof cfg.buildRelayPath === 'function' ? cfg.buildRelayPath(req, requestUrl) : cfg.relayPath; const search = cfg.forwardSearch !== false ? (requestUrl.search || '') : ''; const relayUrl = `${relayBaseUrl}${path}${search}`; const reqHeaders = cfg.requestHeaders || { Accept: 'application/json' }; const response = await fetchWithTimeout(relayUrl, { headers: getRelayHeaders(reqHeaders), }, cfg.timeout || 15000); if (cfg.onlyOk && !response.ok && cfg.fallback) { return cfg.fallback(req, corsHeaders); } const extraHeaders = cfg.extraHeaders ? cfg.extraHeaders(response) : {}; const body = await response.text(); const isSuccess = response.status >= 200 && response.status < 300; const cacheHeaders = cfg.cacheHeaders ? cfg.cacheHeaders(isSuccess) : {}; return buildRelayResponse(response, body, { ...cacheHeaders, ...extraHeaders, ...corsHeaders }); } catch (error) { if (cfg.fallback) return cfg.fallback(req, corsHeaders); const isTimeout = error?.name === 'AbortError'; return jsonResponse({ error: isTimeout ? 'Relay timeout' : 'Relay request failed', details: error?.message || String(error), }, isTimeout ? 504 : 502, corsHeaders); } }; } ================================================ FILE: api/_rss-allowed-domains.js ================================================ // Edge-compatible ESM wrapper for shared RSS allowed domains. // Source of truth: shared/rss-allowed-domains.json // NOTE: Cannot use `import ... with { type: "json" }` — Vercel esbuild doesn't support import attributes. export default [ "feeds.bbci.co.uk", "www.theguardian.com", "feeds.npr.org", "news.google.com", "www.aljazeera.com", "www.aljazeera.net", "rss.cnn.com", "hnrss.org", "feeds.arstechnica.com", "www.theverge.com", "www.cnbc.com", "feeds.marketwatch.com", "www.defenseone.com", "www.bellingcat.com", "techcrunch.com", "huggingface.co", "www.technologyreview.com", "rss.arxiv.org", "export.arxiv.org", "www.federalreserve.gov", "www.sec.gov", "www.whitehouse.gov", "www.state.gov", "www.defense.gov", "home.treasury.gov", "www.justice.gov", "tools.cdc.gov", "www.fema.gov", "www.dhs.gov", "www.thedrive.com", "krebsonsecurity.com", "finance.yahoo.com", "thediplomat.com", "venturebeat.com", "foreignpolicy.com", "www.ft.com", "openai.com", "www.reutersagency.com", "feeds.reuters.com", "rsshub.app", "asia.nikkei.com", "www.cfr.org", "www.csis.org", "www.politico.com", "www.brookings.edu", "layoffs.fyi", "www.defensenews.com", "www.militarytimes.com", "taskandpurpose.com", "news.usni.org", "www.oryxspioenkop.com", "www.gov.uk", "www.foreignaffairs.com", "www.atlanticcouncil.org", "www.zdnet.com", "www.techmeme.com", "www.darkreading.com", "www.schneier.com", "www.ransomware.live", "rss.politico.com", "www.anandtech.com", "www.tomshardware.com", "www.semianalysis.com", "feed.infoq.com", "thenewstack.io", "devops.com", "dev.to", "lobste.rs", "changelog.com", "seekingalpha.com", "news.crunchbase.com", "www.saastr.com", "feeds.feedburner.com", "www.producthunt.com", "www.axios.com", "api.axios.com", "github.blog", "githubnext.com", "mshibanami.github.io", "www.engadget.com", "news.mit.edu", "dev.events", "www.ycombinator.com", "a16z.com", "www.a16z.news", "review.firstround.com", "www.sequoiacap.com", "www.nfx.com", "www.aaronsw.com", "bothsidesofthetable.com", "www.lennysnewsletter.com", "stratechery.com", "www.eu-startups.com", "tech.eu", "sifted.eu", "www.techinasia.com", "kr-asia.com", "techcabal.com", "disrupt-africa.com", "lavca.org", "contxto.com", "inc42.com", "yourstory.com", "pitchbook.com", "www.cbinsights.com", "www.techstars.com", "asharqbusiness.com", "asharq.com", "www.omanobserver.om", "english.alarabiya.net", "www.timesofisrael.com", "www.haaretz.com", "www.scmp.com", "kyivindependent.com", "www.themoscowtimes.com", "feeds.24.com", "feeds.news24.com", "feeds.capi24.com", "www.france24.com", "www.euronews.com", "de.euronews.com", "es.euronews.com", "fr.euronews.com", "it.euronews.com", "pt.euronews.com", "ru.euronews.com", "gr.euronews.com", "www.lemonde.fr", "rss.dw.com", "www.bild.de", "www.africanews.com", "fr.africanews.com", "www.premiumtimesng.com", "www.vanguardngr.com", "www.channelstv.com", "dailytrust.com", "www.thisdaylive.com", "www.naftemporiki.gr", "www.in.gr", "www.iefimerida.gr", "www.lasillavacia.com", "www.channelnewsasia.com", "japantoday.com", "www.thehindu.com", "indianexpress.com", "www.twz.com", "gcaptain.com", "news.un.org", "www.iaea.org", "www.who.int", "www.cisa.gov", "www.crisisgroup.org", "rusi.org", "warontherocks.com", "responsiblestatecraft.org", "www.fpri.org", "jamestown.org", "www.chathamhouse.org", "ecfr.eu", "www.gmfus.org", "www.wilsoncenter.org", "www.lowyinstitute.org", "www.mei.edu", "www.stimson.org", "www.cnas.org", "carnegieendowment.org", "www.rand.org", "fas.org", "www.armscontrol.org", "www.nti.org", "thebulletin.org", "www.iss.europa.eu", "www.fao.org", "worldbank.org", "www.imf.org", "www.bbc.com", "www.spiegel.de", "www.tagesschau.de", "newsfeed.zeit.de", "feeds.elpais.com", "e00-elmundo.uecdn.es", "www.repubblica.it", "www.ansa.it", "xml2.corriereobjects.it", "feeds.nos.nl", "www.nrc.nl", "www.telegraaf.nl", "www.dn.se", "www.svd.se", "www.svt.se", "www.asahi.com", "www.clarin.com", "oglobo.globo.com", "feeds.folha.uol.com.br", "www.eltiempo.com", "www.eluniversal.com.mx", "www.jeuneafrique.com", "www.lorientlejour.com", "www.hurriyet.com.tr", "tvn24.pl", "www.polsatnews.pl", "www.rp.pl", "meduza.io", "novayagazeta.eu", "www.bangkokpost.com", "vnexpress.net", "www.abc.net.au", "islandtimes.org", "www.brasilparalelo.com.br", "mexiconewsdaily.com", "insightcrime.org", "www.primicias.ec", "www.infobae.com", "www.eluniverso.com", "news.ycombinator.com", "www.coindesk.com", "cointelegraph.com", "travel.state.gov", "www.safetravel.govt.nz", "th.usembassy.gov", "ae.usembassy.gov", "de.usembassy.gov", "ua.usembassy.gov", "mx.usembassy.gov", "in.usembassy.gov", "pk.usembassy.gov", "co.usembassy.gov", "pl.usembassy.gov", "bd.usembassy.gov", "it.usembassy.gov", "do.usembassy.gov", "mm.usembassy.gov", "wwwnc.cdc.gov", "www.ecdc.europa.eu", "www.afro.who.int", "www.goodnewsnetwork.org", "www.positive.news", "reasonstobecheerful.world", "www.optimistdaily.com", "www.upworthy.com", "www.dailygood.org", "www.goodgoodgood.co", "www.good.is", "www.sunnyskyz.com", "thebetterindia.com", "singularityhub.com", "humanprogress.org", "greatergood.berkeley.edu", "www.onlygoodnewsdaily.com", "news.mongabay.com", "conservationoptimism.org", "www.shareable.net", "www.yesmagazine.org", "www.sciencedaily.com", "feeds.nature.com", "www.nature.com", "www.livescience.com", "www.newscientist.com", "www.pbs.org", "feeds.abcnews.com", "feeds.nbcnews.com", "www.cbsnews.com", "moxie.foxnews.com", "feeds.content.dowjones.io", "thehill.com", "www.flightglobal.com", "simpleflying.com", "aerotime.aero", "thepointsguy.com", "airlinegeeks.com", "onemileatatime.com", "viewfromthewing.com", "www.aviationpros.com", "www.aviationweek.com", "www.kitco.com", "www.mining.com", "www.commoditytrademantra.com", "oilprice.com", "www.rigzone.com", "www.eia.gov", "www.mining-journal.com", "www.northernminer.com", "www.miningweekly.com", "www.mining-technology.com", "www.australianmining.com.au", "news.goldseek.com", "news.silverseek.com" ]; ================================================ FILE: api/_turnstile.js ================================================ const TURNSTILE_VERIFY_URL = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'; export function getClientIp(request) { // Prefer platform-populated IP headers before falling back to x-forwarded-for. return ( request.headers.get('x-real-ip') || request.headers.get('cf-connecting-ip') || request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || 'unknown' ); } export async function verifyTurnstile({ token, ip, logPrefix = '[turnstile]', missingSecretPolicy = 'allow', }) { const secret = process.env.TURNSTILE_SECRET_KEY; if (!secret) { if (missingSecretPolicy === 'allow') return true; const isDevelopment = (process.env.VERCEL_ENV ?? 'development') === 'development'; if (isDevelopment) return true; console.error(`${logPrefix} TURNSTILE_SECRET_KEY not set in production, rejecting`); return false; } try { const res = await fetch(TURNSTILE_VERIFY_URL, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ secret, response: token, remoteip: ip }), }); const data = await res.json(); return data.success === true; } catch { return false; } } ================================================ FILE: api/_turnstile.test.mjs ================================================ import assert from 'node:assert/strict'; import test from 'node:test'; import { getClientIp, verifyTurnstile } from './_turnstile.js'; const originalFetch = globalThis.fetch; const originalEnv = { ...process.env }; const originalConsoleError = console.error; function restoreEnv() { Object.keys(process.env).forEach((key) => { if (!(key in originalEnv)) delete process.env[key]; }); Object.assign(process.env, originalEnv); } test.afterEach(() => { globalThis.fetch = originalFetch; console.error = originalConsoleError; restoreEnv(); }); test('getClientIp prefers x-real-ip, then cf-connecting-ip, then x-forwarded-for', () => { const request = new Request('https://worldmonitor.app/api/test', { headers: { 'x-forwarded-for': '198.51.100.8, 203.0.113.10', 'cf-connecting-ip': '203.0.113.7', 'x-real-ip': '192.0.2.5', }, }); assert.equal(getClientIp(request), '192.0.2.5'); }); test('verifyTurnstile allows missing secret when policy is allow', async () => { delete process.env.TURNSTILE_SECRET_KEY; process.env.VERCEL_ENV = 'production'; const ok = await verifyTurnstile({ token: 'token', ip: '192.0.2.1', missingSecretPolicy: 'allow', }); assert.equal(ok, true); }); test('verifyTurnstile rejects missing secret in production when policy is allow-in-development', async () => { delete process.env.TURNSTILE_SECRET_KEY; process.env.VERCEL_ENV = 'production'; console.error = () => {}; const ok = await verifyTurnstile({ token: 'token', ip: '192.0.2.1', logPrefix: '[test]', missingSecretPolicy: 'allow-in-development', }); assert.equal(ok, false); }); test('verifyTurnstile posts to Cloudflare and returns success state', async () => { process.env.TURNSTILE_SECRET_KEY = 'test-secret'; let requestBody; globalThis.fetch = async (_url, options) => { requestBody = options.body; return new Response(JSON.stringify({ success: true })); }; const ok = await verifyTurnstile({ token: 'valid-token', ip: '203.0.113.15', }); assert.equal(ok, true); assert.equal(requestBody.get('secret'), 'test-secret'); assert.equal(requestBody.get('response'), 'valid-token'); assert.equal(requestBody.get('remoteip'), '203.0.113.15'); }); ================================================ FILE: api/_upstash-json.js ================================================ export async function readJsonFromUpstash(key, timeoutMs = 3_000) { const url = process.env.UPSTASH_REDIS_REST_URL; const token = process.env.UPSTASH_REDIS_REST_TOKEN; if (!url || !token) return null; const resp = await fetch(`${url}/get/${encodeURIComponent(key)}`, { headers: { Authorization: `Bearer ${token}` }, signal: AbortSignal.timeout(timeoutMs), }); if (!resp.ok) return null; const data = await resp.json(); if (!data.result) return null; try { return JSON.parse(data.result); } catch { return null; } } ================================================ FILE: api/ais-snapshot.js ================================================ import { createRelayHandler } from './_relay.js'; export const config = { runtime: 'edge' }; export default createRelayHandler({ relayPath: '/ais/snapshot', timeout: 12000, requireApiKey: true, requireRateLimit: true, cacheHeaders: (ok) => ({ 'Cache-Control': ok ? 'public, max-age=60, s-maxage=300, stale-while-revalidate=600, stale-if-error=900' : 'public, max-age=10, s-maxage=30, stale-while-revalidate=120', ...(ok && { 'CDN-Cache-Control': 'public, s-maxage=300, stale-while-revalidate=600, stale-if-error=900' }), }), }); ================================================ FILE: api/aviation/v1/[rpc].ts ================================================ export const config = { runtime: 'edge' }; import { createDomainGateway, serverOptions } from '../../../server/gateway'; import { createAviationServiceRoutes } from '../../../src/generated/server/worldmonitor/aviation/v1/service_server'; import { aviationHandler } from '../../../server/worldmonitor/aviation/v1/handler'; export default createDomainGateway( createAviationServiceRoutes(aviationHandler, serverOptions), ); ================================================ FILE: api/bootstrap.js ================================================ import { getCorsHeaders, getPublicCorsHeaders, isDisallowedOrigin } from './_cors.js'; import { validateApiKey } from './_api-key.js'; import { jsonResponse } from './_json-response.js'; export const config = { runtime: 'edge' }; const BOOTSTRAP_CACHE_KEYS = { earthquakes: 'seismology:earthquakes:v1', outages: 'infra:outages:v1', serviceStatuses: 'infra:service-statuses:v1', marketQuotes: 'market:stocks-bootstrap:v1', commodityQuotes: 'market:commodities-bootstrap:v1', sectors: 'market:sectors:v1', etfFlows: 'market:etf-flows:v1', macroSignals: 'economic:macro-signals:v1', bisPolicy: 'economic:bis:policy:v1', bisExchange: 'economic:bis:eer:v1', bisCredit: 'economic:bis:credit:v1', shippingRates: 'supply_chain:shipping:v2', chokepoints: 'supply_chain:chokepoints:v4', chokepointTransits: 'supply_chain:chokepoint_transits:v1', minerals: 'supply_chain:minerals:v2', giving: 'giving:summary:v1', climateAnomalies: 'climate:anomalies:v1', radiationWatch: 'radiation:observations:v1', thermalEscalation: 'thermal:escalation:v1', wildfires: 'wildfire:fires:v1', cyberThreats: 'cyber:threats-bootstrap:v2', techReadiness: 'economic:worldbank-techreadiness:v1', progressData: 'economic:worldbank-progress:v1', renewableEnergy: 'economic:worldbank-renewable:v1', positiveGeoEvents: 'positive_events:geo-bootstrap:v1', theaterPosture: 'theater_posture:sebuf:stale:v1', riskScores: 'risk:scores:sebuf:stale:v1', naturalEvents: 'natural:events:v1', flightDelays: 'aviation:delays-bootstrap:v1', insights: 'news:insights:v1', predictions: 'prediction:markets-bootstrap:v1', cryptoQuotes: 'market:crypto:v1', gulfQuotes: 'market:gulf-quotes:v1', stablecoinMarkets: 'market:stablecoins:v1', unrestEvents: 'unrest:events:v1', iranEvents: 'conflict:iran-events:v1', ucdpEvents: 'conflict:ucdp-events:v1', temporalAnomalies: 'temporal:anomalies:v1', weatherAlerts: 'weather:alerts:v1', spending: 'economic:spending:v1', techEvents: 'research:tech-events-bootstrap:v1', gdeltIntel: 'intelligence:gdelt-intel:v1', correlationCards: 'correlation:cards-bootstrap:v1', forecasts: 'forecast:predictions:v2', securityAdvisories: 'intelligence:advisories-bootstrap:v1', customsRevenue: 'trade:customs-revenue:v1', sanctionsPressure: 'sanctions:pressure:v1', }; const SLOW_KEYS = new Set([ 'bisPolicy', 'bisExchange', 'bisCredit', 'minerals', 'giving', 'sectors', 'etfFlows', 'wildfires', 'climateAnomalies', 'radiationWatch', 'thermalEscalation', 'cyberThreats', 'techReadiness', 'progressData', 'renewableEnergy', 'naturalEvents', 'cryptoQuotes', 'gulfQuotes', 'stablecoinMarkets', 'unrestEvents', 'ucdpEvents', 'techEvents', 'securityAdvisories', 'customsRevenue', 'sanctionsPressure', ]); const FAST_KEYS = new Set([ 'earthquakes', 'outages', 'serviceStatuses', 'macroSignals', 'chokepoints', 'chokepointTransits', 'marketQuotes', 'commodityQuotes', 'positiveGeoEvents', 'riskScores', 'flightDelays','insights', 'predictions', 'iranEvents', 'temporalAnomalies', 'weatherAlerts', 'spending', 'theaterPosture', 'gdeltIntel', 'correlationCards', 'forecasts', 'shippingRates', ]); // No public/s-maxage: CF (in front of api.worldmonitor.app) ignores Vary: Origin and would // pin ACAO: worldmonitor.app on cached responses, breaking CORS for preview deployments. // Vercel CDN caching is handled by TIER_CDN_CACHE via CDN-Cache-Control below. const TIER_CACHE = { slow: 'max-age=300, stale-while-revalidate=600, stale-if-error=3600', fast: 'max-age=60, stale-while-revalidate=120, stale-if-error=900', }; const TIER_CDN_CACHE = { slow: 'public, s-maxage=7200, stale-while-revalidate=1800, stale-if-error=7200', fast: 'public, s-maxage=600, stale-while-revalidate=120, stale-if-error=900', }; const NEG_SENTINEL = '__WM_NEG__'; async function getCachedJsonBatch(keys) { const result = new Map(); if (keys.length === 0) return result; const url = process.env.UPSTASH_REDIS_REST_URL; const token = process.env.UPSTASH_REDIS_REST_TOKEN; if (!url || !token) return result; // Always read unprefixed keys — bootstrap is a read-only consumer of // production cache data. Preview/branch deploys don't run handlers that // populate prefixed keys, so prefixing would always miss. const pipeline = keys.map((k) => ['GET', k]); const resp = await fetch(`${url}/pipeline`, { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify(pipeline), signal: AbortSignal.timeout(3000), }); if (!resp.ok) return result; const data = await resp.json(); for (let i = 0; i < keys.length; i++) { const raw = data[i]?.result; if (raw) { try { const parsed = JSON.parse(raw); if (parsed !== NEG_SENTINEL) result.set(keys[i], parsed); } catch { /* skip malformed */ } } } return result; } export default async function handler(req) { if (isDisallowedOrigin(req)) return new Response('Forbidden', { status: 403 }); const cors = getCorsHeaders(req); if (req.method === 'OPTIONS') return new Response(null, { status: 204, headers: cors }); const apiKeyResult = validateApiKey(req); if (apiKeyResult.required && !apiKeyResult.valid) return jsonResponse({ error: apiKeyResult.error }, 401, cors); const url = new URL(req.url); const tier = url.searchParams.get('tier'); let registry; if (tier === 'slow' || tier === 'fast') { const tierSet = tier === 'slow' ? SLOW_KEYS : FAST_KEYS; registry = Object.fromEntries(Object.entries(BOOTSTRAP_CACHE_KEYS).filter(([k]) => tierSet.has(k))); } else { const requested = url.searchParams.get('keys')?.split(',').filter(Boolean).sort(); registry = requested ? Object.fromEntries(Object.entries(BOOTSTRAP_CACHE_KEYS).filter(([k]) => requested.includes(k))) : BOOTSTRAP_CACHE_KEYS; } const keys = Object.values(registry); const names = Object.keys(registry); let cached; try { cached = await getCachedJsonBatch(keys); } catch { return jsonResponse({ data: {}, missing: names }, 200, { ...cors, 'Cache-Control': 'no-cache' }); } const data = {}; const missing = []; for (let i = 0; i < names.length; i++) { const val = cached.get(keys[i]); if (val !== undefined) { // Strip seed-internal metadata not intended for API clients if (names[i] === 'forecasts' && val != null && 'enrichmentMeta' in val) { const { enrichmentMeta: _stripped, ...rest } = val; data[names[i]] = rest; } else { data[names[i]] = val; } } else { missing.push(names[i]); } } const cacheControl = (tier && TIER_CACHE[tier]) || 'public, s-maxage=600, stale-while-revalidate=120, stale-if-error=900'; // Bootstrap data is fully public (world events, market prices, seismic data). // Use ACAO: * so CF caches one entry valid for all origins, including Vercel // preview deployments. Per-origin ACAO with Vary: Origin causes CF to pin the // first origin's ACAO on the cached response, breaking CORS for other origins. return jsonResponse({ data, missing }, 200, { ...getPublicCorsHeaders(), 'Cache-Control': cacheControl, 'CDN-Cache-Control': (tier && TIER_CDN_CACHE[tier]) || TIER_CDN_CACHE.fast, }); } ================================================ FILE: api/cache-purge.js ================================================ import { getCorsHeaders } from './_cors.js'; import { jsonResponse } from './_json-response.js'; export const config = { runtime: 'edge' }; const MAX_EXPLICIT_KEYS = 20; const MAX_PATTERNS = 3; const MAX_DELETIONS = 200; const MAX_SCAN_ITERATIONS = 5; const BLOCKLIST_PREFIXES = ['rl:', '__']; const DURABLE_DATA_PREFIXES = ['military:bases:', 'conflict:iran-events:', 'conflict:ucdp-events:']; function getKeyPrefix() { const env = process.env.VERCEL_ENV; if (!env || env === 'production') return ''; const sha = process.env.VERCEL_GIT_COMMIT_SHA?.slice(0, 8) || 'dev'; return `${env}:${sha}:`; } function isBlocklisted(key) { return BLOCKLIST_PREFIXES.some(p => key.startsWith(p)); } function isDurableData(key) { return DURABLE_DATA_PREFIXES.some(p => key.startsWith(p)); } function getRedisCredentials() { const url = process.env.UPSTASH_REDIS_REST_URL; const token = process.env.UPSTASH_REDIS_REST_TOKEN; if (!url || !token) throw new Error('Redis not configured'); return { url, token }; } async function redisPipeline(commands) { const { url, token } = getRedisCredentials(); const resp = await fetch(`${url}/pipeline`, { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }, body: JSON.stringify(commands), signal: AbortSignal.timeout(10_000), }); if (!resp.ok) throw new Error(`Redis pipeline HTTP ${resp.status}`); return resp.json(); } async function redisScan(pattern, maxIterations) { const { url, token } = getRedisCredentials(); const keys = []; let cursor = '0'; let truncated = false; for (let i = 0; i < maxIterations; i++) { const resp = await fetch( `${url}/scan/${encodeURIComponent(cursor)}/MATCH/${encodeURIComponent(pattern)}/COUNT/100`, { headers: { Authorization: `Bearer ${token}` }, signal: AbortSignal.timeout(5_000), } ); if (!resp.ok) throw new Error(`Redis SCAN HTTP ${resp.status}`); const data = await resp.json(); const [nextCursor, batch] = data.result; if (batch?.length) keys.push(...batch); cursor = String(nextCursor); if (cursor === '0') break; if (i === maxIterations - 1) truncated = true; } return { keys, truncated }; } async function timingSafeEqual(a, b) { const encoder = new TextEncoder(); const aBuf = encoder.encode(a); const bBuf = encoder.encode(b); if (aBuf.byteLength !== bBuf.byteLength) return false; const key = await crypto.subtle.importKey('raw', aBuf, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']); const sig = await crypto.subtle.sign('HMAC', key, bBuf); const expected = await crypto.subtle.sign('HMAC', key, aBuf); const sigArr = new Uint8Array(sig); const expArr = new Uint8Array(expected); if (sigArr.length !== expArr.length) return false; let diff = 0; for (let i = 0; i < sigArr.length; i++) diff |= sigArr[i] ^ expArr[i]; return diff === 0; } export default async function handler(req) { const corsHeaders = getCorsHeaders(req, 'POST, OPTIONS'); if (req.method === 'OPTIONS') { return new Response(null, { status: 204, headers: corsHeaders }); } if (req.method !== 'POST') { return jsonResponse({ error: 'Method not allowed' }, 405, corsHeaders); } const auth = req.headers.get('authorization') || ''; const secret = process.env.RELAY_SHARED_SECRET; if (!secret || !(await timingSafeEqual(auth, `Bearer ${secret}`))) { return jsonResponse({ error: 'Unauthorized' }, 401, corsHeaders); } let body; try { body = await req.json(); } catch { return jsonResponse({ error: 'Invalid JSON body' }, 422, corsHeaders); } const { keys: explicitKeys, patterns, dryRun = false } = body || {}; const hasKeys = Array.isArray(explicitKeys) && explicitKeys.length > 0; const hasPatterns = Array.isArray(patterns) && patterns.length > 0; if (!hasKeys && !hasPatterns) { return jsonResponse({ error: 'At least one of "keys" or "patterns" required' }, 422, corsHeaders); } if (hasKeys && explicitKeys.length > MAX_EXPLICIT_KEYS) { return jsonResponse({ error: `"keys" exceeds max of ${MAX_EXPLICIT_KEYS}` }, 422, corsHeaders); } if (hasPatterns && patterns.length > MAX_PATTERNS) { return jsonResponse({ error: `"patterns" exceeds max of ${MAX_PATTERNS}` }, 422, corsHeaders); } if (hasPatterns) { for (const p of patterns) { if (typeof p !== 'string' || !p.endsWith('*') || p === '*') { return jsonResponse({ error: `Invalid pattern "${p}": must end with "*" and cannot be bare "*"` }, 422, corsHeaders); } } } const prefix = getKeyPrefix(); const allKeys = new Set(); let truncated = false; if (hasKeys) { for (const k of explicitKeys) { if (typeof k !== 'string' || !k) continue; if (isBlocklisted(k)) continue; allKeys.add(k); } } if (hasPatterns) { for (const p of patterns) { const prefixedPattern = prefix ? `${prefix}${p}` : p; const scan = await redisScan(prefixedPattern, MAX_SCAN_ITERATIONS); if (scan.truncated) truncated = true; for (const rawKey of scan.keys) { const unprefixed = prefix && rawKey.startsWith(prefix) ? rawKey.slice(prefix.length) : rawKey; if (isBlocklisted(unprefixed)) continue; if (isDurableData(unprefixed)) continue; allKeys.add(unprefixed); } } } const keyList = [...allKeys].slice(0, MAX_DELETIONS); if (keyList.length < allKeys.size) truncated = true; const ip = req.headers.get('x-real-ip') || req.headers.get('cf-connecting-ip') || 'unknown'; const ts = new Date().toISOString(); if (dryRun) { console.log('[cache-purge]', { mode: 'dry-run', matched: keyList.length, deleted: 0, truncated, dryRun: true, ip, ts }); return jsonResponse({ matched: keyList.length, deleted: 0, keys: keyList, dryRun: true, truncated }, 200, corsHeaders); } if (keyList.length === 0) { console.log('[cache-purge]', { mode: 'purge', matched: 0, deleted: 0, truncated, dryRun: false, ip, ts }); return jsonResponse({ matched: 0, deleted: 0, keys: [], dryRun: false, truncated }, 200, corsHeaders); } let deleted = 0; try { const commands = keyList.map(k => ['DEL', prefix ? `${prefix}${k}` : k]); const results = await redisPipeline(commands); deleted = results.reduce((sum, r) => sum + (r.result || 0), 0); } catch (err) { console.log('[cache-purge]', { mode: 'purge-error', matched: keyList.length, error: err.message, ip, ts }); return jsonResponse({ error: 'Redis pipeline failed' }, 502, corsHeaders); } console.log('[cache-purge]', { mode: 'purge', matched: keyList.length, deleted, truncated, dryRun: false, ip, ts }); return jsonResponse({ matched: keyList.length, deleted, keys: keyList, dryRun: false, truncated }, 200, corsHeaders); } ================================================ FILE: api/climate/v1/[rpc].ts ================================================ export const config = { runtime: 'edge' }; import { createDomainGateway, serverOptions } from '../../../server/gateway'; import { createClimateServiceRoutes } from '../../../src/generated/server/worldmonitor/climate/v1/service_server'; import { climateHandler } from '../../../server/worldmonitor/climate/v1/handler'; export default createDomainGateway( createClimateServiceRoutes(climateHandler, serverOptions), ); ================================================ FILE: api/conflict/v1/[rpc].ts ================================================ export const config = { runtime: 'edge' }; import { createDomainGateway, serverOptions } from '../../../server/gateway'; import { createConflictServiceRoutes } from '../../../src/generated/server/worldmonitor/conflict/v1/service_server'; import { conflictHandler } from '../../../server/worldmonitor/conflict/v1/handler'; export default createDomainGateway( createConflictServiceRoutes(conflictHandler, serverOptions), ); ================================================ FILE: api/contact.js ================================================ export const config = { runtime: 'edge' }; import { ConvexHttpClient } from 'convex/browser'; import { getCorsHeaders, isDisallowedOrigin } from './_cors.js'; import { getClientIp, verifyTurnstile } from './_turnstile.js'; import { jsonResponse } from './_json-response.js'; import { createIpRateLimiter } from './_ip-rate-limit.js'; const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const PHONE_RE = /^[+(]?\d[\d\s()./-]{4,23}\d$/; const MAX_FIELD = 500; const MAX_MESSAGE = 2000; const FREE_EMAIL_DOMAINS = new Set([ 'gmail.com', 'googlemail.com', 'yahoo.com', 'yahoo.fr', 'yahoo.co.uk', 'yahoo.co.jp', 'hotmail.com', 'hotmail.fr', 'hotmail.co.uk', 'outlook.com', 'outlook.fr', 'live.com', 'live.fr', 'msn.com', 'aol.com', 'icloud.com', 'me.com', 'mac.com', 'protonmail.com', 'proton.me', 'mail.com', 'zoho.com', 'yandex.com', 'yandex.ru', 'gmx.com', 'gmx.net', 'gmx.de', 'web.de', 'mail.ru', 'inbox.com', 'fastmail.com', 'tutanota.com', 'tuta.io', 'hey.com', 'qq.com', '163.com', '126.com', 'sina.com', 'foxmail.com', 'rediffmail.com', 'ymail.com', 'rocketmail.com', 'wanadoo.fr', 'free.fr', 'laposte.net', 'orange.fr', 'sfr.fr', 't-online.de', 'libero.it', 'virgilio.it', ]); const RATE_LIMIT = 3; const RATE_WINDOW_MS = 60 * 60 * 1000; const rateLimiter = createIpRateLimiter({ limit: RATE_LIMIT, windowMs: RATE_WINDOW_MS }); async function sendNotificationEmail(name, email, organization, phone, message) { const resendKey = process.env.RESEND_API_KEY; if (!resendKey) { console.error('[contact] RESEND_API_KEY not set — lead stored in Convex but notification NOT sent'); return false; } const notifyEmail = process.env.CONTACT_NOTIFY_EMAIL || 'sales@worldmonitor.app'; const emailDomain = (email.split('@')[1] || '').toLowerCase(); try { const res = await fetch('https://api.resend.com/emails', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${resendKey}`, }, body: JSON.stringify({ from: 'World Monitor ', to: [notifyEmail], subject: `[WM Enterprise] ${sanitizeForSubject(name)} from ${sanitizeForSubject(organization)}`, html: `

New Enterprise Contact

Name${escapeHtml(name)}
Email${escapeHtml(email)}
Domain${escapeHtml(emailDomain)}
Company${escapeHtml(organization)}
Phone${escapeHtml(phone)}
Message${escapeHtml(message || 'N/A')}

Sent from worldmonitor.app enterprise contact form

`, }), }); if (!res.ok) { const body = await res.text(); console.error(`[contact] Resend ${res.status}:`, body); return false; } return true; } catch (err) { console.error('[contact] Resend error:', err); return false; } } function escapeHtml(str) { return str .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function sanitizeForSubject(str, maxLen = 50) { return str.replace(/[\r\n\0]/g, '').slice(0, maxLen); } export default async function handler(req) { if (isDisallowedOrigin(req)) { return jsonResponse({ error: 'Origin not allowed' }, 403); } const cors = getCorsHeaders(req, 'POST, OPTIONS'); if (req.method === 'OPTIONS') { return new Response(null, { status: 204, headers: cors }); } if (req.method !== 'POST') { return jsonResponse({ error: 'Method not allowed' }, 405, cors); } const ip = getClientIp(req); if (rateLimiter.isRateLimited(ip)) { return jsonResponse({ error: 'Too many requests' }, 429, cors); } let body; try { body = await req.json(); } catch { return jsonResponse({ error: 'Invalid JSON' }, 400, cors); } if (body.website) { return jsonResponse({ status: 'sent' }, 200, cors); } const turnstileOk = await verifyTurnstile({ token: body.turnstileToken || '', ip, logPrefix: '[contact]', missingSecretPolicy: 'allow-in-development', }); if (!turnstileOk) { return jsonResponse({ error: 'Bot verification failed' }, 403, cors); } const { email, name, organization, phone, message, source } = body; if (!email || typeof email !== 'string' || !EMAIL_RE.test(email)) { return jsonResponse({ error: 'Invalid email' }, 400, cors); } const emailDomain = email.split('@')[1]?.toLowerCase(); if (emailDomain && FREE_EMAIL_DOMAINS.has(emailDomain)) { return jsonResponse({ error: 'Please use your work email address' }, 422, cors); } if (!name || typeof name !== 'string' || name.trim().length === 0) { return jsonResponse({ error: 'Name is required' }, 400, cors); } if (!organization || typeof organization !== 'string' || organization.trim().length === 0) { return jsonResponse({ error: 'Company is required' }, 400, cors); } if (!phone || typeof phone !== 'string' || !PHONE_RE.test(phone.trim())) { return jsonResponse({ error: 'Valid phone number is required' }, 400, cors); } const safeName = name.slice(0, MAX_FIELD); const safeOrg = organization.slice(0, MAX_FIELD); const safePhone = phone.trim().slice(0, 30); const safeMsg = typeof message === 'string' ? message.slice(0, MAX_MESSAGE) : undefined; const safeSource = typeof source === 'string' ? source.slice(0, 100) : 'enterprise-contact'; const convexUrl = process.env.CONVEX_URL; if (!convexUrl) { return jsonResponse({ error: 'Service unavailable' }, 503, cors); } try { const client = new ConvexHttpClient(convexUrl); await client.mutation('contactMessages:submit', { name: safeName, email: email.trim(), organization: safeOrg, phone: safePhone, message: safeMsg, source: safeSource, }); const emailSent = await sendNotificationEmail(safeName, email.trim(), safeOrg, safePhone, safeMsg); return jsonResponse({ status: 'sent', emailSent }, 200, cors); } catch (err) { console.error('[contact] error:', err); return jsonResponse({ error: 'Failed to send message' }, 500, cors); } } ================================================ FILE: api/cyber/v1/[rpc].ts ================================================ export const config = { runtime: 'edge' }; import { createDomainGateway, serverOptions } from '../../../server/gateway'; import { createCyberServiceRoutes } from '../../../src/generated/server/worldmonitor/cyber/v1/service_server'; import { cyberHandler } from '../../../server/worldmonitor/cyber/v1/handler'; export default createDomainGateway( createCyberServiceRoutes(cyberHandler, serverOptions), ); ================================================ FILE: api/data/city-coords.ts ================================================ /** * Comprehensive city geocoding database (500+ cities worldwide). * Extracted from the legacy api/tech-events.js endpoint. */ export interface CityCoord { lat: number; lng: number; country: string; virtual?: boolean; } export const CITY_COORDS: Record = { // North America - USA 'san francisco': { lat: 37.7749, lng: -122.4194, country: 'USA' }, 'san jose': { lat: 37.3382, lng: -121.8863, country: 'USA' }, 'palo alto': { lat: 37.4419, lng: -122.1430, country: 'USA' }, 'mountain view': { lat: 37.3861, lng: -122.0839, country: 'USA' }, 'menlo park': { lat: 37.4530, lng: -122.1817, country: 'USA' }, 'cupertino': { lat: 37.3230, lng: -122.0322, country: 'USA' }, 'sunnyvale': { lat: 37.3688, lng: -122.0363, country: 'USA' }, 'santa clara': { lat: 37.3541, lng: -121.9552, country: 'USA' }, 'redwood city': { lat: 37.4852, lng: -122.2364, country: 'USA' }, 'oakland': { lat: 37.8044, lng: -122.2712, country: 'USA' }, 'berkeley': { lat: 37.8716, lng: -122.2727, country: 'USA' }, 'los angeles': { lat: 34.0522, lng: -118.2437, country: 'USA' }, 'santa monica': { lat: 34.0195, lng: -118.4912, country: 'USA' }, 'pasadena': { lat: 34.1478, lng: -118.1445, country: 'USA' }, 'irvine': { lat: 33.6846, lng: -117.8265, country: 'USA' }, 'san diego': { lat: 32.7157, lng: -117.1611, country: 'USA' }, 'seattle': { lat: 47.6062, lng: -122.3321, country: 'USA' }, 'bellevue': { lat: 47.6101, lng: -122.2015, country: 'USA' }, 'redmond': { lat: 47.6740, lng: -122.1215, country: 'USA' }, 'portland': { lat: 45.5155, lng: -122.6789, country: 'USA' }, 'new york': { lat: 40.7128, lng: -74.0060, country: 'USA' }, 'nyc': { lat: 40.7128, lng: -74.0060, country: 'USA' }, 'manhattan': { lat: 40.7831, lng: -73.9712, country: 'USA' }, 'brooklyn': { lat: 40.6782, lng: -73.9442, country: 'USA' }, 'boston': { lat: 42.3601, lng: -71.0589, country: 'USA' }, 'cambridge': { lat: 42.3736, lng: -71.1097, country: 'USA' }, 'chicago': { lat: 41.8781, lng: -87.6298, country: 'USA' }, 'austin': { lat: 30.2672, lng: -97.7431, country: 'USA' }, 'austin, tx': { lat: 30.2672, lng: -97.7431, country: 'USA' }, 'dallas': { lat: 32.7767, lng: -96.7970, country: 'USA' }, 'houston': { lat: 29.7604, lng: -95.3698, country: 'USA' }, 'denver': { lat: 39.7392, lng: -104.9903, country: 'USA' }, 'boulder': { lat: 40.0150, lng: -105.2705, country: 'USA' }, 'phoenix': { lat: 33.4484, lng: -112.0740, country: 'USA' }, 'scottsdale': { lat: 33.4942, lng: -111.9261, country: 'USA' }, 'miami': { lat: 25.7617, lng: -80.1918, country: 'USA' }, 'orlando': { lat: 28.5383, lng: -81.3792, country: 'USA' }, 'tampa': { lat: 27.9506, lng: -82.4572, country: 'USA' }, 'atlanta': { lat: 33.7490, lng: -84.3880, country: 'USA' }, 'washington': { lat: 38.9072, lng: -77.0369, country: 'USA' }, 'washington dc': { lat: 38.9072, lng: -77.0369, country: 'USA' }, 'washington, dc': { lat: 38.9072, lng: -77.0369, country: 'USA' }, 'dc': { lat: 38.9072, lng: -77.0369, country: 'USA' }, 'reston': { lat: 38.9586, lng: -77.3570, country: 'USA' }, 'philadelphia': { lat: 39.9526, lng: -75.1652, country: 'USA' }, 'pittsburgh': { lat: 40.4406, lng: -79.9959, country: 'USA' }, 'detroit': { lat: 42.3314, lng: -83.0458, country: 'USA' }, 'ann arbor': { lat: 42.2808, lng: -83.7430, country: 'USA' }, 'minneapolis': { lat: 44.9778, lng: -93.2650, country: 'USA' }, 'salt lake city': { lat: 40.7608, lng: -111.8910, country: 'USA' }, 'las vegas': { lat: 36.1699, lng: -115.1398, country: 'USA' }, 'raleigh': { lat: 35.7796, lng: -78.6382, country: 'USA' }, 'durham': { lat: 35.9940, lng: -78.8986, country: 'USA' }, 'chapel hill': { lat: 35.9132, lng: -79.0558, country: 'USA' }, 'charlotte': { lat: 35.2271, lng: -80.8431, country: 'USA' }, 'nashville': { lat: 36.1627, lng: -86.7816, country: 'USA' }, 'indianapolis': { lat: 39.7684, lng: -86.1581, country: 'USA' }, 'columbus': { lat: 39.9612, lng: -82.9988, country: 'USA' }, 'cleveland': { lat: 41.4993, lng: -81.6944, country: 'USA' }, 'cincinnati': { lat: 39.1031, lng: -84.5120, country: 'USA' }, 'st. louis': { lat: 38.6270, lng: -90.1994, country: 'USA' }, 'kansas city': { lat: 39.0997, lng: -94.5786, country: 'USA' }, 'omaha': { lat: 41.2565, lng: -95.9345, country: 'USA' }, 'milwaukee': { lat: 43.0389, lng: -87.9065, country: 'USA' }, 'new orleans': { lat: 29.9511, lng: -90.0715, country: 'USA' }, 'san antonio': { lat: 29.4241, lng: -98.4936, country: 'USA' }, 'albuquerque': { lat: 35.0844, lng: -106.6504, country: 'USA' }, 'tucson': { lat: 32.2226, lng: -110.9747, country: 'USA' }, 'honolulu': { lat: 21.3069, lng: -157.8583, country: 'USA' }, 'anchorage': { lat: 61.2181, lng: -149.9003, country: 'USA' }, // North America - Canada 'toronto': { lat: 43.6532, lng: -79.3832, country: 'Canada' }, 'vancouver': { lat: 49.2827, lng: -123.1207, country: 'Canada' }, 'montreal': { lat: 45.5017, lng: -73.5673, country: 'Canada' }, 'ottawa': { lat: 45.4215, lng: -75.6972, country: 'Canada' }, 'calgary': { lat: 51.0447, lng: -114.0719, country: 'Canada' }, 'edmonton': { lat: 53.5461, lng: -113.4938, country: 'Canada' }, 'winnipeg': { lat: 49.8951, lng: -97.1384, country: 'Canada' }, 'quebec city': { lat: 46.8139, lng: -71.2080, country: 'Canada' }, 'waterloo': { lat: 43.4643, lng: -80.5204, country: 'Canada' }, 'victoria': { lat: 48.4284, lng: -123.3656, country: 'Canada' }, 'halifax': { lat: 44.6488, lng: -63.5752, country: 'Canada' }, // Mexico & Central America 'mexico city': { lat: 19.4326, lng: -99.1332, country: 'Mexico' }, 'guadalajara': { lat: 20.6597, lng: -103.3496, country: 'Mexico' }, 'monterrey': { lat: 25.6866, lng: -100.3161, country: 'Mexico' }, 'tijuana': { lat: 32.5149, lng: -117.0382, country: 'Mexico' }, 'cancun': { lat: 21.1619, lng: -86.8515, country: 'Mexico' }, 'panama city': { lat: 8.9824, lng: -79.5199, country: 'Panama' }, 'san jose cr': { lat: 9.9281, lng: -84.0907, country: 'Costa Rica' }, // South America 'sao paulo': { lat: -23.5505, lng: -46.6333, country: 'Brazil' }, 'são paulo': { lat: -23.5505, lng: -46.6333, country: 'Brazil' }, 'rio de janeiro': { lat: -22.9068, lng: -43.1729, country: 'Brazil' }, 'brasilia': { lat: -15.7975, lng: -47.8919, country: 'Brazil' }, 'belo horizonte': { lat: -19.9167, lng: -43.9345, country: 'Brazil' }, 'porto alegre': { lat: -30.0346, lng: -51.2177, country: 'Brazil' }, 'buenos aires': { lat: -34.6037, lng: -58.3816, country: 'Argentina' }, 'santiago': { lat: -33.4489, lng: -70.6693, country: 'Chile' }, 'bogota': { lat: 4.7110, lng: -74.0721, country: 'Colombia' }, 'bogot\u00e1': { lat: 4.7110, lng: -74.0721, country: 'Colombia' }, 'medellin': { lat: 6.2476, lng: -75.5658, country: 'Colombia' }, 'medell\u00edn': { lat: 6.2476, lng: -75.5658, country: 'Colombia' }, 'lima': { lat: -12.0464, lng: -77.0428, country: 'Peru' }, 'caracas': { lat: 10.4806, lng: -66.9036, country: 'Venezuela' }, 'montevideo': { lat: -34.9011, lng: -56.1645, country: 'Uruguay' }, 'quito': { lat: -0.1807, lng: -78.4678, country: 'Ecuador' }, // Europe - UK & Ireland 'london': { lat: 51.5074, lng: -0.1278, country: 'UK' }, 'cambridge uk': { lat: 52.2053, lng: 0.1218, country: 'UK' }, 'oxford': { lat: 51.7520, lng: -1.2577, country: 'UK' }, 'manchester': { lat: 53.4808, lng: -2.2426, country: 'UK' }, 'birmingham': { lat: 52.4862, lng: -1.8904, country: 'UK' }, 'edinburgh': { lat: 55.9533, lng: -3.1883, country: 'UK' }, 'glasgow': { lat: 55.8642, lng: -4.2518, country: 'UK' }, 'bristol': { lat: 51.4545, lng: -2.5879, country: 'UK' }, 'leeds': { lat: 53.8008, lng: -1.5491, country: 'UK' }, 'liverpool': { lat: 53.4084, lng: -2.9916, country: 'UK' }, 'belfast': { lat: 54.5973, lng: -5.9301, country: 'UK' }, 'cardiff': { lat: 51.4816, lng: -3.1791, country: 'UK' }, 'dublin': { lat: 53.3498, lng: -6.2603, country: 'Ireland' }, 'cork': { lat: 51.8985, lng: -8.4756, country: 'Ireland' }, 'galway': { lat: 53.2707, lng: -9.0568, country: 'Ireland' }, // Europe - Western 'paris': { lat: 48.8566, lng: 2.3522, country: 'France' }, 'lyon': { lat: 45.7640, lng: 4.8357, country: 'France' }, 'marseille': { lat: 43.2965, lng: 5.3698, country: 'France' }, 'toulouse': { lat: 43.6047, lng: 1.4442, country: 'France' }, 'nice': { lat: 43.7102, lng: 7.2620, country: 'France' }, 'bordeaux': { lat: 44.8378, lng: -0.5792, country: 'France' }, 'strasbourg': { lat: 48.5734, lng: 7.7521, country: 'France' }, 'nantes': { lat: 47.2184, lng: -1.5536, country: 'France' }, 'cannes': { lat: 43.5528, lng: 7.0174, country: 'France' }, 'monaco': { lat: 43.7384, lng: 7.4246, country: 'Monaco' }, 'berlin': { lat: 52.5200, lng: 13.4050, country: 'Germany' }, 'munich': { lat: 48.1351, lng: 11.5820, country: 'Germany' }, 'm\u00fcnchen': { lat: 48.1351, lng: 11.5820, country: 'Germany' }, 'frankfurt': { lat: 50.1109, lng: 8.6821, country: 'Germany' }, 'hamburg': { lat: 53.5511, lng: 9.9937, country: 'Germany' }, 'cologne': { lat: 50.9375, lng: 6.9603, country: 'Germany' }, 'k\u00f6ln': { lat: 50.9375, lng: 6.9603, country: 'Germany' }, 'd\u00fcsseldorf': { lat: 51.2277, lng: 6.7735, country: 'Germany' }, 'dusseldorf': { lat: 51.2277, lng: 6.7735, country: 'Germany' }, 'stuttgart': { lat: 48.7758, lng: 9.1829, country: 'Germany' }, 'hanover': { lat: 52.3759, lng: 9.7320, country: 'Germany' }, 'hannover': { lat: 52.3759, lng: 9.7320, country: 'Germany' }, 'dresden': { lat: 51.0504, lng: 13.7373, country: 'Germany' }, 'leipzig': { lat: 51.3397, lng: 12.3731, country: 'Germany' }, 'nuremberg': { lat: 49.4521, lng: 11.0767, country: 'Germany' }, 'amsterdam': { lat: 52.3676, lng: 4.9041, country: 'Netherlands' }, 'rotterdam': { lat: 51.9225, lng: 4.4792, country: 'Netherlands' }, 'the hague': { lat: 52.0705, lng: 4.3007, country: 'Netherlands' }, 'eindhoven': { lat: 51.4416, lng: 5.4697, country: 'Netherlands' }, 'utrecht': { lat: 52.0907, lng: 5.1214, country: 'Netherlands' }, 'brussels': { lat: 50.8503, lng: 4.3517, country: 'Belgium' }, 'antwerp': { lat: 51.2194, lng: 4.4025, country: 'Belgium' }, 'ghent': { lat: 51.0543, lng: 3.7174, country: 'Belgium' }, 'luxembourg': { lat: 49.6116, lng: 6.1319, country: 'Luxembourg' }, 'zurich': { lat: 47.3769, lng: 8.5417, country: 'Switzerland' }, 'z\u00fcrich': { lat: 47.3769, lng: 8.5417, country: 'Switzerland' }, 'geneva': { lat: 46.2044, lng: 6.1432, country: 'Switzerland' }, 'gen\u00e8ve': { lat: 46.2044, lng: 6.1432, country: 'Switzerland' }, 'basel': { lat: 47.5596, lng: 7.5886, country: 'Switzerland' }, 'bern': { lat: 46.9480, lng: 7.4474, country: 'Switzerland' }, 'lausanne': { lat: 46.5197, lng: 6.6323, country: 'Switzerland' }, 'davos': { lat: 46.8027, lng: 9.8360, country: 'Switzerland' }, 'vienna': { lat: 48.2082, lng: 16.3738, country: 'Austria' }, 'wien': { lat: 48.2082, lng: 16.3738, country: 'Austria' }, 'salzburg': { lat: 47.8095, lng: 13.0550, country: 'Austria' }, 'graz': { lat: 47.0707, lng: 15.4395, country: 'Austria' }, 'innsbruck': { lat: 47.2692, lng: 11.4041, country: 'Austria' }, // Europe - Southern 'barcelona': { lat: 41.3851, lng: 2.1734, country: 'Spain' }, 'madrid': { lat: 40.4168, lng: -3.7038, country: 'Spain' }, 'valencia': { lat: 39.4699, lng: -0.3763, country: 'Spain' }, 'seville': { lat: 37.3891, lng: -5.9845, country: 'Spain' }, 'sevilla': { lat: 37.3891, lng: -5.9845, country: 'Spain' }, 'malaga': { lat: 36.7213, lng: -4.4214, country: 'Spain' }, 'm\u00e1laga': { lat: 36.7213, lng: -4.4214, country: 'Spain' }, 'bilbao': { lat: 43.2630, lng: -2.9350, country: 'Spain' }, 'lisbon': { lat: 38.7223, lng: -9.1393, country: 'Portugal' }, 'lisboa': { lat: 38.7223, lng: -9.1393, country: 'Portugal' }, 'porto': { lat: 41.1579, lng: -8.6291, country: 'Portugal' }, 'rome': { lat: 41.9028, lng: 12.4964, country: 'Italy' }, 'roma': { lat: 41.9028, lng: 12.4964, country: 'Italy' }, 'milan': { lat: 45.4642, lng: 9.1900, country: 'Italy' }, 'milano': { lat: 45.4642, lng: 9.1900, country: 'Italy' }, 'florence': { lat: 43.7696, lng: 11.2558, country: 'Italy' }, 'firenze': { lat: 43.7696, lng: 11.2558, country: 'Italy' }, 'venice': { lat: 45.4408, lng: 12.3155, country: 'Italy' }, 'venezia': { lat: 45.4408, lng: 12.3155, country: 'Italy' }, 'turin': { lat: 45.0703, lng: 7.6869, country: 'Italy' }, 'torino': { lat: 45.0703, lng: 7.6869, country: 'Italy' }, 'naples': { lat: 40.8518, lng: 14.2681, country: 'Italy' }, 'napoli': { lat: 40.8518, lng: 14.2681, country: 'Italy' }, 'bologna': { lat: 44.4949, lng: 11.3426, country: 'Italy' }, 'athens': { lat: 37.9838, lng: 23.7275, country: 'Greece' }, 'thessaloniki': { lat: 40.6401, lng: 22.9444, country: 'Greece' }, 'malta': { lat: 35.8989, lng: 14.5146, country: 'Malta' }, 'valletta': { lat: 35.8989, lng: 14.5146, country: 'Malta' }, // Europe - Northern 'stockholm': { lat: 59.3293, lng: 18.0686, country: 'Sweden' }, 'gothenburg': { lat: 57.7089, lng: 11.9746, country: 'Sweden' }, 'g\u00f6teborg': { lat: 57.7089, lng: 11.9746, country: 'Sweden' }, 'malm\u00f6': { lat: 55.6050, lng: 13.0038, country: 'Sweden' }, 'malmo': { lat: 55.6050, lng: 13.0038, country: 'Sweden' }, 'copenhagen': { lat: 55.6761, lng: 12.5683, country: 'Denmark' }, 'k\u00f8benhavn': { lat: 55.6761, lng: 12.5683, country: 'Denmark' }, 'aarhus': { lat: 56.1629, lng: 10.2039, country: 'Denmark' }, 'oslo': { lat: 59.9139, lng: 10.7522, country: 'Norway' }, 'bergen': { lat: 60.3913, lng: 5.3221, country: 'Norway' }, 'helsinki': { lat: 60.1699, lng: 24.9384, country: 'Finland' }, 'espoo': { lat: 60.2055, lng: 24.6559, country: 'Finland' }, 'tampere': { lat: 61.4978, lng: 23.7610, country: 'Finland' }, 'reykjavik': { lat: 64.1466, lng: -21.9426, country: 'Iceland' }, // Europe - Eastern 'warsaw': { lat: 52.2297, lng: 21.0122, country: 'Poland' }, 'warszawa': { lat: 52.2297, lng: 21.0122, country: 'Poland' }, 'krakow': { lat: 50.0647, lng: 19.9450, country: 'Poland' }, 'krak\u00f3w': { lat: 50.0647, lng: 19.9450, country: 'Poland' }, 'wroclaw': { lat: 51.1079, lng: 17.0385, country: 'Poland' }, 'wroc\u0142aw': { lat: 51.1079, lng: 17.0385, country: 'Poland' }, 'gdansk': { lat: 54.3520, lng: 18.6466, country: 'Poland' }, 'prague': { lat: 50.0755, lng: 14.4378, country: 'Czech Republic' }, 'praha': { lat: 50.0755, lng: 14.4378, country: 'Czech Republic' }, 'brno': { lat: 49.1951, lng: 16.6068, country: 'Czech Republic' }, 'budapest': { lat: 47.4979, lng: 19.0402, country: 'Hungary' }, 'bucharest': { lat: 44.4268, lng: 26.1025, country: 'Romania' }, 'bucure\u0219ti': { lat: 44.4268, lng: 26.1025, country: 'Romania' }, 'cluj-napoca': { lat: 46.7712, lng: 23.6236, country: 'Romania' }, 'sofia': { lat: 42.6977, lng: 23.3219, country: 'Bulgaria' }, 'belgrade': { lat: 44.7866, lng: 20.4489, country: 'Serbia' }, 'beograd': { lat: 44.7866, lng: 20.4489, country: 'Serbia' }, 'zagreb': { lat: 45.8150, lng: 15.9819, country: 'Croatia' }, 'ljubljana': { lat: 46.0569, lng: 14.5058, country: 'Slovenia' }, 'bratislava': { lat: 48.1486, lng: 17.1077, country: 'Slovakia' }, 'tallinn': { lat: 59.4370, lng: 24.7536, country: 'Estonia' }, 'riga': { lat: 56.9496, lng: 24.1052, country: 'Latvia' }, 'vilnius': { lat: 54.6872, lng: 25.2797, country: 'Lithuania' }, 'kyiv': { lat: 50.4501, lng: 30.5234, country: 'Ukraine' }, 'kiev': { lat: 50.4501, lng: 30.5234, country: 'Ukraine' }, 'lviv': { lat: 49.8397, lng: 24.0297, country: 'Ukraine' }, 'minsk': { lat: 53.9045, lng: 27.5615, country: 'Belarus' }, 'moscow': { lat: 55.7558, lng: 37.6173, country: 'Russia' }, 'st. petersburg': { lat: 59.9311, lng: 30.3609, country: 'Russia' }, 'saint petersburg': { lat: 59.9311, lng: 30.3609, country: 'Russia' }, // Middle East 'dubai': { lat: 25.2048, lng: 55.2708, country: 'UAE' }, 'abu dhabi': { lat: 24.4539, lng: 54.3773, country: 'UAE' }, 'doha': { lat: 25.2854, lng: 51.5310, country: 'Qatar' }, 'riyadh': { lat: 24.7136, lng: 46.6753, country: 'Saudi Arabia' }, 'jeddah': { lat: 21.4858, lng: 39.1925, country: 'Saudi Arabia' }, 'neom': { lat: 28.0000, lng: 35.0000, country: 'Saudi Arabia' }, 'tel aviv': { lat: 32.0853, lng: 34.7818, country: 'Israel' }, 'jerusalem': { lat: 31.7683, lng: 35.2137, country: 'Israel' }, 'haifa': { lat: 32.7940, lng: 34.9896, country: 'Israel' }, 'amman': { lat: 31.9454, lng: 35.9284, country: 'Jordan' }, 'beirut': { lat: 33.8938, lng: 35.5018, country: 'Lebanon' }, 'istanbul': { lat: 41.0082, lng: 28.9784, country: 'Turkey' }, 'ankara': { lat: 39.9334, lng: 32.8597, country: 'Turkey' }, 'izmir': { lat: 38.4237, lng: 27.1428, country: 'Turkey' }, 'tehran': { lat: 35.6892, lng: 51.3890, country: 'Iran' }, 'cairo': { lat: 30.0444, lng: 31.2357, country: 'Egypt' }, 'muscat': { lat: 23.5880, lng: 58.3829, country: 'Oman' }, 'manama': { lat: 26.2285, lng: 50.5860, country: 'Bahrain' }, 'kuwait city': { lat: 29.3759, lng: 47.9774, country: 'Kuwait' }, // Asia - East 'tokyo': { lat: 35.6762, lng: 139.6503, country: 'Japan' }, 'osaka': { lat: 34.6937, lng: 135.5023, country: 'Japan' }, 'kyoto': { lat: 35.0116, lng: 135.7681, country: 'Japan' }, 'yokohama': { lat: 35.4437, lng: 139.6380, country: 'Japan' }, 'nagoya': { lat: 35.1815, lng: 136.9066, country: 'Japan' }, 'fukuoka': { lat: 33.5904, lng: 130.4017, country: 'Japan' }, 'sapporo': { lat: 43.0618, lng: 141.3545, country: 'Japan' }, 'kobe': { lat: 34.6901, lng: 135.1956, country: 'Japan' }, 'seoul': { lat: 37.5665, lng: 126.9780, country: 'South Korea' }, 'busan': { lat: 35.1796, lng: 129.0756, country: 'South Korea' }, 'incheon': { lat: 37.4563, lng: 126.7052, country: 'South Korea' }, 'beijing': { lat: 39.9042, lng: 116.4074, country: 'China' }, 'shanghai': { lat: 31.2304, lng: 121.4737, country: 'China' }, 'shenzhen': { lat: 22.5431, lng: 114.0579, country: 'China' }, 'guangzhou': { lat: 23.1291, lng: 113.2644, country: 'China' }, 'hong kong': { lat: 22.3193, lng: 114.1694, country: 'Hong Kong' }, 'hangzhou': { lat: 30.2741, lng: 120.1551, country: 'China' }, 'chengdu': { lat: 30.5728, lng: 104.0668, country: 'China' }, 'xian': { lat: 34.3416, lng: 108.9398, country: 'China' }, "xi'an": { lat: 34.3416, lng: 108.9398, country: 'China' }, 'nanjing': { lat: 32.0603, lng: 118.7969, country: 'China' }, 'wuhan': { lat: 30.5928, lng: 114.3055, country: 'China' }, 'tianjin': { lat: 39.3434, lng: 117.3616, country: 'China' }, 'suzhou': { lat: 31.2990, lng: 120.5853, country: 'China' }, 'taipei': { lat: 25.0330, lng: 121.5654, country: 'Taiwan' }, 'kaohsiung': { lat: 22.6273, lng: 120.3014, country: 'Taiwan' }, 'macau': { lat: 22.1987, lng: 113.5439, country: 'Macau' }, 'macao': { lat: 22.1987, lng: 113.5439, country: 'Macau' }, // Asia - Southeast 'singapore': { lat: 1.3521, lng: 103.8198, country: 'Singapore' }, 'kuala lumpur': { lat: 3.1390, lng: 101.6869, country: 'Malaysia' }, 'penang': { lat: 5.4141, lng: 100.3288, country: 'Malaysia' }, 'jakarta': { lat: -6.2088, lng: 106.8456, country: 'Indonesia' }, 'bali': { lat: -8.3405, lng: 115.0920, country: 'Indonesia' }, 'denpasar': { lat: -8.6705, lng: 115.2126, country: 'Indonesia' }, 'bandung': { lat: -6.9175, lng: 107.6191, country: 'Indonesia' }, 'surabaya': { lat: -7.2575, lng: 112.7521, country: 'Indonesia' }, 'bangkok': { lat: 13.7563, lng: 100.5018, country: 'Thailand' }, 'chiang mai': { lat: 18.7883, lng: 98.9853, country: 'Thailand' }, 'phuket': { lat: 7.8804, lng: 98.3923, country: 'Thailand' }, 'ho chi minh city': { lat: 10.8231, lng: 106.6297, country: 'Vietnam' }, 'saigon': { lat: 10.8231, lng: 106.6297, country: 'Vietnam' }, 'hanoi': { lat: 21.0278, lng: 105.8342, country: 'Vietnam' }, 'da nang': { lat: 16.0544, lng: 108.2022, country: 'Vietnam' }, 'manila': { lat: 14.5995, lng: 120.9842, country: 'Philippines' }, 'cebu': { lat: 10.3157, lng: 123.8854, country: 'Philippines' }, 'phnom penh': { lat: 11.5564, lng: 104.9282, country: 'Cambodia' }, 'yangon': { lat: 16.8661, lng: 96.1951, country: 'Myanmar' }, // Asia - South 'mumbai': { lat: 19.0760, lng: 72.8777, country: 'India' }, 'bombay': { lat: 19.0760, lng: 72.8777, country: 'India' }, 'delhi': { lat: 28.7041, lng: 77.1025, country: 'India' }, 'new delhi': { lat: 28.6139, lng: 77.2090, country: 'India' }, 'bangalore': { lat: 12.9716, lng: 77.5946, country: 'India' }, 'bengaluru': { lat: 12.9716, lng: 77.5946, country: 'India' }, 'hyderabad': { lat: 17.3850, lng: 78.4867, country: 'India' }, 'chennai': { lat: 13.0827, lng: 80.2707, country: 'India' }, 'madras': { lat: 13.0827, lng: 80.2707, country: 'India' }, 'pune': { lat: 18.5204, lng: 73.8567, country: 'India' }, 'kolkata': { lat: 22.5726, lng: 88.3639, country: 'India' }, 'calcutta': { lat: 22.5726, lng: 88.3639, country: 'India' }, 'ahmedabad': { lat: 23.0225, lng: 72.5714, country: 'India' }, 'jaipur': { lat: 26.9124, lng: 75.7873, country: 'India' }, 'gurgaon': { lat: 28.4595, lng: 77.0266, country: 'India' }, 'gurugram': { lat: 28.4595, lng: 77.0266, country: 'India' }, 'noida': { lat: 28.5355, lng: 77.3910, country: 'India' }, 'kochi': { lat: 9.9312, lng: 76.2673, country: 'India' }, 'goa': { lat: 15.2993, lng: 74.1240, country: 'India' }, 'karachi': { lat: 24.8607, lng: 67.0011, country: 'Pakistan' }, 'lahore': { lat: 31.5497, lng: 74.3436, country: 'Pakistan' }, 'islamabad': { lat: 33.6844, lng: 73.0479, country: 'Pakistan' }, 'dhaka': { lat: 23.8103, lng: 90.4125, country: 'Bangladesh' }, 'colombo': { lat: 6.9271, lng: 79.8612, country: 'Sri Lanka' }, 'kathmandu': { lat: 27.7172, lng: 85.3240, country: 'Nepal' }, // Africa 'cape town': { lat: -33.9249, lng: 18.4241, country: 'South Africa' }, 'johannesburg': { lat: -26.2041, lng: 28.0473, country: 'South Africa' }, 'pretoria': { lat: -25.7479, lng: 28.2293, country: 'South Africa' }, 'durban': { lat: -29.8587, lng: 31.0218, country: 'South Africa' }, 'lagos': { lat: 6.5244, lng: 3.3792, country: 'Nigeria' }, 'abuja': { lat: 9.0765, lng: 7.3986, country: 'Nigeria' }, 'nairobi': { lat: -1.2921, lng: 36.8219, country: 'Kenya' }, 'accra': { lat: 5.6037, lng: -0.1870, country: 'Ghana' }, 'casablanca': { lat: 33.5731, lng: -7.5898, country: 'Morocco' }, 'marrakech': { lat: 31.6295, lng: -7.9811, country: 'Morocco' }, 'tunis': { lat: 36.8065, lng: 10.1815, country: 'Tunisia' }, 'algiers': { lat: 36.7538, lng: 3.0588, country: 'Algeria' }, 'addis ababa': { lat: 8.9806, lng: 38.7578, country: 'Ethiopia' }, 'dar es salaam': { lat: -6.7924, lng: 39.2083, country: 'Tanzania' }, 'kampala': { lat: 0.3476, lng: 32.5825, country: 'Uganda' }, 'kigali': { lat: -1.9403, lng: 29.8739, country: 'Rwanda' }, 'mauritius': { lat: -20.3484, lng: 57.5522, country: 'Mauritius' }, 'port louis': { lat: -20.1609, lng: 57.5012, country: 'Mauritius' }, // Oceania 'sydney': { lat: -33.8688, lng: 151.2093, country: 'Australia' }, 'melbourne': { lat: -37.8136, lng: 144.9631, country: 'Australia' }, 'brisbane': { lat: -27.4698, lng: 153.0251, country: 'Australia' }, 'perth': { lat: -31.9505, lng: 115.8605, country: 'Australia' }, 'adelaide': { lat: -34.9285, lng: 138.6007, country: 'Australia' }, 'canberra': { lat: -35.2809, lng: 149.1300, country: 'Australia' }, 'gold coast': { lat: -28.0167, lng: 153.4000, country: 'Australia' }, 'auckland': { lat: -36.8509, lng: 174.7645, country: 'New Zealand' }, 'wellington': { lat: -41.2865, lng: 174.7762, country: 'New Zealand' }, 'christchurch': { lat: -43.5321, lng: 172.6362, country: 'New Zealand' }, // Online/Virtual 'online': { lat: 0, lng: 0, country: 'Virtual', virtual: true }, 'virtual': { lat: 0, lng: 0, country: 'Virtual', virtual: true }, 'hybrid': { lat: 0, lng: 0, country: 'Virtual', virtual: true }, }; ================================================ FILE: api/displacement/v1/[rpc].ts ================================================ export const config = { runtime: 'edge' }; import { createDomainGateway, serverOptions } from '../../../server/gateway'; import { createDisplacementServiceRoutes } from '../../../src/generated/server/worldmonitor/displacement/v1/service_server'; import { displacementHandler } from '../../../server/worldmonitor/displacement/v1/handler'; export default createDomainGateway( createDisplacementServiceRoutes(displacementHandler, serverOptions), ); ================================================ FILE: api/download.js ================================================ import { fetchLatestRelease } from './_github-release.js'; // Non-sebuf: returns XML/HTML, stays as standalone Vercel function export const config = { runtime: 'edge' }; const RELEASES_PAGE = 'https://github.com/koala73/worldmonitor/releases/latest'; const PLATFORM_PATTERNS = { 'windows-exe': (name) => name.endsWith('_x64-setup.exe'), 'windows-msi': (name) => name.endsWith('_x64_en-US.msi'), 'macos-arm64': (name) => name.endsWith('_aarch64.dmg'), 'macos-x64': (name) => name.endsWith('_x64.dmg') && !name.includes('setup'), 'linux-appimage': (name) => name.endsWith('_amd64.AppImage'), 'linux-appimage-arm64': (name) => name.endsWith('_aarch64.AppImage'), }; const VARIANT_IDENTIFIERS = { full: ['worldmonitor'], world: ['worldmonitor'], tech: ['techmonitor'], finance: ['financemonitor'], }; function canonicalAssetName(name) { return String(name || '').toLowerCase().replace(/[^a-z0-9]+/g, ''); } function findAssetForVariant(assets, variant, platformMatcher) { const identifiers = VARIANT_IDENTIFIERS[variant] ?? null; if (!identifiers) return null; return assets.find((asset) => { const assetName = String(asset?.name || ''); const normalizedAssetName = canonicalAssetName(assetName); const hasVariantIdentifier = identifiers.some((identifier) => normalizedAssetName.includes(identifier) ); return hasVariantIdentifier && platformMatcher(assetName); }) ?? null; } export default async function handler(req) { const url = new URL(req.url); const platform = url.searchParams.get('platform'); const variant = (url.searchParams.get('variant') || '').toLowerCase(); if (!platform || !PLATFORM_PATTERNS[platform]) { return Response.redirect(RELEASES_PAGE, 302); } try { const release = await fetchLatestRelease('WorldMonitor-Download-Redirect'); if (!release) { return Response.redirect(RELEASES_PAGE, 302); } const matcher = PLATFORM_PATTERNS[platform]; const assets = Array.isArray(release.assets) ? release.assets : []; const asset = variant ? findAssetForVariant(assets, variant, matcher) : assets.find((a) => matcher(String(a?.name || ''))); if (!asset) { return Response.redirect(RELEASES_PAGE, 302); } return new Response(null, { status: 302, headers: { 'Location': asset.browser_download_url, 'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=60, stale-if-error=600', }, }); } catch { return Response.redirect(RELEASES_PAGE, 302); } } ================================================ FILE: api/economic/v1/[rpc].ts ================================================ export const config = { runtime: 'edge' }; import { createDomainGateway, serverOptions } from '../../../server/gateway'; import { createEconomicServiceRoutes } from '../../../src/generated/server/worldmonitor/economic/v1/service_server'; import { economicHandler } from '../../../server/worldmonitor/economic/v1/handler'; export default createDomainGateway( createEconomicServiceRoutes(economicHandler, serverOptions), ); ================================================ FILE: api/eia/[[...path]].js ================================================ // EIA (Energy Information Administration) API proxy // Keeps API key server-side import { getCorsHeaders, isDisallowedOrigin } from '../_cors.js'; export const config = { runtime: 'edge' }; export default async function handler(req) { const cors = getCorsHeaders(req); if (isDisallowedOrigin(req)) { return new Response(JSON.stringify({ error: 'Origin not allowed' }), { status: 403, headers: cors }); } // Only allow GET and OPTIONS methods if (req.method === 'OPTIONS') { return new Response(null, { status: 204, headers: cors }); } if (req.method !== 'GET') { return Response.json({ error: 'Method not allowed' }, { status: 405, headers: cors, }); } const url = new URL(req.url); const path = url.pathname.replace('/api/eia', ''); const apiKey = process.env.EIA_API_KEY; if (!apiKey) { return Response.json({ configured: false, skipped: true, reason: 'EIA_API_KEY not configured', }, { status: 200, headers: cors, }); } // Health check if (path === '/health' || path === '') { return Response.json({ configured: true }, { headers: cors, }); } // Petroleum data endpoint if (path === '/petroleum') { try { const series = { wti: 'PET.RWTC.W', brent: 'PET.RBRTE.W', production: 'PET.WCRFPUS2.W', inventory: 'PET.WCESTUS1.W', }; const results = {}; // Fetch all series in parallel const fetchPromises = Object.entries(series).map(async ([key, seriesId]) => { try { const response = await fetch( `https://api.eia.gov/v2/seriesid/${seriesId}?api_key=${apiKey}&num=2`, { headers: { 'Accept': 'application/json' } } ); if (!response.ok) return null; const data = await response.json(); const values = data?.response?.data || []; if (values.length >= 1) { return { key, data: { current: values[0]?.value, previous: values[1]?.value || values[0]?.value, date: values[0]?.period, unit: values[0]?.unit, } }; } } catch (e) { console.error(`[EIA] Failed to fetch ${key}:`, e.message); } return null; }); const fetchResults = await Promise.all(fetchPromises); for (const result of fetchResults) { if (result) { results[result.key] = result.data; } } return Response.json(results, { headers: { ...cors, 'Cache-Control': 'public, max-age=1800, s-maxage=1800, stale-while-revalidate=300', }, }); } catch (error) { console.error('[EIA] Fetch error:', error); return Response.json({ error: 'Failed to fetch EIA data', }, { status: 500, headers: cors, }); } } return Response.json({ error: 'Not found' }, { status: 404, headers: cors, }); } ================================================ FILE: api/enrichment/_domain.js ================================================ const DOMAIN_SUFFIX_RE = /\.(com|io|co|org|net|ai|dev|app)$/; export function toOrgSlugFromDomain(domain) { return (domain || '') .trim() .toLowerCase() .replace(DOMAIN_SUFFIX_RE, '') .split('.') .pop() || ''; } export function inferCompanyNameFromDomain(domain) { const orgSlug = toOrgSlugFromDomain(domain); if (!orgSlug) return domain || ''; return orgSlug .replace(/-/g, ' ') .replace(/\b\w/g, (c) => c.toUpperCase()); } ================================================ FILE: api/enrichment/company.js ================================================ /** * Company Enrichment API — Vercel Edge Function * Aggregates company data from multiple public sources: * - GitHub org data * - Hacker News mentions * - SEC EDGAR filings (public US companies) * - Tech stack inference from GitHub repos * * GET /api/enrichment/company?domain=example.com * GET /api/enrichment/company?name=Stripe */ import { getCorsHeaders, isDisallowedOrigin } from '../_cors.js'; import { checkRateLimit } from '../_rate-limit.js'; import { inferCompanyNameFromDomain, toOrgSlugFromDomain } from './_domain.js'; export const config = { runtime: 'edge' }; const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'; const CACHE_TTL_SECONDS = 3600; const GITHUB_API_HEADERS = Object.freeze({ Accept: 'application/vnd.github.v3+json', 'User-Agent': UA }); async function fetchGitHubOrg(name) { try { const res = await fetch(`https://api.github.com/orgs/${encodeURIComponent(name)}`, { headers: GITHUB_API_HEADERS, signal: AbortSignal.timeout(5000), }); if (!res.ok) return null; const data = await res.json(); return { name: data.name || data.login, description: data.description, blog: data.blog, location: data.location, publicRepos: data.public_repos, followers: data.followers, avatarUrl: data.avatar_url, createdAt: data.created_at, }; } catch { return null; } } async function fetchGitHubTechStack(orgName) { try { const res = await fetch( `https://api.github.com/orgs/${encodeURIComponent(orgName)}/repos?sort=stars&per_page=10`, { headers: GITHUB_API_HEADERS, signal: AbortSignal.timeout(5000), }, ); if (!res.ok) return []; const repos = await res.json(); const languages = new Map(); for (const repo of repos) { if (repo.language) { languages.set(repo.language, (languages.get(repo.language) || 0) + repo.stargazers_count + 1); } } return Array.from(languages.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 10) .map(([lang, score]) => ({ name: lang, category: 'Programming Language', confidence: Math.min(1, score / 100) })); } catch { return []; } } async function fetchSECData(companyName) { try { const res = await fetch( `https://efts.sec.gov/LATEST/search-index?q=${encodeURIComponent(companyName)}&dateRange=custom&startdt=${getDateMonthsAgo(6)}&enddt=${getTodayISO()}&forms=10-K,10-Q,8-K&from=0&size=5`, { headers: { 'User-Agent': 'WorldMonitor research@worldmonitor.app', 'Accept': 'application/json' }, signal: AbortSignal.timeout(8000), }, ); if (!res.ok) return null; const data = await res.json(); if (!data.hits || !data.hits.hits || data.hits.hits.length === 0) return null; return { totalFilings: data.hits.total?.value || 0, recentFilings: data.hits.hits.slice(0, 5).map((h) => ({ form: h._source?.form_type || h._source?.file_type, date: h._source?.file_date || h._source?.period_of_report, description: h._source?.display_names?.[0] || companyName, })), }; } catch { return null; } } async function fetchHackerNewsMentions(companyName) { try { const res = await fetch( `https://hn.algolia.com/api/v1/search?query=${encodeURIComponent(companyName)}&tags=story&hitsPerPage=5`, { headers: { 'User-Agent': UA }, signal: AbortSignal.timeout(5000), }, ); if (!res.ok) return []; const data = await res.json(); return (data.hits || []).map((h) => ({ title: h.title, url: h.url, points: h.points, comments: h.num_comments, date: h.created_at, })); } catch { return []; } } function getTodayISO() { return toISODate(new Date()); } function getDateMonthsAgo(months) { const d = new Date(); d.setMonth(d.getMonth() - months); return toISODate(d); } function toISODate(date) { return date.toISOString().split('T')[0]; } export default async function handler(req) { const cors = getCorsHeaders(req, 'GET, OPTIONS'); if (req.method === 'OPTIONS') { return new Response(null, { status: 204, headers: cors }); } if (isDisallowedOrigin(req)) { return new Response('Forbidden', { status: 403, headers: cors }); } const rateLimitResult = await checkRateLimit(req, 'enrichment', 30, '60s'); if (rateLimitResult) return rateLimitResult; const url = new URL(req.url); const domain = url.searchParams.get('domain')?.trim().toLowerCase(); const name = url.searchParams.get('name')?.trim(); if (!domain && !name) { return new Response(JSON.stringify({ error: 'Provide ?domain= or ?name= parameter' }), { status: 400, headers: { ...cors, 'Content-Type': 'application/json' }, }); } const companyName = name || (domain ? inferCompanyNameFromDomain(domain) : 'Unknown'); const searchName = domain ? toOrgSlugFromDomain(domain) : companyName.toLowerCase().replace(/\s+/g, ''); const [githubOrg, techStack, secData, hnMentions] = await Promise.all([ fetchGitHubOrg(searchName), fetchGitHubTechStack(searchName), fetchSECData(companyName), fetchHackerNewsMentions(companyName), ]); const enrichedData = { company: { name: githubOrg?.name || companyName, domain: domain || githubOrg?.blog?.replace(/^https?:\/\//, '').replace(/\/$/, '') || null, description: githubOrg?.description || null, location: githubOrg?.location || null, website: githubOrg?.blog || (domain ? `https://${domain}` : null), founded: githubOrg?.createdAt ? new Date(githubOrg.createdAt).getFullYear() : null, }, github: githubOrg ? { publicRepos: githubOrg.publicRepos, followers: githubOrg.followers, avatarUrl: githubOrg.avatarUrl, } : null, techStack: techStack.length > 0 ? techStack : null, secFilings: secData, hackerNewsMentions: hnMentions.length > 0 ? hnMentions : null, enrichedAt: new Date().toISOString(), sources: [ githubOrg ? 'github' : null, techStack.length > 0 ? 'github_repos' : null, secData ? 'sec_edgar' : null, hnMentions.length > 0 ? 'hacker_news' : null, ].filter(Boolean), }; return new Response(JSON.stringify(enrichedData), { status: 200, headers: { ...cors, 'Content-Type': 'application/json', 'Cache-Control': `public, s-maxage=${CACHE_TTL_SECONDS}, stale-while-revalidate=${CACHE_TTL_SECONDS * 2}`, }, }); } ================================================ FILE: api/enrichment/signals.js ================================================ /** * Signal Discovery API — Vercel Edge Function * Discovers activity signals for a company from public sources: * - News mentions (Hacker News) * - GitHub activity spikes * - Job posting signals (HN hiring threads) * * GET /api/enrichment/signals?company=Stripe&domain=stripe.com */ import { getCorsHeaders, isDisallowedOrigin } from '../_cors.js'; import { checkRateLimit } from '../_rate-limit.js'; import { toOrgSlugFromDomain } from './_domain.js'; export const config = { runtime: 'edge' }; const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'; const UPSTREAM_TIMEOUT_MS = 5000; const DEFAULT_HEADERS = Object.freeze({ 'User-Agent': UA }); const GITHUB_HEADERS = Object.freeze({ Accept: 'application/vnd.github.v3+json', ...DEFAULT_HEADERS }); const SIGNAL_KEYWORDS = { hiring_surge: ['hiring', 'we\'re hiring', 'join our team', 'open positions', 'new roles', 'growing team'], funding_event: ['raised', 'funding', 'series', 'investment', 'valuation', 'backed by'], expansion_signal: ['expansion', 'new office', 'opening', 'entering market', 'new region', 'international'], technology_adoption: ['migrating to', 'adopting', 'implementing', 'rolling out', 'tech stack', 'infrastructure'], executive_movement: ['appointed', 'joins as', 'new ceo', 'new cto', 'new vp', 'leadership change', 'promoted to'], financial_trigger: ['revenue', 'ipo', 'acquisition', 'merger', 'quarterly results', 'earnings'], }; function classifySignal(text) { const lower = text.toLowerCase(); for (const [type, keywords] of Object.entries(SIGNAL_KEYWORDS)) { for (const kw of keywords) { if (lower.includes(kw)) return type; } } return 'press_release'; } function scoreSignalStrength(points, comments, recencyDays) { let score = 0; if (points > 100) score += 3; else if (points > 30) score += 2; else score += 1; if (comments > 50) score += 2; else if (comments > 10) score += 1; if (recencyDays <= 3) score += 3; else if (recencyDays <= 7) score += 2; else if (recencyDays <= 14) score += 1; if (score >= 7) return 'critical'; if (score >= 5) return 'high'; if (score >= 3) return 'medium'; return 'low'; } async function fetchHNSignals(companyName) { try { const res = await fetch( `https://hn.algolia.com/api/v1/search_by_date?query=${encodeURIComponent(companyName)}&tags=story&hitsPerPage=20&numericFilters=created_at_i>${Math.floor(Date.now() / 1000) - 30 * 86400}`, { headers: DEFAULT_HEADERS, signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS), }, ); if (!res.ok) return []; const data = await res.json(); const now = Date.now(); return (data.hits || []).map((h) => { const recencyDays = (now - new Date(h.created_at).getTime()) / 86400000; return { type: classifySignal(h.title), title: h.title, url: h.url || `https://news.ycombinator.com/item?id=${h.objectID}`, source: 'Hacker News', sourceTier: 2, timestamp: h.created_at, strength: scoreSignalStrength(h.points || 0, h.num_comments || 0, recencyDays), engagement: { points: h.points, comments: h.num_comments }, }; }); } catch { return []; } } async function fetchGitHubSignals(orgName) { try { const res = await fetch( `https://api.github.com/orgs/${encodeURIComponent(orgName)}/repos?sort=created&per_page=10`, { headers: GITHUB_HEADERS, signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS), }, ); if (!res.ok) return []; const repos = await res.json(); const now = Date.now(); const thirtyDaysAgo = now - 30 * 86400000; return repos .filter((r) => new Date(r.created_at).getTime() > thirtyDaysAgo) .map((r) => ({ type: 'technology_adoption', title: `New repository: ${r.full_name} — ${r.description || 'No description'}`, url: r.html_url, source: 'GitHub', sourceTier: 2, timestamp: r.created_at, strength: r.stargazers_count > 50 ? 'high' : r.stargazers_count > 10 ? 'medium' : 'low', engagement: { stars: r.stargazers_count, forks: r.forks_count }, })); } catch { return []; } } async function fetchJobSignals(companyName) { try { const res = await fetch( `https://hn.algolia.com/api/v1/search?query=${encodeURIComponent(companyName)}&tags=comment,ask_hn&hitsPerPage=10&numericFilters=created_at_i>${Math.floor(Date.now() / 1000) - 60 * 86400}`, { headers: DEFAULT_HEADERS, signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS), }, ); if (!res.ok) return []; const data = await res.json(); const hiringComments = (data.hits || []).filter((h) => { const text = (h.comment_text || '').toLowerCase(); return text.includes('hiring') || text.includes('job') || text.includes('apply'); }); if (hiringComments.length === 0) return []; return [{ type: 'hiring_surge', title: `${companyName} hiring activity (${hiringComments.length} mentions in HN hiring threads)`, url: `https://news.ycombinator.com/item?id=${hiringComments[0].story_id}`, source: 'HN Hiring Threads', sourceTier: 3, timestamp: hiringComments[0].created_at, strength: hiringComments.length >= 3 ? 'high' : 'medium', engagement: { mentions: hiringComments.length }, }]; } catch { return []; } } export default async function handler(req) { const cors = getCorsHeaders(req, 'GET, OPTIONS'); if (req.method === 'OPTIONS') { return new Response(null, { status: 204, headers: cors }); } if (isDisallowedOrigin(req)) { return new Response('Forbidden', { status: 403, headers: cors }); } const rateLimitResult = await checkRateLimit(req, 'signals', 20, '60s'); if (rateLimitResult) return rateLimitResult; const url = new URL(req.url); const company = url.searchParams.get('company')?.trim(); const domain = url.searchParams.get('domain')?.trim().toLowerCase(); if (!company) { return new Response(JSON.stringify({ error: 'Provide ?company= parameter' }), { status: 400, headers: { ...cors, 'Content-Type': 'application/json' }, }); } const orgName = toOrgSlugFromDomain(domain) || company.toLowerCase().replace(/\s+/g, ''); const [hnSignals, githubSignals, jobSignals] = await Promise.all([ fetchHNSignals(company), fetchGitHubSignals(orgName), fetchJobSignals(company), ]); const allSignals = [...hnSignals, ...githubSignals, ...jobSignals] .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); const signalTypeCounts = {}; for (const s of allSignals) { signalTypeCounts[s.type] = (signalTypeCounts[s.type] || 0) + 1; } const result = { company, domain: domain || null, signals: allSignals, summary: { totalSignals: allSignals.length, byType: signalTypeCounts, strongestSignal: allSignals[0] || null, signalDiversity: Object.keys(signalTypeCounts).length, }, discoveredAt: new Date().toISOString(), }; return new Response(JSON.stringify(result), { status: 200, headers: { ...cors, 'Content-Type': 'application/json', 'Cache-Control': 'public, s-maxage=1800, stale-while-revalidate=3600', }, }); } ================================================ FILE: api/forecast/v1/[rpc].ts ================================================ export const config = { runtime: 'edge' }; import { createDomainGateway, serverOptions } from '../../../server/gateway'; import { createForecastServiceRoutes } from '../../../src/generated/server/worldmonitor/forecast/v1/service_server'; import { forecastHandler } from '../../../server/worldmonitor/forecast/v1/handler'; export default createDomainGateway( createForecastServiceRoutes(forecastHandler, serverOptions), ); ================================================ FILE: api/fwdstart.js ================================================ // Non-sebuf: returns XML/HTML, stays as standalone Vercel function import { getCorsHeaders, isDisallowedOrigin } from './_cors.js'; import { jsonResponse } from './_json-response.js'; export const config = { runtime: 'edge' }; // Scrape FwdStart newsletter archive and return as RSS export default async function handler(req) { const cors = getCorsHeaders(req); if (isDisallowedOrigin(req)) { return jsonResponse({ error: 'Origin not allowed' }, 403, cors); } try { const response = await fetch('https://www.fwdstart.me/archive', { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Accept': 'text/html,application/xhtml+xml', }, signal: AbortSignal.timeout(15000), }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const html = await response.text(); const items = []; const seenUrls = new Set(); // Split by embla__slide to get each post block const slideBlocks = html.split('embla__slide'); for (const block of slideBlocks) { // Extract URL const urlMatch = block.match(/href="(\/p\/[^"]+)"/); if (!urlMatch) continue; const url = `https://www.fwdstart.me${urlMatch[1]}`; if (seenUrls.has(url)) continue; seenUrls.add(url); // Extract title from alt attribute const altMatch = block.match(/alt="([^"]+)"/); const title = altMatch ? altMatch[1] : ''; if (!title || title.length < 5) continue; // Extract date - look for "Mon DD, YYYY" pattern const dateMatch = block.match(/(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+(\d{1,2}),?\s+(\d{4})/i); let pubDate = new Date(); if (dateMatch) { const dateStr = `${dateMatch[1]} ${dateMatch[2]}, ${dateMatch[3]}`; const parsed = new Date(dateStr); if (!Number.isNaN(parsed.getTime())) { pubDate = parsed; } } // Extract subtitle/description if available let description = ''; const subtitleMatch = block.match(/line-clamp-3[^>]*>.*?]*>([^<]{20,})<\/span>/s); if (subtitleMatch) { description = subtitleMatch[1].trim(); } items.push({ title, link: url, date: pubDate.toISOString(), description }); } // Build RSS XML const rssItems = items.slice(0, 30).map(item => ` <![CDATA[${item.title}]]> ${item.link} ${item.link} ${new Date(item.date).toUTCString()} FwdStart Newsletter `).join(''); const rss = ` FwdStart Newsletter https://www.fwdstart.me Forward-thinking startup and VC news from MENA and beyond en-us ${new Date().toUTCString()} ${rssItems} `; return new Response(rss, { headers: { 'Content-Type': 'application/xml; charset=utf-8', ...cors, 'Cache-Control': 'public, max-age=1800, s-maxage=1800, stale-while-revalidate=300', }, }); } catch (error) { console.error('FwdStart scraper error:', error); return jsonResponse({ error: 'Failed to fetch FwdStart archive', details: error.message }, 502, cors); } } ================================================ FILE: api/geo.js ================================================ import { jsonResponse } from './_json-response.js'; export const config = { runtime: 'edge' }; export default function handler(req) { const cfCountry = req.headers.get('cf-ipcountry'); const country = (cfCountry && cfCountry !== 'T1' ? cfCountry : null) || req.headers.get('x-vercel-ip-country') || 'XX'; return jsonResponse({ country }, 200, { 'Cache-Control': 'public, max-age=300, s-maxage=3600, stale-if-error=3600', 'Access-Control-Allow-Origin': '*', }); } ================================================ FILE: api/giving/v1/[rpc].ts ================================================ export const config = { runtime: 'edge' }; import { createDomainGateway, serverOptions } from '../../../server/gateway'; import { createGivingServiceRoutes } from '../../../src/generated/server/worldmonitor/giving/v1/service_server'; import { givingHandler } from '../../../server/worldmonitor/giving/v1/handler'; export default createDomainGateway( createGivingServiceRoutes(givingHandler, serverOptions), ); ================================================ FILE: api/gpsjam.js ================================================ import { getCorsHeaders, isDisallowedOrigin } from './_cors.js'; import { jsonResponse } from './_json-response.js'; import { readJsonFromUpstash } from './_upstash-json.js'; export const config = { runtime: 'edge' }; const REDIS_KEY = 'intelligence:gpsjam:v2'; const REDIS_KEY_V1 = 'intelligence:gpsjam:v1'; let cached = null; let cachedAt = 0; const CACHE_TTL = 300_000; let negUntil = 0; const NEG_TTL = 60_000; async function fetchGpsJamData() { const now = Date.now(); if (cached && now - cachedAt < CACHE_TTL) return cached; if (now < negUntil) return null; let data; try { data = await readJsonFromUpstash(REDIS_KEY); } catch { data = null; } if (!data) { let v1; try { v1 = await readJsonFromUpstash(REDIS_KEY_V1); } catch { v1 = null; } if (v1?.hexes) { data = { ...v1, source: v1.source || 'gpsjam.org (normalized)', hexes: v1.hexes.map(hex => { if ('npAvg' in hex) return hex; const pct = hex.pct || 0; return { h3: hex.h3, lat: hex.lat, lon: hex.lon, level: hex.level, region: hex.region, npAvg: pct > 10 ? 0.3 : pct >= 2 ? 0.8 : 1.5, sampleCount: hex.bad || 0, aircraftCount: hex.total || 0, }; }), }; } } if (!data) { negUntil = now + NEG_TTL; return null; } cached = data; cachedAt = now; return data; } export default async function handler(req) { const corsHeaders = getCorsHeaders(req, 'GET, OPTIONS'); if (req.method === 'OPTIONS') { return new Response(null, { status: 204, headers: corsHeaders }); } if (isDisallowedOrigin(req)) { return jsonResponse({ error: 'Origin not allowed' }, 403, corsHeaders); } const data = await fetchGpsJamData(); if (!data) { return jsonResponse( { error: 'GPS interference data temporarily unavailable' }, 503, { 'Cache-Control': 'no-cache, no-store', ...corsHeaders }, ); } return jsonResponse( data, 200, { 'Cache-Control': 's-maxage=3600, stale-while-revalidate=1800, stale-if-error=3600', ...corsHeaders, }, ); } ================================================ FILE: api/health.js ================================================ import { jsonResponse } from './_json-response.js'; export const config = { runtime: 'edge' }; const BOOTSTRAP_KEYS = { earthquakes: 'seismology:earthquakes:v1', outages: 'infra:outages:v1', sectors: 'market:sectors:v1', etfFlows: 'market:etf-flows:v1', climateAnomalies: 'climate:anomalies:v1', wildfires: 'wildfire:fires:v1', marketQuotes: 'market:stocks-bootstrap:v1', commodityQuotes: 'market:commodities-bootstrap:v1', cyberThreats: 'cyber:threats-bootstrap:v2', techReadiness: 'economic:worldbank-techreadiness:v1', progressData: 'economic:worldbank-progress:v1', renewableEnergy: 'economic:worldbank-renewable:v1', positiveGeoEvents: 'positive_events:geo-bootstrap:v1', riskScores: 'risk:scores:sebuf:stale:v1', naturalEvents: 'natural:events:v1', flightDelays: 'aviation:delays-bootstrap:v1', insights: 'news:insights:v1', predictions: 'prediction:markets-bootstrap:v1', cryptoQuotes: 'market:crypto:v1', gulfQuotes: 'market:gulf-quotes:v1', stablecoinMarkets: 'market:stablecoins:v1', unrestEvents: 'unrest:events:v1', iranEvents: 'conflict:iran-events:v1', ucdpEvents: 'conflict:ucdp-events:v1', weatherAlerts: 'weather:alerts:v1', spending: 'economic:spending:v1', techEvents: 'research:tech-events-bootstrap:v1', gdeltIntel: 'intelligence:gdelt-intel:v1', correlationCards: 'correlation:cards-bootstrap:v1', forecasts: 'forecast:predictions:v2', securityAdvisories: 'intelligence:advisories-bootstrap:v1', customsRevenue: 'trade:customs-revenue:v1', sanctionsPressure: 'sanctions:pressure:v1', radiationWatch: 'radiation:observations:v1', }; const STANDALONE_KEYS = { serviceStatuses: 'infra:service-statuses:v1', macroSignals: 'economic:macro-signals:v1', bisPolicy: 'economic:bis:policy:v1', bisExchange: 'economic:bis:eer:v1', bisCredit: 'economic:bis:credit:v1', shippingRates: 'supply_chain:shipping:v2', chokepoints: 'supply_chain:chokepoints:v4', minerals: 'supply_chain:minerals:v2', giving: 'giving:summary:v1', gpsjam: 'intelligence:gpsjam:v2', theaterPosture: 'theater_posture:sebuf:stale:v1', theaterPostureLive: 'theater-posture:sebuf:v1', theaterPostureBackup: 'theater-posture:sebuf:backup:v1', riskScoresLive: 'risk:scores:sebuf:v1', usniFleet: 'usni-fleet:sebuf:v1', usniFleetStale: 'usni-fleet:sebuf:stale:v1', faaDelays: 'aviation:delays:faa:v1', intlDelays: 'aviation:delays:intl:v3', notamClosures: 'aviation:notam:closures:v2', positiveEventsLive: 'positive-events:geo:v1', cableHealth: 'cable-health-v1', cyberThreatsRpc: 'cyber:threats:v2', militaryBases: 'military:bases:active', militaryFlights: 'military:flights:v1', militaryFlightsStale: 'military:flights:stale:v1', temporalAnomalies: 'temporal:anomalies:v1', displacement: `displacement:summary:v1:${new Date().getFullYear()}`, satellites: 'intelligence:satellites:tle:v1', portwatch: 'supply_chain:portwatch:v1', corridorrisk: 'supply_chain:corridorrisk:v1', chokepointTransits: 'supply_chain:chokepoint_transits:v1', transitSummaries: 'supply_chain:transit-summaries:v1', thermalEscalation: 'thermal:escalation:v1', tariffTrendsUs: 'trade:tariffs:v1:840:all:10', }; const SEED_META = { earthquakes: { key: 'seed-meta:seismology:earthquakes', maxStaleMin: 30 }, wildfires: { key: 'seed-meta:wildfire:fires', maxStaleMin: 120 }, outages: { key: 'seed-meta:infra:outages', maxStaleMin: 30 }, climateAnomalies: { key: 'seed-meta:climate:anomalies', maxStaleMin: 120 }, unrestEvents: { key: 'seed-meta:unrest:events', maxStaleMin: 45 }, cyberThreats: { key: 'seed-meta:cyber:threats', maxStaleMin: 480 }, cryptoQuotes: { key: 'seed-meta:market:crypto', maxStaleMin: 30 }, etfFlows: { key: 'seed-meta:market:etf-flows', maxStaleMin: 60 }, gulfQuotes: { key: 'seed-meta:market:gulf-quotes', maxStaleMin: 30 }, stablecoinMarkets:{ key: 'seed-meta:market:stablecoins', maxStaleMin: 60 }, naturalEvents: { key: 'seed-meta:natural:events', maxStaleMin: 120 }, flightDelays: { key: 'seed-meta:aviation:faa', maxStaleMin: 60 }, notamClosures: { key: 'seed-meta:aviation:notam', maxStaleMin: 90 }, predictions: { key: 'seed-meta:prediction:markets', maxStaleMin: 30 }, insights: { key: 'seed-meta:news:insights', maxStaleMin: 30 }, marketQuotes: { key: 'seed-meta:market:stocks', maxStaleMin: 30 }, commodityQuotes: { key: 'seed-meta:market:commodities', maxStaleMin: 30 }, // RPC/warm-ping keys — seed-meta written by relay loops or handlers // serviceStatuses: moved to ON_DEMAND — RPC-populated, no dedicated seed, goes stale when no users visit cableHealth: { key: 'seed-meta:cable-health', maxStaleMin: 90 }, // ais-relay warm-ping runs every 30min; 90min = 3× interval catches missed pings without false positives macroSignals: { key: 'seed-meta:economic:macro-signals', maxStaleMin: 20 }, bisPolicy: { key: 'seed-meta:economic:bis:policy', maxStaleMin: 10080 }, bisExchange: { key: 'seed-meta:economic:bis:eer', maxStaleMin: 10080 }, bisCredit: { key: 'seed-meta:economic:bis:credit', maxStaleMin: 10080 }, shippingRates: { key: 'seed-meta:supply_chain:shipping', maxStaleMin: 420 }, chokepoints: { key: 'seed-meta:supply_chain:chokepoints', maxStaleMin: 60 }, minerals: { key: 'seed-meta:supply_chain:minerals', maxStaleMin: 10080 }, giving: { key: 'seed-meta:giving:summary', maxStaleMin: 10080 }, gpsjam: { key: 'seed-meta:intelligence:gpsjam', maxStaleMin: 720 }, positiveGeoEvents:{ key: 'seed-meta:positive-events:geo', maxStaleMin: 60 }, riskScores: { key: 'seed-meta:intelligence:risk-scores', maxStaleMin: 30 }, // CII warm-ping every 8min; 30min = ~3.5x interval, iranEvents: { key: 'seed-meta:conflict:iran-events', maxStaleMin: 10080 }, ucdpEvents: { key: 'seed-meta:conflict:ucdp-events', maxStaleMin: 420 }, militaryFlights: { key: 'seed-meta:military:flights', maxStaleMin: 30 }, // cron ~10min (LIVE_TTL=600s); 30min = 3x interval, militaryForecastInputs: { key: 'seed-meta:military-forecast-inputs', maxStaleMin: 30 }, // same cron as militaryFlights, satellites: { key: 'seed-meta:intelligence:satellites', maxStaleMin: 180 }, weatherAlerts: { key: 'seed-meta:weather:alerts', maxStaleMin: 30 }, spending: { key: 'seed-meta:economic:spending', maxStaleMin: 120 }, techEvents: { key: 'seed-meta:research:tech-events', maxStaleMin: 480 }, gdeltIntel: { key: 'seed-meta:intelligence:gdelt-intel', maxStaleMin: 420 }, // 6h cron + 1h grace; CACHE_TTL is 24h so per-topic merge always has a prior snapshot forecasts: { key: 'seed-meta:forecast:predictions', maxStaleMin: 90 }, sectors: { key: 'seed-meta:market:sectors', maxStaleMin: 30 }, techReadiness: { key: 'seed-meta:economic:worldbank-techreadiness:v1', maxStaleMin: 10080 }, progressData: { key: 'seed-meta:economic:worldbank-progress:v1', maxStaleMin: 10080 }, renewableEnergy: { key: 'seed-meta:economic:worldbank-renewable:v1', maxStaleMin: 10080 }, intlDelays: { key: 'seed-meta:aviation:intl', maxStaleMin: 90 }, faaDelays: { key: 'seed-meta:aviation:faa', maxStaleMin: 60 }, theaterPosture: { key: 'seed-meta:theater-posture', maxStaleMin: 60 }, correlationCards: { key: 'seed-meta:correlation:cards', maxStaleMin: 15 }, portwatch: { key: 'seed-meta:supply_chain:portwatch', maxStaleMin: 720 }, corridorrisk: { key: 'seed-meta:supply_chain:corridorrisk', maxStaleMin: 120 }, chokepointTransits: { key: 'seed-meta:supply_chain:chokepoint_transits', maxStaleMin: 30 }, // relay every 10min; 30min = 3x interval, transitSummaries: { key: 'seed-meta:supply_chain:transit-summaries', maxStaleMin: 30 }, // relay every 10min; 30min = 3x interval, usniFleet: { key: 'seed-meta:military:usni-fleet', maxStaleMin: 480 }, securityAdvisories: { key: 'seed-meta:intelligence:advisories', maxStaleMin: 120 }, customsRevenue: { key: 'seed-meta:trade:customs-revenue', maxStaleMin: 1440 }, sanctionsPressure: { key: 'seed-meta:sanctions:pressure', maxStaleMin: 720 }, radiationWatch: { key: 'seed-meta:radiation:observations', maxStaleMin: 30 }, thermalEscalation: { key: 'seed-meta:thermal:escalation', maxStaleMin: 240 }, tariffTrendsUs: { key: 'seed-meta:trade:tariffs:v1:840:all:10', maxStaleMin: 900 }, }; // Standalone keys that are populated on-demand by RPC handlers (not seeds). // Empty = WARN not CRIT since they only exist after first request. const ON_DEMAND_KEYS = new Set([ 'riskScoresLive', 'usniFleetStale', 'positiveEventsLive', 'bisPolicy', 'bisExchange', 'bisCredit', 'macroSignals', 'shippingRates', 'chokepoints', 'minerals', 'giving', 'cyberThreatsRpc', 'militaryBases', 'temporalAnomalies', 'displacement', 'corridorrisk', // intermediate key; data flows through transit-summaries:v1 'serviceStatuses', // RPC-populated; seed-meta written on fresh fetch only, goes stale between visits ]); // Keys where 0 records is a valid healthy state (e.g. no airports closed). // The key must still exist in Redis; only the record count can be 0. const EMPTY_DATA_OK_KEYS = new Set(['notamClosures', 'faaDelays', 'gpsjam']); // Cascade groups: if any key in the group has data, all empty siblings are OK. // Theater posture uses live → stale → backup fallback chain. const CASCADE_GROUPS = { theaterPosture: ['theaterPosture', 'theaterPostureLive', 'theaterPostureBackup'], theaterPostureLive: ['theaterPosture', 'theaterPostureLive', 'theaterPostureBackup'], theaterPostureBackup: ['theaterPosture', 'theaterPostureLive', 'theaterPostureBackup'], militaryFlights: ['militaryFlights', 'militaryFlightsStale'], militaryFlightsStale: ['militaryFlights', 'militaryFlightsStale'], }; const NEG_SENTINEL = '__WM_NEG__'; async function redisPipeline(commands) { const url = process.env.UPSTASH_REDIS_REST_URL; const token = process.env.UPSTASH_REDIS_REST_TOKEN; if (!url || !token) throw new Error('Redis not configured'); const resp = await fetch(`${url}/pipeline`, { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify(commands), signal: AbortSignal.timeout(8_000), }); if (!resp.ok) throw new Error(`Redis HTTP ${resp.status}`); return resp.json(); } function parseRedisValue(raw) { if (!raw || raw === NEG_SENTINEL) return null; try { return JSON.parse(raw); } catch { return raw; } } function dataSize(parsed) { if (!parsed) return 0; if (Array.isArray(parsed)) return parsed.length; if (typeof parsed === 'object') { for (const k of ['quotes', 'hexes', 'events', 'stablecoins', 'fires', 'threats', 'earthquakes', 'outages', 'delays', 'items', 'predictions', 'alerts', 'awards', 'papers', 'repos', 'articles', 'signals', 'rates', 'countries', 'chokepoints', 'minerals', 'anomalies', 'flows', 'bases', 'flights', 'theaters', 'fleets', 'warnings', 'closures', 'cables', 'airports', 'closedIcaos', 'categories', 'regions', 'entries', 'satellites', 'sectors', 'statuses', 'scores', 'topics', 'advisories', 'months']) { if (Array.isArray(parsed[k])) return parsed[k].length; } return Object.keys(parsed).length; } return typeof parsed === 'string' ? parsed.length : 1; } export default async function handler(req) { const headers = { 'Content-Type': 'application/json', 'Cache-Control': 'private, no-store, max-age=0', 'CDN-Cache-Control': 'no-store', 'Access-Control-Allow-Origin': '*', }; if (req.method === 'OPTIONS') { return new Response(null, { status: 204, headers }); } const now = Date.now(); const allDataKeys = [ ...Object.values(BOOTSTRAP_KEYS), ...Object.values(STANDALONE_KEYS), ]; const allMetaKeys = Object.values(SEED_META).map(s => s.key); const allKeys = [...allDataKeys, ...allMetaKeys]; let results; try { const commands = allKeys.map(k => ['GET', k]); results = await redisPipeline(commands); } catch (err) { return jsonResponse({ status: 'REDIS_DOWN', error: err.message, checkedAt: new Date(now).toISOString(), }, 503, headers); } const keyValues = new Map(); for (let i = 0; i < allKeys.length; i++) { keyValues.set(allKeys[i], results[i]?.result ?? null); } const checks = {}; let totalChecks = 0; let okCount = 0; let warnCount = 0; let critCount = 0; for (const [name, redisKey] of Object.entries(BOOTSTRAP_KEYS)) { totalChecks++; const raw = keyValues.get(redisKey); const parsed = parseRedisValue(raw); const size = dataSize(parsed); const seedCfg = SEED_META[name]; let seedAge = null; let seedStale = null; if (seedCfg) { const metaRaw = keyValues.get(seedCfg.key); const meta = parseRedisValue(metaRaw); if (meta?.fetchedAt) { seedAge = Math.round((now - meta.fetchedAt) / 60_000); seedStale = seedAge > seedCfg.maxStaleMin; } else { seedStale = true; } } let status; if (!parsed || raw === NEG_SENTINEL) { status = 'EMPTY'; critCount++; } else if (size === 0) { status = 'EMPTY_DATA'; critCount++; } else if (seedStale === true) { status = 'STALE_SEED'; warnCount++; } else { status = 'OK'; okCount++; } const entry = { status, records: size }; if (seedAge !== null) entry.seedAgeMin = seedAge; if (seedCfg) entry.maxStaleMin = seedCfg.maxStaleMin; checks[name] = entry; } for (const [name, redisKey] of Object.entries(STANDALONE_KEYS)) { totalChecks++; const raw = keyValues.get(redisKey); const parsed = parseRedisValue(raw); const size = dataSize(parsed); const isOnDemand = ON_DEMAND_KEYS.has(name); const seedCfg = SEED_META[name]; // Freshness tracking for standalone keys (same logic as bootstrap keys) let seedAge = null; let seedStale = null; if (seedCfg) { const metaRaw = keyValues.get(seedCfg.key); const meta = parseRedisValue(metaRaw); if (meta?.fetchedAt) { seedAge = Math.round((now - meta.fetchedAt) / 60_000); seedStale = seedAge > seedCfg.maxStaleMin; } else { // No seed-meta → data exists but freshness is unknown → stale seedStale = true; } } // Cascade: if this key is empty but a sibling in the cascade group has data, it's OK. const cascadeSiblings = CASCADE_GROUPS[name]; let cascadeCovered = false; if (cascadeSiblings && (!parsed || size === 0)) { for (const sibling of cascadeSiblings) { if (sibling === name) continue; const sibKey = STANDALONE_KEYS[sibling]; if (!sibKey) continue; const sibRaw = keyValues.get(sibKey); const sibParsed = parseRedisValue(sibRaw); if (sibParsed && dataSize(sibParsed) > 0) { cascadeCovered = true; break; } } } let status; if (!parsed || raw === NEG_SENTINEL) { if (cascadeCovered) { status = 'OK_CASCADE'; okCount++; } else if (EMPTY_DATA_OK_KEYS.has(name)) { if (seedStale === true) { status = 'STALE_SEED'; warnCount++; } else { status = 'OK'; okCount++; } } else if (isOnDemand) { status = 'EMPTY_ON_DEMAND'; warnCount++; } else { status = 'EMPTY'; critCount++; } } else if (size === 0) { if (cascadeCovered) { status = 'OK_CASCADE'; okCount++; } else if (EMPTY_DATA_OK_KEYS.has(name)) { if (seedStale === true) { status = 'STALE_SEED'; warnCount++; } else { status = 'OK'; okCount++; } } else if (isOnDemand) { status = 'EMPTY_ON_DEMAND'; warnCount++; } else { status = 'EMPTY_DATA'; critCount++; } } else if (seedStale === true) { status = 'STALE_SEED'; warnCount++; } else { status = 'OK'; okCount++; } const entry = { status, records: size }; if (seedAge !== null) entry.seedAgeMin = seedAge; if (seedCfg) entry.maxStaleMin = seedCfg.maxStaleMin; checks[name] = entry; } // On-demand keys that simply haven't been requested yet should not affect overall status. const onDemandWarnCount = Object.values(checks).filter(c => c.status === 'EMPTY_ON_DEMAND').length; const realWarnCount = warnCount - onDemandWarnCount; let overall; if (critCount === 0 && realWarnCount === 0) overall = 'HEALTHY'; else if (critCount === 0) overall = 'WARNING'; else if (critCount <= 3) overall = 'DEGRADED'; else overall = 'UNHEALTHY'; const httpStatus = overall === 'HEALTHY' || overall === 'WARNING' ? 200 : 503; const url = new URL(req.url); const compact = url.searchParams.get('compact') === '1'; const body = { status: overall, summary: { total: totalChecks, ok: okCount, warn: warnCount, crit: critCount, }, checkedAt: new Date(now).toISOString(), }; if (!compact) { body.checks = checks; } else { const problems = {}; for (const [name, check] of Object.entries(checks)) { if (check.status !== 'OK' && check.status !== 'OK_CASCADE') problems[name] = check; } if (Object.keys(problems).length > 0) body.problems = problems; } return new Response(JSON.stringify(body, null, compact ? 0 : 2), { status: httpStatus, headers, }); } ================================================ FILE: api/imagery/v1/[rpc].ts ================================================ export const config = { runtime: 'edge' }; import { createDomainGateway, serverOptions } from '../../../server/gateway'; import { createImageryServiceRoutes } from '../../../src/generated/server/worldmonitor/imagery/v1/service_server'; import { imageryHandler } from '../../../server/worldmonitor/imagery/v1/handler'; export default createDomainGateway( createImageryServiceRoutes(imageryHandler, serverOptions), ); ================================================ FILE: api/infrastructure/v1/[rpc].ts ================================================ export const config = { runtime: 'edge' }; import { createDomainGateway, serverOptions } from '../../../server/gateway'; import { createInfrastructureServiceRoutes } from '../../../src/generated/server/worldmonitor/infrastructure/v1/service_server'; import { infrastructureHandler } from '../../../server/worldmonitor/infrastructure/v1/handler'; export default createDomainGateway( createInfrastructureServiceRoutes(infrastructureHandler, serverOptions), ); ================================================ FILE: api/intelligence/v1/[rpc].ts ================================================ export const config = { runtime: 'edge' }; import { createDomainGateway, serverOptions } from '../../../server/gateway'; import { createIntelligenceServiceRoutes } from '../../../src/generated/server/worldmonitor/intelligence/v1/service_server'; import { intelligenceHandler } from '../../../server/worldmonitor/intelligence/v1/handler'; export default createDomainGateway( createIntelligenceServiceRoutes(intelligenceHandler, serverOptions), ); ================================================ FILE: api/loaders-xml-wms-regression.test.mjs ================================================ import { strict as assert } from 'node:assert'; import test from 'node:test'; import { XMLLoader } from '@loaders.gl/xml'; import { WMSCapabilitiesLoader, WMSErrorLoader, _WMSFeatureInfoLoader } from '@loaders.gl/wms'; const WMS_CAPABILITIES_XML = ` WMS Test Service alerts world image/png image/jpeg application/vnd.ogc.se_xml Root Layer EPSG:4326 -180 180 -90 90 alerts Alerts 2024-01-01/2024-12-31/P1D `; test('XMLLoader keeps namespace stripping + array paths stable', () => { const xml = 'okyo'; const parsed = XMLLoader.parseTextSync(xml, { xml: { removeNSPrefix: true, arrayPaths: ['root.Child'], }, }); assert.deepEqual(parsed, { root: { Child: [ { value: 'ok', attr: 'x' }, { value: 'yo', attr: 'y' }, ], }, }); }); test('WMSCapabilitiesLoader parses core typed fields from XML capabilities', () => { const parsed = WMSCapabilitiesLoader.parseTextSync(WMS_CAPABILITIES_XML); assert.equal(parsed.version, '1.3.0'); assert.equal(parsed.name, 'WMS'); assert.deepEqual(parsed.requests.GetMap.mimeTypes, ['image/png', 'image/jpeg']); assert.equal(parsed.layers.length, 1); const rootLayer = parsed.layers[0]; assert.deepEqual(rootLayer.geographicBoundingBox, [[-180, -90], [180, 90]]); const alertsLayer = rootLayer.layers[0]; assert.equal(alertsLayer.name, 'alerts'); assert.equal(alertsLayer.queryable, true); assert.deepEqual(alertsLayer.boundingBoxes[0], { crs: 'EPSG:4326', boundingBox: [[-10, -20], [30, 40]], }); assert.deepEqual(alertsLayer.dimensions[0], { name: 'time', units: 'ISO8601', extent: '2024-01-01/2024-12-31/P1D', defaultValue: '2024-01-01', nearestValue: true, }); }); test('WMSErrorLoader extracts namespaced error text and honors throw options', () => { const namespacedErrorXml = 'Bad layer'; const defaultMessage = WMSErrorLoader.parseTextSync(namespacedErrorXml); assert.equal(defaultMessage, 'WMS Service error: Bad layer'); const minimalMessage = WMSErrorLoader.parseTextSync(namespacedErrorXml, { wms: { minimalErrors: true }, }); assert.equal(minimalMessage, 'Bad layer'); assert.throws( () => WMSErrorLoader.parseTextSync(namespacedErrorXml, { wms: { throwOnError: true } }), /WMS Service error: Bad layer/ ); }); test('WMS feature info parsing remains stable for single and repeated FIELDS nodes', () => { const singleFieldsXml = ''; const manyFieldsXml = ''; const single = _WMSFeatureInfoLoader.parseTextSync(singleFieldsXml); const many = _WMSFeatureInfoLoader.parseTextSync(manyFieldsXml); assert.equal(single.features.length, 1); assert.deepEqual(single.features[0]?.attributes, { id: '1', label: 'one' }); assert.equal(many.features.length, 2); assert.equal(many.features[0]?.attributes?.id, '1'); assert.equal(many.features[1]?.attributes?.id, '2'); }); ================================================ FILE: api/maritime/v1/[rpc].ts ================================================ export const config = { runtime: 'edge' }; import { createDomainGateway, serverOptions } from '../../../server/gateway'; import { createMaritimeServiceRoutes } from '../../../src/generated/server/worldmonitor/maritime/v1/service_server'; import { maritimeHandler } from '../../../server/worldmonitor/maritime/v1/handler'; export default createDomainGateway( createMaritimeServiceRoutes(maritimeHandler, serverOptions), ); ================================================ FILE: api/market/v1/[rpc].ts ================================================ export const config = { runtime: 'edge' }; import { createDomainGateway, serverOptions } from '../../../server/gateway'; import { createMarketServiceRoutes } from '../../../src/generated/server/worldmonitor/market/v1/service_server'; import { marketHandler } from '../../../server/worldmonitor/market/v1/handler'; export default createDomainGateway( createMarketServiceRoutes(marketHandler, serverOptions), ); ================================================ FILE: api/mcp-proxy.js ================================================ import { getCorsHeaders, isDisallowedOrigin } from './_cors.js'; import { jsonResponse } from './_json-response.js'; export const config = { runtime: 'edge' }; const TIMEOUT_MS = 15_000; const SSE_CONNECT_TIMEOUT_MS = 10_000; const SSE_RPC_TIMEOUT_MS = 12_000; const MCP_PROTOCOL_VERSION = '2025-03-26'; const BLOCKED_HOST_PATTERNS = [ /^localhost$/i, /^127\./, /^10\./, /^172\.(1[6-9]|2\d|3[01])\./, /^192\.168\./, /^169\.254\./, // link-local + cloud metadata (AWS/GCP/Azure) /^::1$/, /^fd[0-9a-f]{2}:/i, /^fe80:/i, ]; function buildInitPayload() { return { jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: MCP_PROTOCOL_VERSION, capabilities: {}, clientInfo: { name: 'worldmonitor', version: '1.0' }, }, }; } function validateServerUrl(raw) { let url; try { url = new URL(raw); } catch { return null; } if (url.protocol !== 'https:' && url.protocol !== 'http:') return null; const host = url.hostname; if (BLOCKED_HOST_PATTERNS.some(p => p.test(host))) return null; return url; } function buildHeaders(customHeaders) { const h = { 'Content-Type': 'application/json', 'Accept': 'application/json, text/event-stream', 'User-Agent': 'WorldMonitor-MCP-Proxy/1.0', }; if (customHeaders && typeof customHeaders === 'object') { for (const [k, v] of Object.entries(customHeaders)) { if (typeof k === 'string' && typeof v === 'string') { // Strip CRLF to prevent header injection const safeKey = k.replace(/[\r\n]/g, ''); const safeVal = v.replace(/[\r\n]/g, ''); if (safeKey) h[safeKey] = safeVal; } } } return h; } // --- Streamable HTTP transport (MCP 2025-03-26) --- async function postJson(url, body, headers, sessionId) { const h = { ...headers }; if (sessionId) h['Mcp-Session-Id'] = sessionId; const resp = await fetch(url.toString(), { method: 'POST', headers: h, body: JSON.stringify(body), signal: AbortSignal.timeout(TIMEOUT_MS), }); return resp; } async function parseJsonRpcResponse(resp) { const ct = resp.headers.get('content-type') || ''; if (ct.includes('text/event-stream')) { const text = await resp.text(); const lines = text.split('\n'); for (const line of lines) { if (line.startsWith('data: ')) { try { const parsed = JSON.parse(line.slice(6)); if (parsed.result !== undefined || parsed.error !== undefined) return parsed; } catch { /* skip */ } } } throw new Error('No result found in SSE response'); } return resp.json(); } async function sendInitialized(serverUrl, headers, sessionId) { try { await postJson(serverUrl, { jsonrpc: '2.0', method: 'notifications/initialized', params: {}, }, headers, sessionId); } catch { /* non-fatal */ } } async function mcpListTools(serverUrl, customHeaders) { const headers = buildHeaders(customHeaders); const initResp = await postJson(serverUrl, buildInitPayload(), headers, null); if (!initResp.ok) throw new Error(`Initialize failed: HTTP ${initResp.status}`); const sessionId = initResp.headers.get('Mcp-Session-Id') || initResp.headers.get('mcp-session-id'); const initData = await parseJsonRpcResponse(initResp); if (initData.error) throw new Error(`Initialize error: ${initData.error.message}`); await sendInitialized(serverUrl, headers, sessionId); const listResp = await postJson(serverUrl, { jsonrpc: '2.0', id: 2, method: 'tools/list', params: {}, }, headers, sessionId); if (!listResp.ok) throw new Error(`tools/list failed: HTTP ${listResp.status}`); const listData = await parseJsonRpcResponse(listResp); if (listData.error) throw new Error(`tools/list error: ${listData.error.message}`); return listData.result?.tools || []; } async function mcpCallTool(serverUrl, toolName, toolArgs, customHeaders) { const headers = buildHeaders(customHeaders); const initResp = await postJson(serverUrl, buildInitPayload(), headers, null); if (!initResp.ok) throw new Error(`Initialize failed: HTTP ${initResp.status}`); const sessionId = initResp.headers.get('Mcp-Session-Id') || initResp.headers.get('mcp-session-id'); const initData = await parseJsonRpcResponse(initResp); if (initData.error) throw new Error(`Initialize error: ${initData.error.message}`); await sendInitialized(serverUrl, headers, sessionId); const callResp = await postJson(serverUrl, { jsonrpc: '2.0', id: 3, method: 'tools/call', params: { name: toolName, arguments: toolArgs || {} }, }, headers, sessionId); if (!callResp.ok) throw new Error(`tools/call failed: HTTP ${callResp.status}`); const callData = await parseJsonRpcResponse(callResp); if (callData.error) throw new Error(`tools/call error: ${callData.error.message}`); return callData.result; } // --- SSE transport (HTTP+SSE, older MCP spec) --- // Servers whose URL path ends with /sse use this protocol: // 1. Client GETs the SSE URL — server opens a stream and emits an `endpoint` event // containing the URL where the client should POST JSON-RPC messages. // 2. Client POSTs JSON-RPC to that endpoint URL. // 3. Server sends responses on the same SSE stream as `data:` lines. function isSseTransport(url) { const p = url.pathname; return p === '/sse' || p.endsWith('/sse'); } function makeDeferred() { let resolve, reject; const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); return { promise, resolve, reject }; } class SseSession { constructor(sseUrl, headers) { this._sseUrl = sseUrl; this._headers = headers; this._endpointUrl = null; this._endpointDeferred = makeDeferred(); this._pending = new Map(); // rpc id -> deferred this._reader = null; } async connect() { const resp = await fetch(this._sseUrl, { headers: { ...this._headers, Accept: 'text/event-stream', 'Cache-Control': 'no-cache' }, signal: AbortSignal.timeout(SSE_CONNECT_TIMEOUT_MS), }); if (!resp.ok) throw new Error(`SSE connect HTTP ${resp.status}`); this._reader = resp.body.getReader(); this._startReadLoop(); await this._endpointDeferred.promise; } _startReadLoop() { const dec = new TextDecoder(); let buf = ''; let eventType = ''; const reader = this._reader; const self = this; (async () => { try { while (true) { const { done, value } = await reader.read(); if (done) { // Stream closed — if endpoint never arrived, reject so connect() throws if (!self._endpointUrl) { self._endpointDeferred.reject(new Error('SSE stream closed before endpoint event')); } for (const [, d] of self._pending) d.reject(new Error('SSE stream closed')); break; } buf += dec.decode(value, { stream: true }); const lines = buf.split('\n'); buf = lines.pop() ?? ''; for (const line of lines) { if (line.startsWith('event: ')) { eventType = line.slice(7).trim(); } else if (line.startsWith('data: ')) { const data = line.slice(6).trim(); if (eventType === 'endpoint') { // Resolve endpoint URL (relative path or absolute) then re-validate // to prevent SSRF: a malicious server could emit an RFC1918 address. let resolved; try { resolved = new URL(data.startsWith('http') ? data : data, self._sseUrl); } catch { self._endpointDeferred.reject(new Error('SSE endpoint event contains invalid URL')); return; } if (resolved.protocol !== 'https:' && resolved.protocol !== 'http:') { self._endpointDeferred.reject(new Error('SSE endpoint protocol not allowed')); return; } if (BLOCKED_HOST_PATTERNS.some(p => p.test(resolved.hostname))) { self._endpointDeferred.reject(new Error('SSE endpoint host is blocked')); return; } self._endpointUrl = resolved.toString(); self._endpointDeferred.resolve(); } else { try { const msg = JSON.parse(data); if (msg.id !== undefined) { const d = self._pending.get(msg.id); if (d) { self._pending.delete(msg.id); d.resolve(msg); } } } catch { /* skip non-JSON data lines */ } } eventType = ''; } } } } catch (err) { self._endpointDeferred.reject(err); for (const [, d] of self._pending) d.reject(new Error('SSE stream closed')); } })(); } async send(id, method, params) { const deferred = makeDeferred(); this._pending.set(id, deferred); const timer = setTimeout(() => { if (this._pending.has(id)) { this._pending.delete(id); deferred.reject(new Error(`RPC ${method} timed out`)); } }, SSE_RPC_TIMEOUT_MS); try { const postResp = await fetch(this._endpointUrl, { method: 'POST', headers: { ...this._headers, 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id, method, params }), signal: AbortSignal.timeout(SSE_RPC_TIMEOUT_MS), }); if (!postResp.ok) { this._pending.delete(id); throw new Error(`${method} POST HTTP ${postResp.status}`); } return await deferred.promise; } finally { clearTimeout(timer); } } async notify(method, params) { await fetch(this._endpointUrl, { method: 'POST', headers: { ...this._headers, 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', method, params }), signal: AbortSignal.timeout(5_000), }).catch(() => {}); } close() { try { this._reader?.cancel(); } catch { /* ignore */ } } } async function mcpListToolsSse(serverUrl, customHeaders) { const headers = buildHeaders(customHeaders); const session = new SseSession(serverUrl.toString(), headers); try { await session.connect(); const initResp = await session.send(1, 'initialize', { protocolVersion: MCP_PROTOCOL_VERSION, capabilities: {}, clientInfo: { name: 'worldmonitor', version: '1.0' }, }); if (initResp.error) throw new Error(`Initialize error: ${initResp.error.message}`); await session.notify('notifications/initialized', {}); const listResp = await session.send(2, 'tools/list', {}); if (listResp.error) throw new Error(`tools/list error: ${listResp.error.message}`); return listResp.result?.tools || []; } finally { session.close(); } } async function mcpCallToolSse(serverUrl, toolName, toolArgs, customHeaders) { const headers = buildHeaders(customHeaders); const session = new SseSession(serverUrl.toString(), headers); try { await session.connect(); const initResp = await session.send(1, 'initialize', { protocolVersion: MCP_PROTOCOL_VERSION, capabilities: {}, clientInfo: { name: 'worldmonitor', version: '1.0' }, }); if (initResp.error) throw new Error(`Initialize error: ${initResp.error.message}`); await session.notify('notifications/initialized', {}); const callResp = await session.send(2, 'tools/call', { name: toolName, arguments: toolArgs || {} }); if (callResp.error) throw new Error(`tools/call error: ${callResp.error.message}`); return callResp.result; } finally { session.close(); } } // --- Request handler --- export default async function handler(req) { if (isDisallowedOrigin(req)) return new Response('Forbidden', { status: 403 }); const cors = getCorsHeaders(req, 'GET, POST, OPTIONS'); if (req.method === 'OPTIONS') return new Response(null, { status: 204, headers: cors }); try { if (req.method === 'GET') { const url = new URL(req.url); const rawServer = url.searchParams.get('serverUrl'); const rawHeaders = url.searchParams.get('headers'); if (!rawServer) return jsonResponse({ error: 'Missing serverUrl' }, 400, cors); const serverUrl = validateServerUrl(rawServer); if (!serverUrl) return jsonResponse({ error: 'Invalid serverUrl' }, 400, cors); let customHeaders = {}; if (rawHeaders) { try { customHeaders = JSON.parse(rawHeaders); } catch { /* ignore */ } } const tools = isSseTransport(serverUrl) ? await mcpListToolsSse(serverUrl, customHeaders) : await mcpListTools(serverUrl, customHeaders); return jsonResponse({ tools }, 200, cors); } if (req.method === 'POST') { const body = await req.json(); const { serverUrl: rawServer, toolName, toolArgs, customHeaders } = body; if (!rawServer) return jsonResponse({ error: 'Missing serverUrl' }, 400, cors); if (!toolName) return jsonResponse({ error: 'Missing toolName' }, 400, cors); const serverUrl = validateServerUrl(rawServer); if (!serverUrl) return jsonResponse({ error: 'Invalid serverUrl' }, 400, cors); const result = isSseTransport(serverUrl) ? await mcpCallToolSse(serverUrl, toolName, toolArgs || {}, customHeaders || {}) : await mcpCallTool(serverUrl, toolName, toolArgs || {}, customHeaders || {}); return jsonResponse({ result }, 200, { ...cors, 'Cache-Control': 'no-store' }); } return jsonResponse({ error: 'Method not allowed' }, 405, cors); } catch (err) { const msg = err instanceof Error ? err.message : String(err); const isTimeout = msg.includes('TimeoutError') || msg.includes('timed out'); // Return 422 (not 502) so Cloudflare proxy does not replace our JSON body with its own HTML error page return jsonResponse({ error: isTimeout ? 'MCP server timed out' : msg }, isTimeout ? 504 : 422, cors); } } ================================================ FILE: api/military/v1/[rpc].ts ================================================ export const config = { runtime: 'edge' }; import { createDomainGateway, serverOptions } from '../../../server/gateway'; import { createMilitaryServiceRoutes } from '../../../src/generated/server/worldmonitor/military/v1/service_server'; import { militaryHandler } from '../../../server/worldmonitor/military/v1/handler'; export default createDomainGateway( createMilitaryServiceRoutes(militaryHandler, serverOptions), ); ================================================ FILE: api/military-flights.js ================================================ import { getCorsHeaders, isDisallowedOrigin } from './_cors.js'; import { jsonResponse } from './_json-response.js'; import { readJsonFromUpstash } from './_upstash-json.js'; export const config = { runtime: 'edge' }; const REDIS_KEY = 'military:flights:v1'; const STALE_KEY = 'military:flights:stale:v1'; let cached = null; let cachedAt = 0; const CACHE_TTL = 120_000; let negUntil = 0; const NEG_TTL = 30_000; async function fetchMilitaryFlightsData() { const now = Date.now(); if (cached && now - cachedAt < CACHE_TTL) return cached; if (now < negUntil) return null; let data; try { data = await readJsonFromUpstash(REDIS_KEY); } catch { data = null; } if (!data) { try { data = await readJsonFromUpstash(STALE_KEY); } catch { data = null; } } if (!data) { negUntil = now + NEG_TTL; return null; } cached = data; cachedAt = now; return data; } export default async function handler(req) { const corsHeaders = getCorsHeaders(req, 'GET, OPTIONS'); if (req.method === 'OPTIONS') { return new Response(null, { status: 204, headers: corsHeaders }); } if (isDisallowedOrigin(req)) { return jsonResponse({ error: 'Origin not allowed' }, 403, corsHeaders); } const data = await fetchMilitaryFlightsData(); if (!data) { return jsonResponse( { error: 'Military flight data temporarily unavailable' }, 503, { 'Cache-Control': 'no-cache, no-store', ...corsHeaders }, ); } return jsonResponse( data, 200, { 'Cache-Control': 's-maxage=120, stale-while-revalidate=60, stale-if-error=300', ...corsHeaders, }, ); } ================================================ FILE: api/natural/v1/[rpc].ts ================================================ export const config = { runtime: 'edge' }; import { createDomainGateway, serverOptions } from '../../../server/gateway'; import { createNaturalServiceRoutes } from '../../../src/generated/server/worldmonitor/natural/v1/service_server'; import { naturalHandler } from '../../../server/worldmonitor/natural/v1/handler'; export default createDomainGateway( createNaturalServiceRoutes(naturalHandler, serverOptions), ); ================================================ FILE: api/news/v1/[rpc].ts ================================================ export const config = { runtime: 'edge' }; import { createDomainGateway, serverOptions } from '../../../server/gateway'; import { createNewsServiceRoutes } from '../../../src/generated/server/worldmonitor/news/v1/service_server'; import { newsHandler } from '../../../server/worldmonitor/news/v1/handler'; export default createDomainGateway( createNewsServiceRoutes(newsHandler, serverOptions), ); ================================================ FILE: api/og-story.js ================================================ // Non-sebuf: returns XML/HTML, stays as standalone Vercel function /** * Dynamic OG Image Generator for Story Sharing * Returns an SVG image (1200x630) — rich intelligence card for social previews. */ const COUNTRY_NAMES = { UA: 'Ukraine', RU: 'Russia', CN: 'China', US: 'United States', IR: 'Iran', IL: 'Israel', TW: 'Taiwan', KP: 'North Korea', SA: 'Saudi Arabia', TR: 'Turkey', PL: 'Poland', DE: 'Germany', FR: 'France', GB: 'United Kingdom', IN: 'India', PK: 'Pakistan', SY: 'Syria', YE: 'Yemen', MM: 'Myanmar', VE: 'Venezuela', }; const LEVEL_COLORS = { critical: '#ef4444', high: '#f97316', elevated: '#eab308', normal: '#22c55e', low: '#3b82f6', }; const LEVEL_LABELS = { critical: 'CRITICAL INSTABILITY', high: 'HIGH INSTABILITY', elevated: 'ELEVATED INSTABILITY', normal: 'STABLE', low: 'LOW RISK', }; function normalizeLevel(rawLevel) { const level = String(rawLevel || '').toLowerCase(); return Object.hasOwn(LEVEL_COLORS, level) ? level : 'normal'; } export default function handler(req, res) { const url = new URL(req.url, `https://${req.headers.host}`); const countryCode = (url.searchParams.get('c') || '').toUpperCase(); const type = url.searchParams.get('t') || 'ciianalysis'; const score = url.searchParams.get('s'); const level = normalizeLevel(url.searchParams.get('l')); const countryName = COUNTRY_NAMES[countryCode] || countryCode || 'Global'; const levelColor = LEVEL_COLORS[level] || '#eab308'; const levelLabel = LEVEL_LABELS[level] || 'MONITORING'; const parsedScore = score ? Number.parseInt(score, 10) : Number.NaN; const scoreNum = Number.isFinite(parsedScore) ? Math.max(0, Math.min(100, parsedScore)) : null; const dateStr = new Date().toISOString().slice(0, 10); // Score arc (semicircle gauge) const arcRadius = 90; const arcCx = 960; const arcCy = 340; const scoreAngle = scoreNum !== null ? (scoreNum / 100) * Math.PI : 0; const arcEndX = arcCx - arcRadius * Math.cos(scoreAngle); const arcEndY = arcCy - arcRadius * Math.sin(scoreAngle); const largeArc = scoreNum > 50 ? 1 : 0; const svg = ` ${Array.from({length: 30}, (_, i) => ``).join('\n ')} ${Array.from({length: 16}, (_, i) => ``).join('\n ')} WORLDMONITOR ${levelLabel} ${dateStr} ${escapeXml(countryName.toUpperCase())} ${escapeXml(countryCode)} INTELLIGENCE BRIEF ${scoreNum !== null ? ` ${scoreNum} /100 INSTABILITY INDEX 0 25 50 75 100 ${scoreNum > 0 ? `` : ''} ${scoreNum} /100 ${level.toUpperCase()} Threat Classification Military Posture Prediction Markets Signal Convergence Active Signals ` : ` Real-time intelligence analysis Instability Index 20 countries monitored Military Tracking Live flights & vessels Prediction Markets Polymarket integration Signal Convergence Multi-source correlation `} W WORLDMONITOR Real-time global intelligence monitoring VIEW FULL BRIEF → worldmonitor.app · ${dateStr} · Free & open source `; res.setHeader('Content-Type', 'image/svg+xml'); res.setHeader('Cache-Control', 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600'); res.status(200).send(svg); } function escapeXml(str) { return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } ================================================ FILE: api/og-story.test.mjs ================================================ import { strict as assert } from 'node:assert'; import test from 'node:test'; import handler from './og-story.js'; function renderOgStory(query = '') { const req = { url: `https://worldmonitor.app/api/og-story${query ? `?${query}` : ''}`, headers: { host: 'worldmonitor.app' }, }; let statusCode = 0; let body = ''; const headers = {}; const res = { setHeader(name, value) { headers[String(name).toLowerCase()] = String(value); }, status(code) { statusCode = code; return this; }, send(payload) { body = String(payload); }, }; handler(req, res); return { statusCode, body, headers }; } test('normalizes unsupported level values to prevent SVG script injection', () => { const injectedLevel = encodeURIComponent(''); const response = renderOgStory(`c=US&s=50&l=${injectedLevel}`); assert.equal(response.statusCode, 200); assert.equal(/ `; return new Response(html, { status: 200, headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'public, s-maxage=900, stale-while-revalidate=300', // Allow the nested YouTube iframe to call requestStorageAccess() for // unpartitioned cookie access (lets signed-in users skip bot-check). // Scope storage-access permission to self and YouTube only rather than *. 'permissions-policy': 'storage-access=(self "https://www.youtube.com")', }, }); } ================================================ FILE: api/youtube/embed.test.mjs ================================================ import { strict as assert } from 'node:assert'; import test from 'node:test'; import handler from './embed.js'; function makeRequest(query = '') { return new Request(`https://worldmonitor.app/api/youtube/embed${query}`); } test('rejects missing or invalid video ids', async () => { const missing = await handler(makeRequest()); assert.equal(missing.status, 400); const invalid = await handler(makeRequest('?videoId=bad')); assert.equal(invalid.status, 400); }); test('returns embeddable html for valid video id', async () => { const response = await handler(makeRequest('?videoId=iEpJwprxDdk&autoplay=0&mute=1')); assert.equal(response.status, 200); assert.equal(response.headers.get('content-type')?.includes('text/html'), true); const html = await response.text(); assert.equal(html.includes("videoId:'iEpJwprxDdk'"), true); assert.equal(html.includes("host:'https://www.youtube.com'"), true); assert.equal(html.includes('autoplay:0'), true); assert.equal(html.includes('mute:1'), true); assert.equal(html.includes('origin:"https://worldmonitor.app"'), true); assert.equal(html.includes('postMessage'), true); }); test('accepts custom origin parameter', async () => { const response = await handler(makeRequest('?videoId=iEpJwprxDdk&origin=http://127.0.0.1:46123')); const html = await response.text(); assert.equal(html.includes('origin:"http://127.0.0.1:46123"'), true); }); test('uses dedicated parentOrigin for iframe postMessage target', async () => { const response = await handler(makeRequest('?videoId=iEpJwprxDdk&origin=https://worldmonitor.app&parentOrigin=https://tauri.localhost')); const html = await response.text(); assert.match(html, /playerVars:\{[^}]*origin:"https:\/\/worldmonitor\.app"/); assert.match(html, /parentOrigin="https:\/\/tauri\.localhost"/); assert.match(html, /if\(allowedOrigin!==['"]\*['"]&&e\.origin!==allowedOrigin\)return/); }); test('does not accept wildcard parentOrigin query parameter', async () => { const response = await handler(makeRequest('?videoId=iEpJwprxDdk&origin=https://worldmonitor.app&parentOrigin=*')); const html = await response.text(); assert.equal(html.includes('parentOrigin="*"'), false); assert.match(html, /parentOrigin="https:\/\/worldmonitor\.app"/); }); ================================================ FILE: api/youtube/live.js ================================================ // YouTube Live Stream Detection API // Proxies to Railway relay which uses residential proxy for YouTube scraping import { getCorsHeaders, isDisallowedOrigin } from '../_cors.js'; import { getRelayBaseUrl, getRelayHeaders } from '../_relay.js'; export const config = { runtime: 'edge' }; export default async function handler(request) { const cors = getCorsHeaders(request); if (request.method === 'OPTIONS') return new Response(null, { status: 204, headers: cors }); if (isDisallowedOrigin(request)) { return new Response(JSON.stringify({ error: 'Origin not allowed' }), { status: 403, headers: cors }); } const url = new URL(request.url); const channel = url.searchParams.get('channel'); const videoIdParam = url.searchParams.get('videoId'); const params = new URLSearchParams(); if (channel) params.set('channel', channel); if (videoIdParam) params.set('videoId', videoIdParam); const qs = params.toString(); if (!qs) { return new Response(JSON.stringify({ error: 'Missing channel or videoId parameter' }), { status: 400, headers: { ...cors, 'Content-Type': 'application/json' }, }); } // Proxy to Railway relay const relayBase = getRelayBaseUrl(); if (relayBase) { try { const relayHeaders = getRelayHeaders({ 'User-Agent': 'WorldMonitor-Edge/1.0' }); const relayRes = await fetch(`${relayBase}/youtube-live?${qs}`, { headers: relayHeaders }); if (relayRes.ok) { const data = await relayRes.json(); const cacheTime = videoIdParam ? 3600 : 600; return new Response(JSON.stringify(data), { status: 200, headers: { ...cors, 'Content-Type': 'application/json', 'Cache-Control': `public, max-age=${cacheTime}, s-maxage=${cacheTime}, stale-while-revalidate=60`, }, }); } } catch { /* relay unavailable — fall through to direct fetch */ } } // Fallback: direct fetch (works for oembed, limited for live detection from datacenter IPs) if (videoIdParam && /^[A-Za-z0-9_-]{11}$/.test(videoIdParam)) { try { const oembedRes = await fetch( `https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${videoIdParam}&format=json`, { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } }, ); if (oembedRes.ok) { const data = await oembedRes.json(); return new Response(JSON.stringify({ channelName: data.author_name || null, title: data.title || null, videoId: videoIdParam }), { status: 200, headers: { ...cors, 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=3600, s-maxage=3600' }, }); } } catch { /* oembed failed — return minimal response */ } return new Response(JSON.stringify({ channelName: null, title: null, videoId: videoIdParam }), { status: 200, headers: { ...cors, 'Content-Type': 'application/json' }, }); } if (!channel) { return new Response(JSON.stringify({ error: 'Missing channel parameter' }), { status: 400, headers: { ...cors, 'Content-Type': 'application/json' }, }); } // Fallback: direct scrape (limited from datacenter IPs) try { const channelHandle = channel.startsWith('@') ? channel : `@${channel}`; const response = await fetch(`https://www.youtube.com/${channelHandle}/live`, { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' }, redirect: 'follow', }); if (!response.ok) { return new Response(JSON.stringify({ videoId: null, channelExists: false }), { status: 200, headers: { ...cors, 'Content-Type': 'application/json' }, }); } const html = await response.text(); const channelExists = html.includes('"channelId"') || html.includes('og:url'); let channelName = null; const ownerMatch = html.match(/"ownerChannelName"\s*:\s*"([^"]+)"/); if (ownerMatch) channelName = ownerMatch[1]; else { const am = html.match(/"author"\s*:\s*"([^"]+)"/); if (am) channelName = am[1]; } let videoId = null; const detailsIdx = html.indexOf('"videoDetails"'); if (detailsIdx !== -1) { const block = html.substring(detailsIdx, detailsIdx + 5000); const vidMatch = block.match(/"videoId":"([a-zA-Z0-9_-]{11})"/); const liveMatch = block.match(/"isLive"\s*:\s*true/); if (vidMatch && liveMatch) videoId = vidMatch[1]; } let hlsUrl = null; const hlsMatch = html.match(/"hlsManifestUrl"\s*:\s*"([^"]+)"/); if (hlsMatch && videoId) hlsUrl = hlsMatch[1].replace(/\\u0026/g, '&'); return new Response(JSON.stringify({ videoId, isLive: videoId !== null, channelExists, channelName, hlsUrl }), { status: 200, headers: { ...cors, 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=300, s-maxage=600, stale-while-revalidate=120' }, }); } catch { return new Response(JSON.stringify({ videoId: null, error: 'Failed to fetch channel data' }), { status: 200, headers: { ...cors, 'Content-Type': 'application/json' }, }); } } ================================================ FILE: biome.json ================================================ { "$schema": "https://biomejs.dev/schemas/2.4.7/schema.json", "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true }, "files": { "ignoreUnknown": true }, "formatter": { "enabled": false }, "overrides": [ { "includes": ["src/generated/**"], "linter": { "enabled": false } } ], "linter": { "enabled": true, "rules": { "recommended": true, "correctness": { "noUnusedVariables": "off", "noUnusedImports": "off", "noUndeclaredVariables": "off" }, "suspicious": { "noExplicitAny": "off", "noAssignInExpressions": "off", "noDoubleEquals": "warn", "noConsole": "off", "noFallthroughSwitchClause": "error", "noGlobalAssign": "error", "noRedeclare": "error", "noVar": "error", "noControlCharactersInRegex": "off", "noTemplateCurlyInString": "off", "noEmptyBlock": "off", "noImplicitAnyLet": "off", "useIterableCallbackReturn": "off", "noDocumentCookie": "off", "noDuplicateProperties": "off", "noPrototypeBuiltins": "off", "noConfusingVoidType": "off" }, "style": { "noNonNullAssertion": "off", "useConst": "warn", "useTemplate": "off", "useDefaultParameterLast": "warn", "noParameterAssign": "off", "useNodejsImportProtocol": "off", "noUnusedTemplateLiteral": "off", "useImportType": "off", "useArrayLiterals": "warn", "noDescendingSpecificity": "off" }, "complexity": { "noForEach": "off", "noImportantStyles": "off", "useLiteralKeys": "off", "useFlatMap": "warn", "noUselessSwitchCase": "warn", "noUselessConstructor": "warn", "noStaticOnlyClass": "off", "noArguments": "error", "noBannedTypes": "off", "noExcessiveCognitiveComplexity": { "level": "warn", "options": { "maxAllowedComplexity": 50 } } }, "performance": { "noDelete": "off", "noAccumulatingSpread": "warn" }, "security": { "noDangerouslySetInnerHtml": "off" } } }, "javascript": { "globals": ["globalThis"] }, "assist": { "enabled": false } } ================================================ FILE: blog-site/.gitignore ================================================ # build output dist/ # generated OG images (rebuilt at build time) public/og/ # generated types .astro/ # dependencies node_modules/ # logs npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* # environment variables .env .env.production # macOS-specific files .DS_Store # jetbrains setting folder .idea/ ================================================ FILE: blog-site/.vscode/extensions.json ================================================ { "recommendations": ["astro-build.astro-vscode"], "unwantedRecommendations": [] } ================================================ FILE: blog-site/.vscode/launch.json ================================================ { "version": "0.2.0", "configurations": [ { "command": "./node_modules/.bin/astro dev", "name": "Development server", "request": "launch", "type": "node-terminal" } ] } ================================================ FILE: blog-site/README.md ================================================ # Astro Starter Kit: Minimal ```sh npm create astro@latest -- --template minimal ``` > 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! ## 🚀 Project Structure Inside of your Astro project, you'll see the following folders and files: ```text / ├── public/ ├── src/ │ └── pages/ │ └── index.astro └── package.json ``` Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name. There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components. Any static assets, like images, can be placed in the `public/` directory. ## 🧞 Commands All commands are run from the root of the project, from a terminal: | Command | Action | | :------------------------ | :----------------------------------------------- | | `npm install` | Installs dependencies | | `npm run dev` | Starts local dev server at `localhost:4321` | | `npm run build` | Build your production site to `./dist/` | | `npm run preview` | Preview your build locally, before deploying | | `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | | `npm run astro -- --help` | Get help using the Astro CLI | ## 👀 Want to learn more? Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). ================================================ FILE: blog-site/astro.config.mjs ================================================ // @ts-check import { defineConfig } from 'astro/config'; import sitemap from '@astrojs/sitemap'; const POST_DATES = { 'https://www.worldmonitor.app/blog/posts/ai-powered-intelligence-without-the-cloud/': '2026-03-07', 'https://www.worldmonitor.app/blog/posts/build-on-worldmonitor-developer-api-open-source/': '2026-03-09', 'https://www.worldmonitor.app/blog/posts/command-palette-search-everything-instantly/': '2026-03-06', 'https://www.worldmonitor.app/blog/posts/cyber-threat-intelligence-for-security-teams/': '2026-02-24', 'https://www.worldmonitor.app/blog/posts/five-dashboards-one-platform-worldmonitor-variants/': '2026-02-12', 'https://www.worldmonitor.app/blog/posts/live-webcams-from-geopolitical-hotspots/': '2026-03-01', 'https://www.worldmonitor.app/blog/posts/monitor-global-supply-chains-and-commodity-disruptions/': '2026-02-26', 'https://www.worldmonitor.app/blog/posts/natural-disaster-monitoring-earthquakes-fires-volcanoes/': '2026-02-19', 'https://www.worldmonitor.app/blog/posts/osint-for-everyone-open-source-intelligence-democratized/': '2026-02-17', 'https://www.worldmonitor.app/blog/posts/prediction-markets-ai-forecasting-geopolitics/': '2026-03-03', 'https://www.worldmonitor.app/blog/posts/real-time-market-intelligence-for-traders-and-analysts/': '2026-02-21', 'https://www.worldmonitor.app/blog/posts/satellite-imagery-orbital-surveillance/': '2026-02-28', 'https://www.worldmonitor.app/blog/posts/track-global-conflicts-in-real-time/': '2026-02-14', 'https://www.worldmonitor.app/blog/posts/tracking-global-trade-routes-chokepoints-freight-costs/': '2026-03-15', 'https://www.worldmonitor.app/blog/posts/what-is-worldmonitor-real-time-global-intelligence/': '2026-02-10', 'https://www.worldmonitor.app/blog/posts/worldmonitor-in-21-languages-global-intelligence-for-everyone/': '2026-03-04', 'https://www.worldmonitor.app/blog/posts/worldmonitor-vs-traditional-intelligence-tools/': '2026-03-11', 'https://www.worldmonitor.app/blog/': '2026-03-19', }; export default defineConfig({ site: 'https://www.worldmonitor.app', base: '/blog', output: 'static', integrations: [ sitemap({ serialize(item) { const lastmod = POST_DATES[item.url]; if (lastmod) return { ...item, lastmod }; return item; }, }), ], markdown: { shikiConfig: { theme: 'github-dark', }, }, }); ================================================ FILE: blog-site/package.json ================================================ { "name": "blog-site", "type": "module", "version": "0.0.1", "engines": { "node": ">=22.12.0" }, "scripts": { "dev": "astro dev", "generate:og": "node scripts/generate-og-images.mjs", "build": "npm run generate:og && astro build", "preview": "astro preview", "astro": "astro" }, "dependencies": { "@astrojs/rss": "^4.0.16", "@astrojs/sitemap": "^3.7.1", "astro": "^6.0.0", "fast-xml-builder": "^1.1.0", "gray-matter": "^4.0.3", "satori": "^0.25.0", "sharp": "^0.34.5" } } ================================================ FILE: blog-site/public/robots.txt ================================================ User-agent: * Allow: / Sitemap: https://www.worldmonitor.app/blog/sitemap-index.xml ================================================ FILE: blog-site/scripts/generate-og-images.mjs ================================================ import satori from 'satori'; import sharp from 'sharp'; import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from 'node:fs'; import { join, basename } from 'node:path'; import matter from 'gray-matter'; const BLOG_DIR = join(import.meta.dirname, '..', 'src', 'content', 'blog'); const OUT_DIR = join(import.meta.dirname, '..', 'public', 'og'); const WIDTH = 1200; const HEIGHT = 630; const interRegular = readFileSync(join(import.meta.dirname, 'fonts', 'inter-regular.ttf')); const interBold = readFileSync(join(import.meta.dirname, 'fonts', 'inter-bold.ttf')); mkdirSync(OUT_DIR, { recursive: true }); const files = readdirSync(BLOG_DIR).filter(f => f.endsWith('.md')); let generated = 0; function h(type, style, children) { return { type, props: { style, children } }; } for (const file of files) { const slug = basename(file, '.md'); const outPath = join(OUT_DIR, `${slug}.png`); if (existsSync(outPath)) { console.log(` skip ${slug} (exists)`); continue; } const raw = readFileSync(join(BLOG_DIR, file), 'utf-8'); const { data } = matter(raw); const title = data.title || slug; const audience = data.audience || ''; const titleChildren = []; if (audience) { titleChildren.push( h('div', { fontSize: 14, color: '#4ade80', fontWeight: 600, textTransform: 'uppercase', letterSpacing: 2, }, audience) ); } titleChildren.push( h('div', { fontSize: title.length > 60 ? 36 : 44, fontWeight: 700, lineHeight: 1.2, color: '#ffffff', }, title) ); const element = h('div', { width: '100%', height: '100%', display: 'flex', flexDirection: 'column', justifyContent: 'space-between', padding: '60px 72px', backgroundColor: '#050505', fontFamily: 'Inter', color: '#ffffff', }, [ h('div', { display: 'flex', alignItems: 'center', gap: 16 }, [ h('div', { width: 48, height: 48, borderRadius: 10, backgroundColor: '#4ade80', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 18, fontWeight: 700, color: '#050505', }, 'WM'), h('div', { display: 'flex', flexDirection: 'column' }, [ h('span', { fontSize: 16, fontWeight: 700, letterSpacing: 3, color: '#e5e5e5' }, 'WORLD MONITOR'), h('span', { fontSize: 12, color: '#666666', letterSpacing: 1 }, 'BLOG'), ]), ]), h('div', { display: 'flex', flexDirection: 'column', gap: 16, flex: 1, justifyContent: 'center', }, titleChildren), h('div', { display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderTop: '1px solid #222222', paddingTop: 24, }, [ h('span', { fontSize: 14, color: '#666666' }, 'worldmonitor.app/blog'), h('div', { display: 'flex', alignItems: 'center', gap: 8 }, [ h('div', { width: 8, height: 8, borderRadius: 4, backgroundColor: '#4ade80' }, ''), h('span', { fontSize: 14, color: '#4ade80' }, 'Real-time Global Intelligence'), ]), ]), ]); const svg = await satori(element, { width: WIDTH, height: HEIGHT, fonts: [ { name: 'Inter', data: interRegular, weight: 400, style: 'normal' }, { name: 'Inter', data: interBold, weight: 700, style: 'normal' }, ], }); const png = await sharp(Buffer.from(svg)).png({ quality: 90 }).toBuffer(); writeFileSync(outPath, png); console.log(` gen ${slug}.png`); generated++; } console.log(`\nOG images: ${generated} generated, ${files.length - generated} skipped`); ================================================ FILE: blog-site/src/content/blog/ai-powered-intelligence-without-the-cloud.md ================================================ --- title: "AI-Powered Intelligence Without the Cloud: World Monitor's Privacy-First Approach" description: "Run AI-powered intelligence analysis on your own hardware. World Monitor supports Ollama, LM Studio, and in-browser ML for private geopolitical analysis." metaTitle: "Local AI Intelligence Analysis | World Monitor" keywords: "local LLM intelligence, private AI analysis, offline intelligence tool, Ollama OSINT, privacy-first AI dashboard" audience: "Privacy-conscious analysts, security researchers, government users, enterprise security teams, local LLM enthusiasts" heroImage: "/blog/images/blog/ai-powered-intelligence-without-the-cloud.jpg" pubDate: "2026-03-07" --- Every time you paste a sensitive document into ChatGPT, that data touches someone else's servers. For intelligence analysts, security researchers, and anyone handling sensitive geopolitical information, that's not just inconvenient. It's a risk. World Monitor takes a different approach. Every AI feature in the platform can run entirely on your own hardware, with no data leaving your machine. If you're new to the platform, learn [what World Monitor is and how it works](/blog/posts/what-is-worldmonitor-real-time-global-intelligence/). ## The Problem with Cloud-Based Intelligence Tools Most AI-powered analysis tools follow the same pattern: your data goes up to a cloud API, gets processed, and the result comes back. This works fine for writing emails. It's problematic when you're analyzing: - Military deployment patterns - Classified or sensitive government communications - Corporate intelligence on merger targets - Supply chain vulnerabilities in critical infrastructure - Threat assessments for physical security operations Even with enterprise API agreements, the data transits networks you don't control, gets logged in systems you can't audit, and exists on servers in jurisdictions that may not align with your requirements. ## World Monitor's 4-Tier AI Architecture World Monitor solves this with a **4-tier LLM fallback chain** that starts local and only reaches for the cloud if you explicitly allow it: ### Tier 1: Local LLMs (Ollama / LM Studio) Your first and most private option. Install Ollama or LM Studio on your machine, download a model (Llama 3.1, Mistral, Phi, etc.), and point World Monitor at your local instance. What runs locally: - **World Brief generation:** Daily intelligence summaries synthesized from current headlines - **Country dossier analysis:** AI-written assessments for any country's current situation - **Threat classification:** Categorizing news events by threat type and severity - **AI Deduction:** Interactive geopolitical forecasting grounded in live data The desktop app (Tauri) discovers your local Ollama instance automatically. No configuration needed. Just install Ollama, pull a model, and open World Monitor. ### Tier 2: Groq (Llama 3.1 8B) If you want cloud speed with open-source models, Groq runs Llama 3.1 at extremely fast inference speeds. You need a free Groq API key, which is stored in your OS keychain (macOS Keychain, Windows Credential Manager) via the desktop app. ### Tier 3: OpenRouter A fallback provider that gives you access to multiple models (Claude, GPT-4, Mixtral, etc.) through a single API key. Use this if your preferred model isn't available through Groq. ### Tier 4: Browser-Based T5 (Transformers.js) The ultimate fallback. A T5 model runs entirely in your browser via WebAssembly and Web Workers. No API key, no network request, no server. The model weights are cached locally after first download. This tier is limited (T5 is smaller than Llama 3.1), but it means World Monitor's AI features always work, even without internet access. ## In-Browser Machine Learning Beyond the LLM tiers, World Monitor runs several ML pipelines entirely in your browser: ### Named Entity Recognition (NER) Extracts people, organizations, locations, and dates from news headlines. Runs in a Web Worker using Transformers.js with ONNX models. Never touches a server. ### Sentiment Analysis Classifies headline sentiment to detect shifts in media tone about countries, leaders, or events. This feeds into the information velocity component of the CII (Country Instability Index). ### Semantic Search (RAG) World Monitor's **Headline Memory** feature builds a local semantic index of up to 5,000 headlines using ONNX embeddings stored in IndexedDB. When you ask the AI about a topic, it retrieves relevant headlines from your local index for grounded, cited responses. This is a full Retrieval-Augmented Generation pipeline running in your browser. No vector database subscription. No cloud embedding API. Combined with [prediction markets and AI forecasting](/blog/posts/prediction-markets-ai-forecasting-geopolitics/), this local RAG pipeline enables deeply grounded geopolitical analysis. ### 3-Stage Threat Classification The threat pipeline processes every incoming headline through: 1. **Keyword matcher** (instant, rule-based) 2. **Browser ML classifier** (Transformers.js, runs locally) 3. **LLM classifier** (your chosen tier) The first two stages always run locally. The third stage uses whichever LLM tier you've configured. ## The Desktop App: Full Offline Operation World Monitor's Tauri desktop app (available for macOS, Windows, and Linux) takes privacy further: - **OS Keychain Integration:** API keys are stored in your operating system's secure credential store, not in config files or browser storage - **Local Node.js Sidecar:** A bundled Node.js process handles data fetching and processing locally, including API calls that can't run in a browser (due to CORS or TLS requirements) - **Offline Map Caching:** The Progressive Web App caches up to 500 map tiles for offline viewing - **No Telemetry:** The desktop app sends zero analytics or usage data With Ollama installed alongside the desktop app, you have a fully air-gapped intelligence dashboard. Connect to the internet when you want fresh data, disconnect when you want to analyze in private. ## Practical Setup: From Zero to Private Intelligence ### Step 1: Install Ollama ``` curl -fsSL https://ollama.ai/install.sh | sh ollama pull llama3.1 ``` ### Step 2: Open World Monitor Navigate to worldmonitor.app or install the desktop app from GitHub releases. ### Step 3: Configure AI World Monitor auto-detects your local Ollama instance. Open any country dossier or the World Brief panel and the AI analysis generates locally. ### Step 4: Enable Headline Memory (Optional) Opt in to the RAG feature. World Monitor will build a local vector index of headlines you've seen, giving the AI context for more grounded analysis. Total setup time: under 5 minutes. Total data sent to external servers for AI processing: zero. ## Who Needs Private Intelligence Analysis? **Government Analysts:** Classified environments can't send data to commercial AI APIs. World Monitor with Ollama runs entirely within your network boundary. **Corporate Security Teams:** Analyzing threats to executives, facilities, or supply chains often involves information that shouldn't leave the corporate network. **Journalists in Hostile Environments:** Reporters covering authoritarian regimes need tools that don't create a trail of API calls linking them to specific intelligence queries. **Academic Researchers:** IRB (Institutional Review Board) requirements may prohibit sending research data to third-party AI services. Local processing satisfies these constraints. **Financial Compliance:** Material non-public information (MNPI) requirements mean certain geopolitical analysis can't transit external servers. ## Open Source: Trust Through Transparency You don't have to take our word for the privacy claims. World Monitor is fully open source under AGPL-3.0. Every network call, every data flow, every AI prompt is in the codebase for you to audit. Developers can explore the full [typed API layer and proto-first architecture](/blog/posts/build-on-worldmonitor-developer-api-open-source/) to verify exactly how data flows through the system. The proto-first API architecture (92 proto files, 22 typed services) means even the API contracts are transparent. You can see exactly what data each endpoint expects and returns. ## Frequently Asked Questions **Do I need an internet connection to use World Monitor's AI features?** No. With Ollama or LM Studio installed locally, all AI analysis runs on your hardware. The browser-based T5 fallback also works fully offline after the initial model download. You only need internet to fetch fresh data feeds. **Which local LLM models work best with World Monitor?** Llama 3.1 (8B or 70B) and Mistral offer the best balance of quality and speed for intelligence analysis. Smaller models like Phi work on lower-end hardware but produce less detailed assessments. **Is the local AI analysis as good as cloud-based alternatives?** For most intelligence tasks, local models like Llama 3.1 70B produce comparable results to cloud APIs. The browser-based T5 tier is more limited in capability but ensures AI features always remain available regardless of connectivity. --- **Run intelligence analysis on your own terms at [worldmonitor.app](https://worldmonitor.app). Install Ollama for fully private AI. No login, no tracking, no compromise.** ================================================ FILE: blog-site/src/content/blog/build-on-worldmonitor-developer-api-open-source.md ================================================ --- title: "Build on World Monitor: Open APIs, Proto-First Architecture, and the Developer Platform" description: "Build intelligence apps on World Monitor's typed API: 22 services, 92 proto files, 60+ edge functions, and auto-generated TypeScript clients. AGPL-3.0." metaTitle: "Developer API & Open Source Platform | World Monitor" keywords: "open source intelligence API, OSINT API free, geopolitical data API, intelligence platform developer, proto-first API architecture" audience: "Developers, data engineers, startup builders, academic researchers, open-source contributors" heroImage: "/blog/images/blog/build-on-worldmonitor-developer-api-open-source.jpg" pubDate: "2026-03-09" --- Most intelligence platforms are walled gardens. You pay for access, you use their interface, and if you want to build something custom, you're out of luck. The data is locked behind a UI. World Monitor is designed differently. The entire intelligence platform, every data feed, every scoring algorithm, every aggregation pipeline, is built on a **typed API layer** that developers can use, extend, and build upon. ## Proto-First Architecture World Monitor uses **Protocol Buffers (protobuf)** as the single source of truth for all API contracts. The codebase contains: - **92 proto files** defining every data structure and service - **22 typed service domains** covering all intelligence verticals - **Auto-generated TypeScript** clients for type-safe API consumption - **Auto-generated OpenAPI** documentation for REST compatibility This means every API endpoint has: 1. A proto definition that specifies exact request/response types 2. An auto-generated TypeScript client with full type safety 3. An OpenAPI spec for language-agnostic access 4. Runtime validation that rejects malformed requests ### Why Proto-First Matters Protocol Buffers enforce a contract between client and server that can't drift: - **Type safety:** No more guessing what fields an API returns. The proto file is the contract. - **Versioning:** Proto files support backward-compatible evolution. Add fields without breaking clients. - **Code generation:** TypeScript clients are generated, not handwritten. Zero chance of client/server mismatch. - **Documentation:** The proto file IS the documentation. Field names, types, and comments are the API spec. For developers building on World Monitor, this means you can trust the API contracts completely. If the proto says a field is `int64`, it's `int64`. If it says `repeated string`, it's an array of strings. ## 22 Service Domains World Monitor's API is organized into domain-specific services: | Domain | What It Covers | |--------|---------------| | **Conflict** | ACLED events, UCDP data, hotspot scoring | | **Military** | Bases, ADS-B flights, AIS vessels, USNI reports | | **Market** | Stock quotes, forex, commodities, sector performance | | **Crypto** | BTC signals, stablecoin pegs, ETF flows, Fear & Greed | | **Aviation** | Airport delays, flight tracking, airspace data | | **Maritime** | Vessel positions, port status, dark vessel detection | | **Climate** | Temperature anomalies, precipitation, sea level | | **Imagery** | Satellite data via STAC API | | **News** | Aggregated RSS feeds, trending keywords | | **Intelligence** | CII scores, theater posture, convergence events | | **Infrastructure** | Cables, pipelines, nuclear facilities, datacenters | | **Prediction** | Polymarket data, forecast probabilities | | **Cyber** | Threat feeds, C2 servers, malware URLs | | **Disaster** | Earthquakes, fires, volcanic events | | **Displacement** | UNHCR refugee and IDP data | | **Travel** | Government advisories, risk levels | | **Central Bank** | Policy rates, BIS data, REER | | **Tech** | AI labs, startups, accelerators, tech hubs | | **Commodity** | Mining sites, exchange hubs, energy infrastructure | | **Regulation** | AI policy tracking, regulatory changes | | **Health** | System health, data freshness, circuit breaker status | | **Bootstrap** | Hydration data for initial app load | Each domain has its own edge function, proto definitions, and TypeScript client. ## 60+ Vercel Edge Functions The API layer runs on **Vercel Edge Functions**, providing: - **Global edge deployment:** API responses from the nearest edge node - **~85% cold-start reduction** through per-domain thin entry points - **Circuit breakers** per data source (failing upstream won't take down the API) - **Cache-Control headers** with ETag support for efficient CDN caching - **Rate limiting** with Cloudflare-aware client IP detection API endpoints follow the pattern: ``` api.worldmonitor.app/api/{domain}/v1/{rpc} ``` For example: - `api.worldmonitor.app/api/market/v1/quotes` for stock quotes - `api.worldmonitor.app/api/conflict/v1/events` for conflict data - `api.worldmonitor.app/api/intelligence/v1/cii` for Country Instability Index scores ## Building with World Monitor's API ### Custom Dashboards Build a domain-specific dashboard that pulls exactly the data you need. Use the typed TypeScript clients for a seamless development experience: ```typescript // Auto-generated client with full type safety const cii = await intelligenceClient.getCII({ countries: ['US', 'CN', 'RU'] }); // cii.scores is typed as CIIScore[] with all fields known at compile time ``` ### Data Pipelines Feed World Monitor data into your own analytics: - Pull conflict events into a data warehouse for historical analysis - Stream market data alongside geopolitical scores for correlation studies - Build custom alerting on CII threshold changes ### Research Applications Academic researchers can use the API programmatically: - Study the relationship between news velocity and conflict escalation - Analyze prediction market accuracy against actual outcomes (see [prediction markets and AI forecasting](/blog/posts/prediction-markets-ai-forecasting-geopolitics/)) - Build custom scoring models using World Monitor's raw data feeds ### Mobile Apps Build a mobile app that consumes World Monitor's API for a custom mobile intelligence experience. The OpenAPI spec makes it accessible from any language (Swift, Kotlin, Python, Go). ### Slack/Teams Bots Build alerting bots that post to your team channel when: - A country's CII crosses a threshold - A strategic theater posture changes - A prediction market probability shifts significantly - A cyber threat spike is detected in your region of interest ## Self-Hosting World Monitor is AGPL-3.0. You can self-host the entire platform, including [local AI capabilities that run without cloud dependencies](/blog/posts/ai-powered-intelligence-without-the-cloud/): **Frontend:** React + TypeScript + Vite. Standard `npm install && npm run build`. **API:** Vercel Edge Functions. Deploy to Vercel with `vercel deploy`, or adapt for Cloudflare Workers, Deno Deploy, or any edge runtime. **Desktop App:** Tauri. Build with `cargo tauri build` for macOS, Windows, or Linux. **Data Layer:** Redis for caching, with seed scripts that populate data from public sources. Self-hosting gives you: - Complete control over data flows - Custom domain deployment - Network isolation for sensitive environments - Ability to add proprietary data sources ## Contributing The open-source codebase welcomes contributions: - **New data sources:** Add proto definitions, implement handlers, wire into the seed pipeline - **New languages:** Add translation JSON files for additional locale support - **Bug fixes:** Standard GitHub PR workflow - **New panels:** Build new visualization panels using the typed data layer - **Performance:** Edge function optimization, caching improvements, bundle size reduction The proto-first architecture makes contributing safe: the type system catches contract violations at compile time, and auto-generated clients ensure frontend/backend consistency. ## The Developer Stack For reference, World Monitor is built with: | Layer | Technology | |-------|-----------| | Frontend | React, TypeScript, Vite | | 3D Globe | globe.gl, Three.js | | Flat Map | deck.gl, MapLibre | | API | Vercel Edge Functions | | Contracts | Protocol Buffers (92 files) | | Desktop | Tauri (Rust) | | Sidecar | Node.js | | Caching | Redis | | Browser ML | Transformers.js, ONNX | | Styling | CSS Custom Properties | | i18n | i18next (21 locales) | | Testing | Vitest, Playwright | ## Why Build on World Monitor? The intelligence industry has a consolidation problem. A handful of vendors control the data, the algorithms, and the interfaces. Analysts are locked into ecosystems they can't customize, audit, or extend. See how World Monitor [compares to traditional intelligence tools](/blog/posts/worldmonitor-vs-traditional-intelligence-tools/) in practice. World Monitor's open, typed, proto-first architecture is the alternative: - **Audit everything:** Every scoring algorithm, every data pipeline, every API contract is in the codebase - **Extend anything:** Add data sources, build custom panels, create new service domains - **Trust the types:** Proto-generated clients mean no runtime surprises - **Deploy anywhere:** Edge functions, self-hosted, or desktop - **Own your intelligence:** No vendor lock-in, no API key revocation, no price hikes The intelligence platform of the future isn't a product. It's an ecosystem. World Monitor is building the foundation. ## Frequently Asked Questions **Is the World Monitor API free to use?** Yes. World Monitor is AGPL-3.0 open source. You can use the public API at api.worldmonitor.app or self-host the entire stack. There are no API keys required for public endpoints and no usage fees. **What languages can I use to consume the API?** Any language that supports HTTP. The auto-generated OpenAPI spec provides compatibility with Swift, Kotlin, Python, Go, Java, and more. TypeScript clients are generated directly from the proto files for first-class type safety. **How do I add a custom data source to my self-hosted instance?** Define your data structures in a proto file, implement a handler function, wire it into the service registry, and add a seed script to populate Redis. The proto-first architecture ensures type safety across the full stack automatically. --- **Start building at [github.com/koala73/worldmonitor](https://github.com/koala73/worldmonitor). 22 services, 92 proto files, and a global intelligence dataset waiting for your application.** ================================================ FILE: blog-site/src/content/blog/command-palette-search-everything-instantly.md ================================================ --- title: "Cmd+K: Search Everything on the Planet in Under a Second" description: "Fuzzy-search 195 countries, 25+ data layers, and 150+ commands with World Monitor's Cmd+K palette. Multilingual, keyboard-driven intelligence access." metaTitle: "Cmd+K Command Palette Search | World Monitor" keywords: "intelligence dashboard search, command palette dashboard, OSINT search tool, fast country intelligence lookup, keyboard-driven intelligence" audience: "Power users, analysts, developers, keyboard-first professionals" heroImage: "/blog/images/blog/command-palette-search-everything-instantly.jpg" pubDate: "2026-03-06" --- You're monitoring a developing situation. News breaks about a military incident in the South China Sea. You need Taiwan's intelligence dossier, the military bases layer, the AIS maritime panel, and the strategic theater posture, right now. In most dashboards, that's four separate navigation actions. In World Monitor, it's one: **Cmd+K** (or Ctrl+K on Windows/Linux), type what you need, hit Enter. This is one of the key advantages that sets World Monitor apart from [traditional intelligence tools](/blog/posts/worldmonitor-vs-traditional-intelligence-tools/). ## The 150+ Command Universe World Monitor's command palette is a fuzzy-search interface that spans the entire platform. Hit Cmd+K and you can access: ### Countries (195) Type any country name and instantly pull up its full intelligence dossier: CII score, active signals, AI analysis, infrastructure exposure, and 7-day timeline. Country names are searchable in all 21 supported languages, so typing "Allemagne" finds Germany, "Japón" finds Japan. ### Navigation (8 regional presets) Jump to any region: Global, Americas, Europe, MENA, Asia-Pacific, Africa, Oceania, Latin America. The map pans, zooms, and adjusts layer visibility in one action. ### Layer Toggles (25+) Toggle any data layer by name: conflicts, military bases, AIS vessels, flights, undersea cables, pipelines, nuclear facilities, earthquakes, fires, cyber threats, GPS jamming, protests, displacement, datacenters, and more. ### Layer Presets Activate curated layer combinations with a single command: - **Military**: Bases, flights, vessels, GPS jamming - **Finance**: Exchanges, financial centers, commodity hubs - **Infrastructure**: Cables, pipelines, datacenters, ports, nuclear - **Intelligence**: Conflicts, hotspots, protests, OSINT - **Minimal**: Clean map with no overlays - **All / None**: Everything on or everything off ### Panel Shortcuts (50+) Open any panel: news feed, intelligence brief, CII rankings, markets, commodities, crypto, predictions, webcams, world brief, strategic posture, and dozens more. ### View Controls - Dark/light mode toggle - Fullscreen toggle - Data refresh - Time range selection (1h, 6h, 24h, 48h, 7 days) ## Fuzzy Search That Actually Works The command palette uses **case-insensitive fuzzy matching** with intelligent ranking. You don't need exact names: - Type "taiwan" → Shows Taiwan country brief, Taiwan Strait theater, nearby bases - Type "crypto" → Shows crypto panel, stablecoin monitor, BTC signals - Type "fire" → Shows NASA FIRMS layer, fire-related news - Type "base" → Shows military bases layer - Type "iran" → Shows Iran country brief, Iran theater, Iran-related panels Results are grouped by category (Navigate, Layers, Panels, View, Actions, Country) so you can scan quickly even when multiple results match. ## Multilingual Search With [21 languages supported](/blog/posts/worldmonitor-in-21-languages-global-intelligence-for-everyone/), the command palette adapts to your locale. Country names and common commands are searchable in: English, French, German, Spanish, Italian, Portuguese, Dutch, Swedish, Polish, Czech, Romanian, Bulgarian, Greek, Russian, Turkish, Arabic, Chinese (Simplified), Japanese, Korean, Thai, Vietnamese An Arabic-speaking analyst can type country names in Arabic and get the same results. A Japanese user can search in kanji. The search indexes include localized keywords for all 195 countries in every supported language. ## Recent Searches The command palette remembers your **last 8 searches**, displayed at the top when you open it. During fast-moving situations, this means you can rapidly cycle between the same few views without retyping. Monitoring a crisis across three countries? Your recent searches keep those three country briefs one keypress away. ## Keyboard Navigation The entire palette is keyboard-driven: - **Arrow Up/Down**: Navigate results - **Enter**: Execute selected command - **Escape**: Close palette - **Type**: Refine search in real time No mouse needed. For analysts who live in the keyboard, this means World Monitor's entire intelligence platform is accessible without touching a pointing device. ## Mobile Search Experience On mobile, the command palette transforms into a touch-optimized search sheet: - **Category chips** at the top for quick filtering (Countries, Layers, Panels) - **One-handed keyboard layout** optimized for phone use - **Swipe to dismiss** - **Large touch targets** for result selection The same 150+ commands and 195 countries are available on mobile, just with a touch-first interface. ## Context-Aware Suggestions The command palette is panel-aware. When you have specific panels open, related commands surface higher in results. If you're viewing the finance panels, market-related commands rank higher. If you're in the military view, defense-related layers and theaters appear first. ## Power User Workflows ### Morning Intelligence Sweep (60 seconds) 1. Cmd+K → "world brief" → Enter (AI summary) 2. Cmd+K → "cii" → Enter (instability rankings) 3. Cmd+K → "hotspot" → Enter (escalation scores) 4. Cmd+K → "military" preset → Enter (all military layers) 5. Cmd+K → country of interest → Enter (deep dive) ### Breaking Event Response (30 seconds) 1. Cmd+K → country name → Enter (dossier) 2. Cmd+K → "intelligence" preset → Enter (all OSINT layers) 3. Cmd+K → "webcam" → Enter (live video) 4. Cmd+K → "telegram" → Enter (OSINT channels) ### Market Open Preparation (45 seconds) 1. Cmd+K → "finance" preset → Enter 2. Cmd+K → "macro" → Enter (7-signal radar) 3. Cmd+K → "prediction" → Enter (Polymarket) 4. Cmd+K → "commodity" → Enter (price panel) The command palette turns World Monitor from a visual dashboard into a queryable intelligence system. Ask it anything, get there instantly. Explore the [five dashboard variants](/blog/posts/five-dashboards-one-platform-worldmonitor-variants/) to see how the palette adapts to different operational contexts. ## Why It Matters In intelligence analysis, **time to insight** is the critical metric. Every second spent navigating menus, scrolling sidebars, or clicking through panels is a second you're not analyzing. World Monitor's Cmd+K reduces the path from question to answer to a single search query. Type what you need, press Enter, and you're looking at it. For professionals who make time-sensitive decisions based on global intelligence, that speed compounds into a significant advantage. ## Frequently Asked Questions **Does the command palette work on mobile devices?** Yes. On mobile, Cmd+K transforms into a touch-optimized search sheet with category chips, large touch targets, and swipe-to-dismiss. All 150+ commands and 195 countries remain accessible through a touch-first interface. **Can I search in languages other than English?** Absolutely. The command palette indexes country names and keywords in all 21 supported languages. You can type in Arabic, Japanese, Russian, or any other supported language and get accurate results. **How do I customize which commands appear first?** The palette is context-aware: it ranks results based on your currently active panels and layers. Your last 8 searches also appear at the top for quick access during fast-moving situations. --- **Try it now: open [worldmonitor.app](https://worldmonitor.app) and press Cmd+K. Your intelligence is one search away.** ================================================ FILE: blog-site/src/content/blog/cyber-threat-intelligence-for-security-teams.md ================================================ --- title: "Cyber Threat Intelligence Meets Geopolitics: World Monitor for Security Teams" description: "Track botnets, malware URLs, and internet outages with geopolitical context. Integrates Feodo Tracker, URLhaus, and AlienVault OTX on one map." metaTitle: "Cyber Threat Intelligence Dashboard | World Monitor" keywords: "cyber threat intelligence dashboard free, botnet tracking tool, malware monitoring dashboard, internet outage map, threat intelligence OSINT" audience: "SOC analysts, cybersecurity professionals, CISO teams, threat researchers, IT security managers" heroImage: "/blog/images/blog/cyber-threat-intelligence-for-security-teams.jpg" pubDate: "2026-02-24" --- Most cyber threat intelligence platforms show you indicators of compromise in isolation: IP addresses, file hashes, domain names. They tell you what's attacking, but not why. When a wave of phishing campaigns targets European energy companies, is it financially motivated or state-sponsored? When a country's internet goes dark, is it an infrastructure failure or a government-ordered shutdown? When botnet command-and-control servers cluster in a specific region, does it correlate with the geopolitical situation there? World Monitor answers these questions by putting cyber threat data on the same map as [military movements and conflict tracking](/blog/posts/track-global-conflicts-in-real-time/), political instability scores, and infrastructure networks. ## Integrated Threat Feeds ### Feodo Tracker (abuse.ch) The Feodo Tracker identifies active **botnet command-and-control (C2) servers** used by major banking trojans and malware families including Emotet, Dridex, TrickBot, and QakBot. World Monitor maps these C2 servers geographically, showing: - Active C2 server locations - Malware family association - Server hosting details - First seen and last seen timestamps When C2 servers cluster in a country whose CII (Country Instability Index) is rising, it may indicate state tolerance or state sponsorship of cybercrime during periods of geopolitical tension. ### URLhaus (abuse.ch) URLhaus tracks **URLs distributing malware**. World Monitor integrates this feed to show: - Active malware distribution URLs by geography - Payload types being distributed - Hosting infrastructure patterns - Takedown status and timeline ### AlienVault OTX (Open Threat Exchange) The **Open Threat Exchange** is a community-driven threat intelligence platform. World Monitor pulls curated "pulses" (collections of indicators) to show: - Emerging attack campaigns - Geographic targeting patterns - Associated threat actor profiles - Related indicators of compromise ### AbuseIPDB IP reputation data showing addresses associated with brute force attacks, spam, and other malicious activity. ### C2IntelFeeds Additional command-and-control intelligence feeds providing broader coverage of active C2 infrastructure across malware families. ## Internet Outage Detection (Cloudflare Radar) World Monitor integrates **Cloudflare Radar** data to detect and map internet outages globally. This reveals: - **Government-ordered shutdowns** during protests or elections - **Infrastructure failures** from natural disasters or attacks - **Submarine cable cuts** affecting regional connectivity (see [global supply chain and infrastructure monitoring](/blog/posts/monitor-global-supply-chains-and-commodity-disruptions/)) - **BGP hijacking** incidents redirecting traffic through unauthorized networks Mapping outages alongside conflict and protest data creates a powerful correlation: when a country's internet goes dark the same day CII spikes and Telegram OSINT reports protests, the pattern is clear. ## The Cyber Threat Map Layer Toggle the cyber threat layer on World Monitor's globe and you see a geospatial view of active threats: - Red markers for C2 servers - Orange markers for malware distribution URLs - Yellow markers for threat intelligence pulses - Gray overlays for internet outage zones Zoom into a region and the density of threats becomes visible. Pan out and you see global attack patterns. Overlay the military bases layer and you might notice C2 infrastructure clustering near military installations. Overlay the undersea cable layer and see how outages align with physical infrastructure routes. ## Geopolitical Context for Cyber Events This is World Monitor's unique contribution to threat intelligence. Here's what the geopolitical layers add: ### Attribution Context When a new attack campaign targets NATO-aligned countries, World Monitor shows: - Which strategic theaters are currently elevated - Whether the targeted countries have rising CII scores - Active military exercises or deployments in the region - Recent diplomatic events that may have triggered the campaign This doesn't prove attribution, but it provides the context that threat analysts need for informed assessment. ### Infrastructure Risk Assessment World Monitor maps the critical infrastructure that cyber attacks target: - **Undersea cables** carrying 95% of intercontinental internet traffic - **Pipelines** with SCADA systems vulnerable to cyber-physical attacks - **Nuclear facilities** with safety-critical control systems - **Financial centers** processing trillions in daily transactions - **AI datacenters** hosting critical AI infrastructure When you overlay cyber threat data on infrastructure, you see the attack surface visually. A cluster of C2 servers in a country adjacent to undersea cable landing stations raises different concerns than the same cluster in an isolated interior region. ### Predictive Indicators Historically, cyber operations precede kinetic military action. The 2022 Ukraine conflict was preceded by months of cyber attacks against government and infrastructure targets. World Monitor's combined view lets you watch for: - Cyber threat spikes in countries with rising CII scores - New C2 infrastructure deployment near strategic theaters - Internet outage patterns that suggest preparation for information control - Malware campaigns targeting specific national infrastructure ## Practical Workflows for SOC Teams ### Daily Threat Briefing 1. Open World Monitor and check the cyber threat layer 2. Review new C2 servers and malware URLs from the past 24 hours 3. Cross-reference geographic distribution with the CII heatmap 4. Check internet outage overlay for any new blackouts 5. Read the AI-generated World Brief for geopolitical context 6. Set keyword monitors for specific threat actor names or malware families ### Incident Contextualization When responding to an attack: 1. Map the attack infrastructure on World Monitor 2. Check if the source country's CII has been rising 3. Review if the target aligns with active strategic theaters 4. Check Telegram OSINT for any related chatter 5. Assess if physical infrastructure near the attack is at risk 6. Generate an AI brief combining cyber and geopolitical indicators ### Threat Hunting 1. Filter cyber threat layer by specific malware family 2. Identify geographic patterns in C2 infrastructure 3. Correlate with news panel for recent geopolitical events in those regions 4. Check prediction markets for escalation probabilities 5. Monitor for infrastructure cascade effects if attacks succeed ## Why Geopolitical Context Matters for Cybersecurity The cybersecurity industry has spent two decades building tools that analyze threats in isolation. IP addresses, file hashes, and YARA rules are essential, but they exist in a vacuum without geopolitical context. Consider two scenarios: **Scenario A:** A new botnet C2 server appears in Country X. Your threat intel platform flags it. You block the IP. Move on. **Scenario B:** A new botnet C2 server appears in Country X. World Monitor shows that Country X's CII has spiked 15 points in a week. The strategic theater assessment shows elevated posture. ADS-B tracking shows unusual military flights. News velocity for the region has tripled. Telegram OSINT reports government mobilization. Same C2 server. Dramatically different risk assessment. In Scenario B, that server might be part of a state-sponsored operation preceding military action. Your response should be proportionally different. World Monitor doesn't replace your SIEM, your EDR, or your threat intelligence platform. It adds the context layer that tells you why threats are happening and what might come next. For a broader look at how open-source intelligence supports this analysis, see [OSINT for everyone](/blog/posts/osint-for-everyone-open-source-intelligence-democratized/). ## Frequently Asked Questions **How often is the cyber threat data updated?** Threat feeds from Feodo Tracker, URLhaus, and AlienVault OTX are refreshed regularly through automated seed pipelines. Cloudflare Radar outage data updates in near real-time. The freshness of each data source is visible in the platform's health dashboard. **Can I integrate World Monitor's cyber threat data into my existing SIEM?** Yes. World Monitor's API provides typed endpoints for all cyber threat data. You can pull C2 server locations, malware URLs, and threat intelligence pulses programmatically and feed them into Splunk, Elastic, or any SIEM that accepts JSON data. **Does World Monitor detect threats targeting my specific organization?** World Monitor provides geographic and geopolitical threat context rather than organization-specific detection. It complements your EDR and SIEM by showing whether cyber activity in your region correlates with broader geopolitical tensions, helping you prioritize and contextualize alerts. --- **Add geopolitical context to your threat intelligence at [worldmonitor.app](https://worldmonitor.app). Free, open source, and integrated with the intelligence data that matters.** ================================================ FILE: blog-site/src/content/blog/five-dashboards-one-platform-worldmonitor-variants.md ================================================ --- title: "Five Dashboards, One Platform: How World Monitor Serves Every Intelligence Need" description: "World Monitor offers 5 free intelligence dashboards: geopolitical, tech, finance, commodity, and positive news. Switch between them instantly from one platform." metaTitle: "5 Intelligence Dashboards, One Platform | World Monitor" keywords: "intelligence dashboard variants, tech monitoring dashboard, positive news dashboard, multi-purpose intelligence platform, specialized monitoring tools" audience: "General tech audience, product managers, developers, knowledge workers, content creators" heroImage: "/blog/images/blog/five-dashboards-one-platform-worldmonitor-variants.jpg" pubDate: "2026-02-12" --- Most intelligence platforms force you into a single vertical. A financial terminal. A cybersecurity feed. A conflict tracker. If your work spans multiple domains, you're left juggling subscriptions. World Monitor runs **five specialized dashboards** from a single codebase. Switch between them with one click. Each variant curates panels, layers, and data feeds for its specific audience while sharing the same underlying intelligence engine, map infrastructure, and AI capabilities. ## 1. World Monitor: The Geopolitical Command Center **URL:** worldmonitor.app **Panels:** 45 **Focus:** Conflicts, military, infrastructure, geopolitical risk This is the flagship. World Monitor is built for OSINT analysts, defense researchers, journalists, and anyone who needs to [understand global security dynamics](/blog/posts/track-global-conflicts-in-real-time/). **Key features:** - Country Instability Index (CII) for real-time risk scoring across 23+ nations - Strategic Theater Posture for 9 operational theaters (Taiwan Strait, Persian Gulf, Baltic, Korean Peninsula, and more) - 210+ military bases from 9 operators mapped globally - Live ADS-B aircraft tracking with military enrichment - AIS maritime monitoring merged with USNI fleet reports - 26 Telegram OSINT channels via MTProto - OREF rocket alert integration with Hebrew-to-English translation - GPS/GNSS jamming zone detection - Hotspot escalation scoring with geographic convergence detection - AI Deduction panel for geopolitical forecasting **Who it's for:** OSINT researchers, geopolitical analysts, defense academics, journalists covering conflict, humanitarian organizations monitoring field conditions. ## 2. Tech Monitor: The Silicon Valley Radar **URL:** tech.worldmonitor.app **Panels:** 28 **Focus:** AI/ML, startups, cybersecurity, cloud infrastructure Tech Monitor maps the global technology landscape: where AI is being built, where startups are funded, where data centers are concentrated, and where the next unicorn might emerge. **Key features:** - 111 AI datacenters mapped globally with operator details - Startup hub and accelerator locations - AI lab and research center tracking - GitHub Trending integration - Tech Readiness Index by country - Unicorn and late-stage startup tracking - Cloud region mapping (AWS, Azure, GCP) - Cybersecurity threat feeds (abuse.ch, AlienVault OTX) - Service outage monitoring via Cloudflare Radar - Tech-focused news from 100+ specialized RSS feeds **Who it's for:** VC investors evaluating markets, tech executives tracking competitors, developers following industry trends, cybersecurity professionals monitoring threats. ## 3. Finance Monitor: Markets with Context **URL:** finance.worldmonitor.app **Panels:** 27 **Focus:** Markets, central banks, forex, Gulf FDI, macro signals Finance Monitor is for [traders and analysts](/blog/posts/real-time-market-intelligence-for-traders-and-analysts/) who know that markets move on geopolitics. It combines traditional financial data with the intelligence layers that drive price action. **Key features:** - 92 global stock exchanges with trading hours and market caps - 7-signal macro radar with composite BUY/CASH verdict - 13 central bank policy trackers with BIS data - Stablecoin peg monitoring (USDT, USDC, DAI, FDUSD, USDe) - BTC spot ETF flow tracker (IBIT, FBTC, GBTC, and 7 more) - Fear & Greed Index with 30-day history - Bitcoin technical signals (SMA50, SMA200, VWAP, Mayer Multiple) - 64 Gulf FDI investments (Saudi/UAE Vision 2030) - 19 financial centers ranked by GFCI - Polymarket prediction market integration - Forex, bonds, and derivatives panels **Who it's for:** Retail and institutional traders, macro investors, financial analysts, emerging market researchers, fintech builders. ## 4. Commodity Monitor: Raw Materials Intelligence **URL:** commodity.worldmonitor.app **Panels:** 16 **Focus:** Mining, metals, energy, supply chain disruption Commodity Monitor tracks the physical resources that power the global economy: where they're extracted, how they're priced, and [what threatens their supply](/blog/posts/monitor-global-supply-chains-and-commodity-disruptions/). **Key features:** - Live commodity prices (energy, precious metals, critical minerals, agriculture) - 10 major commodity exchange hubs mapped - Mining company and extraction site locations - Critical minerals tracking (lithium, cobalt, rare earths) - Pipeline infrastructure mapping - Energy production and refinery locations - Commodity-focused RSS feeds from specialist sources - Integration with World Monitor's conflict and disaster layers **Who it's for:** Commodity traders, supply chain managers, mining analysts, energy sector professionals, procurement teams, logistics planners. ## 5. Happy Monitor: The Antidote to Doom Scrolling **URL:** happy.worldmonitor.app **Panels:** 10 **Focus:** Good news, human progress, conservation, renewable energy In a world of conflict feeds and crisis dashboards, Happy Monitor exists to track what's going right. It curates positive developments: scientific breakthroughs, conservation wins, renewable energy milestones, and human progress stories. **Key features:** - Good News Feed curated from verified positive news sources - Scientific breakthrough tracking - Conservation and wildlife wins - Renewable energy deployment milestones - Human development progress indicators - Community and social impact stories - Health and medicine advances - Education and literacy progress **Who it's for:** Educators, content creators, mental health-conscious users, impact investors, anyone who wants evidence that progress is real. ## Shared Capabilities Across All Variants Regardless of which variant you use, you get the full platform engine: ### Interactive 3D Globe + Flat Map Dual map engines (globe.gl/Three.js for 3D, deck.gl for flat WebGL) that switch at runtime. Both support all 45 data layers. ### AI Analysis The 4-tier LLM fallback chain (Ollama, Groq, OpenRouter, browser T5) works across all variants. Generate briefs, classify threats, and run analysis privately. ### 21 Languages Full internationalization with lazy-loaded language packs, locale-specific RSS feeds, and RTL support for Arabic. ### Command Palette (Cmd+K) Fuzzy search across 24 result types and 250+ country commands. Find anything instantly. ### 8 Regional Presets Jump between Global, Americas, Europe, MENA, Asia, Africa, Oceania, and Latin America views. ### URL State Sharing Every view state (map position, active layers, selected panels, time range) is encoded in a shareable URL. ### Story Sharing Export intelligence briefs to Twitter/X, LinkedIn, WhatsApp, Telegram, and Reddit with auto-generated Open Graph preview images. ### Desktop App The Tauri app for macOS, Windows, and Linux works with all variants, with OS keychain storage and offline capabilities. ### Progressive Web App Install on any device from the browser. Includes offline map caching (500 tiles). ## Switching Between Variants In the web app, switch variants via the header navigation. Your preferences, language settings, and AI configuration carry across variants. The variants share a single codebase. Every improvement to the core engine benefits all five dashboards simultaneously. A map performance optimization for World Monitor automatically makes Commodity Monitor faster too. ## Why Five Variants Instead of One? **Signal-to-noise ratio.** An OSINT analyst tracking the Taiwan Strait doesn't need stablecoin peg data cluttering their sidebar. A commodity trader monitoring copper prices doesn't need Telegram OSINT channels distracting their view. Each variant curates the information that matters for its audience. The panels are pre-selected. The layers are prioritized. The news feeds are filtered. You get a dashboard that feels purpose-built for your work, without the cognitive load of configuring a general-purpose tool. But when you need to cross domains (the commodity trader wants to check if a conflict is affecting mining operations), switching to World Monitor is one click away. ## One Platform, Zero Cost All five variants are completely free. No freemium gates. No "contact sales" buttons. No feature tiers. The same platform, the same data, the same AI. Available to a solo researcher in Nairobi and a hedge fund analyst in New York. Open source under AGPL-3.0. Deploy it yourself, contribute to it, or just use it. ## Frequently Asked Questions **Can I use multiple dashboard variants at the same time?** Yes. Each variant runs at its own URL, so you can open several in separate browser tabs. Your preferences and language settings carry across all of them. **Do the variants share the same data, or are they separate platforms?** All five variants share a single codebase and the same underlying data engine. The difference is which panels, layers, and feeds are pre-selected for each audience. **Is there a limit on how long I can use the dashboards for free?** No. All five variants are completely free with no time limits, feature gates, or usage caps. --- **Pick your variant and start exploring:** - [worldmonitor.app](https://worldmonitor.app) for geopolitics - [tech.worldmonitor.app](https://tech.worldmonitor.app) for technology - [finance.worldmonitor.app](https://finance.worldmonitor.app) for markets - [commodity.worldmonitor.app](https://commodity.worldmonitor.app) for commodities - [happy.worldmonitor.app](https://happy.worldmonitor.app) for good news ================================================ FILE: blog-site/src/content/blog/live-webcams-from-geopolitical-hotspots.md ================================================ --- title: "Watch the World Live: 31 Webcam Streams from Geopolitical Hotspots" description: "Stream 31 live webcams from Tehran, Kyiv, Jerusalem, Taipei, and beyond. Get real-time situational awareness from 6 global regions on World Monitor, free." metaTitle: "31 Live Webcams from Geopolitical Hotspots | World Monitor" keywords: "live webcams geopolitical hotspots, real-time city cameras, live stream world capitals, OSINT live video, global situation awareness webcams" audience: "OSINT analysts, journalists, security professionals, curious global citizens" heroImage: "/blog/images/blog/live-webcams-from-geopolitical-hotspots.jpg" pubDate: "2026-03-01" --- When news breaks in a foreign capital, your first instinct is to look. Not at a headline. Not at a map. You want to see what's happening on the ground, right now. World Monitor streams **31 live webcams** from geopolitical hotspots across 6 regions, directly inside the intelligence dashboard. No tab switching. No searching for reliable streams. Just click and watch. ## Why Live Video Changes Intelligence Analysis Text reports tell you what someone decided to write. Satellite images tell you what happened hours ago. But a live webcam from a city square shows you what's happening right now: troop movements, protest crowds, normal daily life, or an eerie emptiness that signals something the reports haven't caught yet. During the early hours of major events, live webcams have consistently provided situational awareness before official channels. Analysts watching Kyiv webcams in February 2022 saw military vehicles before wire services confirmed movements. Beirut port cameras captured the 2020 explosion from multiple angles before any reporter could file. World Monitor puts these feeds alongside your intelligence data so you can cross-reference what you're reading with what you're seeing. Learn more about how the platform brings together [real-time conflict tracking](/blog/posts/track-global-conflicts-in-real-time/) with live video. ## 6 Regions, 31 Streams ### Iran & Conflict Zone - **Tehran** city views for monitoring civil activity and normalcy indicators - **Tel Aviv** and **Jerusalem** skylines integrated with OREF siren alerts - **Mecca** for pilgrimage and regional event monitoring - **Beirut** for Lebanon situation awareness ### Eastern Europe - **Kyiv** and **Odessa** for Ukraine conflict monitoring - **St. Petersburg** for Russian domestic activity indicators - **Paris** and **London** for Western European pulse ### Americas - **Washington DC** for government district activity - **New York** for financial district and UN area monitoring - **Los Angeles** and **Miami** for domestic situational awareness ### Asia-Pacific - **Taipei** for Taiwan Strait tension monitoring - **Shanghai** for Chinese economic activity indicators - **Tokyo**, **Seoul**, and **Sydney** for regional coverage ### Space - **ISS Earth View** for orbital perspective - **NASA TV** for space event coverage - **SpaceX** launch feeds ## Smart Streaming Features World Monitor doesn't just embed video. The webcam panel includes intelligence-oriented features: **Region Filtering:** Jump to the region that matters. Monitoring the Middle East? Filter to see only MENA cameras. Tracking the Ukraine conflict? Switch to Eastern Europe. **Grid View vs. Single View:** Toggle between a surveillance-style grid showing multiple feeds simultaneously and a single expanded view for detailed observation. On mobile, single view is forced for performance. **Eco-Idle Pause:** When you switch to another panel or minimize the browser, streams automatically pause to save bandwidth and CPU. They resume when you return. This matters when you're running 31 video feeds alongside a 3D globe with 45 data layers. **Fallback Retry Logic:** Streams go down. Governments block them. CDNs throttle them. World Monitor's player automatically retries failed streams with backoff, and the desktop app routes YouTube embeds through a custom relay to bypass origin restrictions. ## Cross-Reference Video with Intelligence Layers The real power isn't the webcams alone. It's combining them with World Monitor's other data: **Scenario: Unrest in Tehran** 1. CII (Country Instability Index) for Iran spikes 2. Telegram OSINT channels report protests 3. Switch to webcam panel, filter to Iran region 4. Tehran camera shows unusual crowd activity 5. GPS jamming layer shows interference near government buildings 6. News panel confirms government internet throttling via Cloudflare Radar Each data source validates the others. A spike in the CII without visible activity on the webcam might be a false alarm. Unusual webcam activity with no news coverage might be early-stage. When all signals align, you have high-confidence intelligence. This multi-source approach is central to [OSINT for everyone](/blog/posts/osint-for-everyone-open-source-intelligence-democratized/). **Scenario: Taiwan Strait Escalation** 1. Strategic Theater Posture for Taiwan Strait elevates 2. ADS-B shows increased military flight activity 3. AIS shows PLA Navy vessel movements 4. Taipei webcam shows normal city activity (or doesn't) 5. Prediction market odds for Taiwan conflict shift The webcam becomes a ground-truth check against the signals. ## Live Video Streams Beyond Webcams World Monitor also integrates **30+ live news video streams** from major broadcasters: - **Bloomberg TV** for real-time financial coverage - **Sky News** for UK/international breaking news - **Al Jazeera** for Middle East and global south perspective - **Reuters** and **CNN** for general breaking coverage - **Regional broadcasters** for local context These streams use HLS (HTTP Live Streaming) and YouTube Live, with automatic quality adaptation for your connection speed. ## The Desktop App Advantage The Tauri desktop app handles video differently than the browser: - **Staggered iframe loading** prevents the WKWebView engine from throttling when loading multiple video embeds simultaneously - **Custom sidecar relay** for YouTube streams bypasses origin restrictions that block Tauri's local scheme - **OS-level performance optimization** keeps video smooth alongside the 3D globe renderer For analysts who keep World Monitor running as a persistent monitoring station, the desktop app provides the most stable multi-stream experience. The app also supports [satellite imagery and orbital surveillance](/blog/posts/satellite-imagery-orbital-surveillance/) alongside live video feeds. ## Practical Use Cases **Newsroom Monitoring Wall:** Set up World Monitor on a large display in grid view. Six to nine webcam feeds provide a "control room" view alongside the live news feed and conflict map. When something happens, you're already watching. **Executive Protection:** Security teams monitoring principal travel can pull up destination city cameras alongside CII scores and travel advisories to build real-time threat pictures. **Academic Research:** Researchers studying urban dynamics, protest movements, or conflict patterns use timestamped webcam observations as supplementary evidence alongside structured data. **Citizen Awareness:** For globally-minded individuals who want to understand the world beyond headlines, webcams provide an unfiltered, human-scale view of life in distant cities. ## Privacy and Ethics World Monitor only streams publicly available webcam feeds. These are cameras operated by municipalities, broadcasters, tourism boards, and space agencies that are explicitly intended for public viewing. No private cameras, no surveillance feeds, no content that isn't already freely accessible. The platform doesn't record or archive webcam footage. Streams are live and transient, the same as visiting the source directly. ## Frequently Asked Questions **Are the webcam streams available 24/7?** Yes, the streams run continuously. However, individual cameras may go offline due to maintenance, government restrictions, or CDN issues. World Monitor's fallback retry logic automatically reconnects when a stream becomes available again. **Can I use the webcams on mobile devices?** Yes. On mobile, the webcam panel switches to single-view mode for performance. You can filter by region and swipe between cameras. **Do the webcams work in the desktop app?** Yes. The Tauri desktop app includes staggered iframe loading and a custom sidecar relay for YouTube streams, providing the most stable multi-stream experience. --- **See the world in real time at [worldmonitor.app](https://worldmonitor.app). 31 live webcams, 30+ news streams, zero login required.** ================================================ FILE: blog-site/src/content/blog/monitor-global-supply-chains-and-commodity-disruptions.md ================================================ --- title: "Monitor Global Supply Chains and Commodity Disruptions in Real Time" description: "Track commodity prices, port disruptions, pipeline infrastructure, and supply chain risks in real time. Free supply chain monitoring dashboard on World Monitor." metaTitle: "Supply Chain Monitoring Dashboard | World Monitor" keywords: "supply chain monitoring tool, commodity price dashboard, supply chain disruption alerts, global shipping tracker, commodity risk monitoring" audience: "Supply chain managers, commodity traders, logistics professionals, procurement teams, risk analysts" heroImage: "/blog/images/blog/monitor-global-supply-chains-and-commodity-disruptions.jpg" pubDate: "2026-02-26" --- In March 2021, the Ever Given blocked the Suez Canal for six days. Global trade lost an estimated $9.6 billion per day. Most supply chain teams learned about it from Twitter. The companies that recovered fastest were the ones that already had multi-source monitoring in place: ship positions, port congestion data, commodity prices, and alternative route analysis, all visible before the situation hit mainstream news. World Monitor's Commodity Monitor (commodity.worldmonitor.app) gives every supply chain team that capability. ## The Supply Chain Visibility Gap Modern supply chains are global, interconnected, and fragile. A single disruption can cascade across industries: - A drought in Taiwan affects semiconductor fabrication water supply - A coup in Niger disrupts uranium supply for European nuclear plants - Houthi attacks in the Red Sea force rerouting around the Cape of Good Hope - A port strike in Montreal affects grain exports to North Africa - GPS jamming in the Baltic disrupts automated shipping navigation Traditional supply chain tools focus on your own logistics: purchase orders, shipment tracking, inventory levels. They don't tell you about the geopolitical, military, and environmental events that create the disruptions in the first place. For a deeper look at how conflicts affect logistics, see [tracking global trade routes and chokepoints](/blog/posts/tracking-global-trade-routes-chokepoints-freight-costs/). World Monitor fills that gap. ## Live Commodity Pricing The Commodity Monitor tracks real-time prices for: **Energy:** - Crude oil (WTI and Brent) - Natural gas (Henry Hub, TTF) - Coal and uranium **Precious Metals:** - Gold, silver, platinum, palladium **Critical Minerals:** - Lithium, cobalt, nickel - Rare earth elements - Copper, aluminum, zinc **Agricultural:** - Wheat, corn, soybeans - Coffee, cocoa, sugar - Cotton, lumber Prices are sourced from CME, ICE, LME, and other major exchanges. The Commodity panel shows current price, daily change, and trend indicators. ## 10 Commodity Exchange Hubs Mapped World Monitor maps the world's **10 major commodity exchanges**: 1. **CME Group** (Chicago) - Energy, metals, agriculture 2. **ICE** (Atlanta/London) - Energy, soft commodities 3. **LME** (London) - Base metals 4. **SHFE** (Shanghai) - Metals, energy 5. **DCE** (Dalian) - Iron ore, agriculture 6. **TOCOM** (Tokyo) - Precious metals, rubber 7. **DGCX** (Dubai) - Gold, currency futures 8. **MCX** (Mumbai) - Multi-commodity 9. **Rotterdam** (Netherlands) - European energy hub 10. **Houston** (Texas) - North American energy Click any exchange for trading hours, primary instruments, and current market status. ## 83 Strategic Ports Under Watch Maritime chokepoints and major ports are the pressure points of global trade. World Monitor maps **83 strategic ports** with: - Current operational status - Geographic chokepoint proximity (Suez, Strait of Hormuz, Malacca, Panama Canal) - Connection to commodity supply chains - Regional conflict exposure When you overlay the conflict layer, you immediately see which ports are near active hotspots. When Houthi attacks escalate in the Red Sea, you can see which ports are affected and which shipping routes need rerouting, all in one view. ## AIS Maritime Tracking World Monitor's AIS (Automatic Identification System) layer shows live vessel positions from AISStream.io, merged with USNI fleet reports. For supply chain monitoring, this means: - **Track bulk carriers** moving commodities between ports - **Detect dark vessels** that have turned off transponders (potential sanctions evasion) - **Monitor naval presence** near shipping lanes that could signal disruption - **Identify congestion** at major ports by vessel density The USNI merge adds editorial context: which naval task forces are deployed where, and why. This is the difference between seeing dots on a map and understanding the security environment around your shipping routes. ## Pipeline and Undersea Cable Infrastructure World Monitor maps the physical infrastructure that global trade depends on: **Pipelines:** - Major oil and gas pipelines worldwide - Route visualization through conflict zones - Proximity alerts when pipeline routes cross escalating hotspots **Undersea Cables:** - Fiber optic cables carrying 95% of intercontinental data - Landing stations and repair zone indicators - NGA (National Geospatial-Intelligence Agency) navigational warnings for cable repair operations For digital supply chains (cloud services, financial transactions, communications), undersea cable disruption is as significant as a port closure. World Monitor shows both in the same view. ## Mining and Extraction Sites The mining layer maps active mining operations for critical minerals alongside: - Operating companies - Mineral type (lithium, cobalt, rare earth, copper) - Country risk via CII (Country Instability Index) - Proximity to conflict zones When a country's CII starts climbing, supply chain teams can proactively assess which critical mineral supply lines are at risk. ## The Infrastructure Cascade Panel This is where World Monitor's multi-domain approach provides unique value. The **Infrastructure Cascade panel** shows second-order effects of disruptions: A conflict escalation in Region X exposes: - 3 undersea cables within 600km - 2 pipeline routes through the area - 1 major port with reduced operational capacity - 2 mining operations that may suspend activity These cascade effects are what turn a localized incident into a global supply chain event. Traditional monitoring tools show the incident. World Monitor shows the blast radius. ## Natural Disaster Monitoring Supply chains don't just face geopolitical risk. Environmental events are equally disruptive: - **USGS Earthquakes (M4.5+):** Automatic alerts for seismic events near industrial infrastructure - **NASA FIRMS (VIIRS):** Satellite-detected fires that could affect agricultural regions or industrial facilities - **NASA EONET:** Volcanic eruptions, floods, and severe storms - **Cloudflare Radar:** Internet outages that disrupt digital supply chains All of these layer onto the same map as your commodity and infrastructure data. For more on these capabilities, see [natural disaster monitoring with World Monitor](/blog/posts/natural-disaster-monitoring-earthquakes-fires-volcanoes/). ## GPS Jamming: The Invisible Shipping Risk An under-discussed supply chain risk: GPS/GNSS jamming and spoofing. Ships rely on GPS for navigation, and jamming zones (detected by World Monitor from ADS-B anomaly data) can: - Force ships to rely on less precise navigation - Trigger automated route changes that add days to voyages - Indicate military activity that could escalate to shipping lane closures World Monitor maps these jamming zones using H3 hexagonal grid classification, updated in real time from aviation transponder anomalies. ## Practical Workflows for Supply Chain Teams **Daily Morning Check:** 1. Open commodity.worldmonitor.app 2. Review commodity price changes in the dashboard 3. Check the CII heatmap for rising instability in sourcing countries 4. Scan hotspot escalation scores for new disruption risks 5. Review the AI-generated World Brief for overnight developments **Disruption Response:** 1. Event detected (earthquake, conflict, port closure) 2. Toggle relevant map layers (ports, shipping routes, infrastructure) 3. Assess cascade effects via the Infrastructure Cascade panel 4. Check AIS for vessel positions and rerouting patterns 5. Review AI dossier for the affected country 6. Share situation briefing via URL state sharing **Quarterly Risk Assessment:** 1. Review CII trends for all sourcing countries 2. Map critical mineral supply lines against conflict data 3. Identify infrastructure chokepoints with escalation exposure 4. Cross-reference with prediction market data for forward-looking risk 5. Export findings via story sharing for stakeholder briefings ## Free Beats Expensive When Speed Matters Enterprise supply chain risk platforms (Resilinc, Everstream Analytics, Interos) charge five to six figures annually and require weeks of onboarding. World Monitor is available now, in your browser, for free. It's not a replacement for a full supply chain management platform. It's the situational awareness layer that tells you where to look, before your logistics system shows delays. See how World Monitor compares to [traditional intelligence tools](/blog/posts/worldmonitor-vs-traditional-intelligence-tools/). ## Frequently Asked Questions **How often are commodity prices updated?** Prices are sourced from CME, ICE, LME, and other major exchanges with near real-time updates throughout trading hours. The dashboard shows current price, daily change, and trend indicators. **Can I set alerts for specific supply chain disruptions?** Yes. World Monitor's Custom Keyword Monitors let you set persistent alerts for terms like "port closure," "pipeline disruption," or specific commodity names. Matching headlines from 435+ RSS feeds are highlighted in your chosen color. **Does the Commodity Monitor include geopolitical risk context?** Yes. The Country Instability Index (CII), conflict layers, and Infrastructure Cascade panel overlay directly onto commodity and shipping data, so you see disruption risks alongside pricing. --- **Start monitoring at [commodity.worldmonitor.app](https://commodity.worldmonitor.app). Free real-time intelligence for supply chain professionals.** ================================================ FILE: blog-site/src/content/blog/natural-disaster-monitoring-earthquakes-fires-volcanoes.md ================================================ --- title: "Earthquake, Fire, Flood: Real-Time Natural Disaster Monitoring with World Monitor" description: "Track earthquakes, satellite-detected fires, volcanic eruptions, and floods in real time. Free disaster monitoring with geopolitical context on World Monitor." metaTitle: "Natural Disaster Monitoring Dashboard | World Monitor" keywords: "real-time earthquake map, natural disaster monitoring dashboard, NASA fire detection map, disaster tracking tool free, earthquake volcano flood tracker" audience: "Emergency responders, disaster preparedness professionals, insurers, humanitarian organizations, concerned citizens" heroImage: "/blog/images/blog/natural-disaster-monitoring-earthquakes-fires-volcanoes.jpg" pubDate: "2026-02-19" --- On February 6, 2023, two earthquakes struck southern Turkey and northern Syria within hours of each other. Over 50,000 people died. In the first hours, before rescue teams mobilized, the clearest picture of the devastation came from seismic data, satellite fire detection, and population exposure overlays. World Monitor aggregates exactly these data sources into a single, layered view, giving disaster monitors real-time situational awareness from the first tremor to the long-term recovery. ## Four Disaster Data Streams, One Map ### 1. Earthquakes (USGS) World Monitor integrates the **U.S. Geological Survey earthquake feed** for all events magnitude 4.5 and above, globally. Each earthquake appears on the map with: - **Magnitude** (size-scaled marker) - **Depth** (color-coded: shallow events are more destructive) - **Location** with reverse-geocoded place name - **Timestamp** in your local time zone - **Felt reports** when available The USGS feed updates within minutes of a seismic event. For major earthquakes, World Monitor's news panel typically shows wire service alerts within 5-10 minutes, giving you both the raw seismic data and the human reporting side by side. **Why it matters beyond seismology:** Earthquakes trigger cascading effects. A magnitude 7.0 near an undersea cable route can disrupt internet traffic for an entire region. A quake near a nuclear facility triggers safety protocols. A tremor in a politically unstable country can accelerate instability. World Monitor shows all of these connections because the earthquake data shares the map with infrastructure, nuclear facilities, and CII (Country Instability Index) overlays. This is part of the broader approach to [monitoring global supply chains and commodity disruptions](/blog/posts/monitor-global-supply-chains-and-commodity-disruptions/). ### 2. Satellite Fire Detection (NASA FIRMS / VIIRS) The **Visible Infrared Imaging Radiometer Suite (VIIRS)** on NASA's Suomi NPP satellite detects thermal anomalies across the planet. World Monitor maps these detections with: - **Fire Radiative Power (FRP):** How intense is the fire? - **Location** with sub-kilometer accuracy - **Detection confidence level** - **Time of satellite pass** This isn't just wildfire tracking. Satellite fire detection reveals: - **Industrial fires** at refineries, chemical plants, or manufacturing facilities - **Agricultural burning** that affects commodity markets (palm oil, sugarcane) - **Conflict-related fires** from strikes, arson, or scorched-earth tactics - **Urban fires** in densely populated areas When you see a VIIRS hotspot cluster in an area where the conflict layer also shows activity, you may be looking at the thermal signature of an attack before any news outlet reports it. ### 3. Volcanic Eruptions and Severe Weather (NASA EONET) NASA's **Earth Observatory Natural Event Tracker (EONET)** feeds into World Monitor for: - Active volcanic eruptions - Large-scale flooding events - Severe storms and tropical cyclones - Dust storms affecting visibility and aviation - Iceberg calving events Volcanic eruptions are particularly significant for global logistics: a single eruption can close airspace for days (as Eyjafjallajokull did in 2010), disrupt semiconductor manufacturing (sulfur dioxide contamination), and affect global temperature patterns. ### 4. Climate Anomalies World Monitor tracks temperature, precipitation, and sea level anomalies that indicate developing conditions: - **Drought indicators** that threaten agricultural output and water-dependent manufacturing - **Flooding risk** from sustained precipitation anomalies - **Marine heatwaves** that affect fishing yields and ocean shipping routes ## Population Exposure: Who's at Risk? Raw disaster data tells you where something happened. **Population exposure overlays** tell you who's affected. World Monitor integrates WorldPop population density data with disaster events to estimate: - How many people live within the impact zone - Urban vs. rural distribution of affected populations - Proximity to critical infrastructure (hospitals, airports, ports) When an earthquake strikes, the population exposure overlay immediately shows whether it hit a dense urban area or a rural region, dramatically changing the humanitarian response calculation. ## Infrastructure Cascade: What Breaks Next? Natural disasters don't just affect people. They disrupt the systems people depend on. World Monitor's **Infrastructure Cascade panel** automatically calculates second-order effects when a disaster event overlaps with critical infrastructure: - **Undersea cables** within range of an earthquake epicenter - **Pipelines** crossing flood zones - **Ports** exposed to storm surge - **Nuclear facilities** near seismic activity - **Datacenters** in wildfire zones - **Power grid** nodes in affected regions A magnitude 6.5 earthquake off the coast of Portugal might not make global headlines, but if three undersea cables cross that zone, financial transactions between Europe and the Americas could slow for days. World Monitor makes that connection visible. ## Displacement Flows: The Human Aftermath World Monitor integrates **UNHCR displacement data** to show refugee and internally displaced person (IDP) migration patterns. When a disaster strikes, you can see: - Historical displacement from the affected region - Existing refugee populations that may face compounding vulnerability - Transit routes and host countries likely to receive new displacement This data is invaluable for humanitarian organizations planning response operations. ## Practical Workflows ### For Emergency Management 1. Earthquake alert appears on map (USGS, magnitude 6.2) 2. Check population exposure overlay for affected population estimate 3. Review infrastructure cascade for damaged utilities and transport 4. Toggle satellite fire detection for secondary fires 5. Check webcam feeds from nearest major city 6. Monitor news panel for early situation reports 7. Share situation briefing via URL state to team ### For Insurance and Reinsurance 1. Set custom keyword monitors for "earthquake," "wildfire," "flood" 2. When triggered, review magnitude/intensity and location 3. Overlay population density for exposure estimation 4. Check infrastructure layer for insured asset proximity 5. Compare with CII for political stability context (claims processing complexity) 6. Generate AI brief for initial loss assessment context ### For Humanitarian Response 1. Monitor CII for countries with rising instability (pre-existing vulnerability) 2. When disaster strikes vulnerable region, assess compounding risk 3. Review displacement data for existing humanitarian burden 4. Check port and airport status for logistics access 5. Monitor Telegram OSINT for ground-truth reports from local observers 6. Cross-reference with travel advisories for staff safety ### For Commodity Markets 1. Satellite fire detection triggers in major agricultural region 2. Check FRP intensity and affected area 3. Overlay with crop/commodity production zones 4. Assess pipeline/port proximity for energy commodity impact 5. Review AI-generated brief for market implications 6. Monitor commodity price panel for immediate price response ## Real-Time Alerts Through Custom Keyword Monitors World Monitor's **Custom Keyword Monitors** let you set persistent alerts for natural disaster terms: - Set monitors for "earthquake," "tsunami," "wildfire," "hurricane," "volcanic" - Color-code each monitor category - When matching headlines appear in the 435+ RSS feeds, they're highlighted in your custom color - Monitors persist across sessions via localStorage Combined with the map layers, you have a complete early warning system: spatial data on the map, textual alerts in the news panel, AI analysis in the brief, and [live video for ground truth](/blog/posts/live-webcams-from-geopolitical-hotspots/). ## Why World Monitor for Disaster Monitoring Dedicated disaster monitoring platforms exist (GDACS, ReliefWeb, PDC Global). World Monitor's advantage isn't replacing them. It's integrating disaster data with: - Geopolitical context (CII scores, conflict data) - Infrastructure dependency mapping - Financial market impact (commodity prices, exchange status) - AI analysis for rapid situation synthesis - Multi-source verification (satellite, seismic, news, webcam, OSINT) A disaster doesn't happen in isolation. Its impact depends on the political stability of the affected country, the infrastructure that fails, the markets that react, and the humanitarian capacity available. World Monitor shows all of these in one view. Learn more about [what World Monitor is and how it works](/blog/posts/what-is-worldmonitor-real-time-global-intelligence/). ## Frequently Asked Questions **How quickly do earthquake alerts appear on the map?** USGS data typically updates within minutes of a seismic event. World Monitor displays all earthquakes magnitude 4.5 and above globally, with magnitude, depth, location, and timestamp. **Does World Monitor detect wildfires directly?** World Monitor uses NASA FIRMS satellite data (VIIRS sensor) to map thermal anomalies with sub-kilometer accuracy. This covers wildfires, industrial fires, agricultural burning, and conflict-related fires. **Can I set up alerts for natural disasters in specific regions?** Yes. Use Custom Keyword Monitors for terms like "earthquake," "wildfire," or "flood." Matching headlines from 435+ RSS feeds are highlighted in your chosen color and persist across sessions. --- **Monitor natural disasters in context at [worldmonitor.app](https://worldmonitor.app). USGS, NASA, and AI analysis, all in one free dashboard.** ================================================ FILE: blog-site/src/content/blog/osint-for-everyone-open-source-intelligence-democratized.md ================================================ --- title: "OSINT for Everyone: How World Monitor Democratizes Open Source Intelligence" description: "World Monitor brings professional-grade OSINT to everyone. 435+ feeds, live tracking, AI threat analysis, and 45 data layers in one free open source dashboard." metaTitle: "OSINT for Everyone: Free Intelligence Dashboard" keywords: "OSINT tools free, open source intelligence software, OSINT dashboard, intelligence gathering tools, OSINT for beginners" audience: "OSINT researchers, security analysts, journalists, hobbyist investigators" heroImage: "/blog/images/blog/osint-for-everyone-open-source-intelligence-democratized.jpg" pubDate: "2026-02-17" --- Open source intelligence used to require a dozen subscriptions, custom scrapers, and years of domain expertise. A professional OSINT analyst's browser might have 50+ tabs open at any given time: flight trackers, ship trackers, earthquake monitors, conflict databases, Telegram channels, RSS readers, and satellite imagery viewers. World Monitor collapses that entire workflow into a single interactive dashboard. ## The Tab Sprawl Problem If you've ever tried to monitor a developing situation, whether it's a military escalation, a natural disaster, or a supply chain disruption, you know the drill: 1. Open FlightRadar24 for aircraft movements 2. Open MarineTraffic for ship positions 3. Open USGS for earthquake data 4. Open ACLED for conflict events 5. Open Liveuamap for real-time mapping 6. Open Reuters, AP, and Al Jazeera for news 7. Open Telegram for raw OSINT channels 8. Open Polymarket for prediction markets 9. Open gpsjam.org for GPS interference 10. Open NASA FIRMS for fire detection Each tool has its own interface, its own refresh cycle, its own learning curve. Cross-referencing between them is manual and slow. By the time you've built a picture, the situation has moved. World Monitor integrates all of these data sources (and many more) into a single, layered map with real-time updates. Learn more about [what World Monitor is and how it works](/blog/posts/what-is-worldmonitor-real-time-global-intelligence/). ## 435+ Intelligence Feeds, Zero Configuration World Monitor aggregates **435+ RSS feeds** organized across 15 categories: - Geopolitics and defense - Middle East and North Africa - Africa and Sub-Saharan - Think tanks and policy institutes - Technology and AI - Finance and markets - Energy and commodities - Cybersecurity Each feed is classified by a **4-tier credibility system**, so you always know whether you're reading a primary source or secondary analysis. Server-side aggregation reduces API calls by 95%, and per-feed circuit breakers ensure one broken source doesn't take down the dashboard. ## Live Tracking: Ships, Planes, and Signals Three of World Monitor's most powerful layers bring live tracking to your screen: ### ADS-B Aircraft Tracking Military and civilian aircraft positions update in real time via OpenSky and Wingbits enrichment. The system automatically identifies military aircraft and displays their callsigns, types, and flight paths on the map. ### AIS Maritime Monitoring Ship positions from AISStream.io are merged with **USNI Fleet Reports**, giving you both transponder data and editorial context from the U.S. Naval Institute. This combination reveals the complete order-of-battle for major naval deployments, something that usually requires a classified briefing. ### GPS/GNSS Jamming Detection ADS-B anomaly data is processed through an H3 hexagonal grid to identify zones where GPS signals are being jammed or spoofed. This is a critical indicator of electronic warfare activity, and World Monitor maps it automatically. ## 26 Telegram OSINT Channels World Monitor integrates **26 curated Telegram channels** via MTProto, organized by reliability tier: - **Tier 1:** Verified primary sources - **Tier 2:** Established OSINT accounts (Aurora Intel, BNO News, DeepState, OSINT Defender, LiveUAMap) - **Tier 3:** Secondary aggregators (Bellingcat, NEXTA, War Monitor) These channels often break news 15-30 minutes before traditional media. Having them integrated alongside verified feeds gives you both speed and context. ## AI-Powered Threat Classification Raw intelligence is only useful if you can process it. World Monitor runs a **3-stage threat classification pipeline**: 1. **Keyword matching** for immediate categorization 2. **Browser-based ML** (Transformers.js running in Web Workers) for sentiment and entity extraction 3. **LLM classification** for nuanced threat assessment This runs locally in your browser. No data leaves your machine unless you explicitly choose a cloud LLM provider. ## The Country Instability Index One of World Monitor's original contributions to OSINT is the **Country Instability Index (CII)**, a real-time 0-100 score computed for every monitored nation: - **Baseline risk (40%):** Historical conflict data, governance indicators - **Unrest indicators (20%):** Protests, strikes, civil disorder events - **Security events (20%):** Military activity, terrorism, border incidents - **Information velocity (20%):** News volume spikes that indicate developing situations The CII is boosted by real-time signals: proximity to active hotspots, OREF rocket alerts, GPS jamming activity, and travel advisory changes. The result is a heatmap overlay that shows, at a glance, where instability is rising. ## Hotspot Escalation Scoring World Monitor doesn't just show you where things are happening. It tells you where they're getting worse. The **Hotspot Escalation Score** combines: - News activity (35%) - CII score (25%) - Geographic convergence (25%): when 3+ event types co-occur within the same 1-degree grid cell in 24 hours - Military indicators (15%) When a region's escalation score spikes, it surfaces in the Strategic Risk panel before traditional media picks up the story. ## Sharing Intelligence Found something significant? World Monitor's story sharing lets you export intelligence briefs to Twitter/X, LinkedIn, WhatsApp, Telegram, and Reddit, complete with auto-generated Open Graph images for social previews. You can also share map states via URL: the map position, active layers, time range, and selected data points are all encoded in a shareable link. Send a colleague a URL and they see exactly what you see. ## Getting Started with World Monitor for OSINT 1. **Open worldmonitor.app** in any modern browser 2. **Toggle layers** using the left sidebar: start with "Conflicts" and "Military Bases" 3. **Click any data point** on the map for details and source links 4. **Open the [Command Palette](/blog/posts/command-palette-search-everything-instantly/)** (Cmd+K / Ctrl+K) to fuzzy-search across 24 result types and 250+ country commands 5. **Click any country** for its full intelligence dossier with CII score 6. **Set up keyword monitors** for topics you want to track persistently No account needed. No API keys required for the web version. For local AI analysis, install Ollama and point World Monitor at your local instance. You can also explore [AI-powered intelligence without the cloud](/blog/posts/ai-powered-intelligence-without-the-cloud/). ## Why Open Source Matters for OSINT Closed-source intelligence tools are black boxes. You can't verify how they score threats, where their data comes from, or whether their algorithms have blind spots. World Monitor's AGPL-3.0 license means every scoring algorithm, every data pipeline, and every AI prompt is open for inspection. Security researchers can audit it. Academics can cite it. Developers can extend it. And anyone can self-host it for complete operational security. ## Frequently Asked Questions **Is World Monitor really free for OSINT research?** Yes. Every feature, data source, and AI capability is available at no cost with no account required. The platform is open source under AGPL-3.0, so you can also self-host it. **Do I need technical skills to use World Monitor for OSINT?** No. The interface is designed for analysts of all skill levels. Toggle layers on the sidebar, click data points for details, and use the Command Palette (Cmd+K) to search across all intelligence sources instantly. **How does World Monitor compare to traditional OSINT tools?** World Monitor consolidates 435+ feeds, live tracking, AI analysis, and 45 data layers into one dashboard. Traditional tools require juggling dozens of separate platforms. See our [detailed comparison with traditional intelligence tools](/blog/posts/worldmonitor-vs-traditional-intelligence-tools/). --- **Start your OSINT workflow at [worldmonitor.app](https://worldmonitor.app). Free, open source, and no login required.** ================================================ FILE: blog-site/src/content/blog/prediction-markets-ai-forecasting-geopolitics.md ================================================ --- title: "Predict What Happens Next: Prediction Markets and AI Forecasting in World Monitor" description: "World Monitor combines Polymarket prediction odds with AI geopolitical forecasting. See market probabilities alongside live intelligence for actionable insights." metaTitle: "Prediction Markets + AI Forecasting | World Monitor" keywords: "prediction markets geopolitics, Polymarket intelligence tool, AI geopolitical forecasting, geopolitical risk prediction, political prediction dashboard" audience: "Geopolitical analysts, traders using prediction markets, policy researchers, forecasting enthusiasts" heroImage: "/blog/images/blog/prediction-markets-ai-forecasting-geopolitics.jpg" pubDate: "2026-03-03" --- Intelligence is about the past and present. Forecasting is about what comes next. Most dashboards give you one or the other. World Monitor gives you both. By integrating **Polymarket prediction market data** with **AI-powered geopolitical forecasting**, World Monitor lets you see not just what's happening, but what the collective intelligence of bettors and algorithms thinks will happen. ## Polymarket Integration: The Wisdom of Crowds Prediction markets have consistently outperformed expert panels, polls, and traditional forecasting models. When real money is on the line, participants have strong incentives to be accurate rather than ideological. World Monitor pulls real-time data from **Polymarket**, the largest decentralized prediction market: - **Yes/No probability bars** with percentage displays - **Trading volume** ($K/$M) indicating market confidence - **Expiration dates** for time-bound predictions - **Direct links** to the Polymarket question for deeper analysis or trading The predictions panel shows questions relevant to geopolitical events: elections, military escalations, trade deals, policy decisions, sanctions, and diplomatic outcomes. ## What Prediction Markets Tell You (That News Doesn't) News tells you what happened. Analysis tells you what it means. Prediction markets tell you what's likely to happen next, with a probability attached. **Example: Iran Nuclear Deal** - News: "Talks resume in Vienna" - Analysis: "Prospects remain uncertain" - Prediction Market: "Nuclear deal by December: 23% ($4.2M volume)" That 23% is more actionable than any editorial. And the $4.2M volume tells you this isn't idle speculation; it's informed money. **Example: Taiwan Strait** - News: "PLA conducts exercises near Taiwan" - Analysis: "Tensions elevated but unclear" - Prediction Market: "China military action against Taiwan in 2025: 8% ($12M volume)" The 8% is low, but it was 3% last month. The direction matters as much as the level. World Monitor displays these probabilities alongside the intelligence data that drives them, so you can evaluate whether the market is ahead of or behind the signals. ## AI Deduction: Machine Forecasting Grounded in Data World Monitor's **AI Deduction panel** goes beyond simple summarization. It provides interactive geopolitical timeline forecasting: 1. **Select a developing situation** (a country, a theater, a specific event) 2. The AI synthesizes current data: CII scores, news velocity, military signals, prediction market odds 3. It generates **potential escalation and de-escalation paths** with reasoning 4. Each forecast point is grounded in **cited headlines and data points** 5. Cross-reference with prediction market data for market sentiment alignment This isn't the AI guessing. It's the AI organizing the signals you're already seeing into possible futures, with sources you can verify. ### The 4-Tier LLM Chain for Forecasting The AI Deduction feature uses World Monitor's standard 4-tier fallback: 1. **Local LLM (Ollama/LM Studio):** Fully private forecasting on your hardware 2. **Groq (Llama 3.1 8B):** Fast cloud inference 3. **OpenRouter:** Multi-model fallback 4. **Browser T5:** Offline capability via Transformers.js For sensitive forecasting work (government, corporate intelligence), Tier 1 means your analytical queries never leave your network. Learn more about [running AI intelligence locally](/blog/posts/ai-powered-intelligence-without-the-cloud/). ## Triangulating Signals: Markets + AI + Data The most powerful use of World Monitor's forecasting isn't any single source. It's the triangulation: | Signal Source | What It Provides | Strength | |--------------|------------------|----------| | **Prediction markets** | Crowd-aggregated probability | Calibrated, market-tested | | **AI Deduction** | Structured scenario analysis | Comprehensive, sourced | | **CII scores** | Quantitative instability measure | Algorithmic, consistent | | **News velocity** | Information flow rate | Leading indicator | | **Military signals** | Force posture changes | Physical, verifiable | | **Telegram OSINT** | Raw ground-level intelligence | Fast, unfiltered | When all six point in the same direction, confidence is high. When they diverge, you've found an interesting signal: either the market is wrong, the AI is missing context, or there's information asymmetry worth investigating. ## Practical Forecasting Workflows ### Geopolitical Risk Analyst 1. Open the **Predictions panel** for current market odds on key scenarios 2. Compare with **CII trends** for involved countries 3. Check **Strategic Theater Posture** for relevant military theaters 4. Run **AI Deduction** for structured scenario analysis 5. Review **Telegram OSINT** for ground-level context not yet in markets 6. Document assessment using **story sharing** for team distribution ### Macro Trader 1. Review prediction market odds for upcoming elections and policy decisions 2. Overlay with **macro radar** signals (BUY/CASH) 3. Check **central bank tracker** for rate decision probabilities 4. Assess **commodity exposure** if predictions involve resource-rich regions 5. Position based on where prediction market odds diverge from your intelligence assessment ### Policy Researcher 1. Track prediction market evolution over time for a specific issue 2. Compare market-implied probabilities with think tank forecasts 3. Use **AI Deduction** to generate structured scenario trees 4. Cross-reference with **travel advisories** for allied government assessments 5. Build forward-looking briefs combining quantitative and qualitative forecasts ### Humanitarian Planner 1. Monitor prediction markets for conflict escalation probabilities 2. Combine with **CII scores** and **displacement data** for vulnerability mapping 3. Use **AI Deduction** to assess potential population displacement scenarios 4. Pre-position resources based on highest-probability escalation paths 5. Monitor **webcams** and **news velocity** for early indicators that forecasts are materializing ## Prediction Markets as a Leading Indicator Research consistently shows prediction markets move before traditional indicators: - **Elections:** Markets often lead polls by days - **Military events:** Probabilities shift when informed participants spot signals - **Policy decisions:** Market odds adjust on insider signals before official announcements - **Economic events:** Rate decision probabilities incorporate real-time data By integrating these leading indicators alongside lagging indicators (news reports, conflict databases) and coincident indicators (live tracking, webcams, CII), World Monitor gives you the full temporal spectrum of intelligence. ## The Country Intelligence Dossier: Forecasting Context Click any country on the World Monitor globe and the intelligence dossier includes: - **Current prediction market questions** relevant to that country - **CII score with trend direction** (rising/falling instability) - **AI-generated forward-looking assessment** - **Active signals** that may drive near-term outcomes - **Historical pattern** for context (how similar situations have resolved before) This means every country on the map comes with a built-in forecasting context. You don't need to search for predictions separately; they're part of the intelligence picture. For a deeper look at how traders use these signals, see [real-time market intelligence for traders](/blog/posts/real-time-market-intelligence-for-traders-and-analysts/). ## Accuracy and Limitations World Monitor surfaces prediction market data and AI analysis as tools, not oracles: - **Prediction markets** are well-calibrated on average but can be wrong on any individual question - **AI forecasting** is grounded in cited data but can miss context that isn't in the training data - **CII scores** are algorithmic and may not capture rapid shifts from unprecedented events - **No single signal** should drive high-stakes decisions alone The value is in the combination. Multiple independent signals converging on the same forecast is far more reliable than any single source. See how World Monitor [tracks global conflicts in real time](/blog/posts/track-global-conflicts-in-real-time/) to provide the data these forecasts rely on. ## Frequently Asked Questions **How accurate are prediction markets for geopolitical forecasting?** Prediction markets have consistently outperformed expert panels and polls in aggregate. When real money is at stake, participants are incentivized to be accurate. However, no single source should drive high-stakes decisions alone. **Can I run the AI forecasting locally without sending data to the cloud?** Yes. World Monitor supports local LLMs via Ollama or LM Studio. Your analytical queries stay on your machine entirely, making it suitable for sensitive government or corporate intelligence work. **What data does the AI use to generate forecasts?** The AI synthesizes CII scores, news velocity, military signals, Telegram OSINT, and Polymarket odds. Every forecast point is grounded in cited headlines and data points you can verify independently. --- **See what's coming at [worldmonitor.app](https://worldmonitor.app). Prediction markets, AI forecasting, and 45 intelligence layers, all free.** ================================================ FILE: blog-site/src/content/blog/real-time-market-intelligence-for-traders-and-analysts.md ================================================ --- title: "Real-Time Market Intelligence: How Traders Use World Monitor's Finance Dashboard" description: "Monitor 92 stock exchanges, 13 central banks, commodities, and macro signals in one free dashboard. World Monitor Finance gives traders the geopolitical edge." metaTitle: "Real-Time Market Intelligence for Traders | World Monitor" keywords: "real-time market intelligence, stock market dashboard free, financial intelligence platform, macro trading signals, market monitoring tool" audience: "Retail and professional traders, financial analysts, macro investors, fintech enthusiasts" heroImage: "/blog/images/blog/real-time-market-intelligence-for-traders-and-analysts.jpg" pubDate: "2026-02-21" --- Markets don't move in isolation. A drone strike in the Persian Gulf moves oil futures. A surprise rate hold from the ECB shifts forex pairs. A GPS jamming spike near the Baltic signals military exercises that rattle European equities. Traditional financial dashboards show you price. World Monitor shows you context. ## Finance Monitor: Markets Meet Geopolitics World Monitor's Finance variant (finance.worldmonitor.app) combines traditional market data with the geopolitical intelligence that drives price action. It's built for traders who understand that a Reuters headline and a ship position can be more valuable than a moving average. Here's what you get: ## 92 Global Stock Exchanges on One Map Every major exchange, from the NYSE and NASDAQ to the Tadawul and BSE, is plotted on the interactive map with: - Current market cap - Trading hours (with live open/close status) - Regional grouping - Click-through to detailed metrics See at a glance which markets are open, where volume is concentrated, and how exchanges cluster by region. The visual layout makes time zone arbitrage opportunities obvious. ## 7-Signal Macro Radar World Monitor's macro radar synthesizes seven independent signals into a composite **BUY or CASH verdict**: The radar doesn't tell you what to buy. It tells you whether the macro environment favors risk-on or risk-off positioning. Think of it as the weather forecast for markets: you still pick where to go, but you know whether to bring an umbrella. ## 13 Central Bank Policy Trackers Interest rates drive everything. World Monitor tracks policy decisions from 13 central banks: - Federal Reserve (Fed) - European Central Bank (ECB) - Bank of Japan (BoJ) - Bank of England (BoE) - People's Bank of China (PBoC) - Swiss National Bank (SNB) - Reserve Bank of Australia (RBA) - Bank of Canada (BoC) - Reserve Bank of India (RBI) - Bank of Korea (BoK) - Central Bank of Brazil (BCB) - Saudi Arabian Monetary Authority (SAMA) - BIS and IMF for systemic indicators Each tracker includes BIS data on policy rates, real effective exchange rates (REER), and credit-to-GDP ratios, the indicators that matter for macro positioning. ## Commodity Hubs and Energy Markets For commodity traders, World Monitor maps the **10 major commodity exchanges** (CME, ICE, LME, SHFE, DCE, TOCOM, DGCX, MCX, Rotterdam, Houston) alongside live pricing for: - Crude oil (WTI and Brent) - Natural gas - Gold and silver - Critical minerals (lithium, cobalt, rare earths) - Agricultural commodities The Commodity Monitor variant (commodity.worldmonitor.app) goes deeper with mining company locations, pipeline infrastructure, and supply chain disruption alerts. For more on supply chain tracking, see [monitoring global supply chains and commodity disruptions](/blog/posts/monitor-global-supply-chains-and-commodity-disruptions/). ## Crypto Intelligence The crypto panels provide institutional-grade monitoring: - **Stablecoin peg tracker:** USDT, USDC, DAI, FDUSD, and USDe with real-time deviation alerts - **BTC spot ETF flow tracker:** Daily inflows/outflows for IBIT, FBTC, GBTC, and 7 more funds - **Fear & Greed Index:** With 30-day history chart - **Bitcoin technical signals:** SMA50, SMA200, VWAP, and Mayer Multiple - **BTC hashrate** via mempool.space When USDT depegs by 0.3%, you know before most trading desks. ## Gulf FDI Investment Tracker A unique feature for emerging market investors: World Monitor maps **64 Gulf FDI investments** from Saudi Arabia and the UAE, color-coded by status (announced, in progress, completed). This covers the Vision 2030 and beyond, the largest sovereign investment programs in history. For anyone investing in MENA, this is context that Bloomberg charges premium for. ## Prediction Markets Integration World Monitor integrates **Polymarket** data directly into country dossiers and the prediction panel. See what bettors think about upcoming elections, military escalations, trade deals, and policy changes. Prediction markets are consistently among the best forecasting tools available. Having them alongside news feeds and geopolitical scoring lets you triangulate signal from noise. Explore how [prediction markets and AI forecasting](/blog/posts/prediction-markets-ai-forecasting-geopolitics/) work together in World Monitor. ## The Geopolitical Edge Here's what makes World Monitor different from every other financial dashboard: the geopolitical layer. When you see oil prices spiking, you can toggle the military layer and check if there's unusual naval activity in the Strait of Hormuz. When a currency drops, you can check if the country's CII (Country Instability Index) has been rising. When equities sell off, you can look at the Strategic Theater Posture to see if a military theater has escalated. This cross-domain intelligence used to be the province of hedge fund research desks with million-dollar budgets. World Monitor puts it in your browser for free. See how it [compares to traditional intelligence tools](/blog/posts/worldmonitor-vs-traditional-intelligence-tools/). ## 19 Financial Centers Ranked World Monitor maps the world's **19 major financial centers** ranked by the Global Financial Centres Index. Click any center for: - Current ranking and score - Key institutions headquartered there - Regulatory environment overview - Time zone and trading hours ## How Traders Use World Monitor **Morning Routine:** 1. Open Finance Monitor 2. Check macro radar verdict (BUY/CASH) 3. Scan central bank tracker for overnight decisions 4. Review hotspot escalation scores for geopolitical risk 5. Check prediction markets for upcoming event probabilities **During Trading Hours:** - Keep the live news panel open for breaking headlines - Monitor stablecoin pegs for crypto liquidity signals - Watch the CII heatmap for country-level risk changes - Track ETF flows for institutional sentiment **Post-Market:** - Review the day's convergence events (co-occurring geopolitical signals) - Check AI-generated World Brief for overnight synthesis - Share notable findings via built-in story sharing ## Free. No Login. No Data Harvesting. World Monitor Finance is completely free with no account required. Your data stays in your browser. There's no "premium tier" where the useful features hide, every panel, every data source, every AI feature is available to everyone. The entire platform is open source under AGPL-3.0, meaning the algorithms behind every score and signal are auditable. ## Frequently Asked Questions **Is World Monitor Finance free to use?** Yes. Every panel, data source, and AI feature is available at no cost with no account required. There is no premium tier. The platform is open source under AGPL-3.0. **How does World Monitor differ from Bloomberg or Reuters terminals?** World Monitor uniquely overlays geopolitical intelligence (conflict data, military tracking, instability scores) on top of financial data. Traditional terminals focus on price and fundamentals; World Monitor adds the geopolitical context that drives price action. **How often is market data updated?** Market data refreshes in real time during trading hours. Central bank trackers, macro signals, and commodity prices update continuously through server-side aggregation with per-source circuit breakers for reliability. --- **Open Finance Monitor at [finance.worldmonitor.app](https://finance.worldmonitor.app). Your geopolitical edge starts here.** ================================================ FILE: blog-site/src/content/blog/satellite-imagery-orbital-surveillance.md ================================================ --- title: "Satellite Eyes: How World Monitor Brings Orbital Surveillance to Your Browser" description: "Access satellite imagery of geopolitical hotspots in World Monitor. Search by location, time, and cloud cover with STAC API, overlaid on 44 live intelligence layers." metaTitle: "Satellite Imagery for OSINT | World Monitor" keywords: "satellite imagery OSINT, free satellite intelligence, orbital surveillance dashboard, STAC API satellite search, geopolitical satellite monitoring" audience: "OSINT analysts, remote sensing enthusiasts, defense researchers, environmental monitors" heroImage: "/blog/images/blog/satellite-imagery-orbital-surveillance.jpg" pubDate: "2026-02-28" --- Satellite imagery used to require government clearance or a Maxar contract. Today, a growing constellation of Earth observation satellites captures the planet daily, and World Monitor brings that data directly into your intelligence workflow. ## The Orbital Surveillance Layer World Monitor's orbital surveillance layer overlays satellite imagery onto both the 3D globe and flat map views. This isn't just a static basemap. It's searchable, time-filtered satellite data integrated with the same geopolitical intelligence layers you use for everything else. **What you get:** - Real satellite images of geopolitical hotspots - Time-range queries to compare before and after events - Cloud coverage percentage so you know if the image is useful - Resolution metadata for assessing detail level - Seamless overlay with conflict data, military bases, and infrastructure layers ## STAC API: The Engine Behind the Imagery World Monitor connects to satellite data through the **STAC (SpatioTemporal Asset Catalog) API**, the open standard that makes Earth observation data searchable. Instead of browsing through satellite operator portals, you search by: - **Location:** Click any point on the map - **Time range:** Specify when you want imagery from - **Cloud coverage:** Filter out cloudy images The system returns available satellite passes, ranked by relevance, with preview thumbnails directly in the panel. ## Intelligence Use Cases for Satellite Imagery ### Conflict Verification News reports claim a military buildup near a border. The conflict layer shows increased news activity. ADS-B shows military flight patterns. Now pull satellite imagery to see if there are new vehicle concentrations, field camps, or infrastructure construction. Satellite imagery provides the physical evidence that other intelligence signals suggest. See how World Monitor [tracks global conflicts in real time](/blog/posts/track-global-conflicts-in-real-time/) for the data that makes satellite verification actionable. ### Infrastructure Damage Assessment After a reported strike on a pipeline, port, or datacenter, satellite imagery shows the actual damage. Compare pre-event and post-event images using the time-range query to see what changed. ### Environmental Monitoring Track deforestation, mining expansion, flooding, and fire damage. The NASA FIRMS fire layer shows active hotspots; satellite imagery shows the aftermath and extent. For more on natural hazard tracking, see [natural disaster monitoring with World Monitor](/blog/posts/natural-disaster-monitoring-earthquakes-fires-volcanoes/). ### Maritime Intelligence Combine AIS vessel tracking with satellite imagery to: - Verify ship positions in areas where vessels go "dark" (turn off transponders) - Monitor port congestion and new construction at strategic harbors - Track military naval base expansion over time ### Nuclear Facility Monitoring World Monitor maps nuclear facilities worldwide. Satellite imagery adds visual verification: is there new construction? Are cooling systems active? Are there vehicle patterns suggesting operational changes? ## Cross-Layer Integration The orbital layer becomes most powerful when combined with World Monitor's other 44 data layers: | Situation | Intelligence Layers | + Satellite Adds | |-----------|-------------------|------------------| | Military buildup | ADS-B + bases + news | Visual confirmation of troop/vehicle concentrations | | Pipeline attack | Infrastructure + conflict | Damage extent and repair activity | | Port blockade | AIS + maritime + news | Ship congestion visualization | | Nuclear activity | Nuclear facilities + CII | Construction changes, thermal signatures | | Protest camp | Conflict + Telegram OSINT | Crowd size estimation, barricade placement | | Natural disaster | USGS + NASA FIRMS | Damage footprint, flood extent | No other free dashboard lets you overlay satellite imagery on top of real-time conflict data, military tracking, and AI-scored intelligence, in the same view. Explore the full [OSINT capabilities World Monitor offers](/blog/posts/osint-for-everyone-open-source-intelligence-democratized/). ## Resolution and Coverage Satellite imagery resolution varies by source. World Monitor displays metadata for each image so you know what you're working with: - **Low resolution (250m+):** Weather patterns, large-scale environmental changes - **Medium resolution (10-30m):** Land use changes, large military installations - **High resolution (1-5m):** Individual buildings, vehicle concentrations, infrastructure details Coverage depends on satellite revisit rates and cloud conditions. Equatorial regions have more frequent coverage; high-latitude areas may have gaps. The cloud coverage filter helps you quickly find usable images. ## Desktop-Enhanced Experience The orbital surveillance layer is available across all platforms, with the desktop app providing the smoothest experience for high-resolution imagery browsing. The Tauri app's local Node.js sidecar handles STAC API queries efficiently, and CSP (Content Security Policy) is configured to allow satellite preview image loading from trusted sources. ## How to Use It 1. Open World Monitor and toggle the **Orbital Surveillance** layer 2. Navigate to your area of interest on the map 3. Open the **Satellite Imagery** panel 4. Set your time range (last 7 days, 30 days, or custom) 5. Filter by cloud coverage (less than 20% recommended for useful imagery) 6. Browse available passes and click to overlay on the map 7. Toggle other layers (conflicts, infrastructure, military) to cross-reference ## The Future of Open Satellite Intelligence Commercial satellite constellations are growing rapidly. More satellites mean more frequent revisits, higher resolution, and faster delivery. As this data becomes more accessible, tools like World Monitor that integrate imagery into multi-source intelligence workflows will become essential. The days of satellite intelligence being locked in classified systems are ending. World Monitor puts orbital surveillance alongside 44 other intelligence layers, in your browser, for free. ## Frequently Asked Questions **Is the satellite imagery on World Monitor free?** Yes. World Monitor connects to open satellite data through the STAC API standard. You can search, filter, and overlay imagery at no cost with no account required. **What resolution satellite imagery is available?** Resolution varies by source, from 250m+ for weather patterns down to 1-5m for individual buildings and vehicle concentrations. Each image includes resolution metadata so you know the detail level before analyzing it. **Can I compare before and after satellite images of an event?** Yes. Use the time-range query feature to pull imagery from different dates. This is particularly useful for damage assessment, military buildup verification, and tracking infrastructure changes over time. --- **Explore satellite imagery at [worldmonitor.app](https://worldmonitor.app). Toggle the orbital surveillance layer and see the world from above.** ================================================ FILE: blog-site/src/content/blog/track-global-conflicts-in-real-time.md ================================================ --- title: "Track Global Conflicts in Real Time: World Monitor's Geopolitical Intelligence" description: "Monitor active conflicts, military movements, and geopolitical escalation in real time. World Monitor tracks 210+ bases across 9 theaters with live ADS-B data." metaTitle: "Track Global Conflicts in Real Time | World Monitor" keywords: "real-time conflict map, geopolitical intelligence map, military tracking dashboard, conflict monitoring tool, global conflict tracker" audience: "Geopolitical analysts, defense researchers, policy makers, journalists covering conflict" heroImage: "/blog/images/blog/track-global-conflicts-in-real-time.jpg" pubDate: "2026-02-14" --- When a military escalation begins, the first 24 hours define the narrative. Analysts who see the signals early, the unusual flight patterns, the naval repositioning, the news velocity spike, have a decisive advantage over those waiting for the morning briefing. World Monitor was built to give you those 24 hours back. ## A Situation Room in Your Browser World Monitor's core dashboard (worldmonitor.app) is designed around one question: **what's happening in the world right now, and where is it getting worse?** The answer comes from layering multiple intelligence sources onto a single interactive 3D globe: - **ACLED conflict data** for armed clashes, protests, and political violence - **UCDP warfare events** for state-based and non-state conflicts - **Live ADS-B tracking** for military aircraft positions - **AIS vessel monitoring** merged with USNI fleet reports for naval movements - **26 Telegram OSINT channels** for raw, low-latency intelligence - **OREF rocket alerts** with 1,480 Hebrew-to-English siren translations - **GPS/GNSS jamming zones** detected from ADS-B anomalies - **NASA satellite fire detection** (VIIRS) for ground-truth verification Each layer can be toggled independently. Combine them to build a multi-source picture of any developing situation. For a broader look at what the platform offers, see [What Is World Monitor?](/blog/posts/what-is-worldmonitor-real-time-global-intelligence/). ## 9 Strategic Theaters Under Continuous Assessment World Monitor maintains real-time posture assessments for 9 operational theaters: 1. **Iran / Persian Gulf:** Strait of Hormuz chokepoint, IRGC activity, proxy conflict indicators 2. **Taiwan Strait:** PLA military exercises, naval deployments, airspace incursions 3. **Baltic Region:** NATO-Russia friction, Kaliningrad corridor, submarine activity 4. **Korean Peninsula:** DMZ incidents, missile tests, force posture changes 5. **Eastern Mediterranean:** Israel-Hezbollah dynamics, energy disputes, naval presence 6. **Horn of Africa:** Houthi maritime threats, Red Sea shipping disruption, piracy 7. **South China Sea:** Island militarization, fishing militia, freedom of navigation operations 8. **Arctic:** Resource competition, Northern Sea Route, military basing 9. **Black Sea:** Ukraine conflict, grain corridor, naval mine risk Each theater's posture level is synthesized from news velocity, military movements, CII scores of involved nations, and historical escalation patterns. ## The Country Instability Index (CII) Every country monitored by World Monitor receives a **real-time instability score from 0 to 100**, visualized as a choropleth heatmap that turns the globe into a risk map. The CII is computed from four weighted components: - **Baseline risk (40%):** Historical conflict data, governance quality, ethnic fractionalization - **Unrest indicators (20%):** Live protest counts, strike activity, civil disorder events - **Security events (20%):** Active armed conflicts, terrorism incidents, border clashes - **Information velocity (20%):** News volume spikes that often precede or accompany crises Real-time boosters adjust the score based on: - Proximity to active hotspots - OREF rocket alert activity - GPS jamming detection in or near the country - Government travel advisory changes from 4 nations (US, UK, Australia, New Zealand) The result: you can watch instability rise in real time, often before the situation makes international headlines. ## Hotspot Escalation Detection World Monitor's escalation algorithm goes beyond showing where events are happening. It identifies **where situations are getting worse** using a composite score: - **News activity (35%):** Sudden spikes in reporting volume for a geographic area - **CII score (25%):** Baseline instability context - **Geographic convergence (25%):** Multiple event types (conflict, protest, natural disaster, cyber) co-occurring within the same 1-degree grid cell within 24 hours - **Military indicators (15%):** Unusual force movements, exercise activity, weapons tests Geographic convergence is particularly powerful. When you see protests AND military deployments AND a communications outage in the same area within the same day, that pattern has predictive value that individual events don't. ## 210+ Military Bases Mapped The military infrastructure layer maps over **210 bases from 9 operators**, including: - US military installations worldwide - Russian bases and deployment zones - Chinese PLA facilities including South China Sea installations - NATO forward-deployed positions - Other allied and partner nation facilities Each base includes facility type, operating nation, and strategic context. Overlay this with the live ADS-B and AIS layers to see how forces relate to current deployments. ## Live ADS-B and AIS Fusion Two of World Monitor's most operationally significant layers: **ADS-B (Aircraft):** Military and civilian aircraft transponder data from OpenSky, enriched by Wingbits for aircraft type identification. Filter for military callsigns to track reconnaissance flights, tanker orbits, and transport movements in real time. **AIS (Maritime):** Ship positions from AISStream.io merged with editorial analysis from USNI Fleet Reports. This fusion gives you both the "where" (transponder position) and the "why" (fleet deployment context). Dark vessel detection flags ships that have gone silent, a common indicator of sanctions evasion or military operations. ## Infrastructure Cascade Analysis Conflicts don't just affect people. They affect infrastructure that the global economy depends on. World Monitor maps critical infrastructure alongside conflict data: - **Undersea cables** carrying 95% of intercontinental internet traffic - **Oil and gas pipelines** traversing conflict zones - **Nuclear facilities** and their proximity to active hostilities - **AI datacenters** (111 mapped globally) - **Strategic ports** (83) and airports (107) The Infrastructure Cascade panel shows what happens when a conflict zone overlaps with critical infrastructure. A pipeline through a hotspot, a cable landing station near an escalation zone. These second-order effects drive market moves and policy decisions. ## 26 Telegram Channels: The Raw Feed For analysts who want unfiltered intelligence, World Monitor integrates 26 curated Telegram channels via MTProto. Learn more about how this fits into the broader OSINT landscape in [OSINT for Everyone](/blog/posts/osint-for-everyone-open-source-intelligence-democratized/). The channels are tiered by reliability. Tier 1 sources are verified primary reporters. Tier 2 includes established OSINT accounts like Aurora Intel, BNO News, and DeepState. Tier 3 captures secondary aggregators for broader coverage. Telegram often breaks conflict news 15-30 minutes before traditional media. Having these feeds alongside verified data sources lets you distinguish signal from noise. ## AI Deduction and Forecasting World Monitor's AI capabilities aren't just summarization. The **AI Deduction panel** provides interactive geopolitical timeline forecasting grounded in live headlines: - Select a developing situation - The AI synthesizes current data into potential escalation/de-escalation paths - Each forecast is grounded in cited headlines and data points - Cross-reference with Polymarket prediction data for market sentiment This runs on your choice of LLM: local (Ollama, LM Studio), cloud (Groq, OpenRouter), or entirely in-browser (Transformers.js T5 model). For details on the prediction markets integration, see [Prediction Markets and AI Forecasting](/blog/posts/prediction-markets-ai-forecasting-geopolitics/). ## Real-World Use Cases **Conflict Monitoring for NGOs:** Humanitarian organizations use World Monitor to monitor safety conditions for field staff. The CII and escalation scoring provide early warning for deteriorating situations. **Defense Research:** Academic researchers studying conflict patterns use the integrated data layers to correlate military movements with political developments across multiple theaters simultaneously. **Journalism:** Reporters covering conflict use World Monitor to contextualize breaking events. When a missile strikes, the map immediately shows nearby military infrastructure, recent escalation history, and what OSINT channels are saying. **Policy Analysis:** Think tanks and government analysts use the Strategic Theater Posture assessments to brief decision-makers on multi-theater dynamics. ## 8 Regional Presets Jump between regions instantly with 8 preset views: Global, Americas, Europe, MENA, Asia, Africa, Oceania, and Latin America. Each preset adjusts the map view and highlights region-relevant layers. ## Shareable Intelligence Build a picture, then share it. World Monitor encodes your entire view state (map position, active layers, time range) into a URL. Send it to a colleague, and they see exactly what you see. For public sharing, the story export feature generates social-ready briefs with Open Graph images for Twitter/X, LinkedIn, WhatsApp, Telegram, and Reddit. ## Frequently Asked Questions **What data sources does World Monitor use for conflict tracking?** World Monitor aggregates ACLED conflict events, UCDP warfare data, live ADS-B aircraft transponders, AIS maritime positions merged with USNI fleet reports, 26 Telegram OSINT channels, and NASA satellite fire detection. All sources are public and verifiable. **Is World Monitor free to use for conflict monitoring?** Yes. World Monitor is completely free and open source under AGPL-3.0. There is no login, paywall, or data collection. You can also self-host it for full control. **How does the Country Instability Index (CII) work?** The CII scores each country from 0 to 100 using four weighted components: baseline risk (40%), unrest indicators (20%), security events (20%), and information velocity (20%). Real-time boosters adjust scores based on proximity to hotspots, rocket alerts, GPS jamming, and travel advisory changes. --- **Monitor developing situations at [worldmonitor.app](https://worldmonitor.app). Real-time geopolitical intelligence, free and open source.** ================================================ FILE: blog-site/src/content/blog/tracking-global-trade-routes-chokepoints-freight-costs.md ================================================ --- title: "Tracking Global Trade Routes, Chokepoints, and Freight Costs in Real Time" description: "Track 8 maritime chokepoints, freight indices (BDI, SCFI), trade policy, and critical mineral risks in real time. Free supply chain intelligence dashboard." metaTitle: "Real-Time Chokepoint & Freight Index Monitoring | World Monitor" keywords: "chokepoint monitoring, Strait of Hormuz shipping, freight index dashboard, BDI Baltic Dry Index, SCFI container rates, supply chain disruption tracker, trade route intelligence" audience: "Supply chain professionals, commodity traders, logistics analysts, maritime intelligence, geopolitical risk analysts" heroImage: "/blog/images/blog/hormuz-chokepoint-crisis.png" pubDate: "2026-03-15" --- > **Key Takeaways:** Strait of Hormuz traffic down 94.4%. World Monitor tracks 8 corridors, 9 freight indices, WTO trade policy, and critical mineral concentration across one free dashboard. Data updates in real time. The Strait of Hormuz carries 20% of the world's oil. Right now, [World Monitor's](https://worldmonitor.app) live chokepoint tracker shows traffic has dropped 94.4% week-over-week. Tanker transits have collapsed from 60+ daily to single digits. The disruption score is 99%. This is not a hypothetical scenario for a risk assessment deck. This is happening right now, and World Monitor is tracking it live. *Data as of March 15, 2026. Values update in real time on the dashboard.* ## The Hormuz Crisis in Real Time The Iran-Israel conflict has turned the Persian Gulf into an active confrontation zone. Iranian naval blockade risks, mines reported in shipping lanes, and 1,300+ security incidents in the past seven days have effectively shut down the world's most critical energy chokepoint. World Monitor's Supply Chain panel shows this in one view: - **85/100 disruption score** with red status - **94.4% week-over-week traffic decline** - **99% disruption rate** across the corridor - **Transit history chart** showing the cliff-edge collapse in late February - **AI-generated shipping advisory**: reroute via Suez Canal (adds 8-10 days, $150,000-$220,000 per transit), avoid Dubai anchorage, suspend Iran/Iraq crude exports until confrontations cease The chart tells the story: tanker and cargo traffic that had been steady at 40-70 vessels daily suddenly dropped to near zero. This is not a gradual decline. It is a sudden shutdown of one of the world's most important trade arteries. ## Eight Maritime Chokepoints Monitored in Real Time The Hormuz crisis is the most severe, but it is not the only corridor under pressure. World Monitor tracks eight critical maritime chokepoints, each scored by disruption level, vessel traffic, and [conflict intensity](/blog/posts/track-global-conflicts-in-real-time/): The following table shows the current status of all eight corridors as of mid-March 2026: | Corridor | Status | Key Risk | |----------|--------|----------| | **Strait of Hormuz** | Critical | Iran-Israel war, naval blockade, mines | | **Kerch Strait** | Red | Russia controls Kerch Bridge, Azov grain exports restricted | | **Bab el-Mandeb** | Yellow | Houthi attacks on commercial shipping | | **Suez Canal** | Yellow | Red Sea conflict spillover, Iran-Israel war adjacency | | **Bosporus Strait** | Elevated | Black Sea grain corridor tensions | | **Taiwan Strait** | Yellow | PLA military exercises, semiconductor supply risk | | **Cape of Good Hope** | Green | Rerouting destination for Hormuz/Suez diversions | | **Dover Strait** | Green | Europe's busiest shipping lane, currently stable | Each corridor shows live vessel counts, week-over-week traffic changes, disruption percentages, and risk levels. When you click a corridor, you get the full AI-generated situation assessment with specific shipping recommendations. ## What Makes This Different From Port Trackers Traditional maritime tracking tools show you where ships are. World Monitor shows you why they are not where they should be. The corridor disruption table cross-references AIS vessel data with conflict events, navigational warnings, and military activity. When vessel counts drop in the Strait of Hormuz, the system does not just show a number going down. It tells you there are 1,323 security incidents in the past week, Iranian naval confrontations in the shipping lanes, and mines reported in the Persian Gulf. The AI advisory goes further: it recommends specific alternative routes, estimates the cost increase per transit, identifies which cargo types should use air freight instead, and warns against specific anchorage points. ## Real-Time Freight Cost Tracking When chokepoints close, freight costs spike. World Monitor tracks nine freight indices that quantify the cost impact of disruptions: **Container Rates:** - **SCFI** (Shanghai Containerized Freight Index): composite container shipping costs from Shanghai, the world's busiest port. Currently at 1,710, up 14.9% as rerouting demand increases - **CCFI** (China Containerized Freight Index): broader Chinese container export costs. At 1,072, up 1.7% **Bulk Shipping:** - **BDI** (Baltic Dry Index): the benchmark for dry bulk shipping costs (iron ore, coal, grain). At 1,972, up 2.4% - **BCI** (Baltic Capesize Index): largest vessels, long-haul routes. At 2,721, up 5.7%, reflecting longer Cape of Good Hope diversions - **BPI** (Baltic Panamax Index): mid-size vessels, grain and coal. At 1,835 - **BSI** (Baltic Supramax Index): regional trade vessels. At 1,290 - **BHSI** (Baltic Handysize Index): smaller vessels, coastal trade. At 807 **Economic Indicators:** - **Deep Sea Freight Producer Price Index** (BLS): long-term freight cost trends with 24-month history - **Freight Transportation Services Index** (BTS): overall freight sector activity When you see the Hormuz disruption score at 99% and the Capesize Index up 5.7% in the same dashboard, the connection is immediate: ships that would have taken the short route through Hormuz are now going around Africa, and the cost of booking those larger vessels is climbing. For more on how these costs ripple into [commodity markets](/blog/posts/monitor-global-supply-chains-and-commodity-disruptions/), see our commodity monitoring guide. ## Trade Policy Intelligence Supply chain disruptions do not happen in isolation. They intersect with trade policy: tariffs, restrictions, and barriers that shape where goods can flow even when shipping lanes are open. World Monitor's Trade Policy panel tracks: - **Trade Restrictions**: WTO-reported measures by country, showing which economies are tightening import/export controls - **Tariff Trends**: applied tariff rates between major trading partners over time - **Trade Flows**: bilateral trade volumes between economies (e.g., US-China, US-EU), tracking shifts in trade patterns - **Trade Barriers**: SPS (Sanitary and Phytosanitary) and TBT (Technical Barriers to Trade) measures that create non-tariff obstacles - **US Customs Revenue**: Treasury collection data that reflects real trade volumes hitting US ports When the Strait of Hormuz closes, the trade policy data shows the second-order effects: which countries depend on Gulf oil imports, which alternative suppliers face their own trade restrictions, and whether tariff structures make rerouting economically viable. ## Critical Minerals: Concentration Risk Some supply chains cannot be rerouted because the supply itself is concentrated in a handful of countries. The Critical Minerals tab tracks this concentration risk using the HHI (Herfindahl-Hirschman Index), where anything above 2,500 indicates high concentration: | Mineral | Top Producer | Share | HHI Score | Risk | |---------|-------------|-------|-----------|------| | **Gallium** | China | 96% | 9,280 | Critical | | **Cobalt** | DRC | 80% | 6,633 | Critical | | **Germanium** | China | 77% | 6,085 | Critical | | **Rare Earths** | China | 71% | 5,327 | Critical | | **Lithium** | Australia | 50% | 3,529 | High | Gallium at 9,280 means the global supply is almost entirely dependent on a single country. When China announced gallium and germanium export controls in 2023, the semiconductor industry had no short-term alternative. World Monitor makes this concentration visible, so supply chain teams can assess exposure before restrictions are announced. ## How It All Connects Consider the current Hormuz crisis through all four dimensions: 1. **Chokepoints**: Hormuz at 99% disruption, vessels rerouting to Suez and Cape of Good Hope 2. **Freight Costs**: Capesize Index up 5.7% (longer routes need bigger ships), SCFI up 14.9% (container demand shifting) 3. **Trade Policy**: Gulf oil exports affected by the conflict, alternative suppliers face their own trade barriers 4. **Critical Minerals**: Qatar LNG exports transit Hormuz. Disruption affects downstream petrochemical inputs for battery manufacturing No single data source shows this full picture. World Monitor puts chokepoint status, freight indices, trade policy, and mineral supply risk in one panel, updated in real time. Combined with [AI-powered forecasting](/blog/posts/prediction-markets-ai-forecasting-geopolitics/), you can see not just what is happening, but where the situation is heading. ## The Data Sources Transparency matters. Here is where the data comes from: - **Vessel transit data**: AIS (Automatic Identification System) feeds, cross-referenced with historical baselines - **Conflict events**: ACLED (Armed Conflict Location & Event Data Project), 7-day rolling windows - **Shipping advisories**: AI-generated from combined conflict, navigational, and AIS disruption signals - **Container indices**: Shanghai Shipping Exchange (SSE) public JSON API - **Bulk indices**: Baltic Exchange via HandyBulk daily reports - **Economic indices**: FRED (Federal Reserve Economic Data) - **Trade policy**: WTO I-TIP (Integrated Trade Intelligence Portal) - **Critical minerals**: USGS mineral commodity data with HHI calculations All sources are public. No proprietary data feeds. No paywall. ## Frequently Asked Questions **What is the Baltic Dry Index (BDI)?** The BDI measures the cost of shipping dry bulk commodities (iron ore, coal, grain) on major ocean routes. It is widely used as a leading indicator of global trade activity because it reflects real demand for shipping capacity, not speculation. **How does the Strait of Hormuz affect oil prices?** Roughly 20% of the world's oil supply and 25% of global LNG passes through Hormuz. When traffic drops or the strait is threatened, energy markets price in supply disruption risk. The current 94.4% traffic decline is one of the most severe disruptions in the strait's history. **What are the world's most critical shipping chokepoints?** The eight most strategically important chokepoints are: Strait of Hormuz (oil/LNG), Strait of Malacca (Asia-Europe trade), Suez Canal (Mediterranean access), Bab el-Mandeb (Red Sea entry), Panama Canal (Atlantic-Pacific), Bosporus Strait (Black Sea grain), Taiwan Strait (semiconductors), and Dover Strait (North Sea). World Monitor tracks all of these except Malacca and Panama, which are currently low-risk. --- **Open the Supply Chain panel at [worldmonitor.app](https://worldmonitor.app) and click "Chokepoints" for live corridor disruption scores, or "Shipping Rates" to see real-time freight indices. Free for everyone.** ================================================ FILE: blog-site/src/content/blog/what-is-worldmonitor-real-time-global-intelligence.md ================================================ --- title: "What Is World Monitor? The Free Real-Time Global Intelligence Dashboard" description: "World Monitor is a free, open-source intelligence dashboard aggregating news, markets, conflicts, and infrastructure into one real-time view. No login required." metaTitle: "What Is World Monitor? Free Global Intelligence Dashboard" keywords: "global intelligence dashboard, real-time intelligence platform, OSINT dashboard, open source intelligence tool, geopolitical monitoring" audience: "General tech audience, OSINT researchers, analysts, journalists" heroImage: "/blog/images/blog/what-is-worldmonitor-real-time-global-intelligence.jpg" pubDate: "2026-02-10" --- Imagine opening 100 browser tabs every morning: one for Reuters, another for flight tracking, a third for earthquake monitors, a fourth for stock markets, a fifth for military ship positions. Now imagine replacing all of them with a single dashboard. That's World Monitor. ## A Bloomberg Terminal for the Rest of Us World Monitor is a **free, open-source, real-time global intelligence dashboard** that pulls together news, financial markets, military movements, natural disasters, cyber threats, and geopolitical risk scoring into one interactive map. It's the kind of tool that used to be locked behind six-figure enterprise contracts. Now it's available to anyone with a browser. No login. No paywall. No data collection. ## What You See When You Open World Monitor The first thing you notice is the globe. A 3D interactive map powered by globe.gl and Three.js, dotted with live data points: conflict zones pulsing red, military bases marked by operator, undersea cables tracing the ocean floor, and ADS-B aircraft positions updating in real time. On the left, a panel system lets you pull up any combination of 45+ data layers: - **Geopolitical:** Active conflicts, protests, hotspot escalation scores, strategic theater posture assessments across 9 operational theaters (Taiwan Strait, Persian Gulf, Baltic, and more) - **Military:** 210+ military bases, live flight tracking, naval vessel positions merged with USNI fleet reports, GPS jamming detection zones - **Infrastructure:** Nuclear facilities, AI datacenters (111 mapped), undersea cables, pipelines, strategic ports (83), and airports (107) - **Financial:** 92 stock exchanges, 13 central bank policy trackers, commodity prices, Fear & Greed Index, Bitcoin ETF flows, stablecoin peg monitoring - **Natural Disasters:** USGS earthquakes (M4.5+), NASA satellite fire detection, volcanic activity, flood alerts - **Cyber Threats:** Feodo Tracker botnet C2 servers, URLhaus malicious URLs, internet outage detection via Cloudflare Radar Every data point is sourced from public, verifiable feeds: 435+ RSS sources, government APIs, satellite data, and open maritime/aviation transponders. ## Five Dashboards, One Codebase World Monitor isn't one dashboard. It's five: | Dashboard | Focus | URL | |-----------|-------|-----| | **World Monitor** | Geopolitics, conflicts, military, infrastructure | worldmonitor.app | | **Tech Monitor** | AI labs, startups, cybersecurity, cloud infrastructure | tech.worldmonitor.app | | **Finance Monitor** | Markets, central banks, forex, Gulf FDI | finance.worldmonitor.app | | **Commodity Monitor** | Mining, metals, energy, supply chain disruption | commodity.worldmonitor.app | | **Happy Monitor** | Good news, breakthroughs, conservation, renewable energy | happy.worldmonitor.app | Switch between them with a single click. Each variant curates panels and layers for its specific audience while sharing the same underlying intelligence engine. Read more about each variant in [Five Dashboards, One Platform](/blog/posts/five-dashboards-one-platform-worldmonitor-variants/). ## AI That Runs on Your Machine Here's where World Monitor gets interesting for privacy-conscious users. The platform includes a **4-tier AI fallback chain**: 1. **Local LLMs** (Ollama or LM Studio) for fully offline, private analysis 2. **Groq** (Llama 3.1 8B) for fast cloud inference 3. **OpenRouter** as a fallback provider 4. **Browser-based T5** (Transformers.js) that runs entirely in your browser via Web Workers This means you can generate intelligence briefs, classify threats, and run sentiment analysis without sending a single byte to external servers. The desktop app (built with Tauri for macOS, Windows, and Linux) takes this further with OS keychain integration and a local Node.js sidecar for complete offline operation. ## The Country Intelligence Dossier Click any country on the map and you get a full intelligence dossier: - **Country Instability Index (CII):** A real-time 0-100 score calculated from baseline risk (40%), unrest indicators (20%), security events (20%), and information velocity (20%) - **AI-generated analysis** with inline citations from current headlines - **Active signals:** Protests, conflicts, natural disasters, and cyber incidents - **7-day timeline:** What happened this week - **Prediction markets:** What Polymarket bettors think happens next - **Infrastructure exposure:** Pipelines, cables, and datacenters within 600km ## Who Uses World Monitor? The dashboard serves a surprisingly wide audience: - **OSINT researchers** who need a unified view instead of 100 tabs - **Financial analysts** tracking macro signals across 92 exchanges - **Journalists** who need instant context for breaking stories - **Supply chain managers** monitoring disruption risk at ports and commodity hubs - **Policy researchers** studying government spending and trade policy - **Developers** who want to build on top of open, typed APIs (92 proto files, 22 services). See the [Developer API and Open Source guide](/blog/posts/build-on-worldmonitor-developer-api-open-source/) for details. ## Available Everywhere World Monitor works as: - A **web app** at worldmonitor.app (no install needed) - A **Progressive Web App** you can install on any device with offline map caching - A **native desktop app** via Tauri for macOS, Windows, and Linux - Fully **mobile-optimized** with touch gestures, pinch-to-zoom, and bottom-sheet panels It supports **21 languages** including Arabic (with full RTL layout), Japanese, Chinese, and all major European languages. RSS feeds are localized per language, and AI analysis can be generated in your preferred language. See the full language breakdown in [World Monitor in 21 Languages](/blog/posts/worldmonitor-in-21-languages-global-intelligence-for-everyone/). ## Open Source, No Strings World Monitor is released under AGPL-3.0. The entire codebase, every data source, every algorithm, is open for inspection, contribution, and self-hosting. There's no "enterprise tier" waiting behind the free version. This is the product. The tech stack is modern and approachable: React + TypeScript + Vite on the frontend, Vercel Edge Functions for the API layer, and Tauri for the desktop app. ## Frequently Asked Questions **Do I need to create an account to use World Monitor?** No. World Monitor requires no login, no signup, and collects no personal data. Open worldmonitor.app in any browser and start using it immediately. **Can I run World Monitor completely offline?** Yes. The Tauri desktop app (macOS, Windows, Linux) includes a local Node.js sidecar and supports local LLMs via Ollama or LM Studio. You can also install the PWA for offline map caching. **How does World Monitor compare to paid intelligence tools?** World Monitor covers geopolitics, markets, military tracking, and infrastructure in a single free dashboard. Paid tools like Bloomberg or Palantir offer deeper coverage in specific domains but cost thousands to millions per year. See the [full comparison](/blog/posts/worldmonitor-vs-traditional-intelligence-tools/). --- **Try World Monitor now at [worldmonitor.app](https://worldmonitor.app). No signup required.** ================================================ FILE: blog-site/src/content/blog/worldmonitor-in-21-languages-global-intelligence-for-everyone.md ================================================ --- title: "Intelligence Without Borders: World Monitor in 21 Languages" description: "World Monitor supports 21 languages with full RTL Arabic, CJK, and locale-specific news feeds. AI analysis and search in your preferred language, free." metaTitle: "World Monitor in 21 Languages | Multilingual OSINT" keywords: "multilingual intelligence dashboard, Arabic OSINT tool, Japanese intelligence platform, global dashboard localized, RTL intelligence dashboard" audience: "Non-English-speaking analysts, international organizations, global enterprises, multilingual researchers" heroImage: "/blog/images/blog/worldmonitor-in-21-languages-global-intelligence-for-everyone.jpg" pubDate: "2026-03-04" --- The world doesn't operate in English. Crises unfold in Arabic. Markets move in Mandarin. Diplomatic cables are written in French. Military communications happen in Russian. Yet most intelligence platforms are English-only, forcing analysts to work in a second language during high-pressure situations. World Monitor speaks **21 languages** natively, with full interface localization, language-specific news feeds, AI analysis in your preferred language, and search that works in any supported script. ## Full Interface Localization Every element of World Monitor's interface is translated: - Panel titles and descriptions - Layer names and toggle labels - Button text, tooltips, and status messages - Error messages and notifications - Command palette commands - Country names in native language forms This isn't machine translation bolted on as an afterthought. The localization system uses **lazy-loaded language bundles**, meaning only your active language is downloaded. The initial page load is fast regardless of which language you choose, and switching languages loads the new bundle on demand. ## Supported Languages | Language | Script | Direction | Region Coverage | |----------|--------|-----------|-----------------| | English | Latin | LTR | Global | | French | Latin | LTR | France, Africa, Middle East | | German | Latin | LTR | Central Europe | | Spanish | Latin | LTR | Americas, Spain | | Italian | Latin | LTR | Mediterranean | | Portuguese | Latin | LTR | Brazil, Portugal, Africa | | Dutch | Latin | LTR | Netherlands, Belgium | | Swedish | Latin | LTR | Scandinavia | | Polish | Latin | LTR | Eastern Europe | | Czech | Latin | LTR | Central Europe | | Romanian | Latin | LTR | Southeast Europe | | Bulgarian | Cyrillic | LTR | Balkans | | Greek | Greek | LTR | Eastern Mediterranean | | Russian | Cyrillic | LTR | Russia, Central Asia | | Turkish | Latin | LTR | Turkey, Central Asia | | **Arabic** | **Arabic** | **RTL** | **MENA, Gulf** | | Chinese (Simplified) | CJK | LTR | China, Singapore | | Japanese | CJK | LTR | Japan | | Korean | Hangul | LTR | Korea | | Thai | Thai | LTR | Southeast Asia | | Vietnamese | Latin (diacritics) | LTR | Southeast Asia | ## Arabic and RTL: First-Class Support Arabic support isn't just text translation. It requires **Right-to-Left (RTL) layout transformation**: - The entire interface mirrors: sidebars, panels, navigation, buttons - Text alignment switches from left to right - Numerical displays respect locale formatting - Map controls adapt to RTL interaction patterns - The command palette accepts Arabic search queries For analysts in the Middle East and North Africa, this means World Monitor feels native, not like an English tool with Arabic text forced into a left-to-right layout. ## CJK Language Support Chinese, Japanese, and Korean present unique challenges for intelligence platforms: - **Character width:** CJK characters are double-width, requiring layout adjustments - **Input methods:** Search must work with IME (Input Method Editor) composition - **Line breaking:** CJK text doesn't use spaces between words, requiring different text wrapping - **Country names:** Each CJK language has different names for countries (日本 vs 일본 vs 日本) World Monitor handles all of these. The command palette accepts CJK input during IME composition, country search works with local names, and text displays correctly at any zoom level. ## Language-Specific News Feeds This is where multilingual support goes beyond interface translation. World Monitor's **435+ RSS feeds** include **locale-specific sources**: When you switch World Monitor to French, you don't just see English headlines translated. You see French-language sources: Le Monde, France 24, AFP. Switch to Arabic and you see Al Jazeera Arabic, Al Arabiya, local MENA outlets. Switch to Japanese and Japanese news sources appear. This matters because: - **Local sources cover local events first**, often hours before English wire services - **Nuance is lost in translation.** Reading a source in its original language captures tone, emphasis, and cultural context that translation strips away - **Regional perspectives differ.** A French source and a British source cover the same African event with different framing ## AI Analysis in Your Language World Monitor's AI capabilities generate output in your selected language: - **World Brief:** The AI-synthesized daily intelligence summary is generated in your language - **Country Dossiers:** AI analysis adapts to the selected locale - **Threat Classification:** Categorization labels appear in your language - **AI Deduction:** Geopolitical forecasting is generated in the interface language When using local LLMs (Ollama, LM Studio), multilingual output depends on the model's training data. Larger models like Llama 3.1 70B handle most major languages well. The browser-based T5 fallback performs best in English but provides basic multilingual capability. For more on how World Monitor keeps your data private with local AI, see [AI-Powered Intelligence Without the Cloud](/blog/posts/ai-powered-intelligence-without-the-cloud/). ## Multilingual Command Palette The Cmd+K command palette indexes keywords in all 21 languages: - Search for "Allemagne" → Germany (French) - Search for "Japón" → Japan (Spanish) - Search for "ロシア" → Russia (Japanese) - Search for "مصر" → Egypt (Arabic) - Search for "중국" → China (Korean) All 195 countries have searchable names in every supported language. Layer names, panel names, and command keywords are also localized in the search index. Learn more about this feature in [Command Palette: Search Everything Instantly](/blog/posts/command-palette-search-everything-instantly/). ## Auto-Detection World Monitor automatically detects your browser's language preference on first visit. If your browser is set to German, World Monitor opens in German. If your system uses Arabic, you get the full RTL Arabic experience immediately. You can manually switch languages at any time. The preference is saved to localStorage and persists across sessions. ## Use Cases for Multilingual Intelligence ### International Organizations (UN, NATO, EU) Staff from dozens of countries need a common intelligence picture in their working language. World Monitor's 21 languages cover the official languages of the UN (English, French, Spanish, Arabic, Chinese, Russian) and most NATO member languages. ### Multinational Corporations Security teams monitoring global operations need intelligence in the languages of their regional offices. A VP in Dubai sees the dashboard in Arabic. A manager in Tokyo sees it in Japanese. A director in Paris sees it in French. Same data, local language. ### Regional Analysts An analyst focusing on MENA works most effectively in Arabic, reading Arabic sources, with Arabic interface labels. Switching to World Monitor's English version for a cross-regional briefing takes one click. ### Academic Research Researchers studying geopolitics in non-English contexts benefit from seeing data presented in the language of the region they study. Terminology consistency with local academic literature improves when the tool speaks the researcher's language. ### Journalism Correspondents based in foreign bureaus can use World Monitor in the local language, making it easier to cross-reference dashboard intelligence with local source material. See how journalists use World Monitor for [tracking global conflicts](/blog/posts/track-global-conflicts-in-real-time/). ## Technical Implementation For the technically curious: - **i18next** framework with lazy-loaded JSON bundles per locale - **Browser language detection** via i18next LanguageDetector - **Fallback chain:** Requested locale → English for missing keys - **RTL detection:** Automatic `dir="rtl"` attribute application for Arabic - **No full-page reload:** Language switching is instant, handled by React re-renders - **Bundle sizes:** Each language pack is typically 15-30KB (gzipped), loaded only on demand ## Contributing Translations World Monitor is open source. Translation contributions for new languages or improvements to existing translations are welcome through the GitHub repository. The JSON-based translation format makes it straightforward for bilingual contributors to add or refine translations without writing code. ## Frequently Asked Questions **Does switching languages change the news sources I see?** Yes. World Monitor includes locale-specific RSS feeds. Switching to French surfaces sources like Le Monde and France 24, while Arabic shows Al Jazeera Arabic and regional MENA outlets. You get native-language reporting, not just translated English headlines. **How does Arabic RTL support work?** The entire interface mirrors when Arabic is selected: sidebars, panels, navigation, and text alignment all switch to right-to-left. Map controls adapt to RTL interaction patterns, so the experience feels native rather than a forced translation. **Can I contribute translations for a new language?** Yes. World Monitor is open source and uses JSON-based translation files. Bilingual contributors can add or refine translations through the GitHub repository without writing code. --- **Use World Monitor in your language at [worldmonitor.app](https://worldmonitor.app). 21 languages, full RTL support, locale-specific feeds. Free for everyone, everywhere.** ================================================ FILE: blog-site/src/content/blog/worldmonitor-vs-traditional-intelligence-tools.md ================================================ --- title: "World Monitor vs. Traditional Intelligence Tools: Why Free and Open Source Wins" description: "Compare World Monitor to Bloomberg, Palantir, Dataminr, and Recorded Future. Free, open-source multi-domain intelligence vs. six-figure enterprise platforms." metaTitle: "World Monitor vs Bloomberg, Palantir, Dataminr" keywords: "Bloomberg Terminal alternative free, Palantir alternative open source, Dataminr alternative, intelligence platform comparison, free OSINT alternative" audience: "Analysts evaluating tools, budget-conscious teams, procurement decision-makers, open-source advocates" heroImage: "/blog/images/blog/worldmonitor-vs-traditional-intelligence-tools.jpg" pubDate: "2026-03-11" --- A Bloomberg Terminal costs $24,000 per year. A Palantir deployment starts in the millions. Dataminr licenses run six figures for enterprise teams. Recorded Future isn't cheap either. These tools are powerful. They're also gatekept behind budgets that exclude most of the world's analysts, researchers, journalists, and security professionals. World Monitor asks a different question: what if the intelligence dashboard was free? ## The Comparison Let's be direct about what World Monitor is and isn't relative to established platforms. ### World Monitor vs. Bloomberg Terminal **Bloomberg wins at:** - Depth of financial data (tick-level, decades of history) - Trading execution and order management - Fixed income and derivatives pricing - Proprietary analyst research - Terminal-to-terminal messaging **World Monitor wins at:** - Geopolitical intelligence integration with market data - Conflict and military monitoring (Bloomberg has zero) - Visual map-based interface with 45 data layers - AI analysis that runs locally (Bloomberg's AI is cloud-only) - Price: free vs. $24,000/year - Open source transparency **Best for:** Traders who need geopolitical context for macro positioning, not tick-level execution. ### World Monitor vs. Palantir Gotham/Foundry **Palantir wins at:** - Ingesting proprietary organizational data - Custom ontology building - Classified network deployment - Workflow automation at enterprise scale - Dedicated engineering support **World Monitor wins at:** - Zero deployment time (open a browser) - No vendor lock-in (AGPL-3.0 source code) - Public OSINT aggregation out of the box - Self-service without enterprise contracts - Community-driven development - Price: free vs. multi-million dollar contracts **Best for:** Analysts who need public OSINT aggregation today, not a 6-month enterprise deployment. ### World Monitor vs. Dataminr **Dataminr wins at:** - Proprietary social media firehose access (Twitter/X partnership) - Purpose-built alerting and notification workflows - Dedicated analyst support - Enterprise SLA and compliance certifications **World Monitor wins at:** - Broader intelligence scope (Dataminr focuses on social; World Monitor covers military, maritime, aviation, markets, infrastructure) - 26 Telegram OSINT channels (Dataminr has limited Telegram coverage) - AI analysis with local LLM option - Interactive map visualization - No vendor dependency - Price: free vs. six-figure annual licenses **Best for:** Analysts who need multi-domain intelligence, not just social media monitoring. ### World Monitor vs. Recorded Future **Recorded Future wins at:** - Deep dark web and threat intelligence collection - Malware analysis and IOC correlation - Vulnerability intelligence - Dedicated threat analyst team - Enterprise integration ecosystem (SIEM, SOAR) **World Monitor wins at:** - Geopolitical and military intelligence (Recorded Future focuses on cyber) - Financial market integration - Interactive visual map interface - Local AI processing - Real-time conflict and disaster monitoring - Price: free vs. enterprise licensing **Best for:** Analysts who need geopolitical intelligence alongside cyber threat data. ## The Real Advantage: Multi-Domain Fusion The fundamental difference isn't any single feature. It's that World Monitor fuses domains that traditional tools keep separate: | Domain | Bloomberg | Palantir | Dataminr | Recorded Future | World Monitor | |--------|-----------|----------|----------|-----------------|--------------| | Financial markets | Deep | Limited | No | No | Moderate | | Geopolitical events | Limited | Custom | Social only | Cyber focus | Deep | | Military tracking | No | Custom | No | No | ADS-B + AIS + USNI | | Conflict data | No | Custom | Social | Cyber | ACLED + UCDP + Telegram | | Infrastructure mapping | No | Custom | No | Partial | Cables, pipelines, ports, datacenters | | Natural disasters | No | Custom | Limited | No | USGS + NASA FIRMS + EONET | | AI analysis (local) | No | No | No | No | Ollama + LM Studio + browser ML | | Prediction markets | No | No | No | No | Polymarket integration | | Price | $24K/yr | $1M+ | $100K+ | Enterprise | Free | | Open source | No | No | No | No | AGPL-3.0 | No single traditional tool covers all these domains. Analysts typically cobble together 5-6 subscriptions. World Monitor provides integrated coverage across all of them. For a deeper dive into the market intelligence capabilities, see [Real-Time Market Intelligence for Traders](/blog/posts/real-time-market-intelligence-for-traders-and-analysts/). ## What World Monitor Doesn't Do Transparency matters. Here's what you won't get: - **Proprietary data:** World Monitor uses public sources. If data requires private agreements (Twitter firehose, dark web crawlers, classified networks), it's not here. - **Enterprise features:** No SSO, RBAC, audit trails, or compliance certifications. It's a dashboard, not a platform. - **Historical depth:** Financial data doesn't go back decades. Most data reflects the recent past and present. - **Trading execution:** You can't place orders. It's intelligence, not a brokerage. - **SLA guarantees:** It's open source. The community and contributors maintain it, not a support team. - **Custom data ingestion:** You can't connect your proprietary databases. It works with its curated public sources. ## When World Monitor Is the Right Choice **You should use World Monitor if:** - You need a multi-domain intelligence overview and your budget is limited - You want geopolitical context alongside market data - You need AI analysis that runs privately on your hardware - You want to understand what tools like Bloomberg don't show: military movements, conflict escalation, infrastructure exposure - You're a developer who wants typed APIs and open source to build on - You want to evaluate intelligence tooling before committing to enterprise contracts **You should look elsewhere if:** - You need tick-level financial data for high-frequency trading - You need dark web threat intelligence - You need enterprise compliance (SOC2, FedRAMP) - You need to ingest proprietary organizational data - You need guaranteed SLAs and dedicated support ## The Open Source Moat Traditional intelligence vendors protect their value with proprietary data and closed algorithms. World Monitor inverts this: the value is in the **integration**, not the lock-in. Every scoring algorithm is auditable. Every data source is documented. Every API contract is typed in Protocol Buffers. This means: - **Security teams** can verify there are no backdoors or data exfiltration - **Researchers** can reproduce and cite the scoring methodologies - **Developers** can build custom integrations using the 22 typed API services - **Organizations** can self-host for complete control. See the [Developer API and Open Source guide](/blog/posts/build-on-worldmonitor-developer-api-open-source/) for integration details. The AGPL-3.0 license ensures that improvements to the core platform benefit everyone. Forks must also be open source. The commons stays common. ## 21 Languages, Global Access Intelligence shouldn't be English-only. World Monitor supports **21 languages** with: - Fully localized interface including RTL for Arabic - Language-specific RSS feeds - AI analysis in your preferred language - Native character support for CJK languages This means analysts worldwide can use the tool in their working language, not just as a translation layer over English sources. Read the full breakdown in [World Monitor in 21 Languages](/blog/posts/worldmonitor-in-21-languages-global-intelligence-for-everyone/). ## Frequently Asked Questions **Can World Monitor replace a Bloomberg Terminal?** For geopolitical intelligence, conflict monitoring, and macro context, yes. For tick-level financial data, derivatives pricing, and trade execution, no. World Monitor complements Bloomberg by covering domains Bloomberg does not touch, such as military tracking, conflict escalation, and infrastructure mapping. **Is World Monitor secure enough for professional use?** The entire codebase is open source under AGPL-3.0, so security teams can audit every line. AI analysis can run fully offline via local LLMs. No data is collected, and no login is required. **What does "multi-domain fusion" mean in practice?** It means seeing how a conflict zone overlaps with an undersea cable, how a naval repositioning affects shipping routes, or how a protest spike correlates with a currency move. Traditional tools silo these domains; World Monitor layers them on a single map. --- **Compare for yourself at [worldmonitor.app](https://worldmonitor.app). Free, open source, and ready in seconds.** ================================================ FILE: blog-site/src/content.config.ts ================================================ import { defineCollection, z } from 'astro:content'; import { glob } from 'astro/loaders'; const blog = defineCollection({ loader: glob({ pattern: '**/*.md', base: './src/content/blog' }), schema: z.object({ title: z.string(), description: z.string(), metaTitle: z.string(), keywords: z.string(), audience: z.string(), pubDate: z.coerce.date(), heroImage: z.string().optional(), }), }); export const collections = { blog }; ================================================ FILE: blog-site/src/layouts/Base.astro ================================================ --- interface Props { title: string; description: string; metaTitle?: string; keywords?: string; ogType?: string; ogImage?: string; publishedTime?: string; modifiedTime?: string; author?: string; section?: string; jsonLd?: Record; breadcrumbLd?: Record; } const { title, description, metaTitle, keywords, ogType, ogImage, publishedTime, modifiedTime, author, section, jsonLd, breadcrumbLd } = Astro.props; const pageTitle = metaTitle || title; const resolvedOgImage = ogImage || 'https://www.worldmonitor.app/favico/og-image.png'; const resolvedOgType = ogType || 'website'; const keywordTags = keywords ? keywords.split(',').map(k => k.trim()).slice(0, 6) : []; --- {pageTitle} {keywords && } {publishedTime && } {modifiedTime && } {resolvedOgType === 'article' && } {section && } {keywordTags.map(tag => )} {jsonLd && ( ') ?? ''; }); expect(html).toContain('<script>alert(1)</script>'); expect(html).not.toContain(' `; } async function installProWidgetAgentMocks( page: Parameters[0]['page'], responses: MockWidgetResponse[], requestBodies: unknown[] = [], proKeyConfigured = true, ): Promise { await page.route('**/widget-agent/health', async (route) => { expect(route.request().headers()['x-widget-key']).toBe(widgetKey); await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ ok: true, agentEnabled: true, widgetKeyConfigured: true, anthropicConfigured: true, proKeyConfigured, }), }); }); let responseIndex = 0; await page.route('**/widget-agent', async (route) => { const body = route.request().postDataJSON(); requestBodies.push(body); const response = responses[responseIndex]; if (!response) { await route.fulfill({ status: 500, contentType: 'application/json', body: '{"error":"Unexpected call"}' }); return; } responseIndex += 1; await route.fulfill({ status: 200, contentType: 'text/event-stream', headers: { 'cache-control': 'no-cache', connection: 'keep-alive' }, body: buildWidgetSseResponse(response), }); }); } test.describe('AI widget builder', () => { test.beforeEach(async ({ page }) => { await page.addInitScript((key) => { if (!sessionStorage.getItem('__widget_e2e_init__')) { localStorage.clear(); sessionStorage.clear(); localStorage.setItem('worldmonitor-variant', 'happy'); localStorage.setItem('wm-widget-key', key); sessionStorage.setItem('__widget_e2e_init__', '1'); return; } if (!localStorage.getItem('wm-widget-key')) { localStorage.setItem('wm-widget-key', key); } }, widgetKey); }); test('creates a widget through the live modal flow and persists it after reload', async ({ page }) => { const createHtml = buildTallWidgetHtml('Oil vs Gold', 'oil-gold-widget'); await installWidgetAgentMocks( page, [ { delayMs: 250, endpoint: '/rpc/worldmonitor.markets.v1.MarketsService/GetCommodities', title: 'Oil vs Gold', html: createHtml, }, ], [], 500, ); await page.goto('/'); await expect(page.locator('#panelsGrid .ai-widget-block')).toBeVisible({ timeout: 30000 }); await page.locator('#panelsGrid .ai-widget-block').click(); const modal = page.locator('.widget-chat-modal'); const sendButton = modal.locator('.widget-chat-send'); const input = modal.locator('.widget-chat-input'); const preview = modal.locator('.widget-chat-preview'); const footer = modal.locator('.widget-chat-footer'); const footerAction = footer.locator('.widget-chat-action-btn'); await expect(modal).toBeVisible(); await expect(modal.locator('.widget-chat-layout')).toBeVisible(); await expect(modal.locator('.widget-chat-sidebar')).toBeVisible(); await expect(modal.locator('.widget-chat-main')).toBeVisible(); await expect(modal.locator('.widget-chat-example-chip')).toHaveCount(4); await modal.locator('.widget-chat-example-chip').first().click(); await expect(input).toHaveValue(createPrompt); await expect(modal.locator('.widget-chat-readiness')).toContainText('Connected to the widget agent'); await expect(preview).toContainText('Describe the widget you want'); await expect(sendButton).toBeEnabled(); const sidebarBox = await modal.locator('.widget-chat-sidebar').boundingBox(); const mainBox = await modal.locator('.widget-chat-main').boundingBox(); expect(sidebarBox?.width ?? 0).toBeGreaterThan(280); expect(mainBox?.width ?? 0).toBeGreaterThan(320); await sendButton.click(); await expect(preview.locator('.widget-chat-preview-frame')).toBeVisible({ timeout: 30000 }); await expect(preview).toContainText('Oil vs Gold'); await expect(preview.locator('.wm-widget-shell')).toBeVisible(); await expect(preview.locator('.wm-widget-generated')).toBeVisible(); await expect(footerAction).toBeEnabled(); const footerBefore = await footer.boundingBox(); await preview.evaluate((element) => { element.scrollTop = element.scrollHeight; }); const footerAfter = await footer.boundingBox(); expect(Math.abs((footerAfter?.y ?? 0) - (footerBefore?.y ?? 0))).toBeLessThan(2); await expect(footerAction).toBeVisible(); await footerAction.click(); const widgetPanel = page.locator('.custom-widget-panel', { has: page.locator('.panel-title', { hasText: 'Oil vs Gold' }), }); await expect(widgetPanel).toBeVisible({ timeout: 20000 }); await expect(widgetPanel.locator('.wm-widget-shell')).toBeVisible(); await expect(widgetPanel.locator('.wm-widget-generated')).toBeVisible(); const containment = await widgetPanel.locator('.wm-widget-generated').evaluate((element) => { const style = getComputedStyle(element); return { contain: style.contain, overflowX: style.overflowX, overflowY: style.overflowY, }; }); expect(containment.contain).toContain('layout'); expect(containment.contain).toContain('paint'); expect(['clip', 'hidden']).toContain(containment.overflowX); expect(['clip', 'hidden']).toContain(containment.overflowY); const bannerPosition = await widgetPanel.evaluate((panel) => { const panelRect = panel.getBoundingClientRect(); const banner = panel.querySelector('[data-escape-banner="true"]') as HTMLElement | null; const bannerRect = banner?.getBoundingClientRect() ?? null; return { panelRect, bannerRect }; }); expect(bannerPosition.bannerRect).not.toBeNull(); expect(bannerPosition.bannerRect!.top).toBeGreaterThanOrEqual(bannerPosition.panelRect.top - 1); expect(bannerPosition.bannerRect!.left).toBeGreaterThanOrEqual(bannerPosition.panelRect.left - 1); await page.reload(); await expect(page.locator('.custom-widget-panel', { has: page.locator('.panel-title', { hasText: 'Oil vs Gold' }), })).toBeVisible({ timeout: 20000 }); const storedWidgets = await page.evaluate(() => { return JSON.parse(localStorage.getItem('wm-custom-widgets') || '[]') as Array<{ title: string }>; }); expect(storedWidgets.some((entry) => entry.title === 'Oil vs Gold')).toBe(true); }); test('supports modify, keeps session history, exposes touch-sized controls, and cleans storage on delete', async ({ page }) => { const requestBodies: unknown[] = []; await installWidgetAgentMocks(page, [ { endpoint: '/rpc/worldmonitor.markets.v1.MarketsService/GetCommodities', title: 'Oil vs Gold', html: buildTallWidgetHtml('Oil vs Gold', 'oil-gold-widget'), }, { endpoint: '/rpc/worldmonitor.aviation.v1.AviationService/GetAirportDelays', title: 'Flight Delay Watch', html: buildTallWidgetHtml('Flight Delay Watch', 'flight-delay-widget'), }, ], requestBodies); await page.goto('/'); await expect(page.locator('#panelsGrid .ai-widget-block')).toBeVisible({ timeout: 30000 }); await page.locator('#panelsGrid .ai-widget-block').click(); const modal = page.locator('.widget-chat-modal'); await expect(modal.locator('.widget-chat-readiness')).toContainText('Connected to the widget agent'); await modal.locator('.widget-chat-input').fill(createPrompt); await modal.locator('.widget-chat-send').click(); await expect(modal.locator('.widget-chat-action-btn')).toBeEnabled({ timeout: 30000 }); await modal.locator('.widget-chat-action-btn').click(); const widgetPanel = page.locator('.custom-widget-panel', { has: page.locator('.panel-title', { hasText: 'Oil vs Gold' }), }); await expect(widgetPanel).toBeVisible({ timeout: 20000 }); const modifyButton = widgetPanel.locator('.panel-widget-chat-btn'); const colorButton = widgetPanel.locator('.widget-color-btn'); await expect(modifyButton).toBeVisible(); await expect(colorButton).toBeVisible(); const controlSizes = await widgetPanel.evaluate((panel) => { const modify = panel.querySelector('.panel-widget-chat-btn') as HTMLElement | null; const color = panel.querySelector('.widget-color-btn') as HTMLElement | null; const modifyRect = modify?.getBoundingClientRect(); const colorRect = color?.getBoundingClientRect(); return { modifyWidth: modifyRect?.width ?? 0, modifyHeight: modifyRect?.height ?? 0, colorWidth: colorRect?.width ?? 0, colorHeight: colorRect?.height ?? 0, }; }); expect(controlSizes.modifyWidth).toBeGreaterThanOrEqual(32); expect(controlSizes.modifyHeight).toBeGreaterThanOrEqual(32); expect(controlSizes.colorWidth).toBeGreaterThanOrEqual(32); expect(controlSizes.colorHeight).toBeGreaterThanOrEqual(32); const initialAccent = await colorButton.evaluate((button) => getComputedStyle(button).backgroundColor); await colorButton.click(); const updatedAccent = await colorButton.evaluate((button) => getComputedStyle(button).backgroundColor); expect(updatedAccent).not.toBe(initialAccent); await modifyButton.click(); const modifyModal = page.locator('.widget-chat-modal'); await expect(modifyModal).toBeVisible(); await expect(modifyModal.locator('.widget-chat-messages')).toContainText(createPrompt); await expect(modifyModal.locator('.widget-chat-messages')).toContainText('Generated widget: Oil vs Gold'); await expect(modifyModal.locator('.widget-chat-preview')).toContainText('Oil vs Gold'); await modifyModal.locator('.widget-chat-input').fill(modifyPrompt); await modifyModal.locator('.widget-chat-send').click(); await expect(modifyModal.locator('.widget-chat-action-btn')).toBeEnabled({ timeout: 30000 }); await expect(modifyModal.locator('.widget-chat-preview')).toContainText('Flight Delay Watch'); await modifyModal.locator('.widget-chat-action-btn').click(); const updatedPanel = page.locator('.custom-widget-panel', { has: page.locator('.panel-title', { hasText: 'Flight Delay Watch' }), }); await expect(updatedPanel).toBeVisible({ timeout: 20000 }); const storedWidgetMeta = await page.evaluate(() => { const widgets = JSON.parse(localStorage.getItem('wm-custom-widgets') || '[]') as Array<{ id: string; title: string; }>; return widgets.find((entry) => entry.title === 'Flight Delay Watch') ?? null; }); expect(storedWidgetMeta).not.toBeNull(); const secondRequest = requestBodies[1] as { conversationHistory?: Array<{ role: string; content: string }>; currentHtml?: string | null; } | undefined; expect(secondRequest?.currentHtml).toContain('oil-gold-widget'); expect(secondRequest?.conversationHistory?.some((entry) => entry.content.includes(createPrompt))).toBe(true); expect(secondRequest?.conversationHistory?.some((entry) => entry.content.includes('Generated widget: Oil vs Gold'))).toBe(true); await page.evaluate((widgetId: string) => { localStorage.setItem('worldmonitor-panel-spans', JSON.stringify({ [widgetId]: 2 })); localStorage.setItem('worldmonitor-panel-col-spans', JSON.stringify({ [widgetId]: 3 })); }, storedWidgetMeta!.id); await page.evaluate(() => { window.confirm = () => true; }); await updatedPanel.locator('.panel-close-btn').evaluate((button: HTMLButtonElement) => { button.click(); }); await expect(updatedPanel).toHaveCount(0); const cleanedStorage = await page.evaluate(() => { return { widgets: localStorage.getItem('wm-custom-widgets'), rowSpans: localStorage.getItem('worldmonitor-panel-spans'), colSpans: localStorage.getItem('worldmonitor-panel-col-spans'), }; }); expect(cleanedStorage.widgets).toBe('[]'); expect(cleanedStorage.rowSpans).toBeNull(); expect(cleanedStorage.colSpans).toBeNull(); await page.reload(); await expect(page.locator('.custom-widget-panel')).toHaveCount(0); }); }); // --------------------------------------------------------------------------- // PRO tier widget tests // --------------------------------------------------------------------------- test.describe('AI widget builder — PRO tier', () => { test.beforeEach(async ({ page }) => { await page.addInitScript( ({ wKey, pKey }: { wKey: string; pKey: string }) => { if (!sessionStorage.getItem('__widget_pro_e2e_init__')) { localStorage.clear(); sessionStorage.clear(); localStorage.setItem('worldmonitor-variant', 'happy'); localStorage.setItem('wm-widget-key', wKey); localStorage.setItem('wm-pro-key', pKey); sessionStorage.setItem('__widget_pro_e2e_init__', '1'); return; } if (!localStorage.getItem('wm-widget-key')) localStorage.setItem('wm-widget-key', wKey); if (!localStorage.getItem('wm-pro-key')) localStorage.setItem('wm-pro-key', pKey); }, { wKey: widgetKey, pKey: proWidgetKey }, ); }); test('creates a PRO widget: iframe renders with allow-scripts sandbox and PRO badge visible', async ({ page, }) => { const proHtml = buildProWidgetBody('Oil vs Gold Interactive', 'pro-oil-gold'); await installProWidgetAgentMocks(page, [ { endpoint: '/rpc/worldmonitor.markets.v1.MarketsService/GetCommodities', title: 'Oil vs Gold Interactive', html: proHtml, }, ]); await page.goto('/'); await expect(page.locator('#panelsGrid .ai-widget-block-pro')).toBeVisible({ timeout: 30000 }); await page.locator('#panelsGrid .ai-widget-block-pro').click(); const modal = page.locator('.widget-chat-modal'); await expect(modal).toBeVisible(); await expect(modal.locator('.widget-pro-badge')).toBeVisible(); await expect(modal.locator('.widget-chat-readiness')).toContainText('Connected', { timeout: 15000 }); await modal.locator('.widget-chat-input').fill('Interactive chart comparing oil and gold prices'); await modal.locator('.widget-chat-send').click(); await expect(modal.locator('.widget-chat-action-btn')).toBeEnabled({ timeout: 30000 }); await expect(modal.locator('.widget-chat-preview')).toContainText('Oil vs Gold Interactive'); // PRO preview shows iframe (not basic .wm-widget-generated) const previewIframe = modal.locator('.widget-chat-preview iframe'); await expect(previewIframe).toBeVisible(); const sandboxAttr = await previewIframe.getAttribute('sandbox'); expect(sandboxAttr).toBe('allow-scripts'); expect(sandboxAttr).not.toContain('allow-same-origin'); await modal.locator('.widget-chat-action-btn').click(); const widgetPanel = page.locator('.custom-widget-panel', { has: page.locator('.panel-title', { hasText: 'Oil vs Gold Interactive' }), }); await expect(widgetPanel).toBeVisible({ timeout: 20000 }); await expect(widgetPanel.locator('.widget-pro-badge')).toBeVisible(); const panelIframe = widgetPanel.locator('iframe[sandbox="allow-scripts"]'); await expect(panelIframe).toBeVisible(); const iframeHeight = await panelIframe.evaluate((el) => el.getBoundingClientRect().height); expect(iframeHeight).toBeGreaterThanOrEqual(390); }); test('PRO widget stores HTML in wm-pro-html-{id} key and tier:pro in main array', async ({ page, }) => { const proHtml = buildProWidgetBody('Crypto Table', 'pro-crypto'); await installProWidgetAgentMocks(page, [ { endpoint: '/rpc/worldmonitor.markets.v1.MarketsService/GetCommodities', title: 'Crypto Table', html: proHtml, }, ]); await page.goto('/'); await expect(page.locator('#panelsGrid .ai-widget-block-pro')).toBeVisible({ timeout: 30000 }); await page.locator('#panelsGrid .ai-widget-block-pro').click(); const modal = page.locator('.widget-chat-modal'); await expect(modal.locator('.widget-chat-readiness')).toContainText('Connected', { timeout: 15000 }); await modal.locator('.widget-chat-input').fill('Sortable crypto price table'); await modal.locator('.widget-chat-send').click(); await expect(modal.locator('.widget-chat-action-btn')).toBeEnabled({ timeout: 30000 }); await modal.locator('.widget-chat-action-btn').click(); await expect(page.locator('.custom-widget-panel', { has: page.locator('.panel-title', { hasText: 'Crypto Table' }), })).toBeVisible({ timeout: 20000 }); const storage = await page.evaluate(() => { const widgets = JSON.parse(localStorage.getItem('wm-custom-widgets') || '[]') as Array<{ id: string; title: string; tier?: string; html?: string; }>; const entry = widgets.find((w) => w.title === 'Crypto Table'); if (!entry) return null; const proHtmlStored = localStorage.getItem(`wm-pro-html-${entry.id}`); return { entry, proHtmlStored }; }); expect(storage).not.toBeNull(); // Main array must have tier: 'pro' but NO html field expect(storage!.entry.tier).toBe('pro'); expect(storage!.entry.html).toBeUndefined(); // HTML must be in the separate key expect(storage!.proHtmlStored).toContain('pro-crypto'); }); test('modify PRO widget: tier preserved, history passed to server', async ({ page }) => { const requestBodies: unknown[] = []; await installProWidgetAgentMocks( page, [ { endpoint: '/rpc/worldmonitor.markets.v1.MarketsService/GetCommodities', title: 'Oil vs Gold Interactive', html: buildProWidgetBody('Oil vs Gold Interactive', 'pro-oil-gold'), }, { endpoint: '/rpc/worldmonitor.aviation.v1.AviationService/GetAirportDelays', title: 'Flight Interactive', html: buildProWidgetBody('Flight Interactive', 'pro-flight'), }, ], requestBodies, ); await page.goto('/'); await expect(page.locator('#panelsGrid .ai-widget-block-pro')).toBeVisible({ timeout: 30000 }); await page.locator('#panelsGrid .ai-widget-block-pro').click(); const modal = page.locator('.widget-chat-modal'); await expect(modal.locator('.widget-chat-readiness')).toContainText('Connected', { timeout: 15000 }); await modal.locator('.widget-chat-input').fill('Interactive oil gold chart'); await modal.locator('.widget-chat-send').click(); await expect(modal.locator('.widget-chat-action-btn')).toBeEnabled({ timeout: 30000 }); await modal.locator('.widget-chat-action-btn').click(); const widgetPanel = page.locator('.custom-widget-panel', { has: page.locator('.panel-title', { hasText: 'Oil vs Gold Interactive' }), }); await expect(widgetPanel).toBeVisible({ timeout: 20000 }); await widgetPanel.locator('.panel-widget-chat-btn').click(); const modifyModal = page.locator('.widget-chat-modal'); await expect(modifyModal).toBeVisible(); await expect(modifyModal.locator('.widget-pro-badge')).toBeVisible(); await modifyModal.locator('.widget-chat-input').fill('Turn into flight delay interactive chart'); await modifyModal.locator('.widget-chat-send').click(); await expect(modifyModal.locator('.widget-chat-action-btn')).toBeEnabled({ timeout: 30000 }); await modifyModal.locator('.widget-chat-action-btn').click(); await expect(page.locator('.custom-widget-panel', { has: page.locator('.panel-title', { hasText: 'Flight Interactive' }), })).toBeVisible({ timeout: 20000 }); const secondRequest = requestBodies[1] as { tier?: string; conversationHistory?: Array<{ role: string; content: string }>; } | undefined; expect(secondRequest?.tier).toBe('pro'); expect(secondRequest?.conversationHistory?.some((e) => e.content.includes('Interactive oil gold chart'))).toBe(true); // Verify stored widget still has tier: 'pro' const storedTier = await page.evaluate(() => { const widgets = JSON.parse(localStorage.getItem('wm-custom-widgets') || '[]') as Array<{ title: string; tier?: string; }>; return widgets.find((w) => w.title === 'Flight Interactive')?.tier; }); expect(storedTier).toBe('pro'); }); test('proKeyConfigured: false in health response → modal shows PRO unavailable error, button still visible', async ({ page, }) => { await installProWidgetAgentMocks(page, [], [], false); await page.goto('/'); await expect(page.locator('#panelsGrid .ai-widget-block-pro')).toBeVisible({ timeout: 30000 }); await page.locator('#panelsGrid .ai-widget-block-pro').click(); const modal = page.locator('.widget-chat-modal'); await expect(modal).toBeVisible(); // Modal preflight should show a PRO unavailable error message await expect(modal.locator('.widget-chat-readiness')).toContainText( /unavailable|not configured|PRO/i, { timeout: 15000 }, ); // Send button should be disabled (can't generate without PRO key on server) await expect(modal.locator('.widget-chat-send')).toBeDisabled(); // Close modal — PRO button must still be visible await page.keyboard.press('Escape'); await expect(modal).not.toBeVisible(); await expect(page.locator('#panelsGrid .ai-widget-block-pro')).toBeVisible(); }); }); ================================================ FILE: index.html ================================================ World Monitor - Real-Time Global Intelligence Dashboard
================================================ FILE: live-channels.html ================================================ Channel management - World Monitor
================================================ FILE: middleware.ts ================================================ const BOT_UA = /bot|crawl|spider|slurp|archiver|wget|curl\/|python-requests|scrapy|httpclient|go-http|java\/|libwww|perl|ruby|php\/|ahrefsbot|semrushbot|mj12bot|dotbot|baiduspider|yandexbot|sogou|bytespider|petalbot|gptbot|claudebot|ccbot/i; const SOCIAL_PREVIEW_UA = /twitterbot|facebookexternalhit|linkedinbot|slackbot|telegrambot|whatsapp|discordbot|redditbot/i; const SOCIAL_PREVIEW_PATHS = new Set(['/api/story', '/api/og-story']); const PUBLIC_API_PATHS = new Set(['/api/version', '/api/health']); const SOCIAL_IMAGE_UA = /Slack-ImgProxy|Slackbot|twitterbot|facebookexternalhit|linkedinbot|telegrambot|whatsapp|discordbot|redditbot/i; const VARIANT_HOST_MAP: Record = { 'tech.worldmonitor.app': 'tech', 'finance.worldmonitor.app': 'finance', 'happy.worldmonitor.app': 'happy', }; // Source of truth: src/config/variant-meta.ts — keep in sync when variant metadata changes. const VARIANT_OG: Record = { tech: { title: 'Tech Monitor - Real-Time AI & Tech Industry Dashboard', description: 'Real-time AI and tech industry dashboard tracking tech giants, AI labs, startup ecosystems, funding rounds, and tech events worldwide.', image: 'https://tech.worldmonitor.app/favico/tech/og-image.png', url: 'https://tech.worldmonitor.app/', }, finance: { title: 'Finance Monitor - Real-Time Markets & Trading Dashboard', description: 'Real-time finance and trading dashboard tracking global markets, stock exchanges, central banks, commodities, forex, crypto, and economic indicators worldwide.', image: 'https://finance.worldmonitor.app/favico/finance/og-image.png', url: 'https://finance.worldmonitor.app/', }, happy: { title: 'Happy Monitor - Good News & Global Progress', description: 'Curated positive news, progress data, and uplifting stories from around the world.', image: 'https://happy.worldmonitor.app/favico/happy/og-image.png', url: 'https://happy.worldmonitor.app/', }, }; const ALLOWED_HOSTS = new Set([ 'worldmonitor.app', ...Object.keys(VARIANT_HOST_MAP), ]); const VERCEL_PREVIEW_RE = /^[a-z0-9-]+-[a-z0-9]{8,}\.vercel\.app$/; function normalizeHost(raw: string): string { return raw.toLowerCase().replace(/:\d+$/, ''); } function isAllowedHost(host: string): boolean { return ALLOWED_HOSTS.has(host) || VERCEL_PREVIEW_RE.test(host); } export default function middleware(request: Request) { const url = new URL(request.url); const ua = request.headers.get('user-agent') ?? ''; const path = url.pathname; const host = normalizeHost(request.headers.get('host') ?? url.hostname); // Social bot OG response for variant subdomain root pages if (path === '/' && SOCIAL_PREVIEW_UA.test(ua)) { const variant = VARIANT_HOST_MAP[host]; if (variant && isAllowedHost(host)) { const og = VARIANT_OG[variant as keyof typeof VARIANT_OG]; if (og) { const html = ` ${og.title} `; return new Response(html, { status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store', 'Vary': 'User-Agent, Host', }, }); } } } // Only apply bot filtering to /api/* and /favico/* paths if (!path.startsWith('/api/') && !path.startsWith('/favico/')) { return; } // Allow social preview/image bots on OG image assets if (path.startsWith('/favico/') || path.endsWith('.png')) { if (SOCIAL_IMAGE_UA.test(ua)) { return; } } // Allow social preview bots on exact OG routes only if (SOCIAL_PREVIEW_UA.test(ua) && SOCIAL_PREVIEW_PATHS.has(path)) { return; } // Public endpoints bypass all bot filtering if (PUBLIC_API_PATHS.has(path)) { return; } // Block bots from all API routes if (BOT_UA.test(ua)) { return new Response('{"error":"Forbidden"}', { status: 403, headers: { 'Content-Type': 'application/json' }, }); } // No user-agent or suspiciously short — likely a script if (!ua || ua.length < 10) { return new Response('{"error":"Forbidden"}', { status: 403, headers: { 'Content-Type': 'application/json' }, }); } } export const config = { matcher: ['/', '/api/:path*', '/favico/:path*'], }; ================================================ FILE: nixpacks.toml ================================================ # Railway relay build config (root_dir="" — builds from repo root). # Promotes scripts/nixpacks.toml settings here since nixpacks only reads # config from the build root. Adds scripts/ dep install so that packages # in scripts/package.json (e.g. @anthropic-ai/sdk, fast-xml-parser) are # available at runtime when node scripts/ais-relay.cjs is started. [phases.setup] aptPkgs = ["curl"] [variables] NODE_OPTIONS = "--dns-result-order=ipv4first" [phases.install] cmds = ["npm ci"] [phases.build] cmds = ["npm install", "npm install --prefix scripts"] ================================================ FILE: package.json ================================================ { "name": "world-monitor", "private": true, "version": "2.6.5", "license": "AGPL-3.0-only", "type": "module", "scripts": { "lint": "biome lint ./src ./server ./api ./tests ./e2e ./scripts ./middleware.ts", "lint:fix": "biome check ./src ./server ./api ./tests ./e2e ./scripts ./middleware.ts --fix", "lint:boundaries": "node scripts/lint-boundaries.mjs", "lint:unicode": "node scripts/check-unicode-safety.mjs", "lint:unicode:staged": "node scripts/check-unicode-safety.mjs --staged", "lint:md": "markdownlint-cli2 '**/*.md' '!**/node_modules/**' '!.agent/**' '!.agents/**' '!.claude/**' '!.factory/**' '!.windsurf/**' '!skills/**' '!docs/internal/**' '!docs/Docs_To_Review/**'", "version:sync": "node scripts/sync-desktop-version.mjs", "version:check": "node scripts/sync-desktop-version.mjs --check", "dev": "vite", "dev:tech": "cross-env VITE_VARIANT=tech vite", "dev:finance": "cross-env VITE_VARIANT=finance vite", "dev:happy": "cross-env VITE_VARIANT=happy vite", "dev:commodity": "cross-env VITE_VARIANT=commodity vite", "postinstall": "cd blog-site && npm ci --prefer-offline", "build:blog": "cd blog-site && npm run build && rm -rf ../public/blog && mkdir -p ../public/blog && cp -r dist/* ../public/blog/", "build:pro": "cd pro-test && npm install && npm run build", "build": "npm run build:blog && tsc && vite build", "build:sidecar-sebuf": "node scripts/build-sidecar-sebuf.mjs", "build:desktop": "node scripts/build-sidecar-sebuf.mjs && node scripts/build-sidecar-handlers.mjs && tsc && vite build", "build:full": "npm run build:blog && cross-env-shell VITE_VARIANT=full \"tsc && vite build\"", "build:tech": "cross-env-shell VITE_VARIANT=tech \"tsc && vite build\"", "build:finance": "cross-env-shell VITE_VARIANT=finance \"tsc && vite build\"", "build:happy": "cross-env-shell VITE_VARIANT=happy \"tsc && vite build\"", "build:commodity": "cross-env-shell VITE_VARIANT=commodity \"tsc && vite build\"", "typecheck": "tsc --noEmit", "typecheck:api": "tsc --noEmit -p tsconfig.api.json", "typecheck:all": "tsc --noEmit && tsc --noEmit -p tsconfig.api.json", "tauri": "tauri", "preview": "vite preview", "test:e2e:full": "cross-env VITE_VARIANT=full playwright test", "test:e2e:tech": "cross-env VITE_VARIANT=tech playwright test", "test:e2e:finance": "cross-env VITE_VARIANT=finance playwright test", "test:e2e:runtime": "cross-env VITE_VARIANT=full playwright test e2e/runtime-fetch.spec.ts", "test:e2e": "npm run test:e2e:runtime && npm run test:e2e:full && npm run test:e2e:tech && npm run test:e2e:finance", "test:data": "tsx --test tests/*.test.mjs tests/*.test.mts", "test:feeds": "node scripts/validate-rss-feeds.mjs", "test:sidecar": "node --test src-tauri/sidecar/local-api-server.test.mjs api/_cors.test.mjs api/youtube/embed.test.mjs api/cyber-threats.test.mjs api/usni-fleet.test.mjs scripts/ais-relay-rss.test.cjs api/loaders-xml-wms-regression.test.mjs", "test:e2e:visual:full": "cross-env VITE_VARIANT=full playwright test -g \"matches golden screenshots per layer and zoom\"", "test:e2e:visual:tech": "cross-env VITE_VARIANT=tech playwright test -g \"matches golden screenshots per layer and zoom\"", "test:e2e:visual": "npm run test:e2e:visual:full && npm run test:e2e:visual:tech", "test:e2e:visual:update:full": "cross-env VITE_VARIANT=full playwright test -g \"matches golden screenshots per layer and zoom\" --update-snapshots", "test:e2e:visual:update:tech": "cross-env VITE_VARIANT=tech playwright test -g \"matches golden screenshots per layer and zoom\" --update-snapshots", "test:e2e:visual:update": "npm run test:e2e:visual:update:full && npm run test:e2e:visual:update:tech", "desktop:dev": "npm run version:sync && cross-env VITE_DESKTOP_RUNTIME=1 tauri dev -f devtools", "desktop:build:full": "npm run version:sync && cross-env VITE_VARIANT=full VITE_DESKTOP_RUNTIME=1 tauri build", "desktop:build:tech": "npm run version:sync && cross-env VITE_VARIANT=tech VITE_DESKTOP_RUNTIME=1 tauri build --config src-tauri/tauri.tech.conf.json", "desktop:build:finance": "npm run version:sync && cross-env VITE_VARIANT=finance VITE_DESKTOP_RUNTIME=1 tauri build --config src-tauri/tauri.finance.conf.json", "desktop:package:macos:full": "node scripts/desktop-package.mjs --os macos --variant full", "desktop:package:macos:tech": "node scripts/desktop-package.mjs --os macos --variant tech", "desktop:package:windows:full": "node scripts/desktop-package.mjs --os windows --variant full", "desktop:package:windows:tech": "node scripts/desktop-package.mjs --os windows --variant tech", "desktop:package:macos:full:sign": "node scripts/desktop-package.mjs --os macos --variant full --sign", "desktop:package:macos:tech:sign": "node scripts/desktop-package.mjs --os macos --variant tech --sign", "desktop:package:windows:full:sign": "node scripts/desktop-package.mjs --os windows --variant full --sign", "desktop:package:windows:tech:sign": "node scripts/desktop-package.mjs --os windows --variant tech --sign", "desktop:package": "node scripts/desktop-package.mjs" }, "devDependencies": { "@biomejs/biome": "^2.4.7", "@bufbuild/buf": "^1.66.0", "@playwright/test": "^1.52.0", "@tauri-apps/cli": "^2.10.0", "@types/canvas-confetti": "^1.9.0", "@types/d3": "^7.4.3", "@types/dompurify": "^3.0.5", "@types/geojson": "^7946.0.14", "@types/maplibre-gl": "^1.13.2", "@types/marked": "^5.0.2", "@types/papaparse": "^5.5.2", "@types/supercluster": "^7.1.3", "@types/three": "^0.183.1", "@types/topojson-client": "^3.1.5", "@types/topojson-specification": "^1.0.5", "cross-env": "^10.1.0", "esbuild": "^0.27.3", "h3-js": "^4.4.0", "markdownlint-cli2": "^0.21.0", "tsx": "^4.21.0", "typescript": "^5.7.2", "vite": "^6.0.7", "vite-plugin-pwa": "^1.2.0" }, "dependencies": { "@anthropic-ai/sdk": "^0.79.0", "@aws-sdk/client-s3": "^3.1009.0", "@deck.gl/aggregation-layers": "^9.2.6", "@deck.gl/core": "^9.2.6", "@deck.gl/geo-layers": "^9.2.6", "@deck.gl/layers": "^9.2.6", "@deck.gl/mapbox": "^9.2.6", "@protomaps/basemaps": "^5.7.1", "@sentry/browser": "^10.39.0", "@upstash/ratelimit": "^2.0.8", "@upstash/redis": "^1.36.1", "@vercel/analytics": "^2.0.0", "@xenova/transformers": "^2.17.2", "canvas-confetti": "^1.9.4", "convex": "^1.32.0", "d3": "^7.9.0", "deck.gl": "^9.2.6", "dompurify": "^3.1.7", "fast-xml-parser": "^5.3.7", "globe.gl": "^2.45.0", "i18next": "^25.8.10", "i18next-browser-languagedetector": "^8.2.1", "maplibre-gl": "^5.16.0", "marked": "^17.0.3", "onnxruntime-web": "^1.23.2", "papaparse": "^5.5.3", "pmtiles": "^4.4.0", "preact": "^10.25.4", "satellite.js": "^6.0.2", "supercluster": "^8.0.1", "telegram": "^2.26.22", "topojson-client": "^3.1.0", "ws": "^8.19.0", "youtubei.js": "^16.0.1" }, "overrides": { "fast-xml-parser": "^5.3.7", "serialize-javascript": "^7.0.4" } } ================================================ FILE: playwright.config.ts ================================================ import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ testDir: './e2e', workers: 1, timeout: 90000, expect: { timeout: 30000, }, retries: 0, reporter: 'list', use: { baseURL: 'http://127.0.0.1:4173', viewport: { width: 1280, height: 720 }, colorScheme: 'dark', locale: 'en-US', timezoneId: 'UTC', trace: 'retain-on-failure', screenshot: 'only-on-failure', video: 'retain-on-failure', }, projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'], launchOptions: { args: ['--use-angle=swiftshader', '--use-gl=swiftshader'], }, }, }, ], snapshotPathTemplate: '{testDir}/{testFileName}-snapshots/{arg}{ext}', webServer: { command: 'VITE_E2E=1 npm run dev -- --host 127.0.0.1 --port 4173', url: 'http://127.0.0.1:4173/tests/map-harness.html', reuseExistingServer: false, timeout: 120000, }, }); ================================================ FILE: pro-test/.env.example ================================================ # GEMINI_API_KEY: Required for Gemini AI API calls. # AI Studio automatically injects this at runtime from user secrets. # Users configure this via the Secrets panel in the AI Studio UI. GEMINI_API_KEY="MY_GEMINI_API_KEY" # APP_URL: The URL where this applet is hosted. # AI Studio automatically injects this at runtime with the Cloud Run service URL. # Used for self-referential links, OAuth callbacks, and API endpoints. APP_URL="MY_APP_URL" ================================================ FILE: pro-test/.gitignore ================================================ node_modules/ build/ dist/ coverage/ .DS_Store *.log .env* !.env.example ================================================ FILE: pro-test/README.md ================================================
GHBanner
# Run and deploy your AI Studio app This contains everything you need to run your app locally. View your app in AI Studio: https://ai.studio/apps/ef577c64-7776-42d3-bb38-3f0a627564c3 ## Run Locally **Prerequisites:** Node.js 1. Install dependencies: `npm install` 2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key 3. Run the app: `npm run dev` ================================================ FILE: pro-test/index.html ================================================ World Monitor Pro — Markets, Macro & Geopolitical Intelligence
================================================ FILE: pro-test/metadata.json ================================================ { "name": "World Monitor Launch", "description": "Launch waitlist page for World Monitor, the open-source intelligence dashboard.", "requestFramePermissions": [] } ================================================ FILE: pro-test/package.json ================================================ { "name": "worldmonitor-pro", "private": true, "version": "0.1.0", "type": "module", "scripts": { "dev": "vite --port=3000", "build": "vite build && node prerender.mjs", "preview": "vite preview", "lint": "tsc --noEmit" }, "dependencies": { "@tailwindcss/vite": "^4.1.14", "@vitejs/plugin-react": "^5.0.4", "i18next": "^25.8.14", "i18next-browser-languagedetector": "^8.2.1", "lucide-react": "^0.546.0", "motion": "^12.23.24", "react": "^19.0.0", "react-dom": "^19.0.0", "vite": "^6.2.0" }, "devDependencies": { "@types/node": "^22.14.0", "tailwindcss": "^4.1.14", "typescript": "~5.8.2" } } ================================================ FILE: pro-test/prerender.mjs ================================================ #!/usr/bin/env node /** * Postbuild prerender script — injects critical SEO content into the built HTML * so search engines see real content without executing JavaScript. * * This is a lightweight SSG alternative: it embeds key text content * (headings, descriptions, FAQ answers) directly into the HTML body * as a hidden div that gets replaced when React hydrates. */ import { readFileSync, writeFileSync } from 'node:fs'; import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); const htmlPath = resolve(__dirname, '../public/pro/index.html'); const en = JSON.parse(readFileSync(resolve(__dirname, 'src/locales/en.json'), 'utf-8')); const seoContent = `

${en.hero.title1} ${en.hero.title2}

${en.hero.subtitle}

${en.hero.missionLine}

Plans

${en.twoPath.proTitle}

${en.twoPath.proDesc}

${en.twoPath.proF1}

${en.twoPath.proF2}

${en.twoPath.proF3}

${en.twoPath.proF4}

${en.twoPath.entTitle}

${en.twoPath.entDesc}

${en.whyUpgrade.title}

${en.whyUpgrade.noiseTitle}

${en.whyUpgrade.noiseDesc}

${en.whyUpgrade.fasterTitle}

${en.whyUpgrade.fasterDesc}

${en.whyUpgrade.controlTitle}

${en.whyUpgrade.controlDesc}

${en.whyUpgrade.deeperTitle}

${en.whyUpgrade.deeperDesc}

${en.proShowcase.title}

${en.proShowcase.subtitle}

${en.proShowcase.equityResearch}

${en.proShowcase.equityResearchDesc}

${en.proShowcase.geopoliticalAnalysis}

${en.proShowcase.geopoliticalAnalysisDesc}

${en.proShowcase.economyAnalytics}

${en.proShowcase.economyAnalyticsDesc}

${en.proShowcase.riskMonitoring}

${en.proShowcase.riskMonitoringDesc}

${en.proShowcase.morningBriefs}

${en.proShowcase.morningBriefsDesc}

${en.proShowcase.oneKey}

${en.proShowcase.oneKeyDesc}

${en.audience.title}

${en.audience.investorsTitle}

${en.audience.investorsDesc}

${en.audience.tradersTitle}

${en.audience.tradersDesc}

${en.audience.researchersTitle}

${en.audience.researchersDesc}

${en.audience.journalistsTitle}

${en.audience.journalistsDesc}

${en.audience.govTitle}

${en.audience.govDesc}

${en.audience.teamsTitle}

${en.audience.teamsDesc}

${en.dataCoverage.title}

${en.dataCoverage.subtitle}

${en.apiSection.title}

${en.apiSection.subtitle}

${en.enterpriseShowcase.title}

${en.enterpriseShowcase.subtitle}

${en.pricingTable.title}

${en.faq.title}

${en.faq.q1}
${en.faq.a1}
${en.faq.q2}
${en.faq.a2}
${en.faq.q3}
${en.faq.a3}
${en.faq.q4}
${en.faq.a4}
${en.faq.q5}
${en.faq.a5}
${en.faq.q6}
${en.faq.a6}
${en.faq.q7}
${en.faq.a7}
${en.faq.q8}
${en.faq.a8}

${en.finalCta.title}

${en.finalCta.subtitle}

`; let html = readFileSync(htmlPath, 'utf-8'); html = html.replace('
', `
${seoContent}
`); writeFileSync(htmlPath, html, 'utf-8'); console.log('[prerender] Injected SEO content into public/pro/index.html'); ================================================ FILE: pro-test/src/App.tsx ================================================ import { useState, useEffect } from 'react'; import { motion } from 'motion/react'; import { Globe, Activity, ShieldAlert, Zap, Terminal, Database, Send, MessageCircle, Mail, MessageSquare, ChevronDown, ArrowRight, Check, Lock, Server, Cpu, Layers, Bell, Brain, Key, Plug, PanelTop, ExternalLink, BarChart3, Clock, Radio, Ship, Plane, Flame, Cable, Wifi, MapPin, Users, TrendingUp, Filter, Lightbulb, SlidersHorizontal, Telescope, LineChart, Search, Shield, Building2, Landmark, Fuel } from 'lucide-react'; import { t } from './i18n'; import dashboardFallback from './assets/worldmonitor-7-mar-2026.jpg'; import wiredLogo from './assets/wired-logo.svg'; const API_BASE = 'https://api.worldmonitor.app/api'; const TURNSTILE_SITE_KEY = '0x4AAAAAACnaYgHIyxclu8Tj'; const PRO_URL = 'https://worldmonitor.app/pro'; declare global { interface Window { turnstile?: { render: (container: string | HTMLElement, opts: Record) => string; getResponse: (widgetOrId?: string | HTMLElement) => string | undefined; reset: (widgetOrId?: string | HTMLElement) => void; }; } } export function renderTurnstileWidgets(): number { if (!window.turnstile) return 0; let count = 0; document.querySelectorAll('.cf-turnstile:not([data-rendered])').forEach(el => { const widgetId = window.turnstile!.render(el, { sitekey: TURNSTILE_SITE_KEY, size: 'flexible', callback: (token: string) => { el.dataset.token = token; }, 'expired-callback': () => { delete el.dataset.token; }, 'error-callback': () => { delete el.dataset.token; }, }); el.dataset.rendered = 'true'; el.dataset.widgetId = String(widgetId); count++; }); return count; } function getRefCode(): string | undefined { const params = new URLSearchParams(window.location.search); return params.get('ref') || undefined; } function sanitize(val: unknown): string { return String(val ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c] || c)); } function showReferralSuccess(formEl: HTMLFormElement, data: { referralCode?: string; position?: number; status?: string }) { if (data.referralCode == null && data.status == null) { const btn = formEl.querySelector('button[type="submit"]') as HTMLButtonElement; if (btn) { btn.textContent = t('form.joinWaitlist'); btn.disabled = false; } return; } const safeCode = sanitize(data.referralCode); const referralLink = `${PRO_URL}?ref=${safeCode}`; const shareText = encodeURIComponent(t('referral.shareText')); const shareUrl = encodeURIComponent(referralLink); const el = (tag: string, cls: string, text?: string) => { const node = document.createElement(tag); node.className = cls; if (text) node.textContent = text; return node; }; const successDiv = el('div', 'text-center'); const isAlreadyRegistered = data.status === 'already_registered'; const shareHint = t('referral.shareHint'); if (isAlreadyRegistered) { successDiv.appendChild(el('p', 'text-lg font-display font-bold text-wm-green mb-2', t('referral.alreadyOnList'))); } else { successDiv.appendChild(el('p', 'text-lg font-display font-bold text-wm-green mb-2', t('referral.youreIn'))); } successDiv.appendChild(el('p', 'text-sm text-wm-muted mb-4', shareHint)); if (safeCode) { const linkBox = el('div', 'bg-wm-card border border-wm-border px-4 py-3 mb-4 font-mono text-xs text-wm-green break-all select-all cursor-pointer', referralLink); linkBox.addEventListener('click', () => { navigator.clipboard.writeText(referralLink).then(() => { linkBox.textContent = t('referral.copied'); setTimeout(() => { linkBox.textContent = referralLink; }, 2000); }); }); successDiv.appendChild(linkBox); const shareRow = el('div', 'flex gap-3 justify-center flex-wrap'); const shareLinks = [ { label: t('referral.shareOnX'), href: `https://x.com/intent/tweet?text=${shareText}&url=${shareUrl}` }, { label: t('referral.linkedin'), href: `https://www.linkedin.com/sharing/share-offsite/?url=${shareUrl}` }, { label: t('referral.whatsapp'), href: `https://wa.me/?text=${shareText}%20${shareUrl}` }, { label: t('referral.telegram'), href: `https://t.me/share/url?url=${shareUrl}&text=${encodeURIComponent(t('referral.joinWaitlistShare'))}` }, ]; for (const s of shareLinks) { const a = el('a', 'bg-wm-card border border-wm-border px-4 py-2 text-xs font-mono text-wm-muted hover:text-wm-text hover:border-wm-text transition-colors', s.label); (a as HTMLAnchorElement).href = s.href; (a as HTMLAnchorElement).target = '_blank'; (a as HTMLAnchorElement).rel = 'noreferrer'; shareRow.appendChild(a); } successDiv.appendChild(shareRow); } formEl.replaceWith(successDiv); } async function submitWaitlist(email: string, formEl: HTMLFormElement) { const btn = formEl.querySelector('button[type="submit"]') as HTMLButtonElement; const origText = btn.textContent; btn.disabled = true; btn.textContent = t('form.submitting'); const honeypot = (formEl.querySelector('input[name="website"]') as HTMLInputElement)?.value || ''; const turnstileWidget = formEl.querySelector('.cf-turnstile') as HTMLElement | null; const turnstileToken = turnstileWidget?.dataset.token || ''; const ref = getRefCode(); try { const res = await fetch(`${API_BASE}/register-interest`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, source: 'pro-waitlist', website: honeypot, turnstileToken, referredBy: ref }), }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'Registration failed'); showReferralSuccess(formEl, { referralCode: data.referralCode, position: data.position, status: data.status }); } catch (err: any) { btn.textContent = err.message === 'Too many requests' ? t('form.tooManyRequests') : t('form.failedTryAgain'); btn.disabled = false; if (turnstileWidget?.dataset.widgetId && window.turnstile) { window.turnstile.reset(turnstileWidget.dataset.widgetId); delete turnstileWidget.dataset.token; } setTimeout(() => { btn.textContent = origText; }, 3000); } } const SlackIcon = () => ( ); const Logo = () => (
WORLD MONITOR by Someone.ceo
); /* ─── 0. Navbar ─── */ const Navbar = () => ( ); /* ─── 1. Hero — Less noise, more signal ─── */ const WiredBadge = () => ( {t('wired.asFeaturedIn')} WIRED ); const SignalBars = () => { const total = 60; const center = total / 2; const signalRadius = 8; return (
); }; const Hero = () => (

{t('hero.noiseWord')} {t('hero.signalWord')}

{t('hero.valueProps')}

{getRefCode() && (
)}
{ e.preventDefault(); const form = e.currentTarget; const email = new FormData(form).get('email') as string; submitWaitlist(email, form); }}>

{t('hero.launchingDate')}

| {t('hero.tryFreeDashboard')}
); /* ─── 2. Social proof (current — WIRED badge already in hero) ─── */ const SocialProof = () => (
{[ { value: "2M+", label: t('socialProof.uniqueVisitors') }, { value: "421K", label: t('socialProof.peakDailyUsers') }, { value: "190+", label: t('socialProof.countriesReached') }, { value: "435+", label: t('socialProof.liveDataSources') }, ].map((stat, i) => (

{stat.value}

{stat.label}

))}

"{t('socialProof.quote')}"

); /* ─── 3. Two-path split (new — from draft) ─── */ const TwoPathSplit = () => (

Plans

{t('twoPath.proTitle')}

{t('twoPath.proDesc')}

    {[t('twoPath.proF1'), t('twoPath.proF2'), t('twoPath.proF3'), t('twoPath.proF4'), t('twoPath.proF5'), t('twoPath.proF6'), t('twoPath.proF7'), t('twoPath.proF8'), t('twoPath.proF9')].map((f, i) => (
  • ))}
{t('twoPath.proCta')}

{t('twoPath.entTitle')}

{t('twoPath.entDesc')}

  • {t('twoPath.entF1')}
  • {[t('twoPath.entF2'), t('twoPath.entF3'), t('twoPath.entF4'), t('twoPath.entF5'), t('twoPath.entF6'), t('twoPath.entF7'), t('twoPath.entF8'), t('twoPath.entF9'), t('twoPath.entF10'), t('twoPath.entF11')].map((f, i) => (
  • ))}
{t('twoPath.entCta')}
); /* ─── 4. Why Upgrade (new — from draft) ─── */ const WhyUpgrade = () => { const items = [ { icon: