Back to Worldmonitor

Desktop / Web Parity Matrix

docs/desktop-parity-matrix.md

2.10.019.1 KB
Original Source

Desktop / Web Parity Matrix

Baseline established 2026-07-31 for #5902 from origin/main @ 7fc700303. Maintained document: update rows when desktop-coupled behavior changes, and re-verify the whole matrix before each desktop release. Supersedes docs/local-backend-audit.md and docs/Docs_To_Review/local-backend-audit.md for desktop route coverage (both predate the current routing and cite removed files).

Parity here means a user-visible web capability is either (1) working in the supported desktop build, (2) intentionally unavailable with a documented reason and usable fallback, or (3) tracked by a named blocker. It does not mean pixel-for-pixel equivalence: the desktop app reuses the SPA.

Classifications: parity · intentional difference · blocked · n/a.

Headline state (2026-07-31)

FactValueEvidence
Repo version2.10.0 (package.json = tauri.conf.json = Cargo.toml; npm run version:check green)scripts/sync-desktop-version.mjs
Newest published desktop releasev2.5.23, built from e51058e17 (2026-03-01); no -tech tag has ever been publishedGitHub releases; gh run list
Release delta~5 months of main unreleased; the unified map-layer catalog (src/config/map-layer-definitions.ts, #943) postdates the release entirelygit show e51058e17 — file absent
Shipped-build capability lossFixed by #5905: all Tauri build steps now declare the desktop-required client VITE_* set (Clerk/Convex/cyber + relay/basemap parity keys; web-push VAPID remains intentionally excluded on Tauri), a two-way completeness gate (scripts/check-desktop-build-env.mjs, npm run desktop:check-env) blocks regressions, and tag-push or published manual releases hard-fail on empty client secrets. Capabilities activate once the repo secrets are provisioned (ops step in the #5905 PR). Historical state: only VITE_VARIANT, VITE_DESKTOP_RUNTIME, VITE_WS_API_URL, CONVEX_URL were passed — sign-in, subscription entitlements, web push, and the Cyber Threats layer were silently disabled in every CI-built release.github/workflows/build-desktop.yml build-leg env blocks; scripts/check-desktop-build-env.mjs
PR CI before #5902 gatessrc-tauri/ (non-sidecar) changes ran zero PR CI; version:check self-excluded from src-tauri-only diffs; sidecar handler bundling ran only at release time; the inline Rust tests had never run in CItest.yml / lint-code.yml change filters
PR CI after #5902 gatesdesktop-config + desktop-rust jobs gate desktop-coupled paths; twice-weekly installed-app canary (Desktop Canary (Linux)) asserts launch, sidecar readiness, rendered contentthis repo, Test workflow

Release blockers (tracked)

BlockerIssueStatus
Tauri origin-confusion CVE-2026-42184 (CVSS 8.8): lockfile pins tauri 2.10.3, needs ≥ 2.11.1; Cargo.toml carries an unbounded version = "2" caret#5518open — release-blocking security finding
Desktop build env omits Clerk/Convex/VAPID/cyber VITE_* vars → sign-in, Pro, push, Cyber Threats dead in shipped builds#5905fix landed (env declared + completeness gate + release preflight); remaining ops step: provision the four new repo secrets, after which release builds hard-fail if they go empty again
Linux secret storage fails without an activatable Secret Service; every keyring error in SecretsCache::load_from_keychain is silently swallowed (if let Ok), so users get an empty vault with zero diagnostics#802 / #1905open — needs secure fallback + migration + diagnostics
Released 2.5.23 lacks Internet Outages / Cyber Threats layers#5829diagnosed — three compounding causes, see row below
Desktop readiness/error diagnosability; src/services/desktop-readiness.ts:64-65 still cites deleted /api/risk-scores routes in the Service Status UI; sidecar readiness is assumed on port-file timeout (main.rs:1443-1452) rather than verified#1942open — reassess with this evidence
AppImage update metadata (AppImageUpdate/zsync)#5757open
Uninstall experience#5435 / #5487open

#5829 diagnosis (Internet Outages + Cyber Threats missing)

Three compounding causes, none of them SPA desktop-gating:

  1. Release staleness — v2.5.23 predates the unified layer catalog entirely.
  2. Cyber Threats: build-env omissionCYBER_LAYER_ENABLED requires VITE_ENABLE_CYBER_LAYER === 'true' (src/App.ts:155); build-desktop.yml never sets it, so even a fresh release today would hide the layer (force-off at boot App.ts:901-903, toggle hidden App.ts:1770-1771).
  3. Internet Outages: keyring capability gating — on desktop, isFeatureAvailable('internetOutages') requires a valid CLOUDFLARE_API_TOKEN in the OS keyring (src/services/runtime-config.ts:431-444); without it the toggle is hidden (App.ts:1767-1769) with no explanatory UI. Intentional gating presenting as a missing feature (feeds #1942).

Capability matrix

Rows classified from code inspection at 7fc700303. "Desktop" means what the current code does in a current desktop build — not the stale v2.5.23 binary.

Runtime & data routing

CapabilityDesktop behavior vs webClassificationEvidence
API routingAll /api/* calls proxied through the Rust proxy_local_api_request command to a bundled Node sidecar on 127.0.0.1:46123 (port-file discovery, default 46123); web uses direct fetchintentional difference (richer: local-first)src/services/runtime.ts:115-126,346-413; src-tauri/src/main.rs:25
Cloud fallbackOnly with a valid WORLDMONITOR_API_KEY in the keyring, except a small key-free allowlist (register-interest, leads/*, version); web N/Aintentional difference — default install is local+anonymous-cloud-freesrc/services/runtime.ts:299-304,367-378
Sidecar route surface60 routes: 34 sebuf {domain}/v1/[rpc].js (esbuild-bundled at release) + 26 raw .js; .ts-only routes (87 files) are cloud-onlymixed — see gapssrc-tauri/sidecar/local-api-server.mjs:531-557; scripts/build-sidecar-handlers.mjs
Seed-backed RPC dataGAP: sidecar has no Redis credentials; seed-backed handlers return empty 200 locally and the !ok cloud fallback never fires; hardcoded cloudPreferred covers only market/economic/infrastructure/news/research (5 of 34 domains) + /api/bootstrap. 29 domains — incl. all 9 added since v2.5.23 — can serve blank panelsblocked → #5906local-api-server.mjs:686-705,1764-1767; server/_shared/redis.ts:86-88
api/v2/shipping familyGAP: invisible to the handler-build glob (api/{domain}/v1/[rpc].ts only); cloud-only + Pro-gated on desktop, undeclaredblocked → #5907scripts/build-sidecar-handlers.mjs:26-29; src/shared/premium-paths.ts:52-53
Non-[rpc] sibling routes (scenario/v1/run…, supply-chain/v1/country-products…)Mis-routed to the local [rpc].js bracket match, recovered via cloud fallback — works but wastes a local round-tripblocked (minor) → #5906local-api-server.mjs:483-529,1764-1767
Bootstrap timingLonger budgets (fast tier 5s vs 1.2s; slow 8s vs 3s); boot awaits sidecar readiness poll (3s)intentional differencesrc/services/bootstrap.ts:427,333; src/App.ts:1189,1377-1381
Widget agent (SSE)Bypasses sidecar entirely → Railway relay direct (sidecar buffering breaks SSE)intentional differencesrc/utils/proxy.ts:16-32
GeolocationSkips /api/geo; timezone inference insteadintentional differencesrc/utils/user-location.ts:84-93
Anonymous wms_ HMAC sessionWeb-onlyintentional differencesrc/App.ts:1389-1394

Panels, map layers, variants

CapabilityDesktop behavior vs webClassificationEvidence
Map layer catalogIdentical shared registry; no desktop gating of outages/cyberThreats entries (see #5829 diagnosis for why they still don't show)parity in code; blocked in shipped buildssrc/config/map-layer-definitions.ts:80-81
Desktop-locked panelsforecast, oref-sirens, telegram-intel locked; cii, strategic-risk, gdelt-intel, supply-chain downgraded to enhancedintentional difference (documented here as the fallback statement)src/config/panels.ts:32-36,64,118-119
Layers not loaded on desktopAIS, Iran attacks, GPS jamming (premium/relay-dependent); military flights uses direct OpenSky w/ 15-min cache instead of proto RPCintentional differencesrc/app/data-loader.ts:1050,1055,3138; src/services/military-flights.ts:63-72
Variant selectionWeb: hostname. Desktop: localStorage override with in-app switching (reload in place), falling back to build-time VITE_VARIANTintentional differencesrc/config/variant.ts:20-38; src/app/event-handlers.ts:1594-1598
Variant switcher scopeFixed by #5908: the supported model is one published binary that switches all six variants in-app, so no variant needs its own artifact. SITE_VARIANTS is the single source of truth, and /api/download accepts exactly that set (plus the world alias), pinned by tests/desktop-one-binary-model.test.mjs. Historical state: the switcher accepted commodity/energy while the updater knew only full/tech/finance and packaging stopped at full/techparitysrc/config/variant.ts:7; api/download.js:26-34; tests/desktop-one-binary-model.test.mjs
Runtime-config panelForce-enabled on desktop boot; hidden on webintentional differencesrc/App.ts:881-892; src/settings-window.ts:63
Runtime detectionGAP (cosmetic/dev): two detectors disagree — isDesktopRuntime() (VITE flag + broad heuristics) vs raw __TAURI__ checks in 6 files; split-brain under desktop:dev early boot and VITE_DESKTOP_RUNTIME=1 browser buildsblocked (minor) → #5912src/services/runtime.ts:72-113; src/config/variant.ts:20

Auth, billing, entitlements

CapabilityDesktop behavior vs webClassificationEvidence
Clerk sign-inNo desktop gating in code; CSP allowlists Clerk — but dead in shipped builds (missing VITE_CLERK_PUBLISHABLE_KEY)blocked → #5905src/services/clerk.ts:30-49,233-236; src-tauri/tauri.conf.json:32
Convex entitlementsSame pattern: VITE_CONVEX_URL missing from the desktop build (workflow passes non-VITE_ CONVEX_URL, which never reaches the client)blocked → #5905src/services/entitlements.ts:96-99; vite.config.ts (no define)
Premium accessWORLDMONITOR_API_KEY from keyring is the only working Pro path in shipped builds; sidecar attaches the key natively (renderer skip is by design)parity for key users; blocked for subscriberssrc/services/panel-gating.ts:53-58; src/services/premium-fetch.ts:223-237
Billing portal / checkoutEvery billing, checkout and upgrade exit routes through openExternalUrl, which hands the URL to the OS browser via open_url on desktop (5s timeout, Sentry-reported failure, scheme-checked) and reports whether the handoff actually happened, so a failed open surfaces as a checkout error instead of a false "check your browser". The return URL is built from the canonical web origin; the app unlocks over the live Convex entitlement watch with no redirect back in. openBillingPortal reports open-failed rather than opened when nothing opened, and the checkout toast names the OS browser only when the native handoff actually succeeded. All seven remaining renderer call sites were migrated (#6120). Residual: the returning browser cannot acknowledge the purchase (its handleCheckoutReturn needs a session-local attempt record the app holds) → #6121; a plain-http:// external link still cannot leave the app, because the native allowlist is https-only → tracked separatelyintentional difference (desktop pays in the browser) — #5911src/services/external-navigation.ts; src/services/checkout-return-url.ts:resolveCheckoutReturnOrigin; src/services/checkout.ts (navigateToWebSurface + hosted-checkout branch)
Settings → Plan & billing tabGAP: the tab is not rendered at all on desktop — isSignedIn is hardcoded to !isDesktopApp, so renderUpgradeSection / handleUpgradeClick are unreachable there regardless of Clerk. Their desktop branches are correct (#5911) but currently deadblocked → #6108src/components/UnifiedSettings.ts:644,656
#5901 unified user menuNo desktop-specific handling; hosts the billing surface above; menu itself requires Clerk (so absent from shipped builds until the env fix lands)inherits the two rows abovesrc/app/event-handlers.ts:1963 (commit 53181fb71)
Locked-panel affordancesisPanelEntitled returns entitled for premium:'locked' on any desktop runtime; real gates use hasPremiumAccess() so leak is cosmetic (settings picker / CMD+K / analyst offer panels that render locked)intentional-ish, cosmetic; noted for the desktop UX passsrc/config/panels.ts:1243-1245; src/app/panel-layout.ts:2150

Settings & secrets

CapabilityDesktop behavior vs webClassificationEvidence
Secret storageOS keyring via consolidated secrets-vault JSON entry (one prompt), 29 allowlisted keys, plaintext never returns to renderer; web: no-opintentional difference (desktop richer)src-tauri/src/main.rs:38-68,110-161; src/services/runtime-config.ts:456-460,528-548
Linux keyringblocked — #802/#1905 (see blockers)blockedmain.rs:110-161 silent if let Ok
Feature availabilityDesktop requires every declared secret valid locally (25 runtime features); web defers to serverintentional differencesrc/services/runtime-config.ts:431-444
Dedicated settings windowDesktop-only settings.html window with cross-window wm-secrets-updated syncintentional differencesrc/settings-main.ts; runtime-config.ts:394,474
Local LLM / OllamaDesktop-first: ML worker always on, Insights hardcodes cloud+browser modelsintentional differencesrc/App.ts:1323-1324; src/components/InsightsPanel.ts:352
Cloud prefs syncFully disabled on desktopintentional difference — local settings stay localsrc/utils/cloud-prefs-sync.ts:185-187
CapabilityDesktop behavior vs webClassificationEvidence
External linksCapture-phase interceptor rewrites cross-origin anchors to open_url (https-only + localhost http); GHSA-2x6r-safe openerintentional difference (trusted path); covered by src-tauri/open-url-safety.test.mjssrc/app/event-handlers.ts:724-750; src-tauri/src/main.rs:732-745
YouTube embedsLocal sidecar embed bridge (/api/youtube-embed) to dodge tauri:// origin error 153, runtime fallback to cloud bridge, dedicated login window for auth-walled streamsintentional differencesrc/components/LiveNewsPanel.ts:389-394,1589-1594,1688-1692
Fullscreen buttonNot wired on desktopintentional difference (minor); document in UX passsrc/app/event-handlers.ts:669

Updates, notifications, PWA

CapabilityDesktop behavior vs webClassificationEvidence
Update discoveryCustom 6-hourly poll of api.worldmonitor.app/api/version (no Tauri updater plugin, no signed update artifacts); per-arch download via /api/downloadintentional mechanism; correctness fixed by #5908 — one release line, so /releases/latest is the right read, and isNewerDesktopVersion reports an unparseable version as version_unparsable instead of NaN-collapsing it to a silent no_updatesrc/app/desktop-updater.ts:25,66-95; src/utils/desktop-version.ts; api/version.js:13; api/download.js
Web push / service workerIntentionally desktop-excluded (Tauri check) and doubly so via missing VITE_VAPID_PUBLIC_KEYintentional difference — fallback: in-app alertssrc/services/push-notifications.ts:42; src/main.ts:496
Breaking-news alertsRun on desktop; posted via raw XHR to bypass the fetch interceptorparitysrc/services/breaking-news-alerts.ts:214-228
Stale-bundle checkWeb-only (desktop has the updater instead)intentional differencesrc/main.ts:405
Persistent cacheTauri file storage instead of IndexedDB, with browser fallback; 1 KiB key / 5 MiB value bounds enforced in Rustintentional differencesrc/services/persistent-cache.ts:123-219; src-tauri/src/cache_bounds.rs

Release/packaging drift register

Condensed from the release-infra audit; each row feeds a child issue or the release-candidate checklist:

  1. Variant release model structurally brokenresolved by #5908. The supported model is now one published World Monitor binary with in-app variant switching, so the endpoints' single-release read is correct by design. The workflow keeps one build-leg pair and one tag (v__VERSION__), the AppImage re-upload and release-notes step can no longer target different tags, workflow_dispatch defaults to draft: false so a dispatched build is actually served, and the unbuildable tech/finance packaging surface is gone. tests/desktop-one-binary-model.test.mjs fails if any surface drifts back.
  2. Bundled Node 22.14.0 vs CI Node 24 everywhere; SHASUMS fetched without GPG verification. → #5909.
  3. README/docs drift: "Stable" label, missing Linux ARM64 row, windows-exe badges vs windows-msi in-app, wrong updater host/TTL in docs/desktop-app.mdx, phantom 50 MB cap in docs/usage-rate-limits.mdx, api/api-route-exceptions.json misdescribing api/fwdstart.js, no Linux packaging doc. → #5910.
  4. macOS updater artifacts (.app.tar.gz) ship without .sig — no signed update chain. Documented as unsigned fallback; revisit with the release train. → tracked in #5902 §3.

Drift prevention (current state)

  • desktop-config (Test workflow): version consistency, release AppImage post-processing script syntax, and Tauri config/capability JSON parse — on any src-tauri/**, version-file, packaging-script, or desktop-workflow change.
  • Sidecar handler bundle build + per-domain output assertion (Test workflow, unit job): runs on every code PR, because the bundled handlers' esbuild import graph spans src/ and server/ via the @/ alias — a narrower path filter would miss bundle-breaking changes.
  • desktop-rust (Test workflow): cargo test --locked (the previously never-in-CI inline tests in main.rs / cache_bounds.rs) on Rust-affecting changes.
  • Desktop Canary (Linux): twice-weekly scheduled installed-app run from current main — hard-fails on crashed app, unreachable sidecar (127.0.0.1:46123 probe), or blank render; screenshots/logs always uploaded.
  • Cross-platform artifact builds + smoke remain on release candidates (build-desktop.yml).
  • Desktop build-env parity (scripts/check-desktop-build-env.mjs): discovers every Tauri workflow, scans syntax-aware VITE_* property reads, and runs in both the workflow-change and source-change CI legs; a route-table contract test asserting every server/worldmonitor/ domain has an explicit desktop-path decision remains a future candidate.