// Reusable Firmware Upload Component class FirmwareUploadComponent extends Component { constructor(container, viewModel, eventBus) { super(container, viewModel, eventBus); logger.debug('FirmwareUploadComponent: Constructor called'); logger.debug('FirmwareUploadComponent: Container:', container); logger.debug('FirmwareUploadComponent: Container ID:', container?.id); } setupEventListeners() { // Setup firmware file input const firmwareFile = this.findElement('#firmware-file'); if (firmwareFile) { this.addEventListener(firmwareFile, 'change', this.handleFileSelect.bind(this)); } // Setup deploy button const deployBtn = this.findElement('#deploy-btn'); if (deployBtn) { this.addEventListener(deployBtn, 'click', this.handleDeploy.bind(this)); } } setupViewModelListeners() { this.subscribeToProperty('selectedFile', () => { this.updateFileInfo(); this.updateDeployButton(); }); this.subscribeToProperty('isUploading', this.updateUploadState.bind(this)); this.subscribeToProperty('uploadProgress', this.updateUploadProgress.bind(this)); this.subscribeToProperty('uploadResults', this.updateUploadResults.bind(this)); } mount() { super.mount(); logger.debug('FirmwareUploadComponent: Mounting...'); // Initialize UI state this.updateFileInfo(); this.updateDeployButton(); logger.debug('FirmwareUploadComponent: Mounted successfully'); } render() { // Initial render is handled by the HTML template this.updateDeployButton(); } handleFileSelect(event) { const file = event.target.files[0]; this.viewModel.setSelectedFile(file); } async handleDeploy() { const file = this.viewModel.get('selectedFile'); const targetNodes = this.viewModel.get('targetNodes'); if (!file) { alert('Please select a firmware file first.'); return; } if (!targetNodes || targetNodes.length === 0) { alert('No target nodes available for firmware update.'); return; } try { this.viewModel.startUpload(); // Show progress overlay to block UI interactions this.showProgressOverlay(); const confirmed = confirm(`Upload firmware to ${targetNodes.length} node(s)?\n\nTarget nodes:\n${targetNodes.map(n => `• ${n.hostname || n.ip} (${n.ip})`).join('\n')}\n\nThis will update the firmware on all selected nodes.`); if (!confirmed) { this.viewModel.completeUpload(); this.hideProgressOverlay(); return; } // Show upload progress area this.showUploadProgress(file, targetNodes); // Start batch upload const results = await this.performBatchUpload(file, targetNodes); // Display results this.displayUploadResults(results); // Reset interface after successful upload this.viewModel.resetUploadState(); } catch (error) { logger.error('Firmware deployment failed:', error); alert(`Deployment failed: ${error.message}`); } finally { this.viewModel.completeUpload(); this.hideProgressOverlay(); } } async performBatchUpload(file, nodes) { const results = []; const totalNodes = nodes.length; let successfulUploads = 0; for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; const nodeIp = node.ip; try { // Update progress this.updateNodeProgress(i + 1, totalNodes, nodeIp, 'Uploading...'); // Upload to this node const result = await this.performSingleUpload(file, nodeIp); results.push(result); successfulUploads++; // Update progress this.updateNodeProgress(i + 1, totalNodes, nodeIp, 'Completed'); this.updateOverallProgress(successfulUploads, totalNodes); } catch (error) { logger.error(`Failed to upload to node ${nodeIp}:`, error); const errorResult = { nodeIp: nodeIp, hostname: node.hostname || nodeIp, success: false, error: error.message, timestamp: new Date().toISOString() }; results.push(errorResult); // Update progress this.updateNodeProgress(i + 1, totalNodes, nodeIp, 'Failed'); this.updateOverallProgress(successfulUploads, totalNodes); } // Small delay between uploads if (i < nodes.length - 1) { await new Promise(resolve => setTimeout(resolve, 1000)); } } return results; } async performSingleUpload(file, nodeIp) { try { const result = await window.apiClient.uploadFirmware(file, nodeIp); return { nodeIp: nodeIp, hostname: nodeIp, success: true, result: result, timestamp: new Date().toISOString() }; } catch (error) { throw new Error(`Upload to ${nodeIp} failed: ${error.message}`); } } showUploadProgress(file, nodes) { // Update the target nodes section header to show upload progress const targetNodesSection = this.findElement('.target-nodes-section'); if (targetNodesSection) { const h3 = targetNodesSection.querySelector('h3'); if (h3) { h3.innerHTML = ` Firmware Upload Progress (${nodes.length} nodes) `; } } // Add progress info to the firmware-progress-container const container = this.findElement('#firmware-progress-container'); if (container) { const progressHTML = `
0/${nodes.length} Successful (0%)
Status: Preparing upload...
`; container.innerHTML = progressHTML; } // Update existing target nodes to show upload status this.updateTargetNodesForUpload(nodes); } updateTargetNodesForUpload(nodes) { const targetNodesList = this.findElement('.target-nodes-list'); if (!targetNodesList) return; // Update each target node item to show upload status targetNodesList.innerHTML = nodes.map(node => `
${node.hostname || node.ip} ${node.ip}
Pending...
`).join(''); } updateNodeProgress(current, total, nodeIp, status) { const targetNodeItem = this.findElement(`[data-node-ip="${nodeIp}"]`); if (targetNodeItem) { const statusElement = targetNodeItem.querySelector('.status-indicator'); if (statusElement) { statusElement.textContent = status; // Update status-specific styling statusElement.className = 'status-indicator'; if (status === 'Completed') { statusElement.classList.add('success'); } else if (status === 'Failed') { statusElement.classList.add('error'); } else if (status === 'Uploading...') { statusElement.classList.add('uploading'); } else if (status === 'Pending...') { statusElement.classList.add('pending'); } } } } updateOverallProgress(successfulUploads, totalNodes) { const progressBar = this.findElement('#overall-progress-bar'); const progressText = this.findElement('.progress-text'); if (progressBar && progressText) { const successPercentage = Math.round((successfulUploads / totalNodes) * 100); progressBar.style.width = `${successPercentage}%`; progressText.textContent = `${successfulUploads}/${totalNodes} Successful (${successPercentage}%)`; // Update progress bar color based on completion if (successPercentage === 100) { progressBar.style.backgroundColor = '#4ade80'; } else if (successPercentage > 50) { progressBar.style.backgroundColor = '#60a5fa'; } else { progressBar.style.backgroundColor = '#fbbf24'; } // Update progress summary for single-node uploads const progressSummary = this.findElement('#progress-summary'); if (progressSummary && totalNodes === 1) { if (successfulUploads === 1) { progressSummary.innerHTML = 'Status: Upload completed successfully'; } else if (successfulUploads === 0) { progressSummary.innerHTML = 'Status: Upload failed'; } } } } displayUploadResults(results) { const progressHeader = this.findElement('.progress-header h3'); const progressSummary = this.findElement('#progress-summary'); if (progressHeader && progressSummary) { const successCount = results.filter(r => r.success).length; const totalCount = results.length; const successRate = Math.round((successCount / totalCount) * 100); if (totalCount === 1) { // Single node upload if (successCount === 1) { progressHeader.textContent = `Firmware Upload Complete`; progressSummary.innerHTML = `${window.icon('success', { width: 14, height: 14 })} Upload to ${results[0].hostname || results[0].nodeIp} completed successfully at ${new Date().toLocaleTimeString()}`; } else { progressHeader.textContent = `Firmware Upload Failed`; progressSummary.innerHTML = `${window.icon('error', { width: 14, height: 14 })} Upload to ${results[0].hostname || results[0].nodeIp} failed at ${new Date().toLocaleTimeString()}`; } } else if (successCount === totalCount) { // Multi-node upload - all successful progressHeader.textContent = `Firmware Upload Complete (${successCount}/${totalCount} Successful)`; progressSummary.innerHTML = `${window.icon('success', { width: 14, height: 14 })} All uploads completed successfully at ${new Date().toLocaleTimeString()}`; } else { // Multi-node upload - some failed progressHeader.textContent = `Firmware Upload Results (${successCount}/${totalCount} Successful)`; progressSummary.innerHTML = `${window.icon('warning', { width: 14, height: 14 })} Upload completed with ${totalCount - successCount} failure(s) at ${new Date().toLocaleTimeString()}`; } } } updateFileInfo() { const file = this.viewModel.get('selectedFile'); const fileInfo = this.findElement('#file-info'); if (file) { fileInfo.textContent = `${file.name} (${(file.size / 1024).toFixed(1)}KB)`; fileInfo.classList.add('has-file'); } else { fileInfo.textContent = 'No file selected'; fileInfo.classList.remove('has-file'); } this.updateDeployButton(); } updateDeployButton() { const deployBtn = this.findElement('#deploy-btn'); if (deployBtn) { const file = this.viewModel.get('selectedFile'); const targetNodes = this.viewModel.get('targetNodes'); const isUploading = this.viewModel.get('isUploading'); deployBtn.disabled = !file || !targetNodes || targetNodes.length === 0 || isUploading; } } updateUploadState() { const isUploading = this.viewModel.get('isUploading'); const deployBtn = this.findElement('#deploy-btn'); if (deployBtn) { deployBtn.disabled = isUploading; if (isUploading) { deployBtn.classList.add('loading'); // Update button text while keeping the SVG icon const iconSvg = deployBtn.querySelector('svg'); deployBtn.innerHTML = ''; if (iconSvg) { deployBtn.appendChild(iconSvg); } deployBtn.appendChild(document.createTextNode(' Deploying...')); } else { deployBtn.classList.remove('loading'); // Restore original button content with SVG icon deployBtn.innerHTML = ` Deploy `; } } this.updateDeployButton(); } updateUploadProgress() { // This will be implemented when we add upload progress tracking } updateUploadResults() { // This will be implemented when we add upload results display } showProgressOverlay() { // Create overlay element that only covers the left side (main content area) const overlay = document.createElement('div'); overlay.id = 'firmware-upload-overlay'; overlay.className = 'firmware-upload-overlay'; overlay.innerHTML = `
Firmware upload in progress...
Check the drawer for detailed progress
`; // Add to body document.body.appendChild(overlay); // Check if drawer is open and adjust overlay accordingly const drawer = document.querySelector('.details-drawer'); if (drawer && drawer.classList.contains('open')) { overlay.classList.add('drawer-open'); } // Block ESC key during upload this.blockEscapeKey(); // Block drawer close button during upload this.blockDrawerCloseButton(); // Block choose file button during upload this.blockChooseFileButton(); // Store reference for cleanup this.progressOverlay = overlay; } blockDrawerCloseButton() { // Find the drawer close button const closeButton = document.querySelector('.drawer-close'); if (closeButton) { // Store original state this.originalCloseButtonDisabled = closeButton.disabled; this.originalCloseButtonStyle = closeButton.style.cssText; // Disable the close button closeButton.disabled = true; closeButton.style.opacity = '0.5'; closeButton.style.cursor = 'not-allowed'; closeButton.style.pointerEvents = 'none'; // Add visual indicator that it's disabled closeButton.title = 'Cannot close during firmware upload'; } } unblockDrawerCloseButton() { // Restore the drawer close button const closeButton = document.querySelector('.drawer-close'); if (closeButton) { // Restore original state closeButton.disabled = this.originalCloseButtonDisabled || false; closeButton.style.cssText = this.originalCloseButtonStyle || ''; closeButton.title = 'Close'; } } blockChooseFileButton() { // Find the choose file button const chooseFileButton = document.querySelector('.upload-btn-compact'); if (chooseFileButton) { // Store original state this.originalChooseFileButtonDisabled = chooseFileButton.disabled; this.originalChooseFileButtonStyle = chooseFileButton.style.cssText; // Disable the choose file button chooseFileButton.disabled = true; chooseFileButton.style.opacity = '0.5'; chooseFileButton.style.cursor = 'not-allowed'; chooseFileButton.style.pointerEvents = 'none'; // Add visual indicator that it's disabled chooseFileButton.title = 'Cannot change file during upload'; } } unblockChooseFileButton() { // Restore the choose file button const chooseFileButton = document.querySelector('.upload-btn-compact'); if (chooseFileButton) { // Restore original state chooseFileButton.disabled = this.originalChooseFileButtonDisabled || false; chooseFileButton.style.cssText = this.originalChooseFileButtonStyle || ''; chooseFileButton.title = 'Choose File'; } } hideTargetNodesSection() { // Find the target nodes section const targetNodesSection = document.querySelector('.target-nodes-section'); if (targetNodesSection) { // Store original state this.originalTargetNodesSectionDisplay = targetNodesSection.style.display; // Hide the target nodes section targetNodesSection.style.display = 'none'; } } showTargetNodesSection() { // Restore the target nodes section const targetNodesSection = document.querySelector('.target-nodes-section'); if (targetNodesSection) { // Restore original state targetNodesSection.style.display = this.originalTargetNodesSectionDisplay || ''; } } blockEscapeKey() { // Create a keydown event listener that prevents ESC from closing the drawer this.escapeKeyHandler = (event) => { if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); return false; } }; // Add the event listener with capture=true to intercept before drawer's handler document.addEventListener('keydown', this.escapeKeyHandler, true); } unblockEscapeKey() { // Remove the ESC key blocker if (this.escapeKeyHandler) { document.removeEventListener('keydown', this.escapeKeyHandler, true); this.escapeKeyHandler = null; } } hideProgressOverlay() { if (this.progressOverlay) { this.progressOverlay.remove(); this.progressOverlay = null; } // Unblock ESC key this.unblockEscapeKey(); // Unblock drawer close button this.unblockDrawerCloseButton(); // Unblock choose file button this.unblockChooseFileButton(); } } window.FirmwareUploadComponent = FirmwareUploadComponent;