409 lines
14 KiB
JavaScript
409 lines
14 KiB
JavaScript
|
|
/**
|
||
|
|
* Protocol Discovery JavaScript
|
||
|
|
* Handles auto-discovery of protocol endpoints and integration with protocol mapping
|
||
|
|
*/
|
||
|
|
|
||
|
|
class ProtocolDiscovery {
|
||
|
|
constructor() {
|
||
|
|
this.currentScanId = null;
|
||
|
|
this.scanInterval = null;
|
||
|
|
this.isScanning = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Initialize discovery functionality
|
||
|
|
*/
|
||
|
|
init() {
|
||
|
|
this.bindDiscoveryEvents();
|
||
|
|
this.loadDiscoveryStatus();
|
||
|
|
|
||
|
|
// Auto-refresh discovery status every 5 seconds
|
||
|
|
setInterval(() => {
|
||
|
|
if (this.isScanning) {
|
||
|
|
this.loadDiscoveryStatus();
|
||
|
|
}
|
||
|
|
}, 5000);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Bind discovery event handlers
|
||
|
|
*/
|
||
|
|
bindDiscoveryEvents() {
|
||
|
|
// Start discovery scan
|
||
|
|
document.getElementById('start-discovery-scan')?.addEventListener('click', () => {
|
||
|
|
this.startDiscoveryScan();
|
||
|
|
});
|
||
|
|
|
||
|
|
// Stop discovery scan
|
||
|
|
document.getElementById('stop-discovery-scan')?.addEventListener('click', () => {
|
||
|
|
this.stopDiscoveryScan();
|
||
|
|
});
|
||
|
|
|
||
|
|
// Apply discovery results
|
||
|
|
document.getElementById('apply-discovery-results')?.addEventListener('click', () => {
|
||
|
|
this.applyDiscoveryResults();
|
||
|
|
});
|
||
|
|
|
||
|
|
// Refresh discovery status
|
||
|
|
document.getElementById('refresh-discovery-status')?.addEventListener('click', () => {
|
||
|
|
this.loadDiscoveryStatus();
|
||
|
|
});
|
||
|
|
|
||
|
|
// Auto-fill protocol form from discovery
|
||
|
|
document.addEventListener('click', (e) => {
|
||
|
|
if (e.target.classList.contains('use-discovered-endpoint')) {
|
||
|
|
this.useDiscoveredEndpoint(e.target.dataset.endpointId);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Start a new discovery scan
|
||
|
|
*/
|
||
|
|
async startDiscoveryScan() {
|
||
|
|
try {
|
||
|
|
this.setScanningState(true);
|
||
|
|
|
||
|
|
const response = await fetch('/api/v1/dashboard/discovery/scan', {
|
||
|
|
method: 'POST',
|
||
|
|
headers: {
|
||
|
|
'Content-Type': 'application/json'
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = await response.json();
|
||
|
|
|
||
|
|
if (result.success) {
|
||
|
|
this.currentScanId = result.scan_id;
|
||
|
|
this.showNotification('Discovery scan started successfully', 'success');
|
||
|
|
|
||
|
|
// Start polling for scan completion
|
||
|
|
this.pollScanStatus();
|
||
|
|
} else {
|
||
|
|
throw new Error(result.detail || 'Failed to start discovery scan');
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error starting discovery scan:', error);
|
||
|
|
this.showNotification(`Failed to start discovery scan: ${error.message}`, 'error');
|
||
|
|
this.setScanningState(false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Stop current discovery scan
|
||
|
|
*/
|
||
|
|
async stopDiscoveryScan() {
|
||
|
|
// Note: This would require additional API endpoint to stop scans
|
||
|
|
// For now, we'll just stop polling
|
||
|
|
if (this.scanInterval) {
|
||
|
|
clearInterval(this.scanInterval);
|
||
|
|
this.scanInterval = null;
|
||
|
|
}
|
||
|
|
this.setScanningState(false);
|
||
|
|
this.showNotification('Discovery scan stopped', 'info');
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Poll for scan completion
|
||
|
|
*/
|
||
|
|
async pollScanStatus() {
|
||
|
|
if (!this.currentScanId) return;
|
||
|
|
|
||
|
|
this.scanInterval = setInterval(async () => {
|
||
|
|
try {
|
||
|
|
const response = await fetch(`/api/v1/dashboard/discovery/results/${this.currentScanId}`);
|
||
|
|
const result = await response.json();
|
||
|
|
|
||
|
|
if (result.success) {
|
||
|
|
if (result.status === 'completed' || result.status === 'failed') {
|
||
|
|
clearInterval(this.scanInterval);
|
||
|
|
this.scanInterval = null;
|
||
|
|
this.setScanningState(false);
|
||
|
|
|
||
|
|
if (result.status === 'completed') {
|
||
|
|
this.showNotification(`Discovery scan completed. Found ${result.discovered_endpoints.length} endpoints`, 'success');
|
||
|
|
this.displayDiscoveryResults(result);
|
||
|
|
} else {
|
||
|
|
this.showNotification('Discovery scan failed', 'error');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error polling scan status:', error);
|
||
|
|
clearInterval(this.scanInterval);
|
||
|
|
this.scanInterval = null;
|
||
|
|
this.setScanningState(false);
|
||
|
|
}
|
||
|
|
}, 2000);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Load current discovery status
|
||
|
|
*/
|
||
|
|
async loadDiscoveryStatus() {
|
||
|
|
try {
|
||
|
|
const response = await fetch('/api/v1/dashboard/discovery/status');
|
||
|
|
const result = await response.json();
|
||
|
|
|
||
|
|
if (result.success) {
|
||
|
|
this.updateDiscoveryStatusUI(result.status);
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error loading discovery status:', error);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Update discovery status UI
|
||
|
|
*/
|
||
|
|
updateDiscoveryStatusUI(status) {
|
||
|
|
const statusElement = document.getElementById('discovery-status');
|
||
|
|
const scanButton = document.getElementById('start-discovery-scan');
|
||
|
|
const stopButton = document.getElementById('stop-discovery-scan');
|
||
|
|
|
||
|
|
if (!statusElement) return;
|
||
|
|
|
||
|
|
this.isScanning = status.is_scanning;
|
||
|
|
|
||
|
|
if (status.is_scanning) {
|
||
|
|
statusElement.innerHTML = `
|
||
|
|
<div class="alert alert-info">
|
||
|
|
<i class="fas fa-sync fa-spin"></i>
|
||
|
|
Discovery scan in progress... (Scan ID: ${status.current_scan_id})
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
scanButton?.setAttribute('disabled', 'true');
|
||
|
|
stopButton?.removeAttribute('disabled');
|
||
|
|
} else {
|
||
|
|
statusElement.innerHTML = `
|
||
|
|
<div class="alert alert-success">
|
||
|
|
<i class="fas fa-check"></i>
|
||
|
|
Discovery service ready
|
||
|
|
${status.total_discovered_endpoints > 0 ?
|
||
|
|
`- ${status.total_discovered_endpoints} endpoints discovered` :
|
||
|
|
''
|
||
|
|
}
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
scanButton?.removeAttribute('disabled');
|
||
|
|
stopButton?.setAttribute('disabled', 'true');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Display discovery results
|
||
|
|
*/
|
||
|
|
displayDiscoveryResults(result) {
|
||
|
|
const resultsContainer = document.getElementById('discovery-results');
|
||
|
|
if (!resultsContainer) return;
|
||
|
|
|
||
|
|
const endpoints = result.discovered_endpoints || [];
|
||
|
|
|
||
|
|
if (endpoints.length === 0) {
|
||
|
|
resultsContainer.innerHTML = `
|
||
|
|
<div class="alert alert-warning">
|
||
|
|
<i class="fas fa-exclamation-triangle"></i>
|
||
|
|
No endpoints discovered in this scan
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
let html = `
|
||
|
|
<div class="card">
|
||
|
|
<div class="card-header">
|
||
|
|
<h5 class="mb-0">
|
||
|
|
<i class="fas fa-search"></i>
|
||
|
|
Discovery Results (${endpoints.length} endpoints found)
|
||
|
|
</h5>
|
||
|
|
</div>
|
||
|
|
<div class="card-body">
|
||
|
|
<div class="table-responsive">
|
||
|
|
<table class="table table-striped table-hover">
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th>Protocol</th>
|
||
|
|
<th>Device Name</th>
|
||
|
|
<th>Address</th>
|
||
|
|
<th>Capabilities</th>
|
||
|
|
<th>Discovered</th>
|
||
|
|
<th>Actions</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
`;
|
||
|
|
|
||
|
|
endpoints.forEach(endpoint => {
|
||
|
|
const protocolBadge = this.getProtocolBadge(endpoint.protocol_type);
|
||
|
|
const capabilities = endpoint.capabilities ? endpoint.capabilities.join(', ') : 'N/A';
|
||
|
|
const discoveredTime = endpoint.discovered_at ?
|
||
|
|
new Date(endpoint.discovered_at).toLocaleString() : 'N/A';
|
||
|
|
|
||
|
|
html += `
|
||
|
|
<tr>
|
||
|
|
<td>${protocolBadge}</td>
|
||
|
|
<td>${endpoint.device_name || 'Unknown Device'}</td>
|
||
|
|
<td><code>${endpoint.address}${endpoint.port ? ':' + endpoint.port : ''}</code></td>
|
||
|
|
<td><small>${capabilities}</small></td>
|
||
|
|
<td><small>${discoveredTime}</small></td>
|
||
|
|
<td>
|
||
|
|
<button class="btn btn-sm btn-outline-primary use-discovered-endpoint"
|
||
|
|
data-endpoint-id="${endpoint.device_id}"
|
||
|
|
title="Use this endpoint in protocol mapping">
|
||
|
|
<i class="fas fa-plus"></i> Use
|
||
|
|
</button>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
`;
|
||
|
|
});
|
||
|
|
|
||
|
|
html += `
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
<div class="mt-3">
|
||
|
|
<button id="apply-discovery-results" class="btn btn-success">
|
||
|
|
<i class="fas fa-check"></i>
|
||
|
|
Apply All as Protocol Mappings
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
|
||
|
|
resultsContainer.innerHTML = html;
|
||
|
|
|
||
|
|
// Re-bind apply button
|
||
|
|
document.getElementById('apply-discovery-results')?.addEventListener('click', () => {
|
||
|
|
this.applyDiscoveryResults();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Apply discovery results as protocol mappings
|
||
|
|
*/
|
||
|
|
async applyDiscoveryResults() {
|
||
|
|
if (!this.currentScanId) {
|
||
|
|
this.showNotification('No discovery results to apply', 'warning');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get station and pump info from form or prompt
|
||
|
|
const stationId = document.getElementById('station-id')?.value || 'station_001';
|
||
|
|
const pumpId = document.getElementById('pump-id')?.value || 'pump_001';
|
||
|
|
const dataType = document.getElementById('data-type')?.value || 'setpoint';
|
||
|
|
const dbSource = document.getElementById('db-source')?.value || 'frequency_hz';
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await fetch(`/api/v1/dashboard/discovery/apply/${this.currentScanId}`, {
|
||
|
|
method: 'POST',
|
||
|
|
headers: {
|
||
|
|
'Content-Type': 'application/json'
|
||
|
|
},
|
||
|
|
body: JSON.stringify({
|
||
|
|
station_id: stationId,
|
||
|
|
pump_id: pumpId,
|
||
|
|
data_type: dataType,
|
||
|
|
db_source: dbSource
|
||
|
|
})
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = await response.json();
|
||
|
|
|
||
|
|
if (result.success) {
|
||
|
|
this.showNotification(`Successfully created ${result.created_mappings.length} protocol mappings`, 'success');
|
||
|
|
|
||
|
|
// Refresh protocol mappings grid
|
||
|
|
if (window.protocolMappingGrid) {
|
||
|
|
window.protocolMappingGrid.loadProtocolMappings();
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
throw new Error(result.detail || 'Failed to apply discovery results');
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error applying discovery results:', error);
|
||
|
|
this.showNotification(`Failed to apply discovery results: ${error.message}`, 'error');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Use discovered endpoint in protocol form
|
||
|
|
*/
|
||
|
|
useDiscoveredEndpoint(endpointId) {
|
||
|
|
// This would fetch the specific endpoint details and populate the form
|
||
|
|
// For now, we'll just show a notification
|
||
|
|
this.showNotification(`Endpoint ${endpointId} selected for protocol mapping`, 'info');
|
||
|
|
|
||
|
|
// In a real implementation, we would:
|
||
|
|
// 1. Fetch endpoint details
|
||
|
|
// 2. Populate protocol form fields
|
||
|
|
// 3. Switch to protocol mapping tab
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Set scanning state
|
||
|
|
*/
|
||
|
|
setScanningState(scanning) {
|
||
|
|
this.isScanning = scanning;
|
||
|
|
const scanButton = document.getElementById('start-discovery-scan');
|
||
|
|
const stopButton = document.getElementById('stop-discovery-scan');
|
||
|
|
|
||
|
|
if (scanning) {
|
||
|
|
scanButton?.setAttribute('disabled', 'true');
|
||
|
|
stopButton?.removeAttribute('disabled');
|
||
|
|
} else {
|
||
|
|
scanButton?.removeAttribute('disabled');
|
||
|
|
stopButton?.setAttribute('disabled', 'true');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get protocol badge HTML
|
||
|
|
*/
|
||
|
|
getProtocolBadge(protocolType) {
|
||
|
|
const badges = {
|
||
|
|
'modbus_tcp': '<span class="badge bg-primary">Modbus TCP</span>',
|
||
|
|
'modbus_rtu': '<span class="badge bg-info">Modbus RTU</span>',
|
||
|
|
'opc_ua': '<span class="badge bg-success">OPC UA</span>',
|
||
|
|
'rest_api': '<span class="badge bg-warning">REST API</span>'
|
||
|
|
};
|
||
|
|
|
||
|
|
return badges[protocolType] || `<span class="badge bg-secondary">${protocolType}</span>`;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Show notification
|
||
|
|
*/
|
||
|
|
showNotification(message, type = 'info') {
|
||
|
|
// Use existing notification system or create simple alert
|
||
|
|
const alertClass = {
|
||
|
|
'success': 'alert-success',
|
||
|
|
'error': 'alert-danger',
|
||
|
|
'warning': 'alert-warning',
|
||
|
|
'info': 'alert-info'
|
||
|
|
}[type] || 'alert-info';
|
||
|
|
|
||
|
|
const notification = document.createElement('div');
|
||
|
|
notification.className = `alert ${alertClass} alert-dismissible fade show`;
|
||
|
|
notification.innerHTML = `
|
||
|
|
${message}
|
||
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||
|
|
`;
|
||
|
|
|
||
|
|
const container = document.getElementById('discovery-notifications') || document.body;
|
||
|
|
container.appendChild(notification);
|
||
|
|
|
||
|
|
// Auto-remove after 5 seconds
|
||
|
|
setTimeout(() => {
|
||
|
|
if (notification.parentNode) {
|
||
|
|
notification.remove();
|
||
|
|
}
|
||
|
|
}, 5000);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Initialize discovery when DOM is loaded
|
||
|
|
document.addEventListener('DOMContentLoaded', () => {
|
||
|
|
window.protocolDiscovery = new ProtocolDiscovery();
|
||
|
|
window.protocolDiscovery.init();
|
||
|
|
});
|