GenerAI / worldmonitor /src /app /country-intel.ts
amogaddy's picture
Integra World Monitor (AGPL-3.0, self-hosted) nello Space: pagina, menu, e arricchimento notizie per la AI (part 6)
fa9c65f verified
Raw
History Blame Contribute Delete
81.4 kB
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';
// StoryModal is lazy-imported at its call site (below), and its render path
// lazy-imports story-renderer, so both stay off the eager boot graph.
// renderStoryToCanvas was already made dynamic in #4486; #4571 closes the
// remaining StoryModal eager edge. The modal opens on user interaction
// (post-paint), so the import() latency is hidden.
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';
// Iran-events domain sunset (war ended 2026-07). Default OFF: no strikes in the
// country deep-dive or the AI brief. Set VITE_ENABLE_IRAN_ATTACKS=true to restore.
// Guarded with the client-runtime check so node:test never dereferences import.meta.env.
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;
// Re-fire PRO-gated country sections on false→true entitlement transition.
// Without this, a user who opens a country brief before Clerk resolves
// keeps seeing empty national-debt / sanctions / comtrade / tariff cards
// until they reselect the country or reload. Tracks the last-seen
// entitlement so unrelated auth events (session refresh, prefs sync)
// don't re-hammer fetchProSections.
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) {
// Entitlement just resolved — refetch PRO sections for whatever
// country the user is currently viewing. No current country =
// nothing to retry; the next country open will pick up the new
// entitlement naturally.
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'),
})),
});
});
}
// Yield so the deep-dive panel paint lands before the map catch-up
// (highlightCountry deck rebuild + fitCountry fitBounds animation) — country
// click is the field #1 INP offender, presentation-delay-dominated (#4617).
// Re-check the staleness guard after the new async gap so a newer country
// open can't be painted over by this one.
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));
});
// IMF WEO bundle (issue #3027): macro / growth / labor / external from
// the SDMX-3.0 seeded keys. Tolerant: missing data leaves card unchanged.
imfPromise.then((bundle) => {
latestImf = bundle;
if (this.ctx.countryBriefPage?.getCode() !== code) return;
this.ctx.countryBriefPage.updateEconomicIndicators?.(this.buildEconomicIndicators(code, score, latestStock, bundle));
}).catch(() => { /* non-fatal */ });
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: '' });
});
// Fetch multi-sector exposure (all 10 seeded HS2 codes in parallel)
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;
}
// Build a synthetic compat response from sector data (no extra fetch needed)
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);
// Trigger multi-sector cost shock calculator from the same primary chokepoint.
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 { /* RAG unavailable */ }
}
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 { /* server unreachable */ }
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 { /* T5 failed */ }
}
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 {
// /pro live-preview iframe can't carry a Clerk session, so every pro
// section call would 401. Skip the RPCs entirely so the embedded
// preview doesn't spam the parent /pro console with expected failures.
if (IS_EMBEDDED_PREVIEW) return;
const rpcBase = getRpcBaseUrl();
// Pro-section endpoints (national-debt, regional briefs, comtrade flows)
// are premium-gated — premiumFetch injects the Clerk bearer / API key so
// signed-in pro users actually get data instead of 401.
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);
// Trade RPCs (listComtradeFlows + getTariffTrends) are PRO-gated and
// 401 for anonymous/free users. Mirror the hasPremiumAccess() guard
// already used above for the other premium country-brief cards so we
// don't spray the console with 401s on every country click.
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);
}
});
// Housing cycle tile — BIS WS_SPP (residential), WS_CPP (commercial), WS_DSR.
// All three keys are seeded by the bis-extended cron and exposed via the
// public bootstrap endpoint, so one scoped bootstrap call covers the tile.
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;
// Euro area (XM) fallback for EU countries that BIS only publishes as a bloc aggregate.
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) {
// 3800 chars ≈ ~950 tokens; raised from 2200 to include infra context. Monitor p95 LLM latency if timeouts increase.
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})`);
// Infrastructure grounding must appear early so it survives the context char limit
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];
// The signal-aggregator chunk is lazy-loaded; if it fails to load we still
// render the brief from the independent intelligence caches below rather
// than aborting the whole open. Only the cluster-derived counts degrade.
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',
});
}
// IMF WEO indicators (issue #3027): real GDP growth, inflation,
// unemployment, GDP/capita. Appended after the live signals so that
// markets-driven rows take priority on the limited card surface.
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);
// await (not void) so a chunk-load failure rejects openCountryStory's promise and
// reaches the caller's existing .catch() toast handler (country-intel.ts:205).
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;
// When precise geometry returns false (coastal/polygon precision) or null (not loaded),
// fall through to bounding box — matches CII's coordsToBoundsCountry fallback
const b = CountryIntelManager.COUNTRY_BOUNDS[code];
if (!b) return false;
return lat >= b.s && lat <= b.n && lon >= b.w && lon <= b.e;
}
// Near = bounding-box padded by ~2° (~220 km). Captures vessels/aircraft in
// adjacent waters/airspace so the risk chip reflects proximity, not just
// strict territory. See issue #2972 bug 2.
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 {
// Intl.DisplayNames unavailable in older runtimes.
}
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, '🏳️');
}
}