1008 lines
		
	
	
		
			32 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1008 lines
		
	
	
		
			32 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/**
 | 
						|
 * Graph visualization module for DNSRecon
 | 
						|
 * Handles network graph rendering using vis.js with enhanced Phase 2 features
 | 
						|
 */
 | 
						|
 | 
						|
class GraphManager {
 | 
						|
    constructor(containerId) {
 | 
						|
        this.container = document.getElementById(containerId);
 | 
						|
        this.network = null;
 | 
						|
        this.nodes = new vis.DataSet();
 | 
						|
        this.edges = new vis.DataSet();
 | 
						|
        this.isInitialized = false;
 | 
						|
        this.currentLayout = 'physics';
 | 
						|
        this.nodeInfoPopup = null;
 | 
						|
 | 
						|
        // Enhanced graph options for Phase 2
 | 
						|
        this.options = {
 | 
						|
            nodes: {
 | 
						|
                shape: 'dot',
 | 
						|
                size: 15,
 | 
						|
                font: {
 | 
						|
                    size: 12,
 | 
						|
                    color: '#c7c7c7',
 | 
						|
                    face: 'Roboto Mono, monospace',
 | 
						|
                    background: 'rgba(26, 26, 26, 0.9)',
 | 
						|
                    strokeWidth: 2,
 | 
						|
                    strokeColor: '#000000'
 | 
						|
                },
 | 
						|
                borderWidth: 2,
 | 
						|
                borderColor: '#444',
 | 
						|
                shadow: {
 | 
						|
                    enabled: true,
 | 
						|
                    color: 'rgba(0, 0, 0, 0.5)',
 | 
						|
                    size: 5,
 | 
						|
                    x: 2,
 | 
						|
                    y: 2
 | 
						|
                },
 | 
						|
                scaling: {
 | 
						|
                    min: 10,
 | 
						|
                    max: 30,
 | 
						|
                    label: {
 | 
						|
                        enabled: true,
 | 
						|
                        min: 8,
 | 
						|
                        max: 16
 | 
						|
                    }
 | 
						|
                },
 | 
						|
                chosen: {
 | 
						|
                    node: (values, id, selected, hovering) => {
 | 
						|
                        values.borderColor = '#00ff41';
 | 
						|
                        values.borderWidth = 3;
 | 
						|
                        values.shadow = true;
 | 
						|
                        values.shadowColor = 'rgba(0, 255, 65, 0.6)';
 | 
						|
                        values.shadowSize = 10;
 | 
						|
                    }
 | 
						|
                }
 | 
						|
            },
 | 
						|
            edges: {
 | 
						|
                width: 2,
 | 
						|
                color: {
 | 
						|
                    color: '#555',
 | 
						|
                    highlight: '#00ff41',
 | 
						|
                    hover: '#ff9900',
 | 
						|
                    inherit: false
 | 
						|
                },
 | 
						|
                font: {
 | 
						|
                    size: 10,
 | 
						|
                    color: '#999',
 | 
						|
                    face: 'Roboto Mono, monospace',
 | 
						|
                    background: 'rgba(26, 26, 26, 0.8)',
 | 
						|
                    strokeWidth: 1,
 | 
						|
                    strokeColor: '#000000'
 | 
						|
                },
 | 
						|
                arrows: {
 | 
						|
                    to: {
 | 
						|
                        enabled: true,
 | 
						|
                        scaleFactor: 1,
 | 
						|
                        type: 'arrow'
 | 
						|
                    }
 | 
						|
                },
 | 
						|
                smooth: {
 | 
						|
                    enabled: true,
 | 
						|
                    type: 'dynamic',
 | 
						|
                    roundness: 0.6
 | 
						|
                },
 | 
						|
                shadow: {
 | 
						|
                    enabled: true,
 | 
						|
                    color: 'rgba(0, 0, 0, 0.3)',
 | 
						|
                    size: 3,
 | 
						|
                    x: 1,
 | 
						|
                    y: 1
 | 
						|
                },
 | 
						|
                chosen: {
 | 
						|
                    edge: (values, id, selected, hovering) => {
 | 
						|
                        values.color = '#00ff41';
 | 
						|
                        values.width = 4;
 | 
						|
                        values.shadow = true;
 | 
						|
                        values.shadowColor = 'rgba(0, 255, 65, 0.4)';
 | 
						|
                    }
 | 
						|
                }
 | 
						|
            },
 | 
						|
            physics: {
 | 
						|
                enabled: true,
 | 
						|
                stabilization: {
 | 
						|
                    enabled: true,
 | 
						|
                    iterations: 150,
 | 
						|
                    updateInterval: 50
 | 
						|
                },
 | 
						|
                barnesHut: {
 | 
						|
                    gravitationalConstant: -3000,
 | 
						|
                    centralGravity: 0.4,
 | 
						|
                    springLength: 120,
 | 
						|
                    springConstant: 0.05,
 | 
						|
                    damping: 0.1,
 | 
						|
                    avoidOverlap: 0.2
 | 
						|
                },
 | 
						|
                maxVelocity: 30,
 | 
						|
                minVelocity: 0.1,
 | 
						|
                solver: 'barnesHut',
 | 
						|
                timestep: 0.4,
 | 
						|
                adaptiveTimestep: true
 | 
						|
            },
 | 
						|
            interaction: {
 | 
						|
                hover: true,
 | 
						|
                hoverConnectedEdges: true,
 | 
						|
                selectConnectedEdges: true,
 | 
						|
                tooltipDelay: 300,
 | 
						|
                hideEdgesOnDrag: false,
 | 
						|
                hideNodesOnDrag: false,
 | 
						|
                zoomView: true,
 | 
						|
                dragView: true,
 | 
						|
                multiselect: true
 | 
						|
            },
 | 
						|
            layout: {
 | 
						|
                improvedLayout: true,
 | 
						|
                randomSeed: 2
 | 
						|
            }
 | 
						|
        };
 | 
						|
 | 
						|
        this.createNodeInfoPopup();
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Create floating node info popup
 | 
						|
     */
 | 
						|
    createNodeInfoPopup() {
 | 
						|
        this.nodeInfoPopup = document.createElement('div');
 | 
						|
        this.nodeInfoPopup.className = 'node-info-popup';
 | 
						|
        this.nodeInfoPopup.style.display = 'none';
 | 
						|
        document.body.appendChild(this.nodeInfoPopup);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Initialize the network graph with enhanced features
 | 
						|
     */
 | 
						|
    initialize() {
 | 
						|
        if (this.isInitialized) {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        try {
 | 
						|
            const data = {
 | 
						|
                nodes: this.nodes,
 | 
						|
                edges: this.edges
 | 
						|
            };
 | 
						|
 | 
						|
            this.network = new vis.Network(this.container, data, this.options);
 | 
						|
            this.setupNetworkEvents();
 | 
						|
            this.isInitialized = true;
 | 
						|
 | 
						|
            // Hide placeholder
 | 
						|
            const placeholder = this.container.querySelector('.graph-placeholder');
 | 
						|
            if (placeholder) {
 | 
						|
                placeholder.style.display = 'none';
 | 
						|
            }
 | 
						|
 | 
						|
            // Add graph controls
 | 
						|
            this.addGraphControls();
 | 
						|
 | 
						|
            console.log('Enhanced graph initialized successfully');
 | 
						|
        } catch (error) {
 | 
						|
            console.error('Failed to initialize graph:', error);
 | 
						|
            this.showError('Failed to initialize visualization');
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Add interactive graph controls
 | 
						|
     */
 | 
						|
    addGraphControls() {
 | 
						|
        const controlsContainer = document.createElement('div');
 | 
						|
        controlsContainer.className = 'graph-controls';
 | 
						|
        controlsContainer.innerHTML = `
 | 
						|
            <button class="graph-control-btn" id="graph-fit" title="Fit to Screen">[FIT]</button>
 | 
						|
            <button class="graph-control-btn" id="graph-reset" title="Reset View">[RESET]</button>
 | 
						|
            <button class="graph-control-btn" id="graph-physics" title="Toggle Physics">[PHYSICS]</button>
 | 
						|
            <button class="graph-control-btn" id="graph-cluster" title="Cluster Nodes">[CLUSTER]</button>
 | 
						|
            <button class="graph-control-btn" id="graph-clear" title="Clear Graph">[CLEAR]</button>
 | 
						|
        `;
 | 
						|
 | 
						|
        this.container.appendChild(controlsContainer);
 | 
						|
 | 
						|
        // Add control event listeners
 | 
						|
        document.getElementById('graph-fit').addEventListener('click', () => this.fitView());
 | 
						|
        document.getElementById('graph-reset').addEventListener('click', () => this.resetView());
 | 
						|
        document.getElementById('graph-physics').addEventListener('click', () => this.togglePhysics());
 | 
						|
        document.getElementById('graph-cluster').addEventListener('click', () => this.toggleClustering());
 | 
						|
        document.getElementById('graph-clear').addEventListener('click', () => this.clear());
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Setup enhanced network event handlers
 | 
						|
     */
 | 
						|
    setupNetworkEvents() {
 | 
						|
        if (!this.network) return;
 | 
						|
 | 
						|
        // Node click event with enhanced details
 | 
						|
        this.network.on('click', (params) => {
 | 
						|
            if (params.nodes.length > 0) {
 | 
						|
                const nodeId = params.nodes[0];
 | 
						|
                this.showNodeDetails(nodeId);
 | 
						|
                this.highlightNodeConnections(nodeId);
 | 
						|
            } else {
 | 
						|
                this.clearHighlights();
 | 
						|
            }
 | 
						|
        });
 | 
						|
 | 
						|
        // Enhanced hover events
 | 
						|
        this.network.on('hoverNode', (params) => {
 | 
						|
            const nodeId = params.node;
 | 
						|
            const node = this.nodes.get(nodeId);
 | 
						|
            if (node) {
 | 
						|
                this.showNodeInfoPopup(params.pointer.DOM, node);
 | 
						|
                this.highlightConnectedNodes(nodeId, true);
 | 
						|
            }
 | 
						|
        });
 | 
						|
 | 
						|
        this.network.on('blurNode', (params) => {
 | 
						|
            this.hideNodeInfoPopup();
 | 
						|
            this.clearHoverHighlights();
 | 
						|
        });
 | 
						|
 | 
						|
        // Edge hover events
 | 
						|
        this.network.on('hoverEdge', (params) => {
 | 
						|
            const edgeId = params.edge;
 | 
						|
            const edge = this.edges.get(edgeId);
 | 
						|
            if (edge) {
 | 
						|
                this.showEdgeInfo(params.pointer.DOM, edge);
 | 
						|
            }
 | 
						|
        });
 | 
						|
 | 
						|
        this.network.on('blurEdge', () => {
 | 
						|
            this.hideNodeInfoPopup();
 | 
						|
        });
 | 
						|
 | 
						|
        // Double-click to focus on node
 | 
						|
        this.network.on('doubleClick', (params) => {
 | 
						|
            if (params.nodes.length > 0) {
 | 
						|
                const nodeId = params.nodes[0];
 | 
						|
                this.focusOnNode(nodeId);
 | 
						|
            }
 | 
						|
        });
 | 
						|
 | 
						|
        // Context menu (right-click)
 | 
						|
        this.network.on('oncontext', (params) => {
 | 
						|
            params.event.preventDefault();
 | 
						|
            if (params.nodes.length > 0) {
 | 
						|
                this.showNodeContextMenu(params.pointer.DOM, params.nodes[0]);
 | 
						|
            }
 | 
						|
        });
 | 
						|
 | 
						|
        // Stabilization events with progress
 | 
						|
        this.network.on('stabilizationProgress', (params) => {
 | 
						|
            const progress = params.iterations / params.total;
 | 
						|
            this.updateStabilizationProgress(progress);
 | 
						|
        });
 | 
						|
 | 
						|
        this.network.on('stabilizationIterationsDone', () => {
 | 
						|
            this.onStabilizationComplete();
 | 
						|
        });
 | 
						|
 | 
						|
        // Selection events
 | 
						|
        this.network.on('select', (params) => {
 | 
						|
            console.log('Selected nodes:', params.nodes);
 | 
						|
            console.log('Selected edges:', params.edges);
 | 
						|
        });
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Update graph with new data and enhanced processing
 | 
						|
     * @param {Object} graphData - Graph data from backend
 | 
						|
     */
 | 
						|
    updateGraph(graphData) {
 | 
						|
        if (!graphData || !graphData.nodes || !graphData.edges) {
 | 
						|
            console.warn('Invalid graph data received');
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        try {
 | 
						|
            // Initialize if not already done
 | 
						|
            if (!this.isInitialized) {
 | 
						|
                this.initialize();
 | 
						|
            }
 | 
						|
 | 
						|
            // Process nodes with enhanced attributes
 | 
						|
            const processedNodes = graphData.nodes.map(node => this.processNode(node));
 | 
						|
            const processedEdges = graphData.edges.map(edge => this.processEdge(edge));
 | 
						|
 | 
						|
            // Update datasets with animation
 | 
						|
            const existingNodeIds = this.nodes.getIds();
 | 
						|
            const existingEdgeIds = this.edges.getIds();
 | 
						|
 | 
						|
            // Add new nodes with fade-in animation
 | 
						|
            const newNodes = processedNodes.filter(node => !existingNodeIds.includes(node.id));
 | 
						|
            const newEdges = processedEdges.filter(edge => !existingEdgeIds.includes(edge.id));
 | 
						|
 | 
						|
            // Update existing data
 | 
						|
            this.nodes.update(processedNodes);
 | 
						|
            this.edges.update(processedEdges);
 | 
						|
 | 
						|
            // Highlight new additions briefly
 | 
						|
            if (newNodes.length > 0 || newEdges.length > 0) {
 | 
						|
                setTimeout(() => this.highlightNewElements(newNodes, newEdges), 100);
 | 
						|
            }
 | 
						|
 | 
						|
            // Auto-fit view for small graphs or first update
 | 
						|
            if (processedNodes.length <= 10 || existingNodeIds.length === 0) {
 | 
						|
                setTimeout(() => this.fitView(), 800);
 | 
						|
            }
 | 
						|
 | 
						|
            console.log(`Enhanced graph updated: ${processedNodes.length} nodes, ${processedEdges.length} edges (${newNodes.length} new nodes, ${newEdges.length} new edges)`);
 | 
						|
        } catch (error) {
 | 
						|
            console.error('Failed to update enhanced graph:', error);
 | 
						|
            this.showError('Failed to update visualization');
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Process node data with enhanced styling and metadata
 | 
						|
     * @param {Object} node - Raw node data
 | 
						|
     * @returns {Object} Processed node data
 | 
						|
     */
 | 
						|
    processNode(node) {
 | 
						|
        const processedNode = {
 | 
						|
            id: node.id,
 | 
						|
            label: this.formatNodeLabel(node.id, node.type),
 | 
						|
            title: this.createNodeTooltip(node),
 | 
						|
            color: this.getNodeColor(node.type),
 | 
						|
            size: this.getNodeSize(node.type),
 | 
						|
            borderColor: this.getNodeBorderColor(node.type),
 | 
						|
            shape: this.getNodeShape(node.type),
 | 
						|
            metadata: node.metadata || {},
 | 
						|
            type: node.type
 | 
						|
        };
 | 
						|
 | 
						|
        // Add confidence-based styling
 | 
						|
        if (node.confidence) {
 | 
						|
            processedNode.borderWidth = Math.max(2, Math.floor(node.confidence * 5));
 | 
						|
        }
 | 
						|
 | 
						|
        // Add special styling for important nodes
 | 
						|
        if (this.isImportantNode(node)) {
 | 
						|
            processedNode.shadow = {
 | 
						|
                enabled: true,
 | 
						|
                color: 'rgba(0, 255, 65, 0.6)',
 | 
						|
                size: 10,
 | 
						|
                x: 2,
 | 
						|
                y: 2
 | 
						|
            };
 | 
						|
        }
 | 
						|
 | 
						|
        // Style based on certificate validity
 | 
						|
        if (node.has_valid_cert === true) {
 | 
						|
            processedNode.borderColor = '#00ff41'; // Green for valid cert
 | 
						|
        } else if (node.has_valid_cert === false) {
 | 
						|
            processedNode.borderColor = '#ff9900'; // Amber for expired/no cert
 | 
						|
            processedNode.borderDashes = [5, 5];
 | 
						|
        }
 | 
						|
 | 
						|
        return processedNode;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Process edge data with enhanced styling and metadata
 | 
						|
     * @param {Object} edge - Raw edge data
 | 
						|
     * @returns {Object} Processed edge data
 | 
						|
     */
 | 
						|
    processEdge(edge) {
 | 
						|
        const confidence = edge.confidence_score || 0;
 | 
						|
        const processedEdge = {
 | 
						|
            id: `${edge.from}-${edge.to}`,
 | 
						|
            from: edge.from,
 | 
						|
            to: edge.to,
 | 
						|
            label: this.formatEdgeLabel(edge.label, confidence),
 | 
						|
            title: this.createEdgeTooltip(edge),
 | 
						|
            width: this.getEdgeWidth(confidence),
 | 
						|
            color: this.getEdgeColor(confidence),
 | 
						|
            dashes: confidence < 0.6 ? [5, 5] : false,
 | 
						|
            metadata: {
 | 
						|
                relationship_type: edge.label,
 | 
						|
                confidence_score: confidence,
 | 
						|
                source_provider: edge.source_provider,
 | 
						|
                discovery_timestamp: edge.discovery_timestamp
 | 
						|
            }
 | 
						|
        };
 | 
						|
 | 
						|
        // Add animation for high-confidence edges
 | 
						|
        if (confidence >= 0.8) {
 | 
						|
            processedEdge.shadow = {
 | 
						|
                enabled: true,
 | 
						|
                color: 'rgba(0, 255, 65, 0.3)',
 | 
						|
                size: 5,
 | 
						|
                x: 1,
 | 
						|
                y: 1
 | 
						|
            };
 | 
						|
        }
 | 
						|
 | 
						|
        return processedEdge;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Format node label for display
 | 
						|
     * @param {string} nodeId - Node identifier
 | 
						|
     * @param {string} nodeType - Node type
 | 
						|
     * @returns {string} Formatted label
 | 
						|
     */
 | 
						|
    formatNodeLabel(nodeId, nodeType) {
 | 
						|
        // Truncate long domain names
 | 
						|
        if (nodeId.length > 20) {
 | 
						|
            return nodeId.substring(0, 17) + '...';
 | 
						|
        }
 | 
						|
        return nodeId;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Format edge label for display
 | 
						|
     * @param {string} relationshipType - Type of relationship
 | 
						|
     * @param {number} confidence - Confidence score
 | 
						|
     * @returns {string} Formatted label
 | 
						|
     */
 | 
						|
    formatEdgeLabel(relationshipType, confidence) {
 | 
						|
        if (!relationshipType) return '';
 | 
						|
 | 
						|
        const confidenceText = confidence >= 0.8 ? '●' : confidence >= 0.6 ? '◐' : '○';
 | 
						|
        return `${relationshipType} ${confidenceText}`;
 | 
						|
    }
 | 
						|
    
 | 
						|
    /**
 | 
						|
     * Get node color based on type
 | 
						|
     * @param {string} nodeType - Node type
 | 
						|
     * @returns {string} Color value
 | 
						|
     */
 | 
						|
    getNodeColor(nodeType) {
 | 
						|
        const colors = {
 | 
						|
            'domain': '#00ff41',     // Green
 | 
						|
            'ip': '#ff9900',         // Amber
 | 
						|
            'certificate': '#c7c7c7', // Gray
 | 
						|
            'asn': '#00aaff'         // Blue
 | 
						|
        };
 | 
						|
        return colors[nodeType] || '#ffffff';
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Get node border color based on type
 | 
						|
     * @param {string} nodeType - Node type
 | 
						|
     * @returns {string} Border color value
 | 
						|
     */
 | 
						|
    getNodeBorderColor(nodeType) {
 | 
						|
        const borderColors = {
 | 
						|
            'domain': '#00aa2e',
 | 
						|
            'ip': '#cc7700',
 | 
						|
            'certificate': '#999999',
 | 
						|
            'asn': '#0088cc'
 | 
						|
        };
 | 
						|
        return borderColors[nodeType] || '#666666';
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Get node size based on type
 | 
						|
     * @param {string} nodeType - Node type
 | 
						|
     * @returns {number} Node size
 | 
						|
     */
 | 
						|
    getNodeSize(nodeType) {
 | 
						|
        const sizes = {
 | 
						|
            'domain': 12,
 | 
						|
            'ip': 14,
 | 
						|
            'certificate': 10,
 | 
						|
            'asn': 16
 | 
						|
        };
 | 
						|
        return sizes[nodeType] || 12;
 | 
						|
    }
 | 
						|
    
 | 
						|
    /**
 | 
						|
     * Get enhanced node shape based on type
 | 
						|
     * @param {string} nodeType - Node type
 | 
						|
     * @returns {string} Shape name
 | 
						|
     */
 | 
						|
    getNodeShape(nodeType) {
 | 
						|
        const shapes = {
 | 
						|
            'domain': 'dot',
 | 
						|
            'ip': 'square',
 | 
						|
            'certificate': 'diamond',
 | 
						|
            'asn': 'triangle'
 | 
						|
        };
 | 
						|
        return shapes[nodeType] || 'dot';
 | 
						|
    }
 | 
						|
    
 | 
						|
    /**
 | 
						|
     * Get edge color based on confidence
 | 
						|
     * @param {number} confidence - Confidence score
 | 
						|
     * @returns {string} Edge color
 | 
						|
     */
 | 
						|
    getEdgeColor(confidence) {
 | 
						|
        if (confidence >= 0.8) {
 | 
						|
            return '#00ff41'; // High confidence - green
 | 
						|
        } else if (confidence >= 0.6) {
 | 
						|
            return '#ff9900'; // Medium confidence - amber
 | 
						|
        } else {
 | 
						|
            return '#666666'; // Low confidence - gray
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Get edge width based on confidence
 | 
						|
     * @param {number} confidence - Confidence score
 | 
						|
     * @returns {number} Edge width
 | 
						|
     */
 | 
						|
    getEdgeWidth(confidence) {
 | 
						|
        if (confidence >= 0.8) {
 | 
						|
            return 3;
 | 
						|
        } else if (confidence >= 0.6) {
 | 
						|
            return 2;
 | 
						|
        } else {
 | 
						|
            return 1;
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    /**
 | 
						|
     * Create node tooltip
 | 
						|
     * @param {Object} node - Node data
 | 
						|
     * @returns {string} HTML tooltip content
 | 
						|
     */
 | 
						|
    createNodeTooltip(node) {
 | 
						|
        let tooltip = `<div style="font-family: 'Roboto Mono', monospace; font-size: 11px;">`;
 | 
						|
        tooltip += `<div style="color: #00ff41; font-weight: bold; margin-bottom: 4px;">${node.id}</div>`;
 | 
						|
        tooltip += `<div style="color: #999; margin-bottom: 2px;">Type: ${node.type}</div>`;
 | 
						|
        
 | 
						|
        if (node.metadata && Object.keys(node.metadata).length > 0) {
 | 
						|
            tooltip += `<div style="color: #999; margin-top: 4px; border-top: 1px solid #444; padding-top: 4px;">`;
 | 
						|
            tooltip += `Click for details</div>`;
 | 
						|
        }
 | 
						|
        
 | 
						|
        tooltip += `</div>`;
 | 
						|
        return tooltip;
 | 
						|
    }
 | 
						|
    
 | 
						|
    /**
 | 
						|
     * Create edge tooltip
 | 
						|
     * @param {Object} edge - Edge data
 | 
						|
     * @returns {string} HTML tooltip content
 | 
						|
     */
 | 
						|
    createEdgeTooltip(edge) {
 | 
						|
        let tooltip = `<div style="font-family: 'Roboto Mono', monospace; font-size: 11px;">`;
 | 
						|
        tooltip += `<div style="color: #00ff41; font-weight: bold; margin-bottom: 4px;">${edge.label || 'Relationship'}</div>`;
 | 
						|
        tooltip += `<div style="color: #999; margin-bottom: 2px;">Confidence: ${(edge.confidence_score * 100).toFixed(1)}%</div>`;
 | 
						|
        
 | 
						|
        if (edge.source_provider) {
 | 
						|
            tooltip += `<div style="color: #999; margin-bottom: 2px;">Source: ${edge.source_provider}</div>`;
 | 
						|
        }
 | 
						|
        
 | 
						|
        if (edge.discovery_timestamp) {
 | 
						|
            const date = new Date(edge.discovery_timestamp);
 | 
						|
            tooltip += `<div style="color: #666; font-size: 10px;">Discovered: ${date.toLocaleString()}</div>`;
 | 
						|
        }
 | 
						|
        
 | 
						|
        tooltip += `</div>`;
 | 
						|
        return tooltip;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Determine if node is important based on connections or metadata
 | 
						|
     * @param {Object} node - Node data
 | 
						|
     * @returns {boolean} True if node is important
 | 
						|
     */
 | 
						|
    isImportantNode(node) {
 | 
						|
        // Mark nodes as important based on criteria
 | 
						|
        if (node.type === 'domain' && node.id.includes('www.')) return false;
 | 
						|
        if (node.metadata && node.metadata.connection_count > 3) return true;
 | 
						|
        if (node.type === 'asn') return true;
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Show node details in modal
 | 
						|
     * @param {string} nodeId - Node identifier
 | 
						|
     */
 | 
						|
    showNodeDetails(nodeId) {
 | 
						|
        const node = this.nodes.get(nodeId);
 | 
						|
        if (!node) return;
 | 
						|
 | 
						|
        // Trigger custom event for main application to handle
 | 
						|
        const event = new CustomEvent('nodeSelected', {
 | 
						|
            detail: { nodeId, node }
 | 
						|
        });
 | 
						|
        document.dispatchEvent(event);
 | 
						|
    }
 | 
						|
    
 | 
						|
    /**
 | 
						|
     * Show enhanced node info popup
 | 
						|
     * @param {Object} position - Mouse position
 | 
						|
     * @param {Object} node - Node data
 | 
						|
     */
 | 
						|
    showNodeInfoPopup(position, node) {
 | 
						|
        if (!this.nodeInfoPopup) return;
 | 
						|
 | 
						|
        const html = `
 | 
						|
            <div class="node-info-title">${node.id}</div>
 | 
						|
            <div class="node-info-detail">
 | 
						|
                <span class="node-info-label">Type:</span>
 | 
						|
                <span class="node-info-value">${node.type || 'Unknown'}</span>
 | 
						|
            </div>
 | 
						|
            ${node.metadata && Object.keys(node.metadata).length > 0 ? 
 | 
						|
                '<div class="node-info-detail"><span class="node-info-label">Details:</span><span class="node-info-value">Click for more</span></div>' : 
 | 
						|
                ''}
 | 
						|
        `;
 | 
						|
 | 
						|
        this.nodeInfoPopup.innerHTML = html;
 | 
						|
        this.nodeInfoPopup.style.display = 'block';
 | 
						|
        this.nodeInfoPopup.style.left = position.x + 15 + 'px';
 | 
						|
        this.nodeInfoPopup.style.top = position.y - 10 + 'px';
 | 
						|
 | 
						|
        // Ensure popup stays in viewport
 | 
						|
        const rect = this.nodeInfoPopup.getBoundingClientRect();
 | 
						|
        if (rect.right > window.innerWidth) {
 | 
						|
            this.nodeInfoPopup.style.left = position.x - rect.width - 15 + 'px';
 | 
						|
        }
 | 
						|
        if (rect.bottom > window.innerHeight) {
 | 
						|
            this.nodeInfoPopup.style.top = position.y - rect.height + 10 + 'px';
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Show edge information tooltip
 | 
						|
     * @param {Object} position - Mouse position
 | 
						|
     * @param {Object} edge - Edge data
 | 
						|
     */
 | 
						|
    showEdgeInfo(position, edge) {
 | 
						|
        if (!this.nodeInfoPopup) return;
 | 
						|
        
 | 
						|
        const confidence = edge.metadata ? edge.metadata.confidence_score : 0;
 | 
						|
        const provider = edge.metadata ? edge.metadata.source_provider : 'Unknown';
 | 
						|
        
 | 
						|
        const html = `
 | 
						|
            <div class="node-info-title">${edge.metadata ? edge.metadata.relationship_type : 'Relationship'}</div>
 | 
						|
            <div class="node-info-detail">
 | 
						|
                <span class="node-info-label">Confidence:</span>
 | 
						|
                <span class="node-info-value">${(confidence * 100).toFixed(1)}%</span>
 | 
						|
            </div>
 | 
						|
            <div class="node-info-detail">
 | 
						|
                <span class="node-info-label">Provider:</span>
 | 
						|
                <span class="node-info-value">${provider}</span>
 | 
						|
            </div>
 | 
						|
        `;
 | 
						|
        
 | 
						|
        this.nodeInfoPopup.innerHTML = html;
 | 
						|
        this.nodeInfoPopup.style.display = 'block';
 | 
						|
        this.nodeInfoPopup.style.left = position.x + 15 + 'px';
 | 
						|
        this.nodeInfoPopup.style.top = position.y - 10 + 'px';
 | 
						|
    }
 | 
						|
    
 | 
						|
    /**
 | 
						|
     * Hide node info popup
 | 
						|
     */
 | 
						|
    hideNodeInfoPopup() {
 | 
						|
        if (this.nodeInfoPopup) {
 | 
						|
            this.nodeInfoPopup.style.display = 'none';
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    /**
 | 
						|
     * Highlight node connections
 | 
						|
     * @param {string} nodeId - Node to highlight
 | 
						|
     */
 | 
						|
    highlightNodeConnections(nodeId) {
 | 
						|
        const connectedNodes = this.network.getConnectedNodes(nodeId);
 | 
						|
        const connectedEdges = this.network.getConnectedEdges(nodeId);
 | 
						|
        
 | 
						|
        // Update node colors
 | 
						|
        const nodeUpdates = connectedNodes.map(id => ({
 | 
						|
            id: id,
 | 
						|
            borderColor: '#ff9900',
 | 
						|
            borderWidth: 3
 | 
						|
        }));
 | 
						|
        
 | 
						|
        nodeUpdates.push({
 | 
						|
            id: nodeId,
 | 
						|
            borderColor: '#00ff41',
 | 
						|
            borderWidth: 4
 | 
						|
        });
 | 
						|
        
 | 
						|
        // Update edge colors
 | 
						|
        const edgeUpdates = connectedEdges.map(id => ({
 | 
						|
            id: id,
 | 
						|
            color: { color: '#ff9900' },
 | 
						|
            width: 3
 | 
						|
        }));
 | 
						|
        
 | 
						|
        this.nodes.update(nodeUpdates);
 | 
						|
        this.edges.update(edgeUpdates);
 | 
						|
        
 | 
						|
        // Store for cleanup
 | 
						|
        this.highlightedElements = {
 | 
						|
            nodes: connectedNodes.concat([nodeId]),
 | 
						|
            edges: connectedEdges
 | 
						|
        };
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Highlight connected nodes on hover
 | 
						|
     * @param {string} nodeId - Node ID
 | 
						|
     * @param {boolean} highlight - Whether to highlight or unhighlight
 | 
						|
     */
 | 
						|
    highlightConnectedNodes(nodeId, highlight) {
 | 
						|
        const connectedNodes = this.network.getConnectedNodes(nodeId);
 | 
						|
        const connectedEdges = this.network.getConnectedEdges(nodeId);
 | 
						|
        
 | 
						|
        if (highlight) {
 | 
						|
            // Dim all other elements
 | 
						|
            this.dimUnconnectedElements([nodeId, ...connectedNodes], connectedEdges);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Dim elements not connected to the specified nodes
 | 
						|
     * @param {Array} nodeIds - Node IDs to keep highlighted
 | 
						|
     * @param {Array} edgeIds - Edge IDs to keep highlighted
 | 
						|
     */
 | 
						|
    dimUnconnectedElements(nodeIds, edgeIds) {
 | 
						|
        const allNodes = this.nodes.get();
 | 
						|
        const allEdges = this.edges.get();
 | 
						|
        
 | 
						|
        const nodeUpdates = allNodes.map(node => ({
 | 
						|
            id: node.id,
 | 
						|
            opacity: nodeIds.includes(node.id) ? 1 : 0.3
 | 
						|
        }));
 | 
						|
        
 | 
						|
        const edgeUpdates = allEdges.map(edge => ({
 | 
						|
            id: edge.id,
 | 
						|
            opacity: edgeIds.includes(edge.id) ? 1 : 0.1
 | 
						|
        }));
 | 
						|
        
 | 
						|
        this.nodes.update(nodeUpdates);
 | 
						|
        this.edges.update(edgeUpdates);
 | 
						|
    }
 | 
						|
    
 | 
						|
    /**
 | 
						|
     * Clear all highlights
 | 
						|
     */
 | 
						|
    clearHighlights() {
 | 
						|
        if (this.highlightedElements) {
 | 
						|
            // Reset highlighted nodes
 | 
						|
            const nodeUpdates = this.highlightedElements.nodes.map(id => {
 | 
						|
                const originalNode = this.nodes.get(id);
 | 
						|
                return {
 | 
						|
                    id: id,
 | 
						|
                    borderColor: this.getNodeBorderColor(originalNode.type),
 | 
						|
                    borderWidth: 2
 | 
						|
                };
 | 
						|
            });
 | 
						|
            
 | 
						|
            // Reset highlighted edges
 | 
						|
            const edgeUpdates = this.highlightedElements.edges.map(id => {
 | 
						|
                const originalEdge = this.edges.get(id);
 | 
						|
                return {
 | 
						|
                    id: id,
 | 
						|
                    color: this.getEdgeColor(originalEdge.metadata ? originalEdge.metadata.confidence_score : 0.5),
 | 
						|
                    width: this.getEdgeWidth(originalEdge.metadata ? originalEdge.metadata.confidence_score : 0.5)
 | 
						|
                };
 | 
						|
            });
 | 
						|
            
 | 
						|
            this.nodes.update(nodeUpdates);
 | 
						|
            this.edges.update(edgeUpdates);
 | 
						|
            
 | 
						|
            this.highlightedElements = null;
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Clear hover highlights
 | 
						|
     */
 | 
						|
    clearHoverHighlights() {
 | 
						|
        const allNodes = this.nodes.get();
 | 
						|
        const allEdges = this.edges.get();
 | 
						|
        
 | 
						|
        const nodeUpdates = allNodes.map(node => ({ id: node.id, opacity: 1 }));
 | 
						|
        const edgeUpdates = allEdges.map(edge => ({ id: edge.id, opacity: 1 }));
 | 
						|
        
 | 
						|
        this.nodes.update(nodeUpdates);
 | 
						|
        this.edges.update(edgeUpdates);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Highlight newly added elements
 | 
						|
     * @param {Array} newNodes - New nodes
 | 
						|
     * @param {Array} newEdges - New edges
 | 
						|
     */
 | 
						|
    highlightNewElements(newNodes, newEdges) {
 | 
						|
        // Briefly highlight new nodes
 | 
						|
        const nodeHighlights = newNodes.map(node => ({
 | 
						|
            id: node.id,
 | 
						|
            borderColor: '#00ff41',
 | 
						|
            borderWidth: 4,
 | 
						|
            shadow: {
 | 
						|
                enabled: true,
 | 
						|
                color: 'rgba(0, 255, 65, 0.8)',
 | 
						|
                size: 15,
 | 
						|
                x: 2,
 | 
						|
                y: 2
 | 
						|
            }
 | 
						|
        }));
 | 
						|
        
 | 
						|
        // Briefly highlight new edges
 | 
						|
        const edgeHighlights = newEdges.map(edge => ({
 | 
						|
            id: edge.id,
 | 
						|
            color: '#00ff41',
 | 
						|
            width: 4
 | 
						|
        }));
 | 
						|
        
 | 
						|
        this.nodes.update(nodeHighlights);
 | 
						|
        this.edges.update(edgeHighlights);
 | 
						|
        
 | 
						|
        // Reset after animation
 | 
						|
        setTimeout(() => {
 | 
						|
            const nodeResets = newNodes.map(node => ({
 | 
						|
                id: node.id,
 | 
						|
                borderColor: this.getNodeBorderColor(node.type),
 | 
						|
                borderWidth: 2,
 | 
						|
                shadow: node.shadow || { enabled: false }
 | 
						|
            }));
 | 
						|
            
 | 
						|
            const edgeResets = newEdges.map(edge => ({
 | 
						|
                id: edge.id,
 | 
						|
                color: this.getEdgeColor(edge.metadata ? edge.metadata.confidence_score : 0.5),
 | 
						|
                width: this.getEdgeWidth(edge.metadata ? edge.metadata.confidence_score : 0.5)
 | 
						|
            }));
 | 
						|
            
 | 
						|
            this.nodes.update(nodeResets);
 | 
						|
            this.edges.update(edgeResets);
 | 
						|
        }, 2000);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Update stabilization progress
 | 
						|
     * @param {number} progress - Progress value (0-1)
 | 
						|
     */
 | 
						|
    updateStabilizationProgress(progress) {
 | 
						|
        // Could show a progress indicator if needed
 | 
						|
        console.log(`Graph stabilization: ${(progress * 100).toFixed(1)}%`);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Handle stabilization completion
 | 
						|
     */
 | 
						|
    onStabilizationComplete() {
 | 
						|
        console.log('Graph stabilization complete');
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Focus view on specific node
 | 
						|
     * @param {string} nodeId - Node to focus on
 | 
						|
     */
 | 
						|
    focusOnNode(nodeId) {
 | 
						|
        const nodePosition = this.network.getPositions([nodeId]);
 | 
						|
        if (nodePosition[nodeId]) {
 | 
						|
            this.network.moveTo({
 | 
						|
                position: nodePosition[nodeId],
 | 
						|
                scale: 1.5,
 | 
						|
                animation: {
 | 
						|
                    duration: 1000,
 | 
						|
                    easingFunction: 'easeInOutQuart'
 | 
						|
                }
 | 
						|
            });
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    /**
 | 
						|
     * Toggle physics simulation
 | 
						|
     */
 | 
						|
    togglePhysics() {
 | 
						|
        const currentPhysics = this.network.physics.physicsEnabled;
 | 
						|
        this.network.setOptions({ physics: !currentPhysics });
 | 
						|
        
 | 
						|
        const button = document.getElementById('graph-physics');
 | 
						|
        if (button) {
 | 
						|
            button.textContent = currentPhysics ? '[PHYSICS OFF]' : '[PHYSICS ON]';
 | 
						|
            button.style.color = currentPhysics ? '#ff9900' : '#00ff41';
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Toggle node clustering
 | 
						|
     */
 | 
						|
    toggleClustering() {
 | 
						|
        // Simple clustering by node type
 | 
						|
        const clusterOptionsByType = {
 | 
						|
            joinCondition: (childOptions) => {
 | 
						|
                return childOptions.type === 'domain';
 | 
						|
            },
 | 
						|
            clusterNodeProperties: {
 | 
						|
                id: 'domain-cluster',
 | 
						|
                borderWidth: 3,
 | 
						|
                shape: 'database',
 | 
						|
                label: 'Domains',
 | 
						|
                color: '#00ff41'
 | 
						|
            }
 | 
						|
        };
 | 
						|
        
 | 
						|
        if (this.network.clustering.isCluster('domain-cluster')) {
 | 
						|
            this.network.clustering.openCluster('domain-cluster');
 | 
						|
        } else {
 | 
						|
            this.network.clustering.cluster(clusterOptionsByType);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Fit the view to show all nodes
 | 
						|
     */
 | 
						|
    fitView() {
 | 
						|
        if (this.network) {
 | 
						|
            this.network.fit({
 | 
						|
                animation: {
 | 
						|
                    duration: 1000,
 | 
						|
                    easingFunction: 'easeInOutQuad'
 | 
						|
                }
 | 
						|
            });
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Reset the view to initial state
 | 
						|
     */
 | 
						|
    resetView() {
 | 
						|
        if (this.network) {
 | 
						|
            this.network.moveTo({
 | 
						|
                position: { x: 0, y: 0 },
 | 
						|
                scale: 1,
 | 
						|
                animation: {
 | 
						|
                    duration: 1000,
 | 
						|
                    easingFunction: 'easeInOutQuad'
 | 
						|
                }
 | 
						|
            });
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Clear the graph
 | 
						|
     */
 | 
						|
    clear() {
 | 
						|
        this.nodes.clear();
 | 
						|
        this.edges.clear();
 | 
						|
 | 
						|
        // Show placeholder
 | 
						|
        const placeholder = this.container.querySelector('.graph-placeholder');
 | 
						|
        if (placeholder) {
 | 
						|
            placeholder.style.display = 'flex';
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Show error message
 | 
						|
     * @param {string} message - Error message
 | 
						|
     */
 | 
						|
    showError(message) {
 | 
						|
        const placeholder = this.container.querySelector('.graph-placeholder .placeholder-text');
 | 
						|
        if (placeholder) {
 | 
						|
            placeholder.textContent = `Error: ${message}`;
 | 
						|
            placeholder.style.color = '#ff6b6b';
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Get network statistics
 | 
						|
     * @returns {Object} Statistics object
 | 
						|
     */
 | 
						|
    getStatistics() {
 | 
						|
        return {
 | 
						|
            nodeCount: this.nodes.length,
 | 
						|
            edgeCount: this.edges.length,
 | 
						|
            //isStabilized: this.network ? this.network.isStabilized() : false
 | 
						|
        };
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Export graph as image (if needed for future implementation)
 | 
						|
     * @param {string} format - Image format ('png', 'jpeg')
 | 
						|
     * @returns {string} Data URL of the image
 | 
						|
     */
 | 
						|
    exportAsImage(format = 'png') {
 | 
						|
        if (!this.network) return null;
 | 
						|
 | 
						|
        // This would require additional vis.js functionality
 | 
						|
        // Placeholder for future implementation
 | 
						|
        console.log('Image export not yet implemented');
 | 
						|
        return null;
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
// Export for use in main.js
 | 
						|
window.GraphManager = GraphManager; |