-
-
-
-
-
-
- Firmware Update
-
-
-
-
-
-
-
-
-
-
-
- Deploy
-
-
-
-
+
@@ -272,6 +246,7 @@
+
diff --git a/public/scripts/api-client.js b/public/scripts/api-client.js
index fa0a458..3f9e6ed 100644
--- a/public/scripts/api-client.js
+++ b/public/scripts/api-client.js
@@ -136,6 +136,98 @@ class ApiClient {
}
});
}
+
+ // Registry API methods
+ async getRegistryBaseUrl() {
+ // Auto-detect registry server URL based on current location
+ const currentHost = window.location.hostname;
+
+ // If accessing from localhost, use localhost:8080
+ // If accessing from another device, use the same hostname but port 8080
+ if (currentHost === 'localhost' || currentHost === '127.0.0.1') {
+ return 'http://localhost:8080';
+ } else {
+ return `http://${currentHost}:8080`;
+ }
+ }
+
+ async uploadFirmwareToRegistry(metadata, firmwareFile) {
+ const registryBaseUrl = await this.getRegistryBaseUrl();
+ const formData = new FormData();
+ formData.append('metadata', JSON.stringify(metadata));
+ formData.append('firmware', firmwareFile);
+
+ const response = await fetch(`${registryBaseUrl}/firmware`, {
+ method: 'POST',
+ body: formData
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ throw new Error(`Registry upload failed: ${errorText}`);
+ }
+
+ return await response.json();
+ }
+
+ async updateFirmwareMetadata(name, version, metadata) {
+ const registryBaseUrl = await this.getRegistryBaseUrl();
+
+ const response = await fetch(`${registryBaseUrl}/firmware/${encodeURIComponent(name)}/${encodeURIComponent(version)}`, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(metadata)
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ throw new Error(`Registry metadata update failed: ${errorText}`);
+ }
+
+ return await response.json();
+ }
+
+ async listFirmwareFromRegistry(name = null, version = null) {
+ const registryBaseUrl = await this.getRegistryBaseUrl();
+ const query = {};
+ if (name) query.name = name;
+ if (version) query.version = version;
+
+ const response = await fetch(`${registryBaseUrl}/firmware${Object.keys(query).length ? '?' + new URLSearchParams(query).toString() : ''}`);
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ throw new Error(`Registry list failed: ${errorText}`);
+ }
+
+ return await response.json();
+ }
+
+ async downloadFirmwareFromRegistry(name, version) {
+ const registryBaseUrl = await this.getRegistryBaseUrl();
+ const response = await fetch(`${registryBaseUrl}/firmware/${encodeURIComponent(name)}/${encodeURIComponent(version)}`);
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ throw new Error(`Registry download failed: ${errorText}`);
+ }
+
+ return response.blob();
+ }
+
+ async getRegistryHealth() {
+ const registryBaseUrl = await this.getRegistryBaseUrl();
+ const response = await fetch(`${registryBaseUrl}/health`);
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ throw new Error(`Registry health check failed: ${errorText}`);
+ }
+
+ return await response.json();
+ }
}
// Global API client instance
diff --git a/public/scripts/components/ComponentsLoader.js b/public/scripts/components/ComponentsLoader.js
index f294a9e..9c441f5 100644
--- a/public/scripts/components/ComponentsLoader.js
+++ b/public/scripts/components/ComponentsLoader.js
@@ -1,7 +1,7 @@
(function(){
// Simple readiness flag once all component constructors are present
function allReady(){
- return !!(window.PrimaryNodeComponent && window.ClusterMembersComponent && window.NodeDetailsComponent && window.FirmwareComponent && window.ClusterViewComponent && window.FirmwareViewComponent && window.TopologyGraphComponent && window.MemberCardOverlayComponent && window.ClusterStatusComponent && window.DrawerComponent && window.WiFiConfigComponent);
+ return !!(window.PrimaryNodeComponent && window.ClusterMembersComponent && window.NodeDetailsComponent && window.FirmwareComponent && window.FirmwareFormComponent && window.ClusterViewComponent && window.FirmwareViewComponent && window.TopologyGraphComponent && window.MemberCardOverlayComponent && window.ClusterStatusComponent && window.DrawerComponent && window.WiFiConfigComponent);
}
window.waitForComponentsReady = function(timeoutMs = 5000){
return new Promise((resolve, reject) => {
diff --git a/public/scripts/components/FirmwareComponent.js b/public/scripts/components/FirmwareComponent.js
index 2352603..be55d50 100644
--- a/public/scripts/components/FirmwareComponent.js
+++ b/public/scripts/components/FirmwareComponent.js
@@ -1,109 +1,44 @@
-// Firmware Component
+// Registry Firmware Component - CRUD interface for firmware registry
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);
- // Initialize overlay dialog
- this.overlayDialog = null;
+ // Initialize drawer component
+ this.drawer = new DrawerComponent();
- // 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);
- }
- }
+ // Registry connection status
+ this.registryConnected = false;
+ this.registryError = null;
}
setupEventListeners() {
- // Setup global firmware file input
- const globalFirmwareFile = this.findElement('#global-firmware-file');
- if (globalFirmwareFile) {
- this.addEventListener(globalFirmwareFile, 'change', this.handleFileSelect.bind(this));
+ // Setup refresh button
+ const refreshBtn = this.findElement('#refresh-firmware-btn');
+ if (refreshBtn) {
+ this.addEventListener(refreshBtn, 'click', this.refreshFirmwareList.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 add firmware button
+ const addBtn = this.findElement('#add-firmware-btn');
+ if (addBtn) {
+ this.addEventListener(addBtn, 'click', this.showAddFirmwareForm.bind(this));
+ }
- // Setup WebSocket listener for real-time firmware upload status
- this.setupWebSocketListeners();
-
- // 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));
+ // Setup search input
+ const searchInput = this.findElement('#firmware-search');
+ if (searchInput) {
+ this.addEventListener(searchInput, 'input', this.handleSearch.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();
- });
+ this.subscribeToProperty('firmwareList', this.renderFirmwareList.bind(this));
+ this.subscribeToProperty('isLoading', this.updateLoadingState.bind(this));
+ this.subscribeToProperty('searchQuery', this.updateSearchResults.bind(this));
+ this.subscribeToProperty('registryConnected', this.updateRegistryStatus.bind(this));
}
mount() {
@@ -111,897 +46,482 @@ class FirmwareComponent extends Component {
logger.debug('FirmwareComponent: Mounting...');
- // Initialize overlay dialog
- this.initializeOverlayDialog();
-
- // 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);
- }
+ // Check registry connection and load firmware list
+ this.checkRegistryConnection();
+ this.loadFirmwareList();
logger.debug('FirmwareComponent: Mounted successfully');
}
- render() {
- // Initial render is handled by the HTML template
- this.updateDeployButton();
- }
-
- initializeOverlayDialog() {
- // Create overlay container if it doesn't exist
- let overlayContainer = document.getElementById('firmware-overlay-dialog');
- if (!overlayContainer) {
- overlayContainer = document.createElement('div');
- overlayContainer.id = 'firmware-overlay-dialog';
- overlayContainer.className = 'overlay-dialog';
- document.body.appendChild(overlayContainer);
- }
-
- // Create and initialize the overlay dialog component
- if (!this.overlayDialog) {
- const overlayVM = new ViewModel();
- this.overlayDialog = new OverlayDialogComponent(overlayContainer, overlayVM, this.eventBus);
- this.overlayDialog.mount();
+ async checkRegistryConnection() {
+ try {
+ await window.apiClient.getRegistryHealth();
+ this.registryConnected = true;
+ this.registryError = null;
+ this.viewModel.set('registryConnected', true);
+ } catch (error) {
+ logger.error('Registry connection failed:', error);
+ this.registryConnected = false;
+ this.registryError = error.message;
+ this.viewModel.set('registryConnected', false);
}
}
- handleFileSelect(event) {
- const file = event.target.files[0];
- this.viewModel.setSelectedFile(file);
+ async loadFirmwareList() {
+ try {
+ this.viewModel.set('isLoading', true);
+ const firmwareList = await window.apiClient.listFirmwareFromRegistry();
+ this.viewModel.set('firmwareList', firmwareList);
+ } catch (error) {
+ logger.error('Failed to load firmware list:', error);
+ this.viewModel.set('firmwareList', []);
+ this.showError('Failed to load firmware list: ' + error.message);
+ } finally {
+ this.viewModel.set('isLoading', false);
+ }
}
- handleTargetChange(event) {
- const targetType = event.target.value;
- this.viewModel.setTargetType(targetType);
+ async refreshFirmwareList() {
+ await this.checkRegistryConnection();
+ await this.loadFirmwareList();
}
- 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();
- }
+ renderFirmwareList() {
+ const container = this.findElement('#firmware-list-container');
+ if (!container) return;
- async handleDeploy() {
- const file = this.viewModel.get('selectedFile');
- const targetType = this.viewModel.get('targetType');
- const specificNode = this.viewModel.get('specificNode');
+ const groupedFirmware = this.viewModel.get('firmwareList') || [];
+ const searchQuery = this.viewModel.get('searchQuery') || '';
- if (!file) {
- this.showConfirmationDialog({
- title: 'No File Selected',
- message: 'Please select a firmware file first.',
- confirmText: 'OK',
- cancelText: null,
- onConfirm: () => {},
- onCancel: null
+ // Filter grouped firmware based on search query
+ const filteredGroups = groupedFirmware.map(group => {
+ if (!searchQuery) return group;
+
+ // Split search query into individual terms
+ const searchTerms = searchQuery.toLowerCase().split(/\s+/).filter(term => term.length > 0);
+
+ // Filter firmware versions within the group
+ const filteredFirmware = group.firmware.filter(firmware => {
+ // All search terms must match somewhere in the firmware data
+ return searchTerms.every(term => {
+ // Check group name
+ if (group.name.toLowerCase().includes(term)) {
+ return true;
+ }
+
+ // Check version
+ if (firmware.version.toLowerCase().includes(term)) {
+ return true;
+ }
+
+ // Check labels
+ if (Object.values(firmware.labels || {}).some(label =>
+ label.toLowerCase().includes(term)
+ )) {
+ return true;
+ }
+
+ return false;
+ });
});
+
+ // Return group with filtered firmware, or null if no firmware matches
+ return filteredFirmware.length > 0 ? {
+ ...group,
+ firmware: filteredFirmware
+ } : null;
+ }).filter(group => group !== null);
+
+ if (filteredGroups.length === 0) {
+ container.innerHTML = `
+
+
+
${searchQuery ? 'No firmware found' : 'No firmware available'}
+
+ ${searchQuery ? 'Try adjusting your search terms' : 'Upload your first firmware to get started'}
+
+
+ `;
return;
}
+
+ const firmwareHTML = filteredGroups.map(group => this.renderFirmwareGroup(group)).join('');
- if (targetType === 'specific' && !specificNode) {
- this.showConfirmationDialog({
- title: 'No Node Selected',
- message: 'Please select a specific node to update.',
- confirmText: 'OK',
- cancelText: null,
- onConfirm: () => {},
- onCancel: null
+ container.innerHTML = `
+
+ ${firmwareHTML}
+
+ `;
+
+ // Setup event listeners for firmware items
+ this.setupFirmwareItemListeners();
+ }
+
+ renderFirmwareGroup(group) {
+ const versionsHTML = group.firmware.map(firmware => this.renderFirmwareVersion(firmware)).join('');
+
+ return `
+
+
+
+ ${versionsHTML}
+
+
+ `;
+ }
+
+ renderFirmwareVersion(firmware) {
+ const labels = firmware.labels || {};
+ const labelsHTML = Object.entries(labels).map(([key, value]) =>
+ `
${key}: ${value} `
+ ).join('');
+
+ const sizeKB = Math.round(firmware.size / 1024);
+
+ return `
+
+
+
+
v${this.escapeHtml(firmware.version)}
+
${sizeKB} KB
+
+
+ ${labelsHTML}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ }
+
+ renderFirmwareItem(firmware) {
+ const labels = firmware.labels || {};
+ const labelsHTML = Object.entries(labels).map(([key, value]) =>
+ `
${key}: ${value} `
+ ).join('');
+
+ const sizeKB = Math.round(firmware.size / 1024);
+
+ return `
+
+
+
+
${this.escapeHtml(firmware.name)}
+
v${this.escapeHtml(firmware.version)}
+
${sizeKB} KB
+
+
+ ${labelsHTML}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ }
+
+ setupFirmwareItemListeners() {
+ // Version item clicks (for editing)
+ const versionItems = this.findAllElements('.firmware-version-item.clickable');
+ versionItems.forEach(item => {
+ this.addEventListener(item, 'click', (e) => {
+ // Don't trigger if clicking on action buttons
+ if (e.target.closest('.firmware-version-actions')) {
+ return;
+ }
+
+ const name = item.getAttribute('data-name');
+ const version = item.getAttribute('data-version');
+ this.showEditFirmwareForm(name, version);
});
- return;
- }
-
- // Show confirmation dialog for deployment
- this.showDeploymentConfirmation(file, targetType, specificNode);
+ });
+
+ // Download buttons
+ const downloadBtns = this.findAllElements('.download-btn');
+ downloadBtns.forEach(btn => {
+ this.addEventListener(btn, 'click', (e) => {
+ e.stopPropagation();
+ const name = btn.getAttribute('data-name');
+ const version = btn.getAttribute('data-version');
+ this.downloadFirmware(name, version);
+ });
+ });
+
+ // Delete buttons
+ const deleteBtns = this.findAllElements('.delete-btn');
+ deleteBtns.forEach(btn => {
+ this.addEventListener(btn, 'click', (e) => {
+ e.stopPropagation();
+ const name = btn.getAttribute('data-name');
+ const version = btn.getAttribute('data-version');
+ this.showDeleteConfirmation(name, version);
+ });
+ });
}
- showConfirmationDialog(options) {
- if (!this.overlayDialog) {
- this.initializeOverlayDialog();
- }
-
- this.overlayDialog.show(options);
+ showAddFirmwareForm() {
+ this.openFirmwareForm('Add Firmware', null, null);
}
- showDeploymentConfirmation(file, targetType, specificNode) {
- let title, message;
+ showEditFirmwareForm(name, version) {
+ const groupedFirmware = this.viewModel.get('firmwareList') || [];
- if (targetType === 'all') {
- const nodes = this.viewModel.get('availableNodes') || [];
- title = 'Deploy to All Nodes';
- message = `Upload firmware "${file.name}" to all ${nodes.length} nodes?
This will update:
${nodes.map(n => `• ${n.hostname || n.ip}`).join('
')}`;
- } else if (targetType === 'specific') {
- title = 'Deploy to Specific Node';
- message = `Upload firmware "${file.name}" to node ${specificNode}?`;
- } else if (targetType === 'labels') {
- const nodes = this.viewModel.getAffectedNodesByLabels();
- const labels = this.viewModel.get('selectedLabels') || [];
- title = 'Deploy to Labeled Nodes';
- message = `Upload firmware "${file.name}" to ${nodes.length} node(s) matching labels (${labels.join(', ')})?
This will update:
${nodes.map(n => `• ${n.hostname || n.ip}`).join('
')}`;
+ // Find the firmware in the grouped data
+ let firmware = null;
+ for (const group of groupedFirmware) {
+ if (group.name === name) {
+ firmware = group.firmware.find(f => f.version === version);
+ if (firmware) break;
+ }
}
+ if (firmware) {
+ this.openFirmwareForm('Edit Firmware', firmware, null);
+ }
+ }
+
+ openFirmwareForm(title, firmwareData, onCloseCallback) {
+ this.drawer.openDrawer(title, (contentContainer, setActiveComponent) => {
+ const formComponent = new FirmwareFormComponent(contentContainer, this.viewModel, this.eventBus);
+ setActiveComponent(formComponent);
+
+ formComponent.setFirmwareData(firmwareData);
+ formComponent.setOnSaveCallback(() => {
+ this.loadFirmwareList();
+ this.drawer.closeDrawer();
+ });
+ formComponent.setOnCancelCallback(() => {
+ this.drawer.closeDrawer();
+ });
+
+ formComponent.mount();
+ }, null, onCloseCallback, true); // Hide terminal button
+ }
+
+ async downloadFirmware(name, version) {
+ try {
+ const blob = await window.apiClient.downloadFirmwareFromRegistry(name, version);
+
+ // Create download link
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `${name}-${version}.bin`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ window.URL.revokeObjectURL(url);
+
+ this.showSuccess(`Firmware ${name} v${version} downloaded successfully`);
+ } catch (error) {
+ logger.error('Download failed:', error);
+ this.showError('Download failed: ' + error.message);
+ }
+ }
+
+ showDeleteConfirmation(name, version) {
this.showConfirmationDialog({
- title: title,
- message: message,
- confirmText: 'Deploy',
+ title: 'Delete Firmware',
+ message: `Are you sure you want to delete firmware "${name}" version "${version}"?
This action cannot be undone.`,
+ confirmText: 'Delete',
cancelText: 'Cancel',
- onConfirm: () => this.performDeployment(file, targetType, specificNode),
+ onConfirm: () => this.deleteFirmware(name, version),
onCancel: () => {}
});
}
- async performDeployment(file, targetType, specificNode) {
+ async deleteFirmware(name, version) {
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);
- }
-
- // NOTE: Don't reset upload state here!
- // The upload state should remain active until websocket confirms completion
- // Status updates and finalization happen via websocket messages in checkAndFinalizeUploadResults()
- logger.debug('Firmware upload HTTP requests completed, waiting for websocket status updates');
-
+ // Note: The registry API doesn't have a delete endpoint in the OpenAPI spec
+ // This would need to be implemented in the registry service
+ this.showError('Delete functionality not yet implemented in registry API');
} catch (error) {
- logger.error('Firmware deployment failed:', error);
- this.showConfirmationDialog({
- title: 'Deployment Failed',
- message: `Deployment failed: ${error.message}`,
- confirmText: 'OK',
- cancelText: null,
- onConfirm: () => {},
- onCancel: null
- });
- // Only complete upload on error
- this.viewModel.completeUpload();
+ logger.error('Delete failed:', error);
+ this.showError('Delete failed: ' + error.message);
}
}
- async uploadToAllNodes(file) {
- try {
- // Get current cluster members
- const response = await window.apiClient.getClusterMembers();
- const nodes = response.members || [];
-
- if (nodes.length === 0) {
- this.showConfirmationDialog({
- title: 'No Nodes Available',
- message: 'No nodes available for firmware update.',
- confirmText: 'OK',
- cancelText: null,
- onConfirm: () => {},
- onCancel: null
- });
- return;
- }
-
- // Show upload progress area
- this.showUploadProgress(file, nodes);
-
- // Start batch upload
- const results = await this.performBatchUpload(file, nodes);
-
- // Don't display results here - wait for websocket to confirm all uploads complete
- logger.debug('Batch upload HTTP requests completed, waiting for websocket confirmations');
-
- } catch (error) {
- logger.error('Failed to upload firmware to all nodes:', error);
- throw error;
- }
+ handleSearch(event) {
+ const query = event.target.value;
+ this.viewModel.set('searchQuery', query);
}
- async uploadToSpecificNode(file, nodeIp) {
- try {
- // Show upload progress area
- this.showUploadProgress(file, [{ ip: nodeIp, hostname: nodeIp }]);
-
- // Note: Status updates will come via websocket messages
- // We don't update progress here as the HTTP response is just an acknowledgment
-
- // Perform single node upload (this sends the file and gets acknowledgment)
- const result = await this.performSingleUpload(file, nodeIp);
-
- // Don't immediately mark as completed - wait for websocket status updates
- logger.debug(`Firmware upload initiated for node ${nodeIp}, waiting for completion status via websocket`);
-
- } catch (error) {
- logger.error(`Failed to upload firmware to node ${nodeIp}:`, error);
-
- // For HTTP errors, we can immediately mark as failed since the upload didn't start
- 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;
- }
+ updateSearchResults() {
+ // This method is called when searchQuery property changes
+ // The actual filtering is handled in renderFirmwareList
+ this.renderFirmwareList();
}
- async uploadToLabelFilteredNodes(file) {
- try {
- const nodes = this.viewModel.getAffectedNodesByLabels();
- if (!nodes || nodes.length === 0) {
- this.showConfirmationDialog({
- title: 'No Matching Nodes',
- message: 'No nodes match the selected labels.',
- confirmText: 'OK',
- cancelText: null,
- onConfirm: () => {},
- onCancel: null
- });
- return;
- }
-
- // Show upload progress area
- this.showUploadProgress(file, nodes);
-
- // Start batch upload
- const results = await this.performBatchUpload(file, nodes);
-
- // Don't display results here - wait for websocket to confirm all uploads complete
- logger.debug('Label-filtered upload HTTP requests completed, waiting for websocket confirmations');
- } 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;
-
- // Initialize all nodes as uploading first
- for (let i = 0; i < nodes.length; i++) {
- const node = nodes[i];
- const nodeIp = node.ip;
- this.updateNodeProgress(i + 1, totalNodes, nodeIp, 'Uploading...');
- }
-
- for (let i = 0; i < nodes.length; i++) {
- const node = nodes[i];
- const nodeIp = node.ip;
-
- try {
- // Upload to this node (HTTP call just initiates the upload)
- const result = await this.performSingleUpload(file, nodeIp);
-
- // Don't immediately mark as completed - wait for websocket status
- logger.debug(`Firmware upload initiated for node ${nodeIp}, waiting for completion status via websocket`);
- results.push(result);
-
- } 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);
-
- // For HTTP errors, we can immediately mark as failed since the upload didn't start
- this.updateNodeProgress(i + 1, totalNodes, nodeIp, 'Failed');
- }
-
- // 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);
-
- // IMPORTANT: This HTTP response is just an acknowledgment that the gateway received the file
- // The actual firmware processing happens asynchronously on the device
- // Status updates will come via WebSocket messages, NOT from this HTTP response
- logger.debug(`HTTP acknowledgment received for ${nodeIp}:`, result);
- logger.debug(`This does NOT mean upload is complete - waiting for WebSocket status updates`);
-
- return {
- nodeIp: nodeIp,
- hostname: nodeIp,
- httpAcknowledged: true, // Changed from 'success' to make it clear this is just HTTP ack
- 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');
+ updateLoadingState() {
+ const isLoading = this.viewModel.get('isLoading');
+ const container = this.findElement('#firmware-list-container');
- const progressHTML = `
-
-