import { escapeHtml, sanitizeUrl } from '@/utils/sanitize'; import { formatIntelBrief } from '@/utils/format-intel-brief'; import { collectBriefSources, renderBriefSourcesFooter, type BriefSource } from '@/utils/brief-sources'; import { t } from '@/services/i18n'; import { getCSSColor, showToast } from '@/utils'; import type { CountryScore } from '@/services/country-instability'; import type { NewsItem } from '@/types'; import type { PredictionMarket } from '@/services/prediction'; import type { AssetType } from '@/types'; import type { CountryBriefSignals } from '@/types'; import type { CountryBriefPanel, CountryIntelData, StockIndexData } from '@/components/CountryBriefPanel'; import { getNearbyInfrastructure, haversineDistanceKm } from '@/services/related-assets'; import { PORTS } from '@/config/ports'; import type { Port } from '@/types'; import { exportCountryBriefJSON, exportCountryBriefCSV, exportCountryEvidenceMarkdown } from '@/utils/export'; import type { CountryBriefExport, CountryEvidenceBundleInput } from '@/utils/export'; import { ME_STRIKE_BOUNDS } from '@/services/country-geometry'; import { toFlagEmoji } from '@/utils/country-flag'; import { setTrustedHtml, trustedHtml } from '@/utils/dom-utils'; import { getAuthState } from '@/services/auth-state'; import { evaluateAvailableExportFormats, evaluateExportGate, exportLockToGateReason, hasPremiumAccess, } from '@/services/panel-gating'; import { primeExportGateActivation } from '@/services/export-gate'; import { exportGateCopy } from '@/components/ExportGateControl'; import { trackGateHit } from '@/services/analytics'; type BriefAssetType = AssetType | 'port'; export class CountryBriefPage implements CountryBriefPanel { private static BRIEF_BOUNDS: Record = { ...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 }, }; private static INFRA_ICONS: Record = { pipeline: '\u{1F50C}', cable: '\u{1F310}', datacenter: '\u{1F5A5}\uFE0F', base: '\u{1F3DB}\uFE0F', nuclear: '\u2622\uFE0F', port: '\u2693', }; private static INFRA_LABELS: Record = { pipeline: 'pipeline', cable: 'cable', datacenter: 'datacenter', base: 'base', nuclear: 'nuclear', port: 'port', }; private overlay: HTMLElement; private currentCode: string | null = null; private currentName: string | null = null; private currentHeadlineCount = 0; private currentScore: CountryScore | null = null; private currentSignals: CountryBriefSignals | null = null; private currentBrief: string | null = null; private currentBriefGeneratedAt: string | number | null = null; private currentBriefCached: boolean | null = null; private currentHeadlines: NewsItem[] = []; private onCloseCallback?: () => void; private onShareStory?: (code: string, name: string) => void; private onExportImage?: (code: string, name: string) => void; private abortController: AbortController = new AbortController(); constructor() { this.overlay = document.createElement('div'); this.overlay.className = 'country-brief-overlay'; document.body.appendChild(this.overlay); // Single delegated click handler for all interactive elements. // This prevents listener accumulation when show()/showLoading() replace innerHTML. this.overlay.addEventListener('click', (e) => { const target = e.target as HTMLElement; // Click on overlay background to close if (target.classList.contains('country-brief-overlay')) { this.hide(); return; } // Close button if (target.closest('.cb-close')) { this.hide(); return; } // Link share button (copy URL to clipboard) const linkShareBtn = target.closest('.cb-link-share-btn') as HTMLButtonElement | null; if (linkShareBtn) { if (!this.currentCode || !this.currentName) return; const url = `${window.location.origin}/?c=${this.currentCode}`; navigator.clipboard.writeText(url).then(() => { const orig = linkShareBtn.innerHTML; setTrustedHtml(linkShareBtn, trustedHtml('', "legacy direct innerHTML migration")); setTimeout(() => { setTrustedHtml(linkShareBtn, trustedHtml(orig, "legacy direct innerHTML migration")); }, 1500); }).catch(() => {}); return; } // Share button if (target.closest('.cb-share-btn')) { if (this.onShareStory && this.currentCode && this.currentName) { this.onShareStory(this.currentCode, this.currentName); } return; } // Print button if (target.closest('.cb-print-btn')) { window.print(); return; } // Export button (toggle menu) if (target.closest('.cb-export-btn')) { e.stopPropagation(); const exportMenu = this.overlay.querySelector('.cb-export-menu'); this.syncStructuredExportOptions(); exportMenu?.classList.toggle('hidden'); return; } // Export option buttons const exportOption = target.closest('.cb-export-option') as HTMLElement | null; if (exportOption) { const format = exportOption.dataset.format; if (format === 'image') { if (this.onExportImage && this.currentCode && this.currentName) { this.onExportImage(this.currentCode, this.currentName); } } else if (format === 'pdf') { this.exportPdf(); } else if (format === 'json' || format === 'csv') { if (this.canExportStructuredData(format)) this.exportBrief(format); } else if (format === 'evidence-md') { this.exportBrief(format); } const exportMenu = this.overlay.querySelector('.cb-export-menu'); exportMenu?.classList.add('hidden'); return; } // Citation links if (target.classList.contains('cb-citation')) { const href = target.getAttribute('href'); if (href?.startsWith('#')) { e.preventDefault(); const el = this.overlay.querySelector(href); el?.scrollIntoView({ behavior: 'smooth', block: 'center' }); el?.classList.add('cb-news-highlight'); setTimeout(() => el?.classList.remove('cb-news-highlight'), 2000); } return; } // Clicking anywhere else closes the export menu if open const exportMenu = this.overlay.querySelector('.cb-export-menu'); if (exportMenu && !exportMenu.classList.contains('hidden')) { exportMenu.classList.add('hidden'); } }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && this.overlay.classList.contains('active')) this.hide(); }); } private countryFlag(code: string): string { return toFlagEmoji(code, '๐ŸŒ'); } private levelColor(level: string): string { const varMap: Record = { critical: '--semantic-critical', high: '--semantic-high', elevated: '--semantic-elevated', normal: '--semantic-normal', low: '--semantic-low', }; return getCSSColor(varMap[level] || '--text-dim'); } private levelBadge(level: string): string { const color = this.levelColor(level); const levelKey = level as 'critical' | 'high' | 'elevated' | 'moderate' | 'normal' | 'low'; const label = t(`countryBrief.levels.${levelKey}`); return `${label.toUpperCase()}`; } private trendIndicator(trend: string): string { const arrow = trend === 'rising' ? 'โ†—' : trend === 'falling' ? 'โ†˜' : 'โ†’'; const cls = trend === 'rising' ? 'trend-up' : trend === 'falling' ? 'trend-down' : 'trend-stable'; const trendKey = trend as 'rising' | 'falling' | 'stable'; const trendLabel = t(`countryBrief.trends.${trendKey}`); return `${arrow} ${trendLabel}`; } private scoreRing(score: number, level: string): string { const color = this.levelColor(level); const pct = Math.min(100, Math.max(0, score)); const circumference = 2 * Math.PI * 42; const dashOffset = circumference * (1 - pct / 100); return `
${score}
/ 100
`; } private componentBars(components: CountryScore['components']): string { const items = [ { label: t('modals.countryBrief.components.unrest'), value: components.unrest, icon: '๐Ÿ“ข' }, { label: t('modals.countryBrief.components.conflict'), value: components.conflict, icon: 'โš”' }, { label: t('modals.countryBrief.components.security'), value: components.security, icon: '๐Ÿ›ก๏ธ' }, { label: t('modals.countryBrief.components.information'), value: components.information, icon: '๐Ÿ“ก' }, ]; return items.map(({ label, value, icon }) => { const pct = Math.min(100, Math.max(0, value)); const color = pct >= 70 ? getCSSColor('--semantic-critical') : pct >= 50 ? getCSSColor('--semantic-high') : pct >= 30 ? getCSSColor('--semantic-elevated') : getCSSColor('--semantic-normal'); return `
${icon} ${label}
${Math.round(value)}
`; }).join(''); } private signalChips(signals: CountryBriefSignals): string { const chips: string[] = []; if (signals.criticalNews > 0) chips.push(`๐Ÿšจ ${signals.criticalNews} Critical News`); if (signals.protests > 0) chips.push(`๐Ÿ“ข ${signals.protests} ${t('modals.countryBrief.signals.protests')}`); if (signals.militaryFlights > 0) { const tip = `${signals.militaryFlights} near ยท ${signals.militaryFlightsInCountry} inside borders`; chips.push(`โœˆ๏ธ ${signals.militaryFlights} ${t('modals.countryBrief.signals.militaryAir')}`); } if (signals.militaryVessels > 0) { const tip = `${signals.militaryVessels} near ยท ${signals.militaryVesselsInCountry} inside borders`; chips.push(`โš“ ${signals.militaryVessels} ${t('modals.countryBrief.signals.militarySea')}`); } if (signals.outages > 0) chips.push(`๐ŸŒ ${signals.outages} ${t('modals.countryBrief.signals.outages')}`); if (signals.aisDisruptions > 0) chips.push(`๐Ÿšข ${signals.aisDisruptions} AIS Disruptions`); if (signals.satelliteFires > 0) chips.push(`๐Ÿ”ฅ ${signals.satelliteFires} Satellite Fires`); if (signals.radiationAnomalies > 0) chips.push(`โ˜ข๏ธ ${signals.radiationAnomalies} Radiation Anomalies`); if (signals.temporalAnomalies > 0) chips.push(`โฑ๏ธ ${signals.temporalAnomalies} Temporal Anomalies`); if (signals.cyberThreats > 0) chips.push(`๐Ÿ›ก๏ธ ${signals.cyberThreats} Cyber Threats`); if (signals.earthquakes > 0) chips.push(`๐ŸŒ ${signals.earthquakes} ${t('modals.countryBrief.signals.earthquakes')}`); if (signals.displacementOutflow > 0) { const fmt = signals.displacementOutflow >= 1_000_000 ? `${(signals.displacementOutflow / 1_000_000).toFixed(1)}M` : `${(signals.displacementOutflow / 1000).toFixed(0)}K`; chips.push(`๐ŸŒŠ ${fmt} ${t('modals.countryBrief.signals.displaced')}`); } if (signals.climateStress > 0) chips.push(`๐ŸŒก๏ธ ${t('modals.countryBrief.signals.climate')}`); if (signals.conflictEvents > 0) chips.push(`โš”๏ธ ${signals.conflictEvents} ${t('modals.countryBrief.signals.conflictEvents')}`); if (signals.activeStrikes > 0) chips.push(`\u{1F4A5} ${signals.activeStrikes} ${t('modals.countryBrief.signals.activeStrikes')}`); if (signals.travelAdvisories > 0 && signals.travelAdvisoryMaxLevel) { const advisoryClass = signals.travelAdvisoryMaxLevel === 'do-not-travel' ? 'conflict' : signals.travelAdvisoryMaxLevel === 'reconsider' ? 'outage' : 'military'; const advisoryLabel = signals.travelAdvisoryMaxLevel === 'do-not-travel' ? 'Do Not Travel' : signals.travelAdvisoryMaxLevel === 'reconsider' ? 'Reconsider Travel' : 'Exercise Caution'; chips.push(`\u26A0\uFE0F ${signals.travelAdvisories} Advisory: ${advisoryLabel}`); } if (signals.orefSirens > 0) chips.push(`\u{1F6A8} ${signals.orefSirens} Active Sirens`); if (signals.orefHistory24h > 0) chips.push(`\u{1F553} ${signals.orefHistory24h} Sirens / 24h`); if (signals.aviationDisruptions > 0) chips.push(`\u{1F6AB} ${signals.aviationDisruptions} ${t('modals.countryBrief.signals.aviationDisruptions')}`); if (signals.gpsJammingHexes > 0) chips.push(`\u{1F4E1} ${signals.gpsJammingHexes} ${t('modals.countryBrief.signals.gpsJammingZones')}`); chips.push(`๐Ÿ“ˆ ${t('modals.countryBrief.loadingIndex')}`); return chips.join(''); } public setShareStoryHandler(handler: (code: string, name: string) => void): void { this.onShareStory = handler; } public setExportImageHandler(handler: (code: string, name: string) => void): void { this.onExportImage = handler; } public showLoading(): void { this.currentCode = '__loading__'; setTrustedHtml(this.overlay, trustedHtml(`
๐ŸŒ ${t('modals.countryBrief.identifying')}
${t('modals.countryBrief.locating')}
`, "legacy direct innerHTML migration")); // Close button click is handled via event delegation on the overlay (set up in constructor) this.overlay.classList.add('active'); } public showGeoError(onRetry: () => void): void { this.currentCode = '__error__'; this.overlay.textContent = ''; const page = document.createElement('div'); page.className = 'country-brief-page'; const header = document.createElement('div'); header.className = 'cb-header'; const headerLeft = document.createElement('div'); headerLeft.className = 'cb-header-left'; const flag = document.createElement('span'); flag.className = 'cb-flag'; flag.textContent = '\u26A0\uFE0F'; const title = document.createElement('span'); title.className = 'cb-country-name'; title.textContent = t('countryBrief.geocodeFailed'); headerLeft.append(flag, title); const headerRight = document.createElement('div'); headerRight.className = 'cb-header-right'; const closeX = document.createElement('button'); closeX.className = 'cb-close'; closeX.setAttribute('aria-label', t('components.newsPanel.close')); closeX.textContent = '\u00D7'; headerRight.append(closeX); header.append(headerLeft, headerRight); const body = document.createElement('div'); body.className = 'cb-body'; const errorWrap = document.createElement('div'); errorWrap.className = 'cb-geo-error'; const actions = document.createElement('div'); actions.className = 'cb-geo-error-actions'; const retryBtn = document.createElement('button'); retryBtn.className = 'cb-geo-retry-btn'; retryBtn.textContent = t('countryBrief.retryBtn'); retryBtn.addEventListener('click', () => onRetry(), { once: true }); const closeBtn = document.createElement('button'); closeBtn.className = 'cb-geo-close-btn'; closeBtn.textContent = t('countryBrief.closeBtn'); closeBtn.addEventListener('click', () => this.hide(), { once: true }); actions.append(retryBtn, closeBtn); errorWrap.append(actions); body.append(errorWrap); page.append(header, body); this.overlay.append(page); this.overlay.classList.add('active'); } public get signal(): AbortSignal { return this.abortController.signal; } public show(country: string, code: string, score: CountryScore | null, signals: CountryBriefSignals): void { this.abortController.abort(); this.abortController = new AbortController(); this.currentCode = code; this.currentName = country; this.currentScore = score; this.currentSignals = signals; this.currentBrief = null; this.currentBriefGeneratedAt = null; this.currentBriefCached = null; this.currentHeadlines = []; this.currentHeadlineCount = 0; const flag = this.countryFlag(code); const tierBadge = !signals.isTier1 ? `${t('modals.countryBrief.limitedCoverage')}` : ''; setTrustedHtml(this.overlay, trustedHtml(`
${flag} ${escapeHtml(country)} ${score ? this.levelBadge(score.level) : ''} ${score ? this.trendIndicator(score.trend) : ''} ${tierBadge}
${score ? `

${t('modals.countryBrief.instabilityIndex')}

${this.scoreRing(score.score, score.level)}
${this.componentBars(score.components)}
` : signals.isTier1 ? '' : `

${t('modals.countryBrief.instabilityIndex')}

๐Ÿ“Š ${t('modals.countryBrief.notTracked', { country: escapeHtml(country) })}
`}

${t('modals.countryBrief.intelBrief')}

${t('modals.countryBrief.generatingBrief')}

${t('modals.countryBrief.activeSignals')}

${this.signalChips(signals)}

${t('modals.countryBrief.timeline')}

${t('modals.countryBrief.predictionMarkets')}

${t('modals.countryBrief.loadingMarkets')}
`, "legacy direct innerHTML migration")); // All button click handlers (close, share, print, export, citation, link-share) are handled // via event delegation on the overlay (set up in constructor) this.overlay.classList.add('active'); } public updateBrief(data: CountryIntelData): void { if (data.code !== this.currentCode) return; const section = this.overlay.querySelector('.cb-brief-content'); if (!section) return; if (data.error || data.skipped || !data.brief) { const msg = data.error || data.reason || t('modals.countryBrief.briefUnavailable'); setTrustedHtml(section, trustedHtml(`
${escapeHtml(msg)}
`, "legacy direct innerHTML migration")); return; } this.currentBrief = data.brief; this.currentBriefGeneratedAt = data.generatedAt ?? null; this.currentBriefCached = data.cached === true; const briefSources = collectBriefSources(data.sources ?? [], 6); const formatted = this.formatBrief(data.brief, briefSources, this.currentHeadlineCount); const sourcesFooter = renderBriefSourcesFooter(briefSources, { className: 'cb-brief-sources' }); setTrustedHtml(section, trustedHtml(`
${formatted}
${sourcesFooter} `, "legacy direct innerHTML migration")); } public updateMarkets(markets: PredictionMarket[]): void { const section = this.overlay.querySelector('.cb-markets-content'); if (!section) return; if (markets.length === 0) { setTrustedHtml(section, trustedHtml(`${t('modals.countryBrief.noMarkets')}`, "legacy direct innerHTML migration")); return; } setTrustedHtml(section, trustedHtml(markets.slice(0, 3).map(m => { const pct = Math.round(m.yesPrice); const noPct = 100 - pct; const vol = m.volume ? `$${(m.volume / 1000).toFixed(0)}k vol` : ''; const safeUrl = sanitizeUrl(m.url || ''); const link = safeUrl ? ` โ†—` : ''; return `
${escapeHtml(m.title.slice(0, 100))}${link}
${pct}%
${noPct > 15 ? noPct + '%' : ''}
${vol ? `
${vol}
` : ''}
`; }).join(''), "legacy direct innerHTML migration")); } public updateStock(data: StockIndexData): void { const el = this.overlay.querySelector('.stock-loading'); if (!el) return; if (!data.available) { el.remove(); return; } const pct = parseFloat(data.weekChangePercent); const sign = pct >= 0 ? '+' : ''; const cls = pct >= 0 ? 'stock-up' : 'stock-down'; const arrow = pct >= 0 ? '๐Ÿ“ˆ' : '๐Ÿ“‰'; el.className = `signal-chip stock ${cls}`; setTrustedHtml(el, trustedHtml(`${arrow} ${escapeHtml(data.indexName)}: ${sign}${data.weekChangePercent}% (1W)`, "legacy direct innerHTML migration")); } public updateNews(headlines: NewsItem[]): void { const section = this.overlay.querySelector('.cb-news-section') as HTMLElement | null; const content = this.overlay.querySelector('.cb-news-content'); if (!section || !content || headlines.length === 0) return; const items = headlines.slice(0, 8); this.currentHeadlineCount = items.length; this.currentHeadlines = items; section.style.display = ''; setTrustedHtml(content, trustedHtml(items.map((item, i) => { const safeUrl = sanitizeUrl(item.link); const threatColor = item.threat?.level === 'critical' ? getCSSColor('--threat-critical') : item.threat?.level === 'high' ? getCSSColor('--threat-high') : item.threat?.level === 'medium' ? getCSSColor('--threat-medium') : getCSSColor('--threat-info'); const timeAgo = this.timeAgo(item.pubDate); const cardBody = `
${escapeHtml(item.title)}
${escapeHtml(item.source)} ยท ${timeAgo}
`; if (safeUrl) { return `${cardBody}`; } return `
${cardBody}
`; }).join(''), "legacy direct innerHTML migration")); } public updateInfrastructure(countryCode: string): void { const bounds = CountryBriefPage.BRIEF_BOUNDS[countryCode]; if (!bounds) return; const centroidLat = (bounds.n + bounds.s) / 2; const centroidLon = (bounds.e + bounds.w) / 2; const assets = getNearbyInfrastructure(centroidLat, centroidLon, ['pipeline', 'cable', 'datacenter', 'base', 'nuclear']); const nearbyPorts = PORTS .map((p: Port) => ({ port: p, dist: haversineDistanceKm(centroidLat, centroidLon, p.lat, p.lon) })) .filter(({ dist }) => dist <= 600) .sort((a, b) => a.dist - b.dist) .slice(0, 5); const grouped = new Map>(); for (const a of assets) { const list = grouped.get(a.type) || []; list.push({ name: a.name, distanceKm: a.distanceKm }); grouped.set(a.type, list); } if (nearbyPorts.length > 0) { grouped.set('port', nearbyPorts.map(({ port, dist }) => ({ name: port.name, distanceKm: dist }))); } if (grouped.size === 0) return; const section = this.overlay.querySelector('.cb-infra-section') as HTMLElement | null; const content = this.overlay.querySelector('.cb-infra-content'); if (!section || !content) return; const order: BriefAssetType[] = ['pipeline', 'cable', 'datacenter', 'base', 'nuclear', 'port']; let html = ''; for (const type of order) { const items = grouped.get(type); if (!items || items.length === 0) continue; const icon = CountryBriefPage.INFRA_ICONS[type]; const key = CountryBriefPage.INFRA_LABELS[type]; const label = t(`modals.countryBrief.infra.${key}`); html += `
`; html += `
${icon} ${label}
`; for (const item of items) { html += `
${escapeHtml(item.name)}${Math.round(item.distanceKm)} km
`; } html += `
`; } setTrustedHtml(content, trustedHtml(html, "legacy direct innerHTML migration")); section.style.display = ''; } public getTimelineMount(): HTMLElement | null { return this.overlay.querySelector('.cb-timeline-mount'); } public getCode(): string | null { return this.currentCode; } public getName(): string | null { return this.currentName; } private timeAgo(date: Date): string { const ms = Date.now() - new Date(date).getTime(); const hours = Math.floor(ms / 3600000); if (hours < 1) return t('modals.countryBrief.timeAgo.m', { count: Math.floor(ms / 60000) }); if (hours < 24) return t('modals.countryBrief.timeAgo.h', { count: hours }); return t('modals.countryBrief.timeAgo.d', { count: Math.floor(hours / 24) }); } private formatBrief(text: string, sources: BriefSource[] = [], headlineCount = 0): string { return formatIntelBrief( text, sources.length > 0 ? { sources } : headlineCount > 0 ? { count: headlineCount, hrefPrefix: '#cb-news-' } : undefined, ); } private exportBrief(format: 'json' | 'csv' | 'evidence-md'): void { if (!this.currentCode || !this.currentName) return; if (format === 'evidence-md' && !this.canExportEvidenceBundle()) return; const exportedAt = new Date().toISOString(); const data: CountryBriefExport & CountryEvidenceBundleInput = { country: this.currentName, code: this.currentCode, context: 'Country dossier', generatedAt: exportedAt, exportedAt, }; if (this.currentScore) { data.score = this.currentScore.score; data.level = this.currentScore.level; data.trend = this.currentScore.trend; data.components = this.currentScore.components; } if (this.currentSignals) { data.signals = { criticalNews: this.currentSignals.criticalNews, protests: this.currentSignals.protests, militaryFlights: this.currentSignals.militaryFlights, militaryVessels: this.currentSignals.militaryVessels, outages: this.currentSignals.outages, aisDisruptions: this.currentSignals.aisDisruptions, satelliteFires: this.currentSignals.satelliteFires, radiationAnomalies: this.currentSignals.radiationAnomalies, temporalAnomalies: this.currentSignals.temporalAnomalies, cyberThreats: this.currentSignals.cyberThreats, earthquakes: this.currentSignals.earthquakes, displacementOutflow: this.currentSignals.displacementOutflow, climateStress: this.currentSignals.climateStress, conflictEvents: this.currentSignals.conflictEvents, activeStrikes: this.currentSignals.activeStrikes, orefSirens: this.currentSignals.orefSirens, orefHistory24h: this.currentSignals.orefHistory24h, aviationDisruptions: this.currentSignals.aviationDisruptions, travelAdvisories: this.currentSignals.travelAdvisories, travelAdvisoryMaxLevel: this.currentSignals.travelAdvisoryMaxLevel, gpsJammingHexes: this.currentSignals.gpsJammingHexes, }; } if (this.currentBrief) data.brief = this.currentBrief; if (this.currentBriefGeneratedAt) data.briefGeneratedAt = new Date(this.currentBriefGeneratedAt).toISOString(); if (this.currentBriefCached != null) data.briefCached = this.currentBriefCached; if (this.currentHeadlines.length > 0) { data.headlines = this.currentHeadlines.map(h => ({ title: h.title, source: h.source, link: h.link, pubDate: h.pubDate ? new Date(h.pubDate).toISOString() : undefined, })); } if (format === 'evidence-md') exportCountryEvidenceMarkdown(data); else if (format === 'json') exportCountryBriefJSON(data); else exportCountryBriefCSV(data); } /** * U5: the structured-data exports (JSON/CSV) share the dashboard export * gate. The print button, the image export and the print-based PDF stay * free โ€” they carry no machine-readable payload โ€” and the evidence bundle * keeps its own Pro gate below. */ private syncStructuredExportOptions(): void { const authState = getAuthState(); const verdict = evaluateExportGate(authState); // Keep the locked rows visible so they remain an entry point to the // billing-aware gate. Once unlocked, the catalog is the format allowlist. const availableFormats = verdict.locked ? null : new Set(evaluateAvailableExportFormats(authState)); this.overlay .querySelectorAll('.cb-export-option') .forEach((button) => { const format = button.dataset.format; if (format !== 'json' && format !== 'csv') return; button.hidden = availableFormats !== null && !availableFormats.has(format); }); } private canExportStructuredData(format: 'json' | 'csv'): boolean { const authState = getAuthState(); const verdict = evaluateExportGate(authState); if (!verdict.locked) { if (verdict.pendingActivation) void primeExportGateActivation(); // Re-evaluate at click time so a stale/open menu cannot bypass a live // entitlement change. return evaluateAvailableExportFormats(authState).includes(format); } trackGateHit('export'); showToast(exportGateCopy(exportLockToGateReason(verdict.reason)).desc); return false; } private canExportEvidenceBundle(): boolean { if (hasPremiumAccess(getAuthState())) return true; trackGateHit('evidence-export'); showToast('Evidence export is available on Pro.'); return false; } private exportPdf(): void { const content = this.overlay.querySelector('.cb-body'); const header = this.overlay.querySelector('.cb-header'); if (!content) return; const iframe = document.createElement('iframe'); iframe.style.cssText = 'position:fixed;left:-9999px;width:0;height:0;border:none'; document.body.appendChild(iframe); const doc = iframe.contentDocument || iframe.contentWindow?.document; if (!doc) { document.body.removeChild(iframe); return; } const styles = Array.from(document.querySelectorAll('link[rel="stylesheet"], style')) .map(el => el.outerHTML).join('\n'); doc.open(); doc.write(`${styles} ${header ? header.outerHTML : ''}${content.outerHTML}`); doc.close(); if (iframe.contentWindow) { iframe.contentWindow.onafterprint = () => document.body.removeChild(iframe); } setTimeout(() => { if (iframe.contentWindow) { iframe.contentWindow.print(); } setTimeout(() => { if (iframe.parentNode) document.body.removeChild(iframe); }, 5000); }, 300); } public hide(): void { this.abortController.abort(); this.overlay.classList.remove('active'); this.currentCode = null; this.currentName = null; this.onCloseCallback?.(); } public onClose(cb: () => void): void { this.onCloseCallback = cb; } public isVisible(): boolean { return this.overlay.classList.contains('active'); } }