| import type { AppContext, AppModule, CountryBriefSignals } from '@/app/app-context'; |
| import { getSignalAggregator } from '@/app/lazy-services'; |
| import type { CountrySignalCluster } from '@/services/signal-aggregator'; |
| import { getRpcBaseUrl } from '@/services/rpc-client'; |
| import { premiumFetch } from '@/services/premium-fetch'; |
| import { IS_EMBEDDED_PREVIEW } from '@/utils/embedded-preview'; |
| import type { TimelineEvent } from '@/components/CountryTimeline'; |
| import { CountryTimeline } from '@/components/CountryTimeline'; |
| import type { |
| CountryDeepDiveEconomicIndicator, |
| CountryDeepDiveMilitarySummary, |
| CountryDeepDiveSignalDetails, |
| ChinaCountrySummaryData, |
| } from '@/components/CountryBriefPanel'; |
| import { reverseGeocode } from '@/utils/reverse-geocode'; |
| import { yieldToMain } from '@/utils/after-paint'; |
| import { effectivePubDateMs } from '@/services/feed-date'; |
| import { |
| getCountryAtCoordinates, |
| getCountryCentroid, |
| hasCountryGeometry, |
| isCoordinateInCountry, |
| ME_STRIKE_BOUNDS, |
| iso3ToIso2Code, |
| nameToCountryCode, |
| } from '@/services/country-geometry'; |
| import { getCountryData, TIER1_COUNTRIES, type CountryScore } from '@/services/country-instability'; |
| import { getCachedCountryScore, normalizeCiiCountryCode } from '@/services/cached-risk-scores'; |
| import { dataFreshness } from '@/services/data-freshness'; |
| import { fetchCountryMarkets } from '@/services/prediction'; |
| import { collectStoryData } from '@/services/story-data'; |
| |
| |
| |
| |
| |
|
|
| import { hasPremiumAccess } from '@/services/panel-gating'; |
| import { getAuthState, subscribeAuthState } from '@/services/auth-state'; |
| import { showMapContextMenu } from '@/components/MapContextMenu'; |
| import { BETA_MODE } from '@/config/beta'; |
| import { mlWorker } from '@/services/ml-worker'; |
| import { isHeadlineMemoryEnabled } from '@/services/ai-flow-settings'; |
| import { t, getCurrentLanguage } from '@/services/i18n'; |
| import { trackCountrySelected, trackCountryBriefOpened } from '@/services/analytics'; |
| import { toApiUrl } from '@/services/runtime'; |
| import type { StrategicPosturePanel } from '@/components/StrategicPosturePanel'; |
| import type { NewsItem } from '@/types'; |
| import { |
| buildBriefSourceContextLines, |
| collectBriefSources, |
| type BriefSource, |
| } from '@/utils/brief-sources'; |
| import { getNearbyInfrastructure, preloadInfrastructureTables } from '@/services/related-assets'; |
| import { getCachedMilitaryBases, preloadMilitaryBases } from '@/services/military-base-config'; |
| import { toFlagEmoji } from '@/utils/country-flag'; |
| import { iso2ToIso3, iso2ToComtradeReporterCode } from '@/utils/country-codes'; |
| import { buildDependencyGraph } from '@/services/infrastructure-cascade'; |
| import { getActiveFrameworkForPanel, subscribeFrameworkChange } from '@/services/analysis-framework-store'; |
| import { fetchMultiSectorExposure, fetchCountryProducts, fetchMultiSectorCostShock } from '@/services/supply-chain'; |
| import { getImfCountryBundle, buildImfEconomicIndicators, type ImfCountryBundle } from '@/services/imf-country-data'; |
| import { getChinaDecisionSignalsData } from '@/services/china-decision-signals'; |
| import { EconomicServiceClient, IntelligenceServiceClient, MarketServiceClient, TradeServiceClient } from '@/services/generated-rpc-clients'; |
| import { CHINA_DECISION_SIGNAL_GROUP_IDS } from '../../shared/china-decision-signals'; |
|
|
| |
| |
| |
| const IRAN_ATTACKS_ENABLED = typeof window !== 'undefined' && import.meta.env.VITE_ENABLE_IRAN_ATTACKS === 'true'; |
|
|
| type IntlDisplayNamesCtor = new ( |
| locales: string | string[], |
| options: { type: 'region' } |
| ) => { of: (code: string) => string | undefined }; |
|
|
| type CountryStockSnapshot = { |
| available: boolean; |
| code: string; |
| symbol: string; |
| indexName: string; |
| price: string; |
| weekChangePercent: string; |
| currency: string; |
| fetchedAt: string; |
| }; |
|
|
| type CountryIntelBriefResult = { |
| brief: string; |
| sources: BriefSource[]; |
| generatedAt?: string | number; |
| cached?: boolean; |
| }; |
|
|
| export class CountryIntelManager implements AppModule { |
| private ctx: AppContext; |
| private briefRequestToken = 0; |
| private frameworkUnsubscribe: (() => void) | null = null; |
| private _fwDebounce: ReturnType<typeof setTimeout> | null = null; |
| |
| |
| |
| |
| |
| |
| private authUnsubscribe: (() => void) | null = null; |
| private lastHadPremium = false; |
| private countryBriefPageLoading: Promise<boolean> | null = null; |
|
|
| constructor(ctx: AppContext) { |
| this.ctx = ctx; |
| } |
|
|
| async init(): Promise<void> { |
| await this.setupCountryIntel(); |
| this.frameworkUnsubscribe = subscribeFrameworkChange('country-brief', () => { |
| const page = this.ctx.countryBriefPage; |
| if (!page?.isVisible()) return; |
| const code = page.getCode(); |
| const name = page.getName() ?? code; |
| if (!code || !name) return; |
| if (this._fwDebounce) clearTimeout(this._fwDebounce); |
| this._fwDebounce = setTimeout(() => { |
| void this.openCountryBriefByCode(code, name).catch((err) => this.handleCountryBriefOpenError(err)); |
| }, 400); |
| }); |
|
|
| this.lastHadPremium = hasPremiumAccess(getAuthState()); |
| this.authUnsubscribe = subscribeAuthState(() => { |
| const nowPremium = hasPremiumAccess(getAuthState()); |
| if (nowPremium && !this.lastHadPremium) { |
| |
| |
| |
| |
| const openCode = this.ctx.countryBriefPage?.getCode(); |
| if (openCode) this.fetchProSections(openCode); |
| } |
| this.lastHadPremium = nowPremium; |
| }); |
| } |
|
|
| destroy(): void { |
| if (this._fwDebounce) { clearTimeout(this._fwDebounce); this._fwDebounce = null; } |
| this.ctx.countryTimeline?.destroy(); |
| this.ctx.countryTimeline = null; |
| this.ctx.countryBriefPage = null; |
| this.countryBriefPageLoading = null; |
| this.frameworkUnsubscribe?.(); |
| this.frameworkUnsubscribe = null; |
| this.authUnsubscribe?.(); |
| this.authUnsubscribe = null; |
| } |
|
|
| private handleCountryBriefOpenError(err: unknown): void { |
| console.error('[CountryBrief] Failed to open country brief:', err); |
| this.ctx.map?.setRenderPaused(false); |
| this.showToast('Country brief failed to open. Please try again.'); |
| } |
|
|
| private async setupCountryIntel(): Promise<void> { |
| if (!this.ctx.map) return; |
| this.ctx.map.onCountryClicked((countryClick) => { |
| if (countryClick.code && countryClick.name) { |
| trackCountrySelected(countryClick.code, countryClick.name, 'map'); |
| void this.openCountryBriefByCode(countryClick.code, countryClick.name) |
| .catch((err) => this.handleCountryBriefOpenError(err)); |
| } else { |
| void this.openCountryBrief(countryClick.lat, countryClick.lon) |
| .catch((err) => this.handleCountryBriefOpenError(err)); |
| } |
| }); |
|
|
| this.ctx.map.onMapContextMenu((payload) => { |
| const items = []; |
| if (payload.countryCode && payload.countryName) { |
| items.push({ |
| label: t('contextMenu.openCountryBrief'), |
| action: () => { |
| void this.openCountryBriefByCode(payload.countryCode!, payload.countryName!) |
| .catch((err) => this.handleCountryBriefOpenError(err)); |
| }, |
| }); |
| } else { |
| items.push({ |
| label: t('contextMenu.openCountryBrief'), |
| action: () => { |
| void this.openCountryBrief(payload.lat, payload.lon) |
| .catch((err) => this.handleCountryBriefOpenError(err)); |
| }, |
| }); |
| } |
| items.push({ label: t('contextMenu.copyCoordinates'), action: () => navigator.clipboard.writeText(`${payload.lat.toFixed(5)}, ${payload.lon.toFixed(5)}`).catch(() => {}) }); |
| showMapContextMenu(payload.screenX, payload.screenY, items); |
| }); |
| } |
|
|
| private async ensureCountryBriefPage(): Promise<boolean> { |
| if (this.ctx.countryBriefPage) return true; |
| if (!this.ctx.map || this.ctx.isDestroyed) return false; |
| if (this.countryBriefPageLoading) return this.countryBriefPageLoading; |
| const loading = this.createCountryBriefPage(); |
| this.countryBriefPageLoading = loading; |
| try { |
| return await loading; |
| } finally { |
| if (this.countryBriefPageLoading === loading) this.countryBriefPageLoading = null; |
| } |
| } |
|
|
| private async createCountryBriefPage(): Promise<boolean> { |
| const { CountryDeepDivePanel } = await import('@/components/CountryDeepDivePanel'); |
| if (this.ctx.isDestroyed || !this.ctx.map) return false; |
| this.ctx.countryBriefPage = new CountryDeepDivePanel(this.ctx.map); |
| this.ctx.countryBriefPage.setShareStoryHandler((code, name) => { |
| this.ctx.countryBriefPage?.hide(); |
| void this.openCountryStory(code, name).catch((err) => { |
| console.error('[CountryStory] Failed to open story:', err); |
| this.showToast('Country story failed to open. Please try again.'); |
| }); |
| }); |
| this.ctx.countryBriefPage.setExportImageHandler(async (code, name) => { |
| try { |
| const aggregator = await getSignalAggregator(); |
| const signals = await this.getCountrySignals(code, name); |
| const cluster = aggregator.getCountryClusters().find(c => c.country === code); |
| const regional = aggregator.getRegionalConvergence().filter(r => r.countries.includes(code)); |
| const convergence = cluster ? { |
| score: cluster.convergenceScore, |
| signalTypes: [...cluster.signalTypes], |
| regionalDescriptions: regional.map(r => r.description), |
| } : null; |
| const posturePanel = this.ctx.panels['strategic-posture'] as StrategicPosturePanel | undefined; |
| const postures = posturePanel?.getPostures() || []; |
| const data = collectStoryData(code, name, this.ctx.latestClusters, postures, this.ctx.latestPredictions, signals, convergence); |
| const { renderStoryToCanvas } = await import('@/services/story-renderer'); |
| const canvas = await renderStoryToCanvas(data); |
| const dataUrl = canvas.toDataURL('image/png'); |
| const a = document.createElement('a'); |
| a.href = dataUrl; |
| a.download = `country-brief-${code.toLowerCase()}-${Date.now()}.png`; |
| a.click(); |
| } catch (err) { |
| console.error('[CountryBrief] Image export failed:', err); |
| } |
| }); |
|
|
| this.ctx.countryBriefPage.onClose(() => { |
| this.briefRequestToken++; |
| this.ctx.map?.clearCountryHighlight(); |
| this.ctx.map?.setRenderPaused(false); |
| this.ctx.countryTimeline?.destroy(); |
| this.ctx.countryTimeline = null; |
| }); |
| return true; |
| } |
|
|
| async openCountryBrief(lat: number, lon: number): Promise<void> { |
| if (!(await this.ensureCountryBriefPage())) return; |
| const page = this.ctx.countryBriefPage; |
| if (!page) return; |
| const token = ++this.briefRequestToken; |
| page.showLoading(); |
| this.ctx.map?.setRenderPaused(true); |
|
|
| const localGeo = getCountryAtCoordinates(lat, lon); |
| if (localGeo) { |
| if (token !== this.briefRequestToken) return; |
| await this.openCountryBriefByCode(localGeo.code, localGeo.name); |
| return; |
| } |
|
|
| const geo = await reverseGeocode(lat, lon); |
| if (token !== this.briefRequestToken) return; |
| if (!geo) { |
| page.hide(); |
| this.ctx.map?.setRenderPaused(false); |
| return; |
| } |
|
|
| await this.openCountryBriefByCode(geo.code, geo.country); |
| } |
|
|
| async openCountryBriefByCode(code: string, country: string, opts?: { maximize?: boolean }): Promise<void> { |
| const token = ++this.briefRequestToken; |
| let pageShown = false; |
| let showedLoading = false; |
|
|
| try { |
| if (!(await this.ensureCountryBriefPage())) return; |
| if (token !== this.briefRequestToken || this.ctx.isDestroyed) return; |
| const page = this.ctx.countryBriefPage; |
| if (!page) return; |
| if (!this.hasVisibleRealCountryBrief() || page.getCode() !== code) { |
| page.showLoading(); |
| showedLoading = true; |
| } |
| this.ctx.map?.setRenderPaused(true); |
| trackCountryBriefOpened(code); |
|
|
| const canonicalName = TIER1_COUNTRIES[code] || CountryIntelManager.resolveCountryName(code); |
| if (canonicalName !== code) country = canonicalName; |
| const isChina = code.toUpperCase() === 'CN'; |
|
|
| const scoreCode = normalizeCiiCountryCode(code); |
| const score = getCachedCountryScore(scoreCode); |
|
|
| const signals = await this.getCountrySignals(code, country); |
| if (token !== this.briefRequestToken || this.ctx.isDestroyed || this.ctx.countryBriefPage !== page) return; |
|
|
| page.show(country, code, score, signals); |
| pageShown = true; |
| const updateChinaSummary = (data: ChinaCountrySummaryData): void => { |
| if (!isChina || token !== this.briefRequestToken || this.ctx.countryBriefPage?.getCode()?.toUpperCase() !== 'CN') return; |
| this.ctx.countryBriefPage.updateChinaCountrySummary?.(data); |
| }; |
| if (isChina) { |
| getChinaDecisionSignalsData().then((snapshot) => { |
| if ( |
| token !== this.briefRequestToken |
| || this.ctx.countryBriefPage?.getCode()?.toUpperCase() !== 'CN' |
| ) return; |
| updateChinaSummary({ |
| groups: snapshot.groups.map((group) => ({ |
| id: group.id, |
| state: group.state, |
| signals: group.items.map((item) => { |
| const translation = item.metadata.translation as { state?: unknown } | null; |
| const supersession = item.metadata.supersession as { state?: unknown } | null; |
| return { |
| label: item.label, |
| value: item.summary, |
| source: `${item.sourceName} · ${item.publisherType.replace(/_/g, ' ')}`, |
| sourceUrl: item.sourceUrl ?? undefined, |
| observedAt: item.observedAt ?? undefined, |
| publishedAt: item.publishedAt ?? undefined, |
| effectiveAt: item.effectiveAt ?? undefined, |
| status: typeof supersession?.state === 'string' ? supersession.state : undefined, |
| translationState: typeof translation?.state === 'string' ? translation.state.replace(/_/g, ' ') : undefined, |
| publisherType: item.publisherType, |
| lineageId: item.lineageId, |
| provenance: item.provenance, |
| stale: item.stale, |
| }; |
| }), |
| unavailableReason: group.reason ?? undefined, |
| })), |
| }); |
| }).catch(() => { |
| updateChinaSummary({ |
| groups: CHINA_DECISION_SIGNAL_GROUP_IDS.map((id) => ({ |
| id, |
| state: 'unavailable', |
| signals: [], |
| unavailableReason: t('countryBrief.china.decisionSignalsUnavailable'), |
| })), |
| }); |
| }); |
|
|
| } |
| |
| |
| |
| |
| |
| await yieldToMain(); |
| if (token !== this.briefRequestToken || this.ctx.isDestroyed || this.ctx.countryBriefPage !== page) return; |
| this.ctx.map?.highlightCountry(code); |
| this.ctx.map?.fitCountry(code); |
|
|
| if (opts?.maximize) { |
| requestAnimationFrame(() => { |
| const panel = this.ctx.countryBriefPage; |
| if (panel?.isVisible() && panel.getCode() === code) { |
| panel.maximize?.(); |
| } |
| }); |
| } |
| try { |
| const signalDetails = await this.buildSignalDetails(code); |
| if (token === this.briefRequestToken && this.ctx.countryBriefPage?.getCode() === code) { |
| page.updateSignalDetails?.(signalDetails); |
| } |
| } catch (err) { |
| console.warn('[CountryBrief] signal details unavailable:', err); |
| } |
| if (token !== this.briefRequestToken || this.ctx.countryBriefPage?.getCode() !== code) return; |
| page.updateMilitaryActivity?.(this.buildMilitarySummary(code, country)); |
| page.updateEconomicIndicators?.(this.buildEconomicIndicators(code, score, null)); |
|
|
| const marketClient = new MarketServiceClient(getRpcBaseUrl(), { fetch: (...args: Parameters<typeof globalThis.fetch>) => globalThis.fetch(...args) }); |
| const stockPromise = marketClient.getCountryStockIndex({ countryCode: code }) |
| .then((resp) => ({ |
| available: resp.available, |
| code: resp.code, |
| symbol: resp.symbol, |
| indexName: resp.indexName, |
| price: String(resp.price), |
| weekChangePercent: String(resp.weekChangePercent), |
| currency: resp.currency, |
| fetchedAt: resp.fetchedAt, |
| })) |
| .catch(() => ({ available: false as const, code: '', symbol: '', indexName: '', price: '0', weekChangePercent: '0', currency: '', fetchedAt: '' })); |
|
|
| let latestStock: CountryStockSnapshot | null = null; |
| let latestImf: ImfCountryBundle | null = null; |
| const imfPromise = getImfCountryBundle(code); |
|
|
| stockPromise.then((stock) => { |
| latestStock = stock; |
| if (this.ctx.countryBriefPage?.getCode() !== code) return; |
| this.ctx.countryBriefPage.updateStock(stock); |
| this.ctx.countryBriefPage.updateEconomicIndicators?.(this.buildEconomicIndicators(code, score, stock, latestImf)); |
| }); |
|
|
| |
| |
| imfPromise.then((bundle) => { |
| latestImf = bundle; |
| if (this.ctx.countryBriefPage?.getCode() !== code) return; |
| this.ctx.countryBriefPage.updateEconomicIndicators?.(this.buildEconomicIndicators(code, score, latestStock, bundle)); |
| }).catch(() => { }); |
|
|
| fetchCountryMarkets(country) |
| .then((markets) => { |
| if (this.ctx.countryBriefPage?.getCode() === code) this.ctx.countryBriefPage.updateMarkets(markets); |
| }) |
| .catch(() => { |
| if (this.ctx.countryBriefPage?.getCode() === code) this.ctx.countryBriefPage.updateMarkets([]); |
| }); |
|
|
| const searchTerms = CountryIntelManager.getCountrySearchTerms(country, code); |
| const otherCountryTerms = CountryIntelManager.getOtherCountryTerms(code); |
| const matchingNews = this.ctx.allNews.filter((n) => { |
| const t = n.title.toLowerCase(); |
| return CountryIntelManager.firstMentionPosition(t, searchTerms) !== Infinity; |
| }); |
| const filteredNews = matchingNews.filter((n) => { |
| const t = n.title.toLowerCase(); |
| const ourPos = CountryIntelManager.firstMentionPosition(t, searchTerms); |
| const otherPos = CountryIntelManager.firstMentionPosition(t, otherCountryTerms); |
| return ourPos !== Infinity && (otherPos === Infinity || ourPos <= otherPos); |
| }).sort((a, b) => { |
| const severityDelta = this.newsSeverityRank(b) - this.newsSeverityRank(a); |
| if (severityDelta !== 0) return severityDelta; |
| return effectivePubDateMs(b) - effectivePubDateMs(a); |
| }); |
| page.updateNews(filteredNews.slice(0, 10)); |
|
|
| page.updateInfrastructure(code); |
| void Promise.all([ |
| preloadMilitaryBases().catch(() => []), |
| preloadInfrastructureTables().catch(() => {}), |
| ]) |
| .then(() => { |
| if (this.ctx.countryBriefPage?.getCode() === code) { |
| this.ctx.countryBriefPage.updateInfrastructure(code); |
| this.ctx.countryBriefPage.updateMilitaryActivity?.(this.buildMilitarySummary(code, country)); |
| } |
| }) |
| .catch(() => {}); |
|
|
| const intelClient = new IntelligenceServiceClient(getRpcBaseUrl(), { |
| fetch: (...args: Parameters<typeof globalThis.fetch>) => globalThis.fetch(...args), |
| }); |
| intelClient.getCountryFacts({ countryCode: code }) |
| .then((facts) => { |
| if (this.ctx.countryBriefPage?.getCode() !== code) return; |
| this.ctx.countryBriefPage.updateCountryFacts?.({ |
| headOfState: facts.headOfState, |
| headOfStateTitle: facts.headOfStateTitle, |
| wikipediaSummary: facts.wikipediaSummary, |
| wikipediaThumbnailUrl: facts.wikipediaThumbnailUrl, |
| population: Number(facts.population), |
| capital: facts.capital, |
| languages: facts.languages, |
| currencies: facts.currencies, |
| areaSqKm: facts.areaSqKm, |
| countryName: facts.countryName, |
| }); |
| }) |
| .catch(() => { |
| if (this.ctx.countryBriefPage?.getCode() !== code) return; |
| this.ctx.countryBriefPage.updateCountryFacts?.({ |
| headOfState: '', headOfStateTitle: '', wikipediaSummary: '', |
| wikipediaThumbnailUrl: '', population: 0, capital: '', |
| languages: [], currencies: [], areaSqKm: 0, countryName: '', |
| }); |
| }); |
|
|
| intelClient.getCountryEnergyProfile({ countryCode: code }) |
| .then((profile) => { |
| if (this.ctx.countryBriefPage?.getCode() !== code) return; |
| this.ctx.countryBriefPage.updateEnergyProfile?.({ |
| mixAvailable: profile.mixAvailable, |
| mixYear: profile.mixYear, |
| coalShare: profile.coalShare, |
| gasShare: profile.gasShare, |
| oilShare: profile.oilShare, |
| nuclearShare: profile.nuclearShare, |
| renewShare: profile.renewShare, |
| windShare: profile.windShare, |
| solarShare: profile.solarShare, |
| hydroShare: profile.hydroShare, |
| importShare: profile.importShare, |
| gasStorageAvailable: profile.gasStorageAvailable, |
| gasStorageFillPct: profile.gasStorageFillPct, |
| gasStorageChange1d: profile.gasStorageChange1d, |
| gasStorageTrend: profile.gasStorageTrend, |
| gasStorageDate: profile.gasStorageDate, |
| electricityAvailable: profile.electricityAvailable, |
| electricityPriceMwh: profile.electricityPriceMwh, |
| electricitySource: profile.electricitySource, |
| electricityDate: profile.electricityDate, |
| jodiOilAvailable: profile.jodiOilAvailable, |
| jodiOilDataMonth: profile.jodiOilDataMonth, |
| gasolineDemandKbd: profile.gasolineDemandKbd, |
| gasolineImportsKbd: profile.gasolineImportsKbd, |
| dieselDemandKbd: profile.dieselDemandKbd, |
| dieselImportsKbd: profile.dieselImportsKbd, |
| jetDemandKbd: profile.jetDemandKbd, |
| jetImportsKbd: profile.jetImportsKbd, |
| lpgDemandKbd: profile.lpgDemandKbd, |
| lpgImportsKbd: profile.lpgImportsKbd, |
| crudeImportsKbd: profile.crudeImportsKbd, |
| jodiGasAvailable: profile.jodiGasAvailable, |
| jodiGasDataMonth: profile.jodiGasDataMonth, |
| gasTotalDemandTj: profile.gasTotalDemandTj, |
| gasLngImportsTj: profile.gasLngImportsTj, |
| gasPipeImportsTj: profile.gasPipeImportsTj, |
| gasLngShare: profile.gasLngShare, |
| ieaStocksAvailable: profile.ieaStocksAvailable, |
| ieaStocksDataMonth: profile.ieaStocksDataMonth, |
| ieaDaysOfCover: profile.ieaDaysOfCover, |
| ieaNetExporter: profile.ieaNetExporter, |
| ieaBelowObligation: profile.ieaBelowObligation, |
| emberFossilShare: profile.emberFossilShare, |
| emberRenewShare: profile.emberRenewShare, |
| emberNuclearShare: profile.emberNuclearShare, |
| emberCoalShare: profile.emberCoalShare, |
| emberGasShare: profile.emberGasShare, |
| emberDemandTwh: profile.emberDemandTwh, |
| emberDataMonth: profile.emberDataMonth, |
| emberAvailable: profile.emberAvailable, |
| sprRegime: profile.sprRegime, |
| sprCapacityMb: profile.sprCapacityMb, |
| sprOperator: profile.sprOperator, |
| sprIeaMember: profile.sprIeaMember, |
| sprStockholdingModel: profile.sprStockholdingModel, |
| sprNote: profile.sprNote, |
| sprSource: profile.sprSource, |
| sprAsOf: profile.sprAsOf, |
| sprAvailable: profile.sprAvailable, |
| }); |
| }) |
| .catch(() => { |
| if (this.ctx.countryBriefPage?.getCode() !== code) return; |
| this.ctx.countryBriefPage.updateEnergyProfile?.({ |
| mixAvailable: false, mixYear: 0, coalShare: 0, gasShare: 0, oilShare: 0, |
| nuclearShare: 0, renewShare: 0, windShare: 0, solarShare: 0, hydroShare: 0, |
| importShare: 0, gasStorageAvailable: false, gasStorageFillPct: 0, |
| gasStorageChange1d: 0, gasStorageTrend: '', gasStorageDate: '', electricityAvailable: false, |
| electricityPriceMwh: 0, electricitySource: '', electricityDate: '', |
| jodiOilAvailable: false, jodiOilDataMonth: '', gasolineDemandKbd: 0, |
| gasolineImportsKbd: 0, dieselDemandKbd: 0, dieselImportsKbd: 0, |
| jetDemandKbd: 0, jetImportsKbd: 0, lpgDemandKbd: 0, lpgImportsKbd: 0, |
| crudeImportsKbd: 0, jodiGasAvailable: false, jodiGasDataMonth: '', |
| gasTotalDemandTj: 0, gasLngImportsTj: 0, gasPipeImportsTj: 0, |
| gasLngShare: 0, ieaStocksAvailable: false, ieaStocksDataMonth: '', |
| ieaDaysOfCover: 0, ieaNetExporter: false, ieaBelowObligation: false, |
| emberFossilShare: 0, emberRenewShare: 0, emberNuclearShare: 0, |
| emberCoalShare: 0, emberGasShare: 0, emberDemandTwh: 0, |
| emberDataMonth: '', emberAvailable: false, |
| sprRegime: 'unknown', sprCapacityMb: 0, sprOperator: '', sprIeaMember: false, |
| sprStockholdingModel: '', sprNote: '', sprSource: '', sprAsOf: '', |
| sprAvailable: false, |
| }); |
| }); |
|
|
| intelClient.getCountryPortActivity({ countryCode: code }) |
| .then((activity) => { |
| if (this.ctx.countryBriefPage?.getCode() !== code) return; |
| this.ctx.countryBriefPage.updateMaritimeActivity?.({ |
| available: activity.available, |
| ports: (activity.ports ?? []).map((p) => ({ |
| portId: p.portId, |
| portName: p.portName, |
| lat: p.lat, |
| lon: p.lon, |
| tankerCalls30d: p.tankerCalls30d, |
| trendDeltaPct: p.trendDeltaPct, |
| importTankerDwt: p.importTankerDwt, |
| exportTankerDwt: p.exportTankerDwt, |
| anomalySignal: p.anomalySignal, |
| })), |
| fetchedAt: activity.fetchedAt, |
| }); |
| }) |
| .catch(() => { |
| if (this.ctx.countryBriefPage?.getCode() !== code) return; |
| this.ctx.countryBriefPage.updateMaritimeActivity?.({ available: false, ports: [], fetchedAt: '' }); |
| }); |
|
|
| |
| const sectorExposurePromise = fetchMultiSectorExposure(code); |
| sectorExposurePromise |
| .then((sectors) => { |
| if (this.ctx.countryBriefPage?.getCode() !== code) return; |
| if (sectors.length === 0) { |
| this.ctx.countryBriefPage.updateTradeExposure?.(null); |
| if (hasPremiumAccess(getAuthState())) this.ctx.countryBriefPage.updateMultiSectorCostShock?.(null); |
| return; |
| } |
| |
| const top = sectors[0]!; |
| const syntheticResponse = { |
| iso2: code, |
| hs2: top.hs2, |
| exposures: sectors.slice(0, 3).map(s => ({ |
| chokepointId: s.primaryChokepointId, |
| chokepointName: s.primaryChokepointName, |
| exposureScore: s.exposureScore, |
| coastSide: '', |
| shockSupported: s.hs2 === '27', |
| })), |
| primaryChokepointId: top.primaryChokepointId, |
| vulnerabilityIndex: top.vulnerabilityIndex, |
| fetchedAt: new Date().toISOString(), |
| }; |
| this.ctx.countryBriefPage.updateTradeExposure?.(syntheticResponse, sectors); |
|
|
| |
| if (hasPremiumAccess(getAuthState()) && top.primaryChokepointId) { |
| fetchMultiSectorCostShock(code, top.primaryChokepointId, 30).then(multi => { |
| if (this.ctx.countryBriefPage?.getCode() !== code) return; |
| this.ctx.countryBriefPage.updateMultiSectorCostShock?.(multi); |
| }).catch(() => { |
| if (this.ctx.countryBriefPage?.getCode() === code) this.ctx.countryBriefPage.updateMultiSectorCostShock?.(null); |
| }); |
| } else if (hasPremiumAccess(getAuthState())) { |
| this.ctx.countryBriefPage.updateMultiSectorCostShock?.(null); |
| } |
| }) |
| .catch(() => { |
| if (this.ctx.countryBriefPage?.getCode() !== code) return; |
| this.ctx.countryBriefPage.updateTradeExposure?.(null); |
| if (hasPremiumAccess(getAuthState())) this.ctx.countryBriefPage.updateMultiSectorCostShock?.(null); |
| }); |
|
|
| if (hasPremiumAccess(getAuthState())) { |
| this.fetchProSections(code); |
| } |
|
|
| this.mountCountryTimeline(code, country); |
|
|
| try { |
| const context: Record<string, unknown> = {}; |
| if (score) { |
| context.score = score.score; |
| context.level = score.level; |
| context.trend = score.trend; |
| context.components = score.components; |
| context.change24h = score.change24h; |
| } |
| Object.assign(context, signals); |
|
|
| const aggregator = await getSignalAggregator(); |
| if (token !== this.briefRequestToken || this.ctx.countryBriefPage?.getCode() !== code) return; |
| const countryCluster = aggregator.getCountryClusters().find((c) => c.country === code); |
| if (countryCluster) { |
| context.convergenceScore = countryCluster.convergenceScore; |
| context.signalTypes = [...countryCluster.signalTypes]; |
| } |
|
|
| const convergences = aggregator.getRegionalConvergence() |
| .filter((r) => r.countries.includes(code)); |
| if (convergences.length) { |
| context.regionalConvergence = convergences.map((r) => r.description); |
| } |
|
|
| if (this.ctx.intelligenceCache.advisories) { |
| const countryAdvisories = this.ctx.intelligenceCache.advisories.filter(a => a.country === code); |
| if (countryAdvisories.length > 0) { |
| context.travelAdvisories = countryAdvisories.map(a => ({ source: a.source, level: a.level, title: a.title })); |
| } |
| } |
|
|
| const groundingNews = filteredNews.slice(0, 15); |
| let briefSources = collectBriefSources(groundingNews, 6); |
| const headlines = groundingNews.map((n) => n.title); |
| if (headlines.length) context.headlines = headlines; |
| if (briefSources.length) context.briefSources = briefSources; |
| const briefHeadlines = (context.headlines as string[] | undefined) || []; |
|
|
| const stockData = await stockPromise; |
| if (token !== this.briefRequestToken || this.ctx.countryBriefPage?.getCode() !== code) return; |
| if (stockData.available) { |
| const pct = parseFloat(stockData.weekChangePercent); |
| context.stockIndex = `${stockData.indexName}: ${stockData.price} (${pct >= 0 ? '+' : ''}${stockData.weekChangePercent}% week)`; |
| } |
|
|
| let briefText = ''; |
| let briefResult: CountryIntelBriefResult | null = null; |
| try { |
| let contextSnapshot = this.buildBriefContextSnapshot(country, code, score, signals, context); |
|
|
| if (isHeadlineMemoryEnabled() && mlWorker.isAvailable && mlWorker.isModelLoaded('embeddings') && briefHeadlines.length > 0) { |
| try { |
| const results = await mlWorker.vectorStoreSearch(briefHeadlines.slice(0, 3), 5, 0.3); |
| if (token !== this.briefRequestToken || this.ctx.countryBriefPage?.getCode() !== code) return; |
| if (results.length > 0) { |
| const historical = results.map(r => |
| `- ${r.text} (${new Date(r.pubDate).toISOString().slice(0, 10)})` |
| ).join('\n').slice(0, 350); |
| contextSnapshot = contextSnapshot.slice(0, 1800) |
| + `\n[BEGIN HISTORICAL DATA]\n${historical}\n[END HISTORICAL DATA]`; |
| } |
| } catch { } |
| } |
|
|
| const countryFw = getActiveFrameworkForPanel('country-brief'); |
| briefResult = await this.fetchCountryIntelBrief(code, contextSnapshot, countryFw?.systemPromptAppend ?? ''); |
| if (token !== this.briefRequestToken || this.ctx.countryBriefPage?.getCode() !== code) return; |
| briefText = briefResult.brief; |
| if (briefResult.sources.length > 0) { |
| briefSources = briefResult.sources; |
| } |
| } catch { } |
|
|
| if (briefText) { |
| this.ctx.countryBriefPage?.updateBrief({ |
| brief: briefText, |
| country, |
| code, |
| sources: briefSources, |
| generatedAt: briefResult?.generatedAt, |
| cached: briefResult?.cached, |
| }); |
| } else { |
| let fallbackBrief = ''; |
| const sumModelId = BETA_MODE ? 'summarization-beta' : 'summarization'; |
| if (briefHeadlines.length >= 2 && mlWorker.isAvailable && mlWorker.isModelLoaded(sumModelId)) { |
| try { |
| const lang = getCurrentLanguage(); |
| const prompt = lang === 'fr' |
| ? `Résumez la situation actuelle en ${country} à partir de ces titres : ${briefHeadlines.slice(0, 8).join('. ')}` |
| : `Summarize the current situation in ${country} based on these headlines: ${briefHeadlines.slice(0, 8).join('. ')}`; |
|
|
| const [summary] = await mlWorker.summarize([prompt], BETA_MODE ? 'summarization-beta' : undefined); |
| if (token !== this.briefRequestToken || this.ctx.countryBriefPage?.getCode() !== code) return; |
| if (summary && summary.length > 20) fallbackBrief = summary; |
| } catch { } |
| } |
|
|
| if (fallbackBrief) { |
| this.ctx.countryBriefPage?.updateBrief({ brief: fallbackBrief, country, code, fallback: true, sources: briefSources }); |
| } else { |
| const lines: string[] = []; |
| if (score) lines.push(t('countryBrief.fallback.instabilityIndex', { score: String(score.score), level: t(`countryBrief.levels.${score.level}`), trend: t(`countryBrief.trends.${score.trend}`) })); |
| if (signals.protests > 0) lines.push(t('countryBrief.fallback.protestsDetected', { count: String(signals.protests) })); |
| if (signals.militaryFlights > 0) lines.push(t('countryBrief.fallback.aircraftTracked', { count: String(signals.militaryFlights) })); |
| if (signals.militaryVessels > 0) lines.push(t('countryBrief.fallback.vesselsTracked', { count: String(signals.militaryVessels) })); |
| if (signals.activeStrikes > 0) lines.push(t('countryBrief.fallback.activeStrikes', { count: String(signals.activeStrikes) })); |
| if (signals.travelAdvisoryMaxLevel === 'do-not-travel') lines.push(`⚠️ Travel advisory: Do Not Travel (${signals.travelAdvisories} source${signals.travelAdvisories > 1 ? 's' : ''})`); |
| else if (signals.travelAdvisoryMaxLevel === 'reconsider') lines.push(`⚠️ Travel advisory: Reconsider Travel (${signals.travelAdvisories} source${signals.travelAdvisories > 1 ? 's' : ''})`); |
| if (signals.outages > 0) lines.push(t('countryBrief.fallback.internetOutages', { count: String(signals.outages) })); |
| if (signals.criticalNews > 0) lines.push(`🚨 Critical headlines in scope: ${signals.criticalNews}`); |
| if (signals.cyberThreats > 0) lines.push(`🛡️ Cyber threat indicators: ${signals.cyberThreats}`); |
| if (signals.aisDisruptions > 0) lines.push(`🚢 Maritime AIS disruptions: ${signals.aisDisruptions}`); |
| if (signals.satelliteFires > 0) lines.push(`🔥 Satellite fire detections: ${signals.satelliteFires}`); |
| if (signals.radiationAnomalies > 0) lines.push(`☢️ Radiation anomalies: ${signals.radiationAnomalies}`); |
| if (signals.temporalAnomalies > 0) lines.push(`⏱️ Temporal anomaly alerts: ${signals.temporalAnomalies}`); |
| if (signals.thermalEscalations > 0) lines.push(`🌡️ Thermal escalation clusters: ${signals.thermalEscalations}`); |
| if (signals.earthquakes > 0) lines.push(t('countryBrief.fallback.recentEarthquakes', { count: String(signals.earthquakes) })); |
| if (signals.orefHistory24h > 0) lines.push(`🚨 Sirens in past 24h: ${signals.orefHistory24h}`); |
| if (context.stockIndex) lines.push(t('countryBrief.fallback.stockIndex', { value: context.stockIndex })); |
| if (lines.length > 0) { |
| this.ctx.countryBriefPage?.updateBrief({ brief: lines.join('\n'), country, code, fallback: true }); |
| } else { |
| this.ctx.countryBriefPage?.updateBrief({ brief: '', country, code, error: 'No AI service available. Configure GROQ_API_KEY in Settings for full briefs.' }); |
| } |
| } |
| } |
| } catch (err) { |
| console.error('[CountryBrief] fetch error:', err); |
| if (token !== this.briefRequestToken || this.ctx.countryBriefPage?.getCode() !== code) return; |
| this.ctx.countryBriefPage?.updateBrief({ brief: '', country, code, error: 'Failed to generate brief' }); |
| } |
| } catch (err) { |
| if (token !== this.briefRequestToken) { |
| console.warn('[CountryBrief] Superseded country brief open failed after it was stale:', err); |
| return; |
| } |
| console.error('[CountryBrief] Failed to open country brief:', err); |
| if (!pageShown) { |
| const activePage = this.ctx.countryBriefPage; |
| const activeCode = activePage?.getCode(); |
| if (showedLoading && activePage?.isVisible() && (activeCode === '__loading__' || activeCode === '__error__')) activePage.hide(); |
| if (!this.hasVisibleRealCountryBrief()) this.ctx.map?.setRenderPaused(false); |
| this.showToast('Country brief failed to open. Please try again.'); |
| } |
| } finally { |
| if (!pageShown && token === this.briefRequestToken && !this.hasVisibleRealCountryBrief()) { |
| this.ctx.map?.setRenderPaused(false); |
| } |
| } |
| } |
|
|
| private hasVisibleRealCountryBrief(): boolean { |
| const page = this.ctx.countryBriefPage; |
| if (!page?.isVisible()) return false; |
| const activeCode = page.getCode(); |
| return !!activeCode && activeCode !== '__loading__' && activeCode !== '__error__'; |
| } |
|
|
| private fetchProSections(code: string): void { |
| |
| |
| |
| if (IS_EMBEDDED_PREVIEW) return; |
|
|
| const rpcBase = getRpcBaseUrl(); |
| |
| |
| |
| const economicClient = new EconomicServiceClient(rpcBase, { fetch: premiumFetch }); |
| const intelClientPro = new IntelligenceServiceClient(rpcBase, { fetch: premiumFetch }); |
| const tradeClient = new TradeServiceClient(rpcBase, { fetch: premiumFetch }); |
| const iso3 = iso2ToIso3(code); |
|
|
| economicClient.getNationalDebt({}).then(resp => { |
| if (this.ctx.countryBriefPage?.getCode() !== code) return; |
| const entry = iso3 ? resp.entries?.find(e => e.iso3 === iso3) : null; |
| this.ctx.countryBriefPage.updateNationalDebt?.(entry ? { |
| debtToGdp: entry.debtToGdp, |
| debtUsd: entry.debtUsd, |
| annualGrowth: entry.annualGrowth, |
| source: entry.source, |
| } : null); |
| }).catch(() => { |
| if (this.ctx.countryBriefPage?.getCode() === code) this.ctx.countryBriefPage.updateNationalDebt?.(null); |
| }); |
|
|
| intelClientPro.getCountryRisk({ countryCode: code }).then(resp => { |
| if (this.ctx.countryBriefPage?.getCode() !== code) return; |
| this.ctx.countryBriefPage.updateSanctionsPressure?.(resp.sanctionsCount > 0 ? { |
| entryCount: resp.sanctionsCount, |
| sanctionsActive: resp.sanctionsActive, |
| } : null); |
| }).catch(() => { |
| if (this.ctx.countryBriefPage?.getCode() === code) this.ctx.countryBriefPage.updateSanctionsPressure?.(null); |
| }); |
|
|
| const unCode = iso2ToComtradeReporterCode(code); |
| |
| |
| |
| |
| const hasPremium = hasPremiumAccess(getAuthState()); |
| if (unCode && hasPremium) { |
| tradeClient.listComtradeFlows({ reporterCode: unCode, cmdCode: '', anomaliesOnly: false }).then(resp => { |
| if (this.ctx.countryBriefPage?.getCode() !== code) return; |
| const topFlows = (resp.flows || []) |
| .sort((a, b) => b.tradeValueUsd - a.tradeValueUsd) |
| .slice(0, 5) |
| .map(f => ({ partnerName: f.partnerName, cmdDesc: f.cmdDesc, tradeValueUsd: f.tradeValueUsd, yoyChange: f.yoyChange })); |
| this.ctx.countryBriefPage.updateComtradeFlows?.(topFlows.length > 0 ? topFlows : null); |
| }).catch(() => { |
| if (this.ctx.countryBriefPage?.getCode() === code) this.ctx.countryBriefPage.updateComtradeFlows?.(null); |
| }); |
|
|
| tradeClient.getTariffTrends({ reportingCountry: unCode, productSector: '', years: 10, partnerCountry: '' }).then(resp => { |
| if (this.ctx.countryBriefPage?.getCode() !== code) return; |
| const pts = resp.datapoints || []; |
| const latest = pts[pts.length - 1]; |
| this.ctx.countryBriefPage.updateTariffTrends?.(latest ? { |
| currentRate: resp.effectiveTariffRate?.tariffRate ?? latest.tariffRate, |
| trend: pts.length >= 2 && pts[pts.length - 1]!.tariffRate > pts[pts.length - 2]!.tariffRate ? 'rising' : 'falling', |
| datapoints: pts.map(p => ({ year: p.year, tariffRate: p.tariffRate })), |
| } : null); |
| }).catch(() => { |
| if (this.ctx.countryBriefPage?.getCode() === code) this.ctx.countryBriefPage.updateTariffTrends?.(null); |
| }); |
| } else { |
| this.ctx.countryBriefPage?.updateComtradeFlows?.(null); |
| this.ctx.countryBriefPage?.updateTariffTrends?.(null); |
| } |
|
|
| fetchCountryProducts(code).then(resp => { |
| if (this.ctx.countryBriefPage?.getCode() !== code) return; |
| this.ctx.countryBriefPage.updateProductImports?.(resp.products.length > 0 ? resp : null); |
| }).catch(() => { |
| if (this.ctx.countryBriefPage?.getCode() === code) { |
| this.ctx.countryBriefPage.updateProductImports?.(null); |
| } |
| }); |
|
|
| |
| |
| |
| this.fetchHousingCycle(code); |
| } |
|
|
| private fetchHousingCycle(code: string): void { |
| const page = this.ctx.countryBriefPage; |
| if (!page?.updateHousingCycle) return; |
| const keys = 'bisDsr,bisPropertyResidential,bisPropertyCommercial'; |
| fetch(toApiUrl(`/api/bootstrap?keys=${keys}`), { |
| method: 'GET', |
| headers: { Accept: 'application/json' }, |
| signal: page.signal, |
| }).then(async resp => { |
| if (!resp.ok) return null; |
| return resp.json() as Promise<{ data?: { |
| bisDsr?: { entries?: Array<{ countryCode: string; dsrPct: number; change: number | null; period: string }> }; |
| bisPropertyResidential?: { entries?: Array<{ countryCode: string; indexValue: number; qoqChange: number | null; yoyChange: number | null; period: string }> }; |
| bisPropertyCommercial?: { entries?: Array<{ countryCode: string; indexValue: number; qoqChange: number | null; yoyChange: number | null; period: string }> }; |
| } }>; |
| }).then(body => { |
| if (!body || this.ctx.countryBriefPage?.getCode() !== code) return; |
| const pick = <T extends { countryCode: string }>(arr: T[] | undefined, cc: string): T | null => |
| arr?.find(e => e?.countryCode === cc) ?? null; |
| |
| const EURO_AREA = new Set(['DE', 'FR', 'IT', 'ES', 'NL', 'BE', 'AT', 'IE', 'PT', 'GR', 'FI', 'SK', 'SI', 'LV', 'LT', 'EE', 'CY', 'MT', 'LU', 'HR']); |
| const fallbackCC = EURO_AREA.has(code) ? 'XM' : null; |
| const res = pick(body.data?.bisPropertyResidential?.entries, code) ?? (fallbackCC ? pick(body.data?.bisPropertyResidential?.entries, fallbackCC) : null); |
| const com = pick(body.data?.bisPropertyCommercial?.entries, code) ?? (fallbackCC ? pick(body.data?.bisPropertyCommercial?.entries, fallbackCC) : null); |
| const dsr = pick(body.data?.bisDsr?.entries, code) ?? (fallbackCC ? pick(body.data?.bisDsr?.entries, fallbackCC) : null); |
| this.ctx.countryBriefPage?.updateHousingCycle?.({ |
| residential: res ? { indexValue: res.indexValue, qoqChange: res.qoqChange, yoyChange: res.yoyChange, period: res.period } : null, |
| commercial: com ? { indexValue: com.indexValue, qoqChange: com.qoqChange, yoyChange: com.yoyChange, period: com.period } : null, |
| dsr: dsr ? { dsrPct: dsr.dsrPct, change: dsr.change, period: dsr.period } : null, |
| }); |
| }).catch(() => { |
| if (this.ctx.countryBriefPage?.getCode() === code) { |
| this.ctx.countryBriefPage.updateHousingCycle?.(null); |
| } |
| }); |
| } |
|
|
| refreshOpenBrief(): void { |
| const page = this.ctx.countryBriefPage; |
| if (!page?.isVisible()) return; |
| const code = page.getCode(); |
| if (!code || code === '__loading__' || code === '__error__') return; |
| const name = TIER1_COUNTRIES[code] ?? CountryIntelManager.resolveCountryName(code); |
| const scoreCode = normalizeCiiCountryCode(code); |
| const score = getCachedCountryScore(scoreCode); |
| void this.getCountrySignals(code, name) |
| .then((signals) => { |
| if (page.isVisible() && page.getCode() === code) page.updateScore?.(score, signals); |
| }) |
| .catch((err) => { |
| console.warn('[CountryBrief] refreshOpenBrief signal fetch failed:', err); |
| }); |
| } |
|
|
| private async fetchCountryIntelBrief(code: string, contextSnapshot: string, framework = ''): Promise<CountryIntelBriefResult> { |
| const lang = getCurrentLanguage(); |
| const params = new URLSearchParams({ country_code: code, lang }); |
| const trimmed = contextSnapshot.trim(); |
| if (trimmed.length > 0) { |
| |
| params.set('context', trimmed.slice(0, 3800)); |
| } |
| if (framework) { |
| params.set('framework', framework.slice(0, 2000)); |
| } |
|
|
| const resp = await premiumFetch(toApiUrl(`/api/intelligence/v1/get-country-intel-brief?${params.toString()}`), { |
| method: 'GET', |
| headers: { Accept: 'application/json' }, |
| signal: this.ctx.countryBriefPage?.signal, |
| }); |
| if (!resp.ok) return { brief: '', sources: [] }; |
|
|
| const body = (await resp.json()) as { |
| brief?: string; |
| sources?: BriefSource[]; |
| generatedAt?: string | number; |
| cached?: boolean; |
| }; |
| return { |
| brief: typeof body.brief === 'string' ? body.brief.trim() : '', |
| sources: collectBriefSources(body.sources ?? [], 6), |
| generatedAt: body.generatedAt, |
| cached: body.cached, |
| }; |
| } |
|
|
| private buildBriefContextSnapshot( |
| country: string, |
| code: string, |
| score: CountryScore | null, |
| signals: CountryBriefSignals, |
| context: Record<string, unknown>, |
| ): string { |
| const lines: string[] = []; |
| lines.push(`Country: ${country} (${code})`); |
|
|
| |
| const infraContext = this.buildInfrastructureContext(code); |
| if (infraContext) lines.push(infraContext); |
|
|
| const briefSources = collectBriefSources( |
| Array.isArray(context.briefSources) ? context.briefSources as BriefSource[] : [], |
| 6, |
| ); |
| if (briefSources.length > 0) { |
| lines.push('Brief source articles:'); |
| lines.push(...buildBriefSourceContextLines(briefSources)); |
| } |
|
|
| if (score) { |
| lines.push(`CII: ${score.score}/100 (${score.level}), trend=${score.trend}, 24h_change=${score.change24h}`); |
| lines.push(`CII components: unrest=${Math.round(score.components.unrest)}, conflict=${Math.round(score.components.conflict)}, security=${Math.round(score.components.security)}, information=${Math.round(score.components.information)}`); |
| } |
|
|
| lines.push( |
| `Signals: critical_news=${signals.criticalNews}, protests=${signals.protests}, active_strikes=${signals.activeStrikes}, military_flights=${signals.militaryFlights}, military_vessels=${signals.militaryVessels}, outages=${signals.outages}, aviation_disruptions=${signals.aviationDisruptions}, travel_advisories=${signals.travelAdvisories}, oref_sirens=${signals.orefSirens}, oref_24h=${signals.orefHistory24h}, gps_jamming_hexes=${signals.gpsJammingHexes}, ais_disruptions=${signals.aisDisruptions}, satellite_fires=${signals.satelliteFires}, radiation_anomalies=${signals.radiationAnomalies}, temporal_anomalies=${signals.temporalAnomalies}, cyber_threats=${signals.cyberThreats}, earthquakes=${signals.earthquakes}, conflict_events=${signals.conflictEvents}, thermal_escalations=${signals.thermalEscalations}`, |
| ); |
|
|
| if (signals.travelAdvisoryMaxLevel) { |
| lines.push(`Travel advisory max level: ${signals.travelAdvisoryMaxLevel}`); |
| } |
|
|
| if (signals.sanctionsDesignations > 0) { |
| const newPart = signals.sanctionsNewDesignations > 0 ? `, +${signals.sanctionsNewDesignations} new` : ''; |
| lines.push(`Sanctions: ${signals.sanctionsDesignations} active designations${newPart}`); |
| } |
|
|
| if (signals.displacementOutflow > 0) { |
| lines.push(`Displacement outflow: ${signals.displacementOutflow.toLocaleString()} persons`); |
| } |
| if (signals.climateStress > 0) { |
| lines.push(`Climate stress: ${Math.round(signals.climateStress)}/100`); |
| } |
| if (signals.isTier1) { |
| lines.push(`Major power: yes`); |
| } |
|
|
| const stockIndex = typeof context.stockIndex === 'string' ? context.stockIndex : ''; |
| if (stockIndex) lines.push(`Stock index: ${stockIndex}`); |
|
|
| const convergenceScore = typeof context.convergenceScore === 'number' ? context.convergenceScore : null; |
| const signalTypes = Array.isArray(context.signalTypes) ? context.signalTypes as string[] : []; |
| if (convergenceScore != null || signalTypes.length > 0) { |
| lines.push(`Signal convergence: score=${convergenceScore ?? 0}, types=${signalTypes.slice(0, 8).join(', ')}`); |
| } |
|
|
| const regionalConvergence = Array.isArray(context.regionalConvergence) ? context.regionalConvergence as string[] : []; |
| if (regionalConvergence.length > 0) { |
| lines.push(`Regional context: ${regionalConvergence.slice(0, 3).join(' | ')}`); |
| } |
|
|
| const headlines = Array.isArray(context.headlines) ? context.headlines as string[] : []; |
| if (headlines.length > 0) { |
| lines.push(`Headlines: ${headlines.slice(0, 6).join(' | ')}`); |
| } |
|
|
| return lines.join('\n'); |
| } |
|
|
| private buildInfrastructureContext(code: string): string { |
| try { |
| const graph = buildDependencyGraph(); |
| const countryId = `country:${code}`; |
| const incomingEdges = graph.incoming.get(countryId) || []; |
| const parts: string[] = []; |
|
|
| const cables = incomingEdges |
| .filter(e => (e.type === 'serves' || e.type === 'lands_at') && e.from.startsWith('cable:')) |
| .sort((a, b) => (b.strength ?? 0) - (a.strength ?? 0)) |
| .slice(0, 3) |
| .map(e => { |
| const node = graph.nodes.get(e.from); |
| const share = e.strength ? ` (${Math.round(e.strength * 100)}% capacity)` : ''; |
| return node ? `${node.name}${share}` : ''; |
| }).filter(Boolean); |
| if (cables.length) parts.push(`Cables: ${cables.join(', ')}`); |
|
|
| const pipes = incomingEdges |
| .filter(e => e.type === 'serves' && e.from.startsWith('pipeline:')) |
| .sort((a, b) => (b.strength ?? 0) - (a.strength ?? 0)) |
| .slice(0, 3) |
| .map(e => { |
| const node = graph.nodes.get(e.from); |
| const status = typeof node?.metadata?.status === 'string' ? node.metadata.status : undefined; |
| return node ? `${node.name}${status ? ` (${status})` : ''}` : ''; |
| }).filter(Boolean); |
| if (pipes.length) parts.push(`Pipelines: ${pipes.join(', ')}`); |
|
|
| const ports = incomingEdges |
| .filter(e => e.type === 'serves' && e.from.startsWith('port:')) |
| .sort((a, b) => (b.strength ?? 0) - (a.strength ?? 0)) |
| .slice(0, 3) |
| .map(e => { |
| const node = graph.nodes.get(e.from); |
| const rank = node?.metadata?.rank as number | undefined; |
| const type = node?.metadata?.type as string | undefined; |
| return node ? `${node.name}${rank ? ` (rank #${rank}${type ? ', ' + type : ''})` : ''}` : ''; |
| }).filter(Boolean); |
| if (ports.length) parts.push(`Ports: ${ports.join(', ')}`); |
|
|
| const chokepoints = incomingEdges |
| .filter(e => e.type === 'trade_dependency' && e.from.startsWith('chokepoint:')) |
| .map(e => { |
| const node = graph.nodes.get(e.from); |
| const reason = e.metadata?.relationship as string | undefined; |
| return node ? `${node.name}${reason ? ` (${reason})` : ''}` : ''; |
| }).filter(Boolean) |
| .slice(0, 2); |
| if (chokepoints.length) parts.push(`Waterways: ${chokepoints.join(', ')}`); |
|
|
| return parts.length > 0 ? `Infrastructure exposure: ${parts.join(' | ')}` : ''; |
| } catch { |
| return ''; |
| } |
| } |
|
|
| private mountCountryTimeline(code: string, country: string): void { |
| this.ctx.countryTimeline?.destroy(); |
| this.ctx.countryTimeline = null; |
|
|
| const mount = this.ctx.countryBriefPage?.getTimelineMount(); |
| if (!mount) return; |
|
|
| const events: TimelineEvent[] = []; |
| const countryLower = country.toLowerCase(); |
| const hasGeoShape = hasCountryGeometry(code) || !!CountryIntelManager.COUNTRY_BOUNDS[code]; |
| const inCountry = (lat: number, lon: number) => hasGeoShape && this.isInCountry(lat, lon, code); |
| const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000; |
|
|
| if (this.ctx.intelligenceCache.protests?.events) { |
| for (const e of this.ctx.intelligenceCache.protests.events) { |
| if (e.country?.toLowerCase() === countryLower || inCountry(e.lat, e.lon)) { |
| events.push({ |
| timestamp: new Date(e.time).getTime(), |
| lane: 'protest', |
| label: e.title || `${e.eventType} in ${e.city || e.country}`, |
| severity: e.severity === 'high' ? 'high' : e.severity === 'medium' ? 'medium' : 'low', |
| }); |
| } |
| } |
| } |
|
|
| if (this.ctx.intelligenceCache.earthquakes) { |
| for (const eq of this.ctx.intelligenceCache.earthquakes) { |
| if (inCountry(eq.location?.latitude ?? 0, eq.location?.longitude ?? 0) || eq.place?.toLowerCase().includes(countryLower)) { |
| events.push({ |
| timestamp: eq.occurredAt, |
| lane: 'natural', |
| label: `M${eq.magnitude.toFixed(1)} ${eq.place}`, |
| severity: eq.magnitude >= 6 ? 'critical' : eq.magnitude >= 5 ? 'high' : eq.magnitude >= 4 ? 'medium' : 'low', |
| }); |
| } |
| } |
| } |
|
|
| if (this.ctx.intelligenceCache.military) { |
| for (const f of this.ctx.intelligenceCache.military.flights) { |
| if (hasGeoShape ? this.isInCountry(f.lat, f.lon, code) : f.operatorCountry?.toUpperCase() === code) { |
| events.push({ |
| timestamp: new Date(f.lastSeen).getTime(), |
| lane: 'military', |
| label: `${f.callsign} (${f.aircraftModel || f.aircraftType})`, |
| severity: f.isInteresting ? 'high' : 'low', |
| }); |
| } |
| } |
| for (const v of this.ctx.intelligenceCache.military.vessels) { |
| if (hasGeoShape ? this.isInCountry(v.lat, v.lon, code) : v.operatorCountry?.toUpperCase() === code) { |
| events.push({ |
| timestamp: new Date(v.lastAisUpdate).getTime(), |
| lane: 'military', |
| label: `${v.name} (${v.vesselType})`, |
| severity: v.isDark ? 'high' : 'low', |
| }); |
| } |
| } |
| } |
|
|
| const ciiData = getCountryData(code); |
| if (ciiData?.conflicts) { |
| for (const c of ciiData.conflicts) { |
| events.push({ |
| timestamp: new Date(c.time).getTime(), |
| lane: 'conflict', |
| label: `${c.eventType}: ${c.location || c.country}`, |
| severity: c.fatalities > 0 ? 'critical' : 'high', |
| }); |
| } |
| } |
|
|
| for (const e of this.getCountryStrikes(code, hasGeoShape)) { |
| const rawTs = Number(e.timestamp) || 0; |
| const ts = rawTs < 1e12 ? rawTs * 1000 : rawTs; |
| events.push({ |
| timestamp: ts, |
| lane: 'conflict', |
| label: e.title || `Strike: ${e.locationName}`, |
| severity: (e.severity.toLowerCase() === 'high' || e.severity.toLowerCase() === 'critical') ? 'critical' : 'high', |
| }); |
| } |
|
|
| this.ctx.countryTimeline = new CountryTimeline(mount); |
| this.ctx.countryTimeline.render(events.filter(e => e.timestamp >= sevenDaysAgo)); |
| } |
|
|
| async getCountrySignals(code: string, country: string): Promise<CountryBriefSignals> { |
| const countryLower = country.toLowerCase(); |
| const hasGeoShape = hasCountryGeometry(code) || !!CountryIntelManager.COUNTRY_BOUNDS[code]; |
| |
| |
| |
| let clusters: CountrySignalCluster[] = []; |
| try { |
| clusters = (await getSignalAggregator()).getCountryClusters(); |
| } catch (err) { |
| console.warn('[CountryBrief] signal clusters unavailable, degrading:', err); |
| } |
| const countryCluster = clusters.find(c => c.country === code); |
| const globalCluster = clusters.find(c => c.country === 'XX'); |
| const signalTypeCounts = { |
| aisDisruptions: 0, |
| satelliteFires: 0, |
| radiationAnomalies: 0, |
| temporalAnomalies: 0, |
| }; |
| if (countryCluster) { |
| for (const s of countryCluster.signals) { |
| if (s.type === 'ais_disruption') signalTypeCounts.aisDisruptions++; |
| else if (s.type === 'satellite_fire') signalTypeCounts.satelliteFires++; |
| else if (s.type === 'radiation_anomaly') signalTypeCounts.radiationAnomalies++; |
| else if (s.type === 'temporal_anomaly') signalTypeCounts.temporalAnomalies++; |
| } |
| } |
| const globalTemporalAnomalies = globalCluster |
| ? globalCluster.signals.filter((s) => s.type === 'temporal_anomaly').length |
| : 0; |
|
|
| const searchTerms = CountryIntelManager.getCountrySearchTerms(country, code); |
| const otherCountryTerms = CountryIntelManager.getOtherCountryTerms(code); |
| const criticalNews = this.ctx.latestClusters.filter((cluster) => { |
| const title = cluster.primaryTitle.toLowerCase(); |
| const ourPos = CountryIntelManager.firstMentionPosition(title, searchTerms); |
| const otherPos = CountryIntelManager.firstMentionPosition(title, otherCountryTerms); |
| if (ourPos === Infinity || (otherPos !== Infinity && otherPos < ourPos)) return false; |
| return cluster.isAlert || cluster.threat?.level === 'critical' || cluster.threat?.level === 'high'; |
| }).length; |
|
|
| let protests = 0; |
| if (this.ctx.intelligenceCache.protests?.events) { |
| protests = this.ctx.intelligenceCache.protests.events.filter((e) => |
| e.country?.toLowerCase() === countryLower || (hasGeoShape && this.isInCountry(e.lat, e.lon, code)) |
| ).length; |
| } |
|
|
| let militaryFlights = 0; |
| let militaryVessels = 0; |
| let militaryFlightsInCountry = 0; |
| let militaryVesselsInCountry = 0; |
| if (this.ctx.intelligenceCache.military) { |
| militaryFlights = this.ctx.intelligenceCache.military.flights.filter((f) => |
| hasGeoShape ? this.isNearCountry(f.lat, f.lon, code) : f.operatorCountry?.toUpperCase() === code |
| ).length; |
| militaryVessels = this.ctx.intelligenceCache.military.vessels.filter((v) => |
| hasGeoShape ? this.isNearCountry(v.lat, v.lon, code) : v.operatorCountry?.toUpperCase() === code |
| ).length; |
| militaryFlightsInCountry = this.ctx.intelligenceCache.military.flights.filter((f) => |
| hasGeoShape ? this.isInCountry(f.lat, f.lon, code) : f.operatorCountry?.toUpperCase() === code |
| ).length; |
| militaryVesselsInCountry = this.ctx.intelligenceCache.military.vessels.filter((v) => |
| hasGeoShape ? this.isInCountry(v.lat, v.lon, code) : v.operatorCountry?.toUpperCase() === code |
| ).length; |
| } |
|
|
| let outages = 0; |
| if (this.ctx.intelligenceCache.outages) { |
| outages = this.ctx.intelligenceCache.outages.filter((o) => |
| o.country?.toLowerCase() === countryLower || (hasGeoShape && this.isInCountry(o.lat, o.lon, code)) |
| ).length; |
| } |
|
|
| let earthquakes = 0; |
| if (this.ctx.intelligenceCache.earthquakes) { |
| earthquakes = this.ctx.intelligenceCache.earthquakes.filter((eq) => { |
| if (hasGeoShape) return this.isInCountry(eq.location?.latitude ?? 0, eq.location?.longitude ?? 0, code); |
| return eq.place?.toLowerCase().includes(countryLower); |
| }).length; |
| } |
|
|
| const activeStrikes = this.getCountryStrikes(code, hasGeoShape).length; |
|
|
| let aviationDisruptions = 0; |
| if (this.ctx.intelligenceCache.flightDelays) { |
| aviationDisruptions = this.ctx.intelligenceCache.flightDelays.filter(d => |
| (d.severity === 'major' || d.severity === 'severe' || d.delayType === 'closure') && |
| (hasGeoShape ? this.isInCountry(d.lat, d.lon, code) : d.country?.toLowerCase() === countryLower) |
| ).length; |
| } |
|
|
| const ciiData = getCountryData(code); |
| const isTier1 = !!TIER1_COUNTRIES[code]; |
|
|
| let orefSirens = 0; |
| let orefHistory24h = 0; |
| if (code === 'IL' && this.ctx.intelligenceCache.orefAlerts) { |
| orefSirens = this.ctx.intelligenceCache.orefAlerts.alertCount; |
| orefHistory24h = this.ctx.intelligenceCache.orefAlerts.historyCount24h; |
| } |
|
|
| let travelAdvisories = 0; |
| let travelAdvisoryMaxLevel: string | null = null; |
| const advisoryLevelRank: Record<string, number> = { 'do-not-travel': 4, 'reconsider': 3, 'caution': 2, 'normal': 1, 'info': 0 }; |
| if (this.ctx.intelligenceCache.advisories) { |
| const countryAdvisories = this.ctx.intelligenceCache.advisories.filter(a => a.country === code); |
| travelAdvisories = countryAdvisories.length; |
| for (const a of countryAdvisories) { |
| if (a.level && (advisoryLevelRank[a.level] || 0) > (advisoryLevelRank[travelAdvisoryMaxLevel || ''] || 0)) { |
| travelAdvisoryMaxLevel = a.level; |
| } |
| } |
| } |
|
|
| let cyberThreats = 0; |
| if (this.ctx.cyberThreatsCache) { |
| cyberThreats = this.ctx.cyberThreatsCache.filter((threat) => { |
| if (threat.country && threat.country.length === 2) return threat.country.toUpperCase() === code; |
| return hasGeoShape && this.isInCountry(threat.lat, threat.lon, code); |
| }).length; |
| } |
|
|
| let thermalEscalations = 0; |
| if (this.ctx.intelligenceCache.thermalEscalation) { |
| thermalEscalations = this.ctx.intelligenceCache.thermalEscalation.clusters.filter( |
| (c) => c.countryCode.toUpperCase() === code && c.status !== 'normal', |
| ).length; |
| } |
|
|
| const sanctionsCountry = this.ctx.intelligenceCache.sanctions?.countries.find( |
| (c) => c.countryCode.toUpperCase() === code, |
| ); |
| const sanctionsDesignations = sanctionsCountry?.entryCount ?? 0; |
| const sanctionsNewDesignations = sanctionsCountry?.newEntryCount ?? 0; |
|
|
| return { |
| criticalNews, |
| protests, |
| militaryFlights, |
| militaryVessels, |
| militaryFlightsInCountry, |
| militaryVesselsInCountry, |
| outages, |
| aisDisruptions: signalTypeCounts.aisDisruptions, |
| satelliteFires: signalTypeCounts.satelliteFires, |
| radiationAnomalies: signalTypeCounts.radiationAnomalies, |
| temporalAnomalies: signalTypeCounts.temporalAnomalies > 0 ? signalTypeCounts.temporalAnomalies : globalTemporalAnomalies, |
| cyberThreats, |
| earthquakes, |
| displacementOutflow: ciiData?.displacementOutflow ?? 0, |
| climateStress: ciiData?.climateStress ?? 0, |
| conflictEvents: ciiData?.conflicts?.length ?? 0, |
| activeStrikes, |
| orefSirens, |
| orefHistory24h, |
| aviationDisruptions, |
| travelAdvisories, |
| travelAdvisoryMaxLevel, |
| gpsJammingHexes: (ciiData?.gpsJammingHighCount ?? 0) + (ciiData?.gpsJammingMediumCount ?? 0), |
| isTier1, |
| thermalEscalations, |
| sanctionsDesignations, |
| sanctionsNewDesignations, |
| }; |
| } |
|
|
| private newsSeverityRank(item: NewsItem): number { |
| const level = item.threat?.level; |
| if (level === 'critical') return 5; |
| if (level === 'high') return 4; |
| if (level === 'medium') return 3; |
| if (level === 'low') return 2; |
| if (item.isAlert) return 4; |
| return 1; |
| } |
|
|
| private async buildSignalDetails(code: string): Promise<CountryDeepDiveSignalDetails> { |
| const cluster = (await getSignalAggregator()).getCountryClusters().find((entry) => entry.country === code); |
| if (!cluster) { |
| return { critical: 0, high: 0, medium: 0, low: 0, recentHigh: [] }; |
| } |
|
|
| const details: CountryDeepDiveSignalDetails = { |
| critical: 0, |
| high: 0, |
| medium: 0, |
| low: 0, |
| recentHigh: [], |
| }; |
|
|
| const rankedSignals = [...cluster.signals] |
| .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); |
|
|
| for (const signal of rankedSignals) { |
| const severity = this.normalizeSignalSeverity(signal.type, signal.severity); |
| if (severity === 'critical') details.critical += 1; |
| else if (severity === 'high') details.high += 1; |
| else if (severity === 'medium') details.medium += 1; |
| else details.low += 1; |
| } |
|
|
| details.recentHigh = rankedSignals |
| .map((signal) => ({ |
| type: this.mapSignalType(signal.type), |
| severity: this.normalizeSignalSeverity(signal.type, signal.severity), |
| description: signal.title, |
| timestamp: signal.timestamp, |
| })) |
| .filter((signal) => signal.severity === 'critical' || signal.severity === 'high') |
| .slice(0, 3); |
|
|
| return details; |
| } |
|
|
| private buildMilitarySummary(code: string, country: string): CountryDeepDiveMilitarySummary { |
| const hasGeoShape = hasCountryGeometry(code) || !!CountryIntelManager.COUNTRY_BOUNDS[code]; |
| const flights = this.ctx.intelligenceCache.military?.flights ?? []; |
| const vessels = this.ctx.intelligenceCache.military?.vessels ?? []; |
|
|
| const flightsInCountry = flights.filter((flight) => |
| hasGeoShape ? this.isInCountry(flight.lat, flight.lon, code) : this.sameCountry(code, country, flight.operatorCountry) |
| ); |
| const ownFlights = flightsInCountry.filter((flight) => this.sameCountry(code, country, flight.operatorCountry)).length; |
| const foreignFlights = Math.max(0, flightsInCountry.length - ownFlights); |
|
|
| const vesselsInCountry = vessels.filter((vessel) => |
| hasGeoShape ? this.isInCountry(vessel.lat, vessel.lon, code) : this.sameCountry(code, country, vessel.operatorCountry) |
| ); |
| const foreignVessels = vesselsInCountry.filter((vessel) => !this.sameCountry(code, country, vessel.operatorCountry)).length; |
|
|
| const centroid = getCountryCentroid(code, CountryIntelManager.COUNTRY_BOUNDS); |
| const nearbyBases = centroid |
| ? getNearbyInfrastructure(centroid.lat, centroid.lon, ['base']).slice(0, 3).map((base) => ({ |
| id: base.id, |
| name: base.name, |
| distanceKm: base.distanceKm, |
| country: getCachedMilitaryBases().find((entry) => entry.id === base.id)?.country, |
| })) |
| : []; |
|
|
| return { |
| ownFlights, |
| foreignFlights, |
| nearbyVessels: vesselsInCountry.length, |
| nearestBases: nearbyBases, |
| foreignPresence: foreignFlights > 0 || foreignVessels > 0, |
| }; |
| } |
|
|
| private buildEconomicIndicators( |
| code: string, |
| score: CountryScore | null, |
| stock: CountryStockSnapshot | null, |
| imfBundle?: ImfCountryBundle | null, |
| ): CountryDeepDiveEconomicIndicator[] { |
| const indicators: CountryDeepDiveEconomicIndicator[] = []; |
|
|
| if (stock?.available) { |
| const weekly = Number.parseFloat(stock.weekChangePercent); |
| const weeklyTrend = Number.isFinite(weekly) |
| ? weekly > 0 ? 'up' : weekly < 0 ? 'down' : 'flat' |
| : 'flat'; |
| indicators.push({ |
| label: 'Stock Index', |
| value: `${stock.indexName}: ${stock.price} ${stock.currency}`, |
| trend: weeklyTrend, |
| source: 'Market Service', |
| }); |
| indicators.push({ |
| label: 'Weekly Momentum', |
| value: `${weekly >= 0 ? '+' : ''}${stock.weekChangePercent}%`, |
| trend: weeklyTrend, |
| }); |
| } |
|
|
| if (score) { |
| const trend = score.trend === 'rising' |
| ? 'up' |
| : score.trend === 'falling' |
| ? 'down' |
| : 'flat'; |
| indicators.push({ |
| label: 'Instability Regime', |
| value: `${score.score}/100 (${score.level})`, |
| trend, |
| source: 'CII', |
| }); |
| } |
|
|
| const countryData = getCountryData(code); |
| if (countryData?.displacementOutflow && countryData.displacementOutflow > 0) { |
| const displaced = countryData.displacementOutflow >= 1_000_000 |
| ? `${(countryData.displacementOutflow / 1_000_000).toFixed(1)}M` |
| : `${Math.round(countryData.displacementOutflow / 1000)}K`; |
| indicators.push({ |
| label: 'Displacement Outflow', |
| value: displaced, |
| trend: 'up', |
| source: 'UN-style displacement feed', |
| }); |
| } |
|
|
| |
| |
| |
| if (imfBundle) { |
| for (const ind of buildImfEconomicIndicators(imfBundle)) { |
| indicators.push(ind); |
| } |
| } |
|
|
| return indicators.slice(0, 6); |
| } |
|
|
| private sameCountry(code: string, country: string, raw: string | undefined): boolean { |
| if (!raw) return false; |
| const normalized = raw.trim(); |
| if (!normalized) return false; |
|
|
| const upper = normalized.toUpperCase(); |
| if (upper === code) return true; |
| if (upper.length === 3) { |
| const iso2 = iso3ToIso2Code(upper); |
| if (iso2 === code) return true; |
| } |
|
|
| const fromName = nameToCountryCode(normalized.toLowerCase()); |
| if (fromName === code) return true; |
|
|
| const countryLower = country.toLowerCase(); |
| const rawLower = normalized.toLowerCase(); |
| return rawLower === countryLower || CountryIntelManager.countryTermIndex(rawLower, countryLower) !== -1; |
| } |
|
|
| private mapSignalType(type: string): CountryDeepDiveSignalDetails['recentHigh'][number]['type'] { |
| if (type === 'military_flight' || type === 'military_vessel') return 'MILITARY'; |
| if (type === 'protest') return 'PROTEST'; |
| if (type === 'internet_outage') return 'OUTAGE'; |
| if (type === 'satellite_fire') return 'DISASTER'; |
| if (type === 'radiation_anomaly') return 'DISASTER'; |
| if (type === 'ais_disruption') return 'OUTAGE'; |
| if (type === 'active_strike') return 'MILITARY'; |
| if (type === 'temporal_anomaly') return 'CYBER'; |
| return 'OTHER'; |
| } |
|
|
| private normalizeSignalSeverity( |
| type: string, |
| severity: 'low' | 'medium' | 'high', |
| ): CountryDeepDiveSignalDetails['recentHigh'][number]['severity'] { |
| if (type === 'active_strike' && severity === 'high') return 'critical'; |
| if (type === 'radiation_anomaly' && severity === 'high') return 'critical'; |
| if (severity === 'high') return 'high'; |
| if (severity === 'medium') return 'medium'; |
| return 'low'; |
| } |
|
|
| async openCountryStory(code: string, name: string): Promise<void> { |
| if (!dataFreshness.hasSufficientData() || this.ctx.latestClusters.length === 0) { |
| this.showToast('Data still loading — try again in a moment'); |
| return; |
| } |
| const posturePanel = this.ctx.panels['strategic-posture'] as StrategicPosturePanel | undefined; |
| const postures = posturePanel?.getPostures() || []; |
| const aggregator = await getSignalAggregator(); |
| const signals = await this.getCountrySignals(code, name); |
| const cluster = aggregator.getCountryClusters().find(c => c.country === code); |
| const regional = aggregator.getRegionalConvergence().filter(r => r.countries.includes(code)); |
| const convergence = cluster ? { |
| score: cluster.convergenceScore, |
| signalTypes: [...cluster.signalTypes], |
| regionalDescriptions: regional.map(r => r.description), |
| } : null; |
| const data = collectStoryData(code, name, this.ctx.latestClusters, postures, this.ctx.latestPredictions, signals, convergence); |
| |
| |
| const { openStoryModal } = await import('@/components/StoryModal'); |
| openStoryModal(data); |
| } |
|
|
| showToast(msg: string): void { |
| document.querySelector('.toast-notification')?.remove(); |
| const el = document.createElement('div'); |
| el.className = 'toast-notification'; |
| el.textContent = msg; |
| document.body.appendChild(el); |
| requestAnimationFrame(() => el.classList.add('visible')); |
| setTimeout(() => { el.classList.remove('visible'); setTimeout(() => el.remove(), 300); }, 3000); |
| } |
|
|
| private getCountryStrikes(code: string, hasGeoShape: boolean): typeof this.ctx.intelligenceCache.iranEvents & object { |
| if (!IRAN_ATTACKS_ENABLED) return []; |
| if (!this.ctx.intelligenceCache.iranEvents) return []; |
| const seen = new Set<string>(); |
| return this.ctx.intelligenceCache.iranEvents.filter(e => { |
| if (seen.has(e.id)) return false; |
| seen.add(e.id); |
| return hasGeoShape && this.isInCountry(e.latitude, e.longitude, code); |
| }); |
| } |
|
|
| private isInCountry(lat: number, lon: number, code: string): boolean { |
| const precise = isCoordinateInCountry(lat, lon, code); |
| if (precise === true) return true; |
| |
| |
| const b = CountryIntelManager.COUNTRY_BOUNDS[code]; |
| if (!b) return false; |
| return lat >= b.s && lat <= b.n && lon >= b.w && lon <= b.e; |
| } |
|
|
| |
| |
| |
| private static readonly NEAR_BUFFER_DEG = 2; |
| private isNearCountry(lat: number, lon: number, code: string): boolean { |
| if (this.isInCountry(lat, lon, code)) return true; |
| const b = CountryIntelManager.COUNTRY_BOUNDS[code]; |
| if (!b) return false; |
| const pad = CountryIntelManager.NEAR_BUFFER_DEG; |
| return lat >= b.s - pad && lat <= b.n + pad && lon >= b.w - pad && lon <= b.e + pad; |
| } |
|
|
| static COUNTRY_BOUNDS: Record<string, { n: number; s: number; e: number; w: number }> = { |
| ...ME_STRIKE_BOUNDS, |
| CN: { n: 53.6, s: 18.2, e: 134.8, w: 73.5 }, TW: { n: 25.3, s: 21.9, e: 122, w: 120 }, |
| JP: { n: 45.5, s: 24.2, e: 153.9, w: 122.9 }, KR: { n: 38.6, s: 33.1, e: 131.9, w: 124.6 }, |
| KP: { n: 43.0, s: 37.7, e: 130.7, w: 124.2 }, IN: { n: 35.5, s: 6.7, e: 97.4, w: 68.2 }, |
| PK: { n: 37, s: 24, e: 77, w: 61 }, AF: { n: 38.5, s: 29.4, e: 74.9, w: 60.5 }, |
| UA: { n: 52.4, s: 44.4, e: 40.2, w: 22.1 }, RU: { n: 82, s: 41.2, e: 180, w: 19.6 }, |
| BY: { n: 56.2, s: 51.3, e: 32.8, w: 23.2 }, PL: { n: 54.8, s: 49, e: 24.1, w: 14.1 }, |
| EG: { n: 31.7, s: 22, e: 36.9, w: 25 }, LY: { n: 33, s: 19.5, e: 25, w: 9.4 }, |
| SD: { n: 22, s: 8.7, e: 38.6, w: 21.8 }, US: { n: 49, s: 24.5, e: -66.9, w: -125 }, |
| GB: { n: 58.7, s: 49.9, e: 1.8, w: -8.2 }, DE: { n: 55.1, s: 47.3, e: 15.0, w: 5.9 }, |
| FR: { n: 51.1, s: 41.3, e: 9.6, w: -5.1 }, TR: { n: 42.1, s: 36, e: 44.8, w: 26 }, |
| BR: { n: 5.3, s: -33.8, e: -34.8, w: -73.9 }, |
| }; |
|
|
| static COUNTRY_ALIASES: Record<string, string[]> = { |
| IL: ['israel', 'israeli', 'gaza', 'hamas', 'hezbollah', 'netanyahu', 'idf', 'west bank', 'tel aviv', 'jerusalem'], |
| IR: ['iran', 'iranian', 'tehran', 'persian', 'irgc', 'khamenei'], |
| RU: ['russia', 'russian', 'moscow', 'kremlin', 'putin', 'ukraine war'], |
| UA: ['ukraine', 'ukrainian', 'kyiv', 'zelensky', 'zelenskyy'], |
| CN: ['china', 'chinese', 'beijing', 'taiwan strait', 'south china sea', 'xi jinping'], |
| TW: ['taiwan', 'taiwanese', 'taipei'], |
| KP: ['north korea', 'pyongyang', 'kim jong'], |
| KR: ['south korea', 'seoul'], |
| SA: ['saudi', 'riyadh', 'mbs'], |
| SY: ['syria', 'syrian', 'damascus', 'assad'], |
| YE: ['yemen', 'houthi', 'sanaa'], |
| IQ: ['iraq', 'iraqi', 'baghdad'], |
| AF: ['afghanistan', 'afghan', 'kabul', 'taliban'], |
| PK: ['pakistan', 'pakistani', 'islamabad'], |
| IN: ['india', 'indian', 'new delhi', 'modi'], |
| EG: ['egypt', 'egyptian', 'cairo', 'suez'], |
| LB: ['lebanon', 'lebanese', 'beirut'], |
| TR: ['turkey', 'turkish', 'ankara', 'erdogan', 'türkiye'], |
| US: ['united states', 'american', 'washington', 'pentagon', 'white house'], |
| GB: ['united kingdom', 'british', 'london', 'uk '], |
| BR: ['brazil', 'brazilian', 'brasilia', 'lula', 'bolsonaro'], |
| AE: ['united arab emirates', 'uae', 'emirati', 'dubai', 'abu dhabi'], |
| }; |
|
|
| private static otherCountryTermsCache: Map<string, string[]> = new Map(); |
|
|
| static escapeRegExp(value: string): string { |
| return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); |
| } |
|
|
| static countryTermIndex(text: string, term: string): number { |
| const normalizedTerm = term.trim().toLowerCase(); |
| if (!normalizedTerm) return -1; |
| const match = new RegExp(`(^|[^a-z0-9])${CountryIntelManager.escapeRegExp(normalizedTerm)}(?=$|[^a-z0-9])`, 'i').exec(text); |
| return match ? match.index + (match[1] ?? '').length : -1; |
| } |
|
|
| static firstMentionPosition(text: string, terms: string[]): number { |
| let earliest = Infinity; |
| for (const term of terms) { |
| const idx = CountryIntelManager.countryTermIndex(text, term); |
| if (idx !== -1 && idx < earliest) earliest = idx; |
| } |
| return earliest; |
| } |
|
|
| static getOtherCountryTerms(code: string): string[] { |
| const cached = CountryIntelManager.otherCountryTermsCache.get(code); |
| if (cached) return cached; |
|
|
| const dedup = new Set<string>(); |
| Object.entries(CountryIntelManager.COUNTRY_ALIASES).forEach(([countryCode, aliases]) => { |
| if (countryCode === code) return; |
| aliases.forEach((alias) => { |
| const normalized = alias.toLowerCase(); |
| if (normalized.trim().length > 0) dedup.add(normalized); |
| }); |
| }); |
|
|
| const terms = [...dedup]; |
| CountryIntelManager.otherCountryTermsCache.set(code, terms); |
| return terms; |
| } |
|
|
| static resolveCountryName(code: string): string { |
| if (TIER1_COUNTRIES[code]) return TIER1_COUNTRIES[code]; |
|
|
| try { |
| const displayNamesCtor = (Intl as unknown as { DisplayNames?: IntlDisplayNamesCtor }).DisplayNames; |
| if (!displayNamesCtor) return code; |
| const displayNames = new displayNamesCtor(['en'], { type: 'region' }); |
| const resolved = displayNames.of(code); |
| if (resolved && resolved.toUpperCase() !== code) return resolved; |
| } catch { |
| |
| } |
|
|
| return code; |
| } |
|
|
| static getCountrySearchTerms(country: string, code: string): string[] { |
| const aliases = CountryIntelManager.COUNTRY_ALIASES[code]; |
| if (aliases) return aliases; |
| if (/^[A-Z]{2}$/i.test(country.trim())) return []; |
| return [country.toLowerCase()]; |
| } |
|
|
| static toFlagEmoji(code: string): string { |
| return toFlagEmoji(code, '🏳️'); |
| } |
| } |
|
|