import { GitBranch, Loader2, RefreshCw, ZoomIn, ZoomOut } from 'lucide-react'; import { useCallback, useEffect, useRef, useState } from 'react'; interface GraphNode { id: string; label: string; type: 'domain' | 'entity' | 'signal'; domain: string; severity?: 'critical' | 'high' | 'medium' | 'low' | 'info'; value?: number; description?: string; live?: boolean; x?: number; y?: number; vx?: number; vy?: number; } interface GraphEdge { id: string; source: string; target: string; label: string; strength: number; type: 'causal' | 'correlative' | 'escalation' | 'dependency'; description: string; lastActive: number; } const DOMAIN_COLORS: Record = { vessels: 'var(--gi-accent-blue)', aegis: '#ef4444', firestorm: '#ef4444', terra: '#22c55e', prism: '#8b5cf6', lyte: '#f59e0b', 'szl-holdings': '#8b7ac8', carlota: '#ec4899', }; const SEVERITY_COLORS: Record = { critical: '#ef4444', high: '#f59e0b', medium: '#3b82f6', low: '#6b7280', info: '#22c55e', }; const EDGE_COLORS: Record = { causal: '#ef4444', escalation: '#f59e0b', correlative: '#3b82f6', dependency: '#6b7280', }; const DEMO_NODES: GraphNode[] = [ { id: 'domain-vessels', label: 'SEXTANT', type: 'domain', domain: 'vessels', description: 'Maritime fleet operations across 14 active vessels', }, { id: 'domain-aegis', label: "PARAGON", type: 'domain', domain: 'aegis', description: 'Cybersecurity posture and threat intelligence', }, { id: 'domain-terra', label: 'DOMAINE', type: 'domain', domain: 'terra', description: 'Real estate portfolio — 127 assets across 9 markets', }, { id: 'domain-prism', label: 'PRAXIS', type: 'domain', domain: 'prism', description: 'Cross-domain intelligence and pattern detection', }, { id: 'domain-lyte', label: 'KORA', type: 'domain', domain: 'lyte', description: 'Platform reliability and SLO management', }, { id: 'domain-szl', label: 'Holdings', type: 'domain', domain: 'szl-holdings', description: 'SZL Holdings executive overview', }, { id: 'entity-mv-meridian', label: 'MV Meridian', type: 'entity', domain: 'vessels', severity: 'medium', description: 'Vessel delayed — Bay of Bengal corridor', }, { id: 'entity-sg-port', label: 'SG Port Auth', type: 'entity', domain: 'aegis', severity: 'low', description: 'Singapore port authority access event', }, { id: 'entity-dfw-7', label: 'DFW-Industrial-7', type: 'entity', domain: 'terra', severity: 'low', description: 'Asset appreciation signal — DFW corridor', }, { id: 'signal-fleet-eta', label: 'Fleet ETA Gap', type: 'signal', domain: 'vessels', severity: 'medium', value: 0.78, }, { id: 'signal-perimeter', label: 'Perimeter Scan', type: 'signal', domain: 'aegis', severity: 'info', value: 0.45, }, { id: 'signal-market-vol', label: 'Market Volatility', type: 'signal', domain: 'szl-holdings', severity: 'medium', value: 0.72, }, { id: 'signal-pattern', label: 'Pattern Match', type: 'signal', domain: 'prism', severity: 'low', value: 0.61, }, ]; const DEMO_EDGES: GraphEdge[] = [ { id: 'e1', source: 'domain-vessels', target: 'entity-mv-meridian', label: 'contains', strength: 0.9, type: 'dependency', description: 'MV Meridian is part of the active fleet', lastActive: Date.now() - 3600000, }, { id: 'e2', source: 'entity-mv-meridian', target: 'signal-fleet-eta', label: 'triggers', strength: 0.78, type: 'causal', description: 'Vessel delay caused ETA compliance gap signal', lastActive: Date.now() - 1800000, }, { id: 'e3', source: 'signal-fleet-eta', target: 'domain-terra', label: 'impacts', strength: 0.55, type: 'correlative', description: 'Port delays correlate with DFW property logistics', lastActive: Date.now() - 900000, }, { id: 'e4', source: 'domain-aegis', target: 'entity-sg-port', label: 'monitors', strength: 0.82, type: 'dependency', description: 'PARAGON monitors Singapore port authority access', lastActive: Date.now() - 7200000, }, { id: 'e5', source: 'entity-sg-port', target: 'domain-vessels', label: 'correlates', strength: 0.63, type: 'correlative', description: 'SG port access events correlate with fleet routing', lastActive: Date.now() - 5400000, }, { id: 'e6', source: 'signal-market-vol', target: 'domain-terra', label: 'escalates', strength: 0.71, type: 'escalation', description: 'Volatility index crossing threshold escalates asset review', lastActive: Date.now() - 3600000, }, { id: 'e7', source: 'domain-prism', target: 'signal-pattern', label: 'detects', strength: 0.88, type: 'dependency', description: 'PRAXIS intelligence layer detected cross-domain pattern', lastActive: Date.now() - 2700000, }, { id: 'e8', source: 'signal-pattern', target: 'domain-aegis', label: 'informs', strength: 0.67, type: 'correlative', description: 'Pattern detection informs PARAGON threat posture', lastActive: Date.now() - 2700000, }, { id: 'e9', source: 'domain-szl', target: 'signal-market-vol', label: 'emits', strength: 0.72, type: 'causal', description: 'Holdings portfolio movement emits volatility signal', lastActive: Date.now() - 3600000, }, { id: 'e10', source: 'entity-dfw-7', target: 'domain-terra', label: 'belongs', strength: 0.9, type: 'dependency', description: 'DFW-Industrial-7 is part of the DOMAINE portfolio', lastActive: Date.now() - 86400000, }, ]; const DEMO_STATS = { strongCorrelations: 4, activeEdges: 7 }; function layoutNodes( nodes: GraphNode[], edges: GraphEdge[], width: number, height: number, ): GraphNode[] { const domainNodes = nodes.filter((n) => n.type === 'domain'); const otherNodes = nodes.filter((n) => n.type !== 'domain'); const cx = width / 2; const cy = height / 2; const domainRadius = Math.min(width, height) * 0.28; const entityRadius = Math.min(width, height) * 0.44; const placed = new Map(); domainNodes.forEach((n, i) => { const angle = (2 * Math.PI * i) / domainNodes.length - Math.PI / 2; placed.set(n.id, { x: cx + domainRadius * Math.cos(angle), y: cy + domainRadius * Math.sin(angle), }); }); otherNodes.forEach((n) => { const _connectedDomain = edges.find((e) => { const srcNode = nodes.find((nd) => nd.id === e.source); const tgtNode = nodes.find((nd) => nd.id === e.target); return ( (e.source === n.id || e.target === n.id) && (srcNode?.type === 'domain' || tgtNode?.type === 'domain') ); }); const domainMatch = nodes.find((nd) => nd.type === 'domain' && nd.domain === n.domain); const domainPos = domainMatch ? placed.get(domainMatch.id) : null; if (domainPos) { const _angle = Math.random() * 2 * Math.PI; const r = entityRadius * (0.85 + Math.random() * 0.15); const dp = placed.get(domainMatch?.id ?? '')!; const angleFromCenter = Math.atan2(dp.y - cy, dp.x - cx); const spread = 0.6; const finalAngle = angleFromCenter + (Math.random() - 0.5) * spread; placed.set(n.id, { x: cx + r * Math.cos(finalAngle), y: cy + r * Math.sin(finalAngle), }); } else { placed.set(n.id, { x: cx + (Math.random() - 0.5) * width * 0.7, y: cy + (Math.random() - 0.5) * height * 0.7, }); } }); return nodes.map((n) => ({ ...n, ...placed.get(n.id) })); } interface CorrelationMapVizProps { apiBase?: string; } export function CorrelationMapViz({ apiBase = '' }: CorrelationMapVizProps) { const [nodes, setNodes] = useState([]); const [edges, setEdges] = useState([]); const [stats, setStats] = useState>({}); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [isDemo, setIsDemo] = useState(null); const [selectedNode, setSelectedNode] = useState(null); const [hoveredEdge, setHoveredEdge] = useState(null); const [zoom, setZoom] = useState(1); const [filter, setFilter] = useState<'all' | 'domain' | 'entity' | 'signal'>('all'); const svgRef = useRef(null); const [dimensions, setDimensions] = useState({ w: 800, h: 500 }); useEffect(() => { const obs = new ResizeObserver((entries) => { const entry = entries[0]; if (entry) { setDimensions({ w: entry.contentRect.width, h: Math.max(480, entry.contentRect.height) }); } }); if (svgRef.current?.parentElement) obs.observe(svgRef.current.parentElement); return () => obs.disconnect(); }, []); const fetchGraph = useCallback(async () => { setLoading(true); setError(null); try { const res = await fetch(`${apiBase}/api/correlation-map/live`); if (res.status === 401 || res.status === 403) { const laid = layoutNodes(DEMO_NODES, DEMO_EDGES, dimensions.w, dimensions.h); setNodes(laid); setEdges(DEMO_EDGES); setStats(DEMO_STATS); setIsDemo(true); return; } const data = await res.json(); if (data.success) { const laid = layoutNodes(data.nodes, data.edges, dimensions.w, dimensions.h); setNodes(laid); setEdges(data.edges); setStats(data.stats ?? {}); setIsDemo(false); } else { const laid = layoutNodes(DEMO_NODES, DEMO_EDGES, dimensions.w, dimensions.h); setNodes(laid); setEdges(DEMO_EDGES); setStats(DEMO_STATS); setIsDemo(true); } } catch { const laid = layoutNodes(DEMO_NODES, DEMO_EDGES, dimensions.w, dimensions.h); setNodes(laid); setEdges(DEMO_EDGES); setStats(DEMO_STATS); setIsDemo(true); } finally { setLoading(false); } }, [apiBase, dimensions.w, dimensions.h]); useEffect(() => { fetchGraph(); }, [fetchGraph]); const nodeMap = new Map(nodes.map((n) => [n.id, n])); const filteredNodes = filter === 'all' ? nodes : nodes.filter((n) => n.type === filter); const filteredNodeIds = new Set(filteredNodes.map((n) => n.id)); const filteredEdges = edges.filter( (e) => filteredNodeIds.has(e.source) && filteredNodeIds.has(e.target), ); function getNodeColor(n: GraphNode) { if (n.severity) return SEVERITY_COLORS[n.severity] ?? DOMAIN_COLORS[n.domain] ?? '#6b7280'; return DOMAIN_COLORS[n.domain] ?? '#6b7280'; } function getNodeRadius(n: GraphNode) { if (n.type === 'domain') return 22; if (n.type === 'signal') return 11; return 15; } return (

Correlation Map

{!loading && isDemo === true && ( Demo )} {!loading && isDemo === false && ( Live )} {!loading && ( {filteredNodes.length} nodes · {filteredEdges.length} edges )}
{(['all', 'domain', 'entity', 'signal'] as const).map((f) => ( ))}
{loading && (
Building correlation graph…
)} {error && !loading && (
{error}
)} {Object.entries(EDGE_COLORS).map(([type, color]) => ( ))} {filteredEdges.map((edge) => { const src = nodeMap.get(edge.source); const tgt = nodeMap.get(edge.target); if (src?.x == null || src.y == null || tgt?.x == null || tgt.y == null) return null; const color = EDGE_COLORS[edge.type] ?? '#6b7280'; const isHovered = hoveredEdge?.id === edge.id; const opacity = isHovered ? 0.9 : Math.max(0.15, edge.strength * 0.6); const sw = isHovered ? 2 : Math.max(0.5, edge.strength * 2); const mx = (src.x + tgt.x) / 2; const my = (src.y + tgt.y) / 2; const dx = tgt.x - src.x; const dy = tgt.y - src.y; const len = Math.sqrt(dx * dx + dy * dy); const cpx = mx - (dy / len) * 30; const cpy = my + (dx / len) * 30; return ( setHoveredEdge(edge)} onMouseLeave={() => setHoveredEdge(null)} /> {isHovered && ( {edge.label} )} ); })} {filteredNodes.map((node) => { if (!node.x || !node.y) return null; const color = getNodeColor(node); const r = getNodeRadius(node); const isSelected = selectedNode?.id === node.id; return ( setSelectedNode(isSelected ? null : node)} > {isSelected && ( )} {node.live && ( )} {node.live && Live: real-time data from {node.domain} database} {node.type === 'signal' && } {node.label.length > 14 ? `${node.label.slice(0, 13)}…` : node.label} ); })} {selectedNode && (
{selectedNode.type} {selectedNode.live && ( Live )}
{selectedNode.label}
{selectedNode.description && (

{selectedNode.description}

)} {selectedNode.value !== undefined && (
Signal strength:{' '} {Math.round(selectedNode.value * 100)}
)}
)} {hoveredEdge && !selectedNode && (
{hoveredEdge.type} · {Math.round(hoveredEdge.strength * 100)}% strength
{hoveredEdge.description}
)}
Edge types: {Object.entries(EDGE_COLORS).map(([type, color]) => (
{type}
))}
{Object.keys(stats).length > 0 && (
{stats.strongCorrelations} strong correlations · {stats.activeEdges} active edges
)}
); }