| import type { Monitor, PanelConfig, MapLayers } from '@/types'; |
| import { normalizeExclusiveChoropleths } from '@/components/resilience-choropleth-utils'; |
| import type { AppContext } from '@/app/app-context'; |
| import { |
| REFRESH_INTERVALS, |
| DEFAULT_PANELS, |
| DEFAULT_MAP_LAYERS, |
| MOBILE_DEFAULT_MAP_LAYERS, |
| STORAGE_KEYS, |
| SITE_VARIANT, |
| ALL_PANELS, |
| VARIANT_DEFAULTS, |
| getEffectivePanelConfig, |
| enforceFreePanelLimit, |
| restoreFreeMapPanelAccess, |
| FREE_MAX_PANELS, |
| FREE_MAX_SOURCES, |
| } from '@/config'; |
| import { sanitizeLayersForVariant } from '@/config/map-layer-definitions'; |
| import type { MapVariant } from '@/config/map-layer-definitions'; |
| import { getStoredMapModePreference } from '@/services/map-mode-preference'; |
| import { |
| initDB, |
| cleanOldSnapshots, |
| isAisConfigured, |
| initAisStream, |
| isOutagesConfigured, |
| disconnectAisStream, |
| startFlightHistoryCleanup, |
| stopFlightHistoryCleanup, |
| } from '@/services'; |
| import { enableVesselRuntime, stopLoadedVesselHistoryCleanup } from '@/services/military-vessels-lazy'; |
| import { isProUser } from '@/services/widget-store'; |
| import { mlWorker } from '@/services/ml-worker'; |
| import { getAiFlowSettings, subscribeAiFlowChange, isHeadlineMemoryEnabled } from '@/services/ai-flow-settings'; |
| import { startLearning } from '@/services/country-instability'; |
| import { loadFromStorage, parseMapUrlState, saveToStorage, isMobileDevice, showToast } from '@/utils'; |
| import { clearPanelSpans, invalidatePanelStorageCacheForKeys } from '@/utils/panel-storage'; |
| import type { ParsedMapUrlState } from '@/utils'; |
| import { BreakingNewsBanner } from '@/components/BreakingNewsBanner'; |
| import { initBreakingNewsAlerts, destroyBreakingNewsAlerts } from '@/services/breaking-news-alerts'; |
| import { markLcpDebug } from '@/utils/lcp-debug'; |
| import type { ServiceStatusPanel } from '@/components/ServiceStatusPanel'; |
| import type { MonitorPanel } from '@/components/MonitorPanel'; |
| import type { StablecoinPanel } from '@/components/StablecoinPanel'; |
| import type { EnergyCrisisPanel } from '@/components/EnergyCrisisPanel'; |
| import type { ETFFlowsPanel } from '@/components/ETFFlowsPanel'; |
| import type { MacroSignalsPanel } from '@/components/MacroSignalsPanel'; |
| import type { FearGreedPanel } from '@/components/FearGreedPanel'; |
| import type { HormuzPanel } from '@/components/HormuzPanel'; |
| import type { StrategicPosturePanel } from '@/components/StrategicPosturePanel'; |
| import type { StrategicRiskPanel } from '@/components/StrategicRiskPanel'; |
| import type { GulfEconomiesPanel } from '@/components/GulfEconomiesPanel'; |
| import type { GroceryBasketPanel } from '@/components/GroceryBasketPanel'; |
| import type { BigMacPanel } from '@/components/BigMacPanel'; |
| import type { FuelPricesPanel } from '@/components/FuelPricesPanel'; |
| import type { FaoFoodPriceIndexPanel } from '@/components/FaoFoodPriceIndexPanel'; |
| import type { OilInventoriesPanel } from '@/components/OilInventoriesPanel'; |
| import type { PipelineStatusPanel } from '@/components/PipelineStatusPanel'; |
| import type { StorageFacilityMapPanel } from '@/components/StorageFacilityMapPanel'; |
| import type { FuelShortagePanel } from '@/components/FuelShortagePanel'; |
| import type { EnergyDisruptionsPanel } from '@/components/EnergyDisruptionsPanel'; |
| import type { EnergyRiskOverviewPanel } from '@/components/EnergyRiskOverviewPanel'; |
| import type { ChokepointStripPanel } from '@/components/ChokepointStripPanel'; |
| import type { ClimateNewsPanel } from '@/components/ClimateNewsPanel'; |
| import type { ConsumerPricesPanel } from '@/components/ConsumerPricesPanel'; |
| import type { DefensePatentsPanel } from '@/components/DefensePatentsPanel'; |
| import type { MacroTilesPanel } from '@/components/MacroTilesPanel'; |
| import type { FSIPanel } from '@/components/FSIPanel'; |
| import type { YieldCurvePanel } from '@/components/YieldCurvePanel'; |
| import type { EarningsCalendarPanel } from '@/components/EarningsCalendarPanel'; |
| import type { EconomicCalendarPanel } from '@/components/EconomicCalendarPanel'; |
| import type { CotPositioningPanel } from '@/components/CotPositioningPanel'; |
| import type { LiquidityShiftsPanel } from '@/components/LiquidityShiftsPanel'; |
| import type { PositioningPanel } from '@/components/PositioningPanel'; |
| import type { GoldIntelligencePanel } from '@/components/GoldIntelligencePanel'; |
| import { isDesktopRuntime, waitForSidecarReady } from '@/services/runtime'; |
| import { hasPremiumAccess } from '@/services/panel-gating'; |
| import { BETA_MODE } from '@/config/beta'; |
| import { track, trackEvent, trackDeeplinkOpened, initAuthAnalytics } from '@/services/analytics'; |
| import { preloadCountryGeometry, isCountryGeometryLoaded, getCountryNameByCode } from '@/services/country-geometry'; |
| import { initI18n, t, I18N_RESOURCES_LOADED_EVENT, type I18nResourcesLoadedDetail } from '@/services/i18n'; |
| import { initDeferredDashboardFonts } from '@/bootstrap/secondary-startup'; |
|
|
| import { computeDefaultDisabledSources, getLocaleBoostedSources, getTotalFeedCount, FEEDS, INTEL_SOURCES } from '@/config/feeds'; |
| import { selectSourcesUnderCap, findFullyDisabledCategories } from '@/services/source-cap'; |
| import { |
| cancelBootstrapSlowTier, |
| fetchBootstrapData, |
| getBootstrapHydrationState, |
| markBootstrapAsLive, |
| waitForBootstrapSlowTier, |
| type BootstrapHydrationState, |
| } from '@/services/bootstrap'; |
| import { ensureWmSession, installWmSessionFetchInterceptor, WM_SESSION_DEGRADED_EVENT } from '@/services/wm-session'; |
| import { describeFreshness } from '@/services/persistent-cache'; |
| import { DesktopUpdater } from '@/app/desktop-updater'; |
| import { CountryIntelManager } from '@/app/country-intel'; |
| import { registerWebMcpTools } from '@/services/webmcp'; |
| import { refreshDataFreshnessFromHealth } from '@/services/health-freshness'; |
| import { scheduleAfterFirstPaint } from '@/utils/after-paint'; |
| import type { SearchManager } from '@/app/search-manager'; |
| import { RefreshScheduler } from '@/app/refresh-scheduler'; |
| import { PanelLayoutManager } from '@/app/panel-layout'; |
| import { DataLoaderManager } from '@/app/data-loader'; |
| import { EventHandlerManager } from '@/app/event-handlers'; |
| import { replaceRawI18nKeyPlaceholders } from '@/app/i18n-raw-key-healer'; |
| import { startAccountAuthHandoff } from '@/app/account-auth-handoff'; |
| import { resolveUserRegion, resolvePreciseUserCoordinates, type PreciseCoordinates } from '@/utils/user-location'; |
| import { showProBanner } from '@/components/ProBanner'; |
| import { getAuthState, initAuthState, subscribeAuthState } from '@/services/auth-state'; |
| import { |
| CLOUD_PREFS_APPLIED_EVENT, |
| install as installCloudPrefsSync, |
| onSignIn as cloudPrefsSignIn, |
| onSignOut as cloudPrefsSignOut, |
| type CloudPrefsAppliedDetail, |
| } from '@/utils/cloud-prefs-sync'; |
| import { |
| getConvexClient, |
| getConvexApi, |
| invalidateConvexAuthForSignOut, |
| rebindConvexAuthForWatchHandoff, |
| waitForConvexAuthForUser, |
| } from '@/services/convex-client'; |
| import { |
| assertAccountStillCurrent, |
| isAccountStillCurrent, |
| settleAccountOperation, |
| } from '@/services/account-operation'; |
| import type { Id } from '../convex/_generated/dataModel'; |
| import { initEntitlementSubscription, destroyEntitlementSubscription, resetEntitlementState, onEntitlementChange } from '@/services/entitlements'; |
| import { initSubscriptionWatch, destroySubscriptionWatch } from '@/services/billing'; |
| import { |
| FREE_TIER_FOLLOW_LIMIT, |
| WM_FOLLOWED_COUNTRIES_CAP_DROP, |
| installFollowedCountriesAuthListener, |
| } from '@/services/followed-countries'; |
| import { |
| capturePendingCheckoutIntentFromUrl, |
| initCheckoutWatchers, |
| resumePendingCheckout, |
| } from '@/services/checkout'; |
| import { |
| clearStoredAnonIdentity, |
| getFreshStoredAnonClaimToken, |
| getStoredAnonId, |
| } from '@/services/anonymous-identity-storage'; |
| import { captureReferralFromUrl } from '@/services/referral-capture'; |
| |
| |
| |
| import type { CorrelationPanel } from '@/components/CorrelationPanel'; |
|
|
| const CYBER_LAYER_ENABLED = import.meta.env.VITE_ENABLE_CYBER_LAYER === 'true'; |
| const FREE_MAP_PANEL_ACCESS_KEY = 'worldmonitor-free-map-panel-access-v1'; |
| type SignalModalInstance = import('@/components/SignalModal').SignalModal; |
|
|
| export type { CountryBriefSignals } from '@/app/app-context'; |
|
|
| export class App { |
| private state: AppContext; |
| private pendingDeepLinkCountry: string | null = null; |
| private pendingDeepLinkExpanded = false; |
| private pendingDeepLinkStoryCode: string | null = null; |
| private pendingDeepLinkChokepoint: string | null = null; |
| private chokepointDeepLinkTimer: number | null = null; |
|
|
| private panelLayout: PanelLayoutManager; |
| private dataLoader: DataLoaderManager; |
| private eventHandlers: EventHandlerManager; |
| private searchManager: SearchManager | null = null; |
| private searchManagerLoad: Promise<SearchManager> | null = null; |
| private signalModalLoad: Promise<SignalModalInstance> | null = null; |
| |
| |
| |
| private openSearchEpoch = 0; |
| private searchToggleDesiredOpen = false; |
| private latestSearchAdsb: Parameters<SearchManager['updateFlightSource']>[0] = []; |
| private latestSearchMilitary: Parameters<SearchManager['updateFlightSource']>[1] = []; |
| private countryIntel: CountryIntelManager; |
| private refreshScheduler: RefreshScheduler; |
| private desktopUpdater: DesktopUpdater; |
|
|
| private modules: { destroy(): void }[] = []; |
| private unsubAiFlow: (() => void) | null = null; |
| private unsubFreeTier: (() => void) | null = null; |
| private unsubEntitlementPremiumLoaders: (() => void) | null = null; |
| |
| |
| |
| |
| |
| private uiReady!: Promise<void>; |
| private resolveUiReady!: () => void; |
| |
| |
| |
| |
| private webMcpController: AbortController | null = null; |
| private visiblePanelPrimed = new Set<string>(); |
| private visiblePanelPrimeRaf: number | null = null; |
| private followedCountriesCapDropToastTimer: number | null = null; |
| private bootstrapHydrationState: BootstrapHydrationState = getBootstrapHydrationState(); |
| private cachedModeBannerEl: HTMLElement | null = null; |
| private readonly handleWmSessionDegraded = (): void => { |
| if (!this.state.isDestroyed) { |
| showToast('Anonymous data is temporarily unavailable. Check your cookie settings, then reload.'); |
| } |
| }; |
| private readonly handleViewportPrime = (): void => { |
| if (this.visiblePanelPrimeRaf !== null) return; |
| this.visiblePanelPrimeRaf = window.requestAnimationFrame(() => { |
| this.visiblePanelPrimeRaf = null; |
| void this.primeVisiblePanelData(); |
| |
| |
| |
| |
| |
| |
| void this.dataLoader.loadAllData(); |
| }); |
| }; |
| private readonly handleConnectivityChange = (): void => { |
| this.updateConnectivityUi(); |
| }; |
| private readonly handleI18nResourcesLoaded = (ev: Event): void => { |
| const language = (ev as CustomEvent<I18nResourcesLoadedDetail>).detail?.language; |
| if (language !== 'en') return; |
| |
| |
| replaceRawI18nKeyPlaceholders(this.state.container, t); |
| }; |
| private readonly handleFollowedCountriesCapDrop = (ev: Event): void => { |
| const detail = (ev as CustomEvent<{ kept?: unknown; dropped?: unknown }>).detail; |
| const dropped = typeof detail?.dropped === 'number' ? detail.dropped : 0; |
| const kept = typeof detail?.kept === 'number' ? detail.kept : FREE_TIER_FOLLOW_LIMIT; |
| if (dropped <= 0) return; |
| this.showFollowedCountriesCapDropToast(kept, dropped); |
| }; |
| private readonly handleCloudPrefsApplied = (ev: Event): void => { |
| const keys = (ev as CustomEvent<CloudPrefsAppliedDetail>).detail?.keys ?? []; |
| this.applyCloudSyncedPrefsToRuntime(keys); |
| }; |
|
|
| private applyCloudSyncedPrefsToRuntime(keys: readonly string[]): void { |
| if (keys.length === 0) return; |
|
|
| const keySet = new Set(keys); |
| invalidatePanelStorageCacheForKeys(keys); |
|
|
| if (keySet.has(STORAGE_KEYS.panels)) { |
| this.state.panelSettings = loadFromStorage<Record<string, PanelConfig>>( |
| STORAGE_KEYS.panels, |
| this.state.panelSettings, |
| ); |
| this.panelLayout.applyPanelSettings(); |
| this.state.unifiedSettings?.refreshPanelToggles(); |
| } |
|
|
| const panelOrderKey = this.state.PANEL_ORDER_KEY; |
| if (keySet.has(panelOrderKey) || keySet.has(`${panelOrderKey}-bottom-set`)) { |
| this.panelLayout.applySavedPanelOrder(); |
| } |
|
|
| if (keySet.has(STORAGE_KEYS.mapLayers) && !this.state.initialUrlState?.layers) { |
| const nextLayers = normalizeExclusiveChoropleths( |
| sanitizeLayersForVariant( |
| loadFromStorage<MapLayers>(STORAGE_KEYS.mapLayers, this.state.mapLayers), |
| SITE_VARIANT as MapVariant, |
| ), |
| this.state.mapLayers, |
| ); |
| if (!CYBER_LAYER_ENABLED) nextLayers.cyberThreats = false; |
| this.state.mapLayers = nextLayers; |
| this.state.map?.setLayers(nextLayers); |
| this.dataLoader.syncDataFreshnessWithLayers(); |
| } |
|
|
| if (keySet.has(STORAGE_KEYS.mapMode)) { |
| const mode = getStoredMapModePreference(); |
| if (mode === 'globe') this.state.map?.switchToGlobe(); |
| else this.state.map?.switchToFlat(); |
| } |
|
|
| if (keySet.has(STORAGE_KEYS.disabledFeeds)) { |
| this.state.disabledSources = new Set(loadFromStorage<string[]>(STORAGE_KEYS.disabledFeeds, [])); |
| } |
|
|
| if (keySet.has(STORAGE_KEYS.monitors)) { |
| this.state.monitors = loadFromStorage<Monitor[]>(STORAGE_KEYS.monitors, []); |
| const monitorPanel = this.state.panels['monitors'] as MonitorPanel | undefined; |
| monitorPanel?.setMonitors(this.state.monitors); |
| this.dataLoader.updateMonitorResults(); |
| } |
| } |
|
|
| private isPanelNearViewport(panelId: string, marginPx = 400): boolean { |
| const panel = this.state.panels[panelId] as { isNearViewport?: (marginPx?: number) => boolean } | undefined; |
| return panel?.isNearViewport?.(marginPx) ?? false; |
| } |
|
|
| private isAnyPanelNearViewport(panelIds: string[], marginPx = 400): boolean { |
| return panelIds.some((panelId) => this.isPanelNearViewport(panelId, marginPx)); |
| } |
|
|
| private shouldRefreshIntelligence(): boolean { |
| return this.isAnyPanelNearViewport(['cii', 'strategic-risk', 'strategic-posture']) |
| || !!this.state.countryBriefPage?.isVisible(); |
| } |
|
|
| private shouldRefreshFirms(): boolean { |
| return this.isPanelNearViewport('satellite-fires'); |
| } |
|
|
| private shouldRefreshCorrelation(): boolean { |
| return this.isAnyPanelNearViewport(['military-correlation', 'escalation-correlation', 'economic-correlation', 'disaster-correlation']); |
| } |
|
|
| private getCachedBootstrapUpdatedAt(): number | null { |
| const cachedTierTimestamps = Object.values(this.bootstrapHydrationState.tiers) |
| .filter((tier) => tier.source === 'cached') |
| .map((tier) => tier.updatedAt) |
| .filter((value): value is number => typeof value === 'number' && Number.isFinite(value)); |
|
|
| if (cachedTierTimestamps.length === 0) return null; |
| return Math.min(...cachedTierTimestamps); |
| } |
|
|
| private updateConnectivityUi(): void { |
| const statusIndicator = this.state.container.querySelector('.status-indicator'); |
| const statusLabel = statusIndicator?.querySelector('span:last-child'); |
| const online = typeof navigator === 'undefined' ? true : navigator.onLine !== false; |
| |
| |
| const usingCachedBootstrap = this.bootstrapHydrationState.source === 'cached'; |
| const cachedUpdatedAt = this.getCachedBootstrapUpdatedAt(); |
|
|
| let statusMode: 'live' | 'cached' | 'unavailable' = 'live'; |
| let bannerMessage: string | null = null; |
|
|
| if (!online) { |
| |
| const hasAnyCached = this.bootstrapHydrationState.source === 'cached' || this.bootstrapHydrationState.source === 'mixed'; |
| if (hasAnyCached) { |
| statusMode = 'cached'; |
| const offlineCachedAt = this.bootstrapHydrationState.tiers |
| ? Math.min(...Object.values(this.bootstrapHydrationState.tiers) |
| .filter((tier) => tier.source === 'cached' || tier.source === 'mixed') |
| .map((tier) => tier.updatedAt) |
| .filter((v): v is number => typeof v === 'number' && Number.isFinite(v))) |
| : NaN; |
| const freshness = Number.isFinite(offlineCachedAt) ? describeFreshness(offlineCachedAt) : t('common.cached').toLowerCase(); |
| bannerMessage = t('connectivity.offlineCached', { freshness }); |
| } else { |
| statusMode = 'unavailable'; |
| bannerMessage = t('connectivity.offlineUnavailable'); |
| } |
| } else if (usingCachedBootstrap) { |
| statusMode = 'cached'; |
| const freshness = cachedUpdatedAt ? describeFreshness(cachedUpdatedAt) : t('common.cached').toLowerCase(); |
| bannerMessage = t('connectivity.cachedFallback', { freshness }); |
| } |
|
|
| if (statusIndicator && statusLabel) { |
| statusIndicator.classList.toggle('status-indicator--cached', statusMode === 'cached'); |
| statusIndicator.classList.toggle('status-indicator--unavailable', statusMode === 'unavailable'); |
| statusLabel.textContent = statusMode === 'live' |
| ? t('header.live') |
| : statusMode === 'cached' |
| ? t('header.cached') |
| : t('header.unavailable'); |
| } |
|
|
| if (bannerMessage) { |
| if (!this.cachedModeBannerEl) { |
| this.cachedModeBannerEl = document.createElement('div'); |
| |
| |
| this.cachedModeBannerEl.className = 'cached-mode-banner'; |
| this.cachedModeBannerEl.setAttribute('role', 'status'); |
| this.cachedModeBannerEl.setAttribute('aria-live', 'polite'); |
|
|
| const badge = document.createElement('span'); |
| badge.className = 'cached-mode-banner__badge'; |
| const text = document.createElement('span'); |
| text.className = 'cached-mode-banner__text'; |
| this.cachedModeBannerEl.append(badge, text); |
|
|
| const header = this.state.container.querySelector('.header'); |
| if (header?.parentElement) { |
| header.insertAdjacentElement('afterend', this.cachedModeBannerEl); |
| } else { |
| this.state.container.prepend(this.cachedModeBannerEl); |
| } |
| } |
|
|
| this.cachedModeBannerEl.classList.toggle('cached-mode-banner--unavailable', statusMode === 'unavailable'); |
| const badge = this.cachedModeBannerEl.querySelector('.cached-mode-banner__badge')!; |
| const text = this.cachedModeBannerEl.querySelector('.cached-mode-banner__text')!; |
| badge.textContent = statusMode === 'cached' ? t('header.cached') : t('header.unavailable'); |
| text.textContent = bannerMessage; |
| return; |
| } |
|
|
| this.cachedModeBannerEl?.remove(); |
| this.cachedModeBannerEl = null; |
| } |
|
|
| private async primeVisiblePanelData(forceAll = false): Promise<void> { |
| const tasks: Promise<unknown>[] = []; |
| const primeTask = (key: string, task: () => Promise<unknown>): void => { |
| if (this.visiblePanelPrimed.has(key) || this.state.inFlight.has(key)) return; |
| const wrapped = (async () => { |
| this.state.inFlight.add(key); |
| try { |
| await task(); |
| this.visiblePanelPrimed.add(key); |
| } finally { |
| this.state.inFlight.delete(key); |
| } |
| })(); |
| tasks.push(wrapped); |
| }; |
|
|
| const shouldPrime = (id: string): boolean => forceAll || this.isPanelNearViewport(id); |
| const shouldPrimeAny = (ids: string[]): boolean => forceAll || this.isAnyPanelNearViewport(ids); |
|
|
| if (shouldPrime('service-status')) { |
| const panel = this.state.panels['service-status'] as ServiceStatusPanel | undefined; |
| if (panel) primeTask('service-status', () => panel.fetchStatus()); |
| } |
| if (shouldPrime('macro-signals')) { |
| const panel = this.state.panels['macro-signals'] as MacroSignalsPanel | undefined; |
| if (panel) primeTask('macro-signals', () => panel.fetchData()); |
| } |
| if (shouldPrime('fear-greed')) { |
| const panel = this.state.panels['fear-greed'] as FearGreedPanel | undefined; |
| if (panel) primeTask('fear-greed', () => panel.fetchData()); |
| } |
| if (shouldPrime('hormuz-tracker')) { |
| const panel = this.state.panels['hormuz-tracker'] as HormuzPanel | undefined; |
| if (panel) primeTask('hormuz-tracker', () => panel.fetchData()); |
| } |
| if (shouldPrime('etf-flows')) { |
| const panel = this.state.panels['etf-flows'] as ETFFlowsPanel | undefined; |
| if (panel) primeTask('etf-flows', () => panel.fetchData()); |
| } |
| if (shouldPrime('stablecoins')) { |
| const panel = this.state.panels.stablecoins as StablecoinPanel | undefined; |
| if (panel) primeTask('stablecoins', () => panel.fetchData()); |
| } |
| if (shouldPrime('energy-crisis')) { |
| const panel = this.state.panels['energy-crisis'] as EnergyCrisisPanel | undefined; |
| if (panel) primeTask('energy-crisis', () => panel.fetchData()); |
| } |
| if (shouldPrime('telegram-intel')) { |
| primeTask('telegram-intel', () => this.dataLoader.loadTelegramIntel()); |
| } |
| if (shouldPrime('gulf-economies')) { |
| const panel = this.state.panels['gulf-economies'] as GulfEconomiesPanel | undefined; |
| if (panel) primeTask('gulf-economies', () => panel.fetchData()); |
| } |
| if (shouldPrime('grocery-basket')) { |
| const panel = this.state.panels['grocery-basket'] as GroceryBasketPanel | undefined; |
| if (panel) primeTask('grocery-basket', () => panel.fetchData()); |
| } |
| if (shouldPrime('bigmac')) { |
| const panel = this.state.panels['bigmac'] as BigMacPanel | undefined; |
| if (panel) primeTask('bigmac', () => panel.fetchData()); |
| } |
| if (shouldPrime('fuel-prices')) { |
| const panel = this.state.panels['fuel-prices'] as FuelPricesPanel | undefined; |
| if (panel) primeTask('fuel-prices', () => panel.fetchData()); |
| } |
| if (shouldPrime('fao-food-price-index')) { |
| const panel = this.state.panels['fao-food-price-index'] as FaoFoodPriceIndexPanel | undefined; |
| if (panel) primeTask('fao-food-price-index', () => panel.fetchData()); |
| } |
| if (shouldPrime('oil-inventories')) { |
| const panel = this.state.panels['oil-inventories'] as OilInventoriesPanel | undefined; |
| if (panel) primeTask('oil-inventories', () => panel.fetchData()); |
| } |
| |
| |
| |
| |
| |
| |
| |
| if (shouldPrime('pipeline-status')) { |
| const panel = this.state.panels['pipeline-status'] as PipelineStatusPanel | undefined; |
| if (panel) primeTask('pipeline-status', () => panel.fetchData()); |
| } |
| if (shouldPrime('storage-facility-map')) { |
| const panel = this.state.panels['storage-facility-map'] as StorageFacilityMapPanel | undefined; |
| if (panel) primeTask('storage-facility-map', () => panel.fetchData()); |
| } |
| if (shouldPrime('fuel-shortages')) { |
| const panel = this.state.panels['fuel-shortages'] as FuelShortagePanel | undefined; |
| if (panel) primeTask('fuel-shortages', () => panel.fetchData()); |
| } |
| if (shouldPrime('energy-disruptions')) { |
| const panel = this.state.panels['energy-disruptions'] as EnergyDisruptionsPanel | undefined; |
| if (panel) primeTask('energy-disruptions', () => panel.fetchData()); |
| } |
| if (shouldPrime('energy-risk-overview')) { |
| const panel = this.state.panels['energy-risk-overview'] as EnergyRiskOverviewPanel | undefined; |
| if (panel) primeTask('energy-risk-overview', () => panel.fetchData()); |
| } |
| if (shouldPrime('chokepoint-strip')) { |
| |
| |
| |
| |
| const panel = this.state.panels['chokepoint-strip'] as ChokepointStripPanel | undefined; |
| if (panel) primeTask('chokepoint-strip', () => panel.fetchData()); |
| } |
| if (shouldPrime('climate-news')) { |
| const panel = this.state.panels['climate-news'] as ClimateNewsPanel | undefined; |
| if (panel) primeTask('climate-news', () => panel.fetchData()); |
| } |
| if (shouldPrime('consumer-prices')) { |
| const panel = this.state.panels['consumer-prices'] as ConsumerPricesPanel | undefined; |
| if (panel) primeTask('consumer-prices', () => panel.fetchData()); |
| } |
| if (shouldPrime('defense-patents')) { |
| const panel = this.state.panels['defense-patents'] as DefensePatentsPanel | undefined; |
| if (panel) primeTask('defense-patents', () => { panel.refresh(); return Promise.resolve(); }); |
| } |
| if (shouldPrime('macro-tiles')) { |
| const panel = this.state.panels['macro-tiles'] as MacroTilesPanel | undefined; |
| if (panel) primeTask('macro-tiles', () => panel.fetchData()); |
| } |
| if (shouldPrime('fsi')) { |
| const panel = this.state.panels['fsi'] as FSIPanel | undefined; |
| if (panel) primeTask('fsi', () => panel.fetchData()); |
| } |
| if (shouldPrime('yield-curve')) { |
| const panel = this.state.panels['yield-curve'] as YieldCurvePanel | undefined; |
| if (panel) primeTask('yield-curve', () => panel.fetchData()); |
| } |
| if (shouldPrime('earnings-calendar')) { |
| const panel = this.state.panels['earnings-calendar'] as EarningsCalendarPanel | undefined; |
| if (panel) primeTask('earnings-calendar', () => panel.fetchData()); |
| } |
| if (shouldPrime('economic-calendar')) { |
| const panel = this.state.panels['economic-calendar'] as EconomicCalendarPanel | undefined; |
| if (panel) primeTask('economic-calendar', () => panel.fetchData()); |
| } |
| if (shouldPrime('cot-positioning')) { |
| const panel = this.state.panels['cot-positioning'] as CotPositioningPanel | undefined; |
| if (panel) primeTask('cot-positioning', () => panel.fetchData()); |
| } |
| if (shouldPrime('liquidity-shifts')) { |
| const panel = this.state.panels['liquidity-shifts'] as LiquidityShiftsPanel | undefined; |
| if (panel) primeTask('liquidity-shifts', () => panel.fetchData()); |
| } |
| if (shouldPrime('positioning-247')) { |
| const panel = this.state.panels['positioning-247'] as PositioningPanel | undefined; |
| if (panel) primeTask('positioning-247', () => panel.fetchData()); |
| } |
| if (shouldPrime('gold-intelligence')) { |
| const panel = this.state.panels['gold-intelligence'] as GoldIntelligencePanel | undefined; |
| if (panel) primeTask('gold-intelligence', () => panel.fetchData()); |
| } |
| if (shouldPrime('aaii-sentiment')) { |
| primeTask('aaiiSentiment', () => this.dataLoader.loadAaiiSentiment()); |
| } |
| if (shouldPrime('market-breadth')) { |
| primeTask('marketBreadth', () => this.dataLoader.loadMarketBreadth()); |
| } |
| if (shouldPrimeAny(['markets', 'heatmap', 'commodities', 'crypto', 'energy-complex'])) { |
| primeTask('markets', () => this.dataLoader.loadMarkets()); |
| } |
| if (shouldPrime('polymarket')) { |
| primeTask('predictions', () => this.dataLoader.loadPredictions()); |
| } |
| if (shouldPrime('economic')) { |
| primeTask('fred', () => this.dataLoader.loadFredData()); |
| primeTask('spending', () => this.dataLoader.loadGovernmentSpending()); |
| primeTask('bis', () => this.dataLoader.loadBisData()); |
| } |
| if (shouldPrime('global-procurement') && hasPremiumAccess()) { |
| primeTask('global-tenders', () => this.dataLoader.loadGlobalTenders()); |
| } |
| if (shouldPrime('energy-complex')) { |
| primeTask('oil', () => this.dataLoader.loadOilAnalytics()); |
| } |
| |
| |
| |
| if (shouldPrime('supply-chain')) { |
| primeTask('supplyChain', () => this.dataLoader.loadSupplyChain()); |
| } |
| if (shouldPrime('china-corridors')) { |
| primeTask('chinaCorridors', () => this.dataLoader.loadChinaCorridors()); |
| } |
| if (shouldPrime('china-activity-nowcast')) { |
| primeTask('chinaActivityNowcast', () => this.dataLoader.loadChinaActivityNowcast()); |
| } |
| if (shouldPrime('cross-source-signals')) { |
| primeTask('crossSourceSignals', () => this.dataLoader.loadCrossSourceSignals()); |
| } |
|
|
| const _wmAccess = hasPremiumAccess(); |
| if (_wmAccess) { |
| if (shouldPrime('trade-policy')) { |
| primeTask('tradePolicy', () => this.dataLoader.loadTradePolicy()); |
| } |
| if (shouldPrime('stock-analysis')) { |
| primeTask('stockAnalysis', () => this.dataLoader.loadStockAnalysis()); |
| } |
| if (shouldPrime('stock-backtest')) { |
| primeTask('stockBacktest', () => this.dataLoader.loadStockBacktest()); |
| } |
| if (shouldPrime('daily-market-brief')) { |
| primeTask('dailyMarketBrief', () => this.dataLoader.loadDailyMarketBrief()); |
| } |
| if (shouldPrime('market-implications')) { |
| primeTask('marketImplications', () => this.dataLoader.loadMarketImplications()); |
| } |
| } |
|
|
| if (tasks.length > 0) { |
| await Promise.allSettled(tasks); |
| } |
| } |
|
|
| constructor(containerId: string) { |
| const el = document.getElementById(containerId); |
| if (!el) throw new Error(`Container ${containerId} not found`); |
|
|
| this.uiReady = new Promise<void>((resolve) => { |
| this.resolveUiReady = resolve; |
| }); |
|
|
| const PANEL_ORDER_KEY = 'panel-order'; |
| const PANEL_SPANS_KEY = 'worldmonitor-panel-spans'; |
|
|
| const isMobile = isMobileDevice(); |
| const isDesktopApp = isDesktopRuntime(); |
| const monitors = loadFromStorage<Monitor[]>(STORAGE_KEYS.monitors, []); |
|
|
| |
| const defaultLayers = isMobile ? MOBILE_DEFAULT_MAP_LAYERS : DEFAULT_MAP_LAYERS; |
|
|
| let mapLayers: MapLayers; |
| let panelSettings: Record<string, PanelConfig>; |
|
|
| |
| const isDynamicPanel = (k: string) => !ALL_PANELS[k] && (k === 'runtime-config' || k.startsWith('cw-') || k.startsWith('mcp-')); |
|
|
| const currentVariant = SITE_VARIANT; |
| let storedVariant: string | null = null; |
| let storageAvailable = true; |
| try { |
| storedVariant = localStorage.getItem('worldmonitor-variant'); |
| const probeKey = 'wm-storage-capability-probe'; |
| localStorage.setItem(probeKey, '1'); |
| localStorage.removeItem(probeKey); |
| } catch { |
| storageAvailable = false; |
| } |
|
|
| |
| |
| if (!storageAvailable) { |
| mapLayers = normalizeExclusiveChoropleths( |
| sanitizeLayersForVariant({ ...defaultLayers }, currentVariant as MapVariant), null, |
| ); |
| panelSettings = { ...DEFAULT_PANELS }; |
| } else if (storedVariant !== currentVariant) { |
| |
| console.log(`[App] Variant check: stored="${storedVariant}", current="${currentVariant}"`); |
| |
| console.log('[App] Variant changed - seeding new defaults, disabling cross-variant panels'); |
| localStorage.setItem('worldmonitor-variant', currentVariant); |
| |
| localStorage.removeItem(STORAGE_KEYS.mapLayers); |
| mapLayers = normalizeExclusiveChoropleths( |
| sanitizeLayersForVariant({ ...defaultLayers }, currentVariant as MapVariant), null, |
| ); |
| |
| panelSettings = loadFromStorage<Record<string, PanelConfig>>(STORAGE_KEYS.panels, {}); |
| const newVariantKeys = new Set(VARIANT_DEFAULTS[currentVariant] ?? []); |
| for (const key of Object.keys(panelSettings)) { |
| if (!newVariantKeys.has(key) && !isDynamicPanel(key) && panelSettings[key]) { |
| panelSettings[key] = { ...panelSettings[key]!, enabled: false }; |
| } |
| } |
| for (const key of newVariantKeys) { |
| if (!(key in panelSettings)) { |
| panelSettings[key] = { ...getEffectivePanelConfig(key, currentVariant) }; |
| } |
| } |
| } else { |
| mapLayers = normalizeExclusiveChoropleths( |
| sanitizeLayersForVariant( |
| loadFromStorage<MapLayers>(STORAGE_KEYS.mapLayers, defaultLayers), |
| currentVariant as MapVariant, |
| ), null, |
| ); |
| panelSettings = loadFromStorage<Record<string, PanelConfig>>( |
| STORAGE_KEYS.panels, |
| DEFAULT_PANELS |
| ); |
|
|
| |
| const PANEL_KEY_RENAMES_MIGRATION_KEY = 'worldmonitor-panel-key-renames-v2.6.8'; |
| if (!localStorage.getItem(PANEL_KEY_RENAMES_MIGRATION_KEY)) { |
| let migrated = false; |
| const keyRenames: Array<[string, string]> = [ |
| ['live-youtube', 'live-webcams'], |
| ['pinned-webcams', 'windy-webcams'], |
| ...(SITE_VARIANT === 'finance' ? [['regulation', 'fin-regulation'] as [string, string]] : []), |
| ]; |
| |
| if (SITE_VARIANT !== 'finance' && panelSettings['regulation']) { |
| delete panelSettings['regulation']; |
| migrated = true; |
| } |
| for (const [legacyKey, nextKey] of keyRenames) { |
| if (!panelSettings[legacyKey] || panelSettings[nextKey]) continue; |
| panelSettings[nextKey] = { |
| ...DEFAULT_PANELS[nextKey], |
| ...panelSettings[legacyKey], |
| name: DEFAULT_PANELS[nextKey]?.name ?? panelSettings[legacyKey].name, |
| }; |
| delete panelSettings[legacyKey]; |
| migrated = true; |
| } |
| |
| for (const [legacyKey, nextKey] of keyRenames) { |
| for (const orderKey of [PANEL_ORDER_KEY, PANEL_ORDER_KEY + '-bottom-set', PANEL_ORDER_KEY + '-bottom']) { |
| try { |
| const raw = localStorage.getItem(orderKey); |
| if (!raw) continue; |
| const arr = JSON.parse(raw); |
| if (!Array.isArray(arr)) continue; |
| const idx = arr.indexOf(legacyKey); |
| if (idx !== -1) { arr[idx] = nextKey; localStorage.setItem(orderKey, JSON.stringify(arr)); migrated = true; } |
| } catch { } |
| } |
| } |
| if (migrated) saveToStorage(STORAGE_KEYS.panels, panelSettings); |
| localStorage.setItem(PANEL_KEY_RENAMES_MIGRATION_KEY, 'done'); |
| } |
|
|
| |
| for (const key of Object.keys(ALL_PANELS)) { |
| if (!(key in panelSettings)) { |
| const config = getEffectivePanelConfig(key, SITE_VARIANT); |
| const isInVariant = (VARIANT_DEFAULTS[SITE_VARIANT] ?? []).includes(key); |
| panelSettings[key] = { ...config, enabled: isInVariant && config.enabled }; |
| } |
| } |
|
|
| |
| const UNIFIED_MIGRATION_KEY = 'worldmonitor-unified-panels-v1'; |
| if (!localStorage.getItem(UNIFIED_MIGRATION_KEY)) { |
| const variantDefaults = new Set(VARIANT_DEFAULTS[SITE_VARIANT] ?? []); |
| for (const key of Object.keys(ALL_PANELS)) { |
| if (!(key in panelSettings)) { |
| const config = getEffectivePanelConfig(key, SITE_VARIANT); |
| panelSettings[key] = { ...config, enabled: variantDefaults.has(key) && config.enabled }; |
| } |
| } |
| saveToStorage(STORAGE_KEYS.panels, panelSettings); |
| localStorage.setItem(UNIFIED_MIGRATION_KEY, 'done'); |
| } |
|
|
| |
| |
| const HAPPY_PANEL_FIX_KEY = 'worldmonitor-happy-panel-fix-v1'; |
| if (SITE_VARIANT === 'happy' && !localStorage.getItem(HAPPY_PANEL_FIX_KEY)) { |
| const happyKeys = new Set(VARIANT_DEFAULTS['happy'] ?? []); |
| let fixed = false; |
| for (const key of Object.keys(panelSettings)) { |
| if (!happyKeys.has(key) && !isDynamicPanel(key) && panelSettings[key]?.enabled) { |
| panelSettings[key] = { ...panelSettings[key]!, enabled: false }; |
| fixed = true; |
| } |
| } |
| if (fixed) saveToStorage(STORAGE_KEYS.panels, panelSettings); |
| localStorage.setItem(HAPPY_PANEL_FIX_KEY, 'done'); |
| } |
|
|
| console.log('[App] Loaded panel settings from storage:', Object.entries(panelSettings).filter(([_, v]) => !v.enabled).map(([k]) => k)); |
|
|
| |
| const PANEL_ORDER_MIGRATION_KEY = 'worldmonitor-panel-order-v1.9'; |
| if (!localStorage.getItem(PANEL_ORDER_MIGRATION_KEY)) { |
| const savedOrder = localStorage.getItem(PANEL_ORDER_KEY); |
| if (savedOrder) { |
| try { |
| const order: string[] = JSON.parse(savedOrder); |
| const priorityPanels = ['insights', 'strategic-posture', 'cii', 'strategic-risk']; |
| const filtered = order.filter(k => !priorityPanels.includes(k) && k !== 'live-news'); |
| const liveNewsIdx = order.indexOf('live-news'); |
| const newOrder = liveNewsIdx !== -1 ? ['live-news'] : []; |
| newOrder.push(...priorityPanels.filter(p => order.includes(p))); |
| newOrder.push(...filtered); |
| localStorage.setItem(PANEL_ORDER_KEY, JSON.stringify(newOrder)); |
| console.log('[App] Migrated panel order to v1.9 layout'); |
| } catch { |
| |
| } |
| } |
| localStorage.setItem(PANEL_ORDER_MIGRATION_KEY, 'done'); |
| } |
|
|
| |
| if (currentVariant === 'tech') { |
| const TECH_INSIGHTS_MIGRATION_KEY = 'worldmonitor-tech-insights-top-v1'; |
| if (!localStorage.getItem(TECH_INSIGHTS_MIGRATION_KEY)) { |
| const savedOrder = localStorage.getItem(PANEL_ORDER_KEY); |
| if (savedOrder) { |
| try { |
| const order: string[] = JSON.parse(savedOrder); |
| const filtered = order.filter(k => k !== 'insights' && k !== 'live-news'); |
| const newOrder: string[] = []; |
| if (order.includes('live-news')) newOrder.push('live-news'); |
| if (order.includes('insights')) newOrder.push('insights'); |
| newOrder.push(...filtered); |
| localStorage.setItem(PANEL_ORDER_KEY, JSON.stringify(newOrder)); |
| console.log('[App] Tech variant: Migrated insights panel to top'); |
| } catch { |
| |
| } |
| } |
| localStorage.setItem(TECH_INSIGHTS_MIGRATION_KEY, 'done'); |
| } |
| } |
| } |
|
|
| if (storageAvailable) { |
| |
| const PANEL_PRUNE_KEY = 'worldmonitor-panel-prune-v1'; |
| if (!localStorage.getItem(PANEL_PRUNE_KEY)) { |
| const validKeys = new Set(Object.keys(ALL_PANELS)); |
| let pruned = false; |
| for (const key of Object.keys(panelSettings)) { |
| if (!validKeys.has(key) && key !== 'runtime-config') { |
| delete panelSettings[key]; |
| pruned = true; |
| } |
| } |
| if (pruned) saveToStorage(STORAGE_KEYS.panels, panelSettings); |
| for (const orderKey of [PANEL_ORDER_KEY, PANEL_ORDER_KEY + '-bottom-set', PANEL_ORDER_KEY + '-bottom']) { |
| try { |
| const raw = localStorage.getItem(orderKey); |
| if (!raw) continue; |
| const arr = JSON.parse(raw); |
| if (!Array.isArray(arr)) continue; |
| const filtered = arr.filter((k: string) => validKeys.has(k)); |
| if (filtered.length !== arr.length) localStorage.setItem(orderKey, JSON.stringify(filtered)); |
| } catch { localStorage.removeItem(orderKey); } |
| } |
| localStorage.setItem(PANEL_PRUNE_KEY, 'done'); |
| } |
|
|
| |
| const LAYOUT_RESET_MIGRATION_KEY = 'worldmonitor-layout-reset-v2.5'; |
| if (!localStorage.getItem(LAYOUT_RESET_MIGRATION_KEY)) { |
| const hadSavedOrder = !!localStorage.getItem(PANEL_ORDER_KEY); |
| const hadSavedSpans = !!localStorage.getItem(PANEL_SPANS_KEY); |
| if (hadSavedOrder || hadSavedSpans) { |
| localStorage.removeItem(PANEL_ORDER_KEY); |
| localStorage.removeItem(PANEL_ORDER_KEY + '-bottom'); |
| localStorage.removeItem(PANEL_ORDER_KEY + '-bottom-set'); |
| clearPanelSpans(); |
| console.log('[App] Applied layout reset migration (v2.5): cleared panel order/spans'); |
| } |
| localStorage.setItem(LAYOUT_RESET_MIGRATION_KEY, 'done'); |
| } |
| } |
|
|
| |
| if (isDesktopApp) { |
| if (!panelSettings['runtime-config'] || !panelSettings['runtime-config'].enabled) { |
| panelSettings['runtime-config'] = { |
| ...panelSettings['runtime-config'], |
| name: panelSettings['runtime-config']?.name ?? 'Desktop Configuration', |
| enabled: true, |
| priority: panelSettings['runtime-config']?.priority ?? 2, |
| }; |
| saveToStorage(STORAGE_KEYS.panels, panelSettings); |
| } |
| } |
|
|
| const initialUrlState: ParsedMapUrlState | null = parseMapUrlState(window.location.search, mapLayers); |
| if (initialUrlState.layers) { |
| mapLayers = normalizeExclusiveChoropleths( |
| sanitizeLayersForVariant(initialUrlState.layers, currentVariant as MapVariant), null, |
| ); |
| initialUrlState.layers = mapLayers; |
| } |
| if (!CYBER_LAYER_ENABLED) { |
| mapLayers.cyberThreats = false; |
| } |
| |
| if (currentVariant === 'full' && storageAvailable) { |
| const baseKey = 'worldmonitor-sources-reduction-v3'; |
| if (!localStorage.getItem(baseKey)) { |
| const defaultDisabled = computeDefaultDisabledSources(); |
| saveToStorage(STORAGE_KEYS.disabledFeeds, defaultDisabled); |
| localStorage.setItem(baseKey, 'done'); |
| const total = getTotalFeedCount(); |
| console.log(`[App] Sources reduction: ${defaultDisabled.length} disabled, ${total - defaultDisabled.length} enabled`); |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| let explicitLocale = ''; |
| try { explicitLocale = localStorage.getItem('wm-locale-explicit') || ''; } catch { } |
| const userLang = ((explicitLocale || navigator.language || 'en').split('-')[0] ?? 'en').toLowerCase(); |
| const localeKey = `worldmonitor-locale-boost-${userLang}`; |
| if (userLang !== 'en' && !localStorage.getItem(localeKey)) { |
| const boosted = getLocaleBoostedSources(userLang); |
| if (boosted.size > 0) { |
| const current = loadFromStorage<string[]>(STORAGE_KEYS.disabledFeeds, []); |
| const updated = current.filter(name => !boosted.has(name)); |
| saveToStorage(STORAGE_KEYS.disabledFeeds, updated); |
| console.log(`[App] Locale boost (${userLang}): enabled ${current.length - updated.length} sources`); |
| } |
| localStorage.setItem(localeKey, 'done'); |
| } |
| } |
|
|
| const disabledSources = new Set(loadFromStorage<string[]>(STORAGE_KEYS.disabledFeeds, [])); |
|
|
| |
| this.state = { |
| map: null, |
| isMobile, |
| isDesktopApp, |
| container: el, |
| panels: {}, |
| newsPanels: {}, |
| newsCategoryPanelKeys: new Map(), |
| panelSettings, |
| mapLayers, |
| allNews: [], |
| newsByCategory: {}, |
| latestMarkets: [], |
| latestPredictions: [], |
| latestTechEvents: [], |
| latestClusters: [], |
| intelligenceCache: {}, |
| cyberThreatsCache: null, |
| disabledSources, |
| currentTimeRange: '7d', |
| inFlight: new Set(), |
| seenGeoAlerts: new Set(), |
| monitors, |
| signalModal: null, |
| ensureSignalModal: () => this.ensureSignalModal(), |
| statusPanel: null, |
| searchModal: null, |
| findingsBadge: null, |
| breakingBanner: null, |
| playbackControl: null, |
| exportPanel: null, |
| unifiedSettings: null, |
| pizzintIndicator: null, |
| correlationEngine: null, |
| llmStatusIndicator: null, |
| countryBriefPage: null, |
| countryTimeline: null, |
| positivePanel: null, |
| countersPanel: null, |
| progressPanel: null, |
| breakthroughsPanel: null, |
| heroPanel: null, |
| digestPanel: null, |
| speciesPanel: null, |
| renewablePanel: null, |
| authModal: null, |
| authHeaderWidget: null, |
| tvMode: null, |
| happyAllItems: [], |
| isDestroyed: false, |
| isPlaybackMode: false, |
| isIdle: false, |
| initialLoadComplete: false, |
| resolvedLocation: 'global', |
| activeChokepoint: initialUrlState.chokepoint ?? null, |
| initialUrlState, |
| PANEL_ORDER_KEY, |
| PANEL_SPANS_KEY, |
| }; |
|
|
| |
| this.refreshScheduler = new RefreshScheduler(this.state); |
| this.countryIntel = new CountryIntelManager(this.state); |
| this.desktopUpdater = new DesktopUpdater(this.state); |
|
|
| this.dataLoader = new DataLoaderManager(this.state, { |
| renderCriticalBanner: (postures) => this.panelLayout.renderCriticalBanner(postures), |
| refreshOpenCountryBrief: () => this.countryIntel.refreshOpenBrief(), |
| }); |
|
|
| this.panelLayout = new PanelLayoutManager(this.state, { |
| openCountryStory: (code, name) => { |
| void this.countryIntel.openCountryStory(code, name).catch((err) => { |
| console.error('[CountryStory] Failed to open story:', err); |
| showToast('Country story failed to open. Please try again.'); |
| }); |
| }, |
| openCountryBrief: (code) => { |
| const name = CountryIntelManager.resolveCountryName(code); |
| void this.countryIntel.openCountryBriefByCode(code, name).catch((err) => { |
| console.error('[CountryBrief] Failed to open country brief:', err); |
| this.state.map?.setRenderPaused(false); |
| showToast('Country brief failed to open. Please try again.'); |
| }); |
| }, |
| openSearch: () => { |
| track('search-open', { source: 'pro-onboarding' }); |
| void this.openSearch(); |
| }, |
| loadAllData: () => this.dataLoader.loadAllData(), |
| updateMonitorResults: () => this.dataLoader.updateMonitorResults(), |
| loadSecurityAdvisories: () => this.dataLoader.loadSecurityAdvisories(), |
| applyMapLayerChange: (layer, enabled, source) => this.eventHandlers.applyMapLayerChange(layer, enabled, source), |
| }); |
|
|
| this.eventHandlers = new EventHandlerManager(this.state, { |
| openSearch: (options) => { void this.openSearch(options); }, |
| updateSearchIndex: () => this.updateSearchIndexIfReady(), |
| loadAllData: () => this.dataLoader.loadAllData(), |
| invalidateNewsHydration: () => this.dataLoader.invalidateNewsHydration(), |
| flushStaleRefreshes: () => this.refreshScheduler.flushStaleRefreshes(), |
| setHiddenSince: (ts) => this.refreshScheduler.setHiddenSince(ts), |
| loadDataForLayer: (layer) => { void this.dataLoader.loadDataForLayer(layer as keyof MapLayers); }, |
| waitForAisData: () => this.dataLoader.waitForAisData(), |
| syncDataFreshnessWithLayers: () => this.dataLoader.syncDataFreshnessWithLayers(), |
| ensureCorrectZones: () => this.panelLayout.ensureCorrectZones(), |
| applySavedPanelOrder: (panelOrder?: string[]) => this.panelLayout.applySavedPanelOrder(panelOrder), |
| refreshCiiAfterFocalPointsReady: () => this.dataLoader.refreshCiiAfterFocalPointsReady(), |
| stopLayerActivity: (layer) => this.dataLoader.stopLayerActivity(layer), |
| mountLiveNewsIfReady: () => this.panelLayout.mountLiveNewsIfReady(), |
| updateFlightSource: (adsb, military) => this.updateFlightSourceIfReady(adsb, military), |
| }); |
|
|
| |
| this.dataLoader.updateSearchIndex = () => this.updateSearchIndexIfReady(); |
|
|
| |
| this.modules = [ |
| this.desktopUpdater, |
| this.panelLayout, |
| this.countryIntel, |
| this.dataLoader, |
| this.refreshScheduler, |
| this.eventHandlers, |
| ]; |
| } |
|
|
| private ensureSignalModal(): Promise<SignalModalInstance> { |
| if (this.state.signalModal) return Promise.resolve(this.state.signalModal); |
| if (this.signalModalLoad) return this.signalModalLoad; |
|
|
| this.signalModalLoad = import('@/components/SignalModal') |
| .then(({ SignalModal }) => { |
| if (this.state.isDestroyed) { |
| throw new Error('App destroyed before signal modal loaded'); |
| } |
| const signalModal = new SignalModal(); |
| signalModal.setLocationClickHandler((lat, lon) => { |
| this.state.map?.setCenter(lat, lon, 4); |
| }); |
| this.state.signalModal = signalModal; |
| return signalModal; |
| }) |
| .catch((err) => { |
| this.signalModalLoad = null; |
| throw err; |
| }); |
|
|
| return this.signalModalLoad; |
| } |
|
|
| private ensureSearchManager(): Promise<SearchManager> { |
| if (this.searchManager) return Promise.resolve(this.searchManager); |
| if (this.searchManagerLoad) return this.searchManagerLoad; |
|
|
| this.searchManagerLoad = import('@/app/search-manager') |
| .then(({ SearchManager }) => { |
| if (this.state.isDestroyed) { |
| throw new Error('App destroyed before search manager loaded'); |
| } |
|
|
| const manager = new SearchManager(this.state, { |
| openCountryBriefByCode: (code, country) => { |
| void this.countryIntel.openCountryBriefByCode(code, country).catch((err) => { |
| console.error('[CountryBrief] Failed to open country brief:', err); |
| this.state.map?.setRenderPaused(false); |
| showToast('Country brief failed to open. Please try again.'); |
| }); |
| }, |
| enablePanel: (panelId) => this.eventHandlers.enablePanelById(panelId), |
| }); |
| manager.init(); |
| manager.updateFlightSource(this.latestSearchAdsb, this.latestSearchMilitary); |
| this.searchManager = manager; |
| this.modules.push(manager); |
| return manager; |
| }) |
| .finally(() => { |
| this.searchManagerLoad = null; |
| }); |
|
|
| return this.searchManagerLoad; |
| } |
|
|
| private updateSearchIndexIfReady(): void { |
| this.searchManager?.updateSearchIndex(); |
| } |
|
|
| private updateFlightSourceIfReady( |
| adsb: Parameters<SearchManager['updateFlightSource']>[0], |
| military: Parameters<SearchManager['updateFlightSource']>[1], |
| ): void { |
| this.latestSearchAdsb = adsb; |
| this.latestSearchMilitary = military; |
| this.searchManager?.updateFlightSource(adsb, military); |
| } |
|
|
| private async openSearch(options: { toggle?: boolean; throwOnFailure?: boolean } = {}): Promise<void> { |
| |
| |
| |
| |
| |
| |
| |
| let epoch = this.openSearchEpoch; |
| try { |
| await this.waitForUiReady(); |
|
|
| const existingModal = this.state.searchModal; |
| if (options.toggle && existingModal?.isOpen()) { |
| existingModal.close(); |
| return; |
| } |
|
|
| const togglingBeforeLoad = Boolean(options.toggle) && !this.searchManager; |
| if (togglingBeforeLoad) { |
| this.searchToggleDesiredOpen = !this.searchToggleDesiredOpen; |
| } |
|
|
| epoch = ++this.openSearchEpoch; |
| const manager = await this.ensureSearchManager(); |
| if (this.openSearchEpoch !== epoch) return; |
|
|
| const wantOpen = togglingBeforeLoad ? this.searchToggleDesiredOpen : true; |
| if (!wantOpen) return; |
|
|
| manager.updateSearchIndex(); |
| const modal = this.state.searchModal; |
| if (!modal) throw new Error('Search modal is not initialised'); |
| modal.open(); |
| } catch (error) { |
| if (!this.state.isDestroyed) { |
| console.warn('[search] Failed to load search manager:', error); |
| if (!options.throwOnFailure) showToast('Search failed to load. Please try again.'); |
| } |
| if (options.throwOnFailure) throw error; |
| } finally { |
| |
| if (this.openSearchEpoch === epoch) this.searchToggleDesiredOpen = false; |
| } |
| } |
|
|
| private async waitForSlowBootstrapCheckpoint(): Promise<void> { |
| markLcpDebug('wm:data:slow-tier-wait-start'); |
| try { |
| const settled = await waitForBootstrapSlowTier(isDesktopRuntime() ? 8_500 : 3_500); |
| markLcpDebug('wm:data:slow-tier-wait-end', { settled }); |
| if (this.state.isDestroyed) return; |
| this.bootstrapHydrationState = getBootstrapHydrationState(); |
| this.updateConnectivityUi(); |
| } catch { |
| markLcpDebug('wm:data:slow-tier-wait-error'); |
| } |
| } |
|
|
| private async preloadCountryGeometryForPostLcpWork(): Promise<void> { |
| markLcpDebug('wm:data:country-geometry-start'); |
| try { |
| await preloadCountryGeometry(); |
| markLcpDebug('wm:data:country-geometry-ready'); |
| } catch { |
| markLcpDebug('wm:data:country-geometry-error'); |
| } |
| } |
|
|
| private startPostLcpIntelligence(countryGeometryReady: Promise<void>, geometryAlreadyApplied: boolean): void { |
| void countryGeometryReady.finally(() => { |
| if (this.state.isDestroyed) return; |
| |
| |
| |
| if (!geometryAlreadyApplied) { |
| this.dataLoader.refreshGeometryDependentCiiAfterCountryGeometry(); |
| } |
| |
| |
| void this.loadInitialCorrelationEngine(); |
| startLearning(); |
| }); |
| } |
|
|
| private async loadInitialCorrelationEngine(): Promise<void> { |
| try { |
| const { |
| CorrelationEngine, |
| militaryAdapter, |
| escalationAdapter, |
| economicAdapter, |
| disasterAdapter, |
| } = await import('@/services/correlation-engine'); |
|
|
| if (this.state.isDestroyed) return; |
| const engine = new CorrelationEngine(); |
| engine.registerAdapter(militaryAdapter); |
| engine.registerAdapter(escalationAdapter); |
| engine.registerAdapter(economicAdapter); |
| engine.registerAdapter(disasterAdapter); |
| this.state.correlationEngine = engine; |
|
|
| await engine.run(this.state); |
| if (this.state.isDestroyed) return; |
| for (const domain of ['military', 'escalation', 'economic', 'disaster'] as const) { |
| const panel = this.state.panels[`${domain}-correlation`] as CorrelationPanel | undefined; |
| panel?.updateCards(engine.getCards(domain)); |
| } |
| } catch (error) { |
| console.warn('[CorrelationEngine] Initial lazy load/run failed:', error); |
| } |
| } |
|
|
| public async init(): Promise<void> { |
| const initStart = performance.now(); |
| markLcpDebug('wm:boot:app-init-start'); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| this.webMcpController = registerWebMcpTools({ |
| openCountryBriefByCode: async (code, country) => { |
| await this.waitForUiReady(); |
| if (!this.state.countryBriefPage) { |
| throw new Error('Country brief panel is not initialised'); |
| } |
| await this.countryIntel.openCountryBriefByCode(code, country); |
| }, |
| resolveCountryName: (code) => CountryIntelManager.resolveCountryName(code), |
| openSearch: async () => { |
| |
| |
| |
| |
| await this.openSearch({ throwOnFailure: true }); |
| }, |
| }); |
|
|
| window.addEventListener(I18N_RESOURCES_LOADED_EVENT, this.handleI18nResourcesLoaded); |
|
|
| await initDB(); |
| startFlightHistoryCleanup(); |
| |
| |
| |
| enableVesselRuntime(); |
| await initI18n(); |
| markLcpDebug('wm:boot:i18n-ready'); |
| initDeferredDashboardFonts(); |
| |
| |
| |
| document.title = t('shell.documentTitle'); |
| const setMeta = (sel: string, val: string) => { |
| const el = document.querySelector(sel); |
| if (el) el.setAttribute('content', val); |
| }; |
| setMeta('meta[name="description"]', t('shell.metaDescription')); |
| setMeta('meta[property="og:title"]', t('shell.documentTitle')); |
| setMeta('meta[property="og:description"]', t('shell.metaDescription')); |
| setMeta('meta[name="twitter:title"]', t('shell.documentTitle')); |
| setMeta('meta[name="twitter:description"]', t('shell.metaDescription')); |
| |
| |
| |
| const ogLocaleMap: Record<string, string> = { |
| en: 'en_US', bg: 'bg_BG', cs: 'cs_CZ', fr: 'fr_FR', de: 'de_DE', el: 'el_GR', |
| es: 'es_ES', hr: 'hr_HR', hu: 'hu_HU', it: 'it_IT', pl: 'pl_PL', pt: 'pt_BR', |
| nl: 'nl_NL', sv: 'sv_SE', ru: 'ru_RU', ar: 'ar_SA', fa: 'fa_IR', zh: 'zh_CN', |
| ja: 'ja_JP', ko: 'ko_KR', ro: 'ro_RO', tr: 'tr_TR', th: 'th_TH', vi: 'vi_VN', |
| hi: 'hi_IN', |
| }; |
| const baseLang = (document.documentElement.lang || 'en').split('-')[0] || 'en'; |
| setMeta('meta[property="og:locale"]', ogLocaleMap[baseLang] || `${baseLang}_${baseLang.toUpperCase()}`); |
| const srH1 = document.querySelector('body > h1'); |
| if (srH1) srH1.textContent = t('shell.documentTitle'); |
| const aiFlow = getAiFlowSettings(); |
| if (aiFlow.browserModel || isDesktopRuntime()) { |
| await mlWorker.init(); |
| if (BETA_MODE) mlWorker.loadModel('summarization-beta').catch(() => { }); |
| } |
|
|
| |
| |
| |
| |
| if (isHeadlineMemoryEnabled()) { |
| mlWorker.init().then(ok => { |
| if (ok) mlWorker.loadModel('embeddings').catch(() => { }); |
| }).catch(() => { }); |
| } |
|
|
| this.unsubAiFlow = subscribeAiFlowChange((key) => { |
| if (key === 'browserModel') { |
| const s = getAiFlowSettings(); |
| if (s.browserModel) { |
| mlWorker.init().then(ok => { |
| |
| if (ok && isHeadlineMemoryEnabled()) { |
| mlWorker.loadModel('embeddings').catch(() => { }); |
| } |
| }).catch(() => { }); |
| } else if (!isDesktopRuntime()) { |
| |
| |
| |
| mlWorker.terminate(); |
| } |
| } |
| if (key === 'headlineMemory') { |
| if (isHeadlineMemoryEnabled()) { |
| mlWorker.init().then(ok => { |
| if (ok) mlWorker.loadModel('embeddings').catch(() => { }); |
| }).catch(() => { }); |
| } else { |
| mlWorker.unloadModel('embeddings').catch(() => { }); |
| const s = getAiFlowSettings(); |
| if (!s.browserModel && !isDesktopRuntime()) { |
| mlWorker.terminate(); |
| } |
| } |
| } |
| }); |
|
|
| |
| if (!isAisConfigured()) { |
| this.state.mapLayers.ais = false; |
| } else if (this.state.mapLayers.ais) { |
| initAisStream(); |
| } |
|
|
| |
| if (isDesktopRuntime()) { |
| await waitForSidecarReady(3000); |
| markLcpDebug('wm:boot:sidecar-ready'); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| if (!isDesktopRuntime()) { |
| window.addEventListener(WM_SESSION_DEGRADED_EVENT, this.handleWmSessionDegraded); |
| installWmSessionFetchInterceptor(); |
| await ensureWmSession(); |
| markLcpDebug('wm:boot:session-ready'); |
| } |
|
|
| |
| |
| |
| await fetchBootstrapData(() => { |
| if (this.state.isDestroyed) return; |
| this.bootstrapHydrationState = getBootstrapHydrationState(); |
| this.updateConnectivityUi(); |
| }); |
| markLcpDebug('wm:boot:fast-bootstrap-ready'); |
| this.bootstrapHydrationState = getBootstrapHydrationState(); |
|
|
| |
| await initAuthState(); |
| initAuthAnalytics(); |
| installCloudPrefsSync(SITE_VARIANT); |
| window.addEventListener(CLOUD_PREFS_APPLIED_EVENT, this.handleCloudPrefsApplied); |
| |
| |
| |
| installFollowedCountriesAuthListener(); |
| window.addEventListener(WM_FOLLOWED_COUNTRIES_CAP_DROP, this.handleFollowedCountriesCapDrop); |
| this.enforceFreeTierLimits(); |
|
|
| let _prevUserId: string | null = null; |
| let _convexWatchHandoffGeneration = 0; |
| |
| |
| |
| |
| |
| let _prevHadPremium = hasPremiumAccess(); |
| |
| |
| |
| |
| |
| |
| |
| const firePremiumLoaders = (): void => { |
| this.enforceFreeTierLimits(); |
| const hadPremium = _prevHadPremium; |
| const nowPremium = hasPremiumAccess(); |
| if (nowPremium && !hadPremium) { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| void this.dataLoader.loadTradePolicy(); |
| void this.dataLoader.loadStockAnalysis(); |
| void this.dataLoader.loadStockBacktest(); |
| void this.dataLoader.loadDailyMarketBrief(); |
| void this.dataLoader.loadMarketImplications(); |
| void this.dataLoader.loadWsbTickers(); |
| void this.dataLoader.loadResilienceRanking(); |
| void this.dataLoader.loadGlobalTenders(); |
| } else if (!nowPremium && hadPremium) { |
| |
| |
| void this.dataLoader.clearGlobalTenders(); |
| } |
| _prevHadPremium = nowPremium; |
| }; |
| this.unsubEntitlementPremiumLoaders = onEntitlementChange(() => firePremiumLoaders()); |
| this.unsubFreeTier = subscribeAuthState((session) => { |
| firePremiumLoaders(); |
|
|
| const userId = session.user?.id ?? null; |
| if (userId !== null && userId !== _prevUserId) { |
| const handoffGeneration = ++_convexWatchHandoffGeneration; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| void startAccountAuthHandoff({ |
| userId, |
| isCurrent: () => ( |
| handoffGeneration === _convexWatchHandoffGeneration && |
| getAuthState().user?.id === userId |
| ), |
| effects: { |
| destroyEntitlementSubscription, |
| resetEntitlementState, |
| destroySubscriptionWatch, |
| rebindConvexAuthForWatchHandoff, |
| initEntitlementSubscription, |
| initSubscriptionWatch, |
| cloudPrefsSignIn: (nextUserId) => cloudPrefsSignIn(nextUserId, SITE_VARIANT), |
| }, |
| }); |
|
|
| |
| const anonId = getStoredAnonId(); |
| if (anonId) { |
| void (async () => { |
| const [client, api] = await Promise.all([getConvexClient(), getConvexApi()]); |
| if (!client || !api) return; |
| |
| |
| |
| const ready = await waitForConvexAuthForUser(userId, 10_000); |
| if (!ready) { |
| console.warn('[billing] claimSubscription skipped — Convex auth not ready'); |
| return; |
| } |
| const claimToken = getFreshStoredAnonClaimToken() ?? undefined; |
| const result = await settleAccountOperation( |
| userId, |
| 'claiming the anonymous subscription', |
| () => client.mutation(api.payments.billing.claimSubscription, { |
| anonId, |
| ...(claimToken ? { claimToken } : {}), |
| }), |
| ); |
| assertAccountStillCurrent(userId, 'claiming the anonymous subscription'); |
| const claimed = result.claimed; |
| const totalClaimed = claimed.subscriptions + claimed.entitlements + |
| claimed.customers + claimed.payments; |
| if (totalClaimed > 0) { |
| console.log('[billing] Claimed anon subscription on sign-in:', claimed); |
| } |
| |
| |
| clearStoredAnonIdentity(); |
| })().catch((err: unknown) => { |
| if (!isAccountStillCurrent(userId)) return; |
| console.warn('[billing] claimSubscription failed:', err); |
| |
| }); |
| } |
|
|
| |
| |
| |
| const businessInviteGrantId = new URLSearchParams(window.location.search).get('accept-business-invite'); |
| const businessInviteToken = new URLSearchParams(window.location.search).get('token'); |
| if (businessInviteGrantId && businessInviteToken) { |
| void (async () => { |
| const [client, api] = await Promise.all([getConvexClient(), getConvexApi()]); |
| if (!client || !api) return; |
| const ready = await waitForConvexAuthForUser(userId, 10_000); |
| if (!ready) { |
| console.warn('[business-seats] acceptBusinessInvite skipped — Convex auth not ready'); |
| return; |
| } |
| try { |
| await settleAccountOperation( |
| userId, |
| 'accepting the Business Pro seat invite', |
| () => client.mutation(api.payments.businessSeats.acceptBusinessInvite, { |
| grantId: businessInviteGrantId as Id<'businessProGrants'>, |
| token: businessInviteToken, |
| }), |
| ); |
| assertAccountStillCurrent(userId, 'accepting the Business Pro seat invite'); |
| showToast('Pro seat activated'); |
| } catch (err) { |
| if (!isAccountStillCurrent(userId)) return; |
| const msg = err instanceof Error ? err.message : 'Failed to accept invite'; |
| if (msg.includes('INVITE_EMAIL_MISMATCH')) { |
| showToast('This invite is for a different email address'); |
| } else if (msg.includes('INVITE_EXPIRED')) { |
| showToast('This invite has expired'); |
| } else if (msg.includes('BUSINESS_NOT_ACTIVE')) { |
| showToast('The Business plan that sent this invite is no longer active'); |
| } else if (msg.includes('INVITE_ALREADY_USED')) { |
| showToast('This invite has already been used'); |
| } else { |
| showToast('Could not accept invite'); |
| } |
| console.warn('[business-seats] acceptBusinessInvite failed:', err); |
| } finally { |
| |
| const url = new URL(window.location.href); |
| url.searchParams.delete('accept-business-invite'); |
| url.searchParams.delete('token'); |
| window.history.replaceState({}, '', url.toString()); |
| } |
| })(); |
| } |
| void resumePendingCheckout({ |
| openAuth: () => this.state.authModal?.open(), |
| }); |
| } else if (userId === null && _prevUserId !== null) { |
| |
| |
| |
| invalidateConvexAuthForSignOut(); |
| |
| |
| _convexWatchHandoffGeneration++; |
| destroyEntitlementSubscription(); |
| destroySubscriptionWatch(); |
| cloudPrefsSignOut(); |
| resetEntitlementState(); |
| } |
| _prevUserId = userId; |
| }); |
|
|
|
|
| const geoCoordsPromise: Promise<PreciseCoordinates | null> = |
| this.state.isMobile && this.state.initialUrlState?.lat === undefined && this.state.initialUrlState?.lon === undefined |
| ? resolvePreciseUserCoordinates(5000) |
| : Promise.resolve(null); |
|
|
| const resolvedRegion = await resolveUserRegion(); |
| this.state.resolvedLocation = resolvedRegion; |
|
|
| |
| |
| |
| markLcpDebug('wm:layout:init-start'); |
| await this.panelLayout.init(); |
| markLcpDebug('wm:layout:init-complete'); |
| this.eventHandlers.setupSearchControls(); |
| showProBanner(this.state.container); |
| this.updateConnectivityUi(); |
| window.addEventListener('online', this.handleConnectivityChange); |
| window.addEventListener('offline', this.handleConnectivityChange); |
|
|
| const mobileGeoCoords = await geoCoordsPromise; |
| if (mobileGeoCoords && this.state.map) { |
| this.state.map.setCenter(mobileGeoCoords.lat, mobileGeoCoords.lon, 6); |
| } |
|
|
| |
| if (SITE_VARIANT === 'happy') { |
| await this.dataLoader.hydrateHappyPanelsFromCache(); |
| } |
|
|
| |
| if (!this.state.isMobile) { |
| void this.initFindingsBadge(); |
| } |
|
|
| initBreakingNewsAlerts(); |
| this.state.breakingBanner = new BreakingNewsBanner(); |
|
|
| |
| this.eventHandlers.startHeaderClock(); |
| this.eventHandlers.setupPlaybackControl(); |
| this.eventHandlers.setupStatusPanel(); |
| this.eventHandlers.setupPizzIntIndicator(); |
| this.eventHandlers.setupLlmStatusIndicator(); |
| this.eventHandlers.setupExportPanel(); |
| this.eventHandlers.setupSearchControls(); |
|
|
| |
| |
| this.eventHandlers.setupUnifiedSettings(); |
| this.eventHandlers.setupAuthWidget(); |
| |
| |
| |
| |
| |
| captureReferralFromUrl(); |
| |
| |
| |
| initCheckoutWatchers(); |
| |
| |
| |
| |
| |
| |
| const pendingCheckout = capturePendingCheckoutIntentFromUrl(); |
| if (pendingCheckout) { |
| |
| |
| void resumePendingCheckout({ |
| openAuth: () => this.state.authModal?.open(), |
| }); |
| } |
|
|
| |
| |
| this.eventHandlers.setupMapLayerHandlers(); |
| await this.countryIntel.init(); |
| |
| this.resolveUiReady(); |
|
|
| |
| this.eventHandlers.init(); |
| |
| const initState = parseMapUrlState(window.location.search, this.state.mapLayers); |
| this.pendingDeepLinkCountry = initState.country ?? null; |
| this.pendingDeepLinkExpanded = initState.expanded === true; |
| this.pendingDeepLinkChokepoint = initState.chokepoint ?? null; |
| const earlyParams = new URLSearchParams(window.location.search); |
| this.pendingDeepLinkStoryCode = earlyParams.get('c') ?? null; |
| this.eventHandlers.setupUrlStateSync(); |
| if (import.meta.env.VITE_E2E === '1') { |
| document.documentElement.dataset.wmEventHandlersReady = 'true'; |
| } |
|
|
| this.state.countryBriefPage?.onStateChange?.(() => { |
| this.eventHandlers.syncUrlState(); |
| }); |
|
|
| |
| |
| this.handleDeepLinks(); |
|
|
| |
| this.dataLoader.syncDataFreshnessWithLayers(); |
| const slowTierReady = this.waitForSlowBootstrapCheckpoint(); |
| if (this.state.isDestroyed) return; |
| |
| |
| |
| |
| window.addEventListener('scroll', this.handleViewportPrime, { passive: true }); |
| window.addEventListener('resize', this.handleViewportPrime); |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| await slowTierReady; |
| if (this.state.isDestroyed) return; |
| |
| |
| |
| |
| |
| const geometryReadyBeforeFanout = isCountryGeometryLoaded(); |
| markLcpDebug('wm:data:initial-fanout-start'); |
| await Promise.all([ |
| this.dataLoader.loadAllData(), |
| this.primeVisiblePanelData(), |
| ]); |
| markLcpDebug('wm:data:initial-fanout-complete'); |
| const countryGeometryReady = this.preloadCountryGeometryForPostLcpWork(); |
|
|
| |
| markBootstrapAsLive(); |
| this.bootstrapHydrationState = getBootstrapHydrationState(); |
| this.updateConnectivityUi(); |
|
|
| |
| |
| this.startPostLcpIntelligence(countryGeometryReady, geometryReadyBeforeFanout); |
|
|
| |
| if (!isAisConfigured()) { |
| this.state.map?.hideLayerToggle('ais'); |
| } |
| if (isOutagesConfigured() === false) { |
| this.state.map?.hideLayerToggle('outages'); |
| } |
| if (!CYBER_LAYER_ENABLED) { |
| this.state.map?.hideLayerToggle('cyberThreats'); |
| } |
|
|
| |
| this.setupRefreshIntervals(); |
| this.eventHandlers.setupSnapshotSaving(); |
| cleanOldSnapshots().catch((e) => console.warn('[Storage] Snapshot cleanup failed:', e)); |
|
|
| |
| this.desktopUpdater.init(); |
|
|
| |
| trackEvent('wm_app_loaded', { |
| load_time_ms: Math.round(performance.now() - initStart), |
| panel_count: Object.keys(this.state.panels).length, |
| }); |
| this.eventHandlers.setupPanelViewTracking(); |
| } |
|
|
| |
| |
| |
| |
| |
| private enforceFreeTierLimits(): void { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const schemaVersion = loadFromStorage<number>(STORAGE_KEYS.disabledFeedsSchema, 0); |
| if (schemaVersion < 1) { |
| const disabled = new Set(loadFromStorage<string[]>(STORAGE_KEYS.disabledFeeds, [])); |
| const recoverable = findFullyDisabledCategories(FEEDS, disabled); |
| if (recoverable.length > 0) { |
| for (const name of recoverable) disabled.delete(name); |
| saveToStorage(STORAGE_KEYS.disabledFeeds, Array.from(disabled)); |
| console.log(`[App] One-time v1-cap-bug migration: re-enabled ${recoverable.length} source(s) from fully-disabled categories. This will not run again.`); |
| } |
| saveToStorage(STORAGE_KEYS.disabledFeedsSchema, 1); |
| } |
|
|
| if (isProUser()) return; |
|
|
| |
| |
| |
| |
| |
| let panelSettings = loadFromStorage<Record<string, PanelConfig>>(STORAGE_KEYS.panels, {}); |
| let panelsChanged = false; |
| try { |
| if (!localStorage.getItem(FREE_MAP_PANEL_ACCESS_KEY)) { |
| const restoredPanels = restoreFreeMapPanelAccess(panelSettings); |
| if (panelSettings.map?.enabled !== restoredPanels.map?.enabled) { |
| panelSettings = restoredPanels; |
| panelsChanged = true; |
| } |
| localStorage.setItem(FREE_MAP_PANEL_ACCESS_KEY, 'done'); |
| } |
| } catch { |
| |
| } |
| const clampedPanels = enforceFreePanelLimit(panelSettings, false); |
| for (const key of Object.keys(panelSettings)) { |
| if (panelSettings[key]?.enabled !== clampedPanels[key]?.enabled) { |
| panelsChanged = true; |
| break; |
| } |
| } |
| if (panelsChanged) { |
| saveToStorage(STORAGE_KEYS.panels, clampedPanels); |
| this.state.panelSettings = clampedPanels; |
| console.log(`[App] Free tier: enforced ${FREE_MAX_PANELS}-panel limit (disabled over-cap / cw-* panels)`); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const disabledSources = new Set(loadFromStorage<string[]>(STORAGE_KEYS.disabledFeeds, [])); |
| const totalEligible = (() => { |
| const s = new Set<string>(); |
| Object.values(FEEDS).forEach((feeds) => feeds?.forEach((f) => s.add(f.name))); |
| INTEL_SOURCES.forEach((f) => s.add(f.name)); |
| let count = 0; |
| for (const name of s) if (!disabledSources.has(name)) count++; |
| return count; |
| })(); |
| if (totalEligible > FREE_MAX_SOURCES) { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| let explicitLocale = ''; |
| try { explicitLocale = localStorage.getItem('wm-locale-explicit') || ''; } catch { } |
| const userLang = ((explicitLocale || navigator.language || 'en').split('-')[0] ?? 'en').toLowerCase(); |
| const protectedNames = userLang === 'en' ? new Set<string>() : getLocaleBoostedSources(userLang); |
| const { keep, autoDisabled } = selectSourcesUnderCap(FEEDS, INTEL_SOURCES, disabledSources, FREE_MAX_SOURCES, protectedNames); |
| |
| |
| |
| |
| |
| for (const name of autoDisabled) { |
| if (!keep.has(name)) disabledSources.add(name); |
| } |
| saveToStorage(STORAGE_KEYS.disabledFeeds, Array.from(disabledSources)); |
| console.log(`[App] Free tier: round-robin disabled ${autoDisabled.size} source(s) to enforce ${FREE_MAX_SOURCES}-source limit (per-category fairness)`); |
| } |
| } |
|
|
| public destroy(): void { |
| this.state.isDestroyed = true; |
| cancelBootstrapSlowTier(); |
| window.removeEventListener('scroll', this.handleViewportPrime); |
| window.removeEventListener('resize', this.handleViewportPrime); |
| window.removeEventListener('online', this.handleConnectivityChange); |
| window.removeEventListener('offline', this.handleConnectivityChange); |
| window.removeEventListener(I18N_RESOURCES_LOADED_EVENT, this.handleI18nResourcesLoaded); |
| window.removeEventListener(WM_FOLLOWED_COUNTRIES_CAP_DROP, this.handleFollowedCountriesCapDrop); |
| window.removeEventListener(CLOUD_PREFS_APPLIED_EVENT, this.handleCloudPrefsApplied); |
| if (this.visiblePanelPrimeRaf !== null) { |
| window.cancelAnimationFrame(this.visiblePanelPrimeRaf); |
| this.visiblePanelPrimeRaf = null; |
| } |
| if (this.chokepointDeepLinkTimer !== null) { |
| window.clearTimeout(this.chokepointDeepLinkTimer); |
| this.chokepointDeepLinkTimer = null; |
| } |
|
|
| |
| for (let i = this.modules.length - 1; i >= 0; i--) { |
| this.modules[i]!.destroy(); |
| } |
|
|
| |
| this.unsubAiFlow?.(); |
| this.unsubFreeTier?.(); |
| this.unsubEntitlementPremiumLoaders?.(); |
| mlWorker.terminate(); |
| this.state.findingsBadge?.destroy(); |
| this.state.findingsBadge = null; |
| this.state.breakingBanner?.destroy(); |
| destroyBreakingNewsAlerts(); |
| this.cachedModeBannerEl?.remove(); |
| this.cachedModeBannerEl = null; |
| window.removeEventListener(WM_SESSION_DEGRADED_EVENT, this.handleWmSessionDegraded); |
| if (this.followedCountriesCapDropToastTimer !== null) { |
| window.clearTimeout(this.followedCountriesCapDropToastTimer); |
| this.followedCountriesCapDropToastTimer = null; |
| } |
| this.state.map?.destroy(); |
| disconnectAisStream(); |
| stopFlightHistoryCleanup(); |
| stopLoadedVesselHistoryCleanup(); |
| |
| |
| |
| this.webMcpController?.abort(); |
| this.webMcpController = null; |
| } |
|
|
| private async initFindingsBadge(): Promise<void> { |
| try { |
| const { IntelligenceGapBadge } = await import('@/components/IntelligenceGapBadge'); |
| if (this.state.isDestroyed) return; |
| this.state.findingsBadge = new IntelligenceGapBadge(); |
| this.state.findingsBadge.setOnSignalClick((signal) => { |
| if (this.state.countryBriefPage?.isVisible()) return; |
| if (localStorage.getItem('wm-settings-open') === '1') return; |
| void this.state.ensureSignalModal() |
| .then((signalModal) => { |
| if (!this.state.isDestroyed) signalModal.showSignal(signal); |
| }) |
| .catch((err) => { |
| console.warn('[SignalModal] Failed to show signal:', err); |
| }); |
| }); |
| this.state.findingsBadge.setOnAlertClick((alert) => { |
| if (this.state.countryBriefPage?.isVisible()) return; |
| if (localStorage.getItem('wm-settings-open') === '1') return; |
| void this.state.ensureSignalModal() |
| .then((signalModal) => { |
| if (!this.state.isDestroyed) signalModal.showAlert(alert); |
| }) |
| .catch((err) => { |
| console.warn('[SignalModal] Failed to show alert:', err); |
| }); |
| }); |
| } catch (error) { |
| console.warn('[IntelligenceGapBadge] Lazy init failed:', error); |
| } |
| } |
|
|
| private showFollowedCountriesCapDropToast(kept: number, dropped: number): void { |
| if (this.followedCountriesCapDropToastTimer !== null) { |
| window.clearTimeout(this.followedCountriesCapDropToastTimer); |
| this.followedCountriesCapDropToastTimer = null; |
| } |
| document.querySelector('.wm-followed-cap-drop-toast')?.remove(); |
|
|
| const toast = document.createElement('div'); |
| toast.className = 'wm-followed-cap-drop-toast update-toast'; |
| toast.setAttribute('role', 'status'); |
| toast.setAttribute('aria-live', 'polite'); |
|
|
| const body = document.createElement('div'); |
| body.className = 'update-toast-body'; |
|
|
| const title = document.createElement('div'); |
| title.className = 'update-toast-title'; |
| title.textContent = 'Follow limit reached'; |
|
|
| const detail = document.createElement('div'); |
| detail.className = 'update-toast-detail'; |
| const countryWord = dropped === 1 ? 'country was' : 'countries were'; |
| detail.textContent = `${kept} kept. ${dropped} ${countryWord} not added because the free plan supports ${FREE_TIER_FOLLOW_LIMIT} followed countries.`; |
|
|
| body.append(title, detail); |
|
|
| const action = document.createElement('button'); |
| action.type = 'button'; |
| action.className = 'update-toast-action'; |
| action.dataset.action = 'upgrade'; |
| action.textContent = 'Upgrade'; |
|
|
| const dismiss = document.createElement('button'); |
| dismiss.type = 'button'; |
| dismiss.className = 'update-toast-dismiss'; |
| dismiss.dataset.action = 'dismiss'; |
| dismiss.setAttribute('aria-label', 'Dismiss'); |
| dismiss.textContent = '\u00d7'; |
|
|
| toast.append(body, action, dismiss); |
|
|
| this.followedCountriesCapDropToastTimer = window.setTimeout(() => { |
| toast.remove(); |
| this.followedCountriesCapDropToastTimer = null; |
| }, 8000); |
| toast.addEventListener('click', (e) => { |
| const clickedAction = (e.target as HTMLElement) |
| .closest<HTMLElement>('[data-action]') |
| ?.dataset.action; |
| if (clickedAction === 'upgrade') { |
| window.open('/pro#pricing', '_blank', 'noopener,noreferrer'); |
| if (this.followedCountriesCapDropToastTimer !== null) { |
| window.clearTimeout(this.followedCountriesCapDropToastTimer); |
| this.followedCountriesCapDropToastTimer = null; |
| } |
| toast.remove(); |
| } else if (clickedAction === 'dismiss') { |
| if (this.followedCountriesCapDropToastTimer !== null) { |
| window.clearTimeout(this.followedCountriesCapDropToastTimer); |
| this.followedCountriesCapDropToastTimer = null; |
| } |
| toast.remove(); |
| } |
| }); |
|
|
| document.body.appendChild(toast); |
| window.requestAnimationFrame(() => toast.classList.add('visible')); |
| } |
|
|
| |
| |
| |
| |
| |
| private async waitForUiReady(timeoutMs = 10_000): Promise<void> { |
| let timer: ReturnType<typeof setTimeout> | null = null; |
| const timeout = new Promise<never>((_, reject) => { |
| timer = setTimeout( |
| () => reject(new Error(`UI did not initialise within ${timeoutMs}ms`)), |
| timeoutMs, |
| ); |
| }); |
| try { |
| await Promise.race([this.uiReady, timeout]); |
| } finally { |
| if (timer !== null) clearTimeout(timer); |
| } |
| } |
|
|
| private handleDeepLinks(): void { |
| const url = new URL(window.location.href); |
| const DEEP_LINK_INITIAL_DELAY_MS = 1500; |
|
|
| |
| const storyCode = this.pendingDeepLinkStoryCode ?? url.searchParams.get('c'); |
| this.pendingDeepLinkStoryCode = null; |
| if (url.pathname === '/story' || storyCode) { |
| const countryCode = storyCode; |
| if (countryCode) { |
| trackDeeplinkOpened('country', countryCode); |
| const countryName = getCountryNameByCode(countryCode.toUpperCase()) || countryCode; |
| setTimeout(() => { |
| void this.countryIntel.openCountryBriefByCode(countryCode.toUpperCase(), countryName, { |
| maximize: true, |
| }).catch((err) => { |
| console.error('[CountryBrief] Failed to open country brief:', err); |
| this.state.map?.setRenderPaused(false); |
| showToast('Country brief failed to open. Please try again.'); |
| }); |
| this.eventHandlers.syncUrlState(); |
| }, DEEP_LINK_INITIAL_DELAY_MS); |
| return; |
| } |
| } |
|
|
| |
| const deepLinkCountry = this.pendingDeepLinkCountry; |
| const deepLinkExpanded = this.pendingDeepLinkExpanded; |
| this.pendingDeepLinkCountry = null; |
| this.pendingDeepLinkExpanded = false; |
| if (deepLinkCountry) { |
| trackDeeplinkOpened('country', deepLinkCountry); |
| const cName = CountryIntelManager.resolveCountryName(deepLinkCountry); |
| setTimeout(() => { |
| void this.countryIntel.openCountryBriefByCode(deepLinkCountry, cName, { |
| maximize: deepLinkExpanded, |
| }).catch((err) => { |
| console.error('[CountryBrief] Failed to open country brief:', err); |
| this.state.map?.setRenderPaused(false); |
| showToast('Country brief failed to open. Please try again.'); |
| }); |
| this.eventHandlers.syncUrlState(); |
| }, DEEP_LINK_INITIAL_DELAY_MS); |
| } |
|
|
| |
| |
| |
| const deepLinkChokepoint = this.pendingDeepLinkChokepoint; |
| this.pendingDeepLinkChokepoint = null; |
| if (deepLinkChokepoint) { |
| trackDeeplinkOpened('chokepoint', deepLinkChokepoint); |
| this.state.activeChokepoint = deepLinkChokepoint; |
| this.chokepointDeepLinkTimer = window.setTimeout(() => { |
| this.chokepointDeepLinkTimer = null; |
| if (this.state.isDestroyed) return; |
| this.state.mapLayers.waterways = true; |
| this.state.map?.enableLayer('waterways'); |
| this.state.map?.openChokepoint(deepLinkChokepoint); |
| this.eventHandlers.syncUrlState(); |
| }, DEEP_LINK_INITIAL_DELAY_MS); |
| } |
| } |
|
|
| private setupRefreshIntervals(): void { |
| |
| this.refreshScheduler.scheduleRefresh('news', () => this.dataLoader.loadNews(), REFRESH_INTERVALS.feeds); |
| |
| |
| |
| scheduleAfterFirstPaint(() => { |
| this.refreshScheduler.scheduleRefresh( |
| 'health-freshness', |
| async () => { await refreshDataFreshnessFromHealth(); }, |
| REFRESH_INTERVALS.healthFreshness, |
| undefined, |
| { runImmediately: true }, |
| ); |
| }); |
|
|
| |
| if (SITE_VARIANT !== 'happy') { |
| this.refreshScheduler.registerAll([ |
| { |
| name: 'markets', |
| fn: () => this.dataLoader.loadMarkets(), |
| intervalMs: REFRESH_INTERVALS.markets, |
| condition: () => this.isAnyPanelNearViewport(['markets', 'heatmap', 'commodities', 'crypto', 'crypto-heatmap', 'defi-tokens', 'ai-tokens', 'other-tokens']), |
| }, |
| { |
| name: 'predictions', |
| fn: () => this.dataLoader.loadPredictions(), |
| intervalMs: REFRESH_INTERVALS.predictions, |
| condition: () => this.isPanelNearViewport('polymarket'), |
| }, |
| { |
| name: 'forecasts', |
| fn: () => this.dataLoader.loadForecasts(), |
| intervalMs: REFRESH_INTERVALS.forecasts, |
| condition: () => this.isPanelNearViewport('forecast'), |
| }, |
| { name: 'pizzint', fn: () => this.dataLoader.loadPizzInt(), intervalMs: REFRESH_INTERVALS.pizzint, condition: () => SITE_VARIANT === 'full' }, |
| { name: 'natural', fn: () => this.dataLoader.loadNatural(), intervalMs: REFRESH_INTERVALS.natural, condition: () => this.state.mapLayers.natural }, |
| { name: 'weather', fn: () => this.dataLoader.loadWeatherAlerts(), intervalMs: REFRESH_INTERVALS.weather, condition: () => this.state.mapLayers.weather }, |
| { name: 'fred', fn: () => this.dataLoader.loadFredData(), intervalMs: REFRESH_INTERVALS.fred, condition: () => this.isPanelNearViewport('economic') }, |
| { name: 'spending', fn: () => this.dataLoader.loadGovernmentSpending(), intervalMs: REFRESH_INTERVALS.spending, condition: () => this.isPanelNearViewport('economic') }, |
| { name: 'global-tenders', fn: () => this.dataLoader.loadGlobalTenders(), intervalMs: REFRESH_INTERVALS.spending, condition: () => hasPremiumAccess() && this.isPanelNearViewport('global-procurement') }, |
| { name: 'bis', fn: () => this.dataLoader.loadBisData(), intervalMs: REFRESH_INTERVALS.bis, condition: () => this.isPanelNearViewport('economic') }, |
| { name: 'oil', fn: () => this.dataLoader.loadOilAnalytics(), intervalMs: REFRESH_INTERVALS.oil, condition: () => this.isPanelNearViewport('energy-complex') }, |
| { name: 'firms', fn: () => this.dataLoader.loadFirmsData(), intervalMs: REFRESH_INTERVALS.firms, condition: () => this.shouldRefreshFirms() }, |
| { name: 'ais', fn: () => this.dataLoader.loadAisSignals(), intervalMs: REFRESH_INTERVALS.ais, condition: () => this.state.mapLayers.ais }, |
| { name: 'cables', fn: () => this.dataLoader.loadCableActivity(), intervalMs: REFRESH_INTERVALS.cables, condition: () => this.state.mapLayers.cables }, |
| { name: 'cableHealth', fn: () => this.dataLoader.loadCableHealth(), intervalMs: REFRESH_INTERVALS.cableHealth, condition: () => this.state.mapLayers.cables }, |
| { name: 'flights', fn: () => this.dataLoader.loadFlightDelays(), intervalMs: REFRESH_INTERVALS.flights, condition: () => this.state.mapLayers.flights }, |
| { |
| name: 'cyberThreats', fn: () => { |
| this.state.cyberThreatsCache = null; |
| return this.dataLoader.loadCyberThreats(); |
| }, intervalMs: REFRESH_INTERVALS.cyberThreats, condition: () => CYBER_LAYER_ENABLED && this.state.mapLayers.cyberThreats |
| }, |
| ]); |
| } |
|
|
| if (SITE_VARIANT === 'finance') { |
| this.refreshScheduler.scheduleRefresh( |
| 'stock-analysis', |
| () => this.dataLoader.loadStockAnalysis(), |
| REFRESH_INTERVALS.stockAnalysis, |
| () => hasPremiumAccess() && this.isPanelNearViewport('stock-analysis'), |
| ); |
| this.refreshScheduler.scheduleRefresh( |
| 'daily-market-brief', |
| () => this.dataLoader.loadDailyMarketBrief(), |
| REFRESH_INTERVALS.dailyMarketBrief, |
| () => hasPremiumAccess() && this.isPanelNearViewport('daily-market-brief'), |
| ); |
| this.refreshScheduler.scheduleRefresh( |
| 'stock-backtest', |
| () => this.dataLoader.loadStockBacktest(), |
| REFRESH_INTERVALS.stockBacktest, |
| () => hasPremiumAccess() && this.isPanelNearViewport('stock-backtest'), |
| ); |
| this.refreshScheduler.scheduleRefresh( |
| 'market-implications', |
| () => this.dataLoader.loadMarketImplications(), |
| REFRESH_INTERVALS.marketImplications, |
| () => hasPremiumAccess() && this.isPanelNearViewport('market-implications'), |
| ); |
| } |
|
|
| |
| this.refreshScheduler.scheduleRefresh( |
| 'service-status', |
| () => (this.state.panels['service-status'] as ServiceStatusPanel).fetchStatus(), |
| REFRESH_INTERVALS.serviceStatus, |
| () => this.isPanelNearViewport('service-status') |
| ); |
| this.refreshScheduler.scheduleRefresh( |
| 'stablecoins', |
| () => (this.state.panels.stablecoins as StablecoinPanel).fetchData(), |
| REFRESH_INTERVALS.stablecoins, |
| () => this.isPanelNearViewport('stablecoins') |
| ); |
| this.refreshScheduler.scheduleRefresh( |
| 'energy-crisis', |
| () => (this.state.panels['energy-crisis'] as EnergyCrisisPanel).fetchData(), |
| REFRESH_INTERVALS.energyCrisis, |
| () => this.isPanelNearViewport('energy-crisis') |
| ); |
| this.refreshScheduler.scheduleRefresh( |
| 'etf-flows', |
| () => (this.state.panels['etf-flows'] as ETFFlowsPanel).fetchData(), |
| REFRESH_INTERVALS.etfFlows, |
| () => this.isPanelNearViewport('etf-flows') |
| ); |
| this.refreshScheduler.scheduleRefresh( |
| 'macro-signals', |
| () => (this.state.panels['macro-signals'] as MacroSignalsPanel).fetchData(), |
| REFRESH_INTERVALS.macroSignals, |
| () => this.isPanelNearViewport('macro-signals') |
| ); |
| this.refreshScheduler.scheduleRefresh( |
| 'defense-patents', |
| () => { (this.state.panels['defense-patents'] as DefensePatentsPanel).refresh(); return Promise.resolve(); }, |
| REFRESH_INTERVALS.defensePatents, |
| () => this.isPanelNearViewport('defense-patents') |
| ); |
| this.refreshScheduler.scheduleRefresh( |
| 'fear-greed', |
| () => (this.state.panels['fear-greed'] as FearGreedPanel).fetchData(), |
| REFRESH_INTERVALS.fearGreed, |
| () => this.isPanelNearViewport('fear-greed') |
| ); |
| this.refreshScheduler.scheduleRefresh( |
| 'hormuz-tracker', |
| () => (this.state.panels['hormuz-tracker'] as HormuzPanel).fetchData(), |
| REFRESH_INTERVALS.hormuzTracker, |
| () => this.isPanelNearViewport('hormuz-tracker') |
| ); |
| this.refreshScheduler.scheduleRefresh( |
| 'positioning-247', |
| () => (this.state.panels['positioning-247'] as PositioningPanel).fetchData(), |
| REFRESH_INTERVALS.hyperliquidFlow, |
| () => this.isPanelNearViewport('positioning-247') |
| ); |
| this.refreshScheduler.scheduleRefresh( |
| 'strategic-posture', |
| () => (this.state.panels['strategic-posture'] as StrategicPosturePanel).refresh(), |
| REFRESH_INTERVALS.strategicPosture, |
| () => this.isPanelNearViewport('strategic-posture') |
| ); |
| this.refreshScheduler.scheduleRefresh( |
| 'strategic-risk', |
| () => (this.state.panels['strategic-risk'] as StrategicRiskPanel).refresh(), |
| REFRESH_INTERVALS.strategicRisk, |
| () => this.isPanelNearViewport('strategic-risk') |
| ); |
|
|
| this.refreshScheduler.scheduleRefresh( |
| 'wsb-tickers', |
| () => this.dataLoader.loadWsbTickers(), |
| REFRESH_INTERVALS.wsbTickers, |
| () => hasPremiumAccess() && this.isPanelNearViewport('wsb-ticker-scanner'), |
| ); |
|
|
| |
| if (SITE_VARIANT !== 'happy') { |
| this.refreshScheduler.scheduleRefresh('temporalBaseline', () => this.dataLoader.refreshTemporalBaseline(), REFRESH_INTERVALS.temporalBaseline, () => this.shouldRefreshIntelligence()); |
| } |
|
|
| |
| |
| |
| |
| if (SITE_VARIANT === 'full' || SITE_VARIANT === 'finance' || SITE_VARIANT === 'commodity' || SITE_VARIANT === 'energy') { |
| this.refreshScheduler.scheduleRefresh('tradePolicy', () => this.dataLoader.loadTradePolicy(), REFRESH_INTERVALS.tradePolicy, () => hasPremiumAccess() && this.isPanelNearViewport('trade-policy')); |
| this.refreshScheduler.scheduleRefresh('supplyChain', () => this.dataLoader.loadSupplyChain(), REFRESH_INTERVALS.supplyChain, () => this.isPanelNearViewport('supply-chain')); |
| this.refreshScheduler.scheduleRefresh('chinaCorridors', () => this.dataLoader.loadChinaCorridors(), REFRESH_INTERVALS.chinaCorridors, () => this.isPanelNearViewport('china-corridors')); |
| this.refreshScheduler.scheduleRefresh('chinaActivityNowcast', () => this.dataLoader.loadChinaActivityNowcast(), REFRESH_INTERVALS.chinaActivityNowcast, () => this.isPanelNearViewport('china-activity-nowcast')); |
| } |
|
|
| this.refreshScheduler.scheduleRefresh( |
| 'cross-source-signals', |
| () => this.dataLoader.loadCrossSourceSignals(), |
| REFRESH_INTERVALS.crossSourceSignals, |
| () => this.isPanelNearViewport('cross-source-signals'), |
| ); |
|
|
| |
| this.refreshScheduler.scheduleRefresh( |
| 'telegram-intel', |
| () => this.dataLoader.loadTelegramIntel(), |
| REFRESH_INTERVALS.telegramIntel, |
| () => this.isPanelNearViewport('telegram-intel') |
| ); |
|
|
| this.refreshScheduler.scheduleRefresh( |
| 'gulf-economies', |
| () => (this.state.panels['gulf-economies'] as GulfEconomiesPanel).fetchData(), |
| REFRESH_INTERVALS.gulfEconomies, |
| () => this.isPanelNearViewport('gulf-economies') |
| ); |
|
|
| this.refreshScheduler.scheduleRefresh( |
| 'grocery-basket', |
| () => (this.state.panels['grocery-basket'] as GroceryBasketPanel).fetchData(), |
| REFRESH_INTERVALS.groceryBasket, |
| () => this.isPanelNearViewport('grocery-basket') |
| ); |
|
|
| this.refreshScheduler.scheduleRefresh( |
| 'bigmac', |
| () => (this.state.panels['bigmac'] as BigMacPanel).fetchData(), |
| REFRESH_INTERVALS.groceryBasket, |
| () => this.isPanelNearViewport('bigmac') |
| ); |
|
|
| this.refreshScheduler.scheduleRefresh( |
| 'fuel-prices', |
| () => (this.state.panels['fuel-prices'] as FuelPricesPanel).fetchData(), |
| REFRESH_INTERVALS.fuelPrices, |
| () => this.isPanelNearViewport('fuel-prices') |
| ); |
|
|
| this.refreshScheduler.scheduleRefresh( |
| 'fao-food-price-index', |
| () => (this.state.panels['fao-food-price-index'] as FaoFoodPriceIndexPanel).fetchData(), |
| REFRESH_INTERVALS.faoFoodPriceIndex, |
| () => this.isPanelNearViewport('fao-food-price-index') |
| ); |
|
|
| this.refreshScheduler.scheduleRefresh( |
| 'oil-inventories', |
| () => (this.state.panels['oil-inventories'] as OilInventoriesPanel).fetchData(), |
| REFRESH_INTERVALS.oilInventories, |
| () => this.isPanelNearViewport('oil-inventories') |
| ); |
|
|
| this.refreshScheduler.scheduleRefresh( |
| 'pipeline-status', |
| () => (this.state.panels['pipeline-status'] as PipelineStatusPanel).fetchData(), |
| REFRESH_INTERVALS.pipelineStatus, |
| () => this.isPanelNearViewport('pipeline-status') |
| ); |
|
|
| this.refreshScheduler.scheduleRefresh( |
| 'storage-facility-map', |
| () => (this.state.panels['storage-facility-map'] as StorageFacilityMapPanel).fetchData(), |
| REFRESH_INTERVALS.storageFacilityMap, |
| () => this.isPanelNearViewport('storage-facility-map') |
| ); |
|
|
| this.refreshScheduler.scheduleRefresh( |
| 'fuel-shortages', |
| () => (this.state.panels['fuel-shortages'] as FuelShortagePanel).fetchData(), |
| REFRESH_INTERVALS.fuelShortages, |
| () => this.isPanelNearViewport('fuel-shortages') |
| ); |
|
|
| this.refreshScheduler.scheduleRefresh( |
| 'energy-disruptions', |
| () => (this.state.panels['energy-disruptions'] as EnergyDisruptionsPanel).fetchData(), |
| REFRESH_INTERVALS.energyDisruptions, |
| () => this.isPanelNearViewport('energy-disruptions') |
| ); |
|
|
| this.refreshScheduler.scheduleRefresh( |
| 'energy-risk-overview', |
| () => (this.state.panels['energy-risk-overview'] as EnergyRiskOverviewPanel).fetchData(), |
| REFRESH_INTERVALS.energyRiskOverview, |
| () => this.isPanelNearViewport('energy-risk-overview') |
| ); |
|
|
| this.refreshScheduler.scheduleRefresh( |
| 'chokepoint-strip', |
| () => (this.state.panels['chokepoint-strip'] as ChokepointStripPanel).fetchData(), |
| REFRESH_INTERVALS.chokepointStrip, |
| () => this.isPanelNearViewport('chokepoint-strip') |
| ); |
|
|
| this.refreshScheduler.scheduleRefresh( |
| 'climate-news', |
| () => (this.state.panels['climate-news'] as ClimateNewsPanel).fetchData(), |
| REFRESH_INTERVALS.climateNews, |
| () => this.isPanelNearViewport('climate-news') |
| ); |
|
|
| this.refreshScheduler.scheduleRefresh( |
| 'macro-tiles', |
| () => (this.state.panels['macro-tiles'] as MacroTilesPanel).fetchData(), |
| REFRESH_INTERVALS.macroTiles, |
| () => this.isPanelNearViewport('macro-tiles') |
| ); |
| this.refreshScheduler.scheduleRefresh( |
| 'fsi', |
| () => (this.state.panels['fsi'] as FSIPanel).fetchData(), |
| REFRESH_INTERVALS.fsi, |
| () => this.isPanelNearViewport('fsi') |
| ); |
| this.refreshScheduler.scheduleRefresh( |
| 'yield-curve', |
| () => (this.state.panels['yield-curve'] as YieldCurvePanel).fetchData(), |
| REFRESH_INTERVALS.yieldCurve, |
| () => this.isPanelNearViewport('yield-curve') |
| ); |
| this.refreshScheduler.scheduleRefresh( |
| 'earnings-calendar', |
| () => (this.state.panels['earnings-calendar'] as EarningsCalendarPanel).fetchData(), |
| REFRESH_INTERVALS.earningsCalendar, |
| () => this.isPanelNearViewport('earnings-calendar') |
| ); |
| this.refreshScheduler.scheduleRefresh( |
| 'economic-calendar', |
| () => (this.state.panels['economic-calendar'] as EconomicCalendarPanel).fetchData(), |
| REFRESH_INTERVALS.economicCalendar, |
| () => this.isPanelNearViewport('economic-calendar') |
| ); |
| this.refreshScheduler.scheduleRefresh( |
| 'cot-positioning', |
| () => (this.state.panels['cot-positioning'] as CotPositioningPanel).fetchData(), |
| REFRESH_INTERVALS.cotPositioning, |
| () => this.isPanelNearViewport('cot-positioning') |
| ); |
| this.refreshScheduler.scheduleRefresh( |
| 'gold-intelligence', |
| () => (this.state.panels['gold-intelligence'] as GoldIntelligencePanel).fetchData(), |
| REFRESH_INTERVALS.goldIntelligence, |
| () => this.isPanelNearViewport('gold-intelligence') |
| ); |
| this.refreshScheduler.scheduleRefresh( |
| 'aaii-sentiment', |
| () => this.dataLoader.loadAaiiSentiment(), |
| REFRESH_INTERVALS.aaiiSentiment, |
| () => this.isPanelNearViewport('aaii-sentiment') |
| ); |
| this.refreshScheduler.scheduleRefresh( |
| 'market-breadth', |
| () => this.dataLoader.loadMarketBreadth(), |
| REFRESH_INTERVALS.marketBreadth, |
| () => this.isPanelNearViewport('market-breadth') |
| ); |
|
|
| |
| if (SITE_VARIANT === 'full') { |
| this.refreshScheduler.scheduleRefresh('intelligence', () => { |
| const { military, iranEvents } = this.state.intelligenceCache; |
| this.state.intelligenceCache = {}; |
| if (military) this.state.intelligenceCache.military = military; |
| if (iranEvents) this.state.intelligenceCache.iranEvents = iranEvents; |
| return this.dataLoader.loadIntelligenceSignals(); |
| }, REFRESH_INTERVALS.intelligence, () => this.shouldRefreshIntelligence()); |
| } |
|
|
| |
| this.refreshScheduler.scheduleRefresh( |
| 'correlation-engine', |
| async () => { |
| const engine = this.state.correlationEngine; |
| if (!engine) return; |
| await engine.run(this.state); |
| for (const domain of ['military', 'escalation', 'economic', 'disaster'] as const) { |
| const panel = this.state.panels[`${domain}-correlation`] as CorrelationPanel | undefined; |
| panel?.updateCards(engine.getCards(domain)); |
| } |
| }, |
| REFRESH_INTERVALS.correlationEngine, |
| () => this.shouldRefreshCorrelation(), |
| ); |
| } |
| } |
|
|