diff --git a/public/script.js b/public/script.js
index 7c5a51c..2956289 100644
--- a/public/script.js
+++ b/public/script.js
@@ -754,28 +754,276 @@ async function handleGlobalFirmwareUpload(event) {
// Upload firmware to all nodes
async function uploadFirmwareToAllNodes(file) {
- const response = await client.getClusterMembers();
- const nodes = response.members || [];
-
- if (nodes.length === 0) {
- alert('No nodes available for firmware update.');
- return;
+ try {
+ // Get current cluster members
+ const response = await client.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
+ showFirmwareUploadProgress(file, nodes);
+
+ // Start batch upload
+ const results = await performBatchFirmwareUpload(file, nodes);
+
+ // Display results
+ displayFirmwareUploadResults(results);
+
+ } catch (error) {
+ console.error('Failed to upload firmware to all nodes:', error);
+ alert(`Upload failed: ${error.message}`);
}
-
- const confirmed = confirm(`Upload firmware to all ${nodes.length} nodes? This will update: ${nodes.map(n => n.hostname || n.ip).join(', ')}`);
- if (!confirmed) return;
-
- // TODO: Implement batch upload logic
- alert(`Firmware upload to all ${nodes.length} nodes initiated. This feature is coming soon!`);
}
// Upload firmware to specific node
async function uploadFirmwareToSpecificNode(file, nodeIp) {
- const confirmed = confirm(`Upload firmware to node ${nodeIp}?`);
- if (!confirmed) return;
+ try {
+ const confirmed = confirm(`Upload firmware to node ${nodeIp}?`);
+ if (!confirmed) return;
+
+ // Show upload progress area
+ showFirmwareUploadProgress(file, [{ ip: nodeIp, hostname: nodeIp }]);
+
+ // Perform single node upload
+ const result = await performSingleFirmwareUpload(file, nodeIp);
+
+ // Display results
+ displayFirmwareUploadResults([result]);
+
+ } catch (error) {
+ console.error(`Failed to upload firmware to node ${nodeIp}:`, error);
+ alert(`Upload failed: ${error.message}`);
+ }
+}
+
+// Perform batch firmware upload to multiple nodes
+async function performBatchFirmwareUpload(file, nodes) {
+ const results = [];
+ const totalNodes = nodes.length;
- // TODO: Implement single node upload logic
- alert(`Firmware upload to node ${nodeIp} initiated. This feature is coming soon!`);
+ for (let i = 0; i < nodes.length; i++) {
+ const node = nodes[i];
+ const nodeIp = node.ip;
+
+ try {
+ // Update progress
+ updateFirmwareUploadProgress(i + 1, totalNodes, nodeIp, 'Uploading...');
+
+ // Upload to this node
+ const result = await performSingleFirmwareUpload(file, nodeIp);
+ results.push(result);
+
+ // Update progress
+ updateFirmwareUploadProgress(i + 1, totalNodes, nodeIp, 'Completed');
+
+ } catch (error) {
+ console.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
+ updateFirmwareUploadProgress(i + 1, totalNodes, nodeIp, 'Failed');
+ }
+
+ // Small delay between uploads to avoid overwhelming the network
+ if (i < nodes.length - 1) {
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ }
+ }
+
+ return results;
+}
+
+// Perform single firmware upload to a specific node
+async function performSingleFirmwareUpload(file, nodeIp) {
+ try {
+ // Create FormData for the upload
+ const formData = new FormData();
+ formData.append('file', file);
+
+ // Upload to backend
+ const response = await fetch(`/api/node/update?ip=${encodeURIComponent(nodeIp)}`, {
+ method: 'POST',
+ body: formData
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`);
+ }
+
+ const result = await response.json();
+
+ return {
+ nodeIp: nodeIp,
+ hostname: nodeIp, // Will be updated if we have more info
+ success: true,
+ result: result,
+ timestamp: new Date().toISOString()
+ };
+
+ } catch (error) {
+ throw new Error(`Upload to ${nodeIp} failed: ${error.message}`);
+ }
+}
+
+// Show firmware upload progress area
+function showFirmwareUploadProgress(file, nodes) {
+ const container = document.getElementById('firmware-nodes-list');
+
+ const progressHTML = `
+
+
+
+ ${nodes.map(node => `
+
+
+ ${node.hostname || node.ip}
+ ${node.ip}
+
+
Pending...
+
+
+ `).join('')}
+
+
+ `;
+
+ container.innerHTML = progressHTML;
+}
+
+// Update firmware upload progress
+function updateFirmwareUploadProgress(current, total, nodeIp, status) {
+ const progressItem = document.querySelector(`[data-node-ip="${nodeIp}"]`);
+ if (progressItem) {
+ const statusElement = progressItem.querySelector('.progress-status');
+ const timeElement = progressItem.querySelector('.progress-time');
+
+ if (statusElement) {
+ statusElement.textContent = status;
+
+ // Add status-specific styling
+ statusElement.className = 'progress-status';
+ if (status === 'Completed') {
+ statusElement.classList.add('success');
+ if (timeElement) {
+ timeElement.textContent = new Date().toLocaleTimeString();
+ }
+ } else if (status === 'Failed') {
+ statusElement.classList.add('error');
+ if (timeElement) {
+ timeElement.textContent = new Date().toLocaleTimeString();
+ }
+ } else if (status === 'Uploading...') {
+ statusElement.classList.add('uploading');
+ if (timeElement) {
+ timeElement.textContent = 'Started: ' + new Date().toLocaleTimeString();
+ }
+ }
+ }
+ }
+
+ // Update overall progress
+ const progressHeader = document.querySelector('.progress-header h3');
+ const progressBar = document.getElementById('overall-progress-bar');
+ const progressText = document.querySelector('.progress-text');
+
+ if (progressHeader) {
+ progressHeader.textContent = `📤 Firmware Upload Progress (${current}/${total})`;
+ }
+
+ if (progressBar && progressText) {
+ const percentage = Math.round((current / total) * 100);
+ progressBar.style.width = `${percentage}%`;
+ progressText.textContent = `${percentage}% Complete`;
+
+ // Update progress bar color based on completion
+ if (percentage === 100) {
+ progressBar.style.backgroundColor = '#4ade80';
+ } else if (percentage > 50) {
+ progressBar.style.backgroundColor = '#60a5fa';
+ } else {
+ progressBar.style.backgroundColor = '#fbbf24';
+ }
+ }
+}
+
+// Display firmware upload results
+function displayFirmwareUploadResults(results) {
+ const container = document.getElementById('firmware-nodes-list');
+
+ const successCount = results.filter(r => r.success).length;
+ const failureCount = results.filter(r => !r.success).length;
+
+ const resultsHTML = `
+
+
+
+ ${results.map(result => `
+
+
+ ${result.hostname || result.nodeIp}
+ ${result.nodeIp}
+
+
+ ${result.success ? '✅ Success' : '❌ Failed'}
+
+
+ ${result.success ?
+ `Uploaded successfully at ${new Date(result.timestamp).toLocaleTimeString()}` :
+ `Error: ${result.error}`
+ }
+
+
+ `).join('')}
+
+
+
+
+
+
+ `;
+
+ container.innerHTML = resultsHTML;
+}
+
+// Clear firmware upload results
+function clearFirmwareResults() {
+ const container = document.getElementById('firmware-nodes-list');
+ container.innerHTML = '';
}
// Populate node select dropdown
diff --git a/public/styles.css b/public/styles.css
index 3792669..41d5b3b 100644
--- a/public/styles.css
+++ b/public/styles.css
@@ -1103,4 +1103,246 @@ p {
padding: 0.2rem;
margin-top: 0.5rem;
}
+}
+
+/* Firmware upload progress and results styling */
+.firmware-upload-progress,
+.firmware-upload-results {
+ background: rgba(0, 0, 0, 0.3);
+ border-radius: 16px;
+ backdrop-filter: blur(10px);
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ padding: 1.5rem;
+ margin-top: 1rem;
+}
+
+.progress-header,
+.results-header {
+ margin-bottom: 1.5rem;
+ padding-bottom: 1rem;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
+}
+
+.progress-header h3,
+.results-header h3 {
+ margin: 0 0 0.5rem 0;
+ color: #ecf0f1;
+ font-size: 1.2rem;
+ font-weight: 600;
+}
+
+.progress-info,
+.results-summary {
+ display: flex;
+ gap: 1rem;
+ flex-wrap: wrap;
+ font-size: 0.9rem;
+ color: rgba(255, 255, 255, 0.7);
+}
+
+.progress-info span,
+.results-summary span {
+ padding: 0.25rem 0.5rem;
+ background: rgba(255, 255, 255, 0.05);
+ border-radius: 6px;
+ border: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+.overall-progress {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ margin-top: 1rem;
+}
+
+.progress-bar-container {
+ flex: 1;
+ height: 8px;
+ background: rgba(255, 255, 255, 0.1);
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.progress-bar {
+ height: 100%;
+ background: #fbbf24;
+ border-radius: 4px;
+ transition: width 0.3s ease, background-color 0.3s ease;
+ width: 0%;
+}
+
+.progress-text {
+ font-size: 0.9rem;
+ color: rgba(255, 255, 255, 0.8);
+ font-weight: 500;
+ min-width: 80px;
+ text-align: right;
+}
+
+.success-count {
+ color: #4ade80 !important;
+ border-color: rgba(74, 222, 128, 0.3) !important;
+}
+
+.failure-count {
+ color: #f87171 !important;
+ border-color: rgba(248, 113, 113, 0.3) !important;
+}
+
+.total-count {
+ color: #60a5fa !important;
+ border-color: rgba(96, 165, 250, 0.3) !important;
+}
+
+.progress-list,
+.results-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.progress-item,
+.result-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1rem;
+ background: rgba(255, 255, 255, 0.05);
+ border-radius: 12px;
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ transition: all 0.3s ease;
+}
+
+.progress-item:hover,
+.result-item:hover {
+ background: rgba(255, 255, 255, 0.08);
+ border-color: rgba(255, 255, 255, 0.15);
+}
+
+.progress-node-info,
+.result-node-info {
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+}
+
+.node-name {
+ font-weight: 600;
+ color: #ecf0f1;
+}
+
+.node-ip {
+ font-size: 0.85rem;
+ color: rgba(255, 255, 255, 0.6);
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+}
+
+.progress-status,
+.result-status {
+ font-weight: 600;
+ padding: 0.5rem 1rem;
+ border-radius: 8px;
+ font-size: 0.9rem;
+ min-width: 100px;
+ text-align: center;
+}
+
+.progress-time {
+ font-size: 0.8rem;
+ color: rgba(255, 255, 255, 0.5);
+ min-width: 120px;
+ text-align: right;
+}
+
+.progress-status.success,
+.result-status.success {
+ background: rgba(74, 222, 128, 0.1);
+ color: #4ade80;
+ border: 1px solid rgba(74, 222, 128, 0.2);
+}
+
+.progress-status.error,
+.result-status.error {
+ background: rgba(248, 113, 113, 0.1);
+ color: #f87171;
+ border: 1px solid rgba(248, 113, 113, 0.2);
+}
+
+.progress-status.uploading {
+ background: rgba(251, 191, 36, 0.1);
+ color: #fbbf24;
+ border: 1px solid rgba(251, 191, 36, 0.2);
+ animation: pulse 1.5s ease-in-out infinite alternate;
+}
+
+.result-details {
+ font-size: 0.85rem;
+ color: rgba(255, 255, 255, 0.7);
+ max-width: 300px;
+ text-align: right;
+}
+
+.results-actions {
+ display: flex;
+ gap: 1rem;
+ margin-top: 1.5rem;
+ padding-top: 1rem;
+ border-top: 1px solid rgba(255, 255, 255, 0.08);
+}
+
+.clear-btn,
+.refresh-btn {
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.05) 100%);
+ border: 1px solid rgba(255, 255, 255, 0.15);
+ color: rgba(255, 255, 255, 0.9);
+ padding: 0.75rem 1.25rem;
+ border-radius: 12px;
+ cursor: pointer;
+ font-size: 0.9rem;
+ font-weight: 500;
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.clear-btn:hover,
+.refresh-btn:hover {
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, rgba(255, 255, 255, 0.08) 100%);
+ border-color: rgba(255, 255, 255, 0.25);
+ transform: translateY(-2px);
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
+}
+
+.clear-btn:active,
+.refresh-btn:active {
+ transform: translateY(0);
+}
+
+/* Responsive design for progress and results */
+@media (max-width: 768px) {
+ .progress-item,
+ .result-item {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 0.75rem;
+ }
+
+ .progress-status,
+ .result-status {
+ align-self: flex-end;
+ }
+
+ .result-details {
+ text-align: left;
+ max-width: none;
+ }
+
+ .progress-info,
+ .results-summary {
+ flex-direction: column;
+ gap: 0.5rem;
+ }
+
+ .results-actions {
+ flex-direction: column;
+ }
}
\ No newline at end of file