// Firmware Component class FirmwareComponent extends Component { constructor(container, viewModel, eventBus) { super(container, viewModel, eventBus); logger.debug('FirmwareComponent: Constructor called'); logger.debug('FirmwareComponent: Container:', container); logger.debug('FirmwareComponent: Container ID:', container?.id); // Check if the dropdown exists in the container if (container) { const dropdown = container.querySelector('#specific-node-select'); logger.debug('FirmwareComponent: Dropdown found in constructor:', !!dropdown); if (dropdown) { logger.debug('FirmwareComponent: Dropdown tagName:', dropdown.tagName); logger.debug('FirmwareComponent: Dropdown id:', dropdown.id); } } } setupEventListeners() { // Setup global firmware file input const globalFirmwareFile = this.findElement('#global-firmware-file'); if (globalFirmwareFile) { this.addEventListener(globalFirmwareFile, 'change', this.handleFileSelect.bind(this)); } // Setup target selection const targetRadios = this.findAllElements('input[name="target-type"]'); targetRadios.forEach(radio => { this.addEventListener(radio, 'change', this.handleTargetChange.bind(this)); }); // Setup specific node select change handler const specificNodeSelect = this.findElement('#specific-node-select'); logger.debug('FirmwareComponent: setupEventListeners - specificNodeSelect found:', !!specificNodeSelect); if (specificNodeSelect) { logger.debug('FirmwareComponent: specificNodeSelect element:', specificNodeSelect); logger.debug('FirmwareComponent: specificNodeSelect tagName:', specificNodeSelect.tagName); logger.debug('FirmwareComponent: specificNodeSelect id:', specificNodeSelect.id); // Store the bound handler as an instance property this._boundNodeSelectHandler = this.handleNodeSelect.bind(this); this.addEventListener(specificNodeSelect, 'change', this._boundNodeSelectHandler); logger.debug('FirmwareComponent: Event listener added to specificNodeSelect'); } else { logger.warn('FirmwareComponent: specificNodeSelect element not found during setupEventListeners'); } // Setup label select change handler (single-select add-to-chips) const labelSelect = this.findElement('#label-select'); if (labelSelect) { this._boundLabelSelectHandler = (e) => { const value = e.target.value; if (!value) return; const current = this.viewModel.get('selectedLabels') || []; if (!current.includes(value)) { this.viewModel.setSelectedLabels([...current, value]); } // Reset select back to placeholder e.target.value = ''; this.renderSelectedLabelChips(); this.updateAffectedNodesPreview(); this.updateDeployButton(); }; this.addEventListener(labelSelect, 'change', this._boundLabelSelectHandler); } // 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('targetType', () => { this.updateTargetVisibility(); this.updateDeployButton(); this.updateAffectedNodesPreview(); }); this.subscribeToProperty('specificNode', this.updateDeployButton.bind(this)); this.subscribeToProperty('availableNodes', () => { this.populateNodeSelect(); this.populateLabelSelect(); this.updateDeployButton(); this.updateAffectedNodesPreview(); }); this.subscribeToProperty('uploadProgress', this.updateUploadProgress.bind(this)); this.subscribeToProperty('uploadResults', this.updateUploadResults.bind(this)); this.subscribeToProperty('isUploading', this.updateUploadState.bind(this)); this.subscribeToProperty('selectedLabels', () => { this.populateLabelSelect(); this.updateAffectedNodesPreview(); this.updateDeployButton(); }); } mount() { super.mount(); logger.debug('FirmwareComponent: Mounting...'); // Check if the dropdown exists when mounted const dropdown = this.findElement('#specific-node-select'); logger.debug('FirmwareComponent: Mount - dropdown found:', !!dropdown); if (dropdown) { logger.debug('FirmwareComponent: Mount - dropdown tagName:', dropdown.tagName); logger.debug('FirmwareComponent: Mount - dropdown id:', dropdown.id); logger.debug('FirmwareComponent: Mount - dropdown innerHTML:', dropdown.innerHTML); } // Initialize target visibility and label list on first mount try { this.updateTargetVisibility(); this.populateLabelSelect(); this.updateAffectedNodesPreview(); } catch (e) { logger.warn('FirmwareComponent: Initialization after mount failed:', e); } logger.debug('FirmwareComponent: 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); } handleTargetChange(event) { const targetType = event.target.value; this.viewModel.setTargetType(targetType); } handleNodeSelect(event) { const nodeIp = event.target.value; logger.debug('FirmwareComponent: handleNodeSelect called with nodeIp:', nodeIp); logger.debug('Event:', event); logger.debug('Event target:', event.target); logger.debug('Event target value:', event.target.value); this.viewModel.setSpecificNode(nodeIp); // Also update the deploy button state this.updateDeployButton(); } async handleDeploy() { const file = this.viewModel.get('selectedFile'); const targetType = this.viewModel.get('targetType'); const specificNode = this.viewModel.get('specificNode'); if (!file) { alert('Please select a firmware file first.'); return; } if (targetType === 'specific' && !specificNode) { alert('Please select a specific node to update.'); return; } try { this.viewModel.startUpload(); if (targetType === 'all') { await this.uploadToAllNodes(file); } else if (targetType === 'specific') { await this.uploadToSpecificNode(file, specificNode); } else if (targetType === 'labels') { await this.uploadToLabelFilteredNodes(file); } // 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(); } } async uploadToAllNodes(file) { try { // Get current cluster members const response = await window.apiClient.getClusterMembers(); const nodes = response.members || []; if (nodes.length === 0) { alert('No nodes available for firmware update.'); return; } const confirmed = confirm(`Upload firmware to all ${nodes.length} nodes? This will update: ${nodes.map(n => n.hostname || n.ip).join(', ')}`); if (!confirmed) return; // Show upload progress area this.showUploadProgress(file, nodes); // Start batch upload const results = await this.performBatchUpload(file, nodes); // Display results this.displayUploadResults(results); } catch (error) { logger.error('Failed to upload firmware to all nodes:', error); throw error; } } async uploadToSpecificNode(file, nodeIp) { try { const confirmed = confirm(`Upload firmware to node ${nodeIp}?`); if (!confirmed) return; // Show upload progress area this.showUploadProgress(file, [{ ip: nodeIp, hostname: nodeIp }]); // Update progress to show starting this.updateNodeProgress(1, 1, nodeIp, 'Uploading...'); // Perform single node upload const result = await this.performSingleUpload(file, nodeIp); // Update progress to show completion this.updateNodeProgress(1, 1, nodeIp, 'Completed'); this.updateOverallProgress(1, 1); // Display results this.displayUploadResults([result]); } catch (error) { logger.error(`Failed to upload firmware to node ${nodeIp}:`, error); // Update progress to show failure this.updateNodeProgress(1, 1, nodeIp, 'Failed'); this.updateOverallProgress(0, 1); // Display error results const errorResult = { nodeIp: nodeIp, hostname: nodeIp, success: false, error: error.message, timestamp: new Date().toISOString() }; this.displayUploadResults([errorResult]); throw error; } } async uploadToLabelFilteredNodes(file) { try { const nodes = this.viewModel.getAffectedNodesByLabels(); if (!nodes || nodes.length === 0) { alert('No nodes match the selected labels.'); return; } const labels = this.viewModel.get('selectedLabels') || []; const confirmed = confirm(`Upload firmware to ${nodes.length} node(s) matching labels (${labels.join(', ')})?`); if (!confirmed) return; // Show upload progress area this.showUploadProgress(file, nodes); // Start batch upload const results = await this.performBatchUpload(file, nodes); // Display results this.displayUploadResults(results); } catch (error) { logger.error('Failed to upload firmware to label-filtered nodes:', error); throw error; } } 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) { const container = this.findElement('#firmware-nodes-list'); const progressHTML = `