context menu
This commit is contained in:
		
							parent
							
								
									f02381910d
								
							
						
					
					
						commit
						2658bd148b
					
				
							
								
								
									
										76
									
								
								app.py
									
									
									
									
									
								
							
							
						
						
									
										76
									
								
								app.py
									
									
									
									
									
								
							@ -13,6 +13,7 @@ import io
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from core.session_manager import session_manager
 | 
					from core.session_manager import session_manager
 | 
				
			||||||
from config import config
 | 
					from config import config
 | 
				
			||||||
 | 
					from core.graph_manager import NodeType
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
app = Flask(__name__)
 | 
					app = Flask(__name__)
 | 
				
			||||||
@ -278,6 +279,81 @@ def get_graph_data():
 | 
				
			|||||||
        }), 500
 | 
					        }), 500
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.route('/api/graph/node/<node_id>', methods=['DELETE'])
 | 
				
			||||||
 | 
					def delete_graph_node(node_id):
 | 
				
			||||||
 | 
					    """Delete a node from the graph for the current user session."""
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        user_session_id, scanner = get_user_scanner()
 | 
				
			||||||
 | 
					        if not scanner:
 | 
				
			||||||
 | 
					            return jsonify({'success': False, 'error': 'No active session found'}), 404
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        success = scanner.graph.remove_node(node_id)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if success:
 | 
				
			||||||
 | 
					            # Persist the change
 | 
				
			||||||
 | 
					            session_manager.update_session_scanner(user_session_id, scanner)
 | 
				
			||||||
 | 
					            return jsonify({'success': True, 'message': f'Node {node_id} deleted successfully.'})
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            return jsonify({'success': False, 'error': f'Node {node_id} not found in graph.'}), 404
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    except Exception as e:
 | 
				
			||||||
 | 
					        print(f"ERROR: Exception in delete_graph_node endpoint: {e}")
 | 
				
			||||||
 | 
					        traceback.print_exc()
 | 
				
			||||||
 | 
					        return jsonify({'success': False, 'error': f'Internal server error: {str(e)}'}), 500
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.route('/api/graph/revert', methods=['POST'])
 | 
				
			||||||
 | 
					def revert_graph_action():
 | 
				
			||||||
 | 
					    """Reverts a graph action, such as re-adding a deleted node."""
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        data = request.get_json()
 | 
				
			||||||
 | 
					        if not data or 'type' not in data or 'data' not in data:
 | 
				
			||||||
 | 
					            return jsonify({'success': False, 'error': 'Invalid revert request format'}), 400
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        user_session_id, scanner = get_user_scanner()
 | 
				
			||||||
 | 
					        if not scanner:
 | 
				
			||||||
 | 
					            return jsonify({'success': False, 'error': 'No active session found'}), 404
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        action_type = data['type']
 | 
				
			||||||
 | 
					        action_data = data['data']
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if action_type == 'delete':
 | 
				
			||||||
 | 
					            # Re-add the node
 | 
				
			||||||
 | 
					            node_to_add = action_data.get('node')
 | 
				
			||||||
 | 
					            if node_to_add:
 | 
				
			||||||
 | 
					                scanner.graph.add_node(
 | 
				
			||||||
 | 
					                    node_id=node_to_add['id'],
 | 
				
			||||||
 | 
					                    node_type=NodeType(node_to_add['type']),
 | 
				
			||||||
 | 
					                    attributes=node_to_add.get('attributes'),
 | 
				
			||||||
 | 
					                    description=node_to_add.get('description'),
 | 
				
			||||||
 | 
					                    metadata=node_to_add.get('metadata')
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            # Re-add the edges
 | 
				
			||||||
 | 
					            edges_to_add = action_data.get('edges', [])
 | 
				
			||||||
 | 
					            for edge in edges_to_add:
 | 
				
			||||||
 | 
					                # Add edge only if both nodes exist to prevent errors
 | 
				
			||||||
 | 
					                if scanner.graph.graph.has_node(edge['from']) and scanner.graph.graph.has_node(edge['to']):
 | 
				
			||||||
 | 
					                    scanner.graph.add_edge(
 | 
				
			||||||
 | 
					                        source_id=edge['from'],
 | 
				
			||||||
 | 
					                        target_id=edge['to'],
 | 
				
			||||||
 | 
					                        relationship_type=edge['metadata']['relationship_type'],
 | 
				
			||||||
 | 
					                        confidence_score=edge['metadata']['confidence_score'],
 | 
				
			||||||
 | 
					                        source_provider=edge['metadata']['source_provider'],
 | 
				
			||||||
 | 
					                        raw_data=edge.get('raw_data', {})
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            # Persist the change
 | 
				
			||||||
 | 
					            session_manager.update_session_scanner(user_session_id, scanner)
 | 
				
			||||||
 | 
					            return jsonify({'success': True, 'message': 'Delete action reverted successfully.'})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return jsonify({'success': False, 'error': f'Unknown revert action type: {action_type}'}), 400
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    except Exception as e:
 | 
				
			||||||
 | 
					        print(f"ERROR: Exception in revert_graph_action endpoint: {e}")
 | 
				
			||||||
 | 
					        traceback.print_exc()
 | 
				
			||||||
 | 
					        return jsonify({'success': False, 'error': f'Internal server error: {str(e)}'}), 500
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@app.route('/api/export', methods=['GET'])
 | 
					@app.route('/api/export', methods=['GET'])
 | 
				
			||||||
def export_results():
 | 
					def export_results():
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										1
									
								
								cache/crtsh/app_fleischkombinat-ost_de.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								cache/crtsh/app_fleischkombinat-ost_de.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					{"domain":"app.fleischkombinat-ost.de","first_cached":"2025-09-14T21:11:17.304989+00:00","last_upstream_query":"2025-09-14T21:11:17.304992+00:00","upstream_query_count":1,"certificates":[{"issuer_ca_id":295819,"issuer_name":"C=US, O=Let's Encrypt, CN=E6","common_name":"app.fleischkombinat-ost.de","name_value":"app.fleischkombinat-ost.de","id":19374493240,"entry_timestamp":"2025-07-01T14:09:36.354","not_before":"2025-07-01T13:11:00","not_after":"2025-09-29T13:10:59","serial_number":"0693231ff5e3212cabc2588e38b5d8337528","result_count":2},{"issuer_ca_id":295819,"issuer_name":"C=US, O=Let's Encrypt, CN=E6","common_name":"app.fleischkombinat-ost.de","name_value":"app.fleischkombinat-ost.de","id":19374489847,"entry_timestamp":"2025-07-01T14:09:30.117","not_before":"2025-07-01T13:11:00","not_after":"2025-09-29T13:10:59","serial_number":"0693231ff5e3212cabc2588e38b5d8337528","result_count":2}]}
 | 
				
			||||||
							
								
								
									
										1
									
								
								cache/crtsh/cc24_dev.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								cache/crtsh/cc24_dev.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								cache/crtsh/cloud_cc24_dev.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								cache/crtsh/cloud_cc24_dev.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					{"domain":"cloud.cc24.dev","first_cached":"2025-09-14T21:36:46.109884+00:00","last_upstream_query":"2025-09-14T21:36:46.109891+00:00","upstream_query_count":1,"certificates":[{"issuer_ca_id":295815,"issuer_name":"C=US, O=Let's Encrypt, CN=R11","common_name":"cloud.cc24.dev","name_value":"cloud.cc24.dev","id":20275852221,"entry_timestamp":"2025-08-12T00:08:47.671","not_before":"2025-08-11T23:10:16","not_after":"2025-11-09T23:10:15","serial_number":"0531b80da7039a455eb889201f8e62ba8cc9","result_count":2},{"issuer_ca_id":295815,"issuer_name":"C=US, O=Let's Encrypt, CN=R11","common_name":"cloud.cc24.dev","name_value":"cloud.cc24.dev","id":20275837420,"entry_timestamp":"2025-08-12T00:08:47.014","not_before":"2025-08-11T23:10:16","not_after":"2025-11-09T23:10:15","serial_number":"0531b80da7039a455eb889201f8e62ba8cc9","result_count":2},{"issuer_ca_id":295815,"issuer_name":"C=US, O=Let's Encrypt, CN=R11","common_name":"cloud.cc24.dev","name_value":"cloud.cc24.dev","id":18987132752,"entry_timestamp":"2025-06-13T00:06:00.445","not_before":"2025-06-12T23:07:30","not_after":"2025-09-10T23:07:29","serial_number":"066841f1e247045c3bb244d599955addbc66","result_count":2},{"issuer_ca_id":295815,"issuer_name":"C=US, O=Let's Encrypt, CN=R11","common_name":"cloud.cc24.dev","name_value":"cloud.cc24.dev","id":18987133145,"entry_timestamp":"2025-06-13T00:06:00.158","not_before":"2025-06-12T23:07:30","not_after":"2025-09-10T23:07:29","serial_number":"066841f1e247045c3bb244d599955addbc66","result_count":2},{"issuer_ca_id":295815,"issuer_name":"C=US, O=Let's Encrypt, CN=R11","common_name":"cloud.cc24.dev","name_value":"cloud.cc24.dev","id":17831462233,"entry_timestamp":"2025-04-14T00:17:56.109","not_before":"2025-04-13T23:19:24","not_after":"2025-07-12T23:19:23","serial_number":"051efd08e5e21db1fe47698ba7cb273c05b7","result_count":2},{"issuer_ca_id":295815,"issuer_name":"C=US, O=Let's Encrypt, CN=R11","common_name":"cloud.cc24.dev","name_value":"cloud.cc24.dev","id":17831396685,"entry_timestamp":"2025-04-14T00:17:54.708","not_before":"2025-04-13T23:19:24","not_after":"2025-07-12T23:19:23","serial_number":"051efd08e5e21db1fe47698ba7cb273c05b7","result_count":2},{"issuer_ca_id":295814,"issuer_name":"C=US, O=Let's Encrypt, CN=R10","common_name":"cloud.cc24.dev","name_value":"cloud.cc24.dev","id":17170810540,"entry_timestamp":"2025-02-13T00:02:07.841","not_before":"2025-02-12T23:03:37","not_after":"2025-05-13T23:03:36","serial_number":"03fe022033cd38b75215385397375a1a3741","result_count":2},{"issuer_ca_id":295814,"issuer_name":"C=US, O=Let's Encrypt, CN=R10","common_name":"cloud.cc24.dev","name_value":"cloud.cc24.dev","id":16704105434,"entry_timestamp":"2025-02-13T00:02:07.708","not_before":"2025-02-12T23:03:37","not_after":"2025-05-13T23:03:36","serial_number":"03fe022033cd38b75215385397375a1a3741","result_count":2},{"issuer_ca_id":295815,"issuer_name":"C=US, O=Let's Encrypt, CN=R11","common_name":"cloud.cc24.dev","name_value":"cloud.cc24.dev","id":17122390503,"entry_timestamp":"2025-02-09T15:21:20.386","not_before":"2025-02-09T14:22:49","not_after":"2025-05-10T14:22:48","serial_number":"031fcf4e1368ddbc276836806c2d3df6c376","result_count":2},{"issuer_ca_id":295815,"issuer_name":"C=US, O=Let's Encrypt, CN=R11","common_name":"cloud.cc24.dev","name_value":"cloud.cc24.dev","id":16635540435,"entry_timestamp":"2025-02-09T15:21:19.831","not_before":"2025-02-09T14:22:49","not_after":"2025-05-10T14:22:48","serial_number":"031fcf4e1368ddbc276836806c2d3df6c376","result_count":2}]}
 | 
				
			||||||
							
								
								
									
										1
									
								
								cache/crtsh/code_cc24_dev.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								cache/crtsh/code_cc24_dev.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								cache/crtsh/console_s3_cc24_dev.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								cache/crtsh/console_s3_cc24_dev.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					{"domain":"console.s3.cc24.dev","first_cached":"2025-09-14T21:33:25.149502+00:00","last_upstream_query":"2025-09-14T21:33:25.149505+00:00","upstream_query_count":1,"certificates":[{"issuer_ca_id":295819,"issuer_name":"C=US, O=Let's Encrypt, CN=E6","common_name":"console.s3.cc24.dev","name_value":"console.s3.cc24.dev","id":20287575466,"entry_timestamp":"2025-08-12T12:55:37.077","not_before":"2025-08-12T11:57:05","not_after":"2025-11-10T11:57:04","serial_number":"066bdfa83088f8d7e67284da94dd5d122ed6","result_count":2},{"issuer_ca_id":295819,"issuer_name":"C=US, O=Let's Encrypt, CN=E6","common_name":"console.s3.cc24.dev","name_value":"console.s3.cc24.dev","id":20287575457,"entry_timestamp":"2025-08-12T12:55:36.75","not_before":"2025-08-12T11:57:05","not_after":"2025-11-10T11:57:04","serial_number":"066bdfa83088f8d7e67284da94dd5d122ed6","result_count":2}]}
 | 
				
			||||||
							
								
								
									
										1
									
								
								cache/crtsh/dnsrecon_cc24_dev.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								cache/crtsh/dnsrecon_cc24_dev.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					{"domain":"dnsrecon.cc24.dev","first_cached":"2025-09-14T21:33:58.773156+00:00","last_upstream_query":"2025-09-14T21:33:58.773159+00:00","upstream_query_count":1,"certificates":[{"issuer_ca_id":295809,"issuer_name":"C=US, O=Let's Encrypt, CN=E8","common_name":"dnsrecon.cc24.dev","name_value":"dnsrecon.cc24.dev","id":20965278266,"entry_timestamp":"2025-09-12T09:49:23.247","not_before":"2025-09-12T08:50:53","not_after":"2025-12-11T08:50:52","serial_number":"060fbe619a364febd85aebccb1c6fcf7153f","result_count":2},{"issuer_ca_id":295809,"issuer_name":"C=US, O=Let's Encrypt, CN=E8","common_name":"dnsrecon.cc24.dev","name_value":"dnsrecon.cc24.dev","id":20965277886,"entry_timestamp":"2025-09-12T09:49:23.039","not_before":"2025-09-12T08:50:53","not_after":"2025-12-11T08:50:52","serial_number":"060fbe619a364febd85aebccb1c6fcf7153f","result_count":2}]}
 | 
				
			||||||
							
								
								
									
										1
									
								
								cache/crtsh/element_cc24_dev.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								cache/crtsh/element_cc24_dev.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								cache/crtsh/fleischkombinat-ost_de.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								cache/crtsh/fleischkombinat-ost_de.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					{"domain":"fleischkombinat-ost.de","first_cached":"2025-09-14T21:11:00.028593+00:00","last_upstream_query":"2025-09-14T21:11:00.028596+00:00","upstream_query_count":1,"certificates":[{"issuer_ca_id":295819,"issuer_name":"C=US, O=Let's Encrypt, CN=E6","common_name":"app.fleischkombinat-ost.de","name_value":"app.fleischkombinat-ost.de","id":19374493240,"entry_timestamp":"2025-07-01T14:09:36.354","not_before":"2025-07-01T13:11:00","not_after":"2025-09-29T13:10:59","serial_number":"0693231ff5e3212cabc2588e38b5d8337528","result_count":2},{"issuer_ca_id":295819,"issuer_name":"C=US, O=Let's Encrypt, CN=E6","common_name":"app.fleischkombinat-ost.de","name_value":"app.fleischkombinat-ost.de","id":19374489847,"entry_timestamp":"2025-07-01T14:09:30.117","not_before":"2025-07-01T13:11:00","not_after":"2025-09-29T13:10:59","serial_number":"0693231ff5e3212cabc2588e38b5d8337528","result_count":2},{"issuer_ca_id":295810,"issuer_name":"C=US, O=Let's Encrypt, CN=E5","common_name":"fleischkombinat-ost.de","name_value":"fleischkombinat-ost.de","id":19374378473,"entry_timestamp":"2025-07-01T14:01:50.593","not_before":"2025-07-01T13:03:20","not_after":"2025-09-29T13:03:19","serial_number":"06315dfed8c93d1497c26b21c448857b6f2c","result_count":2},{"issuer_ca_id":295810,"issuer_name":"C=US, O=Let's Encrypt, CN=E5","common_name":"fleischkombinat-ost.de","name_value":"fleischkombinat-ost.de","id":19374376791,"entry_timestamp":"2025-07-01T14:01:50.385","not_before":"2025-07-01T13:03:20","not_after":"2025-09-29T13:03:19","serial_number":"06315dfed8c93d1497c26b21c448857b6f2c","result_count":2},{"issuer_ca_id":158800,"issuer_name":"C=AT, O=ZeroSSL, CN=ZeroSSL RSA Domain Secure Site CA","common_name":"*.fleischkombinat-ost.de","name_value":"*.fleischkombinat-ost.de\nfleischkombinat-ost.de","id":19369530786,"entry_timestamp":"2025-07-01T09:12:02.496","not_before":"2025-07-01T00:00:00","not_after":"2025-09-29T23:59:59","serial_number":"07c51a2c164b3a6c5769b0e03a9f4085","result_count":3},{"issuer_ca_id":158800,"issuer_name":"C=AT, O=ZeroSSL, CN=ZeroSSL RSA Domain Secure Site CA","common_name":"*.fleischkombinat-ost.de","name_value":"*.fleischkombinat-ost.de\nfleischkombinat-ost.de","id":19369530780,"entry_timestamp":"2025-07-01T09:12:01.04","not_before":"2025-07-01T00:00:00","not_after":"2025-09-29T23:59:59","serial_number":"07c51a2c164b3a6c5769b0e03a9f4085","result_count":3}]}
 | 
				
			||||||
							
								
								
									
										1
									
								
								cache/crtsh/forensics_cc24_dev.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								cache/crtsh/forensics_cc24_dev.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								cache/crtsh/forum_cc24_dev.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								cache/crtsh/forum_cc24_dev.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					{"domain":"forum.cc24.dev","first_cached":"2025-09-14T21:37:55.208070+00:00","last_upstream_query":"2025-09-14T21:37:55.208073+00:00","upstream_query_count":1,"certificates":[{"issuer_ca_id":295815,"issuer_name":"C=US, O=Let's Encrypt, CN=R11","common_name":"forum.cc24.dev","name_value":"forum.cc24.dev","id":20275855672,"entry_timestamp":"2025-08-12T00:09:25.872","not_before":"2025-08-11T23:10:53","not_after":"2025-11-09T23:10:52","serial_number":"05e7fa80df3e45a7ecec44d25dafad0904e5","result_count":2},{"issuer_ca_id":295815,"issuer_name":"C=US, O=Let's Encrypt, CN=R11","common_name":"forum.cc24.dev","name_value":"forum.cc24.dev","id":20275850214,"entry_timestamp":"2025-08-12T00:09:23.776","not_before":"2025-08-11T23:10:53","not_after":"2025-11-09T23:10:52","serial_number":"05e7fa80df3e45a7ecec44d25dafad0904e5","result_count":2},{"issuer_ca_id":295814,"issuer_name":"C=US, O=Let's Encrypt, CN=R10","common_name":"forum.cc24.dev","name_value":"forum.cc24.dev","id":18987146858,"entry_timestamp":"2025-06-13T00:06:40.302","not_before":"2025-06-12T23:08:03","not_after":"2025-09-10T23:08:02","serial_number":"055efe65125ee83f14545a2f5d99590e635f","result_count":2},{"issuer_ca_id":295814,"issuer_name":"C=US, O=Let's Encrypt, CN=R10","common_name":"forum.cc24.dev","name_value":"forum.cc24.dev","id":18987145127,"entry_timestamp":"2025-06-13T00:06:34.097","not_before":"2025-06-12T23:08:03","not_after":"2025-09-10T23:08:02","serial_number":"055efe65125ee83f14545a2f5d99590e635f","result_count":2},{"issuer_ca_id":295814,"issuer_name":"C=US, O=Let's Encrypt, CN=R10","common_name":"forum.cc24.dev","name_value":"forum.cc24.dev","id":17831472866,"entry_timestamp":"2025-04-14T00:18:33.004","not_before":"2025-04-13T23:20:02","not_after":"2025-07-12T23:20:01","serial_number":"064fefe879ff5e732ce6c6a63f1e911db568","result_count":2},{"issuer_ca_id":295814,"issuer_name":"C=US, O=Let's Encrypt, CN=R10","common_name":"forum.cc24.dev","name_value":"forum.cc24.dev","id":17831402949,"entry_timestamp":"2025-04-14T00:18:32.224","not_before":"2025-04-13T23:20:02","not_after":"2025-07-12T23:20:01","serial_number":"064fefe879ff5e732ce6c6a63f1e911db568","result_count":2},{"issuer_ca_id":295815,"issuer_name":"C=US, O=Let's Encrypt, CN=R11","common_name":"forum.cc24.dev","name_value":"forum.cc24.dev","id":17170813968,"entry_timestamp":"2025-02-13T00:02:16.332","not_before":"2025-02-12T23:03:44","not_after":"2025-05-13T23:03:43","serial_number":"036e8e11288b54228648cab399477ea43c56","result_count":2},{"issuer_ca_id":295815,"issuer_name":"C=US, O=Let's Encrypt, CN=R11","common_name":"forum.cc24.dev","name_value":"forum.cc24.dev","id":16705244862,"entry_timestamp":"2025-02-13T00:02:14.119","not_before":"2025-02-12T23:03:44","not_after":"2025-05-13T23:03:43","serial_number":"036e8e11288b54228648cab399477ea43c56","result_count":2},{"issuer_ca_id":295815,"issuer_name":"C=US, O=Let's Encrypt, CN=R11","common_name":"forum.cc24.dev","name_value":"forum.cc24.dev","id":17122401304,"entry_timestamp":"2025-02-09T15:21:53.869","not_before":"2025-02-09T14:23:23","not_after":"2025-05-10T14:23:22","serial_number":"03e0928358e7636b3cc59a7dbc3b188bd7ca","result_count":2},{"issuer_ca_id":295815,"issuer_name":"C=US, O=Let's Encrypt, CN=R11","common_name":"forum.cc24.dev","name_value":"forum.cc24.dev","id":16636457115,"entry_timestamp":"2025-02-09T15:21:53.738","not_before":"2025-02-09T14:23:23","not_after":"2025-05-10T14:23:22","serial_number":"03e0928358e7636b3cc59a7dbc3b188bd7ca","result_count":2}]}
 | 
				
			||||||
							
								
								
									
										1
									
								
								cache/crtsh/git_cc24_dev.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								cache/crtsh/git_cc24_dev.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								cache/crtsh/graph_cc24_dev.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								cache/crtsh/graph_cc24_dev.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								cache/crtsh/hoarder_cc24_dev.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								cache/crtsh/hoarder_cc24_dev.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					{"domain":"hoarder.cc24.dev","first_cached":"2025-09-14T21:33:21.961821+00:00","last_upstream_query":"2025-09-14T21:33:21.961824+00:00","upstream_query_count":1,"certificates":[{"issuer_ca_id":295814,"issuer_name":"C=US, O=Let's Encrypt, CN=R10","common_name":"hoarder.cc24.dev","name_value":"hoarder.cc24.dev","id":17651896110,"entry_timestamp":"2025-04-03T19:39:13.874","not_before":"2025-04-03T18:40:43","not_after":"2025-07-02T18:40:42","serial_number":"062e2d17ef9c31ca560ab40299e0e00701c0","result_count":2},{"issuer_ca_id":295814,"issuer_name":"C=US, O=Let's Encrypt, CN=R10","common_name":"hoarder.cc24.dev","name_value":"hoarder.cc24.dev","id":17610697718,"entry_timestamp":"2025-04-03T19:39:13.522","not_before":"2025-04-03T18:40:43","not_after":"2025-07-02T18:40:42","serial_number":"062e2d17ef9c31ca560ab40299e0e00701c0","result_count":2}]}
 | 
				
			||||||
							
								
								
									
										1
									
								
								cache/crtsh/hub_cc24_dev.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								cache/crtsh/hub_cc24_dev.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					{"domain":"hub.cc24.dev","first_cached":"2025-09-14T21:33:48.364903+00:00","last_upstream_query":"2025-09-14T21:33:48.364908+00:00","upstream_query_count":1,"certificates":[{"issuer_ca_id":295810,"issuer_name":"C=US, O=Let's Encrypt, CN=E5","common_name":"hub.cc24.dev","name_value":"hub.cc24.dev","id":20317572962,"entry_timestamp":"2025-08-14T00:02:25.791","not_before":"2025-08-13T23:03:55","not_after":"2025-11-11T23:03:54","serial_number":"05ee38e8ae7bff4c5c414784e2c64d933b7a","result_count":2},{"issuer_ca_id":295810,"issuer_name":"C=US, O=Let's Encrypt, CN=E5","common_name":"hub.cc24.dev","name_value":"hub.cc24.dev","id":20317572564,"entry_timestamp":"2025-08-14T00:02:25.581","not_before":"2025-08-13T23:03:55","not_after":"2025-11-11T23:03:54","serial_number":"05ee38e8ae7bff4c5c414784e2c64d933b7a","result_count":2},{"issuer_ca_id":295819,"issuer_name":"C=US, O=Let's Encrypt, CN=E6","common_name":"hub.cc24.dev","name_value":"hub.cc24.dev","id":20189230503,"entry_timestamp":"2025-08-08T00:05:51.4","not_before":"2025-08-07T23:07:21","not_after":"2025-11-05T23:07:20","serial_number":"0502ddbfff49d3df12f3a15ba33ad895ccf1","result_count":2},{"issuer_ca_id":295819,"issuer_name":"C=US, O=Let's Encrypt, CN=E6","common_name":"hub.cc24.dev","name_value":"hub.cc24.dev","id":20189230585,"entry_timestamp":"2025-08-08T00:05:51.234","not_before":"2025-08-07T23:07:21","not_after":"2025-11-05T23:07:20","serial_number":"0502ddbfff49d3df12f3a15ba33ad895ccf1","result_count":2},{"issuer_ca_id":295819,"issuer_name":"C=US, O=Let's Encrypt, CN=E6","common_name":"hub.cc24.dev","name_value":"hub.cc24.dev","id":20060643580,"entry_timestamp":"2025-08-02T00:02:15.895","not_before":"2025-08-01T23:03:45","not_after":"2025-10-30T23:03:44","serial_number":"05c750b95573daa27e1a55a8803a2e6d21ad","result_count":2},{"issuer_ca_id":295819,"issuer_name":"C=US, O=Let's Encrypt, CN=E6","common_name":"hub.cc24.dev","name_value":"hub.cc24.dev","id":20060643807,"entry_timestamp":"2025-08-02T00:02:15.683","not_before":"2025-08-01T23:03:45","not_after":"2025-10-30T23:03:44","serial_number":"05c750b95573daa27e1a55a8803a2e6d21ad","result_count":2},{"issuer_ca_id":295819,"issuer_name":"C=US, O=Let's Encrypt, CN=E6","common_name":"hub.cc24.dev","name_value":"hub.cc24.dev","id":19929376671,"entry_timestamp":"2025-07-27T00:02:55.144","not_before":"2025-07-26T23:04:22","not_after":"2025-10-24T23:04:21","serial_number":"052c4fe632cf76d0306310cfb9faecc224ba","result_count":2},{"issuer_ca_id":295819,"issuer_name":"C=US, O=Let's Encrypt, CN=E6","common_name":"hub.cc24.dev","name_value":"hub.cc24.dev","id":19929375724,"entry_timestamp":"2025-07-27T00:02:52.759","not_before":"2025-07-26T23:04:22","not_after":"2025-10-24T23:04:21","serial_number":"052c4fe632cf76d0306310cfb9faecc224ba","result_count":2},{"issuer_ca_id":295819,"issuer_name":"C=US, O=Let's Encrypt, CN=E6","common_name":"hub.cc24.dev","name_value":"hub.cc24.dev","id":19803769083,"entry_timestamp":"2025-07-21T00:01:15.171","not_before":"2025-07-20T23:02:40","not_after":"2025-10-18T23:02:39","serial_number":"05e9927e9ea80c233e8a9502f11e4958cd06","result_count":2},{"issuer_ca_id":295819,"issuer_name":"C=US, O=Let's Encrypt, CN=E6","common_name":"hub.cc24.dev","name_value":"hub.cc24.dev","id":19803778513,"entry_timestamp":"2025-07-21T00:01:10.987","not_before":"2025-07-20T23:02:40","not_after":"2025-10-18T23:02:39","serial_number":"05e9927e9ea80c233e8a9502f11e4958cd06","result_count":2},{"issuer_ca_id":295810,"issuer_name":"C=US, O=Let's Encrypt, CN=E5","common_name":"hub.cc24.dev","name_value":"hub.cc24.dev","id":19665369127,"entry_timestamp":"2025-07-14T21:15:11.092","not_before":"2025-07-14T20:16:38","not_after":"2025-10-12T20:16:37","serial_number":"069e06aa4855496ab6b766889d6b45135d4e","result_count":2},{"issuer_ca_id":295810,"issuer_name":"C=US, O=Let's Encrypt, CN=E5","common_name":"hub.cc24.dev","name_value":"hub.cc24.dev","id":19665373526,"entry_timestamp":"2025-07-14T21:15:08.866","not_before":"2025-07-14T20:16:38","not_after":"2025-10-12T20:16:37","serial_number":"069e06aa4855496ab6b766889d6b45135d4e","result_count":2}]}
 | 
				
			||||||
							
								
								
									
										1
									
								
								cache/crtsh/keep_cc24_dev.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								cache/crtsh/keep_cc24_dev.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								cache/crtsh/matrix_cc24_dev.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								cache/crtsh/matrix_cc24_dev.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								cache/crtsh/misp_cc24_dev.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								cache/crtsh/misp_cc24_dev.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								cache/crtsh/mx_kundenserver_de.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								cache/crtsh/mx_kundenserver_de.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								cache/crtsh/s3_cc24_dev.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								cache/crtsh/s3_cc24_dev.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					{"domain":"s3.cc24.dev","first_cached":"2025-09-14T21:38:35.568153+00:00","last_upstream_query":"2025-09-14T21:38:35.568155+00:00","upstream_query_count":1,"certificates":[{"issuer_ca_id":295819,"issuer_name":"C=US, O=Let's Encrypt, CN=E6","common_name":"console.s3.cc24.dev","name_value":"console.s3.cc24.dev","id":20287575466,"entry_timestamp":"2025-08-12T12:55:37.077","not_before":"2025-08-12T11:57:05","not_after":"2025-11-10T11:57:04","serial_number":"066bdfa83088f8d7e67284da94dd5d122ed6","result_count":2},{"issuer_ca_id":295819,"issuer_name":"C=US, O=Let's Encrypt, CN=E6","common_name":"console.s3.cc24.dev","name_value":"console.s3.cc24.dev","id":20287575457,"entry_timestamp":"2025-08-12T12:55:36.75","not_before":"2025-08-12T11:57:05","not_after":"2025-11-10T11:57:04","serial_number":"066bdfa83088f8d7e67284da94dd5d122ed6","result_count":2},{"issuer_ca_id":295819,"issuer_name":"C=US, O=Let's Encrypt, CN=E6","common_name":"s3.cc24.dev","name_value":"s3.cc24.dev","id":20285693027,"entry_timestamp":"2025-08-12T10:58:41.611","not_before":"2025-08-12T10:00:11","not_after":"2025-11-10T10:00:10","serial_number":"06f0f73404258136977fdbfe4cf51db786a7","result_count":2},{"issuer_ca_id":295819,"issuer_name":"C=US, O=Let's Encrypt, CN=E6","common_name":"s3.cc24.dev","name_value":"s3.cc24.dev","id":20285693495,"entry_timestamp":"2025-08-12T10:58:41.366","not_before":"2025-08-12T10:00:11","not_after":"2025-11-10T10:00:10","serial_number":"06f0f73404258136977fdbfe4cf51db786a7","result_count":2}]}
 | 
				
			||||||
							
								
								
									
										1
									
								
								cache/crtsh/timesketch_cc24_dev.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								cache/crtsh/timesketch_cc24_dev.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								cache/crtsh/www_overcuriousity_org.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								cache/crtsh/www_overcuriousity_org.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					{"domain":"www.overcuriousity.org","first_cached":"2025-09-14T21:16:24.041839+00:00","last_upstream_query":"2025-09-14T21:16:24.041842+00:00","upstream_query_count":1,"certificates":[{"issuer_ca_id":295819,"issuer_name":"C=US, O=Let's Encrypt, CN=E6","common_name":"signaling.mikoshi.de","name_value":"www.overcuriousity.org","id":19208272013,"entry_timestamp":"2025-06-23T18:20:29.619","not_before":"2025-06-23T17:21:56","not_after":"2025-09-21T17:21:55","serial_number":"0540be6c8cb99dcaa5492af7b934f40466f9","result_count":1},{"issuer_ca_id":295819,"issuer_name":"C=US, O=Let's Encrypt, CN=E6","common_name":"signaling.mikoshi.de","name_value":"www.overcuriousity.org","id":19208263539,"entry_timestamp":"2025-06-23T18:20:26.82","not_before":"2025-06-23T17:21:56","not_after":"2025-09-21T17:21:55","serial_number":"0540be6c8cb99dcaa5492af7b934f40466f9","result_count":1}]}
 | 
				
			||||||
@ -414,6 +414,29 @@ class GraphManager:
 | 
				
			|||||||
        self.last_modified = datetime.now(timezone.utc).isoformat()
 | 
					        self.last_modified = datetime.now(timezone.utc).isoformat()
 | 
				
			||||||
        return True
 | 
					        return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def remove_node(self, node_id: str) -> bool:
 | 
				
			||||||
 | 
					        """Remove a node and its connected edges from the graph."""
 | 
				
			||||||
 | 
					        if not self.graph.has_node(node_id):
 | 
				
			||||||
 | 
					            return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Remove node from the graph (NetworkX handles removing connected edges)
 | 
				
			||||||
 | 
					        self.graph.remove_node(node_id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Clean up the correlation index
 | 
				
			||||||
 | 
					        keys_to_delete = []
 | 
				
			||||||
 | 
					        for value, nodes in self.correlation_index.items():
 | 
				
			||||||
 | 
					            if node_id in nodes:
 | 
				
			||||||
 | 
					                del nodes[node_id]
 | 
				
			||||||
 | 
					            if not nodes: # If no other nodes are associated with this value, remove it
 | 
				
			||||||
 | 
					                keys_to_delete.append(value)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        for key in keys_to_delete:
 | 
				
			||||||
 | 
					            if key in self.correlation_index:
 | 
				
			||||||
 | 
					                del self.correlation_index[key]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.last_modified = datetime.now(timezone.utc).isoformat()
 | 
				
			||||||
 | 
					        return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_node_count(self) -> int:
 | 
					    def get_node_count(self) -> int:
 | 
				
			||||||
        """Get total number of nodes in the graph."""
 | 
					        """Get total number of nodes in the graph."""
 | 
				
			||||||
        return self.graph.number_of_nodes()
 | 
					        return self.graph.number_of_nodes()
 | 
				
			||||||
 | 
				
			|||||||
@ -397,6 +397,35 @@ input[type="text"]:focus, select:focus {
 | 
				
			|||||||
    background: rgba(42, 42, 42, 1);
 | 
					    background: rgba(42, 42, 42, 1);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.graph-context-menu {
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    z-index: 1000;
 | 
				
			||||||
 | 
					    background-color: #2a2a2a;
 | 
				
			||||||
 | 
					    border: 1px solid #444;
 | 
				
			||||||
 | 
					    box-shadow: 0 2px 5px rgba(0,0,0,0.5);
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					    font-family: 'Roboto Mono', monospace;
 | 
				
			||||||
 | 
					    font-size: 0.9rem;
 | 
				
			||||||
 | 
					    color: #c7c7c7;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.graph-context-menu ul {
 | 
				
			||||||
 | 
					    list-style: none;
 | 
				
			||||||
 | 
					    padding: 0;
 | 
				
			||||||
 | 
					    margin: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.graph-context-menu ul li {
 | 
				
			||||||
 | 
					    padding: 0.75rem 1rem;
 | 
				
			||||||
 | 
					    cursor: pointer;
 | 
				
			||||||
 | 
					    transition: background-color 0.2s ease;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.graph-context-menu ul li:hover {
 | 
				
			||||||
 | 
					    background-color: #3a3a3a;
 | 
				
			||||||
 | 
					    color: #00ff41;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.graph-placeholder {
 | 
					.graph-placeholder {
 | 
				
			||||||
    display: flex;
 | 
					    display: flex;
 | 
				
			||||||
    align-items: center;
 | 
					    align-items: center;
 | 
				
			||||||
 | 
				
			|||||||
@ -12,6 +12,8 @@ class GraphManager {
 | 
				
			|||||||
        this.isInitialized = false;
 | 
					        this.isInitialized = false;
 | 
				
			||||||
        this.currentLayout = 'physics';
 | 
					        this.currentLayout = 'physics';
 | 
				
			||||||
        this.nodeInfoPopup = null;
 | 
					        this.nodeInfoPopup = null;
 | 
				
			||||||
 | 
					        this.contextMenu = null;
 | 
				
			||||||
 | 
					        this.history = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.options = {
 | 
					        this.options = {
 | 
				
			||||||
            nodes: {
 | 
					            nodes: {
 | 
				
			||||||
@ -117,6 +119,8 @@ class GraphManager {
 | 
				
			|||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.createNodeInfoPopup();
 | 
					        this.createNodeInfoPopup();
 | 
				
			||||||
 | 
					        this.createContextMenu();
 | 
				
			||||||
 | 
					        document.body.addEventListener('click', () => this.hideContextMenu());
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@ -128,6 +132,24 @@ class GraphManager {
 | 
				
			|||||||
        this.nodeInfoPopup.style.display = 'none';
 | 
					        this.nodeInfoPopup.style.display = 'none';
 | 
				
			||||||
        document.body.appendChild(this.nodeInfoPopup);
 | 
					        document.body.appendChild(this.nodeInfoPopup);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Create context menu
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    createContextMenu() {
 | 
				
			||||||
 | 
					        this.contextMenu = document.createElement('div');
 | 
				
			||||||
 | 
					        this.contextMenu.id = 'graph-context-menu';
 | 
				
			||||||
 | 
					        this.contextMenu.className = 'graph-context-menu';
 | 
				
			||||||
 | 
					        this.contextMenu.style.display = 'none';
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Prevent body click listener from firing when clicking the menu itself
 | 
				
			||||||
 | 
					        this.contextMenu.addEventListener('click', (event) => {
 | 
				
			||||||
 | 
					            event.stopPropagation();
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        document.body.appendChild(this.contextMenu);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Initialize the network graph
 | 
					     * Initialize the network graph
 | 
				
			||||||
@ -173,6 +195,8 @@ class GraphManager {
 | 
				
			|||||||
            <button class="graph-control-btn" id="graph-fit" title="Fit to Screen">[FIT]</button>
 | 
					            <button class="graph-control-btn" id="graph-fit" title="Fit to Screen">[FIT]</button>
 | 
				
			||||||
            <button class="graph-control-btn" id="graph-physics" title="Toggle Physics">[PHYSICS]</button>
 | 
					            <button class="graph-control-btn" id="graph-physics" title="Toggle Physics">[PHYSICS]</button>
 | 
				
			||||||
            <button class="graph-control-btn" id="graph-cluster" title="Cluster Nodes">[CLUSTER]</button>
 | 
					            <button class="graph-control-btn" id="graph-cluster" title="Cluster Nodes">[CLUSTER]</button>
 | 
				
			||||||
 | 
					            <button class="graph-control-btn" id="graph-unhide" title="Unhide All">[UNHIDE]</button>
 | 
				
			||||||
 | 
					            <button class="graph-control-btn" id="graph-revert" title="Revert Last Action">[REVERT]</button>
 | 
				
			||||||
        `;
 | 
					        `;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.container.appendChild(controlsContainer);
 | 
					        this.container.appendChild(controlsContainer);
 | 
				
			||||||
@ -181,6 +205,8 @@ class GraphManager {
 | 
				
			|||||||
        document.getElementById('graph-fit').addEventListener('click', () => this.fitView());
 | 
					        document.getElementById('graph-fit').addEventListener('click', () => this.fitView());
 | 
				
			||||||
        document.getElementById('graph-physics').addEventListener('click', () => this.togglePhysics());
 | 
					        document.getElementById('graph-physics').addEventListener('click', () => this.togglePhysics());
 | 
				
			||||||
        document.getElementById('graph-cluster').addEventListener('click', () => this.toggleClustering());
 | 
					        document.getElementById('graph-cluster').addEventListener('click', () => this.toggleClustering());
 | 
				
			||||||
 | 
					        document.getElementById('graph-unhide').addEventListener('click', () => this.unhideAll());
 | 
				
			||||||
 | 
					        document.getElementById('graph-revert').addEventListener('click', () => this.revertLastAction());
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@ -189,8 +215,29 @@ class GraphManager {
 | 
				
			|||||||
    setupNetworkEvents() {
 | 
					    setupNetworkEvents() {
 | 
				
			||||||
        if (!this.network) return;
 | 
					        if (!this.network) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Use a standard DOM event listener for the context menu for better reliability
 | 
				
			||||||
 | 
					        this.container.addEventListener('contextmenu', (event) => {
 | 
				
			||||||
 | 
					            event.preventDefault();
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Get coordinates relative to the canvas
 | 
				
			||||||
 | 
					            const pointer = {
 | 
				
			||||||
 | 
					                x: event.offsetX,
 | 
				
			||||||
 | 
					                y: event.offsetY
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            const nodeId = this.network.getNodeAt(pointer);
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if (nodeId) {
 | 
				
			||||||
 | 
					                // Pass the original client event for positioning
 | 
				
			||||||
 | 
					                this.showContextMenu(nodeId, event);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                this.hideContextMenu();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Node click event with details
 | 
					        // Node click event with details
 | 
				
			||||||
        this.network.on('click', (params) => {
 | 
					        this.network.on('click', (params) => {
 | 
				
			||||||
 | 
					            this.hideContextMenu();
 | 
				
			||||||
            if (params.nodes.length > 0) {
 | 
					            if (params.nodes.length > 0) {
 | 
				
			||||||
                const nodeId = params.nodes[0];
 | 
					                const nodeId = params.nodes[0];
 | 
				
			||||||
                if (this.network.isCluster(nodeId)) {
 | 
					                if (this.network.isCluster(nodeId)) {
 | 
				
			||||||
@ -216,10 +263,6 @@ class GraphManager {
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.network.on('oncontext', (params) => {
 | 
					 | 
				
			||||||
            params.event.preventDefault();
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Stabilization events with progress
 | 
					        // Stabilization events with progress
 | 
				
			||||||
        this.network.on('stabilizationProgress', (params) => {
 | 
					        this.network.on('stabilizationProgress', (params) => {
 | 
				
			||||||
            const progress = params.iterations / params.total;
 | 
					            const progress = params.iterations / params.total;
 | 
				
			||||||
@ -846,6 +889,7 @@ class GraphManager {
 | 
				
			|||||||
    clear() {
 | 
					    clear() {
 | 
				
			||||||
        this.nodes.clear();
 | 
					        this.nodes.clear();
 | 
				
			||||||
        this.edges.clear();
 | 
					        this.edges.clear();
 | 
				
			||||||
 | 
					        this.history = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Show placeholder
 | 
					        // Show placeholder
 | 
				
			||||||
        const placeholder = this.container.querySelector('.graph-placeholder');
 | 
					        const placeholder = this.container.querySelector('.graph-placeholder');
 | 
				
			||||||
@ -919,6 +963,238 @@ class GraphManager {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        console.log('Filters applied.');
 | 
					        console.log('Filters applied.');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Show context menu for a node
 | 
				
			||||||
 | 
					     * @param {string} nodeId - The ID of the node
 | 
				
			||||||
 | 
					     * @param {Event} event - The contextmenu event
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    showContextMenu(nodeId, event) {
 | 
				
			||||||
 | 
					        this.contextMenu.innerHTML = `
 | 
				
			||||||
 | 
					            <ul>
 | 
				
			||||||
 | 
					                <li data-action="hide" data-node-id="${nodeId}">Hide Node</li>
 | 
				
			||||||
 | 
					                <li data-action="delete" data-node-id="${nodeId}">Delete Node</li>
 | 
				
			||||||
 | 
					            </ul>
 | 
				
			||||||
 | 
					        `;
 | 
				
			||||||
 | 
					        this.contextMenu.style.left = `${event.clientX}px`;
 | 
				
			||||||
 | 
					        this.contextMenu.style.top = `${event.clientY}px`;
 | 
				
			||||||
 | 
					        this.contextMenu.style.display = 'block';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.contextMenu.querySelectorAll('li').forEach(item => {
 | 
				
			||||||
 | 
					            item.addEventListener('click', (e) => {
 | 
				
			||||||
 | 
					                const action = e.target.dataset.action;
 | 
				
			||||||
 | 
					                const nodeId = e.target.dataset.nodeId;
 | 
				
			||||||
 | 
					                this.performContextMenuAction(action, nodeId);
 | 
				
			||||||
 | 
					                this.hideContextMenu();
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Hide the context menu
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    hideContextMenu() {
 | 
				
			||||||
 | 
					        if (this.contextMenu) {
 | 
				
			||||||
 | 
					            this.contextMenu.style.display = 'none';
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Perform action from the context menu
 | 
				
			||||||
 | 
					     * @param {string} action - The action to perform ('hide' or 'delete')
 | 
				
			||||||
 | 
					     * @param {string} nodeId - The ID of the node
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    performContextMenuAction(action, nodeId) {
 | 
				
			||||||
 | 
					        switch (action) {
 | 
				
			||||||
 | 
					            case 'hide':
 | 
				
			||||||
 | 
					                this.hideNodeAndOrphans(nodeId);
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            case 'delete':
 | 
				
			||||||
 | 
					                this.deleteNodeAndOrphans(nodeId);
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Add an operation to the history stack
 | 
				
			||||||
 | 
					     * @param {string} type - The type of operation ('hide', 'delete')
 | 
				
			||||||
 | 
					     * @param {Object} data - The data needed to revert the operation
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    addToHistory(type, data) {
 | 
				
			||||||
 | 
					        this.history.push({ type, data });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Revert the last action
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async revertLastAction() {
 | 
				
			||||||
 | 
					        const lastAction = this.history.pop();
 | 
				
			||||||
 | 
					        if (!lastAction) {
 | 
				
			||||||
 | 
					            console.log('No actions to revert.');
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					        switch (lastAction.type) {
 | 
				
			||||||
 | 
					            case 'hide':
 | 
				
			||||||
 | 
					                // Revert hiding nodes by un-hiding them
 | 
				
			||||||
 | 
					                const updates = lastAction.data.nodeIds.map(id => ({ id: id, hidden: false }));
 | 
				
			||||||
 | 
					                this.nodes.update(updates);
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            case 'delete':
 | 
				
			||||||
 | 
					                try {
 | 
				
			||||||
 | 
					                    const response = await fetch('/api/graph/revert', {
 | 
				
			||||||
 | 
					                        method: 'POST',
 | 
				
			||||||
 | 
					                        headers: {
 | 
				
			||||||
 | 
					                            'Content-Type': 'application/json',
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                        body: JSON.stringify(lastAction)
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                    const result = await response.json();
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					                    if (result.success) {
 | 
				
			||||||
 | 
					                        console.log('Delete action reverted successfully on backend.');
 | 
				
			||||||
 | 
					                        // Re-add all nodes and edges from the history to the local view
 | 
				
			||||||
 | 
					                        this.nodes.add(lastAction.data.nodes);
 | 
				
			||||||
 | 
					                        this.edges.add(lastAction.data.edges);
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        console.error('Failed to revert delete action on backend:', result.error);
 | 
				
			||||||
 | 
					                        // Push the action back onto the history stack if the API call failed
 | 
				
			||||||
 | 
					                        this.history.push(lastAction);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                } catch (error) {
 | 
				
			||||||
 | 
					                    console.error('Error during revert API call:', error);
 | 
				
			||||||
 | 
					                    this.history.push(lastAction);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 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
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    unhideAll() {
 | 
				
			||||||
 | 
					        const allNodes = this.nodes.get({
 | 
				
			||||||
 | 
					            filter: (node) => node.hidden === true
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        const updates = allNodes.map(node => ({ id: node.id, hidden: false }));
 | 
				
			||||||
 | 
					        this.nodes.update(updates);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Export for use in main.js
 | 
					// Export for use in main.js
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user