* fix(fear-greed): add undici to scripts/package.json (ERR_MODULE_NOT_FOUND on Railway)
* fix(market): route sectors/commodities to correct RPC endpoints
fetchMultipleStocks called listMarketQuotes which reads market:stocks-bootstrap:v1.
Sector ETFs (XLK, XLF...) and commodity futures (GC=F, CL=F...) are NOT in that key,
so the live-fetch fallback always returned empty after the one-shot bootstrap hydration
was consumed, causing panels to show "data temporarily unavailable" on every reload.
Fix: add fetchSectors() -> getSectorSummary (reads market:sectors:v1) and
fetchCommodityQuotes() -> listCommodityQuotes (reads market:commodities-bootstrap:v1),
each with their own circuit breaker and persistent cache. Remove useCommodityBreaker
option from fetchMultipleStocks which no longer serves commodities.
* feat(heatmap): show friendly sector names instead of ETF tickers
The relay seeds name:ticker into Redis (market:sectors:v1), so the
heatmap showed XLK/XLF/etc which is non-intuitive for most users.
Fix: build a sectorNameMap from shared/sectors.json (keyed by symbol)
and apply it in both the hydrated and live fetch paths. Also update
sectors.json names from ultra-short aliases (Tech, Finance) to clearer
labels (Technology, Financials, Health Care, etc).
Closes#2194
* sync scripts/shared/sectors.json
* feat(heatmap): show ticker + sector name side by side
Each tile now shows:
XLK <- dim ticker (for professionals)
Technology <- full sector name (for laymen)
+1.23%
Sector names updated: Tech→Technology, Finance→Financials,
Health→Health Care, Real Est→Real Estate, Comms→Comm. Svcs, etc.
Refs #2194
* feat(gdelt-intel): add tone/vol sparkline summary bar to GdeltIntelPanel
Surfaces GDELT tone and volume timeline data (seeded by PR #2087) in the
existing intel panel. A compact summary bar appears between the topic tabs
and article list, showing:
- 14-day tone sparkline (red=negative, green=positive) + current value badge
- 14-day volume sparkline + last-point count
- Gracefully hidden when timeline data is unavailable
Changes:
- gdelt-intel.ts: add TopicTimeline type, fetchTopicTimeline() with 5-min
client cache; reuses existing IntelligenceServiceClient
- GdeltIntelPanel.ts: fetch timeline in parallel with articles via
Promise.all; renderTopicSummary() builds the bar using miniSparkline();
cached timeline served immediately on tab switch
- main.css: .gdelt-topic-summary / .gdelt-trend-group / .gdelt-trend-value
styles
* fix(gdelt-intel): add tone/vol sparkline info to panel ? tooltip (all 21 locales)
* fix(gdelt-intel): rename GDELT Intelligence → News Intelligence in panel tooltip (all 21 locales)
* feat(commodities): expand tracking to cover agricultural and coal futures
Adds 9 new commodity symbols to cover the price rally visible in our
intelligence feeds: Newcastle Coal (MTF=F), Wheat (ZW=F), Corn (ZC=F),
Soybeans (ZS=F), Rough Rice (ZR=F), Coffee (KC=F), Sugar (SB=F),
Cocoa (CC=F), and Cotton (CT=F).
Also fixes ais-relay seeder to use display names from commodities.json
instead of raw symbols, so seeded data is self-consistent.
* fix(commodities): gold standard cache, 3-col grid, cleanup
- Add upstashExpire on zero-quotes failure path so bootstrap key TTL
extends during Yahoo outages (gold standard pattern)
- Remove unreachable fallback in retry loop (COMMODITY_META always has
the symbol since it mirrors COMMODITY_SYMBOLS)
- Switch commodities panel to 3-column grid (19 items → ~7 rows vs 10)
* fix(seeders): apply gold standard TTL-extend+retry pattern to Aviation, NOTAM, Cyber, PositiveEvents
* feat(consumer-prices): default to All — global comparison table as landing view
- DEFAULT_MARKET = 'all' so panel opens with the global view
- 🌍 All pill added at front of market bar
- All view fetches all 9 markets in parallel via fetchAllMarketsOverview()
and renders a comparison table: Market / Index / WoW / Spread / Updated
- Clicking any market row drills into that market's full tab view
- SINGLE_MARKETS exported for use in All-view iteration
- CSS: .cp-global-table and row styles
Adds a compact market pill row above the tabs so users can switch
between all 9 tracked markets (AE, AU, BR, GB, IN, KE, SA, SG, US).
Selecting a market updates settings.market + settings.basket and
re-fetches all panel data. Selection persists in localStorage.
Previously DEFAULT_MARKET='ae' was hardcoded with no UI escape hatch,
causing every tab (Overview, Categories, Movers, Retailer Spread,
Data Health) to always show UAE data only.
* fix(panels): align font sizes, border-radius, and font-family with app style
Telegram Intel, Daily Market Brief, Stock Analysis, and Backtesting panels
were using inconsistent styling compared to the rest of the dashboard.
- telegram-intel-text: 13px -> 12px (match body font size)
- DailyMarketBriefPanel summary: 13px -> 12px; border-radius 12px -> 4px
- StockAnalysisPanel/StockBacktestPanel: font-family:monospace -> var(--font-mono)
so ticker/signal badges respect the user's font preference toggle
* fix(map): replace hardcoded font-family:monospace with var(--font-mono) in tooltips
* fix(free-tier): exclude cw-* panels from quota count and disable on launch
Custom widget panels (cw-*) are not loaded for free users but were
counted against the FREE_MAX_PANELS cap, causing invisible panels to
silently consume quota and block enabling/undoing visible panels.
- App.ts: disable any enabled cw-* panels for free users on launch,
exclude them from the enabled count before trimming
- UnifiedSettings.ts toggleDraftPanel: exclude cw-* from count
- event-handlers.ts performUndo: exclude cw-* from count
- settings-window.ts: exclude cw-* from count
* fix(settings-window): hide cw-* panels for free users in standalone settings
* feat(settings): show PRO-locked panels with badge and lock icon for free users
Applies all review fixes from PR #2080 onto current main (which already
has the base feature from #2076):
P1 — pendingHash guard: data changing mid-flight no longer caches stale
HTML under the new hash. autoVisualize now receives startHash and only
writes cachedWidgetHtml / renders when pendingHash === lastJsonHash.
P1 — Stream-end flush: panel no longer gets stuck on loading if stream
closes before a 'done' event. After the read loop, if !rendered and
resultHtml exists (html_complete fired), render it. If !rendered and no
HTML, show error and reset hashes.
P1 — extractJsonData fallback gated on content wrapper absence: was
returning the whole result object (including isError etc.) when content
array had no JSON text. Now only falls back when result.content is not
an array.
P2 — Hash extended to 8192 chars (was 1000).
P2 — destroyController: AbortController on the class; destroy() calls
abort() to cancel any in-flight fetch. AbortError silently returns.
P2 — Dead CSS removed: .mcp-panel-widget .wm-widget-shell and
.wm-widget-body / .wm-widget-generated only apply to basic-tier widgets;
MCP always uses the pro iframe path.
P3 — toolName sliced to 100 chars before LLM prompt injection.
* feat(mcp): auto-visualize JSON data via widget agent
When McpDataPanel receives JSON results and widget key is configured,
automatically pipe the data through the widget agent to generate an
interactive HTML visualization instead of showing raw JSON.
- Uses pro tier (Chart.js iframe) if wm-pro-key is set, basic (SVG/CSS)
otherwise
- Caches generated HTML by content hash to avoid regenerating on refresh
cycles when data hasn't changed
- Falls back to raw JSON display on error or if widget not configured
- Shows radar loading animation during generation
* fix(mcp): always use pro tier for auto-visualization
MCP panel is a PRO-only feature, so auto-visualization always uses
pro tier (Chart.js iframe, full interactivity) with X-Pro-Key header.
Removes basic tier fallback path that was incorrect.
* fix(consumer-prices): harden scrape/aggregate/publish pipeline
- scrape: treat 0-product parse as error (increments errorsCount, skips
pagesSucceeded) so noon_grocery_ae missing eggs_12/tomatoes_1kg marks
the run partial instead of completed
- publish: fix freshData gate (freshnessMin >= 0) so a scrape finishing
at exactly 0 min lag still advances seed-meta
- aggregate: wrap per-basket aggregation in try/catch so one failing
basket does not skip remaining baskets; re-throw if any failed
- seed-consumer-prices.mjs: require --force flag to prevent accidentally
stomping publish.ts 26h TTLs with short 10-60min fallback TTLs
* fix(consumer-prices): correct basket comparison with intersection + dedup
Both aggregate.ts and the retailer spread snapshot were summing ALL
matched SKUs per retailer without deduplication, making Carrefour
appear most expensive simply because it had more matched products
(31 "items" vs Noon's 20 for a 12-item basket).
Fixes:
- aggregate.ts retailer_spread_pct: deduplicate per (retailer, basketItem)
taking cheapest price, then only compare on items all retailers carry
- worldmonitor.ts buildRetailerSpreadSnapshot: same dedup + intersection
logic in SQL — one best_price per (retailer, basket_item), common_items
CTE filters to items every active retailer covers
- exa-search.ts parseListing: log whether Exa returned 0 results or
results with no extractable price, to distinguish the two failure modes
* fix(consumer-prices-panel): correct parse rate display, category names, and freshness colors
- parseSuccessRate is stored as 0-100 but UI was doing *100 again (shows 10000%)
- Category name builder converts snake_case to Title Case (Cooking_oil → Cooking Oil)
- Add missing cp-fresh--ok/warn/stale/unknown CSS classes (freshness labels had no color)
- Add border-radius to stat cards and range buttons; add font-family to range buttons
- Add padding + bottom border to cp-range-bar for visual separation
* fix(consumer-prices): gate overview spread_pct query to last 2 days
buildOverviewSnapshot queried retailer_spread_pct with no recency
filter, so ORDER BY metric_date DESC LIMIT 1 would serve an
arbitrarily old row when today's aggregate run omitted a write
(no retailer intersection). Add INTERVAL '2 days' cutoff — covers
24h cron cadence plus scheduling drift. Falls through to 0 (→ UI
shows '—') when no recent value exists.
Wire setLayerLoading/setLayerReady into fetchViewportAircraft so the
flights toggle gives visible feedback during the API call: the plane
icon swaps to an hourglass and the label pulses yellow while in flight,
resolving to green-bordered active state once positions arrive.
Users no longer need to guess whether data is loading or simply absent.
The indicator fires only when a real fetch starts (viewport changed),
never on cache hits or stale-response discards.
* feat(economic): add WoW tracking and fix plumbing for bigmac/grocery-basket panels
Phase 1 — Fix Plumbing:
- Adjust CACHE_TTL to 10 days (864000s) for bigmac and grocery-basket seeds
- Align health.js SEED_META maxStaleMin to 10080 (7 days) for both
- Add grocery-basket and bigmac to seed-health.js SEED_DOMAINS with intervalMin: 5040
- Refactor publish.ts writeSnapshot to accept advanceSeedMeta param; only
advance seed-meta when fresh data exists (overallFreshnessMin < 120)
- Add manual-fallback-only comment to seed-consumer-prices.mjs
Phase 2 — Week-over-Week Tracking:
- Add wow_pct field to BigMacCountryPrice and CountryBasket proto messages
- Add wow_avg_pct, wow_available, prev_fetched_at to both response protos
- Regenerate client/server TypeScript from updated protos
- Add readCurrentSnapshot() helper + WoW computation to seed-bigmac.mjs
and seed-grocery-basket.mjs; write :prev key via extraKeys
- Update BigMacPanel.ts to show per-country WoW column and global avg summary
- Update GroceryBasketPanel.ts to show WoW badge on total row and basket avg summary
- Add .bm-wow-up, .bm-wow-down, .bm-wow-summary, .gb-wow CSS classes
- Fix server handlers to include new WoW fields in fallback responses
* fix(economic): guard :prev extraKey against null on first seed run; eliminate double freshness query in publish.ts
* refactor(economic): address code review findings from PR #1974
- Extract readSeedSnapshot() into _seed-utils.mjs (DRY: was duplicated
verbatim in seed-bigmac and seed-grocery-basket)
- Add FRESH_DATA_THRESHOLD_MIN constant in publish.ts (replace magic 120)
- Fix seed-consumer-prices.mjs contradictory JSDoc (remove stale
"Deployed as: Railway cron service" line that contradicted manual-only warning)
- Add i18n keys panels.bigmacWow / panels.bigmacCountry to en.json
- Replace hardcoded "WoW" / "Country" with t() calls in BigMacPanel
- Replace IIFE-in-ternary pattern with plain if blocks in BigMacPanel
and GroceryBasketPanel (P2/P3 from code review)
* fix(publish): gate advanceSeedMeta on any-retailer freshness, not average
overallFreshnessMin is the arithmetic mean across all retailers, so with
1 fresh + 2 stale retailers the average can exceed 120 min and suppress
seed-meta advancement even while fresh data is being published.
Use retailers.some(r => r.freshnessMin < 120) to correctly implement
"at least one retailer scraped within the last 2 hours."
- seed-bigmac.mjs: expand from 9 ME countries to 50+ global (Americas, Europe, Asia-Pacific, ME, Africa); derive FX_SYMBOLS from COUNTRIES; expand CCY regex and FX_FALLBACKS
- ConsumerPricesPanel: show structured seeding placeholder (icon + title + sub) when upstreamUnavailable instead of bare empty text
- main.css: add cp-* styles for ConsumerPricesPanel and gb-* styles for GroceryBasketPanel (were missing entirely)
* feat(wingbits): show flight route, times, and plane photo in popup
* chore(gen): regenerate military service stubs after WingbitsLiveFlight proto extension
* fix(wingbits): add empty schedule/photo defaults to mapEcsFlight for type safety
* chore(gen): regenerate MilitaryService OpenAPI docs after WingbitsLiveFlight extension
* fix(wingbits): address code review — sanitizeUrl for img src, DEP/ARR column labels, fmtDelayMin edge case, remove debug artifact
* Add cached bootstrap fallback for offline dashboard
* fix(css): add --status-unavailable light theme override
* fix(pwa): address review issues — offline age gate, i18n namespace, innerHTML safety
* fix(pwa): preserve cache staleness for mixed tiers; re-evaluate connectivity after live data loads
P1: setPersistentCache now accepts optional updatedAt so mixed-tier writes
preserve the original cache timestamp instead of resetting the age clock.
P2: markBootstrapAsLive() promotes the hydration state after loadAllData()
succeeds, so the CACHED banner clears once live data has loaded.
---------
Co-authored-by: Nishchaya Sharma <shinzoxD@users.noreply.github.com>
Co-authored-by: Elie Habib <elie.habib@gmail.com>
- Remove persistCache from nationalDebtBreaker: IndexedDB hydration on
first call can deadlock in some browsers, causing the panel to hang
indefinitely showing "Loading debt data from IMF..." with no timeout
- Add 20s hard deadline via Promise.race so loading always resolves
- Extract _fetchNationalDebt() to avoid top-level await in the race
- Lazy-load 20 countries initially with "Load more" button (+20 each tap)
instead of rendering all 187 rows at once; resets on sort/search change
- Ticker only updates visible rows (not all 187)
* feat(economic): add global debt ticker header with deficit/surplus breakdown
* fix(economic): world debt card spans full width, deficit/surplus in 2-col row below
* feat(economic): add National Debt Clock panel with IMF + Treasury data
- Proto: GetNationalDebt RPC in EconomicService with NationalDebtEntry message
- Seed: seed-national-debt.mjs fetches IMF WEO (debt%, GDP, deficit%) + US Treasury FiscalData in parallel; filters aggregates/territories; sorts by total debt; 35-day TTL for monthly Railway cron
- Handler: get-national-debt.ts reads seeded Redis cache key economic:national-debt:v1
- Registry: nationalDebt key added to cache-keys.ts, bootstrap.js (SLOW tier), health.js (maxStaleMin=10080), gateway.ts (daily cache tier)
- Service: getNationalDebtData() in economic/index.ts with bootstrap hydration + RPC fallback
- Panel: NationalDebtPanel.ts with sort tabs (Total/Debt-GDP/1Y Growth), search, live ticking via direct DOM manipulation (avoids setContent debounce)
- Tests: 10 seed formula tests + 8 ticker math tests; all 2064 suite tests green
* fix(economic): address code review findings for national debt clock
* fix(economic): guard runSeed() call to prevent process.exit in test imports
seed-national-debt.mjs called runSeed() at module top-level. When imported
by tests (to access computeEntries), the seed ran, hit missing Redis creds
in CI, and called process.exit(1), failing the entire test suite.
Guard with isMain check so runSeed() only fires on direct execution.
* feat(market): add crypto sectors heatmap and token panels (DeFi, AI, Other) backend
- Add shared/crypto-sectors.json, defi-tokens.json, ai-tokens.json, other-tokens.json configs
- Add scripts/seed-crypto-sectors.mjs and seed-token-panels.mjs seed scripts
- Add proto messages for ListCryptoSectors, ListDefiTokens, ListAiTokens, ListOtherTokens
- Add change7d field (field 6) to CryptoQuote proto message
- Run buf generate to produce updated TypeScript bindings
- Add server handlers for all 4 new RPCs reading from seeded Redis cache
- Wire handlers into marketHandler and register cache keys with BOOTSTRAP_TIERS=slow
- Wire seedCryptoSectors and seedTokenPanels into ais-relay.cjs seedAllMarketData loop
* feat(panels): add crypto sectors heatmap and token panels (DeFi, AI, Other)
- Add TokenData interface to src/types/index.ts
- Wire ListCryptoSectorsResponse/ListDefiTokensResponse/ListAiTokensResponse/ListOtherTokensResponse into market service with circuit breakers and hydration fallbacks
- Add CryptoHeatmapPanel, TokenListPanel, DefiTokensPanel, AiTokensPanel, OtherTokensPanel to MarketPanel.ts
- Register 4 new panels in panels.ts FINANCE_PANELS and cryptoDigital category
- Instantiate new panels in panel-layout.ts
- Load data in data-loader.ts loadMarkets() alongside existing crypto fetch
* fix(crypto-panels): resolve test failures and type errors post-review
- Add @ts-nocheck to regenerated market service_server/client (matches repo convention)
- Add 4 new RPC routes to RPC_CACHE_TIER in gateway.ts (route-cache-tier test)
- Sync scripts/shared/ with shared/ for new token/sector JSON configs
- Restore non-market generated files to origin/main state (avoid buf version diff)
* fix(crypto-panels): address code review findings (P1-P3)
- ais-relay seedTokenPanels: add empty-guard before Redis write to
prevent overwriting cached data when all IDs are unresolvable
- server _feeds.ts: sync 4 missing crypto feeds (Wu Blockchain, Messari,
NFT News, Stablecoin Policy) with client-side feeds.ts
- data-loader: expose panel refs outside try block so catch can call
showRetrying(); log error instead of swallowing silently
- MarketPanel: replace hardcoded English error strings with t() calls
(failedSectorData / failedCryptoData) to honour user locale
- seed-token-panels.mjs: remove unused getRedisCredentials import
- cache-keys.ts: one BOOTSTRAP_TIERS entry per line for consistency
* fix(crypto-panels): three correctness fixes for RSS proxy, refresh, and Redis write visibility
- api/_rss-allowed-domains.js: add 7 new crypto domains (decrypt.co,
blockworks.co, thedefiant.io, bitcoinmagazine.com, www.dlnews.com,
cryptoslate.com, unchainedcrypto.com) so rss-proxy.js accepts the
new finance feeds instead of rejecting them as disallowed hosts
- src/App.ts: add crypto-heatmap/defi-tokens/ai-tokens/other-tokens to
the periodic markets refresh viewport condition so panels on screen
continue receiving live updates, not just the initial load
- ais-relay seedTokenPanels: capture upstashSet return values and log
PARTIAL if any Redis write fails, matching seedCryptoSectors pattern
* fix(widgets): reduce design drift between AI widgets and default panels
- System prompt: add explicit visual design rules with anti-patterns
(no font-family overrides, CSS vars only, max 4px radius, compact rows)
Restore 6 intel topics (sanctions + intelligence), fix disp-stat → disp-stat-box class name
Add correct HTML pattern examples for rows, tables, stats grids
- widget-sanitizer.ts PRO iframe: monospace font stack, CSS variable palette,
font-family:inherit!important on *, table/th/td baseline styles, change-positive/negative classes
- main.css: enforce font-family:inherit!important on .wm-widget-generated and all descendants
so inline style="font-family:..." from AI output cannot override the monospace look
* fix(widgets): correct table wrapper structure, ban PRO style blocks
- trade-tariffs-table is a wrapper div around <table>, not a class on
the table itself; fix example and add explicit anti-pattern note
- PRO widget prompt: disallow <style> blocks in body content since
they load after the iframe head CSS and can override the monospace
font and dark palette guardrails (source-order wins)
* feat: effective tariff rate source
* fix(trade): extract parse helpers, fix tests, add health monitoring
- Extract htmlToPlainText/toIsoDate/parseBudgetLabEffectiveTariffHtml
to scripts/_trade-parse-utils.mjs so tests can import directly
- Fix toIsoDate to use month-name lookup instead of fragile
new Date(\`\${text} UTC\`) which is not spec-guaranteed
- Replace new Function() test reconstruction with direct ESM import
- Add test fixtures for parser patterns 2 and 3 (previously untested)
- Add tariffTrendsUs to health.js STANDALONE_KEYS + SEED_META
(key trade:tariffs:v1:840:all:10, maxStaleMin 900 = 2.5x the 6h TTL)
* fix(test): update sourceVersion assertion for budgetlab addition
---------
Co-authored-by: Elie Habib <elie.habib@gmail.com>
* feat(cii): add displacement signal and extend ACLED window for post-conflict accuracy
ACLED's 7-day window scores post-ceasefire countries (e.g. Lebanon) near
zero despite ongoing destruction, because battle events dried up after the
November 2024 ceasefire. Two fixes:
1. Displacement signal (UNHCR): reads displacement:summary:v1:YEAR from
Redis and maps ISO3→ISO2 codes. Log-scaled boost (max +20pts) means
Lebanon (~1.2M displaced) gets ~12pts, Syria (~7M) gets ~18pts, and
peaceful countries get 0. This signal persists long after a ceasefire
because UNHCR data updates annually, not per-battle.
2. ACLED 30-day window with time decay: extends from 7 to 30 days.
Events 0-7 days old: weight 1.0 | Events 8-30 days old: weight 0.4.
Captures conflict tail without over-weighting stale events.
Together these make CII reflect structural reality (displaced populations,
reconstruction crisis) rather than just last-week's ACLED event count.
* fix(cii): add displacedByIso3 to emptyAux fallback
* fix(cii): rescale displacement boost and paginate 30-day ACLED query
P1: The previous formula Math.log10(n)*4 capped at 100K displaced
(log10(100000)=5, 5*4=20), making 100K and 10M identical. Rescaled
to (log10(n)-5)*8+4 anchored at the 100K threshold:
100K→+4 | 500K→+10 | 1M→+12 | 5M→+18 | 10M→+20, floor 0.
P2: A single 30-day fetch with limit:1500 silently drops tail events
once the global count exceeds the cap, which cuts post-conflict
countries (low recent, higher older activity) exactly when they need
the signal most. Split into two independent cached queries:
- days 0-7: limit 1000 (full weight, same as before)
- days 8-30: limit 1000 (0.4 weight, separate Redis cache key)
Each window gets its own event budget; no shared-function changes.
* feat(mcp): add MCP data panel to let users connect any MCP server
Users can now connect an MCP server directly from the dashboard via a new
"Connect MCP" block alongside "Create with AI". The modal fetches available
tools from the server, lets the user pick a tool + configure args + refresh
interval, then renders a live auto-refreshing panel.
- api/mcp-proxy.js: Vercel edge proxy implementing MCP streamable-HTTP
protocol (initialize + tools/list / tools/call, SSE + JSON responses)
- src/services/mcp-store.ts: localStorage CRUD for McpPanelSpec configs
- src/components/McpConnectModal.ts: connection wizard (URL, auth header,
tool selection, args, title, refresh interval)
- src/components/McpDataPanel.ts: auto-refreshing panel with configure (⚙)
and refresh-now (↻) buttons; renders text or formatted JSON from tool result
- panel-layout.ts: loads persisted MCP panels on boot, adds "Connect MCP"
block, addMcpPanel() integration
- event-handlers.ts: handles mcp- panel close (confirm + delete) and
wm:mcp-configure event for re-opening the modal
- CSS: mcp-connect-modal, mcp-data-panel, shared btn/btn-primary/etc classes
- i18n: mcp.* keys in en.json
- tests: mcp-proxy.js added to ALLOWED_LEGACY_ENDPOINTS allowlist
* fix(mcp): add common.cancel i18n key used by McpConnectModal
* feat(mcp): add Quick Connect presets for Brave, Exa, Tavily, Context7, Web Fetch
* fix(mcp): replace search-heavy presets with diverse tool presets (GitHub, Slack, Radar, Maps, Postgres, Fetch)
* feat(mcp): add 10 quick-connect presets
Add Linear, Sentry, Datadog, Stripe, Overpass (OSM), Perplexity,
Polygon.io, Notion, Airtable, and Shodan to MCP_PRESETS, bringing
the total to 16 diverse integrations. Make preset list scrollable
(max-height 260px) to accommodate the longer list.
* fix(mcp): SSRF protection, CRLF sanitization, auth hint styling
- Block private IP ranges (RFC1918, link-local, cloud metadata 169.254.x.x)
in mcp-proxy validateServerUrl; remove dead localhost-allow logic
- Strip CRLF from forwarded header keys/values to prevent header injection
- Use mcp-status-info class (amber) for auth notes instead of mcp-status-loading
- Simplify redundant JSON.parse guard in preset click handler
* fix(mcp): correct protocol version and send notifications/initialized
- Bump MCP_PROTOCOL_VERSION from 2024-11-05 to 2025-03-26: the current
transport (direct POST, JSON or SSE response) is Streamable HTTP
defined in the 2025-03-26 revision. The 2024-11-05 HTTP transport
requires opening an SSE endpoint first to discover the JSON-RPC URL,
which this proxy does not do. Advertising the wrong version caused
compliant 2024-11-05 servers to fail during connection.
- Add sendInitialized() helper that fires notifications/initialized
after a successful initialize handshake. MCP lifecycle requires this
notification before any tool traffic; servers that enforce it would
reject every tools/list or tools/call from this proxy. Called in
both mcpListTools() and mcpCallTool(). Response is awaited but
treated as fire-and-forget so non-compliant servers do not break.
* refactor(sanctions): rename panel, restructure layout, fix stale empty cache
- Rename panel from "Sanctions Pressure" to "Sanctions & Designations"
- Make countries the hero section; demote programs to bottom
- Remove redundant Top Country/Program headline rows and Source Mix card
- Summary now shows New, Total, Vessels, Aircraft (4 cards instead of 6)
- Fix stale empty-state cache: circuit breaker now skips caching results
with totalCount=0, so a failed seed no longer persists zeros in
IndexedDB across reloads
- Single "Sanctions data unavailable." empty state instead of three
cascading "No X available" messages
- Remove dead CSS for .sanctions-headlines / .sanctions-headline / -label / -value
* fix(circuit-breaker): evict stale cache when shouldCache rejects SWR result
When a background SWR refresh returns a result that shouldCache() rejects,
evict the stale cache entry rather than silently discarding the result.
Without this, a service returning empty after having real data (e.g. OFAC
seed missing from Redis) continues serving the old payload indefinitely via
stale-while-revalidate, up to the 24h persistent-cache ceiling.
After this fix the stale window is bounded to one SWR cycle: the first
foreground load after the feed goes down still serves cached data, the
background refresh evicts it, and the next load surfaces the unavailable state.
* fix(sanctions): move stale-cache eviction to service, revert circuit-breaker change
The circuit-breaker SWR eviction on shouldCache rejection broke the existing
market-quote test (which correctly expects stale prices to survive a transient
empty response). The concerns are legitimately different:
- Market quotes: shouldCache rejection must preserve stale cache (transient blips)
- Sanctions: feed down must surface as unavailable, not serve old designations
Fix: revert circuit-breaker.ts to original SWR behavior, and instead call
breaker.clearCache() explicitly inside the sanctions fn() callback when
totalCount === 0. shouldCache still prevents writing the empty result, and
the explicit clearCache() evicts any stale entry so the next SWR cycle
returns the unavailable state.
* chore(hooks): add unit test suite to pre-push gate
npm run test:data was missing from pre-push — only edge-functions.test.mjs
and mdx-lint.test.mjs ran locally. CI catches all tests/*.test.mjs but the
hook did not, causing a broken circuit-breaker change to reach GitHub before
the regression was detected.
* feat(widgets): add Exa web search + fix widget API endpoints
- Replace Tavily with Exa as primary stock-news search provider
(Exa → Brave → SerpAPI → Google News RSS cascade)
- Add search_web tool to widget agent so AI can fetch live data
about any topic beyond the pre-defined RPC catalog
- Exa primary (type:auto + content snippets), Brave fallback
- Fix all widget tool endpoints: /rpc/... paths were hitting
Vercel catch-all and returning SPA HTML instead of JSON data
- Fix wm-widget-shell min-height causing fixed-size border that
clipped AI widget content
- Add HTML response guard in tool handler
- Update env key: TAVILY_API_KEYS → EXA_API_KEYS throughout
* fix(stock-news): use type 'neural' for Exa search (type 'news' is invalid)
* feat(widgets): add PRO interactive widgets via iframe srcdoc
Introduces a PRO tier for AI-generated widgets that supports full JS
execution (Chart.js, sortable tables, animated counters) via sandboxed
iframes — no Docker, no build step required.
Key design decisions:
- Server returns <body> + inline <script> only; client builds the full
<!DOCTYPE html> skeleton with CSP guaranteed as the first <head> child
so the AI can never inject or bypass the security policy
- sandbox="allow-scripts" only — no allow-same-origin, no allow-forms
- PRO HTML stored in separate wm-pro-html-{id} localStorage key to
isolate 80KB quota pressure from the main widget metadata array
- Raw localStorage.setItem() for PRO writes with HTML-first write order
and metadata rollback on failure (bypasses saveToStorage which swallows
QuotaExceededError)
- Separate PRO_WIDGET_KEY env var + x-pro-key header gate on Railway
- Separate rate limit bucket (20/hr PRO vs 10/hr basic)
- Claude Sonnet 4.6 (8192 tokens, 10 turns, 120s) for PRO vs Haiku for
basic; health endpoint exposes proKeyConfigured for modal preflight
* feat(pro): gate finance panels and widget buttons behind wm-pro-key
The PRO localStorage key now unlocks the three previously desktop-only
finance panels (stock-analysis, stock-backtest, daily-market-brief) on
the web variant, giving PRO users access without needing WORLDMONITOR_API_KEY.
Button visibility is now cleanly separated by key:
- wm-widget-key only → basic "Create with AI" button
- wm-pro-key only → PRO "Create Interactive" button only
- both keys → both buttons
- no key → neither button
Widget boot loader also accepts either key so PRO-only users see their
saved interactive widgets on page load.
* fix(widgets): inject Chart.js CDN into PRO iframe shell so new Chart() is defined
* feat(ui): separate macro stress and energy complex panels
Cherry-picked from chained branch onto clean main. Splits the Economic
panel into focused Macro Stress (FRED indicators) and Energy Complex
(oil analytics + market tape) panels.
Also fixes negative dollar-B changes now display with minus sign (was
rendering as positive due to Math.abs without sign prefix).
* fix: restore BIS/spending loading, split commodity/energy flags
Addresses 2 blockers from PR review:
1. Restore BIS and USASpending data loading in all 3 call sites
(prime tasks, periodic refresh, initial load). Restore full
spending and centralBanks tab rendering in EconomicPanel while
keeping the new macro stress indicators view.
2. Split shared commoditiesLoaded flag into separate metalsLoaded
and energyLoaded flags so each panel falls back independently.
Energy data arriving cannot suppress metals fallback fetch.
EconomicPanel now has 3 tabs: Indicators (macro stress), Spending,
Central Banks. Oil tab removed (lives in EnergyComplexPanel).
* fix(i18n): restore economic panel translation keys for BIS/spending tabs
PR removed i18n keys needed by the restored spending and centralBanks
tabs (gov, centralBanks, awards, policyRate, etc.). Also updated the
infoTooltip to accurately describe the mixed surface (macro + gov +
central banks) instead of claiming macro-only.
* feat(sanctions): add OFAC sanctions pressure intelligence
* fix(sanctions): strip _state from API response, fix code/name alignment, cap seed limit
- trimResponse now destructures _state before spreading to prevent seed
internals leaking to API clients during the atomicPublish→afterPublish window
- buildLocationMap and extractPartyCountries now sort (code, name) as aligned
pairs instead of calling uniqueSorted independently on each array; fixes
code↔name mispairing for OFAC-specific codes like XC (Crimea) where
alphabetic order of codes and names diverges
- DEFAULT_RECENT_LIMIT reduced from 120 to 60 to match MAX_ITEMS_LIMIT so
seeded entries beyond the handler cap are not written unnecessarily
- Add tests/sanctions-pressure.test.mjs covering all three invariants
* fix(sanctions): register sanctions:pressure:v1 in health.js BOOTSTRAP_KEYS and SEED_META
Adds sanctionsPressure to health.js so the health endpoint monitors the
seeded key for emptiness (CRIT) and freshness via seed-meta:sanctions:pressure
(maxStaleMin: 720 matches 12h seed TTL). Without this, health was blind to
stale or missing sanctions data.
- Renamed "Revenue" tab to "US Revenue" for clarity
- Added 12-month bar chart above the table showing customs revenue trend
- Bars highlighted in red when revenue exceeds 1.5x prior year average
- Makes the tariff impact (308% YoY increase) visually obvious
Visual redesign of prediction market cards:
- Source badge (KALSHI/POLYMARKET) moved to card header with matching
left accent stripe (blue for Kalshi, purple for Polymarket)
- Gradient bars replace flat colors, with inner glow on strong signals
- Conviction labels (Lean Yes / Lean No / Toss-up) based on threshold
- Rounded bar corners (6px), thicker bars (28px), better spacing
- Hover state on cards for interactivity feel
* fix: use data-font attribute instead of inline style for font toggle
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* fix: use inline style for font-body-base to win cascade over variant themes
Copilot review flagged that [data-font="system"] in @layer base loses to
unlayered variant themes (e.g. happy-theme sets --font-body: Nunito).
Switch back to inline style on --font-body-base so user font preference
always wins the cascade, while keeping the RTL/CJK composition via
var(--font-body-base) fallback.
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Elie Habib <elie.habib@gmail.com>
* feat(supply-chain): restructure panel for scannable metrics and richer shipping tab
Chokepoints tab: replace text blob description with structured metric rows
showing warnings, vessels, WoW change, disruption %, and risk level. Move
routing advisory into styled callout box on expand. Remove duplicated
riskSummary and warning counts from server description field.
Shipping Rates tab: add corridor disruption snapshot table showing per-corridor
vessel counts, WoW traffic changes, disruption %, and risk levels. Tab now
renders useful data even without FRED API key.
Add CSS for expanded card state (previously no-op), metric rows, disruption
color coding, routing advisory callout, and disruption table.
* test(supply-chain): add restructure tests, update description assertion
Add 7 structural tests for panel restructure:
- activeHasData accepts chokepointData without FRED
- renderShipping delegates to renderDisruptionSnapshot
- null/empty loading states
- data-cp-id and data-chart-cp preservation
- conditional description rendering
- server description no longer duplicates warning counts
Update existing test: description now asserts counts are NOT in text
(they moved to structured fields).
* fix(supply-chain): increase Redis timeout for PortWatch and remove content height cap
Root cause: getCachedJson has a 1500ms timeout, but the PortWatch
payload (~149KB for 13 chokepoints x 175 days) exceeds this on
high-latency Edge regions. The fetch silently times out and returns
null, so the handler builds responses with empty transit summaries.
Fix: add optional timeoutMs param to getCachedJson, use 5000ms for
the PortWatch fetch. Also remove the 300px max-height on
.economic-content so the Supply Chain panel fills available height.
* refactor(supply-chain): move transit summary assembly to Railway relay
Vercel Edge was reading 3 large Redis keys (PortWatch 149KB, transit
counts, CorridorRisk) and assembling transit summaries on every request.
The 1500ms Redis timeout caused the 149KB PortWatch fetch to silently
fail on high-latency Edge regions (Mumbai bom1), leaving all transit
data empty.
Now Railway builds the pre-assembled transit summaries (including
anomaly detection) and writes them to a single key. Vercel reads
ONE small pre-built key instead of 3 raw keys.
Flow: Railway seeds PortWatch + transit counts -> builds summaries ->
writes supply_chain:transit-summaries:v1 -> Vercel reads it.
This follows the gold standard: "Vercel reads Redis ONLY; Railway
makes ALL external API calls and data assembly."
* test(supply-chain): add sync tests for relay threat levels and name mappings
detectTrafficAnomalyRelay and CHOKEPOINT_THREAT_LEVELS in the relay are
duplicated from _scoring.mjs and get-chokepoint-status.ts because
ais-relay.cjs is CJS. Added sync tests that validate:
- Every canonical chokepoint has a relay threat level
- Relay threat levels match handler CHOKEPOINTS config
- RELAY_NAME_TO_ID covers all canonical chokepoints
This catches drift between the two source-of-truth files.
* fix(ui): restore bounded scroll on economic-content with flex layout
The previous fix replaced max-height: 300px with flex: 1 1 auto, but
.panel-content was not a flex container so the flex rule was ignored.
This caused tabs to scroll away with the content.
Fix: use :has(.economic-content) to make .panel-content a flex column
only for panels with tabbed economic content. Tabs stay pinned, content
area scrolls independently.
* feat(supply-chain): fix CorridorRisk API integration (open beta, no key needed)
The CorridorRisk API is in open beta at corridorrisk.io/api/corridors
(not api.corridorrisk.io/v1/corridors). No API key required during beta.
Changes:
- Fix URL to corridorrisk.io/api/corridors
- Remove API key requirement (open beta)
- Update name matching for actual API names (e.g. "Persian Gulf &
Strait of Hormuz" -> hormuz_strait)
- Derive riskLevel from score (>=70 critical, >=50 high, etc.)
- Store riskScore, vesselCount, eventCount7d, riskSummary
- Feed CorridorRisk data into transit summaries
* test(supply-chain): comprehensive transit summary integration tests
75 tests across 10 suites covering:
- Relay seedTransitSummaries assembly (Redis key, fields, triggers)
- CorridorRisk name mapping and risk level derivation from score
- Handler reads pre-built summaries (not raw upstream keys)
- Handler isolation: no PortWatchData/CorridorRiskData/CANONICAL_CHOKEPOINTS imports
- detectTrafficAnomalyRelay sync with _scoring.mjs (side-by-side execution)
- detectTrafficAnomaly edge cases (boundaries, threat levels, unsorted history)
- CHOKEPOINT_THREAT_LEVELS relay-handler sync validation
* fix(supply-chain): hydrate transit summaries from Redis on relay restart
After relay restart, latestPortwatchData and latestCorridorRiskData are
null. The initial seedTransitSummaries call (35s after boot) would
return early with no data, leaving the transit-summaries:v1 key stale
until the next PortWatch seed completes (6+ seconds later).
Fix: seedTransitSummaries now reads persisted PortWatch and CorridorRisk
data from Redis when in-memory state is empty. This covers the cold-start
gap so Vercel always has fresh transit summaries.
Also adds 5 tests validating the hydration path order and assignment.
* fix(supply-chain): add fallback to raw Redis keys when pre-built summaries are empty
P1: If supply_chain:transit-summaries:v1 is absent (relay not deployed,
restart in progress, or transient PortWatch failure), the handler now
falls back to reading the raw portwatch, corridorrisk, and transit count
keys directly and assembling summaries on the fly.
This ensures corridor risk data (riskLevel, incidentCount7d, disruptionPct)
is never silently zeroed out, and users keep history/counts even during
the 6-hour PortWatch re-seed window.
Strategy: pre-built summaries (fast path) -> raw keys fallback (slow path)
-> all-zero defaults (last resort).
* feat(ui): add glowing border pulse effect for search highlights
Replace subtle background fade with a 3-pulse blue glow animation
(3s duration) on panels and map countries when navigating via search
or breaking news alerts. Helps users spot the target result.
* fix(map): reset country highlight opacity after pulse animation ends
Without this, fill-opacity decays to near-zero at animation end,
leaving the country nearly invisible until the brief panel closes.
* fix(ui): cancel stale highlight timers and clean up pulse RAF on destroy
- Cancel previous setTimeout before scheduling new highlight, preventing
premature class removal on rapid re-search (Codex P2)
- Extract shared applyHighlight() in SearchManager with WeakMap tracking
- Cancel countryPulseRaf in DeckGLMap.destroy() to prevent orphaned RAF
* fix(ui): theme-aware highlight opacity and per-element timer tracking
- Use getHighlightRestOpacity() to read current map theme instead of
hardcoded dark-theme values (0.12), fixing light-theme regression
- Pulse animation base values now scale from theme-correct resting opacity
- BreakingNewsBanner: switch from single timer slot to WeakMap per-element
tracking, preventing permanent highlight on rapid alert clicks
* feat(telegram): add media support, LIVE indicator, and error state
- Display images and videos from Telegram messages with media grid
- Add LIVE indicator badge for messages < 10 minutes old
- Show error state when relay returns errors
- Add "View Source" action button per message
- Update TelegramItem type with optional mediaUrls field
Co-authored-by: lspassos1 <lspassos@icloud.com>
* fix(telegram): escape HTML entities in message text, add noopener to media links
- Escape &, <, >, " in item.text before safeHtml() to prevent
untrusted Telegram messages from injecting links or styling
- Add noopener,noreferrer to window.open() on image click to
prevent tabnabbing via window.opener reference
* fix(telegram): guard null text, add missing i18n keys
- Guard item.text with fallback to empty string for media-only messages
- Add missing viewSource and live i18n keys to all 21 locale files
- Replace hardcoded 'LIVE' string with t() call
* fix(telegram): surface relay errors to panel error state
data-loader catch block was swallowing fetch failures silently.
Now forwards error to TelegramIntelPanel.setData() so users see
the error state UI instead of a stale/empty panel.
* fix(telegram): remove hardcoded English error fallback
Use enabled:false without error field to trigger the existing
translated disabled message in TelegramIntelPanel.
---------
Co-authored-by: lspassos1 <lspassos@icloud.com>
* feat(panels): improve drag UX and add close buttons to live panels
* fix: address PR feedback from koala73 and SebastienMelki
* fix: remove unused variable in llm-health probe
---------
Co-authored-by: rayanhabib07 <rayanhabib07@gmail.com>
* feat(predictions): add Kalshi as prediction market data source
* fix(predictions): address Kalshi integration review feedback
- Gate Kalshi fetch behind category check to avoid wasted calls on tech-scoped requests
- Replace fragile double-cast bootstrap typing with BootstrapMarket interface
- Fix zero-price falsy bug in seed script using Number.isFinite guard
- Align RPC market selection with seed script (highest-volume via single-pass loop)
- Raise Kalshi volume threshold to 5000 for signal quality parity
- Add missing .prediction-source badge CSS with per-source color variants
* fix(predictions): address P1/P2 review items for Kalshi integration
- Apply isExcluded() filter and volume threshold (5000) to live Kalshi
RPC path so cache-miss results match seed curation quality
- Include FINANCE_TAGS in seed allTags so 'markets' tag is fetched
- Align Kalshi title mapping (market.title || event.title) between
seed and RPC handler
- Remove silent geopolitical fallback for finance variant so missing
finance bootstrap falls through to RPC fetch
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(predictions): prefer yes_sub_title for Kalshi multi-contract events
For multi-contract Kalshi events (e.g. papal election candidates),
market.title is the generic event question while yes_sub_title
identifies the specific contract. Use yes_sub_title when present
in both seed and RPC paths so titles are accurate and consistent.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(predictions): use general Kalshi trading API subdomain
Switch from api.elections.kalshi.com (elections-only) to
trading-api.kalshi.com so economy, crypto, and other non-election
markets are included in the finance variant.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Elie Habib <elie.habib@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>