full re implementation
This commit is contained in:
565
static/js/graph.js
Normal file
565
static/js/graph.js
Normal file
@@ -0,0 +1,565 @@
|
||||
/**
|
||||
* Graph visualization module for DNSRecon
|
||||
* Handles network graph rendering using vis.js
|
||||
*/
|
||||
|
||||
class GraphManager {
|
||||
constructor(containerId) {
|
||||
this.container = document.getElementById(containerId);
|
||||
this.network = null;
|
||||
this.nodes = new vis.DataSet();
|
||||
this.edges = new vis.DataSet();
|
||||
this.isInitialized = false;
|
||||
|
||||
// Graph options for cybersecurity theme
|
||||
this.options = {
|
||||
nodes: {
|
||||
shape: 'dot',
|
||||
size: 12,
|
||||
font: {
|
||||
size: 11,
|
||||
color: '#c7c7c7',
|
||||
face: 'Roboto Mono, monospace',
|
||||
background: 'rgba(26, 26, 26, 0.8)',
|
||||
strokeWidth: 1,
|
||||
strokeColor: '#000000'
|
||||
},
|
||||
borderWidth: 2,
|
||||
borderColor: '#444',
|
||||
shadow: {
|
||||
enabled: true,
|
||||
color: 'rgba(0, 0, 0, 0.3)',
|
||||
size: 3,
|
||||
x: 1,
|
||||
y: 1
|
||||
},
|
||||
scaling: {
|
||||
min: 8,
|
||||
max: 20
|
||||
}
|
||||
},
|
||||
edges: {
|
||||
width: 2,
|
||||
color: {
|
||||
color: '#444',
|
||||
highlight: '#00ff41',
|
||||
hover: '#ff9900'
|
||||
},
|
||||
font: {
|
||||
size: 10,
|
||||
color: '#999',
|
||||
face: 'Roboto Mono, monospace',
|
||||
background: 'rgba(26, 26, 26, 0.8)',
|
||||
strokeWidth: 1,
|
||||
strokeColor: '#000000'
|
||||
},
|
||||
arrows: {
|
||||
to: {
|
||||
enabled: true,
|
||||
scaleFactor: 0.8,
|
||||
type: 'arrow'
|
||||
}
|
||||
},
|
||||
smooth: {
|
||||
enabled: true,
|
||||
type: 'dynamic',
|
||||
roundness: 0.5
|
||||
},
|
||||
shadow: {
|
||||
enabled: true,
|
||||
color: 'rgba(0, 0, 0, 0.2)',
|
||||
size: 2,
|
||||
x: 1,
|
||||
y: 1
|
||||
}
|
||||
},
|
||||
physics: {
|
||||
enabled: true,
|
||||
stabilization: {
|
||||
enabled: true,
|
||||
iterations: 100,
|
||||
updateInterval: 25
|
||||
},
|
||||
barnesHut: {
|
||||
gravitationalConstant: -2000,
|
||||
centralGravity: 0.3,
|
||||
springLength: 95,
|
||||
springConstant: 0.04,
|
||||
damping: 0.09,
|
||||
avoidOverlap: 0.1
|
||||
},
|
||||
maxVelocity: 50,
|
||||
minVelocity: 0.1,
|
||||
solver: 'barnesHut',
|
||||
timestep: 0.35,
|
||||
adaptiveTimestep: true
|
||||
},
|
||||
interaction: {
|
||||
hover: true,
|
||||
hoverConnectedEdges: true,
|
||||
selectConnectedEdges: true,
|
||||
tooltipDelay: 200,
|
||||
hideEdgesOnDrag: false,
|
||||
hideNodesOnDrag: false
|
||||
},
|
||||
layout: {
|
||||
improvedLayout: true
|
||||
}
|
||||
};
|
||||
|
||||
this.setupEventHandlers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the network graph
|
||||
*/
|
||||
initialize() {
|
||||
if (this.isInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = {
|
||||
nodes: this.nodes,
|
||||
edges: this.edges
|
||||
};
|
||||
|
||||
this.network = new vis.Network(this.container, data, this.options);
|
||||
this.setupNetworkEvents();
|
||||
this.isInitialized = true;
|
||||
|
||||
// Hide placeholder
|
||||
const placeholder = this.container.querySelector('.graph-placeholder');
|
||||
if (placeholder) {
|
||||
placeholder.style.display = 'none';
|
||||
}
|
||||
|
||||
console.log('Graph initialized successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize graph:', error);
|
||||
this.showError('Failed to initialize visualization');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup network event handlers
|
||||
*/
|
||||
setupNetworkEvents() {
|
||||
if (!this.network) return;
|
||||
|
||||
// Node click event
|
||||
this.network.on('click', (params) => {
|
||||
if (params.nodes.length > 0) {
|
||||
const nodeId = params.nodes[0];
|
||||
this.showNodeDetails(nodeId);
|
||||
}
|
||||
});
|
||||
|
||||
// Hover events for tooltips
|
||||
this.network.on('hoverNode', (params) => {
|
||||
const nodeId = params.node;
|
||||
const node = this.nodes.get(nodeId);
|
||||
if (node) {
|
||||
this.showTooltip(params.pointer.DOM, node);
|
||||
}
|
||||
});
|
||||
|
||||
this.network.on('blurNode', () => {
|
||||
this.hideTooltip();
|
||||
});
|
||||
|
||||
// Stabilization events
|
||||
this.network.on('stabilizationProgress', (params) => {
|
||||
const progress = params.iterations / params.total;
|
||||
this.updateStabilizationProgress(progress);
|
||||
});
|
||||
|
||||
this.network.on('stabilizationIterationsDone', () => {
|
||||
this.onStabilizationComplete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update graph with new data
|
||||
* @param {Object} graphData - Graph data from backend
|
||||
*/
|
||||
updateGraph(graphData) {
|
||||
if (!graphData || !graphData.nodes || !graphData.edges) {
|
||||
console.warn('Invalid graph data received');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Initialize if not already done
|
||||
if (!this.isInitialized) {
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
// Process nodes
|
||||
const processedNodes = graphData.nodes.map(node => this.processNode(node));
|
||||
const processedEdges = graphData.edges.map(edge => this.processEdge(edge));
|
||||
|
||||
// Update datasets
|
||||
this.nodes.clear();
|
||||
this.edges.clear();
|
||||
this.nodes.add(processedNodes);
|
||||
this.edges.add(processedEdges);
|
||||
|
||||
// Fit the view if this is the first update or graph is small
|
||||
if (processedNodes.length <= 10) {
|
||||
setTimeout(() => this.fitView(), 500);
|
||||
}
|
||||
|
||||
console.log(`Graph updated: ${processedNodes.length} nodes, ${processedEdges.length} edges`);
|
||||
} catch (error) {
|
||||
console.error('Failed to update graph:', error);
|
||||
this.showError('Failed to update visualization');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process node data for visualization
|
||||
* @param {Object} node - Raw node data
|
||||
* @returns {Object} Processed node data
|
||||
*/
|
||||
processNode(node) {
|
||||
const processedNode = {
|
||||
id: node.id,
|
||||
label: this.formatNodeLabel(node.id, node.type),
|
||||
title: this.createNodeTooltip(node),
|
||||
color: this.getNodeColor(node.type),
|
||||
size: this.getNodeSize(node.type),
|
||||
borderColor: this.getNodeBorderColor(node.type),
|
||||
metadata: node.metadata || {}
|
||||
};
|
||||
|
||||
// Add type-specific styling
|
||||
if (node.type === 'domain') {
|
||||
processedNode.shape = 'dot';
|
||||
} else if (node.type === 'ip') {
|
||||
processedNode.shape = 'square';
|
||||
} else if (node.type === 'certificate') {
|
||||
processedNode.shape = 'diamond';
|
||||
} else if (node.type === 'asn') {
|
||||
processedNode.shape = 'triangle';
|
||||
}
|
||||
|
||||
return processedNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process edge data for visualization
|
||||
* @param {Object} edge - Raw edge data
|
||||
* @returns {Object} Processed edge data
|
||||
*/
|
||||
processEdge(edge) {
|
||||
const confidence = edge.confidence_score || 0;
|
||||
const processedEdge = {
|
||||
id: `${edge.from}-${edge.to}`,
|
||||
from: edge.from,
|
||||
to: edge.to,
|
||||
label: this.formatEdgeLabel(edge.label, confidence),
|
||||
title: this.createEdgeTooltip(edge),
|
||||
width: this.getEdgeWidth(confidence),
|
||||
color: this.getEdgeColor(confidence),
|
||||
dashes: confidence < 0.6 ? [5, 5] : false
|
||||
};
|
||||
|
||||
return processedEdge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format node label for display
|
||||
* @param {string} nodeId - Node identifier
|
||||
* @param {string} nodeType - Node type
|
||||
* @returns {string} Formatted label
|
||||
*/
|
||||
formatNodeLabel(nodeId, nodeType) {
|
||||
// Truncate long domain names
|
||||
if (nodeId.length > 20) {
|
||||
return nodeId.substring(0, 17) + '...';
|
||||
}
|
||||
return nodeId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format edge label for display
|
||||
* @param {string} relationshipType - Type of relationship
|
||||
* @param {number} confidence - Confidence score
|
||||
* @returns {string} Formatted label
|
||||
*/
|
||||
formatEdgeLabel(relationshipType, confidence) {
|
||||
if (!relationshipType) return '';
|
||||
|
||||
const confidenceText = confidence >= 0.8 ? '●' : confidence >= 0.6 ? '◐' : '○';
|
||||
return `${relationshipType} ${confidenceText}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get node color based on type
|
||||
* @param {string} nodeType - Node type
|
||||
* @returns {string} Color value
|
||||
*/
|
||||
getNodeColor(nodeType) {
|
||||
const colors = {
|
||||
'domain': '#00ff41', // Green
|
||||
'ip': '#ff9900', // Amber
|
||||
'certificate': '#c7c7c7', // Gray
|
||||
'asn': '#00aaff' // Blue
|
||||
};
|
||||
return colors[nodeType] || '#ffffff';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get node border color based on type
|
||||
* @param {string} nodeType - Node type
|
||||
* @returns {string} Border color value
|
||||
*/
|
||||
getNodeBorderColor(nodeType) {
|
||||
const borderColors = {
|
||||
'domain': '#00aa2e',
|
||||
'ip': '#cc7700',
|
||||
'certificate': '#999999',
|
||||
'asn': '#0088cc'
|
||||
};
|
||||
return borderColors[nodeType] || '#666666';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get node size based on type
|
||||
* @param {string} nodeType - Node type
|
||||
* @returns {number} Node size
|
||||
*/
|
||||
getNodeSize(nodeType) {
|
||||
const sizes = {
|
||||
'domain': 12,
|
||||
'ip': 14,
|
||||
'certificate': 10,
|
||||
'asn': 16
|
||||
};
|
||||
return sizes[nodeType] || 12;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get edge color based on confidence
|
||||
* @param {number} confidence - Confidence score
|
||||
* @returns {string} Edge color
|
||||
*/
|
||||
getEdgeColor(confidence) {
|
||||
if (confidence >= 0.8) {
|
||||
return '#00ff41'; // High confidence - green
|
||||
} else if (confidence >= 0.6) {
|
||||
return '#ff9900'; // Medium confidence - amber
|
||||
} else {
|
||||
return '#666666'; // Low confidence - gray
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get edge width based on confidence
|
||||
* @param {number} confidence - Confidence score
|
||||
* @returns {number} Edge width
|
||||
*/
|
||||
getEdgeWidth(confidence) {
|
||||
if (confidence >= 0.8) {
|
||||
return 3;
|
||||
} else if (confidence >= 0.6) {
|
||||
return 2;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create node tooltip
|
||||
* @param {Object} node - Node data
|
||||
* @returns {string} HTML tooltip content
|
||||
*/
|
||||
createNodeTooltip(node) {
|
||||
let tooltip = `<div style="font-family: 'Roboto Mono', monospace; font-size: 11px;">`;
|
||||
tooltip += `<div style="color: #00ff41; font-weight: bold; margin-bottom: 4px;">${node.id}</div>`;
|
||||
tooltip += `<div style="color: #999; margin-bottom: 2px;">Type: ${node.type}</div>`;
|
||||
|
||||
if (node.metadata && Object.keys(node.metadata).length > 0) {
|
||||
tooltip += `<div style="color: #999; margin-top: 4px; border-top: 1px solid #444; padding-top: 4px;">`;
|
||||
tooltip += `Click for details</div>`;
|
||||
}
|
||||
|
||||
tooltip += `</div>`;
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create edge tooltip
|
||||
* @param {Object} edge - Edge data
|
||||
* @returns {string} HTML tooltip content
|
||||
*/
|
||||
createEdgeTooltip(edge) {
|
||||
let tooltip = `<div style="font-family: 'Roboto Mono', monospace; font-size: 11px;">`;
|
||||
tooltip += `<div style="color: #00ff41; font-weight: bold; margin-bottom: 4px;">${edge.label || 'Relationship'}</div>`;
|
||||
tooltip += `<div style="color: #999; margin-bottom: 2px;">Confidence: ${(edge.confidence_score * 100).toFixed(1)}%</div>`;
|
||||
|
||||
if (edge.source_provider) {
|
||||
tooltip += `<div style="color: #999; margin-bottom: 2px;">Source: ${edge.source_provider}</div>`;
|
||||
}
|
||||
|
||||
if (edge.discovery_timestamp) {
|
||||
const date = new Date(edge.discovery_timestamp);
|
||||
tooltip += `<div style="color: #666; font-size: 10px;">Discovered: ${date.toLocaleString()}</div>`;
|
||||
}
|
||||
|
||||
tooltip += `</div>`;
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show node details in modal
|
||||
* @param {string} nodeId - Node identifier
|
||||
*/
|
||||
showNodeDetails(nodeId) {
|
||||
const node = this.nodes.get(nodeId);
|
||||
if (!node) return;
|
||||
|
||||
// Trigger custom event for main application to handle
|
||||
const event = new CustomEvent('nodeSelected', {
|
||||
detail: { nodeId, node }
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show tooltip
|
||||
* @param {Object} position - Mouse position
|
||||
* @param {Object} node - Node data
|
||||
*/
|
||||
showTooltip(position, node) {
|
||||
// Tooltip is handled by vis.js automatically
|
||||
// This method is for custom tooltip implementation if needed
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide tooltip
|
||||
*/
|
||||
hideTooltip() {
|
||||
// Tooltip hiding is handled by vis.js automatically
|
||||
}
|
||||
|
||||
/**
|
||||
* Update stabilization progress
|
||||
* @param {number} progress - Progress value (0-1)
|
||||
*/
|
||||
updateStabilizationProgress(progress) {
|
||||
// Could show a progress indicator if needed
|
||||
console.log(`Graph stabilization: ${(progress * 100).toFixed(1)}%`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle stabilization completion
|
||||
*/
|
||||
onStabilizationComplete() {
|
||||
console.log('Graph stabilization complete');
|
||||
}
|
||||
|
||||
/**
|
||||
* Fit the view to show all nodes
|
||||
*/
|
||||
fitView() {
|
||||
if (this.network) {
|
||||
this.network.fit({
|
||||
animation: {
|
||||
duration: 1000,
|
||||
easingFunction: 'easeInOutQuad'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the view to initial state
|
||||
*/
|
||||
resetView() {
|
||||
if (this.network) {
|
||||
this.network.moveTo({
|
||||
position: { x: 0, y: 0 },
|
||||
scale: 1,
|
||||
animation: {
|
||||
duration: 1000,
|
||||
easingFunction: 'easeInOutQuad'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the graph
|
||||
*/
|
||||
clear() {
|
||||
this.nodes.clear();
|
||||
this.edges.clear();
|
||||
|
||||
// Show placeholder
|
||||
const placeholder = this.container.querySelector('.graph-placeholder');
|
||||
if (placeholder) {
|
||||
placeholder.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show error message
|
||||
* @param {string} message - Error message
|
||||
*/
|
||||
showError(message) {
|
||||
const placeholder = this.container.querySelector('.graph-placeholder .placeholder-text');
|
||||
if (placeholder) {
|
||||
placeholder.textContent = `Error: ${message}`;
|
||||
placeholder.style.color = '#ff6b6b';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup control event handlers
|
||||
*/
|
||||
setupEventHandlers() {
|
||||
// Reset view button
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const resetBtn = document.getElementById('reset-view');
|
||||
if (resetBtn) {
|
||||
resetBtn.addEventListener('click', () => this.resetView());
|
||||
}
|
||||
|
||||
const fitBtn = document.getElementById('fit-view');
|
||||
if (fitBtn) {
|
||||
fitBtn.addEventListener('click', () => this.fitView());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get network statistics
|
||||
* @returns {Object} Statistics object
|
||||
*/
|
||||
getStatistics() {
|
||||
return {
|
||||
nodeCount: this.nodes.length,
|
||||
edgeCount: this.edges.length,
|
||||
//isStabilized: this.network ? this.network.isStabilized() : false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Export graph as image (if needed for future implementation)
|
||||
* @param {string} format - Image format ('png', 'jpeg')
|
||||
* @returns {string} Data URL of the image
|
||||
*/
|
||||
exportAsImage(format = 'png') {
|
||||
if (!this.network) return null;
|
||||
|
||||
// This would require additional vis.js functionality
|
||||
// Placeholder for future implementation
|
||||
console.log('Image export not yet implemented');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in main.js
|
||||
window.GraphManager = GraphManager;
|
||||
969
static/js/main.js
Normal file
969
static/js/main.js
Normal file
@@ -0,0 +1,969 @@
|
||||
/**
|
||||
* Main application logic for DNSRecon web interface
|
||||
* Handles UI interactions, API communication, and data flow
|
||||
* DEBUG VERSION WITH EXTRA LOGGING
|
||||
*/
|
||||
|
||||
class DNSReconApp {
|
||||
constructor() {
|
||||
console.log('DNSReconApp constructor called');
|
||||
this.graphManager = null;
|
||||
this.scanStatus = 'idle';
|
||||
this.pollInterval = null;
|
||||
this.currentSessionId = null;
|
||||
|
||||
// UI Elements
|
||||
this.elements = {};
|
||||
|
||||
// Application state
|
||||
this.isScanning = false;
|
||||
this.lastGraphUpdate = null;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the application
|
||||
*/
|
||||
init() {
|
||||
console.log('DNSReconApp init called');
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
console.log('DOM loaded, initializing application...');
|
||||
try {
|
||||
this.initializeElements();
|
||||
this.setupEventHandlers();
|
||||
this.initializeGraph();
|
||||
this.updateStatus();
|
||||
this.loadProviders();
|
||||
|
||||
console.log('DNSRecon application initialized successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize DNSRecon application:', error);
|
||||
this.showError(`Initialization failed: ${error.message}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize DOM element references
|
||||
*/
|
||||
initializeElements() {
|
||||
console.log('Initializing DOM elements...');
|
||||
this.elements = {
|
||||
// Form elements
|
||||
targetDomain: document.getElementById('target-domain'),
|
||||
maxDepth: document.getElementById('max-depth'),
|
||||
startScan: document.getElementById('start-scan'),
|
||||
stopScan: document.getElementById('stop-scan'),
|
||||
exportResults: document.getElementById('export-results'),
|
||||
|
||||
// Status elements
|
||||
scanStatus: document.getElementById('scan-status'),
|
||||
targetDisplay: document.getElementById('target-display'),
|
||||
depthDisplay: document.getElementById('depth-display'),
|
||||
progressDisplay: document.getElementById('progress-display'),
|
||||
indicatorsDisplay: document.getElementById('indicators-display'),
|
||||
relationshipsDisplay: document.getElementById('relationships-display'),
|
||||
progressFill: document.getElementById('progress-fill'),
|
||||
|
||||
// Provider elements
|
||||
providerList: document.getElementById('provider-list'),
|
||||
|
||||
// Modal elements
|
||||
nodeModal: document.getElementById('node-modal'),
|
||||
modalTitle: document.getElementById('modal-title'),
|
||||
modalDetails: document.getElementById('modal-details'),
|
||||
modalClose: document.getElementById('modal-close'),
|
||||
|
||||
// Other elements
|
||||
sessionId: document.getElementById('session-id'),
|
||||
connectionStatus: document.getElementById('connection-status')
|
||||
};
|
||||
|
||||
// Verify critical elements exist
|
||||
const requiredElements = ['targetDomain', 'startScan', 'scanStatus'];
|
||||
for (const elementName of requiredElements) {
|
||||
if (!this.elements[elementName]) {
|
||||
throw new Error(`Required element '${elementName}' not found in DOM`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('DOM elements initialized successfully');
|
||||
this.createMessageContainer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a message container for showing user feedback
|
||||
*/
|
||||
createMessageContainer() {
|
||||
// Check if message container already exists
|
||||
let messageContainer = document.getElementById('message-container');
|
||||
if (!messageContainer) {
|
||||
messageContainer = document.createElement('div');
|
||||
messageContainer.id = 'message-container';
|
||||
messageContainer.className = 'message-container';
|
||||
messageContainer.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
max-width: 400px;
|
||||
`;
|
||||
document.body.appendChild(messageContainer);
|
||||
console.log('Message container created');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup event handlers
|
||||
*/
|
||||
setupEventHandlers() {
|
||||
console.log('Setting up event handlers...');
|
||||
|
||||
try {
|
||||
// Form interactions
|
||||
this.elements.startScan.addEventListener('click', (e) => {
|
||||
console.log('Start scan button clicked');
|
||||
e.preventDefault();
|
||||
this.startScan();
|
||||
});
|
||||
|
||||
this.elements.stopScan.addEventListener('click', (e) => {
|
||||
console.log('Stop scan button clicked');
|
||||
e.preventDefault();
|
||||
this.stopScan();
|
||||
});
|
||||
|
||||
this.elements.exportResults.addEventListener('click', (e) => {
|
||||
console.log('Export results button clicked');
|
||||
e.preventDefault();
|
||||
this.exportResults();
|
||||
});
|
||||
|
||||
// Enter key support for target domain input
|
||||
this.elements.targetDomain.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter' && !this.isScanning) {
|
||||
console.log('Enter key pressed in domain input');
|
||||
this.startScan();
|
||||
}
|
||||
});
|
||||
|
||||
// Modal interactions
|
||||
if (this.elements.modalClose) {
|
||||
this.elements.modalClose.addEventListener('click', () => this.hideModal());
|
||||
}
|
||||
|
||||
if (this.elements.nodeModal) {
|
||||
this.elements.nodeModal.addEventListener('click', (e) => {
|
||||
if (e.target === this.elements.nodeModal) {
|
||||
this.hideModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Custom events
|
||||
document.addEventListener('nodeSelected', (e) => {
|
||||
this.showNodeModal(e.detail.nodeId, e.detail.node);
|
||||
});
|
||||
|
||||
// Keyboard shortcuts
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
this.hideModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Window events
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (this.isScanning) {
|
||||
return 'A scan is currently in progress. Are you sure you want to leave?';
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Event handlers set up successfully');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to setup event handlers:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize graph visualization
|
||||
*/
|
||||
initializeGraph() {
|
||||
try {
|
||||
console.log('Initializing graph manager...');
|
||||
this.graphManager = new GraphManager('network-graph');
|
||||
console.log('Graph manager initialized successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize graph manager:', error);
|
||||
this.showError('Failed to initialize graph visualization');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a reconnaissance scan
|
||||
*/
|
||||
async startScan() {
|
||||
console.log('=== STARTING SCAN ===');
|
||||
|
||||
try {
|
||||
const targetDomain = this.elements.targetDomain.value.trim();
|
||||
const maxDepth = parseInt(this.elements.maxDepth.value);
|
||||
|
||||
console.log(`Target domain: "${targetDomain}", Max depth: ${maxDepth}`);
|
||||
|
||||
// Validation
|
||||
if (!targetDomain) {
|
||||
console.log('Validation failed: empty domain');
|
||||
this.showError('Please enter a target domain');
|
||||
this.elements.targetDomain.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isValidDomain(targetDomain)) {
|
||||
console.log(`Validation failed: invalid domain format for "${targetDomain}"`);
|
||||
this.showError('Please enter a valid domain name (e.g., example.com)');
|
||||
this.elements.targetDomain.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Validation passed, setting UI state to scanning...');
|
||||
this.setUIState('scanning');
|
||||
this.showInfo('Starting reconnaissance scan...');
|
||||
|
||||
console.log('Making API call to start scan...');
|
||||
|
||||
const requestData = {
|
||||
target_domain: targetDomain,
|
||||
max_depth: maxDepth
|
||||
};
|
||||
|
||||
console.log('Request data:', requestData);
|
||||
|
||||
const response = await this.apiCall('/api/scan/start', 'POST', requestData);
|
||||
|
||||
console.log('API response received:', response);
|
||||
|
||||
if (response.success) {
|
||||
this.currentSessionId = response.scan_id;
|
||||
console.log('Starting polling with session ID:', this.currentSessionId);
|
||||
this.startPolling();
|
||||
this.showSuccess('Reconnaissance scan started successfully');
|
||||
|
||||
// Clear previous graph
|
||||
this.graphManager.clear();
|
||||
|
||||
console.log(`Scan started for ${targetDomain} with depth ${maxDepth}`);
|
||||
|
||||
// Force an immediate status update
|
||||
console.log('Forcing immediate status update...');
|
||||
setTimeout(() => {
|
||||
this.updateStatus();
|
||||
this.updateGraph();
|
||||
}, 100);
|
||||
|
||||
} else {
|
||||
throw new Error(response.error || 'Failed to start scan');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to start scan:', error);
|
||||
this.showError(`Failed to start scan: ${error.message}`);
|
||||
this.setUIState('idle');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the current scan
|
||||
*/
|
||||
async stopScan() {
|
||||
try {
|
||||
console.log('Stopping scan...');
|
||||
const response = await this.apiCall('/api/scan/stop', 'POST');
|
||||
|
||||
if (response.success) {
|
||||
this.showSuccess('Scan stop requested');
|
||||
console.log('Scan stop requested');
|
||||
} else {
|
||||
throw new Error(response.error || 'Failed to stop scan');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to stop scan:', error);
|
||||
this.showError(`Failed to stop scan: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export scan results
|
||||
*/
|
||||
async exportResults() {
|
||||
try {
|
||||
console.log('Exporting results...');
|
||||
|
||||
// Create a temporary link to trigger download
|
||||
const link = document.createElement('a');
|
||||
link.href = '/api/export';
|
||||
link.download = ''; // Let server determine filename
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
this.showSuccess('Results export initiated');
|
||||
console.log('Results export initiated');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to export results:', error);
|
||||
this.showError(`Failed to export results: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start polling for scan updates
|
||||
*/
|
||||
startPolling() {
|
||||
console.log('=== STARTING POLLING ===');
|
||||
|
||||
if (this.pollInterval) {
|
||||
console.log('Clearing existing poll interval');
|
||||
clearInterval(this.pollInterval);
|
||||
}
|
||||
|
||||
this.pollInterval = setInterval(() => {
|
||||
console.log('--- Polling tick ---');
|
||||
this.updateStatus();
|
||||
this.updateGraph();
|
||||
}, 1000); // Poll every 1 second for debugging
|
||||
|
||||
console.log('Polling started with 1 second interval');
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop polling for updates
|
||||
*/
|
||||
stopPolling() {
|
||||
console.log('=== STOPPING POLLING ===');
|
||||
if (this.pollInterval) {
|
||||
clearInterval(this.pollInterval);
|
||||
this.pollInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update scan status from server
|
||||
*/
|
||||
async updateStatus() {
|
||||
try {
|
||||
console.log('Updating status...');
|
||||
const response = await this.apiCall('/api/scan/status');
|
||||
|
||||
console.log('Status response:', response);
|
||||
|
||||
if (response.success) {
|
||||
const status = response.status;
|
||||
console.log('Current scan status:', status.status);
|
||||
console.log('Current progress:', status.progress_percentage + '%');
|
||||
console.log('Graph stats:', status.graph_statistics);
|
||||
|
||||
this.updateStatusDisplay(status);
|
||||
|
||||
// Handle status changes
|
||||
if (status.status !== this.scanStatus) {
|
||||
console.log(`*** STATUS CHANGED: ${this.scanStatus} -> ${status.status} ***`);
|
||||
this.handleStatusChange(status.status);
|
||||
}
|
||||
|
||||
this.scanStatus = status.status;
|
||||
} else {
|
||||
console.error('Status update failed:', response);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to update status:', error);
|
||||
this.showConnectionError();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update graph from server
|
||||
*/
|
||||
async updateGraph() {
|
||||
try {
|
||||
console.log('Updating graph...');
|
||||
const response = await this.apiCall('/api/graph');
|
||||
|
||||
console.log('Graph response:', response);
|
||||
|
||||
if (response.success) {
|
||||
const graphData = response.graph;
|
||||
|
||||
console.log('Graph data received:');
|
||||
console.log('- Nodes:', graphData.nodes ? graphData.nodes.length : 0);
|
||||
console.log('- Edges:', graphData.edges ? graphData.edges.length : 0);
|
||||
|
||||
if (graphData.nodes) {
|
||||
graphData.nodes.forEach(node => {
|
||||
console.log(` Node: ${node.id} (${node.type})`);
|
||||
});
|
||||
}
|
||||
|
||||
if (graphData.edges) {
|
||||
graphData.edges.forEach(edge => {
|
||||
console.log(` Edge: ${edge.from} -> ${edge.to} (${edge.label})`);
|
||||
});
|
||||
}
|
||||
|
||||
// Only update if data has changed
|
||||
if (this.hasGraphChanged(graphData)) {
|
||||
console.log('*** GRAPH DATA CHANGED - UPDATING VISUALIZATION ***');
|
||||
this.graphManager.updateGraph(graphData);
|
||||
this.lastGraphUpdate = Date.now();
|
||||
|
||||
// Update relationship count in status
|
||||
const edgeCount = graphData.edges ? graphData.edges.length : 0;
|
||||
if (this.elements.relationshipsDisplay) {
|
||||
this.elements.relationshipsDisplay.textContent = edgeCount;
|
||||
}
|
||||
} else {
|
||||
console.log('Graph data unchanged, skipping update');
|
||||
}
|
||||
} else {
|
||||
console.error('Graph update failed:', response);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to update graph:', error);
|
||||
// Don't show error for graph updates to avoid spam
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update status display elements
|
||||
* @param {Object} status - Status object from server
|
||||
*/
|
||||
updateStatusDisplay(status) {
|
||||
try {
|
||||
console.log('Updating status display...');
|
||||
|
||||
// Update status text
|
||||
if (this.elements.scanStatus) {
|
||||
this.elements.scanStatus.textContent = this.formatStatus(status.status);
|
||||
console.log('Updated status display:', status.status);
|
||||
}
|
||||
if (this.elements.targetDisplay) {
|
||||
this.elements.targetDisplay.textContent = status.target_domain || 'None';
|
||||
}
|
||||
if (this.elements.depthDisplay) {
|
||||
this.elements.depthDisplay.textContent = `${status.current_depth}/${status.max_depth}`;
|
||||
}
|
||||
if (this.elements.progressDisplay) {
|
||||
this.elements.progressDisplay.textContent = `${status.progress_percentage.toFixed(1)}%`;
|
||||
}
|
||||
if (this.elements.indicatorsDisplay) {
|
||||
this.elements.indicatorsDisplay.textContent = status.indicators_processed || 0;
|
||||
}
|
||||
|
||||
// Update progress bar
|
||||
if (this.elements.progressFill) {
|
||||
this.elements.progressFill.style.width = `${status.progress_percentage}%`;
|
||||
}
|
||||
|
||||
// Update session ID
|
||||
if (this.currentSessionId && this.elements.sessionId) {
|
||||
this.elements.sessionId.textContent = `Session: ${this.currentSessionId}`;
|
||||
}
|
||||
|
||||
console.log('Status display updated successfully');
|
||||
} catch (error) {
|
||||
console.error('Error updating status display:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle status changes
|
||||
* @param {string} newStatus - New scan status
|
||||
*/
|
||||
handleStatusChange(newStatus) {
|
||||
console.log(`=== STATUS CHANGE: ${this.scanStatus} -> ${newStatus} ===`);
|
||||
|
||||
switch (newStatus) {
|
||||
case 'running':
|
||||
this.setUIState('scanning');
|
||||
this.showSuccess('Scan is running');
|
||||
break;
|
||||
|
||||
case 'completed':
|
||||
this.setUIState('completed');
|
||||
this.stopPolling();
|
||||
this.showSuccess('Scan completed successfully');
|
||||
// Force a final graph update
|
||||
console.log('Scan completed - forcing final graph update');
|
||||
setTimeout(() => this.updateGraph(), 100);
|
||||
break;
|
||||
|
||||
case 'failed':
|
||||
this.setUIState('failed');
|
||||
this.stopPolling();
|
||||
this.showError('Scan failed');
|
||||
break;
|
||||
|
||||
case 'stopped':
|
||||
this.setUIState('stopped');
|
||||
this.stopPolling();
|
||||
this.showSuccess('Scan stopped');
|
||||
break;
|
||||
|
||||
case 'idle':
|
||||
this.setUIState('idle');
|
||||
this.stopPolling();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set UI state based on scan status
|
||||
* @param {string} state - UI state
|
||||
*/
|
||||
setUIState(state) {
|
||||
console.log(`Setting UI state to: ${state}`);
|
||||
|
||||
switch (state) {
|
||||
case 'scanning':
|
||||
this.isScanning = true;
|
||||
if (this.elements.startScan) this.elements.startScan.disabled = true;
|
||||
if (this.elements.stopScan) this.elements.stopScan.disabled = false;
|
||||
if (this.elements.targetDomain) this.elements.targetDomain.disabled = true;
|
||||
if (this.elements.maxDepth) this.elements.maxDepth.disabled = true;
|
||||
break;
|
||||
|
||||
case 'idle':
|
||||
case 'completed':
|
||||
case 'failed':
|
||||
case 'stopped':
|
||||
this.isScanning = false;
|
||||
if (this.elements.startScan) this.elements.startScan.disabled = false;
|
||||
if (this.elements.stopScan) this.elements.stopScan.disabled = true;
|
||||
if (this.elements.targetDomain) this.elements.targetDomain.disabled = false;
|
||||
if (this.elements.maxDepth) this.elements.maxDepth.disabled = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load provider information
|
||||
*/
|
||||
async loadProviders() {
|
||||
try {
|
||||
console.log('Loading providers...');
|
||||
const response = await this.apiCall('/api/providers');
|
||||
|
||||
if (response.success) {
|
||||
this.updateProviderDisplay(response.providers);
|
||||
console.log('Providers loaded successfully');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load providers:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update provider display
|
||||
* @param {Object} providers - Provider information
|
||||
*/
|
||||
updateProviderDisplay(providers) {
|
||||
if (!this.elements.providerList) return;
|
||||
|
||||
this.elements.providerList.innerHTML = '';
|
||||
|
||||
for (const [name, info] of Object.entries(providers)) {
|
||||
const providerItem = document.createElement('div');
|
||||
providerItem.className = 'provider-item';
|
||||
|
||||
const status = info.enabled ? 'enabled' : 'disabled';
|
||||
const statusClass = info.enabled ? 'enabled' : 'disabled';
|
||||
|
||||
providerItem.innerHTML = `
|
||||
<div>
|
||||
<div class="provider-name">${name.toUpperCase()}</div>
|
||||
<div class="provider-stats">
|
||||
Requests: ${info.statistics.total_requests || 0} |
|
||||
Success Rate: ${(info.statistics.success_rate || 0).toFixed(1)}%
|
||||
</div>
|
||||
</div>
|
||||
<div class="provider-status ${statusClass}">${status}</div>
|
||||
`;
|
||||
|
||||
this.elements.providerList.appendChild(providerItem);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show node details modal
|
||||
* @param {string} nodeId - Node identifier
|
||||
* @param {Object} node - Node data
|
||||
*/
|
||||
showNodeModal(nodeId, node) {
|
||||
if (!this.elements.nodeModal) return;
|
||||
|
||||
if (this.elements.modalTitle) {
|
||||
this.elements.modalTitle.textContent = `Node Details: ${nodeId}`;
|
||||
}
|
||||
|
||||
let detailsHtml = '';
|
||||
detailsHtml += `<div class="detail-row"><span class="detail-label">Identifier:</span><span class="detail-value">${nodeId}</span></div>`;
|
||||
detailsHtml += `<div class="detail-row"><span class="detail-label">Type:</span><span class="detail-value">${node.metadata.type || 'Unknown'}</span></div>`;
|
||||
|
||||
if (node.metadata) {
|
||||
for (const [key, value] of Object.entries(node.metadata)) {
|
||||
if (key !== 'type') {
|
||||
detailsHtml += `<div class="detail-row"><span class="detail-label">${this.formatLabel(key)}:</span><span class="detail-value">${this.formatValue(value)}</span></div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.elements.modalDetails) {
|
||||
this.elements.modalDetails.innerHTML = detailsHtml;
|
||||
}
|
||||
this.elements.nodeModal.style.display = 'block';
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the modal
|
||||
*/
|
||||
hideModal() {
|
||||
if (this.elements.nodeModal) {
|
||||
this.elements.nodeModal.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if graph data has changed
|
||||
* @param {Object} graphData - New graph data
|
||||
* @returns {boolean} True if data has changed
|
||||
*/
|
||||
hasGraphChanged(graphData) {
|
||||
// Simple check based on node and edge counts
|
||||
const currentStats = this.graphManager.getStatistics();
|
||||
const newNodeCount = graphData.nodes ? graphData.nodes.length : 0;
|
||||
const newEdgeCount = graphData.edges ? graphData.edges.length : 0;
|
||||
|
||||
const changed = currentStats.nodeCount !== newNodeCount || currentStats.edgeCount !== newEdgeCount;
|
||||
|
||||
console.log(`Graph change check: Current(${currentStats.nodeCount}n, ${currentStats.edgeCount}e) vs New(${newNodeCount}n, ${newEdgeCount}e) = ${changed}`);
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make API call to server
|
||||
* @param {string} endpoint - API endpoint
|
||||
* @param {string} method - HTTP method
|
||||
* @param {Object} data - Request data
|
||||
* @returns {Promise<Object>} Response data
|
||||
*/
|
||||
async apiCall(endpoint, method = 'GET', data = null) {
|
||||
console.log(`Making API call: ${method} ${endpoint}`, data ? data : '(no data)');
|
||||
|
||||
try {
|
||||
const options = {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
|
||||
if (data && method !== 'GET') {
|
||||
options.body = JSON.stringify(data);
|
||||
console.log('Request body:', options.body);
|
||||
}
|
||||
|
||||
console.log('Fetch options:', options);
|
||||
const response = await fetch(endpoint, options);
|
||||
|
||||
console.log('Response status:', response.status, response.statusText);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('Response data:', result);
|
||||
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`API call failed for ${method} ${endpoint}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate domain name - improved validation
|
||||
* @param {string} domain - Domain to validate
|
||||
* @returns {boolean} True if valid
|
||||
*/
|
||||
isValidDomain(domain) {
|
||||
console.log(`Validating domain: "${domain}"`);
|
||||
|
||||
// Basic checks
|
||||
if (!domain || typeof domain !== 'string') {
|
||||
console.log('Validation failed: empty or non-string domain');
|
||||
return false;
|
||||
}
|
||||
if (domain.length > 253) {
|
||||
console.log('Validation failed: domain too long');
|
||||
return false;
|
||||
}
|
||||
if (domain.startsWith('.') || domain.endsWith('.')) {
|
||||
console.log('Validation failed: domain starts or ends with dot');
|
||||
return false;
|
||||
}
|
||||
if (domain.includes('..')) {
|
||||
console.log('Validation failed: domain contains double dots');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Split into parts and validate each
|
||||
const parts = domain.split('.');
|
||||
if (parts.length < 2) {
|
||||
console.log('Validation failed: domain has less than 2 parts');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check each part
|
||||
for (const part of parts) {
|
||||
if (!part || part.length > 63) {
|
||||
console.log(`Validation failed: invalid part "${part}"`);
|
||||
return false;
|
||||
}
|
||||
if (part.startsWith('-') || part.endsWith('-')) {
|
||||
console.log(`Validation failed: part "${part}" starts or ends with hyphen`);
|
||||
return false;
|
||||
}
|
||||
if (!/^[a-zA-Z0-9-]+$/.test(part)) {
|
||||
console.log(`Validation failed: part "${part}" contains invalid characters`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check TLD (last part) is alphabetic
|
||||
const tld = parts[parts.length - 1];
|
||||
if (!/^[a-zA-Z]{2,}$/.test(tld)) {
|
||||
console.log(`Validation failed: invalid TLD "${tld}"`);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log('Domain validation passed');
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format status text for display
|
||||
* @param {string} status - Raw status
|
||||
* @returns {string} Formatted status
|
||||
*/
|
||||
formatStatus(status) {
|
||||
const statusMap = {
|
||||
'idle': 'Idle',
|
||||
'running': 'Running',
|
||||
'completed': 'Completed',
|
||||
'failed': 'Failed',
|
||||
'stopped': 'Stopped'
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format label for display
|
||||
* @param {string} label - Raw label
|
||||
* @returns {string} Formatted label
|
||||
*/
|
||||
formatLabel(label) {
|
||||
return label.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Format value for display
|
||||
* @param {*} value - Raw value
|
||||
* @returns {string} Formatted value
|
||||
*/
|
||||
formatValue(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.join(', ');
|
||||
} else if (typeof value === 'object') {
|
||||
return JSON.stringify(value, null, 2);
|
||||
} else {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show success message
|
||||
* @param {string} message - Success message
|
||||
*/
|
||||
showSuccess(message) {
|
||||
this.showMessage(message, 'success');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show info message
|
||||
* @param {string} message - Info message
|
||||
*/
|
||||
showInfo(message) {
|
||||
this.showMessage(message, 'info');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show error message
|
||||
* @param {string} message - Error message
|
||||
*/
|
||||
showError(message) {
|
||||
this.showMessage(message, 'error');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show connection error
|
||||
*/
|
||||
showConnectionError() {
|
||||
if (this.elements.connectionStatus) {
|
||||
this.elements.connectionStatus.style.backgroundColor = '#ff6b6b';
|
||||
}
|
||||
const statusText = this.elements.connectionStatus?.parentElement?.querySelector('.status-text');
|
||||
if (statusText) {
|
||||
statusText.textContent = 'Connection Error';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show message with visual feedback
|
||||
* @param {string} message - Message text
|
||||
* @param {string} type - Message type (success, error, warning, info)
|
||||
*/
|
||||
showMessage(message, type = 'info') {
|
||||
console.log(`${type.toUpperCase()}: ${message}`);
|
||||
|
||||
// Create message element
|
||||
const messageElement = document.createElement('div');
|
||||
messageElement.className = `message-toast message-${type}`;
|
||||
messageElement.style.cssText = `
|
||||
background: ${this.getMessageColor(type)};
|
||||
color: #fff;
|
||||
padding: 12px 20px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
font-size: 0.9rem;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.3);
|
||||
border-left: 4px solid ${this.getMessageBorderColor(type)};
|
||||
animation: slideInRight 0.3s ease-out;
|
||||
`;
|
||||
|
||||
messageElement.innerHTML = `
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span>${message}</span>
|
||||
<button onclick="this.parentElement.parentElement.remove()"
|
||||
style="background: none; border: none; color: #fff; cursor: pointer; font-size: 16px; margin-left: 10px;">×</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add to container
|
||||
const container = document.getElementById('message-container');
|
||||
if (container) {
|
||||
container.appendChild(messageElement);
|
||||
|
||||
// Auto-remove after delay
|
||||
setTimeout(() => {
|
||||
if (messageElement.parentNode) {
|
||||
messageElement.style.animation = 'slideOutRight 0.3s ease-out';
|
||||
setTimeout(() => {
|
||||
if (messageElement.parentNode) {
|
||||
messageElement.remove();
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}, type === 'error' ? 8000 : 5000); // Errors stay longer
|
||||
}
|
||||
|
||||
// Update connection status to show activity
|
||||
if (type === 'success' && this.elements.connectionStatus) {
|
||||
this.elements.connectionStatus.style.backgroundColor = '#00ff41';
|
||||
setTimeout(() => {
|
||||
if (this.elements.connectionStatus) {
|
||||
this.elements.connectionStatus.style.backgroundColor = '#00ff41';
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get message background color based on type
|
||||
* @param {string} type - Message type
|
||||
* @returns {string} CSS color
|
||||
*/
|
||||
getMessageColor(type) {
|
||||
const colors = {
|
||||
'success': '#2c5c34',
|
||||
'error': '#5c2c2c',
|
||||
'warning': '#5c4c2c',
|
||||
'info': '#2c3e5c'
|
||||
};
|
||||
return colors[type] || colors.info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get message border color based on type
|
||||
* @param {string} type - Message type
|
||||
* @returns {string} CSS color
|
||||
*/
|
||||
getMessageBorderColor(type) {
|
||||
const colors = {
|
||||
'success': '#00ff41',
|
||||
'error': '#ff6b6b',
|
||||
'warning': '#ff9900',
|
||||
'info': '#00aaff'
|
||||
};
|
||||
return colors[type] || colors.info;
|
||||
}
|
||||
}
|
||||
|
||||
// Add CSS animations for message toasts
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideOutRight {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.message-container {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.message-toast {
|
||||
pointer-events: auto;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// Initialize application when page loads
|
||||
console.log('Creating DNSReconApp instance...');
|
||||
const app = new DNSReconApp();
|
||||
Reference in New Issue
Block a user