The /api/youtube/live validation endpoint may return 429 or non-JSON
responses (Vercel WAF, YouTube rate limiting). Previously this caused
res.json() to parse HTML → either throw (caught, channel added) or
return channelExists:false (blocked add with red border).
Now only blocks when the API explicitly returns 200 OK with
channelExists:false — any non-OK status or error allows the add.
Also bumps version to 2.5.13.
* chore: bump v2.5.12
## Changelog
- fix(linux): enable keyring persistence via Secret Service + keyutils (#419)
- fix(ci): use weston+XWayland for Linux smoke test (#417)
- ci: add standalone Test Linux App workflow (#414)
- ci: skip Typecheck and Lint on fork PRs (#415)
- perf: optimize Wingbits API usage and reduce unnecessary polling (#416)
* fix(linux): append host GStreamer plugins to AppImage search path
The linuxdeploy GStreamer hook force-overrides GST_PLUGIN_PATH_1_0 and
GST_PLUGIN_SYSTEM_PATH_1_0 to only contain bundled plugins from the CI
build system (Ubuntu 24.04, GStreamer 1.24). On hosts with newer
GStreamer (e.g. Arch 1.28), codec plugins like gst-libav and
fakevideosink from gst-plugins-bad are invisible — WebKit can't play
video.
Append common host GStreamer plugin directories as fallback so the
system's codec plugins are discoverable while bundled plugins retain
priority.
Also fixes:
- tauri.conf.json devUrl port mismatch (5173 → 3000) breaking desktop:dev
- live-channels-window YouTube validation allowing add on non-OK responses
Linux had no keyring backend feature enabled — keyring v3 fell back to
in-memory mock store. Secrets appeared to save but were lost on restart.
Added `linux-native-sync-persistent` (kernel keyutils + D-Bus Secret
Service combo) and `crypto-rust` for Secret Service encryption. This
uses GNOME Keyring or KDE Wallet on desktop Linux for persistent storage.
* ci(linux): add AppImage smoke test to desktop build
Launch the built AppImage under Xvfb after the Linux build to catch
startup crashes and render failures automatically. Uploads a screenshot
artifact for visual inspection.
* Optimize Wingbits API usage and panel polling
* fix(ci): use weston+XWayland for Linux smoke test instead of pure Wayland
Previous attempt used GDK_BACKEND=wayland which caused GTK init panic
(tao requires X11). Now: weston headless with XWayland provides X11
through a real compositor. Falls back to Xvfb if weston fails.
Also uploads weston/app logs as artifacts for debugging.
* refactor(ci): use xwfb-run for Linux smoke test display
xwfb-run (from xwayland-run package) is purpose-built for this:
Xwayland on a headless Wayland compositor, replaces xvfb-run.
Falls back to plain Xvfb if xwfb-run is unavailable.
Uploads display-server and app logs as artifacts.
The squash merge of #413 put smoke test steps into build-desktop.yml
instead of the intended standalone workflow. This commit:
- Reverts build-desktop.yml to its original state
- Adds test-linux-app.yml as a separate Linux-only smoke test workflow
Launch the built AppImage under Xvfb after the Linux build to catch
startup crashes and render failures automatically. Uploads a screenshot
artifact for visual inspection.
showOfflineMessage() and showEmbedError() replaced innerHTML without
cleaning up this.player, so initializePlayer() bailed early on the
next channel switch — leaving a black screen. destroyPlayer() is
idempotent so the onError path won't double-destroy.
Co-authored-by: N Cho-chin <Niboshi-Wasabi@users.noreply.github.com>
- Show rate-limited message instead of generic "Failed to load" on Markets,
ETF, Commodities, and Sector panels when Yahoo returns 429
- fetchYahooQuotesBatch returns rateLimited flag; early-exit after 3 misses
- ETF panel skips retry loop when rate-limited, shows specific i18n message
- Fallback Finnhub symbols through Yahoo when API key missing
- 401-retry in runtime fetch patch for stale sidecar token after restart
- diagFetch auth helper for settings window diagnostic endpoints
- Verbose toggle writes to writable dataDir instead of read-only app bundle
* fix: sequential Yahoo calls, sector fallback, and missing try-catch guards
- list-commodity-quotes: replace Promise.all with fetchYahooQuotesBatch to prevent Yahoo 429
- get-sector-summary: add Yahoo Finance fallback when FINNHUB_API_KEY is missing
- list-etf-flows: sequential fetch loop + add missing try-catch around cachedFetchJson
- get-macro-signals: replace unnecessary Promise.allSettled([single]) with direct await
* fix: tighten AI summary prompts to 2 sentences / 60 words max
Summaries were often verbose walls of text despite "2-3 sentences" in
the prompt. Changed to "2 concise sentences MAX (under 60 words total)"
across all brief, analysis, and fallback prompts. Reduced max_tokens
from 150 to 100 to hard-cap output length.
The header.settings i18n key was incorrectly set to "PANELS" (and its
translations) for the settings modal title. The modal contains General,
Panels, and Sources tabs — the overall title should be "SETTINGS".
- CSS.escape() on data-news-id querySelector to prevent SyntaxError
when news item IDs contain special characters (WORLDMONITOR-5J)
- typeof guard on this.player.destroy() for partially-initialized
YouTube players (WORLDMONITOR-5C/5B)
- 11 new ignoreErrors patterns for IndexedDB races, browser vendor
internals, and extension-injected errors
- beforeSend filter for blob: URL extension frames
* fix: sort tariff datapoints newest-first in trade policy panel
* fix: update tests broken by cachedFetchJson migration
- Restore "Strip unterminated" comment in summarize-article.ts that
tests use to locate the unterminated tag stripping section
- Update ACLED tests to check for cachedFetchJson instead of removed
getCachedJson/setCachedJson patterns
* chore: bump version to 2.5.9 and make pre-push hook executable
* docs: update README with supply chain intel, universal CII, Happy Monitor, security hardening, and recent features
* fix: sort tariff datapoints newest-first in trade policy panel
* fix: update tests broken by cachedFetchJson migration
- Restore "Strip unterminated" comment in summarize-article.ts that
tests use to locate the unterminated tag stripping section
- Update ACLED tests to check for cachedFetchJson instead of removed
getCachedJson/setCachedJson patterns
* fix: sort supply chain chokepoints by disruption score descending
* fix: sort tariff datapoints newest-first in trade policy panel
* fix: update tests broken by cachedFetchJson migration
- Restore "Strip unterminated" comment in summarize-article.ts that
tests use to locate the unterminated tag stripping section
- Update ACLED tests to check for cachedFetchJson instead of removed
getCachedJson/setCachedJson patterns
Add a "Streaming" section to the GENERAL settings tab with a quality
dropdown (Auto / Low 360p / Medium 480p / High / HD 720p). The setting
persists to localStorage and applies to all live streams:
- LiveWebcamsPanel: appends vq= to direct embeds, passes through proxy
- LiveNewsPanel: sets quality via YT.Player API onReady + desktop proxy
- YouTube embed proxy: accepts vq param, calls setPlaybackQuality()
Closes#365
- Add to tech variant security feed group
- Add to main variant global feeds as type: 'cyber'
- Add www.ransomware.live to RSS proxy allowlist
- Set source tier to 3 (specialty)
PR #382 accidentally removed the maplibregl.FilterSpecification type
cast, causing tsc to infer (string | string[])[] which doesn't satisfy
setFilter's parameter type. This broke the Vercel build.
- Replace non-existent BDIY (Baltic Dry Index) FRED series with real
public series: PCU483111483111 (Deep Sea Freight PPI) and TSIFRGHT
(Freight Transportation Index). BDIY is proprietary Baltic Exchange
data not available on FRED — all shipping requests were failing.
- Make "upstream unavailable" banner per-tab instead of global — shipping
failure no longer shows the warning on chokepoints and minerals tabs
that have valid data.
- Remove inconsistent isFeatureAvailable('supplyChain') gate from
frontend fetchShippingRates() — server handler already returns empty
when FRED_API_KEY is missing, and chokepoints/minerals had no gate.
mlWorker.init() was called unconditionally in App.init(), ignoring the
browserModel setting. This caused ONNX model downloads, 120s embed
timeouts, and clustering warnings even with the toggle OFF.
- Gate mlWorker.init() on browserModel setting (or desktop runtime)
- Subscribe to setting changes for dynamic init/terminate on toggle
- Replace mlWorker.init() in sentiment-gate with isAvailable check
- Clean up subscription in App.destroy() to prevent HMR listener leaks
* feat: dynamic sidecar port with EADDRINUSE fallback
Rust probes port 46123 via TcpListener::bind; if busy, binds port 0 for
an OS-assigned ephemeral port. The actual port is stored in LocalApiState,
passed to sidecar via LOCAL_API_PORT env, and exposed to frontend via
get_local_api_port IPC command.
Frontend resolves the port lazily on first API call (with retry-on-failure
semantics) and caches it. All hardcoded 46123 references replaced with
dynamic getApiBaseUrl()/getLocalApiPort() accessors. CSP connect-src
broadened to http://127.0.0.1:* (frame-src unchanged).
* fix: scope CSP to desktop builds and eliminate port TOCTOU race
P1: Remove http://127.0.0.1:* from index.html (web build CSP). The
wildcard allowed web app JS to probe arbitrary localhost services.
Vite's htmlVariantPlugin now injects localhost CSP only when
VITE_DESKTOP_RUNTIME=1 (desktop builds).
P2: Replace Rust probe_available_port() (bind→release→spawn race)
with a confirmed port handshake. Sidecar now handles EADDRINUSE
fallback internally and writes the actual bound port to a file.
Rust polls the port file (up to 5s) to store only the confirmed port.
* fix: isSafeUrl ReferenceError — addresses scoped inside try block
`let addresses = []` was declared inside the outer `try` block but
referenced after the `catch` on line 200. `let` is block-scoped so
every request through isSafeUrl crashed with:
ReferenceError: addresses is not defined
Move the declaration before the `try` so it's in scope for the return.
* feat: add Supply Chain Disruption Intelligence service
Add new supply_chain domain with 3 RPCs:
- GetShippingRates: FRED Baltic Dry Index with spike detection
- GetChokepointStatus: NGA nav warnings + AIS vessel aggregation
for 6 global chokepoints with composite disruption scoring
- GetCriticalMinerals: HHI concentration analysis for 7 strategic
minerals from USGS 2024 production data
Frontend: 3-tab SupplyChainPanel with event delegation, sparkline
SVG, escapeHtml XSS hardening, and circuit breakers per RPC.
13 unit tests for pure scoring functions (_scoring.mjs).
* fix: chokepoint upstream detection, FRED weekly frequency, missing CSS
P1: Chokepoint handler propagates upstreamUnavailable when maritime
sources fail or return empty, instead of always reporting false.
P1: FRED BDI uses frequency=w for true 52-week window.
P2: Add CSS for sc-status-dot, sc-dot-*, sc-risk-* classes.
* fix: allow partial chokepoint data when one upstream source succeeds
* fix: use cachedFetchJson for theater posture to prevent Wingbits API stampede
The theater posture handler used manual getCachedJson/setCachedJson which
has no request coalescing. During the window between a cache miss and the
cache write completing (~15s Wingbits timeout), every concurrent request
bypassed the cache and fired its own POST to Wingbits with 9 bounding
boxes — causing ~1500 requests/min.
Switch to cachedFetchJson which deduplicates concurrent in-flight
requests via a shared promise, so only one upstream fetch runs at a time.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: migrate all handlers to cachedFetchJson, fix metadata labeling with cachedFetchJsonWithMeta
- Migrate 48 server handlers from manual getCachedJson/setCachedJson to
cachedFetchJson, eliminating cache stampede vulnerability across the
entire codebase (concurrent cache misses now coalesce into one fetch)
- Add cachedFetchJsonWithMeta to redis.ts — returns { data, source }
atomically, fixing TOCTOU race between separate cache check and fetch
- Wire summarize-article.ts and get-usni-fleet-report.ts to use
cachedFetchJsonWithMeta for correct provider/cached metadata
- Add try/catch to 7 handlers that lost graceful degradation
- Add 4 tests for WithMeta source labeling: cache hit, fresh miss,
coalesced followers, and TOCTOU regression protection
* chore: trigger PR sync
---------
Co-authored-by: Mikael Sundberg <mikael.sundberg82@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- Suppress "WTO data temporarily unavailable" banner when cached data
exists on the active tab — only show when truly empty + upstream down
- Add all missing CSS for trade policy panel (228 lines) — cards with
hover states, color-coded status badges, structured tariff table,
trade flow metrics layout, source links, and warning banner styling
Two bugs:
1. Single shared circuit breaker cached one FRED series response for all 7 series,
causing identical wrong values across indicators. Now uses per-series breakers.
2. Tab click listeners were destroyed by setContent() debounce. Replaced with
event delegation on the stable content element.
- Remove "Bound Rate" column from tariffs table — WTO TP_B_0090
bound rate data unavailable on our subscription tier (always 0.0%)
- Fix restrictions status labels: backend returns high/moderate/low
but panel expected IN_FORCE/TERMINATED — now matches actual values
- Update all 17 locale files with new tariff level labels
wtoFetch() silently returned null on missing key, HTTP errors, and
exceptions — making it impossible to diagnose why trade data shows
"temporarily unavailable". Now logs the specific failure reason.
Switch all 4 WTO trade endpoints from manual getCachedJson/setCachedJson
to cachedFetchJson, which coalesces concurrent cache misses into a single
upstream WTO API call. This prevents hammering the WTO API when multiple
requests arrive during a cache miss window.
Also register tradePolicy with the RefreshScheduler at a 10-minute
interval (full/finance variants) — previously it was only fetched once
at startup with no periodic refresh.
https://claude.ai/code/session_01FdvFhpLALL9iJaF8iXMjtu
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add WTO trade policy service with 4 RPC endpoints and TradePolicyPanel
Adds a new `trade` RPC domain backed by the WTO API (apiportal.wto.org) for
trade policy intelligence: quantitative restrictions, tariff timeseries,
bilateral trade flows, and SPS/TBT barrier notifications.
New files: 6 protos, generated server/client, 4 server handlers + shared WTO
fetch utility, client service with circuit breakers, TradePolicyPanel (4 tabs),
and full API key infrastructure (Rust keychain, sidecar, runtime config).
Panel registered for FULL and FINANCE variants with data loader integration,
command palette entry, status panel tracking, data freshness monitoring, and
i18n across all 17 locale files.
https://claude.ai/code/session_01HZXyoQp6xK3TX8obDzv6Ye
* chore: update package-lock.json
https://claude.ai/code/session_01HZXyoQp6xK3TX8obDzv6Ye
* fix: move tab click listener to constructor to prevent leak
The delegated click handler was added inside render(), which runs
on every data update (4× per load cycle). Since the listener targets
this.content (a persistent container), each call stacked a duplicate
handler. Moving it to the constructor binds it exactly once.
https://claude.ai/code/session_01HZXyoQp6xK3TX8obDzv6Ye
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add BIS Central Bank Policy Tracker to EconomicPanel
Add a new "Central Banks" tab to the Economic panel powered by the BIS
(Bank for International Settlements) free SDMX REST API. Displays policy
rates, effective exchange rates, and credit-to-GDP ratios for 12 major
central banks (Fed, ECB, BOE, BOJ, SNB, MAS, RBI, RBA, PBOC, BOC, BOK,
BCB).
- Proto: 4 new files (bis_data, get_bis_policy_rates, get_bis_exchange_rates,
get_bis_credit) + 3 RPCs added to EconomicService
- Server: shared CSV fetch/parse helper using papaparse, 3 handlers with
Redis caching (6h/12h TTL), batched single-request per dataset
- Frontend: 3 separate circuit breakers, fetchBisData() with Promise.all
- UI: Central Banks tab with policy rates (sorted by rate, colored by
cut/hike/hold), real EER indices, and credit-to-GDP ratios
- Integration: 'bis' DataSourceId, BIS in StatusPanel WORLD_APIS, 60min
refresh interval, data loader wiring
- i18n: BIS keys added to all 17 locale files
No API key required — BIS is free/public. All BIS-derived text rendered
via escapeHtml() to prevent XSS.
https://claude.ai/code/session_01N73WokR4NPuSg8JpPz7nYZ
* chore: update package-lock.json
https://claude.ai/code/session_01N73WokR4NPuSg8JpPz7nYZ
* fix: BIS policy tracker - URL bug, error logging, UX, and data cleanup
- Fix double '?' in fetchBisCSV URL construction (format=csv was silently ignored)
- Add error logging to all 3 BIS server handlers (previously silent catch blocks)
- Fix misleading "BIS data loading..." empty state to "temporarily unavailable - will retry"
- Remove unused nominal EER fetch to save bandwidth (only real EER is displayed)
- Fix previousRatio rounding inconsistency in credit-to-GDP fallback path
https://claude.ai/code/session_01N73WokR4NPuSg8JpPz7nYZ
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix: add redirect target domains to RSS proxy allowlist
HuffPost and Nature feeds redirect to different hostnames
(chaski.huffpost.com, www.nature.com) that weren't in ALLOWED_DOMAINS,
causing the proxy to reject the redirect and fall through to the
Railway relay (which is currently down).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: remove dead RSS feeds and add 9 new positive news sources
Remove Sunny Skyz (old URL dead) and HuffPost Good News (feed discontinued).
Replace with verified working feeds:
- Positive: Upworthy, DailyGood, Good Good Good, GOOD Magazine,
Sunny Skyz (new URL), The Better India
- Science: Singularity Hub, Human Progress, Greater Good (Berkeley)
Also add redirect target domains (www.nature.com) to proxy allowlist
and remove dead HuffPost domains.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add fallback data for Progress/Renewable panels + expand Breakthroughs sources
World Bank API is intermittent (some indicators time out with 0 bytes).
Both Human Progress and Renewable Energy panels silently returned empty
when API was unavailable and no IndexedDB cache existed.
- Add hardcoded fallback datasets (verified from World Bank, Feb 2026)
for all 4 progress indicators and renewable energy global/regional data
- Use fallback instead of empty on any API failure or empty response
- Add Singularity Hub, Human Progress, Greater Good (Berkeley) to
Breakthroughs panel source filter so new science feeds show up
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Update data layer count to 36+, add Happy Monitor variant to Live Demos,
expand Cmd+K command palette description, and add trade routes to
Infrastructure section.
Static layer showing 19 major global shipping lanes as multi-segment
arcs through strategic chokepoints, with amber dots highlighting
active waterway passages. Routes are waypointed through STRATEGIC_WATERWAYS
coordinates so arcs visually pass through Suez, Malacca, Hormuz, etc.
rather than taking misleading great-circle shortcuts.
- New src/config/trade-routes.ts with 19 routes, segment resolver
- ArcLayer (greatCircle per segment) + ScatterplotLayer for chokepoints
- Status-based coloring (active/disrupted/high_risk) with light theme support
- Finance variant ON by default, all others OFF
- Toggle UI, layer help, command palette, URL state persistence
- Translated layer labels across all 17 locales
HuffPost and Nature feeds redirect to different hostnames
(chaski.huffpost.com, www.nature.com) that weren't in ALLOWED_DOMAINS,
causing the proxy to reject the redirect and fall through to the
Railway relay (which is currently down).
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* chore: remove .claudedocs from repo and add to gitignore
Sentry triage log was accidentally committed in v2.4.0. These are
ephemeral Claude session artifacts, not project documentation.
* fix: tighten Sentry noise filters for maplibre/deck.gl and third-party errors
The beforeSend filename regex missed `maplibre-*` chunks and hashes
containing `-`, letting ~3700 map-internal crashes through. Also widen
imageManager filter for Safari wording and add filters for YouTube
widget API, Sentry SDK logs.js, Facebook WebView, and TDZ errors.
* chore: remove .claudedocs from repo and add to gitignore
Sentry triage log was accidentally committed in v2.4.0. These are
ephemeral Claude session artifacts, not project documentation.
* fix: tighten Sentry noise filters for maplibre/deck.gl and third-party errors
The beforeSend filename regex missed `maplibre-*` chunks and hashes
containing `-`, letting ~3700 map-internal crashes through. Also widen
imageManager filter for Safari wording and add filters for YouTube
widget API, Sentry SDK logs.js, Facebook WebView, and TDZ errors.
* feat: add country brief commands for all ISO countries to command palette
Generates country commands from all ~200 ISO 3166-1 codes using
Intl.DisplayNames for name resolution. Curated countries (30) retain
rich searchAliases for keyword matching (e.g. "kremlin" → Russia).
* feat: update command palette empty state and translate all 16 locales
Update placeholder, hint, and empty state text to reflect command
palette capabilities (countries, layers, panels, navigation, settings).
Add example commands row with styled kbd tags. Translate all strings
across ar, de, el, es, fr, it, ja, nl, pl, pt, ru, sv, th, tr, vi, zh.