release: v2.4.45 — Live Webcams verified-live default grid + e2e drift fixes (runtime-fetch, keyword-spike)

- Live Webcams: replace dead default feeds with browser-verified-live 24/7 streams
  (grid = Kyiv/Tel-Aviv/Miami/Taipei, each confirmed PLAYING/isLive this session via
  the YouTube IFrame API in a full-codec browser); prune 6 non-embeddable feeds, add
  Monterey Bay. Fixes the "mostly not available" webcam panel.
- e2e (dev-only, no bundle impact): runtime-fetch fallback test → current /v1/world/*
  paths + real host discriminator; keyword-spike test → mid-sentence proper-noun +
  initI18n + opt-in badge flag; drop the obsolete arch-download test (feature removed
  for Hanzo); loadMarkets test → yahoo-batch envelope, no commodities. 7/7 pass.

Also carries the analyst-dock hzc-* selector drift fixes (19/19 pass).

Claude-Session: https://claude.ai/code/session_01NP4ehjWW2h98FkE4YjUKuJ
This commit is contained in:
hanzo-dev
2026-07-22 04:55:52 -07:00
parent 973a57e55b
commit eb34d6bb35
4 changed files with 55 additions and 109 deletions
+23 -6
View File
@@ -10,11 +10,22 @@ test.describe('keyword spike modal/badge flow', () => {
const trending = await import('/src/services/trending-keywords.ts');
const correlation = await import('/src/services/correlation.ts');
// The badge/modal render through i18n (t()); the app bootstrap initializes it
// before any component mounts. This isolated harness must do the same, else
// t() returns undefined and getSignalContext().actionableInsight.split() throws.
const { initI18n } = await import('/src/services/i18n.ts');
await initI18n();
const previousConfig = trending.getTrendingConfig();
const headerRight = document.createElement('div');
headerRight.className = 'header-right';
document.body.appendChild(headerRight);
// The findings badge is opt-in (default OFF): its constructor only mounts and
// renders when the user has enabled it. Simulate an opted-in session so the
// badge mounts into .header-right and its count/dropdown render.
localStorage.setItem('worldmonitor-intel-findings', 'shown');
const modal = new SignalModal();
const badge = new IntelligenceGapBadge();
badge.setOnSignalClick((signal) => modal.showSignal(signal));
@@ -27,13 +38,18 @@ test.describe('keyword spike modal/badge flow', () => {
});
const now = new Date();
// Keep the trending proper noun ("Iran") mid-sentence and capitalized: the
// significance filter (isLikelyProperNoun) only counts capitalization for
// tokens that appear past position 0, so a sentence-leading term reads as an
// ordinary word and gets suppressed. Mid-sentence, it registers as a genuine
// entity spiking across sources — no ML worker required.
const headlines = [
{ source: 'Reuters', title: 'Iran sanctions pressure rises amid talks', link: 'https://example.com/reuters/1' },
{ source: 'AP', title: 'Iran sanctions debate intensifies in Washington', link: 'https://example.com/ap/1' },
{ source: 'BBC', title: 'Iran sanctions trigger fresh market concerns', link: 'https://example.com/bbc/1' },
{ source: 'Reuters', title: 'Iran sanctions package draws regional response', link: 'https://example.com/reuters/2' },
{ source: 'AP', title: 'Iran sanctions proposal gains momentum', link: 'https://example.com/ap/2' },
{ source: 'BBC', title: 'Iran sanctions timeline shortens after warnings', link: 'https://example.com/bbc/2' },
{ source: 'Reuters', title: 'Talks on Iran sanctions stall in Washington', link: 'https://example.com/reuters/1' },
{ source: 'AP', title: 'New Iran sanctions debate intensifies among allies', link: 'https://example.com/ap/1' },
{ source: 'BBC', title: 'Markets watch Iran sanctions with fresh concern', link: 'https://example.com/bbc/1' },
{ source: 'Reuters', title: 'Regional powers weigh Iran sanctions package', link: 'https://example.com/reuters/2' },
{ source: 'AP', title: 'Momentum grows for Iran sanctions proposal', link: 'https://example.com/ap/2' },
{ source: 'BBC', title: 'Analysts see Iran sanctions timeline shortening', link: 'https://example.com/bbc/2' },
].map(item => ({
...item,
pubDate: now,
@@ -101,6 +117,7 @@ test.describe('keyword spike modal/badge flow', () => {
if (store?.previousConfig) {
trending.updateTrendingConfig(store.previousConfig);
}
localStorage.removeItem('worldmonitor-intel-findings');
delete (window as unknown as Record<string, unknown>).__keywordSpikeTest;
});
});
+28 -94
View File
@@ -93,17 +93,22 @@ test.describe('desktop runtime routing guardrails', () => {
calls.push(url);
if (url.includes('127.0.0.1:46123/api/fred-data')) {
// The runtime patch only intercepts /v1/world/* paths. It first hits the
// local sidecar (127.0.0.1:46123); on failure it retries the same path
// against the remote API base — same-origin under VITE_VARIANT=full, so the
// fallback arrives as a relative /v1/world/* URL, distinct from the absolute
// local attempt.
if (url.includes('127.0.0.1:46123/v1/world/fred-data')) {
return responseJson({ error: 'missing local api key' }, 500);
}
if (url.includes('worldmonitor.app/api/fred-data')) {
if (url.startsWith('/v1/world/fred-data')) {
return responseJson({ observations: [{ value: '321.5' }] }, 200);
}
if (url.includes('127.0.0.1:46123/api/stablecoin-markets')) {
if (url.includes('127.0.0.1:46123/v1/world/stablecoin-markets')) {
throw new Error('ECONNREFUSED');
}
if (url.includes('worldmonitor.app/api/stablecoin-markets')) {
if (url.startsWith('/v1/world/stablecoin-markets')) {
return responseJson({ stablecoins: [{ symbol: 'USDT' }] }, 200);
}
@@ -117,10 +122,10 @@ test.describe('desktop runtime routing guardrails', () => {
try {
runtime.installRuntimeFetchPatch();
const fredResponse = await window.fetch('/api/fred-data?series_id=CPIAUCSL');
const fredResponse = await window.fetch('/v1/world/fred-data?series_id=CPIAUCSL');
const fredBody = await fredResponse.json() as { observations?: Array<{ value: string }> };
const stableResponse = await window.fetch('/api/stablecoin-markets');
const stableResponse = await window.fetch('/v1/world/stablecoin-markets');
const stableBody = await stableResponse.json() as { stablecoins?: Array<{ symbol: string }> };
return {
@@ -146,10 +151,10 @@ test.describe('desktop runtime routing guardrails', () => {
expect(result.stableStatus).toBe(200);
expect(result.stableSymbol).toBe('USDT');
expect(result.calls.some((url) => url.includes('127.0.0.1:46123/api/fred-data'))).toBe(true);
expect(result.calls.some((url) => url.includes('worldmonitor.app/api/fred-data'))).toBe(true);
expect(result.calls.some((url) => url.includes('127.0.0.1:46123/api/stablecoin-markets'))).toBe(true);
expect(result.calls.some((url) => url.includes('worldmonitor.app/api/stablecoin-markets'))).toBe(true);
expect(result.calls.some((url) => url.includes('127.0.0.1:46123/v1/world/fred-data'))).toBe(true);
expect(result.calls.some((url) => url.startsWith('/v1/world/fred-data'))).toBe(true);
expect(result.calls.some((url) => url.includes('127.0.0.1:46123/v1/world/stablecoin-markets'))).toBe(true);
expect(result.calls.some((url) => url.startsWith('/v1/world/stablecoin-markets'))).toBe(true);
});
test('chunk preload reload guard is one-shot until app boot clears it', async ({ page }) => {
@@ -225,64 +230,12 @@ test.describe('desktop runtime routing guardrails', () => {
expect(result.storedValue).toBe('1');
});
test('update badge picks architecture-correct desktop download url', async ({ page }) => {
await page.goto('/tests/runtime-harness.html');
const result = await page.evaluate(async () => {
const { App } = await import('/src/App.ts');
const globalWindow = window as unknown as {
__TAURI__?: { core?: { invoke?: (command: string) => Promise<unknown> } };
};
const previousTauri = globalWindow.__TAURI__;
const releaseUrl = 'https://github.com/koala73/worldmonitor/releases/latest';
const appProto = App.prototype as unknown as {
resolveUpdateDownloadUrl: (releaseUrl: string) => Promise<string>;
mapDesktopDownloadPlatform: (os: string, arch: string) => string | null;
};
const fakeApp = {
mapDesktopDownloadPlatform: appProto.mapDesktopDownloadPlatform,
};
try {
globalWindow.__TAURI__ = {
core: {
invoke: async (command: string) => {
if (command !== 'get_desktop_runtime_info') throw new Error(`Unexpected command: ${command}`);
return { os: 'macos', arch: 'aarch64' };
},
},
};
const macArm = await appProto.resolveUpdateDownloadUrl.call(fakeApp, releaseUrl);
globalWindow.__TAURI__ = {
core: {
invoke: async () => ({ os: 'windows', arch: 'amd64' }),
},
};
const windowsX64 = await appProto.resolveUpdateDownloadUrl.call(fakeApp, releaseUrl);
globalWindow.__TAURI__ = {
core: {
invoke: async () => ({ os: 'linux', arch: 'x86_64' }),
},
};
const linuxFallback = await appProto.resolveUpdateDownloadUrl.call(fakeApp, releaseUrl);
return { macArm, windowsX64, linuxFallback };
} finally {
if (previousTauri === undefined) {
delete globalWindow.__TAURI__;
} else {
globalWindow.__TAURI__ = previousTauri;
}
}
});
expect(result.macArm).toBe('https://worldmonitor.app/api/download?platform=macos-arm64');
expect(result.windowsX64).toBe('https://worldmonitor.app/api/download?platform=windows-exe');
expect(result.linuxFallback).toBe('https://github.com/koala73/worldmonitor/releases/latest');
});
// The upstream worldmonitor.app update-badge + arch-aware desktop-download
// machinery (App.resolveUpdateDownloadUrl / mapDesktopDownloadPlatform, driven by
// the get_desktop_runtime_info Tauri command) was removed for Hanzo —
// App.checkForUpdate is now a deliberate no-op and downloads are served from
// /v1/world/download?platform=* via DownloadBanner. No behavior remains to assert
// here, so the obsolete arch-resolution test was removed.
test('loadMarkets keeps Yahoo-backed data when Finnhub is skipped', async ({ page }) => {
await page.goto('/tests/runtime-harness.html');
@@ -324,8 +277,6 @@ test.describe('desktop runtime routing guardrails', () => {
const marketConfigErrors: string[] = [];
const heatmapRenders: number[] = [];
const heatmapConfigErrors: string[] = [];
const commoditiesRenders: number[] = [];
const commoditiesConfigErrors: string[] = [];
const cryptoRenders: number[] = [];
const apiStatuses: Array<{ name: string; status: string }> = [];
@@ -334,7 +285,7 @@ test.describe('desktop runtime routing guardrails', () => {
calls.push(url);
const parsed = new URL(url);
if (parsed.pathname === '/api/finnhub') {
if (parsed.pathname === '/v1/world/finnhub') {
return responseJson({
quotes: [],
skipped: true,
@@ -342,12 +293,14 @@ test.describe('desktop runtime routing guardrails', () => {
});
}
if (parsed.pathname === '/api/yahoo-finance') {
const symbol = parsed.searchParams.get('symbol') ?? 'UNKNOWN';
return responseJson(yahooChart(symbol));
if (parsed.pathname === '/v1/world/yahoo-batch') {
const symbols = (parsed.searchParams.get('symbols') ?? '').split(',').filter(Boolean);
return responseJson({
results: symbols.map((symbol) => ({ symbol, chart: yahooChart(symbol) })),
});
}
if (parsed.pathname === '/api/coingecko') {
if (parsed.pathname === '/v1/world/coingecko') {
return responseJson([
{ id: 'bitcoin', current_price: 50000, price_change_percentage_24h: 1.2, sparkline_in_7d: { price: [1, 2, 3] } },
{ id: 'ethereum', current_price: 3000, price_change_percentage_24h: -0.5, sparkline_in_7d: { price: [1, 2, 3] } },
@@ -369,10 +322,6 @@ test.describe('desktop runtime routing guardrails', () => {
renderHeatmap: (data: Array<unknown>) => heatmapRenders.push(data.length),
showConfigError: (message: string) => heatmapConfigErrors.push(message),
},
commodities: {
renderCommodities: (data: Array<unknown>) => commoditiesRenders.push(data.length),
showConfigError: (message: string) => commoditiesConfigErrors.push(message),
},
crypto: {
renderCrypto: (data: Array<unknown>) => cryptoRenders.push(data.length),
},
@@ -388,25 +337,14 @@ test.describe('desktop runtime routing guardrails', () => {
await (App.prototype as unknown as { loadMarkets: (thisArg: unknown) => Promise<void> })
.loadMarkets.call(fakeApp);
const commoditySymbols = ['^VIX', 'GC=F', 'CL=F', 'NG=F', 'SI=F', 'HG=F'];
const commodityYahooCalls = commoditySymbols.map((symbol) =>
calls.some((url) => {
const parsed = new URL(url);
return parsed.pathname === '/api/yahoo-finance' && parsed.searchParams.get('symbol') === symbol;
})
);
return {
marketRenders,
marketConfigErrors,
heatmapRenders,
heatmapConfigErrors,
commoditiesRenders,
commoditiesConfigErrors,
cryptoRenders,
apiStatuses,
latestMarketsCount: fakeApp.latestMarkets.length,
commodityYahooCalls,
};
} finally {
window.fetch = originalFetch;
@@ -420,10 +358,6 @@ test.describe('desktop runtime routing guardrails', () => {
expect(result.heatmapRenders.length).toBe(0);
expect(result.heatmapConfigErrors).toEqual(['FINNHUB_API_KEY not configured — add in Settings']);
expect(result.commoditiesRenders.some((count) => count > 0)).toBe(true);
expect(result.commoditiesConfigErrors.length).toBe(0);
expect(result.commodityYahooCalls.every(Boolean)).toBe(true);
expect(result.cryptoRenders.some((count) => count > 0)).toBe(true);
expect(result.apiStatuses.some((entry) => entry.name === 'Finnhub' && entry.status === 'error')).toBe(true);
expect(result.apiStatuses.some((entry) => entry.name === 'CoinGecko' && entry.status === 'ok')).toBe(true);
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "@hanzo/world",
"description": "Hanzo World — real-time global intelligence dashboard.",
"private": true,
"version": "2.4.44",
"version": "2.4.45",
"type": "module",
"scripts": {
"lint:md": "markdownlint-cli2 '**/*.md'",
+3 -8
View File
@@ -20,23 +20,18 @@ interface WebcamFeed {
const WEBCAM_FEEDS: WebcamFeed[] = [
// Middle East
{ id: 'tel-aviv', city: 'Tel Aviv', country: 'Israel', region: 'middle-east', channelHandle: '@IsraelLiveCam', fallbackVideoId: '-VLcYT5QBrY' },
{ id: 'mecca', city: 'Mecca', country: 'Saudi Arabia', region: 'middle-east', channelHandle: '@MakkahLive', fallbackVideoId: 'DEcpmPUbkDQ' },
// Europe
{ id: 'kyiv', city: 'Kyiv', country: 'Ukraine', region: 'europe', channelHandle: '@DWNews', fallbackVideoId: '-Q7FuPINDjA' },
{ id: 'odessa', city: 'Odessa', country: 'Ukraine', region: 'europe', channelHandle: '@UkraineLiveCam', fallbackVideoId: 'e2gC37ILQmk' },
{ id: 'paris', city: 'Paris', country: 'France', region: 'europe', channelHandle: '@PalaisIena', fallbackVideoId: 'OzYp4NRZlwQ' },
{ id: 'st-petersburg', city: 'St. Petersburg', country: 'Russia', region: 'europe', channelHandle: '@SPBLiveCam', fallbackVideoId: 'CjtIYbmVfck' },
{ id: 'london', city: 'London', country: 'UK', region: 'europe', channelHandle: '@EarthCam', fallbackVideoId: 'Lxqcg1qt0XU' },
// Americas
{ id: 'new-york', city: 'New York', country: 'USA', region: 'americas', channelHandle: '@EarthCam', fallbackVideoId: '4qyZLflp-sI' },
{ id: 'los-angeles', city: 'Los Angeles', country: 'USA', region: 'americas', channelHandle: '@VeniceVHotel', fallbackVideoId: 'EO_1LWqsCNE' },
{ id: 'miami', city: 'Miami', country: 'USA', region: 'americas', channelHandle: '@FloridaLiveCams', fallbackVideoId: '5YCajRjvWCg' },
// Asia-Pacific — Taipei first (strait hotspot), then Shanghai, Tokyo, Seoul
{ id: 'monterey', city: 'Monterey Bay', country: 'USA', region: 'americas', channelHandle: '@montereybayaquarium', fallbackVideoId: 'zL68biE6wAs' },
// Asia-Pacific — Taipei (strait hotspot), then Shanghai
{ id: 'taipei', city: 'Taipei', country: 'Taiwan', region: 'asia', channelHandle: '@JackyWuTaipei', fallbackVideoId: 'z_fY1pj1VBw' },
{ id: 'shanghai', city: 'Shanghai', country: 'China', region: 'asia', channelHandle: '@SkylineWebcams', fallbackVideoId: '76EwqI5XZIc' },
{ id: 'tokyo', city: 'Tokyo', country: 'Japan', region: 'asia', channelHandle: '@TokyoLiveCam4K', fallbackVideoId: '4pu9sF5Qssw' },
{ id: 'seoul', city: 'Seoul', country: 'South Korea', region: 'asia', channelHandle: '@UNvillage_live', fallbackVideoId: '-JhoMGoAfFc' },
{ id: 'sydney', city: 'Sydney', country: 'Australia', region: 'asia', channelHandle: '@WebcamSydney', fallbackVideoId: '7pcL-0Wo77U' },
];
const MAX_GRID_CELLS = 4;
@@ -72,7 +67,7 @@ export class LiveWebcamsPanel extends Panel {
return WEBCAM_FEEDS.filter(f => f.region === this.regionFilter);
}
private static readonly ALL_GRID_IDS = ['kyiv', 'mecca', 'london', 'new-york'];
private static readonly ALL_GRID_IDS = ['kyiv', 'tel-aviv', 'miami', 'taipei'];
private get gridFeeds(): WebcamFeed[] {
if (this.regionFilter === 'all') {