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 = ` +
+
+

📤 Firmware Upload Progress

+
+ File: ${file.name} + Size: ${(file.size / 1024).toFixed(1)}KB + Targets: ${nodes.length} node(s) +
+
+
+
+
+ 0% Complete +
+
+
+ ${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 = ` +
+
+

📊 Upload Results

+
+ ✅ ${successCount} Successful + ❌ ${failureCount} Failed + 📊 ${results.length} Total +
+
+
+ ${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