import { Panel } from './Panel'; import { escapeHtml, unsafeRawHtml } from '@/utils/sanitize'; interface CrossSourceSignal { id: string; type: string; theater: string; summary: string; severity: string; severityScore: number; detectedAt: number; contributingTypes: string[]; signalCount: number; } interface CrossSourceSignalsData { signals: CrossSourceSignal[]; evaluatedAt: number; compositeCount: number; } const SEVERITY_COLOR: Record = { CROSS_SOURCE_SIGNAL_SEVERITY_CRITICAL: 'var(--semantic-critical)', CROSS_SOURCE_SIGNAL_SEVERITY_HIGH: '#ff8c8c', CROSS_SOURCE_SIGNAL_SEVERITY_MEDIUM: 'var(--yellow)', CROSS_SOURCE_SIGNAL_SEVERITY_LOW: 'var(--text-dim)', }; const SEVERITY_LABEL: Record = { CROSS_SOURCE_SIGNAL_SEVERITY_CRITICAL: 'CRITICAL', CROSS_SOURCE_SIGNAL_SEVERITY_HIGH: 'HIGH', CROSS_SOURCE_SIGNAL_SEVERITY_MEDIUM: 'MED', CROSS_SOURCE_SIGNAL_SEVERITY_LOW: 'LOW', }; // Filled badge styles: bg + border + text per severity const SEVERITY_BADGE_STYLE: Record = { CROSS_SOURCE_SIGNAL_SEVERITY_CRITICAL: 'background:var(--semantic-critical);color:#fff;border:1px solid var(--semantic-critical)', CROSS_SOURCE_SIGNAL_SEVERITY_HIGH: 'background:rgba(255,140,140,0.15);color:#ff8c8c;border:1px solid rgba(255,140,140,0.4)', CROSS_SOURCE_SIGNAL_SEVERITY_MEDIUM: 'background:rgba(245,197,66,0.08);color:var(--yellow);border:1px solid rgba(245,197,66,0.35)', CROSS_SOURCE_SIGNAL_SEVERITY_LOW: 'background:transparent;color:var(--text-dim);border:1px solid var(--border)', }; const TYPE_LABEL: Record = { CROSS_SOURCE_SIGNAL_TYPE_COMPOSITE_ESCALATION: 'COMPOSITE', CROSS_SOURCE_SIGNAL_TYPE_THERMAL_SPIKE: 'THERMAL', CROSS_SOURCE_SIGNAL_TYPE_GPS_JAMMING: 'GPS JAM', CROSS_SOURCE_SIGNAL_TYPE_MILITARY_FLIGHT_SURGE: 'MIL FLTX', CROSS_SOURCE_SIGNAL_TYPE_UNREST_SURGE: 'UNREST', CROSS_SOURCE_SIGNAL_TYPE_OREF_ALERT_CLUSTER: 'ADVISORY', CROSS_SOURCE_SIGNAL_TYPE_VIX_SPIKE: 'VIX', CROSS_SOURCE_SIGNAL_TYPE_COMMODITY_SHOCK: 'COMDTY', CROSS_SOURCE_SIGNAL_TYPE_CYBER_ESCALATION: 'CYBER', CROSS_SOURCE_SIGNAL_TYPE_SHIPPING_DISRUPTION: 'SHIPPING', CROSS_SOURCE_SIGNAL_TYPE_SANCTIONS_SURGE: 'SANCTIONS', CROSS_SOURCE_SIGNAL_TYPE_EARTHQUAKE_SIGNIFICANT: 'QUAKE', CROSS_SOURCE_SIGNAL_TYPE_RADIATION_ANOMALY: 'RADIATION', CROSS_SOURCE_SIGNAL_TYPE_INFRASTRUCTURE_OUTAGE: 'INFRA', CROSS_SOURCE_SIGNAL_TYPE_WILDFIRE_ESCALATION: 'WILDFIRE', CROSS_SOURCE_SIGNAL_TYPE_DISPLACEMENT_SURGE: 'DISPLCMT', CROSS_SOURCE_SIGNAL_TYPE_FORECAST_DETERIORATION: 'FORECAST', CROSS_SOURCE_SIGNAL_TYPE_MARKET_STRESS: 'MARKET', CROSS_SOURCE_SIGNAL_TYPE_WEATHER_EXTREME: 'WEATHER', CROSS_SOURCE_SIGNAL_TYPE_MEDIA_TONE_DETERIORATION: 'MEDIA', CROSS_SOURCE_SIGNAL_TYPE_RISK_SCORE_SPIKE: 'RISK', }; // Category icon prefix for type badges const TYPE_ICON: Record = { CROSS_SOURCE_SIGNAL_TYPE_COMPOSITE_ESCALATION: 'โšก', CROSS_SOURCE_SIGNAL_TYPE_THERMAL_SPIKE: '๐Ÿ”ด', CROSS_SOURCE_SIGNAL_TYPE_EARTHQUAKE_SIGNIFICANT: '๐Ÿ”ด', CROSS_SOURCE_SIGNAL_TYPE_RADIATION_ANOMALY: '๐Ÿ”ด', CROSS_SOURCE_SIGNAL_TYPE_WILDFIRE_ESCALATION: '๐Ÿ”ด', CROSS_SOURCE_SIGNAL_TYPE_GPS_JAMMING: '๐Ÿ“ก', CROSS_SOURCE_SIGNAL_TYPE_CYBER_ESCALATION: '๐Ÿ“ก', CROSS_SOURCE_SIGNAL_TYPE_MILITARY_FLIGHT_SURGE: 'โœˆ๏ธ', CROSS_SOURCE_SIGNAL_TYPE_VIX_SPIKE: '๐Ÿ“Š', CROSS_SOURCE_SIGNAL_TYPE_COMMODITY_SHOCK: '๐Ÿ“Š', CROSS_SOURCE_SIGNAL_TYPE_MARKET_STRESS: '๐Ÿ“Š', CROSS_SOURCE_SIGNAL_TYPE_RISK_SCORE_SPIKE: '๐Ÿ“Š', }; const TYPE_ICON_DEFAULT = 'โš ๏ธ'; export class CrossSourceSignalsPanel extends Panel { private signals: CrossSourceSignal[] = []; private evaluatedAt: Date | null = null; private compositeCount = 0; constructor() { super({ id: 'cross-source-signals', title: 'Cross-Source Signal Aggregator', showCount: true, trackActivity: true, infoTooltip: 'Aggregates 15+ real-time data streams every 15 minutes. Ranks cross-domain signals by severity and detects composite escalation when 3 or more signal categories co-fire in the same theater.', }); // Inject keyframe once โ€” used by the composite banner pulse dot const style = document.createElement('style'); style.textContent = '@keyframes cross-source-pulse-dot{0%,100%{opacity:1}50%{opacity:.15}}'; document.head.appendChild(style); this.showLoading('Loading signal data...'); } public setData(data: CrossSourceSignalsData): void { this.signals = data.signals ?? []; this.evaluatedAt = data.evaluatedAt ? new Date(data.evaluatedAt) : null; this.compositeCount = data.compositeCount ?? 0; this.setCount(this.signals.length); this.resetRetryBackoff(); this.render(); } public showFetchError(): void { this.showError('Signal data unavailable โ€” upstream feeds unreachable.', () => {/* refreshed by scheduler */}); } private ageSuffix(ts: number): string { const diffMs = Date.now() - ts; const mins = Math.floor(diffMs / 60000); if (mins < 2) return 'just now'; if (mins < 60) return `${mins}m ago`; return `${Math.floor(mins / 60)}h ago`; } private renderSignal(sig: CrossSourceSignal, index: number): string { const isComposite = sig.type === 'CROSS_SOURCE_SIGNAL_TYPE_COMPOSITE_ESCALATION'; const sevColor = SEVERITY_COLOR[sig.severity] ?? 'var(--text-dim)'; const sevBadgeStyle = SEVERITY_BADGE_STYLE[sig.severity] ?? SEVERITY_BADGE_STYLE.CROSS_SOURCE_SIGNAL_SEVERITY_LOW; const typeLabel = TYPE_LABEL[sig.type] ?? sig.type.replace('CROSS_SOURCE_SIGNAL_TYPE_', ''); const typeIcon = TYPE_ICON[sig.type] ?? TYPE_ICON_DEFAULT; const age = this.ageSuffix(sig.detectedAt); const cardStyle = isComposite ? 'box-shadow:0 0 0 1px rgba(255,80,80,0.3),0 2px 8px rgba(255,80,80,0.08);border-color:rgba(255,80,80,0.25)' : 'border:1px solid var(--border)'; const contributors = isComposite && sig.contributingTypes.length > 0 ? `
${ sig.contributingTypes.slice(0, 5).map(t => `${escapeHtml(t)}` ).join('') }
` : ''; return `
${index + 1}
${typeIcon}${escapeHtml(typeLabel)} ${escapeHtml(SEVERITY_LABEL[sig.severity] ?? '')} ${escapeHtml(sig.theater)} ยท ${escapeHtml(age)}
${escapeHtml(sig.summary)}
${contributors}
`; } private render(): void { if (this.signals.length === 0) { if (!this.evaluatedAt) { this.showError('Signal aggregator is initializing. First evaluation runs within 15 minutes.', () => {/* refreshed by scheduler */}); } else { this.setSafeContent(unsafeRawHtml('
No cross-source signals detected.
', 'legacy Panel.setContent() migration')); } return; } const evalTime = this.evaluatedAt ? `Evaluated ${this.evaluatedAt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}` : ''; const compositeNote = this.compositeCount > 0 ? `
${this.compositeCount} composite escalation zone${this.compositeCount > 1 ? 's' : ''} detected
` : ''; const signalRows = this.signals.map((s, i) => this.renderSignal(s, i)).join(''); this.setSafeContent(unsafeRawHtml(`
${compositeNote} ${signalRows} ${evalTime ? `
${escapeHtml(evalTime)}
` : ''}
`, 'legacy Panel.setContent() migration')); } }