fix graph trueRoot
This commit is contained in:
		
							parent
							
								
									30ee21f087
								
							
						
					
					
						commit
						c347581a6c
					
				@ -1002,16 +1002,159 @@ class GraphManager {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
     /* 
 | 
			
		||||
     * @param {Set} excludedNodeIds - Node IDs to exclude from analysis (for simulation)
 | 
			
		||||
     * @param {Set} excludedEdgeTypes - Edge types to exclude from traversal
 | 
			
		||||
     * @param {Set} excludedNodeTypes - Node types to exclude from traversal
 | 
			
		||||
     * @returns {Object} Analysis results with reachable/unreachable nodes
 | 
			
		||||
     */
 | 
			
		||||
    analyzeGraphReachability(excludedNodeIds = new Set(), excludedEdgeTypes = new Set(), excludedNodeTypes = new Set()) {
 | 
			
		||||
        console.log("Performing comprehensive reachability analysis...");
 | 
			
		||||
        
 | 
			
		||||
        const analysis = {
 | 
			
		||||
            reachableNodes: new Set(),
 | 
			
		||||
            unreachableNodes: new Set(),
 | 
			
		||||
            isolatedClusters: [],
 | 
			
		||||
            affectedNodes: new Set()
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if (this.nodes.length === 0) return analysis;
 | 
			
		||||
 | 
			
		||||
        // Build adjacency list excluding specified elements
 | 
			
		||||
        const adjacencyList = {};
 | 
			
		||||
        this.nodes.getIds().forEach(id => {
 | 
			
		||||
            if (!excludedNodeIds.has(id)) {
 | 
			
		||||
                adjacencyList[id] = [];
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this.edges.forEach(edge => {
 | 
			
		||||
            const edgeType = edge.metadata?.relationship_type || '';
 | 
			
		||||
            if (!excludedEdgeTypes.has(edgeType) && 
 | 
			
		||||
                !excludedNodeIds.has(edge.from) && 
 | 
			
		||||
                !excludedNodeIds.has(edge.to)) {
 | 
			
		||||
                
 | 
			
		||||
                if (adjacencyList[edge.from]) {
 | 
			
		||||
                    adjacencyList[edge.from].push(edge.to);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // BFS traversal from true roots
 | 
			
		||||
        const traversalQueue = [];
 | 
			
		||||
        
 | 
			
		||||
        // Start from true roots that aren't excluded
 | 
			
		||||
        this.trueRootIds.forEach(rootId => {
 | 
			
		||||
            if (!excludedNodeIds.has(rootId)) {
 | 
			
		||||
                const node = this.nodes.get(rootId);
 | 
			
		||||
                if (node && !excludedNodeTypes.has(node.type)) {
 | 
			
		||||
                    if (!analysis.reachableNodes.has(rootId)) {
 | 
			
		||||
                        traversalQueue.push(rootId);
 | 
			
		||||
                        analysis.reachableNodes.add(rootId);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // BFS to find all reachable nodes
 | 
			
		||||
        let queueIndex = 0;
 | 
			
		||||
        while (queueIndex < traversalQueue.length) {
 | 
			
		||||
            const currentNode = traversalQueue[queueIndex++];
 | 
			
		||||
            
 | 
			
		||||
            for (const neighbor of (adjacencyList[currentNode] || [])) {
 | 
			
		||||
                if (!analysis.reachableNodes.has(neighbor)) {
 | 
			
		||||
                    const node = this.nodes.get(neighbor);
 | 
			
		||||
                    if (node && !excludedNodeTypes.has(node.type)) {
 | 
			
		||||
                        analysis.reachableNodes.add(neighbor);
 | 
			
		||||
                        traversalQueue.push(neighbor);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Identify unreachable nodes (maintaining forensic integrity)
 | 
			
		||||
        Object.keys(adjacencyList).forEach(nodeId => {
 | 
			
		||||
            if (!analysis.reachableNodes.has(nodeId)) {
 | 
			
		||||
                analysis.unreachableNodes.add(nodeId);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Find isolated clusters among unreachable nodes
 | 
			
		||||
        analysis.isolatedClusters = this.findIsolatedClusters(
 | 
			
		||||
            Array.from(analysis.unreachableNodes), 
 | 
			
		||||
            adjacencyList
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        console.log(`Reachability analysis complete:`, {
 | 
			
		||||
            reachable: analysis.reachableNodes.size,
 | 
			
		||||
            unreachable: analysis.unreachableNodes.size,
 | 
			
		||||
            clusters: analysis.isolatedClusters.length
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return analysis;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get network statistics
 | 
			
		||||
     * @returns {Object} Statistics object
 | 
			
		||||
     * Find isolated clusters within a set of nodes
 | 
			
		||||
     * Used for forensic analysis to identify disconnected subgraphs
 | 
			
		||||
     */
 | 
			
		||||
    findIsolatedClusters(nodeIds, adjacencyList) {
 | 
			
		||||
        const visited = new Set();
 | 
			
		||||
        const clusters = [];
 | 
			
		||||
 | 
			
		||||
        for (const nodeId of nodeIds) {
 | 
			
		||||
            if (!visited.has(nodeId)) {
 | 
			
		||||
                const cluster = [];
 | 
			
		||||
                const stack = [nodeId];
 | 
			
		||||
                
 | 
			
		||||
                while (stack.length > 0) {
 | 
			
		||||
                    const current = stack.pop();
 | 
			
		||||
                    if (!visited.has(current)) {
 | 
			
		||||
                        visited.add(current);
 | 
			
		||||
                        cluster.push(current);
 | 
			
		||||
                        
 | 
			
		||||
                        // Add unvisited neighbors within the unreachable set
 | 
			
		||||
                        for (const neighbor of (adjacencyList[current] || [])) {
 | 
			
		||||
                            if (nodeIds.includes(neighbor) && !visited.has(neighbor)) {
 | 
			
		||||
                                stack.push(neighbor);
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                if (cluster.length > 0) {
 | 
			
		||||
                    clusters.push(cluster);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return clusters;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * ENHANCED: Get comprehensive graph statistics with forensic information
 | 
			
		||||
     * Updates the existing getStatistics() method
 | 
			
		||||
     */
 | 
			
		||||
    getStatistics() {
 | 
			
		||||
        return {
 | 
			
		||||
        const basicStats = {
 | 
			
		||||
            nodeCount: this.nodes.length,
 | 
			
		||||
            edgeCount: this.edges.length,
 | 
			
		||||
            largeEntityMembersHidden: this.largeEntityMembers.size
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Add forensic statistics
 | 
			
		||||
        const visibleNodes = this.nodes.get({ filter: node => !node.hidden });
 | 
			
		||||
        const hiddenNodes = this.nodes.get({ filter: node => node.hidden });
 | 
			
		||||
        
 | 
			
		||||
        return {
 | 
			
		||||
            ...basicStats,
 | 
			
		||||
            forensicStatistics: {
 | 
			
		||||
                visibleNodes: visibleNodes.length,
 | 
			
		||||
                hiddenNodes: hiddenNodes.length,
 | 
			
		||||
                trueRoots: this.trueRootIds.size,
 | 
			
		||||
                integrityStatus: visibleNodes.length > 0 && this.trueRootIds.size > 0 ? 'INTACT' : 'COMPROMISED'
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    computeTrueRoots() {
 | 
			
		||||
@ -1069,75 +1212,185 @@ class GraphManager {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * ENHANCED: Apply filters using consolidated reachability analysis
 | 
			
		||||
     * Replaces the existing applyAllFilters() method
 | 
			
		||||
     */
 | 
			
		||||
    applyAllFilters() {
 | 
			
		||||
        console.log("Applying all filters (robust orphan detection)...");
 | 
			
		||||
        console.log("Applying filters with enhanced reachability analysis...");
 | 
			
		||||
        if (this.nodes.length === 0) return;
 | 
			
		||||
    
 | 
			
		||||
        // 1. Get filter criteria from the UI
 | 
			
		||||
        const hiddenNodeTypes = new Set();
 | 
			
		||||
        this.filterPanel.querySelectorAll('input[data-filter-type="node"]:not(:checked)').forEach(cb => {
 | 
			
		||||
            hiddenNodeTypes.add(cb.value);
 | 
			
		||||
 | 
			
		||||
        // Get filter criteria from UI
 | 
			
		||||
        const excludedNodeTypes = new Set();
 | 
			
		||||
        this.filterPanel?.querySelectorAll('input[data-filter-type="node"]:not(:checked)').forEach(cb => {
 | 
			
		||||
            excludedNodeTypes.add(cb.value);
 | 
			
		||||
        });
 | 
			
		||||
    
 | 
			
		||||
        const hiddenEdgeTypes = new Set();
 | 
			
		||||
        this.filterPanel.querySelectorAll('input[data-filter-type="edge"]:not(:checked)').forEach(cb => {
 | 
			
		||||
            hiddenEdgeTypes.add(cb.value);
 | 
			
		||||
 | 
			
		||||
        const excludedEdgeTypes = new Set();
 | 
			
		||||
        this.filterPanel?.querySelectorAll('input[data-filter-type="edge"]:not(:checked)').forEach(cb => {
 | 
			
		||||
            excludedEdgeTypes.add(cb.value);
 | 
			
		||||
        });
 | 
			
		||||
    
 | 
			
		||||
        // 2. Build adjacency list for the visible part of the graph
 | 
			
		||||
        const adj = {};
 | 
			
		||||
        this.nodes.getIds().forEach(id => adj[id] = []);
 | 
			
		||||
        this.edges.forEach(edge => {
 | 
			
		||||
            if (!hiddenEdgeTypes.has(edge.metadata.relationship_type)) {
 | 
			
		||||
                adj[edge.from].push(edge.to);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    
 | 
			
		||||
        // 3. Traverse from "true roots" to find all reachable nodes
 | 
			
		||||
        const reachableNodes = new Set();
 | 
			
		||||
        const queue = [];
 | 
			
		||||
    
 | 
			
		||||
        // Start the traversal from true roots that aren't hidden by type
 | 
			
		||||
        this.trueRootIds.forEach(rootId => {
 | 
			
		||||
            const node = this.nodes.get(rootId);
 | 
			
		||||
            if (node && !hiddenNodeTypes.has(node.type)) {
 | 
			
		||||
                if (!reachableNodes.has(rootId)) {
 | 
			
		||||
                    queue.push(rootId);
 | 
			
		||||
                    reachableNodes.add(rootId);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    
 | 
			
		||||
        let head = 0;
 | 
			
		||||
        while (head < queue.length) {
 | 
			
		||||
            const u = queue[head++];
 | 
			
		||||
            
 | 
			
		||||
            for (const v of (adj[u] || [])) {
 | 
			
		||||
                if (!reachableNodes.has(v)) {
 | 
			
		||||
                    const node = this.nodes.get(v);
 | 
			
		||||
                    if (node && !hiddenNodeTypes.has(node.type)) {
 | 
			
		||||
                        reachableNodes.add(v);
 | 
			
		||||
                        queue.push(v);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    
 | 
			
		||||
        // 4. Create final node and edge visibility updates
 | 
			
		||||
 | 
			
		||||
        // Perform comprehensive analysis
 | 
			
		||||
        const analysis = this.analyzeGraphReachability(new Set(), excludedEdgeTypes, excludedNodeTypes);
 | 
			
		||||
 | 
			
		||||
        // Apply visibility updates
 | 
			
		||||
        const nodeUpdates = this.nodes.map(node => ({
 | 
			
		||||
            id: node.id,
 | 
			
		||||
            hidden: !reachableNodes.has(node.id)
 | 
			
		||||
            hidden: !analysis.reachableNodes.has(node.id)
 | 
			
		||||
        }));
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        const edgeUpdates = this.edges.map(edge => ({
 | 
			
		||||
            id: edge.id,
 | 
			
		||||
            hidden: hiddenEdgeTypes.has(edge.metadata.relationship_type) || !reachableNodes.has(edge.from) || !reachableNodes.has(edge.to)
 | 
			
		||||
            hidden: excludedEdgeTypes.has(edge.metadata?.relationship_type || '') ||
 | 
			
		||||
                   !analysis.reachableNodes.has(edge.from) ||
 | 
			
		||||
                   !analysis.reachableNodes.has(edge.to)
 | 
			
		||||
        }));
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
        this.nodes.update(nodeUpdates);
 | 
			
		||||
        this.edges.update(edgeUpdates);
 | 
			
		||||
    
 | 
			
		||||
        console.log(`Filters applied. Reachable nodes: ${reachableNodes.size}`);
 | 
			
		||||
 | 
			
		||||
        console.log(`Enhanced filters applied. Visible nodes: ${analysis.reachableNodes.size}`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * ENHANCED: Hide node with forensic integrity using reachability analysis
 | 
			
		||||
     * Replaces the existing hideNodeAndOrphans() method
 | 
			
		||||
     */
 | 
			
		||||
    hideNodeWithReachabilityAnalysis(nodeId) {
 | 
			
		||||
        console.log(`Hiding node ${nodeId} with reachability analysis...`);
 | 
			
		||||
        
 | 
			
		||||
        // Simulate hiding this node and analyze impact
 | 
			
		||||
        const excludedNodes = new Set([nodeId]);
 | 
			
		||||
        const analysis = this.analyzeGraphReachability(excludedNodes);
 | 
			
		||||
        
 | 
			
		||||
        // Nodes that will become unreachable (should be hidden)
 | 
			
		||||
        const nodesToHide = [nodeId, ...Array.from(analysis.unreachableNodes)];
 | 
			
		||||
        
 | 
			
		||||
        // Store history for potential revert
 | 
			
		||||
        const historyData = { 
 | 
			
		||||
            nodeIds: nodesToHide,
 | 
			
		||||
            operation: 'hide_with_reachability',
 | 
			
		||||
            timestamp: Date.now()
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Apply hiding with forensic documentation
 | 
			
		||||
        const updates = nodesToHide.map(id => ({ 
 | 
			
		||||
            id: id, 
 | 
			
		||||
            hidden: true,
 | 
			
		||||
            forensicNote: `Hidden due to reachability analysis from ${nodeId}`
 | 
			
		||||
        }));
 | 
			
		||||
        
 | 
			
		||||
        this.nodes.update(updates);
 | 
			
		||||
        this.addToHistory('hide', historyData);
 | 
			
		||||
 | 
			
		||||
        console.log(`Forensic hide operation: ${nodesToHide.length} nodes hidden`, {
 | 
			
		||||
            originalTarget: nodeId,
 | 
			
		||||
            cascadeNodes: nodesToHide.length - 1,
 | 
			
		||||
            isolatedClusters: analysis.isolatedClusters.length
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            hiddenNodes: nodesToHide,
 | 
			
		||||
            isolatedClusters: analysis.isolatedClusters
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * ENHANCED: Delete node with forensic integrity using reachability analysis
 | 
			
		||||
     * Replaces the existing deleteNodeAndOrphans() method
 | 
			
		||||
     */
 | 
			
		||||
    async deleteNodeWithReachabilityAnalysis(nodeId) {
 | 
			
		||||
        console.log(`Deleting node ${nodeId} with reachability analysis...`);
 | 
			
		||||
        
 | 
			
		||||
        // Simulate deletion and analyze impact
 | 
			
		||||
        const excludedNodes = new Set([nodeId]);
 | 
			
		||||
        const analysis = this.analyzeGraphReachability(excludedNodes);
 | 
			
		||||
        
 | 
			
		||||
        // Nodes that will become unreachable (should be deleted)
 | 
			
		||||
        const nodesToDelete = [nodeId, ...Array.from(analysis.unreachableNodes)];
 | 
			
		||||
        
 | 
			
		||||
        // Collect forensic data before deletion
 | 
			
		||||
        const historyData = {
 | 
			
		||||
            nodes: nodesToDelete.map(id => this.nodes.get(id)).filter(Boolean),
 | 
			
		||||
            edges: [],
 | 
			
		||||
            operation: 'delete_with_reachability',
 | 
			
		||||
            timestamp: Date.now(),
 | 
			
		||||
            forensicAnalysis: {
 | 
			
		||||
                originalTarget: nodeId,
 | 
			
		||||
                cascadeNodes: nodesToDelete.length - 1,
 | 
			
		||||
                isolatedClusters: analysis.isolatedClusters.length,
 | 
			
		||||
                clusterSizes: analysis.isolatedClusters.map(cluster => cluster.length)
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Collect affected edges
 | 
			
		||||
        nodesToDelete.forEach(id => {
 | 
			
		||||
            const connectedEdgeIds = this.network.getConnectedEdges(id);
 | 
			
		||||
            const edges = this.edges.get(connectedEdgeIds);
 | 
			
		||||
            historyData.edges.push(...edges);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Remove duplicates from edges
 | 
			
		||||
        historyData.edges = Array.from(new Map(historyData.edges.map(e => [e.id, e])).values());
 | 
			
		||||
 | 
			
		||||
        // Perform backend deletion with forensic logging
 | 
			
		||||
        let operationFailed = false;
 | 
			
		||||
        
 | 
			
		||||
        for (const targetNodeId of nodesToDelete) {
 | 
			
		||||
            try {
 | 
			
		||||
                const response = await fetch(`/api/graph/node/${targetNodeId}`, {
 | 
			
		||||
                    method: 'DELETE',
 | 
			
		||||
                    headers: {
 | 
			
		||||
                        'Content-Type': 'application/json',
 | 
			
		||||
                    },
 | 
			
		||||
                    body: JSON.stringify({
 | 
			
		||||
                        forensicContext: {
 | 
			
		||||
                            operation: 'reachability_cascade_delete',
 | 
			
		||||
                            originalTarget: nodeId,
 | 
			
		||||
                            analysisTimestamp: historyData.timestamp
 | 
			
		||||
                        }
 | 
			
		||||
                    })
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                const result = await response.json();
 | 
			
		||||
                if (!result.success) {
 | 
			
		||||
                    console.error(`Backend deletion failed for node ${targetNodeId}:`, result.error);
 | 
			
		||||
                    operationFailed = true;
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                console.log(`Node ${targetNodeId} deleted from backend with forensic context`);
 | 
			
		||||
                this.nodes.remove({ id: targetNodeId });
 | 
			
		||||
                
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                console.error(`API error during deletion of node ${targetNodeId}:`, error);
 | 
			
		||||
                operationFailed = true;
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Handle operation results
 | 
			
		||||
        if (!operationFailed) {
 | 
			
		||||
            this.addToHistory('delete', historyData);
 | 
			
		||||
            console.log(`Forensic delete operation completed:`, historyData.forensicAnalysis);
 | 
			
		||||
            
 | 
			
		||||
            return {
 | 
			
		||||
                success: true,
 | 
			
		||||
                deletedNodes: nodesToDelete,
 | 
			
		||||
                forensicAnalysis: historyData.forensicAnalysis
 | 
			
		||||
            };
 | 
			
		||||
        } else {
 | 
			
		||||
            // Revert UI changes if backend operations failed
 | 
			
		||||
            console.log("Reverting UI changes due to backend failure");
 | 
			
		||||
            this.nodes.add(historyData.nodes);
 | 
			
		||||
            this.edges.add(historyData.edges);
 | 
			
		||||
            
 | 
			
		||||
            return {
 | 
			
		||||
                success: false,
 | 
			
		||||
                error: "Backend deletion failed, UI reverted"
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -1207,29 +1460,34 @@ class GraphManager {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Perform action from the context menu
 | 
			
		||||
     * @param {string} action - The action to perform ('hide' or 'delete')
 | 
			
		||||
     * @param {string} nodeId - The ID of the node
 | 
			
		||||
     * UPDATED: Enhanced context menu actions using new methods
 | 
			
		||||
     * Updates the existing performContextMenuAction() method
 | 
			
		||||
     */
 | 
			
		||||
    performContextMenuAction(action, nodeId) {
 | 
			
		||||
        console.log('Performing action:', action, 'on node:', nodeId);
 | 
			
		||||
        console.log('Performing enhanced action:', action, 'on node:', nodeId);
 | 
			
		||||
        
 | 
			
		||||
        switch (action) {
 | 
			
		||||
            case 'focus':
 | 
			
		||||
                this.focusOnNode(nodeId);
 | 
			
		||||
                break;
 | 
			
		||||
                
 | 
			
		||||
            case 'hide':
 | 
			
		||||
                this.hideNodeAndOrphans(nodeId);
 | 
			
		||||
                // Use enhanced method with reachability analysis
 | 
			
		||||
                this.hideNodeWithReachabilityAnalysis(nodeId);
 | 
			
		||||
                break;
 | 
			
		||||
                
 | 
			
		||||
            case 'delete':
 | 
			
		||||
                this.deleteNodeAndOrphans(nodeId);
 | 
			
		||||
                // Use enhanced method with reachability analysis
 | 
			
		||||
                this.deleteNodeWithReachabilityAnalysis(nodeId);
 | 
			
		||||
                break;
 | 
			
		||||
                
 | 
			
		||||
            case 'details':
 | 
			
		||||
                const node = this.nodes.get(nodeId);
 | 
			
		||||
                if (node) {
 | 
			
		||||
                    this.showNodeDetails(node);
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
                
 | 
			
		||||
            default:
 | 
			
		||||
                console.warn('Unknown action:', action);
 | 
			
		||||
        }
 | 
			
		||||
@ -1289,121 +1547,6 @@ class GraphManager {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Hide a node and recursively hide any neighbors that become disconnected.
 | 
			
		||||
     * @param {string} nodeId - The ID of the node to start hiding from.
 | 
			
		||||
     */
 | 
			
		||||
    hideNodeAndOrphans(nodeId) {
 | 
			
		||||
        const historyData = { nodeIds: [] };
 | 
			
		||||
        const queue = [nodeId];
 | 
			
		||||
        const visited = new Set([nodeId]);
 | 
			
		||||
 | 
			
		||||
        while (queue.length > 0) {
 | 
			
		||||
            const currentId = queue.shift();
 | 
			
		||||
            const node = this.nodes.get(currentId);
 | 
			
		||||
            if (!node || node.hidden) continue;
 | 
			
		||||
 | 
			
		||||
            // 1. Hide the current node and add to history
 | 
			
		||||
            this.nodes.update({ id: currentId, hidden: true });
 | 
			
		||||
            historyData.nodeIds.push(currentId);
 | 
			
		||||
 | 
			
		||||
            // 2. Check its neighbors
 | 
			
		||||
            const neighbors = this.network.getConnectedNodes(currentId);
 | 
			
		||||
            for (const neighborId of neighbors) {
 | 
			
		||||
                if (visited.has(neighborId)) continue;
 | 
			
		||||
                
 | 
			
		||||
                const connectedEdges = this.network.getConnectedEdges(neighborId);
 | 
			
		||||
                let hasVisibleEdge = false;
 | 
			
		||||
                // 3. See if the neighbor still has any visible connections
 | 
			
		||||
                for (const edgeId of connectedEdges) {
 | 
			
		||||
                    const edge = this.edges.get(edgeId);
 | 
			
		||||
                    const sourceNode = this.nodes.get(edge.from);
 | 
			
		||||
                    const targetNode = this.nodes.get(edge.to);
 | 
			
		||||
                    if ((sourceNode && !sourceNode.hidden) && (targetNode && !targetNode.hidden)) {
 | 
			
		||||
                        hasVisibleEdge = true;
 | 
			
		||||
                        break;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                // 4. If no visible connections, add to queue to be hidden
 | 
			
		||||
                if (!hasVisibleEdge) {
 | 
			
		||||
                    queue.push(neighborId);
 | 
			
		||||
                    visited.add(neighborId);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        if (historyData.nodeIds.length > 0) {
 | 
			
		||||
            this.addToHistory('hide', historyData);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Delete a node and recursively delete any neighbors that become disconnected.
 | 
			
		||||
     * @param {string} nodeId - The ID of the node to start deleting from.
 | 
			
		||||
     */
 | 
			
		||||
    async deleteNodeAndOrphans(nodeId) {
 | 
			
		||||
        const deletionQueue = [nodeId];
 | 
			
		||||
        const processedForDeletion = new Set([nodeId]);
 | 
			
		||||
        const historyData = { nodes: [], edges: [] };
 | 
			
		||||
        let operationFailed = false;
 | 
			
		||||
 | 
			
		||||
        while (deletionQueue.length > 0) {
 | 
			
		||||
            const currentId = deletionQueue.shift();
 | 
			
		||||
            const node = this.nodes.get(currentId);
 | 
			
		||||
            if (!node) continue;
 | 
			
		||||
 | 
			
		||||
            const neighbors = this.network.getConnectedNodes(currentId);
 | 
			
		||||
            const connectedEdgeIds = this.network.getConnectedEdges(currentId);
 | 
			
		||||
            const edges = this.edges.get(connectedEdgeIds);
 | 
			
		||||
            
 | 
			
		||||
            // Store state for potential revert
 | 
			
		||||
            historyData.nodes.push(node);
 | 
			
		||||
            historyData.edges.push(...edges);
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                const response = await fetch(`/api/graph/node/${currentId}`, { method: 'DELETE' });
 | 
			
		||||
                const result = await response.json();
 | 
			
		||||
 | 
			
		||||
                if (!result.success) {
 | 
			
		||||
                    console.error(`Failed to delete node ${currentId} from backend:`, result.error);
 | 
			
		||||
                    operationFailed = true;
 | 
			
		||||
                    break; 
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                console.log(`Node ${currentId} deleted from backend.`);
 | 
			
		||||
                this.nodes.remove({ id: currentId }); // Remove from view
 | 
			
		||||
                
 | 
			
		||||
                // Check if former neighbors are now orphans
 | 
			
		||||
                neighbors.forEach(neighborId => {
 | 
			
		||||
                    if (!processedForDeletion.has(neighborId) && this.nodes.get(neighborId)) {
 | 
			
		||||
                        if (this.network.getConnectedEdges(neighborId).length === 0) {
 | 
			
		||||
                            deletionQueue.push(neighborId);
 | 
			
		||||
                            processedForDeletion.add(neighborId);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                console.error('Error during node deletion API call:', error);
 | 
			
		||||
                operationFailed = true;
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Add to history only if the entire operation was successful
 | 
			
		||||
        if (!operationFailed && historyData.nodes.length > 0) {
 | 
			
		||||
            // Ensure edges in history are unique
 | 
			
		||||
            historyData.edges = Array.from(new Map(historyData.edges.map(e => [e.id, e])).values());
 | 
			
		||||
            this.addToHistory('delete', historyData);
 | 
			
		||||
        } else if (operationFailed) {
 | 
			
		||||
            console.log("Reverting UI changes due to failed delete operation.");
 | 
			
		||||
            // If any part of the chain failed, restore the UI to its original state
 | 
			
		||||
            this.nodes.add(historyData.nodes);
 | 
			
		||||
            this.edges.add(historyData.edges);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Unhide all hidden nodes
 | 
			
		||||
     */
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user