2648 lines
		
	
	
		
			100 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			2648 lines
		
	
	
		
			100 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/**
 | 
						||
 * Main application logic for DNSRecon web interface
 | 
						||
 * Handles UI interactions, API communication, and data flow
 | 
						||
 * UPDATED: Now compatible with a strictly flat, unified data model for attributes.
 | 
						||
 */
 | 
						||
 | 
						||
class DNSReconApp {
 | 
						||
    constructor() {
 | 
						||
        console.log('DNSReconApp constructor called');
 | 
						||
        this.graphManager = null;
 | 
						||
        this.scanStatus = 'idle';
 | 
						||
        this.pollInterval = null;
 | 
						||
        this.currentSessionId = null;
 | 
						||
        
 | 
						||
        this.elements = {};
 | 
						||
        
 | 
						||
        this.isScanning = false;
 | 
						||
        this.lastGraphUpdate = null;
 | 
						||
        
 | 
						||
        this.init();
 | 
						||
    }
 | 
						||
    
 | 
						||
    /**
 | 
						||
     * Initialize the application
 | 
						||
     */
 | 
						||
    init() {
 | 
						||
        console.log('DNSReconApp init called');
 | 
						||
        document.addEventListener('DOMContentLoaded', () => {
 | 
						||
            console.log('DOM loaded, initializing application...');
 | 
						||
            try {
 | 
						||
                this.initializeElements();
 | 
						||
                this.setupEventHandlers();
 | 
						||
                this.initializeGraph();
 | 
						||
                this.updateStatus();
 | 
						||
                this.loadProviders();
 | 
						||
                this.initializeEnhancedModals();
 | 
						||
                this.addCheckboxStyling();
 | 
						||
                
 | 
						||
                this.updateGraph();
 | 
						||
                
 | 
						||
                console.log('DNSRecon application initialized successfully');
 | 
						||
            } catch (error) {
 | 
						||
                console.error('Failed to initialize DNSRecon application:', error);
 | 
						||
                this.showError(`Initialization failed: ${error.message}`);
 | 
						||
            }
 | 
						||
        });
 | 
						||
    }
 | 
						||
        
 | 
						||
    /**
 | 
						||
     * Initialize DOM element references
 | 
						||
     */
 | 
						||
    initializeElements() {
 | 
						||
        console.log('Initializing DOM elements...');
 | 
						||
        this.elements = {
 | 
						||
            // Form elements
 | 
						||
            targetInput: document.getElementById('target-input'),
 | 
						||
            maxDepth: document.getElementById('max-depth'),
 | 
						||
            startScan: document.getElementById('start-scan'),
 | 
						||
            addToGraph: document.getElementById('add-to-graph'),
 | 
						||
            stopScan: document.getElementById('stop-scan'),
 | 
						||
            exportOptions: document.getElementById('export-options'),
 | 
						||
            exportModal: document.getElementById('export-modal'),
 | 
						||
            exportModalClose: document.getElementById('export-modal-close'),
 | 
						||
            exportGraphJson: document.getElementById('export-graph-json'),
 | 
						||
            exportTargetsTxt: document.getElementById('export-targets-txt'),
 | 
						||
            exportExecutiveSummary: document.getElementById('export-executive-summary'),
 | 
						||
            configureSettings: document.getElementById('configure-settings'),
 | 
						||
            
 | 
						||
            // Status elements
 | 
						||
            scanStatus: document.getElementById('scan-status'),
 | 
						||
            targetDisplay: document.getElementById('target-display'),
 | 
						||
            depthDisplay: document.getElementById('depth-display'),
 | 
						||
            relationshipsDisplay: document.getElementById('relationships-display'),
 | 
						||
            progressCompact: document.getElementById('progress-compact'),
 | 
						||
            progressFill: document.getElementById('progress-fill'),
 | 
						||
            
 | 
						||
            // Provider elements
 | 
						||
            providerList: document.getElementById('provider-list'),
 | 
						||
            
 | 
						||
            // Node Modal elements
 | 
						||
            nodeModal: document.getElementById('node-modal'),
 | 
						||
            modalTitle: document.getElementById('modal-title'),
 | 
						||
            modalDetails: document.getElementById('modal-details'),
 | 
						||
            modalClose: document.getElementById('modal-close'),
 | 
						||
 | 
						||
            // Settings Modal elements
 | 
						||
            settingsModal: document.getElementById('settings-modal'),
 | 
						||
            settingsModalClose: document.getElementById('settings-modal-close'),
 | 
						||
            
 | 
						||
 | 
						||
            // Other elements
 | 
						||
            sessionId: document.getElementById('session-id'),
 | 
						||
            connectionStatus: document.getElementById('connection-status'),
 | 
						||
        };
 | 
						||
 | 
						||
        // Verify critical elements exist
 | 
						||
        const requiredElements = ['targetInput', 'startScan', 'scanStatus'];
 | 
						||
        for (const elementName of requiredElements) {
 | 
						||
            if (!this.elements[elementName]) {
 | 
						||
                throw new Error(`Required element '${elementName}' not found in DOM`);
 | 
						||
            }
 | 
						||
        }
 | 
						||
 | 
						||
        console.log('DOM elements initialized successfully');
 | 
						||
        this.createMessageContainer();
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Create a message container for showing user feedback
 | 
						||
     */
 | 
						||
    createMessageContainer() {
 | 
						||
        // Check if message container already exists
 | 
						||
        let messageContainer = document.getElementById('message-container');
 | 
						||
        if (!messageContainer) {
 | 
						||
            messageContainer = document.createElement('div');
 | 
						||
            messageContainer.id = 'message-container';
 | 
						||
            messageContainer.className = 'message-container';
 | 
						||
            messageContainer.style.cssText = `
 | 
						||
                position: fixed;
 | 
						||
                top: 20px;
 | 
						||
                right: 20px;
 | 
						||
                z-index: 1000;
 | 
						||
                max-width: 400px;
 | 
						||
            `;
 | 
						||
            document.body.appendChild(messageContainer);
 | 
						||
        }
 | 
						||
    }
 | 
						||
    
 | 
						||
    /**
 | 
						||
     * Setup event handlers
 | 
						||
     */
 | 
						||
    setupEventHandlers() {
 | 
						||
        console.log('Setting up event handlers...');
 | 
						||
        
 | 
						||
        try {
 | 
						||
            // Form interactions
 | 
						||
 | 
						||
            this.initializeModalFunctionality();
 | 
						||
            this.elements.startScan.addEventListener('click', (e) => {
 | 
						||
                e.preventDefault();
 | 
						||
                this.startScan();
 | 
						||
            });
 | 
						||
 | 
						||
            this.elements.addToGraph.addEventListener('click', (e) => {
 | 
						||
                e.preventDefault();
 | 
						||
                this.startScan(false);
 | 
						||
            });
 | 
						||
            
 | 
						||
            this.elements.stopScan.addEventListener('click', (e) => {
 | 
						||
                e.preventDefault();
 | 
						||
                this.stopScan();
 | 
						||
            });
 | 
						||
            
 | 
						||
            this.elements.exportOptions.addEventListener('click', (e) => {
 | 
						||
                e.preventDefault();
 | 
						||
                this.showExportModal();
 | 
						||
            });
 | 
						||
 | 
						||
            if (this.elements.exportModalClose) {
 | 
						||
                this.elements.exportModalClose.addEventListener('click', () => this.hideExportModal());
 | 
						||
            }
 | 
						||
            if (this.elements.exportModal) {
 | 
						||
                this.elements.exportModal.addEventListener('click', (e) => {
 | 
						||
                    if (e.target === this.elements.exportModal) this.hideExportModal();
 | 
						||
                });
 | 
						||
            }
 | 
						||
            if (this.elements.exportGraphJson) {
 | 
						||
                this.elements.exportGraphJson.addEventListener('click', () => this.exportGraphJson());
 | 
						||
            }
 | 
						||
            if (this.elements.exportTargetsTxt) {
 | 
						||
                this.elements.exportTargetsTxt.addEventListener('click', () => this.exportTargetsTxt());
 | 
						||
            }
 | 
						||
            if (this.elements.exportExecutiveSummary) {
 | 
						||
                this.elements.exportExecutiveSummary.addEventListener('click', () => this.exportExecutiveSummary());
 | 
						||
            }
 | 
						||
 | 
						||
 | 
						||
            this.elements.configureSettings.addEventListener('click', () => this.showSettingsModal());
 | 
						||
            
 | 
						||
            // Enter key support for target domain input
 | 
						||
            this.elements.targetInput.addEventListener('keypress', (e) => {
 | 
						||
                if (e.key === 'Enter' && !this.isScanning) {
 | 
						||
                    this.startScan();
 | 
						||
                }
 | 
						||
            });
 | 
						||
            
 | 
						||
            // Node Modal interactions
 | 
						||
            if (this.elements.modalClose) {
 | 
						||
                this.elements.modalClose.addEventListener('click', () => this.hideModal());
 | 
						||
            }
 | 
						||
            if (this.elements.nodeModal) {
 | 
						||
                this.elements.nodeModal.addEventListener('click', (e) => {
 | 
						||
                    if (e.target === this.elements.nodeModal) this.hideModal();
 | 
						||
                });
 | 
						||
            }
 | 
						||
 | 
						||
            // Settings Modal interactions
 | 
						||
            if (this.elements.settingsModalClose) {
 | 
						||
                this.elements.settingsModalClose.addEventListener('click', () => this.hideSettingsModal());
 | 
						||
            }
 | 
						||
            if (this.elements.settingsModal) {
 | 
						||
                this.elements.settingsModal.addEventListener('click', (e) => {
 | 
						||
                    if (e.target === this.elements.settingsModal) this.hideSettingsModal();
 | 
						||
                });
 | 
						||
            }
 | 
						||
            if (this.elements.saveApiKeys) {
 | 
						||
                this.elements.saveApiKeys.removeEventListener('click', this.saveApiKeys);
 | 
						||
            }
 | 
						||
            if (this.elements.resetApiKeys) {
 | 
						||
                this.elements.resetApiKeys.removeEventListener('click', this.resetApiKeys);
 | 
						||
            }
 | 
						||
            
 | 
						||
            // Setup new handlers
 | 
						||
            const saveSettingsBtn = document.getElementById('save-settings');
 | 
						||
            const resetSettingsBtn = document.getElementById('reset-settings');
 | 
						||
            
 | 
						||
            if (saveSettingsBtn) {
 | 
						||
                saveSettingsBtn.addEventListener('click', () => this.saveSettings());
 | 
						||
            }
 | 
						||
            if (resetSettingsBtn) {
 | 
						||
                resetSettingsBtn.addEventListener('click', () => this.resetSettings());
 | 
						||
            }
 | 
						||
 | 
						||
            // Listen for the custom event from the graph
 | 
						||
            document.addEventListener('nodeSelected', (e) => {
 | 
						||
                this.showNodeModal(e.detail.node);
 | 
						||
            });
 | 
						||
 | 
						||
            // Listen for the new iterateScan event from the graph context menu
 | 
						||
            document.addEventListener('iterateScan', (e) => {
 | 
						||
                if (this.isScanning) {
 | 
						||
                    this.showWarning('A scan is already in progress.');
 | 
						||
                    return;
 | 
						||
                }
 | 
						||
                const { nodeId } = e.detail;
 | 
						||
                console.log(`Received iterateScan event for node: ${nodeId}`);
 | 
						||
                this.elements.targetInput.value = nodeId;
 | 
						||
                this.startScan(false, nodeId); // Pass nodeId to force rescan
 | 
						||
            });
 | 
						||
            
 | 
						||
            // Keyboard shortcuts
 | 
						||
            document.addEventListener('keydown', (e) => {
 | 
						||
                if (e.key === 'Escape') {
 | 
						||
                    this.hideModal();
 | 
						||
                    this.hideSettingsModal();
 | 
						||
                    this.hideExportModal(); // Add this line
 | 
						||
                }
 | 
						||
            });
 | 
						||
            
 | 
						||
            // Window events
 | 
						||
            window.addEventListener('beforeunload', () => {
 | 
						||
                if (this.isScanning) {
 | 
						||
                    return 'A scan is currently in progress. Are you sure you want to leave?';
 | 
						||
                }
 | 
						||
            });
 | 
						||
            
 | 
						||
            console.log('Event handlers set up successfully');
 | 
						||
            
 | 
						||
        } catch (error) {
 | 
						||
            console.error('Failed to setup event handlers:', error);
 | 
						||
            throw error;
 | 
						||
        }
 | 
						||
    }
 | 
						||
    
 | 
						||
    /**
 | 
						||
     * Initialize graph visualization
 | 
						||
     */
 | 
						||
    initializeGraph() {
 | 
						||
        try {
 | 
						||
            console.log('Initializing graph manager...');
 | 
						||
            this.graphManager = new GraphManager('network-graph');
 | 
						||
            console.log('Graph manager initialized successfully');
 | 
						||
        } catch (error) {
 | 
						||
            console.error('Failed to initialize graph manager:', error);
 | 
						||
            this.showError('Failed to initialize graph visualization');
 | 
						||
        }
 | 
						||
    }
 | 
						||
    
 | 
						||
    /**
 | 
						||
     * Start scan with error handling
 | 
						||
     */
 | 
						||
    async startScan(clearGraph = true, forceRescanTarget = null) {
 | 
						||
        console.log('=== STARTING SCAN ===');
 | 
						||
        
 | 
						||
        try {
 | 
						||
            const target = this.elements.targetInput.value.trim();
 | 
						||
            const maxDepth = parseInt(this.elements.maxDepth.value);
 | 
						||
            
 | 
						||
            console.log(`Target: "${target}", Max depth: ${maxDepth}`);
 | 
						||
            
 | 
						||
            // Validation
 | 
						||
            if (!target) {
 | 
						||
                console.log('Validation failed: empty target');
 | 
						||
                this.showError('Please enter a target domain or IP');
 | 
						||
                this.elements.targetInput.focus();
 | 
						||
                return;
 | 
						||
            }
 | 
						||
            
 | 
						||
            if (!this.isValidTarget(target)) {
 | 
						||
                console.log(`Validation failed: invalid target format for "${target}"`);
 | 
						||
                this.showError('Please enter a valid domain name (e.g., example.com) or IP address (e.g., 8.8.8.8)');
 | 
						||
                this.elements.targetInput.focus();
 | 
						||
                return;
 | 
						||
            }
 | 
						||
            
 | 
						||
            console.log('Validation passed, setting UI state to scanning...');
 | 
						||
            this.setUIState('scanning');
 | 
						||
            this.showInfo('Starting reconnaissance scan...');
 | 
						||
            
 | 
						||
            console.log('Making API call to start scan...');
 | 
						||
            
 | 
						||
            const requestData = {
 | 
						||
                target: target,
 | 
						||
                max_depth: maxDepth,
 | 
						||
                clear_graph: clearGraph,
 | 
						||
                force_rescan_target: forceRescanTarget
 | 
						||
            };
 | 
						||
            
 | 
						||
            const response = await this.apiCall('/api/scan/start', 'POST', requestData);
 | 
						||
                        
 | 
						||
            if (response.success) {
 | 
						||
                this.currentSessionId = response.scan_id;
 | 
						||
                this.showSuccess('Reconnaissance scan started successfully');
 | 
						||
 | 
						||
                if (clearGraph) {
 | 
						||
                    this.graphManager.clear();
 | 
						||
                }
 | 
						||
                
 | 
						||
                console.log(`Scan started for ${target} with depth ${maxDepth}`);
 | 
						||
                
 | 
						||
                // Start polling immediately with faster interval for responsiveness
 | 
						||
                this.startPolling(1000);
 | 
						||
                
 | 
						||
                // Force an immediate status update
 | 
						||
                console.log('Forcing immediate status update...');
 | 
						||
                setTimeout(() => {
 | 
						||
                    this.updateStatus();
 | 
						||
                    this.updateGraph();
 | 
						||
                }, 100);
 | 
						||
                
 | 
						||
            } else {
 | 
						||
                throw new Error(response.error || 'Failed to start scan');
 | 
						||
            }
 | 
						||
            
 | 
						||
        } catch (error) {
 | 
						||
            console.error('Failed to start scan:', error);
 | 
						||
            this.showError(`Failed to start scan: ${error.message}`);
 | 
						||
            this.setUIState('idle');
 | 
						||
        }
 | 
						||
    }
 | 
						||
    /**
 | 
						||
     * Scan stop with immediate UI feedback
 | 
						||
     */
 | 
						||
    async stopScan() {
 | 
						||
        try {
 | 
						||
            console.log('Stopping scan...');
 | 
						||
            
 | 
						||
            // Immediately disable stop button and show stopping state
 | 
						||
            if (this.elements.stopScan) {
 | 
						||
                this.elements.stopScan.disabled = true;
 | 
						||
                this.elements.stopScan.innerHTML = '<span class="btn-icon">[STOPPING]</span><span>Stopping...</span>';
 | 
						||
            }
 | 
						||
            
 | 
						||
            // Show immediate feedback
 | 
						||
            this.showInfo('Stopping scan...');
 | 
						||
            
 | 
						||
            const response = await this.apiCall('/api/scan/stop', 'POST');
 | 
						||
            
 | 
						||
            if (response.success) {
 | 
						||
                this.showSuccess('Scan stop requested');
 | 
						||
                
 | 
						||
                // Force immediate status update
 | 
						||
                setTimeout(() => {
 | 
						||
                    this.updateStatus();
 | 
						||
                }, 100);
 | 
						||
                
 | 
						||
                // Continue polling for a bit to catch the status change
 | 
						||
                this.startPolling(500); // Fast polling to catch status change
 | 
						||
                
 | 
						||
                // Stop fast polling after 10 seconds
 | 
						||
                setTimeout(() => {
 | 
						||
                    if (this.scanStatus === 'stopped' || this.scanStatus === 'idle') {
 | 
						||
                        this.stopPolling();
 | 
						||
                    }
 | 
						||
                }, 10000);
 | 
						||
                
 | 
						||
            } else {
 | 
						||
                throw new Error(response.error || 'Failed to stop scan');
 | 
						||
            }
 | 
						||
            
 | 
						||
        } catch (error) {
 | 
						||
            console.error('Failed to stop scan:', error);
 | 
						||
            this.showError(`Failed to stop scan: ${error.message}`);
 | 
						||
            
 | 
						||
            // Re-enable stop button on error
 | 
						||
            if (this.elements.stopScan) {
 | 
						||
                this.elements.stopScan.disabled = false;
 | 
						||
                this.elements.stopScan.innerHTML = '<span class="btn-icon">[STOP]</span><span>Terminate Scan</span>';
 | 
						||
            }
 | 
						||
        }
 | 
						||
    }
 | 
						||
    
 | 
						||
    /**
 | 
						||
     * Show Export modal
 | 
						||
     */
 | 
						||
    showExportModal() {
 | 
						||
        if (this.elements.exportModal) {
 | 
						||
            this.elements.exportModal.style.display = 'block';
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Hide Export modal
 | 
						||
     */
 | 
						||
    hideExportModal() {
 | 
						||
        if (this.elements.exportModal) {
 | 
						||
            this.elements.exportModal.style.display = 'none';
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Export graph data as JSON with proper error handling
 | 
						||
     */
 | 
						||
    async exportGraphJson() {
 | 
						||
        try {
 | 
						||
            console.log('Exporting graph data as JSON...');
 | 
						||
            
 | 
						||
            // Show loading state
 | 
						||
            if (this.elements.exportGraphJson) {
 | 
						||
                const originalContent = this.elements.exportGraphJson.innerHTML;
 | 
						||
                this.elements.exportGraphJson.innerHTML = '<span class="btn-icon">[...]</span><span>Exporting...</span>';
 | 
						||
                this.elements.exportGraphJson.disabled = true;
 | 
						||
                
 | 
						||
                // Store original content for restoration
 | 
						||
                this.elements.exportGraphJson._originalContent = originalContent;
 | 
						||
            }
 | 
						||
            
 | 
						||
            // Make API call to get export data
 | 
						||
            const response = await fetch('/api/export', {
 | 
						||
                method: 'GET',
 | 
						||
                headers: {
 | 
						||
                    'Content-Type': 'application/json'
 | 
						||
                }
 | 
						||
            });
 | 
						||
            
 | 
						||
            if (!response.ok) {
 | 
						||
                const errorData = await response.json().catch(() => ({}));
 | 
						||
                throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);
 | 
						||
            }
 | 
						||
            
 | 
						||
            // Check if response is JSON or file download
 | 
						||
            const contentType = response.headers.get('content-type');
 | 
						||
            if (contentType && contentType.includes('application/json') && !response.headers.get('content-disposition')) {
 | 
						||
                // This is an error response in JSON format
 | 
						||
                const errorData = await response.json();
 | 
						||
                throw new Error(errorData.error || 'Export failed');
 | 
						||
            }
 | 
						||
            
 | 
						||
            // Get the filename from headers or create one
 | 
						||
            const contentDisposition = response.headers.get('content-disposition');
 | 
						||
            let filename = 'dnsrecon_export.json';
 | 
						||
            if (contentDisposition) {
 | 
						||
                const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
 | 
						||
                if (filenameMatch) {
 | 
						||
                    filename = filenameMatch[1].replace(/['"]/g, '');
 | 
						||
                }
 | 
						||
            }
 | 
						||
            
 | 
						||
            // Create blob and download
 | 
						||
            const blob = await response.blob();
 | 
						||
            const url = window.URL.createObjectURL(blob);
 | 
						||
            const link = document.createElement('a');
 | 
						||
            link.href = url;
 | 
						||
            link.download = filename;
 | 
						||
            document.body.appendChild(link);
 | 
						||
            link.click();
 | 
						||
            document.body.removeChild(link);
 | 
						||
            window.URL.revokeObjectURL(url);
 | 
						||
            
 | 
						||
            this.showSuccess('Graph data exported successfully');
 | 
						||
            this.hideExportModal();
 | 
						||
            
 | 
						||
        } catch (error) {
 | 
						||
            console.error('Failed to export graph data:', error);
 | 
						||
            this.showError(`Export failed: ${error.message}`);
 | 
						||
        } finally {
 | 
						||
            // Restore button state
 | 
						||
            if (this.elements.exportGraphJson) {
 | 
						||
                const originalContent = this.elements.exportGraphJson._originalContent || 
 | 
						||
                    '<span class="btn-icon">[JSON]</span><span>Export Graph Data</span>';
 | 
						||
                this.elements.exportGraphJson.innerHTML = originalContent;
 | 
						||
                this.elements.exportGraphJson.disabled = false;
 | 
						||
            }
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    async exportTargetsTxt() {
 | 
						||
        await this.exportFile('/api/export/targets', this.elements.exportTargetsTxt, 'Exporting Targets...');
 | 
						||
    }
 | 
						||
 | 
						||
    async exportExecutiveSummary() {
 | 
						||
        await this.exportFile('/api/export/summary', this.elements.exportExecutiveSummary, 'Generating Summary...');
 | 
						||
    }
 | 
						||
 | 
						||
    async exportFile(endpoint, buttonElement, loadingMessage) {
 | 
						||
        try {
 | 
						||
            console.log(`Exporting from ${endpoint}...`);
 | 
						||
 | 
						||
            const originalContent = buttonElement.innerHTML;
 | 
						||
            buttonElement.innerHTML = `<span class="btn-icon">[...]</span><span>${loadingMessage}</span>`;
 | 
						||
            buttonElement.disabled = true;
 | 
						||
 | 
						||
            const response = await fetch(endpoint, { method: 'GET' });
 | 
						||
 | 
						||
            if (!response.ok) {
 | 
						||
                const errorData = await response.json().catch(() => ({}));
 | 
						||
                throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);
 | 
						||
            }
 | 
						||
 | 
						||
            const contentDisposition = response.headers.get('content-disposition');
 | 
						||
            let filename = 'export.txt';
 | 
						||
            if (contentDisposition) {
 | 
						||
                const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
 | 
						||
                if (filenameMatch) {
 | 
						||
                    filename = filenameMatch[1].replace(/['"]/g, '');
 | 
						||
                }
 | 
						||
            }
 | 
						||
            
 | 
						||
            const blob = await response.blob();
 | 
						||
            const url = window.URL.createObjectURL(blob);
 | 
						||
            const link = document.createElement('a');
 | 
						||
            link.href = url;
 | 
						||
            link.download = filename;
 | 
						||
            document.body.appendChild(link);
 | 
						||
            link.click();
 | 
						||
            document.body.removeChild(link);
 | 
						||
            window.URL.revokeObjectURL(url);
 | 
						||
            
 | 
						||
            this.showSuccess('File exported successfully');
 | 
						||
            this.hideExportModal();
 | 
						||
        } catch (error) {
 | 
						||
            console.error(`Failed to export from ${endpoint}:`, error);
 | 
						||
            this.showError(`Export failed: ${error.message}`);
 | 
						||
        } finally {
 | 
						||
            const originalContent = buttonElement._originalContent || buttonElement.innerHTML;
 | 
						||
            buttonElement.innerHTML = originalContent;
 | 
						||
            buttonElement.disabled = false;
 | 
						||
        }
 | 
						||
    }
 | 
						||
    
 | 
						||
    /**
 | 
						||
     * Start polling for scan updates with configurable interval
 | 
						||
     */
 | 
						||
    startPolling(interval = 2000) {
 | 
						||
        console.log('=== STARTING POLLING ===');
 | 
						||
        
 | 
						||
        if (this.pollInterval) {
 | 
						||
            console.log('Clearing existing poll interval');
 | 
						||
            clearInterval(this.pollInterval);
 | 
						||
        }
 | 
						||
        
 | 
						||
        this.pollInterval = setInterval(() => {
 | 
						||
            this.updateStatus();
 | 
						||
            this.updateGraph();
 | 
						||
            this.loadProviders();
 | 
						||
        }, interval);
 | 
						||
        
 | 
						||
        console.log(`Polling started with ${interval}ms interval`);
 | 
						||
    }
 | 
						||
    
 | 
						||
    /**
 | 
						||
     * Stop polling for updates
 | 
						||
     */
 | 
						||
    stopPolling() {
 | 
						||
        console.log('=== STOPPING POLLING ===');
 | 
						||
        if (this.pollInterval) {
 | 
						||
            clearInterval(this.pollInterval);
 | 
						||
            this.pollInterval = null;
 | 
						||
        }
 | 
						||
    }
 | 
						||
    
 | 
						||
    /**
 | 
						||
     * Status update with better error handling
 | 
						||
     */
 | 
						||
    async updateStatus() {
 | 
						||
        try {
 | 
						||
            const response = await this.apiCall('/api/scan/status');
 | 
						||
            
 | 
						||
            
 | 
						||
            if (response.success && response.status) {
 | 
						||
                const status = response.status;
 | 
						||
                
 | 
						||
                this.updateStatusDisplay(status);
 | 
						||
                
 | 
						||
                // Handle status changes
 | 
						||
                if (status.status !== this.scanStatus) {
 | 
						||
                    console.log(`*** STATUS CHANGED: ${this.scanStatus} -> ${status.status} ***`);
 | 
						||
                    this.handleStatusChange(status.status, status.task_queue_size);
 | 
						||
                }
 | 
						||
                
 | 
						||
                this.scanStatus = status.status;
 | 
						||
            } else {
 | 
						||
                console.error('Status update failed:', response);
 | 
						||
                // Don't show error for status updates to avoid spam
 | 
						||
            }
 | 
						||
            
 | 
						||
        } catch (error) {
 | 
						||
            console.error('Failed to update status:', error);
 | 
						||
            this.showConnectionError();
 | 
						||
        }
 | 
						||
    }
 | 
						||
    
 | 
						||
    /**
 | 
						||
     * Update graph from server
 | 
						||
     */
 | 
						||
    async updateGraph() {
 | 
						||
        try {
 | 
						||
            console.log('Updating graph...');
 | 
						||
            const response = await this.apiCall('/api/graph');
 | 
						||
            
 | 
						||
            
 | 
						||
            if (response.success) {
 | 
						||
                const graphData = response.graph;
 | 
						||
                
 | 
						||
                console.log('Graph data received:');
 | 
						||
                console.log('- Nodes:', graphData.nodes ? graphData.nodes.length : 0);
 | 
						||
                console.log('- Edges:', graphData.edges ? graphData.edges.length : 0);
 | 
						||
                
 | 
						||
                // FIXED: Always update graph, even if empty - let GraphManager handle placeholder
 | 
						||
                if (this.graphManager) {
 | 
						||
                    this.graphManager.updateGraph(graphData);
 | 
						||
                    this.lastGraphUpdate = Date.now();
 | 
						||
                    
 | 
						||
                    // Update relationship count in status
 | 
						||
                    const edgeCount = graphData.edges ? graphData.edges.length : 0;
 | 
						||
                    if (this.elements.relationshipsDisplay) {
 | 
						||
                        this.elements.relationshipsDisplay.textContent = edgeCount;
 | 
						||
                    }
 | 
						||
                }
 | 
						||
            } else {
 | 
						||
                console.error('Graph update failed:', response);
 | 
						||
                // FIXED: Show placeholder when graph update fails
 | 
						||
                if (this.graphManager && this.graphManager.container) {
 | 
						||
                    const placeholder = this.graphManager.container.querySelector('.graph-placeholder');
 | 
						||
                    if (placeholder) {
 | 
						||
                        placeholder.style.display = 'flex';
 | 
						||
                    }
 | 
						||
                }
 | 
						||
            }
 | 
						||
            
 | 
						||
        } catch (error) {
 | 
						||
            console.error('Failed to update graph:', error);
 | 
						||
            // FIXED: Show placeholder on error
 | 
						||
            if (this.graphManager && this.graphManager.container) {
 | 
						||
                const placeholder = this.graphManager.container.querySelector('.graph-placeholder');
 | 
						||
                if (placeholder) {
 | 
						||
                    placeholder.style.display = 'flex';
 | 
						||
                }
 | 
						||
            }
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    
 | 
						||
    /**
 | 
						||
     * Update status display elements
 | 
						||
     * @param {Object} status - Status object from server
 | 
						||
     */
 | 
						||
    updateStatusDisplay(status) {
 | 
						||
        try {            
 | 
						||
            // Update status text with animation
 | 
						||
            if (this.elements.scanStatus) {
 | 
						||
                const formattedStatus = this.formatStatus(status.status);
 | 
						||
                if (this.elements.scanStatus.textContent !== formattedStatus) {
 | 
						||
                    this.elements.scanStatus.textContent = formattedStatus;
 | 
						||
                    this.elements.scanStatus.classList.add('fade-in');
 | 
						||
                    setTimeout(() => this.elements.scanStatus.classList.remove('fade-in'), 300);
 | 
						||
                }
 | 
						||
                
 | 
						||
                // Add status-specific classes for styling
 | 
						||
                this.elements.scanStatus.className = `status-value status-${status.status}`;
 | 
						||
            }
 | 
						||
            
 | 
						||
            if (this.elements.targetDisplay) {
 | 
						||
                this.elements.targetDisplay.textContent = status.target_domain || 'None';
 | 
						||
            }
 | 
						||
            if (this.elements.depthDisplay) {
 | 
						||
                this.elements.depthDisplay.textContent = `${status.current_depth}/${status.max_depth}`;
 | 
						||
            }
 | 
						||
            
 | 
						||
            // Update progress bar and compact display
 | 
						||
            if (this.elements.progressFill) {
 | 
						||
                const completed = status.indicators_completed || 0;
 | 
						||
                const totalTasks = status.total_tasks_ever_enqueued || 0; 
 | 
						||
                const progressPercentage = status.progress_percentage || 0;
 | 
						||
 | 
						||
                this.elements.progressFill.style.width = `${progressPercentage}%`;
 | 
						||
                if (this.elements.progressCompact) {
 | 
						||
                    this.elements.progressCompact.textContent = `${completed}/${totalTasks} - ${Math.round(progressPercentage)}%`;
 | 
						||
                }
 | 
						||
 | 
						||
                // Add pulsing animation for active scans
 | 
						||
                if (status.status === 'running') {
 | 
						||
                    this.elements.progressFill.parentElement.classList.add('scanning');
 | 
						||
                } else {
 | 
						||
                    this.elements.progressFill.parentElement.classList.remove('scanning');
 | 
						||
                }
 | 
						||
            }
 | 
						||
            
 | 
						||
            // Update session ID display with user session info
 | 
						||
            if (this.elements.sessionId) {
 | 
						||
                const scanSessionId = this.currentSessionId;
 | 
						||
                const userSessionId = status.user_session_id;
 | 
						||
                
 | 
						||
                if (scanSessionId && userSessionId) {
 | 
						||
                    this.elements.sessionId.textContent = `Session: ${userSessionId.substring(0, 8)}... | Scan: ${scanSessionId}`;
 | 
						||
                } else if (userSessionId) {
 | 
						||
                    this.elements.sessionId.textContent = `User Session: ${userSessionId.substring(0, 8)}...`;
 | 
						||
                } else {
 | 
						||
                    this.elements.sessionId.textContent = 'Session: Loading...';
 | 
						||
                }
 | 
						||
            }
 | 
						||
 | 
						||
            this.setUIState(status.status, status.task_queue_size);
 | 
						||
        } catch (error) {
 | 
						||
            console.error('Error updating status display:', error);
 | 
						||
        }
 | 
						||
    }
 | 
						||
    
 | 
						||
    /**
 | 
						||
     * Handle status changes with improved state synchronization
 | 
						||
     * @param {string} newStatus - New scan status
 | 
						||
     */
 | 
						||
    handleStatusChange(newStatus, task_queue_size) {
 | 
						||
        console.log(`=== STATUS CHANGE: ${this.scanStatus} -> ${newStatus} ===`);
 | 
						||
        
 | 
						||
        switch (newStatus) {
 | 
						||
            case 'running':
 | 
						||
                this.setUIState('scanning', task_queue_size);
 | 
						||
                this.showSuccess('Scan is running');
 | 
						||
                // Increase polling frequency for active scans
 | 
						||
                this.startPolling(1000); // Poll every 1 second for running scans
 | 
						||
                this.updateConnectionStatus('active');
 | 
						||
                break;
 | 
						||
                
 | 
						||
            case 'completed':
 | 
						||
                this.setUIState('completed', task_queue_size);
 | 
						||
                this.stopPolling();
 | 
						||
                this.showSuccess('Scan completed successfully');
 | 
						||
                this.updateConnectionStatus('completed');
 | 
						||
                this.loadProviders();
 | 
						||
                // Force a final graph update
 | 
						||
                console.log('Scan completed - forcing final graph update');
 | 
						||
                setTimeout(() => this.updateGraph(), 100);
 | 
						||
                break;
 | 
						||
                
 | 
						||
            case 'failed':
 | 
						||
                this.setUIState('failed', task_queue_size);
 | 
						||
                this.stopPolling();
 | 
						||
                this.showError('Scan failed');
 | 
						||
                this.updateConnectionStatus('error');
 | 
						||
                this.loadProviders();
 | 
						||
                break;
 | 
						||
                
 | 
						||
            case 'stopped':
 | 
						||
                this.setUIState('stopped', task_queue_size);
 | 
						||
                this.stopPolling();
 | 
						||
                this.showSuccess('Scan stopped');
 | 
						||
                this.updateConnectionStatus('stopped');
 | 
						||
                this.loadProviders();
 | 
						||
                break;
 | 
						||
                
 | 
						||
            case 'idle':
 | 
						||
                this.setUIState('idle', task_queue_size);
 | 
						||
                this.stopPolling();
 | 
						||
                this.updateConnectionStatus('idle');
 | 
						||
                break;
 | 
						||
                
 | 
						||
            default:
 | 
						||
                console.warn(`Unknown status: ${newStatus}`);
 | 
						||
                break;
 | 
						||
        }
 | 
						||
    }
 | 
						||
    
 | 
						||
    /**
 | 
						||
     * Update connection status indicator
 | 
						||
     * @param {string} status - Connection status
 | 
						||
     */
 | 
						||
    updateConnectionStatus(status) {
 | 
						||
        if (!this.elements.connectionStatus) return;
 | 
						||
        
 | 
						||
        const statusColors = {
 | 
						||
            'idle': '#c7c7c7',
 | 
						||
            'active': '#00ff41',
 | 
						||
            'completed': '#00aa2e',
 | 
						||
            'stopped': '#ff9900',
 | 
						||
            'error': '#ff6b6b'
 | 
						||
        };
 | 
						||
        
 | 
						||
        this.elements.connectionStatus.style.backgroundColor = statusColors[status] || '#c7c7c7';
 | 
						||
        
 | 
						||
        const statusText = this.elements.connectionStatus.parentElement?.querySelector('.status-text');
 | 
						||
        if (statusText) {
 | 
						||
            const statusTexts = {
 | 
						||
                'idle': 'System Ready',
 | 
						||
                'active': 'Scanning Active',
 | 
						||
                'completed': 'Scan Complete',
 | 
						||
                'stopped': 'Scan Stopped',
 | 
						||
                'error': 'Connection Error'
 | 
						||
            };
 | 
						||
            
 | 
						||
            statusText.textContent = statusTexts[status] || 'System Online';
 | 
						||
        }
 | 
						||
    }
 | 
						||
    
 | 
						||
    /**
 | 
						||
     * UI state management with immediate button updates
 | 
						||
     */
 | 
						||
    setUIState(state, task_queue_size) {        
 | 
						||
        const isQueueEmpty = task_queue_size === 0;
 | 
						||
 | 
						||
        switch (state) {
 | 
						||
            case 'scanning':
 | 
						||
                this.isScanning = true;
 | 
						||
                if (this.graphManager) {
 | 
						||
                    this.graphManager.isScanning = true;
 | 
						||
                }
 | 
						||
                if (this.elements.startScan) {
 | 
						||
                    this.elements.startScan.disabled = true;
 | 
						||
                    this.elements.startScan.classList.add('loading');
 | 
						||
                    this.elements.startScan.innerHTML = '<span class="btn-icon">[SCANNING]</span><span>Scanning...</span>';
 | 
						||
                }
 | 
						||
                if (this.elements.addToGraph) {
 | 
						||
                    this.elements.addToGraph.disabled = true;
 | 
						||
                    this.elements.addToGraph.classList.add('loading');
 | 
						||
                }
 | 
						||
                if (this.elements.stopScan) {
 | 
						||
                    this.elements.stopScan.disabled = false;
 | 
						||
                    this.elements.stopScan.classList.remove('loading');
 | 
						||
                    this.elements.stopScan.innerHTML = '<span class="btn-icon">[STOP]</span><span>Terminate Scan</span>';
 | 
						||
                }
 | 
						||
                if (this.elements.targetInput) this.elements.targetInput.disabled = true;
 | 
						||
                if (this.elements.maxDepth) this.elements.maxDepth.disabled = true;
 | 
						||
                if (this.elements.configureSettings) this.elements.configureSettings.disabled = true;
 | 
						||
                break;
 | 
						||
                
 | 
						||
            case 'idle':
 | 
						||
            case 'completed':
 | 
						||
            case 'failed':
 | 
						||
            case 'stopped':
 | 
						||
                this.isScanning = false;
 | 
						||
                if (this.graphManager) {
 | 
						||
                    this.graphManager.isScanning = false;
 | 
						||
                }
 | 
						||
                if (this.elements.startScan) {
 | 
						||
                    this.elements.startScan.disabled = !isQueueEmpty;
 | 
						||
                    this.elements.startScan.classList.remove('loading');
 | 
						||
                    this.elements.startScan.innerHTML = '<span class="btn-icon">[RUN]</span><span>Start Reconnaissance</span>';
 | 
						||
                }
 | 
						||
                if (this.elements.addToGraph) {
 | 
						||
                    this.elements.addToGraph.disabled = !isQueueEmpty;
 | 
						||
                    this.elements.addToGraph.classList.remove('loading');
 | 
						||
                }
 | 
						||
                if (this.elements.stopScan) {
 | 
						||
                    this.elements.stopScan.disabled = true;
 | 
						||
                    this.elements.stopScan.innerHTML = '<span class="btn-icon">[STOP]</span><span>Terminate Scan</span>';
 | 
						||
                }
 | 
						||
                if (this.elements.targetInput) this.elements.targetInput.disabled = false;
 | 
						||
                if (this.elements.maxDepth) this.elements.maxDepth.disabled = false;
 | 
						||
                if (this.elements.configureSettings) this.elements.configureSettings.disabled = false;
 | 
						||
                break;
 | 
						||
        }
 | 
						||
    }
 | 
						||
    
 | 
						||
    /**
 | 
						||
     * Load provider information
 | 
						||
     */
 | 
						||
    async loadProviders() {
 | 
						||
        try {
 | 
						||
            const response = await this.apiCall('/api/providers');
 | 
						||
            
 | 
						||
            if (response.success) {
 | 
						||
                this.updateProviderDisplay(response.providers);
 | 
						||
                this.buildSettingsModal(response.providers); // Updated to use new function
 | 
						||
                console.log('Providers loaded successfully');
 | 
						||
            }
 | 
						||
            
 | 
						||
        } catch (error) {
 | 
						||
            console.error('Failed to load providers:', error);
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Build the enhanced settings modal with provider configuration and API keys
 | 
						||
     * @param {Object} providers - Provider information from backend
 | 
						||
     */
 | 
						||
    buildSettingsModal(providers) {
 | 
						||
        this.buildProviderConfigSection(providers);
 | 
						||
        this.buildApiKeySection(providers);
 | 
						||
        this.updateSettingsCounts(providers);
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Build the provider configuration section with enable/disable checkboxes
 | 
						||
     * @param {Object} providers - Provider information
 | 
						||
     */
 | 
						||
    buildProviderConfigSection(providers) {
 | 
						||
        const providerConfigList = document.getElementById('provider-config-list');
 | 
						||
        if (!providerConfigList) return;
 | 
						||
 | 
						||
        providerConfigList.innerHTML = '';
 | 
						||
 | 
						||
        for (const [name, info] of Object.entries(providers)) {
 | 
						||
            const providerConfig = document.createElement('div');
 | 
						||
            providerConfig.className = 'provider-item';
 | 
						||
            
 | 
						||
            const statusClass = info.enabled ? 'enabled' : 'disabled';
 | 
						||
            const statusIcon = info.enabled ? '✓' : '✗';
 | 
						||
            
 | 
						||
            providerConfig.innerHTML = `
 | 
						||
                <div class="provider-header">
 | 
						||
                    <div class="provider-name">${info.display_name}</div>
 | 
						||
                    <div class="provider-status ${statusClass}">
 | 
						||
                        ${statusIcon} ${info.enabled ? 'Enabled' : 'Disabled'}
 | 
						||
                    </div>
 | 
						||
                </div>
 | 
						||
                <div class="status-row">
 | 
						||
                    <div class="status-label">
 | 
						||
                        <label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
 | 
						||
                            <input type="checkbox" 
 | 
						||
                                data-provider="${name}" 
 | 
						||
                                class="provider-toggle"
 | 
						||
                                ${info.enabled ? 'checked' : ''}
 | 
						||
                                style="appearance: none; width: 16px; height: 16px; border: 2px solid #555; background: #1a1a1a; cursor: pointer; position: relative;">
 | 
						||
                            <span>Auto-process with this provider</span>
 | 
						||
                        </label>
 | 
						||
                    </div>
 | 
						||
                </div>
 | 
						||
            `;
 | 
						||
            
 | 
						||
            providerConfigList.appendChild(providerConfig);
 | 
						||
        }
 | 
						||
 | 
						||
        // Add checkbox styling and event handlers
 | 
						||
        this.setupProviderCheckboxes();
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Setup provider checkbox styling and event handlers
 | 
						||
     */
 | 
						||
    setupProviderCheckboxes() {
 | 
						||
        const checkboxes = document.querySelectorAll('.provider-toggle');
 | 
						||
        
 | 
						||
        checkboxes.forEach(checkbox => {
 | 
						||
            // Apply existing checkbox styling
 | 
						||
            checkbox.style.cssText = `
 | 
						||
                appearance: none;
 | 
						||
                width: 16px;
 | 
						||
                height: 16px;
 | 
						||
                border: 2px solid #555;
 | 
						||
                background: #1a1a1a;
 | 
						||
                cursor: pointer;
 | 
						||
                position: relative;
 | 
						||
                border-radius: 3px;
 | 
						||
                transition: all 0.3s ease;
 | 
						||
            `;
 | 
						||
            
 | 
						||
            // Update visual state
 | 
						||
            this.updateCheckboxAppearance(checkbox);
 | 
						||
            
 | 
						||
            // Add change event handler
 | 
						||
            checkbox.addEventListener('change', (e) => {
 | 
						||
                this.updateCheckboxAppearance(e.target);
 | 
						||
            });
 | 
						||
        });
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Add CSS for checkbox styling since we're using existing styles
 | 
						||
     */
 | 
						||
    addCheckboxStyling() {
 | 
						||
        // Add CSS for the checkboxes to work with existing styles
 | 
						||
        const style = document.createElement('style');
 | 
						||
        style.textContent = `
 | 
						||
            .provider-toggle[data-checked="true"]::after {
 | 
						||
                content: '✓';
 | 
						||
                position: absolute;
 | 
						||
                top: -2px;
 | 
						||
                left: 2px;
 | 
						||
                color: #1a1a1a;
 | 
						||
                font-size: 12px;
 | 
						||
                font-weight: bold;
 | 
						||
            }
 | 
						||
            
 | 
						||
            .provider-toggle:hover {
 | 
						||
                border-color: #00ff41;
 | 
						||
            }
 | 
						||
            
 | 
						||
            .api-key-status-row {
 | 
						||
                transition: all 0.3s ease;
 | 
						||
            }
 | 
						||
            
 | 
						||
            .provider-item {
 | 
						||
                margin-bottom: 1rem;
 | 
						||
            }
 | 
						||
            
 | 
						||
            .provider-item:last-child {
 | 
						||
                margin-bottom: 0;
 | 
						||
            }
 | 
						||
        `;
 | 
						||
        document.head.appendChild(style);
 | 
						||
    }
 | 
						||
 | 
						||
 | 
						||
    /**
 | 
						||
     * Update checkbox appearance based on checked state
 | 
						||
     */
 | 
						||
    updateCheckboxAppearance(checkbox) {
 | 
						||
        if (checkbox.checked) {
 | 
						||
            checkbox.style.background = '#00ff41';
 | 
						||
            checkbox.style.borderColor = '#00ff41';
 | 
						||
            checkbox.style.setProperty('content', '"✓"', 'important');
 | 
						||
            
 | 
						||
            // Add checkmark via pseudo-element simulation
 | 
						||
            checkbox.setAttribute('data-checked', 'true');
 | 
						||
        } else {
 | 
						||
            checkbox.style.background = '#1a1a1a';
 | 
						||
            checkbox.style.borderColor = '#555';
 | 
						||
            checkbox.removeAttribute('data-checked');
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Enhanced API key section builder - FIXED to always allow API key input
 | 
						||
     * @param {Object} providers - Provider information
 | 
						||
     */
 | 
						||
    buildApiKeySection(providers) {
 | 
						||
        const apiKeyInputs = document.getElementById('api-key-inputs');
 | 
						||
        if (!apiKeyInputs) return;
 | 
						||
 | 
						||
        apiKeyInputs.innerHTML = '';
 | 
						||
        let hasApiKeyProviders = false;
 | 
						||
 | 
						||
        for (const [name, info] of Object.entries(providers)) {
 | 
						||
            if (info.requires_api_key) {
 | 
						||
                hasApiKeyProviders = true;
 | 
						||
                
 | 
						||
                const inputGroup = document.createElement('div');
 | 
						||
                inputGroup.className = 'provider-item';
 | 
						||
                
 | 
						||
                // Check if API key is set via backend (not clearable) or frontend (clearable)
 | 
						||
                const isBackendConfigured = info.api_key_source === 'backend';
 | 
						||
                
 | 
						||
                if (info.api_key_configured && isBackendConfigured) {
 | 
						||
                    // API key is configured via backend - show status only
 | 
						||
                    inputGroup.innerHTML = `
 | 
						||
                        <div class="provider-header">
 | 
						||
                            <div class="provider-name">${info.display_name}</div>
 | 
						||
                            <div class="provider-status enabled">✓ Backend Configured</div>
 | 
						||
                        </div>
 | 
						||
                        <div class="api-key-status-row" style="padding: 0.75rem; background: rgba(0, 255, 65, 0.1); border-radius: 4px; border: 1px solid rgba(0, 255, 65, 0.3);">
 | 
						||
                            <div style="display: flex; justify-content: space-between; align-items: center;">
 | 
						||
                                <div>
 | 
						||
                                    <div class="status-value">API Key Active</div>
 | 
						||
                                    <div class="status-label" style="font-size: 0.8rem;">
 | 
						||
                                        Configured via environment variable
 | 
						||
                                    </div>
 | 
						||
                                </div>
 | 
						||
                            </div>
 | 
						||
                        </div>
 | 
						||
                    `;
 | 
						||
                } else if (info.api_key_configured && !isBackendConfigured) {
 | 
						||
                    // API key is configured via frontend - show status with clear option
 | 
						||
                    inputGroup.innerHTML = `
 | 
						||
                        <div class="provider-header">
 | 
						||
                            <div class="provider-name">${info.display_name}</div>
 | 
						||
                            <div class="provider-status enabled">✓ Web Configured</div>
 | 
						||
                        </div>
 | 
						||
                        <div class="api-key-status-row" style="padding: 0.75rem; background: rgba(0, 255, 65, 0.1); border-radius: 4px; border: 1px solid rgba(0, 255, 65, 0.3);">
 | 
						||
                            <div style="display: flex; justify-content: space-between; align-items: center;">
 | 
						||
                                <div>
 | 
						||
                                    <div class="status-value">API Key Active</div>
 | 
						||
                                    <div class="status-label" style="font-size: 0.8rem;">
 | 
						||
                                        Set via web interface (session-only)
 | 
						||
                                    </div>
 | 
						||
                                </div>
 | 
						||
                                <button class="clear-api-key-btn btn-secondary" data-provider="${name}" style="padding: 0.4rem 0.8rem; font-size: 0.8rem;">
 | 
						||
                                    <span class="btn-icon">[×]</span>
 | 
						||
                                    <span>Clear</span>
 | 
						||
                                </button>
 | 
						||
                            </div>
 | 
						||
                        </div>
 | 
						||
                    `;
 | 
						||
                } else {
 | 
						||
                    // API key not configured - ALWAYS show input field
 | 
						||
                    const statusClass = info.enabled ? 'enabled' : 'api-key-required';
 | 
						||
                    const statusText = info.enabled ? '○ Ready for API Key' : '⚠️ API Key Required';
 | 
						||
                    
 | 
						||
                    inputGroup.innerHTML = `
 | 
						||
                        <div class="provider-header">
 | 
						||
                            <div class="provider-name">${info.display_name}</div>
 | 
						||
                            <div class="provider-status ${statusClass}">
 | 
						||
                                ${statusText}
 | 
						||
                            </div>
 | 
						||
                        </div>
 | 
						||
                        <div class="input-group">
 | 
						||
                            <label for="${name}-api-key">API Key</label>
 | 
						||
                            <input type="password" 
 | 
						||
                                id="${name}-api-key" 
 | 
						||
                                data-provider="${name}" 
 | 
						||
                                placeholder="Enter ${info.display_name} API Key"
 | 
						||
                                autocomplete="off">
 | 
						||
                            <div class="apikey-help">
 | 
						||
                                ${info.api_key_help || `Provides enhanced ${info.display_name.toLowerCase()} data and context.`}
 | 
						||
                                ${!info.enabled ? ' Enable the provider above to use this API key.' : ''}
 | 
						||
                            </div>
 | 
						||
                        </div>
 | 
						||
                    `;
 | 
						||
                }
 | 
						||
                
 | 
						||
                apiKeyInputs.appendChild(inputGroup);
 | 
						||
            }
 | 
						||
        }
 | 
						||
        
 | 
						||
        if (!hasApiKeyProviders) {
 | 
						||
            apiKeyInputs.innerHTML = `
 | 
						||
                <div class="status-row">
 | 
						||
                    <div class="status-label">No providers require API keys</div>
 | 
						||
                    <div class="status-value">All Active</div>
 | 
						||
                </div>
 | 
						||
            `;
 | 
						||
        }
 | 
						||
        
 | 
						||
        // Setup clear button event handlers
 | 
						||
        this.setupApiKeyClearHandlers();
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Setup API key clear button handlers
 | 
						||
     */
 | 
						||
    setupApiKeyClearHandlers() {
 | 
						||
        document.querySelectorAll('.clear-api-key-btn').forEach(button => {
 | 
						||
            button.addEventListener('click', (e) => {
 | 
						||
                e.preventDefault();
 | 
						||
                const provider = e.currentTarget.dataset.provider;
 | 
						||
                this.clearSingleApiKey(provider, e.currentTarget);
 | 
						||
            });
 | 
						||
        });
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Clear a single API key with immediate feedback
 | 
						||
     */
 | 
						||
    async clearSingleApiKey(provider, buttonElement) {
 | 
						||
        try {
 | 
						||
            // Show immediate feedback
 | 
						||
            const originalContent = buttonElement.innerHTML;
 | 
						||
            buttonElement.innerHTML = '<span class="btn-icon">[...]</span><span>Clearing...</span>';
 | 
						||
            buttonElement.disabled = true;
 | 
						||
            
 | 
						||
            const response = await this.apiCall('/api/config/api-keys', 'POST', { [provider]: '' });
 | 
						||
            
 | 
						||
            if (response.success) {
 | 
						||
                // Find the parent container and update it
 | 
						||
                const providerContainer = buttonElement.closest('.provider-item');
 | 
						||
                const statusRow = providerContainer.querySelector('.api-key-status-row');
 | 
						||
                
 | 
						||
                // Animate out the current status
 | 
						||
                statusRow.style.transition = 'all 0.3s ease';
 | 
						||
                statusRow.style.opacity = '0';
 | 
						||
                statusRow.style.transform = 'translateX(-10px)';
 | 
						||
                
 | 
						||
                setTimeout(() => {
 | 
						||
                    // Replace with input field
 | 
						||
                    const providerName = buttonElement.dataset.provider;
 | 
						||
                    const apiKeySection = this.elements.apiKeyInputs;
 | 
						||
                    
 | 
						||
                    // Rebuild the API key section to reflect changes
 | 
						||
                    this.loadProviders();
 | 
						||
                    
 | 
						||
                    this.showSuccess(`API key for ${provider} has been cleared.`);
 | 
						||
                }, 300);
 | 
						||
                
 | 
						||
            } else {
 | 
						||
                throw new Error(response.error || 'Failed to clear API key');
 | 
						||
            }
 | 
						||
        } catch (error) {
 | 
						||
            // Restore button on error
 | 
						||
            buttonElement.innerHTML = originalContent;
 | 
						||
            buttonElement.disabled = false;
 | 
						||
            this.showError(`Error clearing API key: ${error.message}`);
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Update settings modal counts
 | 
						||
     */
 | 
						||
    updateSettingsCounts(providers) {
 | 
						||
        const providerCount = Object.keys(providers).length;
 | 
						||
        const apiKeyCount = Object.values(providers).filter(p => p.requires_api_key).length;
 | 
						||
        
 | 
						||
        const providerCountElement = document.getElementById('provider-count');
 | 
						||
        const apiKeyCountElement = document.getElementById('api-key-count');
 | 
						||
        
 | 
						||
        if (providerCountElement) providerCountElement.textContent = providerCount;
 | 
						||
        if (apiKeyCountElement) apiKeyCountElement.textContent = apiKeyCount;
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Enhanced save settings function
 | 
						||
     */
 | 
						||
    async saveSettings() {
 | 
						||
        try {
 | 
						||
            const settings = {
 | 
						||
                apiKeys: {},
 | 
						||
                providerSettings: {}
 | 
						||
            };
 | 
						||
            
 | 
						||
            // Collect API key inputs
 | 
						||
            const apiKeyInputs = document.querySelectorAll('#api-key-inputs input[type="password"]');
 | 
						||
            apiKeyInputs.forEach(input => {
 | 
						||
                const provider = input.dataset.provider;
 | 
						||
                const value = input.value.trim();
 | 
						||
                if (provider && value) {
 | 
						||
                    settings.apiKeys[provider] = value;
 | 
						||
                }
 | 
						||
            });
 | 
						||
            
 | 
						||
            // Collect provider enable/disable settings
 | 
						||
            const providerCheckboxes = document.querySelectorAll('.provider-toggle');
 | 
						||
            providerCheckboxes.forEach(checkbox => {
 | 
						||
                const provider = checkbox.dataset.provider;
 | 
						||
                if (provider) {
 | 
						||
                    settings.providerSettings[provider] = {
 | 
						||
                        enabled: checkbox.checked
 | 
						||
                    };
 | 
						||
                }
 | 
						||
            });
 | 
						||
            
 | 
						||
            // Save API keys if any
 | 
						||
            if (Object.keys(settings.apiKeys).length > 0) {
 | 
						||
                const apiKeyResponse = await this.apiCall('/api/config/api-keys', 'POST', settings.apiKeys);
 | 
						||
                if (!apiKeyResponse.success) {
 | 
						||
                    throw new Error(apiKeyResponse.error || 'Failed to save API keys');
 | 
						||
                }
 | 
						||
            }
 | 
						||
            
 | 
						||
            // Save provider settings if any
 | 
						||
            if (Object.keys(settings.providerSettings).length > 0) {
 | 
						||
                const providerResponse = await this.apiCall('/api/config/providers', 'POST', settings.providerSettings);
 | 
						||
                if (!providerResponse.success) {
 | 
						||
                    throw new Error(providerResponse.error || 'Failed to save provider settings');
 | 
						||
                }
 | 
						||
            }
 | 
						||
            
 | 
						||
            this.showSuccess('Settings saved successfully');
 | 
						||
            this.hideSettingsModal();
 | 
						||
            
 | 
						||
            // Reload providers to reflect changes
 | 
						||
            this.loadProviders();
 | 
						||
            
 | 
						||
        } catch (error) {
 | 
						||
            this.showError(`Error saving settings: ${error.message}`);
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Reset settings to defaults
 | 
						||
     */
 | 
						||
    async resetSettings() {
 | 
						||
        try {
 | 
						||
            // Clear all API key inputs
 | 
						||
            const apiKeyInputs = document.querySelectorAll('#api-key-inputs input[type="password"]');
 | 
						||
            apiKeyInputs.forEach(input => {
 | 
						||
                input.value = '';
 | 
						||
            });
 | 
						||
            
 | 
						||
            // Reset all provider checkboxes to enabled (default)
 | 
						||
            const providerCheckboxes = document.querySelectorAll('.provider-toggle');
 | 
						||
            providerCheckboxes.forEach(checkbox => {
 | 
						||
                checkbox.checked = true;
 | 
						||
                this.updateCheckboxAppearance(checkbox);
 | 
						||
            });
 | 
						||
            
 | 
						||
            // Reset recursion depth to default
 | 
						||
            const depthSelect = document.getElementById('max-depth');
 | 
						||
            if (depthSelect) {
 | 
						||
                depthSelect.value = '2';
 | 
						||
            }
 | 
						||
            
 | 
						||
            this.showInfo('Settings reset to defaults');
 | 
						||
            
 | 
						||
        } catch (error) {
 | 
						||
            this.showError(`Error resetting settings: ${error.message}`);
 | 
						||
        }
 | 
						||
    }
 | 
						||
    
 | 
						||
    /**
 | 
						||
     * Update provider display
 | 
						||
     * @param {Object} providers - Provider information
 | 
						||
     */
 | 
						||
    updateProviderDisplay(providers) {
 | 
						||
        if (!this.elements.providerList) return;
 | 
						||
        
 | 
						||
        this.elements.providerList.innerHTML = '';
 | 
						||
        
 | 
						||
        for (const [name, info] of Object.entries(providers)) {
 | 
						||
            const providerItem = document.createElement('div');
 | 
						||
            providerItem.className = 'provider-item fade-in';
 | 
						||
            
 | 
						||
            let statusClass = 'disabled';
 | 
						||
            let statusText = 'Disabled';
 | 
						||
            
 | 
						||
            if (info.enabled) {
 | 
						||
                statusClass = 'enabled';
 | 
						||
                statusText = 'Enabled';
 | 
						||
            } else if (info.requires_api_key) {
 | 
						||
                statusClass = 'api-key-required';
 | 
						||
                statusText = 'API Key Required';
 | 
						||
            }
 | 
						||
            
 | 
						||
            providerItem.innerHTML = `
 | 
						||
                <div class="provider-header">
 | 
						||
                    <div class="provider-name">${info.display_name}</div>
 | 
						||
                    <div class="provider-status ${statusClass}">${statusText}</div>
 | 
						||
                </div>
 | 
						||
                <div class="provider-stats">
 | 
						||
                    <div class="provider-stat">
 | 
						||
                        <span class="provider-stat-label">Requests:</span>
 | 
						||
                        <span class="provider-stat-value">${info.statistics.total_requests || 0}</span>
 | 
						||
                    </div>
 | 
						||
                    <div class="provider-stat">
 | 
						||
                        <span class="provider-stat-label">Success Rate:</span>
 | 
						||
                        <span class="provider-stat-value">${(info.statistics.success_rate || 0).toFixed(1)}%</span>
 | 
						||
                    </div>
 | 
						||
                    <div class="provider-stat">
 | 
						||
                        <span class="provider-stat-label">Relationships:</span>
 | 
						||
                        <span class="provider-stat-value">${info.statistics.relationships_found || 0}</span>
 | 
						||
                    </div>
 | 
						||
                    <div class="provider-stat">
 | 
						||
                        <span class="provider-stat-label">Rate Limit:</span>
 | 
						||
                        <span class="provider-stat-value">${info.rate_limit}/min</span>
 | 
						||
                    </div>
 | 
						||
                </div>
 | 
						||
            `;
 | 
						||
            
 | 
						||
            this.elements.providerList.appendChild(providerItem);
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * UPDATED: Enhanced node details HTML generation for unified data model
 | 
						||
     * Now properly groups attributes by provider/type with organized sections
 | 
						||
     */
 | 
						||
    generateNodeDetailsHtml(node) {
 | 
						||
        if (!node) return '<div class="detail-row"><span class="detail-value">Details not available.</span></div>';
 | 
						||
 | 
						||
        let detailsHtml = '<div class="modal-details">';
 | 
						||
 | 
						||
        // Node Header - compact
 | 
						||
        detailsHtml += `
 | 
						||
            <div class="node-header">
 | 
						||
                <div class="node-type-badge node-type-${node.type}">${this.formatStatus(node.type)}</div>
 | 
						||
                <div class="node-id-display">${node.id}</div>
 | 
						||
            </div>
 | 
						||
        `;
 | 
						||
 | 
						||
        // Quick Stats Bar - compact
 | 
						||
        const incomingCount = node.incoming_edges ? node.incoming_edges.length : 0;
 | 
						||
        const outgoingCount = node.outgoing_edges ? node.outgoing_edges.length : 0;
 | 
						||
        
 | 
						||
        detailsHtml += `
 | 
						||
            <div class="quick-stats">
 | 
						||
                <div class="stat-item">
 | 
						||
                    <span class="stat-icon">←</span>
 | 
						||
                    <span class="stat-value">${incomingCount}</span>
 | 
						||
                    <span class="stat-label">In</span>
 | 
						||
                </div>
 | 
						||
                <div class="stat-item">
 | 
						||
                    <span class="stat-icon">→</span>
 | 
						||
                    <span class="stat-value">${outgoingCount}</span>
 | 
						||
                    <span class="stat-label">Out</span>
 | 
						||
                </div>
 | 
						||
            </div>
 | 
						||
        `;
 | 
						||
 | 
						||
        // Handle different node types
 | 
						||
        if (node.type === 'correlation_object') {
 | 
						||
            detailsHtml += this.generateCorrelationDetails(node);
 | 
						||
        } else if (node.type === 'large_entity') {
 | 
						||
            detailsHtml += this.generateLargeEntityDetails(node);
 | 
						||
        } else {
 | 
						||
            detailsHtml += this.generateStandardNodeDetails(node);
 | 
						||
        }
 | 
						||
 | 
						||
        detailsHtml += '</div>';
 | 
						||
        return detailsHtml;
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * UPDATED: Generate details for standard nodes with organized attribute grouping and data warnings
 | 
						||
     */
 | 
						||
    generateStandardNodeDetails(node) {
 | 
						||
        let html = '';
 | 
						||
 | 
						||
        // Check for and display a crt.sh data warning if it exists
 | 
						||
        const crtshWarningAttr = this.findAttributeByName(node.attributes, 'crtsh_data_warning');
 | 
						||
        if (crtshWarningAttr) {
 | 
						||
            html += `
 | 
						||
                <div class="modal-section" style="border-left: 3px solid #ff9900; background: rgba(255, 153, 0, 0.05);">
 | 
						||
                    <details open>
 | 
						||
                        <summary style="color: #ff9900;">
 | 
						||
                            <span>⚠️ Data Integrity Warning</span>
 | 
						||
                        </summary>
 | 
						||
                        <div class="modal-section-content">
 | 
						||
                            <p class="placeholder-subtext" style="color: #e0e0e0; font-size: 0.8rem; line-height: 1.5;">
 | 
						||
                                ${this.escapeHtml(crtshWarningAttr.value)}
 | 
						||
                                <br><br>
 | 
						||
                                This can occur for very large domains (e.g., google.com) where crt.sh may return a limited subset of all available certificates. As a result, the certificate status may not be fully representative.
 | 
						||
                            </p>
 | 
						||
                        </div>
 | 
						||
                    </details>
 | 
						||
                </div>
 | 
						||
            `;
 | 
						||
        }
 | 
						||
 | 
						||
        // Relationships sections
 | 
						||
        html += this.generateRelationshipsSection(node);
 | 
						||
 | 
						||
        // UPDATED: Enhanced attributes section with intelligent grouping (no formatting)
 | 
						||
        if (node.attributes && Array.isArray(node.attributes) && node.attributes.length > 0) {
 | 
						||
            html += this.generateOrganizedAttributesSection(node.attributes, node.type);
 | 
						||
        }
 | 
						||
 | 
						||
        // Description section
 | 
						||
        html += this.generateDescriptionSection(node);
 | 
						||
 | 
						||
        // Metadata section (collapsed by default)
 | 
						||
        html += this.generateMetadataSection(node);
 | 
						||
 | 
						||
        return html;
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Helper method to find an attribute by name in the standardized attributes list
 | 
						||
     * @param {Array} attributes - List of StandardAttribute objects
 | 
						||
     * @param {string} name - Attribute name to find
 | 
						||
     * @returns {Object|null} The attribute object if found, null otherwise
 | 
						||
     */
 | 
						||
    findAttributeByName(attributes, name) {
 | 
						||
        if (!Array.isArray(attributes)) {
 | 
						||
            return null;
 | 
						||
        }
 | 
						||
        return attributes.find(attr => attr.name === name) || null;
 | 
						||
    }
 | 
						||
 | 
						||
    generateOrganizedAttributesSection(attributes, nodeType) {
 | 
						||
        if (!Array.isArray(attributes) || attributes.length === 0) {
 | 
						||
            return '';
 | 
						||
        }
 | 
						||
 | 
						||
        const groups = this.groupAttributesByProviderAndType(attributes, nodeType);
 | 
						||
        let html = '';
 | 
						||
        
 | 
						||
        const sortedGroups = Object.entries(groups).sort((a, b) => {
 | 
						||
            const priorityOrder = { 'high': 0, 'medium': 1, 'low': 2 };
 | 
						||
            return priorityOrder[a[1].priority] - priorityOrder[b[1].priority];
 | 
						||
        });
 | 
						||
        
 | 
						||
        for (const [groupName, groupData] of sortedGroups) {
 | 
						||
            if (groupData.attributes.length === 0) continue;
 | 
						||
            
 | 
						||
            const isOpen = groupData.priority === 'high';
 | 
						||
            
 | 
						||
            html += `
 | 
						||
                <div class="modal-section">
 | 
						||
                    <details ${isOpen ? 'open' : ''}>
 | 
						||
                        <summary>
 | 
						||
                            <span>${groupData.icon} ${groupName}</span>
 | 
						||
                            <span class="merge-badge">${groupData.attributes.length}</span>
 | 
						||
                        </summary>
 | 
						||
                        <div class="modal-section-content">
 | 
						||
                            <div class="attribute-list">
 | 
						||
            `;
 | 
						||
 | 
						||
            groupData.attributes.forEach(attr => {
 | 
						||
                html += `
 | 
						||
                    <div class="attribute-item-compact">
 | 
						||
                        <span class="attribute-key-compact">${this.escapeHtml(attr.name || 'Unknown')}</span>
 | 
						||
                        <span class="attribute-value-compact">${this.formatAttributeValue(attr)}</span>
 | 
						||
                    </div>
 | 
						||
                `;
 | 
						||
            });
 | 
						||
 | 
						||
            html += '</div></div></details></div>';
 | 
						||
        }
 | 
						||
 | 
						||
        return html;
 | 
						||
    }
 | 
						||
 | 
						||
    formatAttributeValue(attr) {
 | 
						||
        const value = attr.value;
 | 
						||
        const name = attr.name || '';
 | 
						||
        
 | 
						||
        if (value === null || value === undefined) {
 | 
						||
            return 'N/A';
 | 
						||
        }
 | 
						||
        
 | 
						||
        if (Array.isArray(value)) {
 | 
						||
            if (value.length === 0) {
 | 
						||
                return 'Empty Array';
 | 
						||
            }
 | 
						||
            
 | 
						||
            // ENHANCED: Special handling for specific DNS record types
 | 
						||
            if (name.endsWith('_records') || name.includes('record')) {
 | 
						||
                const recordType = name.replace('_records', '').toUpperCase();
 | 
						||
                
 | 
						||
                // Format nicely for DNS records
 | 
						||
                if (value.length <= 5) {
 | 
						||
                    const formattedRecords = value.map(record => {
 | 
						||
                        // Add record type prefix if not already present
 | 
						||
                        if (recordType !== 'DNS' && !record.includes(':')) {
 | 
						||
                            return `${recordType}: ${record}`;
 | 
						||
                        }
 | 
						||
                        return record;
 | 
						||
                    });
 | 
						||
                    return this.escapeHtml(formattedRecords.join('\n'));
 | 
						||
                } else {
 | 
						||
                    const preview = value.slice(0, 3).map(record => {
 | 
						||
                        if (recordType !== 'DNS' && !record.includes(':')) {
 | 
						||
                            return `${recordType}: ${record}`;
 | 
						||
                        }
 | 
						||
                        return record;
 | 
						||
                    }).join('\n');
 | 
						||
                    return this.escapeHtml(`${preview}\n... (+${value.length - 3} more ${recordType} records)`);
 | 
						||
                }
 | 
						||
            }
 | 
						||
            
 | 
						||
            // For other arrays (existing logic)
 | 
						||
            if (value.length <= 3) {
 | 
						||
                return this.escapeHtml(value.join(', '));
 | 
						||
            } else {
 | 
						||
                const preview = value.slice(0, 2).join(', ');
 | 
						||
                return this.escapeHtml(`${preview} ... (${value.length} total)`);
 | 
						||
            }
 | 
						||
        }
 | 
						||
        
 | 
						||
        if (typeof value === 'object') {
 | 
						||
            return 'Object';
 | 
						||
        }
 | 
						||
        
 | 
						||
        return this.escapeHtml(String(value));
 | 
						||
    }
 | 
						||
 | 
						||
    groupAttributesByProviderAndType(attributes, nodeType) {
 | 
						||
        if (!Array.isArray(attributes) || attributes.length === 0) {
 | 
						||
            return {};
 | 
						||
        }
 | 
						||
 | 
						||
        const groups = {
 | 
						||
            'DNS Records': { icon: '📋', priority: 'high', attributes: [] },
 | 
						||
            'Certificate Information': { icon: '🔒', priority: 'high', attributes: [] },
 | 
						||
            'Network Information': { icon: '🌐', priority: 'high', attributes: [] },
 | 
						||
            'Provider Data': { icon: '📊', priority: 'medium', attributes: [] },
 | 
						||
            'Technical Details': { icon: '⚙️', priority: 'low', attributes: [] }
 | 
						||
        };
 | 
						||
 | 
						||
        for (const attr of attributes) {
 | 
						||
            const provider = (attr.provider || '').toLowerCase();
 | 
						||
            const name = (attr.name || '').toLowerCase();
 | 
						||
            const type = (attr.type || '').toLowerCase();
 | 
						||
 | 
						||
            let assigned = false;
 | 
						||
 | 
						||
            // ENHANCED: Better DNS record detection for specific record types
 | 
						||
            if (provider === 'dns' || 
 | 
						||
                name.endsWith('_records') ||  // Catches a_records, mx_records, txt_records, etc.
 | 
						||
                name.includes('record') || 
 | 
						||
                ['ptr', 'mx', 'cname', 'ns', 'txt', 'soa', 'srv', 'caa', 'a_records', 'aaaa_records'].some(keyword => name.includes(keyword))) {
 | 
						||
                groups['DNS Records'].attributes.push(attr);
 | 
						||
                assigned = true;
 | 
						||
            }
 | 
						||
            // Certificate-related attributes
 | 
						||
            else if (provider === 'crtsh' || name.startsWith('cert_') || 
 | 
						||
                    ['certificate', 'ssl', 'tls', 'issuer', 'validity', 'san'].some(keyword => name.includes(keyword))) {
 | 
						||
                groups['Certificate Information'].attributes.push(attr);
 | 
						||
                assigned = true;
 | 
						||
            }
 | 
						||
            // Network/Shodan attributes
 | 
						||
            else if (provider === 'shodan' || 
 | 
						||
                    ['port', 'service', 'banner', 'asn', 'organization', 'country', 'city', 'network'].some(keyword => name.includes(keyword))) {
 | 
						||
                groups['Network Information'].attributes.push(attr);
 | 
						||
                assigned = true;
 | 
						||
            }
 | 
						||
            // Provider-specific data
 | 
						||
            else if (provider && ['shodan_', 'crtsh_', 'dns_'].some(prefix => name.startsWith(prefix))) {
 | 
						||
                groups['Provider Data'].attributes.push(attr);
 | 
						||
                assigned = true;
 | 
						||
            }
 | 
						||
            
 | 
						||
            // If not assigned to any specific group, put in technical details
 | 
						||
            if (!assigned) {
 | 
						||
                groups['Technical Details'].attributes.push(attr);
 | 
						||
            }
 | 
						||
        }
 | 
						||
 | 
						||
        // Remove empty groups
 | 
						||
        Object.keys(groups).forEach(groupName => {
 | 
						||
            if (groups[groupName].attributes.length === 0) {
 | 
						||
                delete groups[groupName];
 | 
						||
            }
 | 
						||
        });
 | 
						||
 | 
						||
        return groups;
 | 
						||
    }
 | 
						||
 | 
						||
    formatEdgeLabel(relationshipType, confidence) {
 | 
						||
        if (!relationshipType) return '';
 | 
						||
 | 
						||
        const confidenceText = confidence >= 0.8 ? '●' : confidence >= 0.6 ? '◐' : '○';
 | 
						||
        return `${relationshipType} ${confidenceText}`;
 | 
						||
    }
 | 
						||
 | 
						||
    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>`;
 | 
						||
        
 | 
						||
        // UPDATED: Use raw provider name (no formatting)
 | 
						||
        if (edge.source_provider) {
 | 
						||
            tooltip += `<div style="color: #999; margin-bottom: 2px;">Provider: ${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;
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * UPDATED: Enhanced correlation details showing the correlated attribute clearly (no formatting)
 | 
						||
     */
 | 
						||
    generateCorrelationDetails(node) {
 | 
						||
        const attributes = node.attributes || [];
 | 
						||
        const correlationValueAttr = attributes.find(attr => attr.name === 'correlation_value');
 | 
						||
        const value = correlationValueAttr ? correlationValueAttr.value : 'Unknown';
 | 
						||
 | 
						||
        const metadataAttr = attributes.find(attr => attr.name === 'correlation_value');
 | 
						||
        const metadata = metadataAttr ? metadataAttr.metadata : {};
 | 
						||
        const correlatedNodes = metadata.correlated_nodes || [];
 | 
						||
        const sources = metadata.sources || [];
 | 
						||
        
 | 
						||
        let html = '';
 | 
						||
        
 | 
						||
        // Show what attribute is being correlated (raw names)
 | 
						||
        const primarySource = sources.length > 0 ? sources[0].attribute : 'unknown';
 | 
						||
        
 | 
						||
        html += `
 | 
						||
            <div class="modal-section">
 | 
						||
                <details open>
 | 
						||
                    <summary>
 | 
						||
                        <span>🔗 Correlation: ${primarySource}</span>
 | 
						||
                        <span class="merge-badge">${correlatedNodes.length}</span>
 | 
						||
                    </summary>
 | 
						||
                    <div class="modal-section-content">
 | 
						||
                        <div class="attribute-list">
 | 
						||
                            <div class="attribute-item-compact">
 | 
						||
                                <span class="attribute-key-compact">Shared Value</span>
 | 
						||
                                <span class="attribute-value-compact"><code>${this.escapeHtml(String(value))}</code></span>
 | 
						||
                            </div>
 | 
						||
                            <div class="attribute-item-compact">
 | 
						||
                                <span class="attribute-key-compact">Attribute Source</span>
 | 
						||
                                <span class="attribute-value-compact">${primarySource}</span>
 | 
						||
                            </div>
 | 
						||
                            <div class="attribute-item-compact">
 | 
						||
                                <span class="attribute-key-compact">Correlated Nodes</span>
 | 
						||
                                <span class="attribute-value-compact">${correlatedNodes.length} nodes</span>
 | 
						||
                            </div>
 | 
						||
                        </div>
 | 
						||
                    </div>
 | 
						||
                </details>
 | 
						||
            </div>
 | 
						||
        `;
 | 
						||
        
 | 
						||
        // Show the correlated nodes
 | 
						||
        if (correlatedNodes.length > 0) {
 | 
						||
            html += `
 | 
						||
                <div class="modal-section">
 | 
						||
                    <details>
 | 
						||
                        <summary>🌐 Correlated Nodes (${correlatedNodes.length})</summary>
 | 
						||
                        <div class="modal-section-content">
 | 
						||
                            <div class="relationship-compact">
 | 
						||
            `;
 | 
						||
            
 | 
						||
            correlatedNodes.forEach(nodeId => {
 | 
						||
                html += `
 | 
						||
                    <div class="relationship-compact-item">
 | 
						||
                        <span class="node-link-compact" data-node-id="${nodeId}">${nodeId}</span>
 | 
						||
                    </div>
 | 
						||
                `;
 | 
						||
            });
 | 
						||
            
 | 
						||
            html += '</div></div></details></div>';
 | 
						||
        }
 | 
						||
        
 | 
						||
        return html;
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * UPDATED: Generate large entity details using unified data model
 | 
						||
     */
 | 
						||
    generateLargeEntityDetails(node) {
 | 
						||
        // Look for attributes in the unified model structure
 | 
						||
        const attributes = node.attributes || [];
 | 
						||
        const nodesAttribute = attributes.find(attr => attr.name === 'nodes');
 | 
						||
        const countAttribute = attributes.find(attr => attr.name === 'count');
 | 
						||
        const nodeTypeAttribute = attributes.find(attr => attr.name === 'node_type');
 | 
						||
        const sourceProviderAttribute = attributes.find(attr => attr.name === 'source_provider');
 | 
						||
        const discoveryDepthAttribute = attributes.find(attr => attr.name === 'discovery_depth');
 | 
						||
        
 | 
						||
        const nodes = nodesAttribute ? nodesAttribute.value : [];
 | 
						||
        const count = countAttribute ? countAttribute.value : 0;
 | 
						||
        const nodeType = nodeTypeAttribute ? nodeTypeAttribute.value : 'nodes';
 | 
						||
        const sourceProvider = sourceProviderAttribute ? sourceProviderAttribute.value : 'Unknown';
 | 
						||
        const discoveryDepth = discoveryDepthAttribute ? discoveryDepthAttribute.value : 'Unknown';
 | 
						||
        
 | 
						||
        let html = `
 | 
						||
            <div class="modal-section">
 | 
						||
                <details open>
 | 
						||
                    <summary>📦 Entity Summary</summary>
 | 
						||
                    <div class="modal-section-content">
 | 
						||
                        <div class="attribute-list">
 | 
						||
                            <div class="attribute-item-compact">
 | 
						||
                                <span class="attribute-key-compact">Contains</span>
 | 
						||
                                <span class="attribute-value-compact">${count} ${nodeType}s</span>
 | 
						||
                            </div>
 | 
						||
                            <div class="attribute-item-compact">
 | 
						||
                                <span class="attribute-key-compact">Provider</span>
 | 
						||
                                <span class="attribute-value-compact">${sourceProvider}</span>
 | 
						||
                            </div>
 | 
						||
                            <div class="attribute-item-compact">
 | 
						||
                                <span class="attribute-key-compact">Depth</span>
 | 
						||
                                <span class="attribute-value-compact">${discoveryDepth}</span>
 | 
						||
                            </div>
 | 
						||
                        </div>
 | 
						||
                    </div>
 | 
						||
                </details>
 | 
						||
            </div>
 | 
						||
            
 | 
						||
            <div class="modal-section">
 | 
						||
                <details open>
 | 
						||
                    <summary>📋 Contained ${nodeType}s (${Array.isArray(nodes) ? nodes.length : 0})</summary>
 | 
						||
                    <div class="modal-section-content">
 | 
						||
                        <div class="relationship-compact">
 | 
						||
        `;
 | 
						||
        
 | 
						||
        const largeEntityId = node.id;
 | 
						||
 | 
						||
        if (Array.isArray(nodes)) {
 | 
						||
            nodes.forEach(innerNodeId => {
 | 
						||
                html += `
 | 
						||
                    <div class="relationship-compact-item">
 | 
						||
                        <span class="node-link-compact" data-node-id="${innerNodeId}">${innerNodeId}</span>
 | 
						||
                        <button class="btn-icon-small extract-node-btn" 
 | 
						||
                                title="Extract to graph"
 | 
						||
                                data-large-entity-id="${largeEntityId}" 
 | 
						||
                                data-node-id="${innerNodeId}">[+]</button>
 | 
						||
                    </div>
 | 
						||
                `;
 | 
						||
            });
 | 
						||
        }
 | 
						||
        
 | 
						||
        html += '</div></div></details></div>';
 | 
						||
        
 | 
						||
        return html;
 | 
						||
    }
 | 
						||
 | 
						||
    generateRelationshipsSection(node) {
 | 
						||
        let html = '';
 | 
						||
        
 | 
						||
        if (node.incoming_edges && node.incoming_edges.length > 0) {
 | 
						||
            html += `
 | 
						||
                <div class="modal-section">
 | 
						||
                    <details>
 | 
						||
                        <summary>⬅️ Source Relationships (${node.incoming_edges.length})</summary>
 | 
						||
                        <div class="modal-section-content">
 | 
						||
                            <div class="relationship-list">
 | 
						||
            `;
 | 
						||
            
 | 
						||
            node.incoming_edges.forEach(edge => {
 | 
						||
                const confidence = edge.data.confidence_score || 0;
 | 
						||
                const confidenceClass = confidence >= 0.8 ? 'high' : confidence >= 0.6 ? 'medium' : 'low';
 | 
						||
                
 | 
						||
                html += `
 | 
						||
                    <div class="relationship-item">
 | 
						||
                        <div class="relationship-source node-link" data-node-id="${edge.from}">
 | 
						||
                            ${edge.from}
 | 
						||
                        </div>
 | 
						||
                        <div class="relationship-type">
 | 
						||
                            <span class="relation-label">${edge.data.relationship_type}</span>
 | 
						||
                            <span class="confidence-indicator confidence-${confidenceClass}" title="Confidence: ${(confidence * 100).toFixed(1)}%">
 | 
						||
                                ${'●'.repeat(Math.ceil(confidence * 3))}
 | 
						||
                            </span>
 | 
						||
                        </div>
 | 
						||
                    </div>
 | 
						||
                `;
 | 
						||
            });
 | 
						||
            
 | 
						||
            html += '</div></div></details></div>';
 | 
						||
        }
 | 
						||
        
 | 
						||
        if (node.outgoing_edges && node.outgoing_edges.length > 0) {
 | 
						||
            html += `
 | 
						||
                <div class="modal-section">
 | 
						||
                    <details>
 | 
						||
                        <summary>➡️ Target Relationships (${node.outgoing_edges.length})</summary>
 | 
						||
                        <div class="modal-section-content">
 | 
						||
                            <div class="relationship-list">
 | 
						||
            `;
 | 
						||
            
 | 
						||
            node.outgoing_edges.forEach(edge => {
 | 
						||
                const confidence = edge.data.confidence_score || 0;
 | 
						||
                const confidenceClass = confidence >= 0.8 ? 'high' : confidence >= 0.6 ? 'medium' : 'low';
 | 
						||
                
 | 
						||
                html += `
 | 
						||
                    <div class="relationship-item">
 | 
						||
                        <div class="relationship-target node-link" data-node-id="${edge.to}">
 | 
						||
                            ${edge.to}
 | 
						||
                        </div>
 | 
						||
                        <div class="relationship-type">
 | 
						||
                            <span class="relation-label">${edge.data.relationship_type}</span>
 | 
						||
                            <span class="confidence-indicator confidence-${confidenceClass}" title="Confidence: ${(confidence * 100).toFixed(1)}%">
 | 
						||
                                ${'●'.repeat(Math.ceil(confidence * 3))}
 | 
						||
                            </span>
 | 
						||
                        </div>
 | 
						||
                    </div>
 | 
						||
                `;
 | 
						||
            });
 | 
						||
            
 | 
						||
            html += '</div></div></details></div>';
 | 
						||
        }
 | 
						||
        
 | 
						||
        return html;
 | 
						||
    }
 | 
						||
 | 
						||
    formatObjectCompact(obj) {
 | 
						||
        if (!obj || typeof obj !== 'object') return '';
 | 
						||
        
 | 
						||
        const entries = Object.entries(obj);
 | 
						||
        if (entries.length <= 2) {
 | 
						||
            let html = '';
 | 
						||
            entries.forEach(([key, value]) => {
 | 
						||
                html += `<div><strong>${key}:</strong> ${this.escapeHtml(String(value))}</div>`;
 | 
						||
            });
 | 
						||
            return html;
 | 
						||
        }
 | 
						||
        
 | 
						||
        // For complex objects, show first entry with expansion
 | 
						||
        return `
 | 
						||
            <div><strong>${entries[0][0]}:</strong> ${this.escapeHtml(String(entries[0][1]))}</div>
 | 
						||
            <details class="object-more">
 | 
						||
                <summary>+${entries.length - 1} more properties...</summary>
 | 
						||
                <div class="object-display">
 | 
						||
                    ${entries.slice(1).map(([key, value]) => 
 | 
						||
                        `<div><strong>${key}:</strong> ${this.escapeHtml(String(value))}</div>`
 | 
						||
                    ).join('')}
 | 
						||
                </div>
 | 
						||
            </details>
 | 
						||
        `;
 | 
						||
    }
 | 
						||
 | 
						||
    generateDescriptionSection(node) {
 | 
						||
        if (!node.description) return '';
 | 
						||
        
 | 
						||
        return `
 | 
						||
            <div class="section-card description-section">
 | 
						||
                <div class="section-header">
 | 
						||
                    <h4><span class="section-icon">📄</span>Description</h4>
 | 
						||
                </div>
 | 
						||
                <div class="description-content">
 | 
						||
                    ${this.escapeHtml(node.description)}
 | 
						||
                </div>
 | 
						||
            </div>
 | 
						||
        `;
 | 
						||
    }
 | 
						||
 | 
						||
    generateMetadataSection(node) {
 | 
						||
        if (!node.metadata || Object.keys(node.metadata).length === 0) return '';
 | 
						||
        
 | 
						||
        return `
 | 
						||
            <div class="section-card metadata-section collapsed">
 | 
						||
                <div class="section-header">
 | 
						||
                    <h4><span class="section-icon">🔧</span>Technical Metadata</h4>
 | 
						||
                    <button class="toggle-section-btn" onclick="this.parentElement.parentElement.classList.toggle('collapsed')">
 | 
						||
                        <span class="toggle-icon">▼</span>
 | 
						||
                    </button>
 | 
						||
                </div>
 | 
						||
                <div class="metadata-content">
 | 
						||
                    ${this.formatObjectToHtml(node.metadata)}
 | 
						||
                </div>
 | 
						||
            </div>
 | 
						||
        `;
 | 
						||
    }
 | 
						||
 | 
						||
    escapeHtml(text) {
 | 
						||
        const div = document.createElement('div');
 | 
						||
        div.textContent = text;
 | 
						||
        return div.innerHTML;
 | 
						||
    }
 | 
						||
    
 | 
						||
    /**
 | 
						||
     * Recursively formats a JavaScript object into an HTML unordered list with collapsible sections.
 | 
						||
     * @param {Object} obj - The object to format.
 | 
						||
     * @returns {string} - An HTML string representing the object.
 | 
						||
     */
 | 
						||
    formatObjectToHtml(obj) {
 | 
						||
        if (!obj || Object.keys(obj).length === 0) {
 | 
						||
            return '<p class="no-data">No data available.</p>';
 | 
						||
        }
 | 
						||
    
 | 
						||
        let html = '<ul>';
 | 
						||
        for (const key in obj) {
 | 
						||
            if (Object.hasOwnProperty.call(obj, key)) {
 | 
						||
                const value = obj[key];
 | 
						||
                const formattedKey = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
 | 
						||
    
 | 
						||
                if (typeof value === 'object' && value !== null) {
 | 
						||
                    html += `<li><details><summary><strong>${formattedKey}</strong></summary>`;
 | 
						||
                    html += this.formatObjectToHtml(value);
 | 
						||
                    html += `</details></li>`;
 | 
						||
                } else {
 | 
						||
                    html += `<li><strong>${formattedKey}:</strong> ${this.formatValue(value)}</li>`;
 | 
						||
                }
 | 
						||
            }
 | 
						||
        }
 | 
						||
        html += '</ul>';
 | 
						||
        return html;
 | 
						||
    }
 | 
						||
 | 
						||
    
 | 
						||
    /**
 | 
						||
     * Enhanced showNodeModal with better event handling
 | 
						||
     */
 | 
						||
    showNodeModal(node) {
 | 
						||
        if (!this.elements.nodeModal || !node) return;
 | 
						||
 | 
						||
        if (this.elements.modalTitle) {
 | 
						||
            this.elements.modalTitle.innerHTML = `
 | 
						||
                <span class="modal-title-icon">${this.getNodeTypeIcon(node.type)}</span>
 | 
						||
                <span class="modal-title-text">${node.id}</span>
 | 
						||
            `;
 | 
						||
        }
 | 
						||
        
 | 
						||
        const detailsHtml = this.generateNodeDetailsHtml(node);
 | 
						||
        
 | 
						||
        if (this.elements.modalDetails) {
 | 
						||
            this.elements.modalDetails.innerHTML = detailsHtml;
 | 
						||
            
 | 
						||
            // Add enhanced event handlers
 | 
						||
            this.addModalEventHandlers();
 | 
						||
        }
 | 
						||
        
 | 
						||
        this.elements.nodeModal.style.display = 'block';
 | 
						||
    }
 | 
						||
    
 | 
						||
    /**
 | 
						||
     * Add event handlers for enhanced modal interactions
 | 
						||
     */
 | 
						||
    addModalEventHandlers() {
 | 
						||
        // Handle node navigation links - FIXED to work properly
 | 
						||
        this.elements.modalDetails.querySelectorAll('.node-link-compact').forEach(link => {
 | 
						||
            link.addEventListener('click', (e) => {
 | 
						||
                e.preventDefault();
 | 
						||
                e.stopPropagation();
 | 
						||
                
 | 
						||
                const nodeId = e.target.dataset.nodeId || e.target.getAttribute('data-node-id');
 | 
						||
                
 | 
						||
                if (nodeId && this.graphManager && this.graphManager.nodes) {
 | 
						||
                    const nextNode = this.graphManager.nodes.get(nodeId);
 | 
						||
                    if (nextNode) {
 | 
						||
                        console.log('Navigating to node:', nextNode);
 | 
						||
                        // Don't hide modal, just update content
 | 
						||
                        this.showNodeModal(nextNode);
 | 
						||
                    } else {
 | 
						||
                        console.warn('Node not found:', nodeId);
 | 
						||
                    }
 | 
						||
                }
 | 
						||
            });
 | 
						||
        });
 | 
						||
 | 
						||
        // Handle the new extract button
 | 
						||
        this.elements.modalDetails.querySelectorAll('.extract-node-btn').forEach(button => {
 | 
						||
            button.addEventListener('click', (e) => {
 | 
						||
                e.preventDefault();
 | 
						||
                e.stopPropagation();
 | 
						||
                
 | 
						||
                const largeEntityId = e.target.dataset.largeEntityId;
 | 
						||
                const nodeId = e.target.dataset.nodeId;
 | 
						||
                
 | 
						||
                console.log(`Extract button clicked for node ${nodeId} from entity ${largeEntityId}`);
 | 
						||
                this.extractNode(largeEntityId, nodeId);
 | 
						||
            });
 | 
						||
        });
 | 
						||
        
 | 
						||
        // Handle legacy node links
 | 
						||
        this.elements.modalDetails.querySelectorAll('.node-link').forEach(link => {
 | 
						||
            link.addEventListener('click', (e) => {
 | 
						||
                e.preventDefault();
 | 
						||
                e.stopPropagation();
 | 
						||
                
 | 
						||
                const nodeId = e.target.dataset.nodeId || e.target.getAttribute('data-node-id');
 | 
						||
                if (nodeId && this.graphManager && this.graphManager.nodes) {
 | 
						||
                    const nextNode = this.graphManager.nodes.get(nodeId);
 | 
						||
                    if (nextNode) {
 | 
						||
                        this.showNodeModal(nextNode);
 | 
						||
                    }
 | 
						||
                }
 | 
						||
            });
 | 
						||
        });
 | 
						||
    }
 | 
						||
 | 
						||
    async extractNode(largeEntityId, nodeId) {
 | 
						||
        try {
 | 
						||
            const response = await this.apiCall('/api/graph/large-entity/extract', 'POST', {
 | 
						||
                large_entity_id: largeEntityId,
 | 
						||
                node_id: nodeId,
 | 
						||
            });
 | 
						||
 | 
						||
            if (response.success) {
 | 
						||
                this.showSuccess(response.message);
 | 
						||
                
 | 
						||
                // If the scanner was idle, it's now running. Start polling to see the new node appear.
 | 
						||
                if (this.scanStatus === 'idle') {
 | 
						||
                    this.startPolling(1000);
 | 
						||
                } else {
 | 
						||
                    // If already scanning, force a quick graph update to see the change sooner.
 | 
						||
                    setTimeout(() => this.updateGraph(), 500);
 | 
						||
                }
 | 
						||
 | 
						||
            } else {
 | 
						||
                throw new Error(response.error || 'Extraction failed on the server.');
 | 
						||
            }
 | 
						||
        } catch (error) {
 | 
						||
            console.error('Failed to extract node:', error);
 | 
						||
            this.showError(`Extraction failed: ${error.message}`);
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    initializeModalFunctionality() {
 | 
						||
        // Make sure the graph manager has node access
 | 
						||
        console.log('Initializing modal functionality...');
 | 
						||
        
 | 
						||
        // Set up event delegation for dynamic content
 | 
						||
        document.addEventListener('click', (e) => {
 | 
						||
            const target = e.target.closest('.node-link-compact, .node-link');
 | 
						||
            if (target) {
 | 
						||
                e.preventDefault();
 | 
						||
                e.stopPropagation();
 | 
						||
                
 | 
						||
                const nodeId = target.dataset.nodeId || target.getAttribute('data-node-id');
 | 
						||
                if (nodeId && this.graphManager && this.graphManager.nodes) {
 | 
						||
                    const nextNode = this.graphManager.nodes.get(nodeId);
 | 
						||
                    if (nextNode) {
 | 
						||
                        this.showNodeModal(nextNode);
 | 
						||
                    }
 | 
						||
                }
 | 
						||
            }
 | 
						||
        });
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Get icon for node type
 | 
						||
     */
 | 
						||
    getNodeTypeIcon(nodeType) {
 | 
						||
        const icons = {
 | 
						||
            'domain': '🌍',
 | 
						||
            'ip': '📍',
 | 
						||
            'asn': '🏢',
 | 
						||
            'large_entity': '📦',
 | 
						||
            'correlation_object': '🔗'
 | 
						||
        };
 | 
						||
        return icons[nodeType] || '●';
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Enhanced hideModal with animation
 | 
						||
     */
 | 
						||
    hideModal() {
 | 
						||
        if (this.elements.nodeModal) {
 | 
						||
            this.elements.nodeModal.classList.add('modal-closing');
 | 
						||
            
 | 
						||
            setTimeout(() => {
 | 
						||
                this.elements.nodeModal.style.display = 'none';
 | 
						||
                this.elements.nodeModal.classList.remove('modal-closing');
 | 
						||
            }, 200);
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Enhanced keyboard navigation for modals
 | 
						||
     */
 | 
						||
    setupModalKeyboardNavigation() {
 | 
						||
        document.addEventListener('keydown', (e) => {
 | 
						||
            if (this.elements.nodeModal && this.elements.nodeModal.style.display === 'block') {
 | 
						||
                switch (e.key) {
 | 
						||
                    case 'Escape':
 | 
						||
                        this.hideModal();
 | 
						||
                        break;
 | 
						||
                    case 'Tab':
 | 
						||
                        this.handleModalTabNavigation(e);
 | 
						||
                        break;
 | 
						||
                    case 'Enter':
 | 
						||
                        if (e.target.classList.contains('node-link') || e.target.classList.contains('node-link-item')) {
 | 
						||
                            e.target.click();
 | 
						||
                        }
 | 
						||
                        break;
 | 
						||
                }
 | 
						||
            }
 | 
						||
        });
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Handle tab navigation within modal
 | 
						||
     */
 | 
						||
    handleModalTabNavigation(e) {
 | 
						||
        const focusableElements = this.elements.nodeModal.querySelectorAll(
 | 
						||
            'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
 | 
						||
        );
 | 
						||
        const firstElement = focusableElements[0];
 | 
						||
        const lastElement = focusableElements[focusableElements.length - 1];
 | 
						||
        
 | 
						||
        if (e.shiftKey) {
 | 
						||
            if (document.activeElement === firstElement) {
 | 
						||
                lastElement.focus();
 | 
						||
                e.preventDefault();
 | 
						||
            }
 | 
						||
        } else {
 | 
						||
            if (document.activeElement === lastElement) {
 | 
						||
                firstElement.focus();
 | 
						||
                e.preventDefault();
 | 
						||
            }
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Initialize enhanced modal functionality
 | 
						||
     */
 | 
						||
    initializeEnhancedModals() {
 | 
						||
        this.setupModalKeyboardNavigation();
 | 
						||
        
 | 
						||
        // Add CSS classes for animations
 | 
						||
        const style = document.createElement('style');
 | 
						||
        style.textContent = `
 | 
						||
            .modal-opening {
 | 
						||
                animation: modalFadeIn 0.3s ease-out;
 | 
						||
            }
 | 
						||
            
 | 
						||
            .modal-closing {
 | 
						||
                animation: modalFadeOut 0.2s ease-in;
 | 
						||
            }
 | 
						||
            
 | 
						||
            @keyframes modalFadeIn {
 | 
						||
                from {
 | 
						||
                    opacity: 0;
 | 
						||
                    transform: scale(0.9);
 | 
						||
                }
 | 
						||
                to {
 | 
						||
                    opacity: 1;
 | 
						||
                    transform: scale(1);
 | 
						||
                }
 | 
						||
            }
 | 
						||
            
 | 
						||
            @keyframes modalFadeOut {
 | 
						||
                from {
 | 
						||
                    opacity: 1;
 | 
						||
                    transform: scale(1);
 | 
						||
                }
 | 
						||
                to {
 | 
						||
                    opacity: 0;
 | 
						||
                    transform: scale(0.95);
 | 
						||
                }
 | 
						||
            }
 | 
						||
            
 | 
						||
            .array-value.expanded .array-items {
 | 
						||
                max-height: none;
 | 
						||
            }
 | 
						||
            
 | 
						||
            .modal-title-icon {
 | 
						||
                margin-right: 0.5rem;
 | 
						||
                font-size: 1.2rem;
 | 
						||
            }
 | 
						||
            
 | 
						||
            .modal-title-text {
 | 
						||
                font-family: 'Special Elite', monospace;
 | 
						||
            }
 | 
						||
        `;
 | 
						||
        document.head.appendChild(style);
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Show Settings modal
 | 
						||
     */
 | 
						||
    showSettingsModal() {
 | 
						||
        if (this.elements.settingsModal) {
 | 
						||
            this.elements.settingsModal.style.display = 'block';
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Hide Settings modal
 | 
						||
     */
 | 
						||
    hideSettingsModal() {
 | 
						||
        if (this.elements.settingsModal) {
 | 
						||
            this.elements.settingsModal.style.display = 'none';
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Save API Keys
 | 
						||
     */
 | 
						||
    async saveApiKeys() {
 | 
						||
        const inputs = this.elements.apiKeyInputs.querySelectorAll('input');
 | 
						||
        const keys = {};
 | 
						||
        inputs.forEach(input => {
 | 
						||
            const provider = input.dataset.provider;
 | 
						||
            const value = input.value.trim();
 | 
						||
            if (provider && value) {
 | 
						||
                keys[provider] = value;
 | 
						||
            }
 | 
						||
        });
 | 
						||
 | 
						||
        if (Object.keys(keys).length === 0) {
 | 
						||
            this.showWarning('No API keys were entered.');
 | 
						||
            return;
 | 
						||
        }
 | 
						||
 | 
						||
        try {
 | 
						||
            const response = await this.apiCall('/api/config/api-keys', 'POST', keys);
 | 
						||
            if (response.success) {
 | 
						||
                this.showSuccess(response.message);
 | 
						||
                this.hideSettingsModal();
 | 
						||
                this.loadProviders(); // Refresh provider status
 | 
						||
            } else {
 | 
						||
                throw new Error(response.error || 'Failed to save API keys');
 | 
						||
            }
 | 
						||
        } catch (error) {
 | 
						||
            this.showError(`Error saving API keys: ${error.message}`);
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Reset API Key fields
 | 
						||
     */
 | 
						||
    resetApiKeys() {
 | 
						||
        const inputs = this.elements.apiKeyInputs.querySelectorAll('input');
 | 
						||
        inputs.forEach(input => {
 | 
						||
            input.value = '';
 | 
						||
        });
 | 
						||
    }
 | 
						||
    
 | 
						||
 | 
						||
        
 | 
						||
    /**
 | 
						||
     * Make API call to server
 | 
						||
     * @param {string} endpoint - API endpoint
 | 
						||
     * @param {string} method - HTTP method
 | 
						||
     * @param {Object} data - Request data
 | 
						||
     * @returns {Promise<Object>} Response data
 | 
						||
     */
 | 
						||
    async apiCall(endpoint, method = 'GET', data = null) {
 | 
						||
        
 | 
						||
        try {
 | 
						||
            const options = {
 | 
						||
                method: method,
 | 
						||
                headers: {
 | 
						||
                    'Content-Type': 'application/json'
 | 
						||
                }
 | 
						||
            };
 | 
						||
            
 | 
						||
            if (data && method !== 'GET') {
 | 
						||
                options.body = JSON.stringify(data);
 | 
						||
                console.log('Request body:', options.body);
 | 
						||
            }
 | 
						||
            
 | 
						||
            const response = await fetch(endpoint, options);
 | 
						||
            
 | 
						||
            
 | 
						||
            if (!response.ok) {
 | 
						||
                throw new Error(`HTTP ${response.status}: ${response.statusText}`);
 | 
						||
            }
 | 
						||
            
 | 
						||
            const result = await response.json();
 | 
						||
            
 | 
						||
            return result;
 | 
						||
            
 | 
						||
        } catch (error) {
 | 
						||
            console.error(`API call failed for ${method} ${endpoint}:`, error);
 | 
						||
            throw error;
 | 
						||
        }
 | 
						||
    }
 | 
						||
    
 | 
						||
 | 
						||
 | 
						||
    /**
 | 
						||
     * Validate domain name
 | 
						||
     * @param {string} domain - Domain to validate
 | 
						||
     * @returns {boolean} True if valid
 | 
						||
     */
 | 
						||
    isValidDomain(domain) {
 | 
						||
        console.log(`Validating domain: "${domain}"`);
 | 
						||
        if (!domain || typeof domain !== 'string' || domain.length > 253 || /^\d{1,3}(\.\d{1,3}){3}$/.test(domain)) {
 | 
						||
            return false;
 | 
						||
        }
 | 
						||
        const parts = domain.split('.');
 | 
						||
        if (parts.length < 2 || parts.some(part => !/^[a-zA-Z0-9-]{1,63}$/.test(part) || part.startsWith('-') || part.endsWith('-'))) {
 | 
						||
            return false;
 | 
						||
        }
 | 
						||
        return true;
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Validate target (domain or IP) - UPDATED for IPv6 support
 | 
						||
     * @param {string} target - Target to validate
 | 
						||
     * @returns {boolean} True if valid
 | 
						||
     */
 | 
						||
    isValidTarget(target) {
 | 
						||
        return this.isValidDomain(target) || this.isValidIp(target);
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Validate IP address (IPv4 or IPv6)
 | 
						||
     * @param {string} ip - IP to validate
 | 
						||
     * @returns {boolean} True if valid
 | 
						||
     */
 | 
						||
    isValidIp(ip) {
 | 
						||
        console.log(`Validating IP: "${ip}"`);
 | 
						||
        
 | 
						||
        if (!ip || typeof ip !== 'string') {
 | 
						||
            return false;
 | 
						||
        }
 | 
						||
        
 | 
						||
        ip = ip.trim();
 | 
						||
        
 | 
						||
        // IPv4 validation
 | 
						||
        if (this.isValidIPv4(ip)) {
 | 
						||
            return true;
 | 
						||
        }
 | 
						||
        
 | 
						||
        // IPv6 validation
 | 
						||
        if (this.isValidIPv6(ip)) {
 | 
						||
            return true;
 | 
						||
        }
 | 
						||
        
 | 
						||
        return false;
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Validate IPv4 address
 | 
						||
     * @param {string} ip - IP to validate
 | 
						||
     * @returns {boolean} True if valid IPv4
 | 
						||
     */
 | 
						||
    isValidIPv4(ip) {
 | 
						||
        const ipv4Pattern = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
 | 
						||
        const match = ip.match(ipv4Pattern);
 | 
						||
        
 | 
						||
        if (!match) {
 | 
						||
            return false;
 | 
						||
        }
 | 
						||
        
 | 
						||
        // Check each octet is between 0-255
 | 
						||
        for (let i = 1; i <= 4; i++) {
 | 
						||
            const octet = parseInt(match[i], 10);
 | 
						||
            if (octet < 0 || octet > 255) {
 | 
						||
                return false;
 | 
						||
            }
 | 
						||
            // Check for leading zeros (except for '0' itself)
 | 
						||
            if (match[i].length > 1 && match[i][0] === '0') {
 | 
						||
                return false;
 | 
						||
            }
 | 
						||
        }
 | 
						||
        
 | 
						||
        return true;
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Validate IPv6 address
 | 
						||
     * @param {string} ip - IP to validate
 | 
						||
     * @returns {boolean} True if valid IPv6
 | 
						||
     */
 | 
						||
    isValidIPv6(ip) {
 | 
						||
        // Handle IPv6 with embedded IPv4 (e.g., ::ffff:192.168.1.1)
 | 
						||
        if (ip.includes('.')) {
 | 
						||
            const lastColon = ip.lastIndexOf(':');
 | 
						||
            if (lastColon !== -1) {
 | 
						||
                const ipv6Part = ip.substring(0, lastColon + 1);
 | 
						||
                const ipv4Part = ip.substring(lastColon + 1);
 | 
						||
                
 | 
						||
                if (this.isValidIPv4(ipv4Part)) {
 | 
						||
                    // Validate the IPv6 part (should end with ::)
 | 
						||
                    return this.isValidIPv6Pure(ipv6Part + '0:0');
 | 
						||
                }
 | 
						||
            }
 | 
						||
        }
 | 
						||
        
 | 
						||
        return this.isValidIPv6Pure(ip);
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Validate pure IPv6 address (no embedded IPv4)
 | 
						||
     * @param {string} ip - IPv6 address to validate
 | 
						||
     * @returns {boolean} True if valid IPv6
 | 
						||
     */
 | 
						||
    isValidIPv6Pure(ip) {
 | 
						||
        // Basic format check
 | 
						||
        if (!ip || ip.length < 2 || ip.length > 39) {
 | 
						||
            return false;
 | 
						||
        }
 | 
						||
        
 | 
						||
        // Check for invalid characters
 | 
						||
        if (!/^[0-9a-fA-F:]+$/.test(ip)) {
 | 
						||
            return false;
 | 
						||
        }
 | 
						||
        
 | 
						||
        // Handle double colon (::) for zero compression
 | 
						||
        const doubleColonCount = (ip.match(/::/g) || []).length;
 | 
						||
        if (doubleColonCount > 1) {
 | 
						||
            return false; // Only one :: allowed
 | 
						||
        }
 | 
						||
        
 | 
						||
        let parts;
 | 
						||
        if (doubleColonCount === 1) {
 | 
						||
            // Expand the :: notation
 | 
						||
            const [before, after] = ip.split('::');
 | 
						||
            const beforeParts = before ? before.split(':') : [];
 | 
						||
            const afterParts = after ? after.split(':') : [];
 | 
						||
            
 | 
						||
            // Calculate how many zero groups the :: represents
 | 
						||
            const totalParts = beforeParts.length + afterParts.length;
 | 
						||
            const zeroGroups = 8 - totalParts;
 | 
						||
            
 | 
						||
            if (zeroGroups < 1) {
 | 
						||
                return false; // :: must represent at least one zero group
 | 
						||
            }
 | 
						||
            
 | 
						||
            // Build the full address
 | 
						||
            parts = beforeParts.concat(Array(zeroGroups).fill('0')).concat(afterParts);
 | 
						||
        } else {
 | 
						||
            // No :: notation, split normally
 | 
						||
            parts = ip.split(':');
 | 
						||
        }
 | 
						||
        
 | 
						||
        // IPv6 should have exactly 8 groups
 | 
						||
        if (parts.length !== 8) {
 | 
						||
            return false;
 | 
						||
        }
 | 
						||
        
 | 
						||
        // Validate each group (1-4 hex digits)
 | 
						||
        for (const part of parts) {
 | 
						||
            if (!part || part.length > 4 || !/^[0-9a-fA-F]+$/.test(part)) {
 | 
						||
                return false;
 | 
						||
            }
 | 
						||
        }
 | 
						||
        
 | 
						||
        return true;
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Format status text for display
 | 
						||
     * @param {string} status - Raw status
 | 
						||
     * @returns {string} Formatted status
 | 
						||
     */
 | 
						||
    formatStatus(status) {
 | 
						||
        const statusMap = {
 | 
						||
            'idle': 'Idle',
 | 
						||
            'running': 'Running',
 | 
						||
            'completed': 'Completed',
 | 
						||
            'failed': 'Failed',
 | 
						||
            'stopped': 'Stopped'
 | 
						||
        };
 | 
						||
        return statusMap[status] || status;
 | 
						||
    }
 | 
						||
    
 | 
						||
    /**
 | 
						||
     * Format value for display
 | 
						||
     * @param {*} value - Raw value
 | 
						||
     * @returns {string} Formatted value
 | 
						||
     */
 | 
						||
    formatValue(value) {
 | 
						||
        if (typeof value === 'object' && value !== null) {
 | 
						||
            // Use <pre> for nicely formatted JSON
 | 
						||
            return `<pre>${JSON.stringify(value, null, 2)}</pre>`;
 | 
						||
        } else {
 | 
						||
            // Escape HTML to prevent XSS issues with string values
 | 
						||
            const strValue = String(value);
 | 
						||
            return strValue.replace(/</g, "<").replace(/>/g, ">");
 | 
						||
        }
 | 
						||
    }
 | 
						||
    
 | 
						||
    /**
 | 
						||
     * Show success message
 | 
						||
     * @param {string} message - Success message
 | 
						||
     */
 | 
						||
    showSuccess(message) {
 | 
						||
        this.showMessage(message, 'success');
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Show info message
 | 
						||
     * @param {string} message - Info message
 | 
						||
     */
 | 
						||
    showInfo(message) {
 | 
						||
        this.showMessage(message, 'info');
 | 
						||
    }
 | 
						||
    
 | 
						||
    showWarning(message) {
 | 
						||
        this.showMessage(message, 'warning');
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Show error message
 | 
						||
     * @param {string} message - Error message
 | 
						||
     */
 | 
						||
    showError(message) {
 | 
						||
        this.showMessage(message, 'error');
 | 
						||
    }
 | 
						||
    
 | 
						||
    /**
 | 
						||
     * Show connection error
 | 
						||
     */
 | 
						||
    showConnectionError() {
 | 
						||
        this.updateConnectionStatus('error');
 | 
						||
    }
 | 
						||
    
 | 
						||
    /**
 | 
						||
     * Show message with visual feedback
 | 
						||
     * @param {string} message - Message text
 | 
						||
     * @param {string} type - Message type (success, error, warning, info)
 | 
						||
     */
 | 
						||
    showMessage(message, type = 'info') {        
 | 
						||
        // Create message element
 | 
						||
        const messageElement = document.createElement('div');
 | 
						||
        messageElement.className = `message-toast message-${type}`;
 | 
						||
        
 | 
						||
        messageElement.innerHTML = `
 | 
						||
            <div style="display: flex; justify-content: space-between; align-items: center; padding: 12px 20px;">
 | 
						||
                <span style="flex: 1;">${message}</span>
 | 
						||
                <button onclick="this.parentElement.parentElement.remove()" 
 | 
						||
                        style="background: none; border: none; color: #fff; cursor: pointer; font-size: 16px; margin-left: 10px; opacity: 0.7;">×</button>
 | 
						||
            </div>
 | 
						||
        `;
 | 
						||
        
 | 
						||
        // Add to container
 | 
						||
        const container = document.getElementById('message-container');
 | 
						||
        if (container) {
 | 
						||
            container.appendChild(messageElement);
 | 
						||
        
 | 
						||
            // Auto-remove after delay
 | 
						||
            setTimeout(() => {
 | 
						||
                if (messageElement.parentNode) {
 | 
						||
                    messageElement.style.animation = 'slideOutRight 0.3s ease-out';
 | 
						||
                    setTimeout(() => {
 | 
						||
                        if (messageElement.parentNode) {
 | 
						||
                            messageElement.remove();
 | 
						||
                        }
 | 
						||
                    }, 300);
 | 
						||
                }
 | 
						||
            }, type === 'error' ? 8000 : 5000); // Errors stay longer
 | 
						||
        }
 | 
						||
        
 | 
						||
        // Update connection status to show activity
 | 
						||
        if (type === 'success' && this.consecutiveErrors === 0) {
 | 
						||
            this.updateConnectionStatus(this.isScanning ? 'active' : 'idle');
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Get message background color based on type
 | 
						||
     * @param {string} type - Message type
 | 
						||
     * @returns {string} CSS color
 | 
						||
     */
 | 
						||
    getMessageColor(type) {
 | 
						||
        const colors = {
 | 
						||
            'success': '#2c5c34',
 | 
						||
            'error': '#5c2c2c', 
 | 
						||
            'warning': '#5c4c2c',
 | 
						||
            'info': '#2c3e5c'
 | 
						||
        };
 | 
						||
        return colors[type] || colors.info;
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Get message border color based on type
 | 
						||
     * @param {string} type - Message type
 | 
						||
     * @returns {string} CSS color
 | 
						||
     */
 | 
						||
    getMessageBorderColor(type) {
 | 
						||
        const colors = {
 | 
						||
            'success': '#00ff41',
 | 
						||
            'error': '#ff6b6b',
 | 
						||
            'warning': '#ff9900', 
 | 
						||
            'info': '#00aaff'
 | 
						||
        };
 | 
						||
        return colors[type] || colors.info;
 | 
						||
    }
 | 
						||
 | 
						||
 | 
						||
}
 | 
						||
 | 
						||
// Add CSS animations for message toasts
 | 
						||
const style = document.createElement('style');
 | 
						||
style.textContent = `
 | 
						||
    @keyframes slideInRight {
 | 
						||
        from {
 | 
						||
            transform: translateX(100%);
 | 
						||
            opacity: 0;
 | 
						||
        }
 | 
						||
        to {
 | 
						||
            transform: translateX(0);
 | 
						||
            opacity: 1;
 | 
						||
        }
 | 
						||
    }
 | 
						||
    
 | 
						||
    @keyframes slideOutRight {
 | 
						||
        from {
 | 
						||
            transform: translateX(0);
 | 
						||
            opacity: 1;
 | 
						||
        }
 | 
						||
        to {
 | 
						||
            transform: translateX(100%);
 | 
						||
            opacity: 0;
 | 
						||
        }
 | 
						||
    }
 | 
						||
    
 | 
						||
    .message-container {
 | 
						||
        pointer-events: auto;
 | 
						||
    }
 | 
						||
    
 | 
						||
    .message-toast {
 | 
						||
        pointer-events: auto;
 | 
						||
    }
 | 
						||
`;
 | 
						||
document.head.appendChild(style);
 | 
						||
 | 
						||
// Initialize application when page loads
 | 
						||
console.log('Creating DNSReconApp instance...');
 | 
						||
const app = new DNSReconApp(); |