* fix(settings): stop paying users hitting 409 on stale Upgrade CTA
UnifiedSettings.renderUpgradeSection branched on `isEntitled()`, which
returns false while the Convex entitlement snapshot is still null during
a cold WebSocket load. The modal's `onEntitlementChange` listener only
re-rendered the `api-keys` tab, so the stale "Upgrade to Pro" button in
the Settings tab was never replaced once the snapshot arrived.
Paying users who opened settings in that window saw "Upgrade to Pro",
clicked it, hit `/api/create-checkout`, got 409 `duplicate_subscription`,
and cascaded into the billing-portal fallback path (which itself can
dead-end on a generic Dodo login). Same class of bug as the 2026-04-17/18
panel-overlay duplicate-subscription incident called out in
panel-gating.ts:20-22 -- different surface, same race.
Three-part fix in src/components/UnifiedSettings.ts:
- renderUpgradeSection() returns a hidden wrapper for non-Dodo premium
(API key / tester key / Clerk pro role) so those users don't get
stuck on the loading placeholder indefinitely.
- A signed-in user whose Convex entitlement snapshot is still null gets
a neutral loading placeholder, not "Upgrade to Pro". An early click
can no longer submit before Convex hydrates.
- open()'s onEntitlementChange handler now swaps the .upgrade-pro-section
element in place when the snapshot arrives. Click handlers are
delegated at overlay level, so replacing the node needs no rebind.
Observed signal behind this fix:
- WORLDMONITOR-NY (2026-04-23): Checkout error: duplicate_subscription
- WORLDMONITOR-NZ (2026-04-23): getCustomerPortalUrl Server Error, same
session, 4s later, triggered by the duplicate-subscription dialog.
* fix(settings): hide loading placeholder to avoid empty bordered card
The base .upgrade-pro-section CSS (main.css:22833) applies margin, padding,
border, and surface background. Without 'hidden', the empty loading
placeholder paints a visibly blank bordered box during the Convex
cold-load window — swapping one bad state (stale 'Upgrade to Pro') for
another (confusing empty card).
Adding 'hidden' lets the browser's default [hidden] { display: none }
suppress the card entirely. Element stays queryable for the replaceWith
swap in open(), so the onEntitlementChange listener still finds it.
* fix(settings): bounded readiness window + click-time isEntitled guard
Addresses P1 review on PR #3349: the initial fix treated
getEntitlementState() === null as "still loading", but null is ALSO a
terminal state when Convex is disabled (no VITE_CONVEX_URL), auth times
out at waitForConvexAuth (10s), or initEntitlementSubscription throws
(entitlements.ts:41,47,58,78). In those cases a signed-in free user
would have seen a permanently empty placeholder instead of the Upgrade
to Pro CTA — a real regression on the main conversion surface.
Changes in src/components/UnifiedSettings.ts:
- Add `entitlementReady` class flag + `entitlementReadyTimer`. The flag
flips true on first snapshot OR after a 12s fallback timer kicks in.
12s > the 10s waitForConvexAuth timeout so healthy-but-slow paths land
on the real state before the fallback fires.
- Seed `entitlementReady = getEntitlementState() !== null` BEFORE the
first render() so the initial paint branches on the current snapshot,
not the stale value from a prior open/close cycle.
- renderUpgradeSection() now gates the loading placeholder on
`!this.entitlementReady` so the signed-in-free branch eventually
renders the Upgrade CTA even when Convex never hydrates.
- handleUpgradeClick() defensively re-checks isEntitled() at click time:
if the snapshot arrives AFTER the 12s timer but BEFORE the user's
click, route to the billing portal instead of triggering
/api/create-checkout against an active subscription (which would 409
and re-enter the exact duplicate_subscription → getCustomerPortalUrl
cascade this PR is trying to eliminate).
- Extract replaceUpgradeSection() helper so both the listener and the
fallback timer share the same in-place swap path.
- close() clears the timer.
* fix(settings): clear entitlementReadyTimer in destroy()
Mirror the close() cleanup. Without this, if destroy() is called during
the 12s fallback window the timer fires after teardown and invokes
replaceUpgradeSection() against a detached overlay. The early-return
inside replaceUpgradeSection (querySelector returns null) makes the
callback a no-op, but the stray async callback + DOM reference stay
alive until fire — tidy them up at destroy time.
World Monitor
Real-time global intelligence dashboard — AI-powered news aggregation, geopolitical monitoring, and infrastructure tracking in a unified situational awareness interface.
Documentation · Releases · Contributing
What It Does
- 500+ 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.
Quick Start
git clone https://github.com/koala73/worldmonitor.git
cd worldmonitor
npm install
npm run dev
Open localhost:5173. No environment variables required for basic operation.
For variant-specific development:
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 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.
Flight Data
Flight data provided gracefully by Wingbits, the most advanced ADS-B flight data solution.
Data Sources
WorldMonitor aggregates 65+ external data sources across geopolitics, finance, energy, climate, aviation, cyber, military, infrastructure, and news intelligence. See the full data sources catalog for providers, feed tiers, and collection methods.
Contributing
Contributions welcome! See CONTRIBUTING.md for guidelines.
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 for full terms. For commercial licensing, contact the maintainer.
Copyright (C) 2024-2026 Elie Habib. All rights reserved.
Author
Elie Habib — GitHub
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 for responsible disclosure guidelines.
worldmonitor.app · docs.worldmonitor.app · finance.worldmonitor.app · commodity.worldmonitor.app
