Commit Graph

372 Commits

Author SHA1 Message Date
Elie Habib
663a58bf80 fix(market): route sectors/commodities to correct RPC endpoints (#2198)
* 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
2026-03-24 17:26:29 +04:00
Hani Mounla
77f4e8f995 Add github stars count to header (#2195)
Co-authored-by: Elie Habib <elie.habib@gmail.com>
2026-03-24 16:42:22 +04:00
Elie Habib
93ce10524c feat(gdelt-intel): tone/vol sparkline summary bar in GdeltIntelPanel (#2152)
* 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)
2026-03-23 20:59:28 +04:00
Elie Habib
3f0c407be5 fix(bigmac): increase CSS specificity for country column left-align (#2144)
.gb-table .gb-item-col/.gb-item-name (0,2,0) beats .gb-table th (0,1,1)
browser default. Belt-and-suspenders for the fix already in #2131.
2026-03-23 15:52:12 +04:00
Elie Habib
1d28c352da feat(commodities): expand tracking to 23 symbols — agriculture and coal (#2135)
* 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)
2026-03-23 14:19:20 +04:00
Elie Habib
638f6320a5 fix(bigmac): left-align country column header and cells (#2131) 2026-03-23 11:10:35 +04:00
Elie Habib
a202b8ebcc feat(consumer-prices): global All view as default, market selector, per-market cache keys (#2128)
* 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
2026-03-23 10:58:37 +04:00
Elie Habib
b9e0286360 feat(consumer-prices): add market selector — panel is global, not AE-only (#2123)
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.
2026-03-23 10:17:50 +04:00
Elie Habib
efb3afd809 fix(panels): align font sizes, border-radius, and font-family with app style (#2122)
* 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
2026-03-23 10:16:23 +04:00
Elie Habib
e648689918 fix(free-tier): exclude cw-* panels from quota count and disable on launch (#2118)
* 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
2026-03-23 09:45:31 +04:00
Elie Habib
32ab184027 feat(mcp): apply review fixes for auto-visualization widget (P1/P2/P3) (#2081)
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.
2026-03-23 01:05:51 +04:00
Elie Habib
a09d6da758 feat(mcp): auto-generate interactive widget from JSON data (#2076)
* 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.
2026-03-23 00:46:45 +04:00
Elie Habib
ddc6603cce feat(infra): Cloudflare Radar DDoS attacks + traffic anomaly endpoints (#2067)
* feat(infra): add Cloudflare Radar DDoS attacks + traffic anomaly endpoints

Extends the existing Cloudflare Radar integration (internet outages) with
two new data streams, both confirmed accessible with the current token:
- L3/L4 DDoS attack summaries (protocol + vector breakdowns, 7d window)
- Traffic anomaly events (DNS/BGP/ICMP anomalies with country + ASN context)

Changes:
- proto: add DdosAttackSummaryEntry + TrafficAnomaly messages; new
  list_internet_ddos_attacks.proto and list_internet_traffic_anomalies.proto;
  wire two new RPCs into InfrastructureService
- buf generate: regenerated server/client TypeScript from updated protos
- seed-internet-outages.mjs: add fetchDdosData() + fetchTrafficAnomalies()
  called inside fetchAll() before runSeed() (process.exit-safe pattern);
  writes cf:radar:ddos:v1 and cf:radar:traffic-anomalies:v1
- list-ddos-attacks.ts + list-traffic-anomalies.ts: read-from-seed handlers
- handler.ts: wire new handlers
- cache-keys.ts + api/bootstrap.js: add ddosAttacks + trafficAnomalies
  bootstrap keys (fast tier); kept in sync to pass bootstrap parity tests
- gateway.ts: add RPC_CACHE_TIER entries (slow) for new routes
- services/infrastructure: add fetchDdosAttacks() + fetchTrafficAnomalies()
  with circuit breakers + hydration support

UI surface (cards alongside outage map) deferred to follow-up.

Closes #2043

* fix(i18n): rename Internet Outages → Internet Disruptions

Broader term covers outages, DDoS events, and traffic anomalies now
seeded from Cloudflare Radar. Updated in en.json (layer label, tooltip,
country brief count strings), map-layer-definitions.ts fallback label,
and commands.ts search keywords.

Other locales retain their translated strings (not degraded — they
already use broader equivalents like "internet disruption" in many langs).

* feat(map): render traffic anomalies + DDoS target locations on disruptions layer

Adds geo-coordinates to both data types so they appear as map markers
under the Internet Disruptions toggle alongside existing outage circles.

- Proto: add latitude/longitude to TrafficAnomaly (fields 10/11), add new
  DdosLocationHit message, add top_target_locations to DdosAttacksResponse
- Seeder: resolve lat/lon from COUNTRY_COORDS for traffic anomalies; fetch
  CF Radar top/locations/target endpoint for DDoS top-target locations
- Server handler: pass topTargetLocations through from Redis seed cache
- DeckGLMap: amber trafficAnomaly layer + purple ddosHit layer with tooltips
- GlobeMap: TrafficAnomalyMarker + DdosHitMarker with emoji indicators
- MapContainer: expose setTrafficAnomalies() + setDdosLocations() setters
- data-loader: fire-and-forget anomaly/DDoS fetches after outages load

* fix(review): address code review findings + add Internet Disruptions panel

- fix: totalCount returns filtered count when country param is set
- fix: countryName uses clientCountryName fallback (was always empty)
- fix: remove duplicate toEpochMsFromIso (consolidate into toEpochMs)
- fix: anomalies guard >= 0 → > 0 (don't write empty array to Redis)
- fix: GlobeMap uses named top-level imports instead of inline imports
- feat: InternetDisruptionsPanel with 3 tabs (Outages / DDoS / Anomalies)
2026-03-22 22:58:41 +04:00
Elie Habib
6d66c06f07 fix(consumer-prices): pipeline hardening, basket spread fix, panel bugs, sw-update test sync (#2040)
* 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.
2026-03-22 11:46:40 +04:00
Elie Habib
f2dea9f3d7 feat(map): show hourglass on aviation layer while fetching positions (#2013)
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.
2026-03-21 21:35:01 +04:00
Elie Habib
2e16159bb6 feat(economic): WoW price tracking + weekly cadence for BigMac & Grocery panels (#1974)
* 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."
2026-03-21 10:56:48 +04:00
Elie Habib
3aa8e627fc fix(wingbits): clamp popup to viewport after live data loads + cap photo height (#1973) 2026-03-21 10:11:10 +04:00
Elie Habib
bed14e859c fix(wingbits): move photo to bottom, increase popup height, fix missing i18n keys (#1959) 2026-03-21 07:59:45 +04:00
Elie Habib
ab2ac1b604 fix(economic): expand bigmac to global, add panel CSS, improve consumer prices empty state (#1962)
- 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)
2026-03-21 07:58:15 +04:00
Elie Habib
a583bea3a8 fix(debt-panel): rename to Global Debt Clock, fix active tab white rectangle (#1949) 2026-03-20 20:57:49 +04:00
Elie Habib
6dbe4f17bf feat(wingbits): show flight route, times, and plane photo in popup (#1947)
* 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
2026-03-20 20:50:35 +04:00
Nishchaya Sharma
94500ebded fix(pwa): show cached dashboard state when offline (#1907)
* 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>
2026-03-20 19:46:31 +04:00
Berk Demirci
23b2be3a90 feat: add explicit emoji key to AI strategic posture panel (#1922)
* feat: add explicit emoji key to AI strategic posture panel

Resolves koala73/worldmonitor#1589

* fix: add Auxiliary emoji entry and revert package-lock.json
2026-03-20 18:37:03 +04:00
Elie Habib
c6099d785a fix(economic): fix national debt panel hang + add lazy pagination (#1928)
- 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)
2026-03-20 17:11:41 +04:00
Elie Habib
2fd98fa407 feat(economic): add global debt ticker header with deficit/surplus breakdown (#1927)
* 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
2026-03-20 16:38:02 +04:00
Elie Habib
c658b8eb94 feat(economic): National Debt Clock — live ticking debt estimates for 180+ countries (#1923)
* 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.
2026-03-20 16:08:48 +04:00
Elie Habib
c0bf784d21 feat(finance): crypto sectors heatmap + DeFi/AI/Alt token panels + expanded crypto news (#1900)
* 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
2026-03-20 10:34:20 +04:00
Elie Habib
70a06041ff fix(widgets): reduce design drift between AI widgets and dashboard panels (#1849)
* 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)
2026-03-19 03:48:42 +04:00
Fayez Bast
cf1fdefe92 feat: effective tariff rate source (#1790)
* 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>
2026-03-19 03:45:32 +04:00
Elie Habib
2268df2bfc feat(mcp): add MCP data panel for user-connected servers (#1835)
* 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.
2026-03-19 02:35:35 +04:00
Elie Habib
bb92815fe0 refactor(sanctions): rename panel, restructure layout, fix stale empty cache (#1799)
* 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.
2026-03-18 10:32:06 +04:00
Elie Habib
cf48144138 feat(widgets): add Exa web search + fix widget API endpoints (#1782)
* 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)
2026-03-17 19:25:08 +04:00
Elie Habib
6d8109a85b feat(widgets): PRO interactive widgets via iframe srcdoc (#1771)
* 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
2026-03-17 18:10:10 +04:00
Elie Habib
0d4519c324 feat(ui): separate macro stress and energy complex panels (#1749)
* 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.
2026-03-17 14:36:14 +04:00
Elie Habib
babb9b6836 feat(sanctions): add OFAC sanctions pressure intelligence (#1739)
* 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.
2026-03-17 11:52:32 +04:00
Elie Habib
4353c20637 feat(widgets): AI widget builder with live WorldMonitor data (#1732) 2026-03-17 09:23:04 +04:00
Elie Habib
3897f8263d feat: add Radiation Watch with seeded anomaly intelligence, map layers, and country exposure (#1735) 2026-03-17 09:18:06 +04:00
Elie Habib
0d7b90e924 feat(trade): rename Revenue tab to US Revenue, add monthly bar chart (#1689)
- 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
2026-03-15 23:31:05 +04:00
Elie Habib
d6fb2a3eef feat(predictions): redesign panel with gradient bars, source accents, conviction labels (#1661)
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
2026-03-15 17:42:20 +04:00
Abdul Taufeeq M
327e3aa869 fix: use data-font attribute instead of inline style for font toggle (#1552)
* 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>
2026-03-15 17:39:56 +04:00
Elie Habib
d6f54ad0fc feat(supply-chain): restructure panel, add corridor disruption to shipping tab (#1652)
* 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).
2026-03-15 16:15:41 +04:00
Elie Habib
13bb3ef080 fix(supply-chain): increase Redis timeout for PortWatch and remove content height cap (#1598)
* 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).
2026-03-14 23:27:27 +04:00
Elie Habib
79ae930eba feat(ui): glowing border pulse for search highlights (#1586)
* 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
2026-03-14 19:36:42 +04:00
Elie Habib
210fe3b915 feat(telegram): add media support, LIVE indicator, and error state (#1566)
* 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>
2026-03-14 15:25:08 +04:00
Elie Habib
158e9693d8 feat(popups): enhance vessel details with flags, USNI intel, and tracking (#1567)
* feat(popups): enhance vessel details with flags, USNI intel, and tracking history

- Add flag emojis for operator countries in vessel and cluster popups
- Add collapsible USNI intel section (strike group, region, description)
- Add collapsible tracking history trail from vessel.track
- Display nearChokepoint, nearBase, lastSeen with aisGapMinutes
- Restructure header with hull badge and badge row
- Fix hemisphere bug: use N/S and E/W based on coordinate sign
- Fix lastAisUpdate null check to prevent "Invalid Date"
- Deduplicate cluster vessel rendering into shared helper
- Add i18n keys (recentTracking, lastReport, nearChokepoint,
  nearBase, lastSeen) across all 21 locale files

Co-authored-by: lspassos1 <lspassos@icloud.com>

* fix(popups): correct track coord order, fix CSS variables, remove dead styles

- Fix coordinate swap: track stores [lat, lon], was passing (lon, lat)
  to formatCoord, now correctly passes (lat, lon)
- Replace undefined --semantic-high-rgb CSS variable with literal
  rgba(255, 136, 0, ...) values matching --semantic-high: #ff8800
- Remove dead CSS classes (.popup-badge.usni-deployed/underway/in-port)
  that were never applied in HTML
- Add missing CSS for .popup-stat.warning (orange highlight for
  nearChokepoint) and .popup-stat.full-width (span full grid row)

---------

Co-authored-by: lspassos1 <lspassos@icloud.com>
2026-03-14 14:07:22 +04:00
Elie Habib
b312e0110f feat(panels): separate Windy webcam into its own panel, restore original Live Webcams (#1561) 2026-03-14 11:12:34 +04:00
Jon Torrez
987ed03f5d feat(webcams): add webcam map layer with Windy API integration (#1540) (#1540)
- Webcam markers on flat, globe, and DeckGL maps with category-based icons
- Server-side spatial queries via Redis GEOSEARCH with quantized bbox caching
- Pinned webcams panel with localStorage persistence
- Seed script for Windy API with regional bounding boxes and adaptive splitting
- Input validation (webcamId regex + encodeURIComponent) and NaN projection guards
- Bandwidth optimizations: zoom threshold, bbox overlap check, 1s cooldown
- Client-side image cache with 200-entry FIFO eviction
- Globe altitude-based viewport estimation for webcam loading
- CSP updates for webcam iframe sources
- Seed-meta key for health.js freshness tracking
2026-03-14 09:34:54 +04:00
Elie Habib
1390ae56de feat(panels): improve drag UX and add close buttons to live panels (#1550)
* 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>
2026-03-14 08:49:03 +04:00
Elie Habib
0114a7661a Add footer links (Pro, Blog, Docs, Status) to mobile sidebar (#1533)
https://claude.ai/code/session_015uawdx2PqeyTprZPhD6jwu

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-13 15:09:36 +04:00
RaulC
af7496cce1 feat(predictions): add Kalshi as prediction market data source (#1355)
* 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>
2026-03-12 23:54:20 +04:00