Moves isProviderAvailable() check from before cachedFetchJson() to inside
the fetcher callback. This ensures cache hits still serve valid data during
provider outages instead of returning empty results.
Changes:
- classify-event: health gate moved inside cachedFetchJson callback
- deduct-situation: same
- get-country-intel-brief: same
- summarize-article: same
- _batch-classify: break → return results on health gate failure
- callLlm (llm.ts): health gate added to provider chain
- local-api-server: /api/llm-health endpoint + startup warmup
Scope cleanup per review:
- Reverted LlmStatusIndicator (extracted to #1528)
- Reverted ACLED credential cleanup (extracted to #1530)
- Reverted isSidecar → isLocalDeployment rename (extracted to #1532)
Co-authored-by: Elie Habib <elie.habib@gmail.com>
ACLED migrated to token-based auth (ACLED_ACCESS_TOKEN). The email/password
OAuth flow is no longer used. Remove the dead validation cases and drop
both keys from ALLOWED_ENV_KEYS.
Extracted from PR #1522 (scope split).
Co-authored-by: Jon Torrez <jrtorrez31337@users.noreply.github.com>
* fix(csp): add commodity variant to CSP and fix iframe variant navigation
- Add commodity.worldmonitor.app to frame-src and frame-ancestors in
vercel.json and index.html CSP — was missing while all other variants
were listed
- Open variant links in new tab when app runs inside an iframe to prevent
sandbox navigation errors ("This content is blocked")
- Add allow-popups and allow-popups-to-escape-sandbox to pro page iframe
sandbox attribute
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(csp): add missing variant subdomains to tauri.conf.json frame-src
Sync tauri.conf.json CSP with index.html and vercel.json by adding
finance, commodity, and happy worldmonitor.app subdomains to frame-src.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add PR screenshots for CSP fix
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Elie Habib <elie.habib@gmail.com>
* feat(cache): key market quote breakers by symbol set
* feat(cache): key market quote breakers by symbol set
* feat(cache): key market quote breakers by symbol set
---------
Co-authored-by: Elie Habib <elie.habib@gmail.com>
* fix(acled): add OAuth token manager with automatic refresh
ACLED access tokens expire every 24 hours, but WorldMonitor stores a
static ACLED_ACCESS_TOKEN with no refresh logic — causing all ACLED
API calls to fail after the first day.
This commit adds `acled-auth.ts`, an OAuth token manager that:
- Exchanges ACLED_EMAIL + ACLED_PASSWORD for an access token (24h)
and refresh token (14d) via the official ACLED OAuth endpoint
- Caches tokens in memory and auto-refreshes before expiry
- Falls back to static ACLED_ACCESS_TOKEN for backward compatibility
- Deduplicates concurrent refresh attempts
- Degrades gracefully when no credentials are configured
The only change to the existing `acled.ts` is replacing the synchronous
`process.env.ACLED_ACCESS_TOKEN` read with an async call to the new
`getAcledAccessToken()` helper.
Fixes#1283
Relates to #290
* fix: address review feedback on ACLED OAuth PR
- Use Redis (Upstash) as L2 token cache to survive Vercel Edge cold starts
(in-memory cache retained as fast-path L1)
- Add CHROME_UA User-Agent header on OAuth token exchange and refresh
- Update seed script to use OAuth flow via getAcledToken() helper
instead of raw process.env.ACLED_ACCESS_TOKEN
- Add security comment to .env.example about plaintext password trade-offs
- Sidecar ACLED_ACCESS_TOKEN case is a validation probe (tests user-provided
value, not process.env) — data fetching delegates to handler modules
* feat(sidecar): add ACLED_EMAIL/ACLED_PASSWORD to env allowlist and validation
- Add ACLED_EMAIL and ACLED_PASSWORD to ALLOWED_ENV_KEYS set
- Add ACLED_EMAIL validation case (store-only, verified with password)
- Add ACLED_PASSWORD validation case with OAuth token exchange via
acleddata.com/api/acled/user/login
- On successful login, store obtained OAuth token in ACLED_ACCESS_TOKEN
- Follows existing validation patterns (Cloudflare challenge handling,
auth failure detection, User-Agent header)
* fix: address remaining review feedback (duplicate OAuth, em dashes, emoji)
- Extract shared ACLED OAuth helper into scripts/shared/acled-oauth.mjs
- Remove ~55 lines of duplicate OAuth logic from seed-unrest-events.mjs,
now imports getAcledToken from the shared helper
- Replace em dashes with ASCII dashes in acled-auth.ts section comments
- Replace em dash with parentheses in sidecar validation message
- Remove emoji from .env.example security note
Addresses koala73's second review: MEDIUM (duplicate OAuth), LOW (em
dashes), LOW (emoji).
* fix: align sidecar OAuth endpoint, fix L1/L2 cache, cleanup artifacts
- Sidecar: switch from /api/acled/user/login (JSON) to /oauth/token
(URL-encoded) to match server/_shared/acled-auth.ts exactly
- acled-auth.ts: check L2 Redis when L1 is expired, not only when L1
is null (fixes stale L1 skipping fresher L2 from another isolate)
- acled-oauth.mjs: remove stray backslash on line 9
- seed-unrest-events.mjs: remove extra blank line at line 13
---------
Co-authored-by: Elie Habib <elie.habib@gmail.com>
Co-authored-by: RepairYourTech <30200484+RepairYourTech@users.noreply.github.com>
* feat(desktop): compile domain handlers + add in-memory sidecar cache
The sidecar was broken for all 23 sebuf/RPC domain routes because
the build script (build-sidecar-handlers.mjs) never existed on main
while package.json already referenced it. This adds the missing script
and an in-memory TTL+LRU cache so the sidecar doesn't need Upstash Redis.
- Add scripts/build-sidecar-handlers.mjs (esbuild multi-entry, 23 domains)
- Add server/_shared/sidecar-cache.ts (500 entries, 50MB max, lazy sweep)
- Modify redis.ts getCachedJson/setCachedJson to use dynamic import for
sidecar cache when LOCAL_API_MODE=tauri-sidecar (zero cost on Vercel Edge)
- Update tauri.conf.json beforeDevCommand to compile handlers
- Add gitignore pattern for compiled api/*/v1/[rpc].js
* fix(desktop): gate premium panel fetches and open footer links in browser
Skip oref-sirens and telegram-intel HTTP requests on desktop when
WORLDMONITOR_API_KEY is not present. Use absolute URLs for footer
links on desktop so the Tauri external link handler opens them in
the system browser instead of navigating within the webview.
* fix(desktop): cloud proxy, bootstrap timeouts, and panel data fixes
- Set Origin header on cloud proxy requests (fixes 401 from API key validator)
- Strip If-None-Match/If-Modified-Since headers (fixes stale 304 responses)
- Add cloud-preferred routing for market/economic/news/infrastructure/research
- Enable cloud fallback via LOCAL_API_CLOUD_FALLBACK env var in main.rs
- Increase bootstrap timeouts on desktop (8s/12s vs 3s/5s) for sidecar proxy hops
- Force per-feed RSS fallback on desktop (server digest has fewer categories)
- Add finance feeds to commodity variant (client + server)
- Remove desktop diagnostics from ServiceStatusPanel (show cloud statuses only)
- Restore DeductionPanel CSS from PR #1162
- Deduplicate repeated sidecar error logs
Reverts commit 04af5ea8 which switched web webcam embeds back to
youtube-nocookie.com and restored sandbox. The nocookie domain triggers
YouTube's "Sign in to confirm you're not a bot" prompt, breaking all
live webcam feeds on the web app.
Changes:
- Web embeds: youtube-nocookie.com -> youtube.com (sends session cookies)
- Remove iframe sandbox attribute (allows storage-access to work)
- Add storage-access to iframe allow attribute
- Sidecar: restore autoplay-based MutationObserver gate
* fix: resolve YouTube 'sign in to confirm' bot-check in embed panels
YouTube was showing a bot-verification prompt in the LiveWebcamsPanel
and LiveNewsPanel despite the user being logged into YouTube in the
same browser session.
LiveWebcamsPanel (primary fix):
- Changed embed domain from youtube-nocookie.com to youtube.com.
The nocookie domain deliberately strips all cookies, so YouTube
can never verify a signed-in session.
- Removed sandbox attribute which blocked the Storage Access API
(allow-storage-access-by-user-activation was missing).
- Added storage-access to iframe allow attribute.
LiveNewsPanel:
- renderDesktopEmbed now passes origin and parentOrigin query params
so postMessage is not silently dropped by the embed.
- Added storage-access to iframe allow attribute.
- Fixed MutationObserver target: was watching this.playerElement but
YT.Player(domElement) replaces that div in its parent, so the
observer never fired. Now observes playerContainer with a YouTube
iframe filter, and YT.Player receives the element ID string so the
iframe is inserted as a child of the div instead.
local-api-server.mjs (youtube-embed handler):
- MutationObserver patches inner YouTube iframe with storage-access.
- Added Permissions-Policy: storage-access=* response header.
- Embed page calls document.requestStorageAccess() on load.
api/youtube/embed.js (Vercel/edge path):
- Added tauri://localhost to ALLOWED_PARENT_ORIGINS.
- Added Permissions-Policy: storage-access=* response header.
- Embed page calls document.requestStorageAccess() on load.
* fix(pr-review): address review feedback on YouTube Storage Access API changes
- LiveWebcamsPanel: tested allow-storage-access-by-user-activation sandbox token
as suggested; reverted — Chrome silently blocks Storage Access API even with
the token present. Documented why sandbox removal is the only working approach.
- LiveWebcamsPanel: added comment documenting youtube-nocookie→youtube.com
privacy trade-off as intentional.
- LiveNewsPanel: wrap YT.Player constructor in try/catch to disconnect
storageObserver on error; add 10 s auto-disconnect timeout to prevent leaks.
- embed.js + local-api-server.mjs: scope permissions-policy storage-access to
self + youtube.com rather than *.
- embed.js + local-api-server.mjs: add gesture-gated requestStorageAccess()
fallback on first user interaction.
- embed.js: remove duplicate tauri://localhost from ALLOWED_PARENT_ORIGINS
(already covered via ALLOWED_ORIGINS spread).
* fix(review): gate sidecar patch on storage-access, revert web webcam path
1. Sidecar MutationObserver: gate iframe patch on storage-access absence
instead of autoplay absence. If YouTube ships iframes with autoplay
already present, the old check would skip adding storage-access entirely.
2. Web webcam path: revert to youtube-nocookie.com and restore sandbox.
The raw YouTube iframe cannot call requestStorageAccess() (no controlled
bridge document), so switching to youtube.com only regressed privacy
and sandbox security without actually fixing the bot-check.
---------
Co-authored-by: Elie Habib <elie.habib@gmail.com>
Since e14af08f (#709), the sidecar strips the browser Origin header but
immediately replaces it with `http://127.0.0.1:<port>` (line 1321 of
local-api-server.mjs). This ensures local handlers receive a valid
Origin for CORS while preventing browser-supplied origins from leaking
into server-to-server calls.
The test was written before that commit and still asserted
`originPresent: false`. Update the test to:
- Assert `originPresent: true` (a localhost origin IS present)
- Assert `originValue` equals `http://127.0.0.1:<port>` (verify it's
the replaced localhost origin, not the browser's)
- Rename the test to describe the actual behavior
npm run test:sidecar: 55/55 pass ✅
* Add premium finance stock analysis suite
* docs: link premium finance from README
Add Premium Stock Analysis entry to the Finance & Markets section
with a link to docs/PREMIUM_FINANCE.md.
* fix: address review feedback on premium finance suite
- Chunk Redis pipelines into batches of 200 (Upstash limit)
- Add try-catch around cachedFetchJson in backtest handler
- Log warnings on Redis pipeline HTTP failures
- Include name in analyze-stock cache key to avoid collisions
- Change analyze-stock and backtest-stock gateway cache to 'slow'
- Add dedup guard for concurrent ledger generation
- Add SerpAPI date pre-filter (tbs=qdr:d/w)
- Extract sanitizeSymbol to shared module
- Extract buildEmptyAnalysisResponse helper
- Fix RSI to use Wilder's smoothing (matches TradingView)
- Add console.warn for daily brief summarization errors
- Fall back to stale data in loadStockBacktest on error
- Make daily-market-brief premium on all platforms
- Use word boundaries for short token headline matching
- Add stock-analysis 15-min refresh interval
- Stagger stock-analysis and backtest requests (200ms)
- Rename signalTone to stockSignalTone
* feat: premium panel gating, code cleanup, and backend simplifications
Recovered stranded changes from fix/desktop-premium-error-unification.
Premium gating:
- Add premium field ('locked'|'enhanced') to PanelConfig and LayerDefinition
- Panel.showLocked() with lock icon, CTA button, and _locked guard
- PRO badge for enhanced panels when no WM API key
- Exponential backoff auto-retry on showError() (15s→30s→60s→180s cap)
- Gate oref-sirens and telegram-intel panels behind WM API key
- Lock gpsJamming and iranAttacks layer toggles, badge ciiChoropleth
- Add tauri-titlebar drag region for custom titlebar
Code cleanup:
- Extract inline CSS from AirlineIntelPanel, WorldClockPanel to panels.css
- Remove unused showGeoError() from CountryBriefPage
- Remove dead geocodeFailed/retryBtn/closeBtn locale keys (20 files)
- Clean up var names and inline styles across 6 components
Backend:
- Remove seed-meta throttle from redis.ts (unnecessary complexity)
- Risk scores: call handler functions directly instead of raw Redis reads
- Update OpenRouter model to gpt-oss-safeguard-20b:nitro
- Add direct UCDP API fetching with version probing
Config:
- Remove titleBarStyle: Overlay from tauri.conf.json
- Add build:pro and build-sidecar-handlers to build:desktop
- Remove DXB/RUH from default aviation watchlist
- Simplify reverse-geocode (remove AbortController wrapper)
* fix: cast handler requests to any for API tsconfig compat
* fix: revert stale changes that conflict with merged PRs
Reverts files to main versions where old branch changes would
overwrite intentional fixes from PRs #1134, #1138, #1144, #1154:
- news/_shared.ts: keep gemini-2.5-flash model (not stale gpt-oss)
- redis.ts: keep seed-meta throttle from PR #1138
- reverse-geocode.ts: keep AbortController timeout from PR #1134
- CountryBriefPage.ts: keep showGeoError() from PR #1134
- country-intel.ts: keep showGeoError usage from PR #1134
- get-risk-scores.ts: revert non-existent imports
- watchlist.ts: keep DXB/RUH airports from PR #1144
- locales: restore geocodeFailed/retryBtn/closeBtn keys
* fix: neutralize language, parallel override loading, fetch timeout
- Rename conflict zone from "War" to "Border Conflict", intensity high→medium
- Rewrite description to factual language (no "open war" claim)
- Load country boundary overrides in parallel with main GeoJSON
- Neutralize comments/docs: reference Natural Earth source, remove political terms
- Add 60s timeout to Natural Earth fetch script (~24MB download)
- Add trailing newline to GeoJSON override file
* fix: restore caller messages in Panel errors and vessel expansion in popups
- Move UCDP direct-fetch cooldown after successful fetch to avoid
suppressing all data for 10 minutes on a single failure
- Use caller-provided messages in showError/showRetrying instead of
discarding them; respect autoRetrySeconds parameter
- Restore cluster-toggle click handler and expandable vessel list
in military cluster popups
* fix(desktop): address code review findings — DRY debounce, error handling, retry cap
- Extract duplicated flush-scheduling into schedule_debounced_flush() helper
- Drop flush_scheduled lock before spawning thread to narrow lock scope
- Add .catch() to lazyPanel() for silent import failure visibility
- Convert happy-variant panels to use lazyPanel() helper (consistency + error handling)
- Cap flush retries at 5 to prevent infinite loop on persistent disk errors
- Only clear sidecar caches when at least one batch entry succeeded
- Log batch fallback error for debugging
* fix: remove unsafe type casts in happy-variant lazy panels
Move ctx property assignments into the loader callback where the
concrete type is known, eliminating all `as unknown as` double casts.
- Rust PersistentCache: generation-counter debounce (2s coalesce) + atomic
flush via temp file + rename to prevent corruption on crash
- Sidecar: add /api/local-env-update-batch endpoint; loadDesktopSecrets()
now pushes all secrets in 1 request instead of 23, with single-endpoint
fallback for older sidecars
- App startup: waitForSidecarReady() polls service-status before bootstrap
fetch so sidecar port-file races no longer cause silent fallback
- Lazy panel instantiation: 16 niche/variant panels converted to dynamic
import().then() — disabled panels cost zero at cold boot
- pauseWhenHidden: true on RefreshScheduler, OREF alerts, and Gulf
Economies poll loops — zero background network when app is hidden
- Remove emrldco.com analytics script and CSP entries from index.html,
vercel.json, and tauri.conf.json
- Replace setStyle() basemap fallback with full map recreation — setStyle()
after a failed initial style load leaves MapLibre in a broken state
- Add 403/Forbidden to error detection patterns for basemap failures
- Scope fallback to pre-style-load errors only (post-load tile errors
don't warrant destroying a working map)
Fixes#903. YouTube live detection from the desktop sidecar now proxies directly to cloud via tryCloudFallback(), bypassing the cloudFallback flag (off by default). Matches the existing register-interest pattern.
explorer.exe treats URLs as file paths, opening a file dialog.
'cmd /c start' properly delegates URLs to the default browser.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(sidecar): add required params to ACLED API key validation probe
The validation endpoint was calling ACLED without event_type, event_date,
or event_date_where parameters. The production code in acled.ts always
passes these — ACLED may reject requests missing them, causing valid
tokens to fail validation.
Add Protests event type and a 7-day date range to match production usage.
Fixes#290.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(military): harden USNI fleet report ship name regex
The ship extraction regex only matched <em> and <i> tags. If USNI
changes HTML to use <strong>, <b>, <span>, or plain text, all ship
parsing silently fails.
Broaden the regex to handle any inline HTML tag or no tag at all.
Add console.warn when a strike group section yields zero ships to
aid debugging when HTML format changes.
Addresses #197 (L-12).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat(conflict): wire UCDP API access token across full stack
UCDP API now requires an `x-ucdp-access-token` header. Renames the
stub `UC_DP_KEY` to `UCDP_ACCESS_TOKEN` (matching ACLED convention)
and wires it through Rust keychain, sidecar allowlist + verification,
handler fetch headers, feature toggles, and desktop settings UI.
- Rename UC_DP_KEY → UCDP_ACCESS_TOKEN in type system and labels
- Add ucdpConflicts feature toggle with required secret
- Add UCDP_ACCESS_TOKEN to Rust SUPPORTED_SECRET_KEYS (24→25)
- Add sidecar ALLOWED_ENV_KEYS entry + validation with dynamic GED version probing
- Handler sends x-ucdp-access-token header when token is present
- UC_DP_KEY fallback in handler for one-release migration window
- Update .env.example, desktop-readiness, and docs
* feat(conflict): pre-fetch UCDP events via Railway cron + Redis cache
Replace the 228-line edge handler that fetched UCDP GED API on every
request with a thin Redis reader. The heavy fetch logic (version
discovery, paginated backward fetch, 1-year trailing window filter)
now runs as a setInterval loop in the Railway relay (ais-relay.cjs)
every 6 hours, writing to Redis key conflict:ucdp-events:v1.
Changes:
- Add UCDP seed loop to ais-relay.cjs (6h interval, 6 pages, 2K cap)
- Rewrite list-ucdp-events.ts as thin Redis reader (35 lines)
- Add conflict:ucdp-events:v1 to bootstrap batch keys
- Protect key from cache-purge via durable data prefix
- Add manual-only seed-ucdp-events workflow + standalone script
- Rename panel "UCDP Events" → "Armed Conflict Events" in locale
- Add 24h TTL + 25h staleness check as safety nets
API keys in URL query strings can leak via server logs, proxy logs,
Referer headers, and error reporting tools. Finnhub supports both
authentication methods — this moves to the header-based approach.
Addresses #197 (L-16).
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
CSP media-src only allowed https: — blocked <video> from loading HLS
streams through the sidecar proxy at http://127.0.0.1:PORT. Direct HLS
channels (Sky, DW, Fox) use https:// CDN URLs and worked; proxied
channels (CNBC, CNN) were silently blocked, falling back to YouTube.
Also remove CNN from PROXIED_HLS_MAP — the upstream stream is wrong.
- Sidecar 401 fix: inject trusted localhost Origin on requests passed to
handler modules. The handler's validateApiKey() was seeing empty Origin
(stripped by toHeaders) + no API key → 401 for ALL desktop API calls.
- Variant fix: check localStorage FIRST when running in Tauri desktop,
so .env.local VITE_VARIANT doesn't override user's variant selection.
- Registration: force-show form for email delivery testing.
- Bump version to 2.5.23.
* feat(live-news): add CNN & CNBC HLS streams via sidecar proxy (desktop only)
Add /api/hls-proxy route to sidecar that proxies HLS manifests and
segments from allowlisted CDN hosts, injecting the required Referer
header that browsers cannot set. Rewrites m3u8 URLs so all segments
and encryption keys also route through the proxy.
Desktop gets native <video> HLS playback for CNN and CNBC; web falls
through to YouTube as before (no bandwidth cost on Vercel).
* fix(types): add missing @types/dompurify dev dependency
- Remove PostHog analytics runtime and configuration
- Add API rate limiting (api/_rate-limit.js)
- Harden traffic controls across edge functions
- Add runtime fallback controls and data-loader improvements
- Add military base data scripts (fetch-mirta-bases, fetch-osm-bases)
- Gitignore large raw data files
- Settings playground prototypes
* fix(desktop): route register-interest to cloud when sidecar lacks CONVEX_URL
The waitlist registration endpoint needs Convex (cloud-only dependency).
The sidecar handler returned 503 without cloud fallback, and
getRemoteApiBaseUrl() returned '' on desktop (VITE_WS_API_URL unset),
so the settings window fetch resolved to tauri://localhost → 404.
Three-layer fix:
1. Sidecar: tryCloudFallback() when CONVEX_URL missing (proxies to
https://worldmonitor.app via remoteBase)
2. runtime.ts: getRemoteApiBaseUrl() defaults to https://worldmonitor.app
on desktop when VITE_WS_API_URL is unset
3. CI: add VITE_WS_API_URL=https://worldmonitor.app to all 4 desktop
build steps
* chore(deps): bump posthog-js to fix pre-push typecheck
Three bugs combine to burn 130% CPU when sidecar auth fails:
1. RefreshScheduler resets backoff multiplier to 1 (fastest) on error,
causing failed endpoints to poll at base interval instead of backing off.
Fix: exponential backoff on errors, same as unchanged-data path.
2. classify-event batch system ignores 401 (auth failure) — only pauses
on 429/5xx. Hundreds of classify calls fire every 2s, each wasted.
Fix: pause 120s on 401, matching the 429/5xx pattern.
3. Fetch patch retries every 401 (refresh token + retry), doubling all
requests to the sidecar even when token refresh consistently fails.
Fix: 60s cooldown after a retry-401 still returns 401.
Also shrinks settings window from 760→600px (min 620→480) to reduce
the empty whitespace below content on all tabs.
Both keys were added to Rust SUPPORTED_SECRET_KEYS and runtime-config.ts
but the sidecar's own ALLOWED_ENV_KEYS was never updated. This caused
"key not in allowlist" 403 when saving/verifying these keys from the
desktop settings UI.
Also adds AviationStack API validation in validateSecretAgainstProvider.
* feat(aviation): add NOTAM closure detection via ICAO API
Adds international airport closure detection via ICAO NOTAMs:
- New fetchNotamClosures() queries ICAO realtime-notams endpoint
- Detects closures via Q-codes (FA/AH/AL/AW/AC/AM) and text patterns
- Batches airports in groups of 20 per API call
- 4-hour cache TTL via cachedFetchJson (stampede-safe)
- NOTAM closures override existing AviationStack alerts for same airport
- Graceful: no ICAO_API_KEY env var = silently skipped
To activate: set ICAO_API_KEY env var (register at dataservices.icao.int)
* feat(settings): add ICAO_API_KEY to desktop app settings
Adds ICAO NOTAM API key to the desktop settings UI:
- Rust: SUPPORTED_SECRET_KEYS [23→24]
- TypeScript: RuntimeSecretKey + RuntimeFeatureId unions
- Feature definition: 'icaoNotams' in Tracking & Sensing category
- Settings UI: label, signup URL, analytics name
* feat(aviation): limit NOTAM queries to MENA airports only
Per user request, ICAO NOTAM closure detection is scoped to
Middle East airports only (region='mena', ~35 airports).
This reduces API calls (2 batches vs 5) and focuses on the
region where closures are most relevant.
* fix(aviation): align NOTAM cache TTL to 30 min (matching FAA/intl)
Register the key in Rust keychain (SUPPORTED_SECRET_KEYS), frontend
RuntimeSecretKey type, feature toggle, and settings UI under
"Tracking & Sensing" category.
* fix(tech): use rss() for CISA feed, drop build from pre-push hook
- CISA Advisories used dead rss.worldmonitor.app domain (404), switch to rss() helper
- Remove Vite build from pre-push hook (tsc already catches errors)
* fix(desktop): enable click-to-play for YouTube embeds in WKWebView
WKWebView blocks programmatic autoplay in cross-origin iframes regardless
of allow attributes, Permissions-Policy, mute-first retries, or secure
context. Documented all 10 approaches tested in docs/internal/.
Changes:
- Switch sidecar embed origin from 127.0.0.1 to localhost (secure context)
- Add MutationObserver + retry chain as best-effort autoplay attempts
- Use postMessage('*') to fix tauri://localhost cross-origin messaging
- Make sidecar play overlay non-interactive (pointer-events:none)
- Fix .webcam-iframe pointer-events:none blocking clicks in grid view
- Add expand button to grid cells for switching to single view on desktop
- Add http://localhost:* to CSP frame-src in index.html and tauri.conf.json
* fix(feeds): replace 25 dead/stale RSS URLs and add feed validation script
- Replace 16 dead feeds (404/403/timeout) with working alternatives
(Google News proxies or corrected direct RSS endpoints)
- Replace 6 empty feeds with correct RSS paths (VnExpress, Tuoi Tre,
Live Science, Greater Good, News24, ScienceDaily)
- Replace 3 stale feeds (CNN World, TVN24, Layoffs.fyi) with active sources
- Remove Disrupt Africa (inactive since Jan 2024)
- Add scripts/validate-rss-feeds.mjs to check all 420 feeds
- Add test:feeds npm script
* feat(live-news): use stable CDN HLS feeds for desktop native playback
Direct HLS feeds bypass YouTube's expiring tokenized URLs and iframe
cookie issues on WKWebView. 10 channels (Sky, DW, France24, Euronews,
Al Arabiya, Al Jazeera, CBS News, TRT World, Sky News Arabia, Al Hadath)
now play via native <video> on desktop with automatic YouTube fallback
when CDN feeds are down (5-min cooldown).
Also:
- Fix euronews handle typo (@euabortnews → @euronews)
- Fix TRT World handle (@taborrtworld → @TRTWorld)
- Add fallbackVideoId to CBS News, Sky News Arabia, TRT World
- Extract hlsManifestUrl from YouTube API for non-mapped channels
- Add sidecar /api/youtube-embed endpoint (auth-exempt for iframes)
- Switch webcam/embed iframes from cloud to local sidecar origin
- CSP: allow frame-src http://127.0.0.1:* for sidecar embeds
- Remove legacy WEBKIT_FORCE_SANDBOX env var (deprecated in WebKitGTK)
- Add 37 tests covering HLS map integrity, decision tree ordering,
cooldown logic, race safety, service layer, sidecar endpoint, and CSP
* fix(summarization): pass panelId as geoContext to prevent Redis cache key collision
When breaking news appeared across multiple panels (World, US, Europe,
Middle East), all panels generated identical cache keys because geoContext
was always undefined. The first panel's summary was served to all others.
* fix(desktop): sidecar embed autoplay, webcam fullscreen, optional channel fallbacks
- Sidecar YouTube embed: use mute param (not hardcoded), add play overlay
for WKWebView autoplay fallback, add postMessage bridge for play/pause/
mute/unmute commands matching the cloud embed handler
- Webcam iframes: only set allowFullscreen on web to prevent grid-breaking
fullscreen on desktop click
- Optional channels: add fallbackVideoId + useFallbackOnly for livenow-fox,
abc-news, nbc-news, wion so they play instead of showing "not currently live"
- Tests: 9 new assertions covering mute param, postMessage bridge, play
overlay, yt-ready message, and optional channel fallback coverage (46 total)
Linux users with NVIDIA proprietary drivers on Wayland report crashes:
"Could not create surfaceless EGL display: EGL_BAD_ALLOC. Aborting..."
WebKitGTK's web process calls eglGetPlatformDisplay with the
EGL_PLATFORM_SURFACELESS_MESA platform, which fails with NVIDIA's EGL
implementation and triggers abort(). WEBKIT_DISABLE_DMABUF_RENDERER=1
(already set) only controls buffer sharing, not EGL initialization.
Detect NVIDIA via /proc/driver/nvidia and:
- Set __NV_DISABLE_EXPLICIT_SYNC=1 to prevent Wayland flickering
- Force GDK_BACKEND=x11 on NVIDIA+Wayland (user can override)
Also bumps version to 2.5.19.
Refs: tauri-apps/tauri#9394, gitbutlerapp/gitbutler#5282
The AppImage bundles GStreamer from CI (Ubuntu 24.04, GStreamer 1.24).
Previously, host plugin directories (/usr/lib/gstreamer-1.0/) were
appended as fallback. This caused ABI version mismatches — host plugins
compiled against a different GStreamer version fail with undefined symbol
errors (gst_util_floor_log2, mpg123_open_handle64, etc.), leaving WebKit
without usable codecs for YouTube playback.
Since PR #434 installs the full GStreamer codec suite on CI, the AppImage
is fully self-contained. Remove the host fallback and block host plugin
scanning to prevent ABI conflicts across distro GStreamer versions.
WebKitGTK promotes iframes, <video>, and canvas elements to GPU-textured
compositing layers. In VMs (Apple Virtualization.framework, QEMU, VMware,
etc.) the virtio-gpu driver often only supports 2D or limited GL, so GBM
buffer allocation for compositing layers fails silently — rendering
iframe/video content as black while the main page works fine.
Detect VM environments via /proc/cpuinfo hypervisor flag and sys_vendor
strings, then set WEBKIT_DISABLE_COMPOSITING_MODE=1 and
LIBGL_ALWAYS_SOFTWARE=1 to force software rendering for all content.
Native Linux machines with real GPUs are unaffected.