diff --git a/public/index.html b/public/index.html index cbb9567..0a3f9ea 100644 --- a/public/index.html +++ b/public/index.html @@ -13,17 +13,86 @@
🚀 Cluster Online
-
-
-
🌐 Cluster Members
- + + +
+
+
+
🌐 Cluster Members
+ +
+ +
+
+
Loading cluster members...
+
+
- -
-
-
Loading cluster members...
+
+ +
+
+
+
📦 Firmware Management
+ +
+ +
+
+
+
+
Total Nodes
+
-
+
+
+
Available Updates
+
-
+
+
+
Last Update
+
-
+
+
+ +
+
+

📁 Upload New Firmware

+
+ + +
Select a .bin or .hex file to upload to all nodes
+
+
+ +
+

🎯 Target Selection

+
+ + + +
+
+
+
+ +
+ +
diff --git a/public/script.js b/public/script.js index 12a27b3..9dc09a9 100644 --- a/public/script.js +++ b/public/script.js @@ -529,8 +529,157 @@ function displayClusterMembers(members, expandedCards = new Map()) { // Load cluster members when page loads document.addEventListener('DOMContentLoaded', function() { refreshClusterMembers(); + setupNavigation(); + setupFirmwareView(); }); // Auto-refresh every 30 seconds // FIXME not working properly: scroll position is not preserved, if there is an upload happening, this mus also be handled -//setInterval(refreshClusterMembers, 30000); \ No newline at end of file +//setInterval(refreshClusterMembers, 30000); + +// Setup navigation menu +function setupNavigation() { + const navTabs = document.querySelectorAll('.nav-tab'); + const viewContents = document.querySelectorAll('.view-content'); + + navTabs.forEach(tab => { + tab.addEventListener('click', () => { + const targetView = tab.dataset.view; + + // Update active tab + navTabs.forEach(t => t.classList.remove('active')); + tab.classList.add('active'); + + // Update active view + viewContents.forEach(view => view.classList.remove('active')); + const targetViewElement = document.getElementById(`${targetView}-view`); + if (targetViewElement) { + targetViewElement.classList.add('active'); + } + + // Refresh the active view + if (targetView === 'cluster') { + refreshClusterMembers(); + } else if (targetView === 'firmware') { + refreshFirmwareView(); + } + }); + }); +} + +// Setup firmware view +function setupFirmwareView() { + // Setup global firmware file input + const globalFirmwareFile = document.getElementById('global-firmware-file'); + if (globalFirmwareFile) { + globalFirmwareFile.addEventListener('change', handleGlobalFirmwareUpload); + } + + // Setup target selection + const targetRadios = document.querySelectorAll('input[name="target-type"]'); + const specificNodeSelect = document.getElementById('specific-node-select'); + + targetRadios.forEach(radio => { + radio.addEventListener('change', () => { + if (radio.value === 'specific') { + specificNodeSelect.style.display = 'block'; + populateNodeSelect(); + } else { + specificNodeSelect.style.display = 'none'; + } + }); + }); +} + +// Handle global firmware upload +async function handleGlobalFirmwareUpload(event) { + const file = event.target.files[0]; + if (!file) return; + + const targetType = document.querySelector('input[name="target-type"]:checked').value; + const specificNode = document.getElementById('specific-node-select').value; + + if (targetType === 'specific' && !specificNode) { + alert('Please select a specific node to update.'); + return; + } + + try { + if (targetType === 'all') { + await uploadFirmwareToAllNodes(file); + } else { + await uploadFirmwareToSpecificNode(file, specificNode); + } + } catch (error) { + console.error('Global firmware upload failed:', error); + alert(`Upload failed: ${error.message}`); + } + + // Clear file input + event.target.value = ''; +} + +// 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; + } + + 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; + + // TODO: Implement single node upload logic + alert(`Firmware upload to node ${nodeIp} initiated. This feature is coming soon!`); +} + +// Populate node select dropdown +function populateNodeSelect() { + const select = document.getElementById('specific-node-select'); + if (!select) return; + + // Clear existing options + select.innerHTML = ''; + + // Get current cluster members and populate + const container = document.getElementById('cluster-members-container'); + const memberCards = container.querySelectorAll('.member-card'); + + memberCards.forEach(card => { + const memberIp = card.dataset.memberIp; + const hostname = card.querySelector('.member-name')?.textContent || memberIp; + + const option = document.createElement('option'); + option.value = memberIp; + option.textContent = `${hostname} (${memberIp})`; + select.appendChild(option); + }); +} + +// Refresh firmware view +function refreshFirmwareView() { + updateFirmwareStats(); + populateNodeSelect(); +} + +// Update firmware statistics +function updateFirmwareStats() { + const container = document.getElementById('cluster-members-container'); + const memberCards = container.querySelectorAll('.member-card'); + + document.getElementById('total-nodes').textContent = memberCards.length; + document.getElementById('available-updates').textContent = '0'; // TODO: Implement update checking + document.getElementById('last-update').textContent = 'Never'; // TODO: Implement last update tracking +} \ No newline at end of file diff --git a/public/styles.css b/public/styles.css index 1429e95..adac78f 100644 --- a/public/styles.css +++ b/public/styles.css @@ -499,6 +499,309 @@ p { opacity: 0.5; } +/* Main Navigation Styles */ +.main-navigation { + display: flex; + gap: 0.25rem; + margin-bottom: 2.5rem; + background: rgba(0, 0, 0, 0.2); + border-radius: 16px; + padding: 0.5rem; + border: 1px solid rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); +} + +.nav-tab { + background: transparent; + border: none; + color: rgba(255, 255, 255, 0.6); + padding: 0.875rem 1.75rem; + border-radius: 12px; + cursor: pointer; + font-size: 0.95rem; + font-weight: 500; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; +} + +.nav-tab::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.05) 100%); + opacity: 0; + transition: opacity 0.3s ease; +} + +.nav-tab:hover { + color: rgba(255, 255, 255, 0.9); + transform: translateY(-1px); +} + +.nav-tab:hover::before { + opacity: 1; +} + +.nav-tab.active { + background: rgba(255, 255, 255, 0.15); + color: #ffffff; + box-shadow: 0 4px 20px rgba(255, 255, 255, 0.1); + transform: translateY(-1px); +} + +.nav-tab.active::before { + opacity: 0; +} + +/* View Content Styles */ +.view-content { + display: none; +} + +.view-content.active { + display: block; +} + +/* Firmware Section Styles */ +.firmware-section { + background: rgba(0, 0, 0, 0.25); + border-radius: 24px; + backdrop-filter: blur(15px); + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3); + border: 1px solid rgba(255, 255, 255, 0.08); + padding: 2.5rem; + margin-bottom: 2rem; + position: relative; + overflow: hidden; +} + +.firmware-section::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 1px; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); +} + +.firmware-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2.5rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); +} + +.firmware-title { + font-size: 1.75rem; + font-weight: 700; + color: #ffffff; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); +} + +/* Firmware Stats */ +.firmware-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 1.5rem; + margin-bottom: 3rem; +} + +.stat-card { + background: linear-gradient(135deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.03) 100%); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 16px; + padding: 2rem 1.5rem; + text-align: center; + position: relative; + overflow: hidden; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.stat-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(90deg, #667eea, #764ba2); + opacity: 0.7; +} + +.stat-card:hover { + transform: translateY(-4px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3); + border-color: rgba(255, 255, 255, 0.15); +} + +.stat-title { + font-size: 0.85rem; + color: rgba(255, 255, 255, 0.6); + margin-bottom: 0.75rem; + text-transform: uppercase; + letter-spacing: 1px; + font-weight: 600; +} + +.stat-value { + font-size: 2.5rem; + font-weight: 800; + color: #ffffff; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); +} + +/* Firmware Actions */ +.firmware-actions { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); + gap: 2.5rem; + margin-bottom: 3rem; +} + +.action-group { + background: rgba(0, 0, 0, 0.2); + border-radius: 20px; + padding: 2rem; + border: 1px solid rgba(255, 255, 255, 0.06); + position: relative; + overflow: hidden; +} + +.action-group::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 1px; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.15), transparent); +} + +.action-group h3 { + color: #ffffff; + margin-bottom: 1.5rem; + font-size: 1.2rem; + font-weight: 600; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); +} + +.upload-area-large { + text-align: center; + padding: 2.5rem; + border: 2px dashed rgba(255, 255, 255, 0.15); + border-radius: 16px; + background: linear-gradient(135deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.01) 100%); + transition: all 0.3s ease; +} + +.upload-area-large:hover { + border-color: rgba(255, 255, 255, 0.25); + background: linear-gradient(135deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.02) 100%); +} + +.upload-btn-large { + background: linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, rgba(255, 255, 255, 0.08) 100%); + border: 1px solid rgba(255, 255, 255, 0.2); + color: #ffffff; + padding: 1.25rem 2.5rem; + border-radius: 12px; + cursor: pointer; + font-size: 1.1rem; + font-weight: 600; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + margin-bottom: 1.5rem; + position: relative; + overflow: hidden; +} + +.upload-btn-large::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); + transition: left 0.5s ease; +} + +.upload-btn-large:hover { + background: linear-gradient(135deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0.12) 100%); + transform: translateY(-3px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3); +} + +.upload-btn-large:hover::before { + left: 100%; +} + +.upload-info-large { + font-size: 0.9rem; + color: rgba(255, 255, 255, 0.7); + line-height: 1.5; +} + +/* Target Selection */ +.target-selection { + display: flex; + flex-direction: column; + gap: 1.25rem; +} + +.target-selection label { + display: flex; + align-items: center; + gap: 0.75rem; + color: rgba(255, 255, 255, 0.9); + cursor: pointer; + padding: 0.75rem; + border-radius: 10px; + transition: all 0.2s ease; + border: 1px solid transparent; +} + +.target-selection label:hover { + background: rgba(255, 255, 255, 0.05); + border-color: rgba(255, 255, 255, 0.1); +} + +.target-selection input[type="radio"] { + margin: 0; + width: 18px; + height: 18px; + accent-color: #667eea; +} + +.target-selection select { + background: linear-gradient(135deg, rgba(0, 0, 0, 0.4) 0%, rgba(0, 0, 0, 0.3) 100%); + border: 1px solid rgba(255, 255, 255, 0.15); + color: #ffffff; + padding: 0.75rem 1rem; + border-radius: 10px; + margin-top: 0.75rem; + font-size: 0.95rem; + transition: all 0.2s ease; + cursor: pointer; +} + +.target-selection select:hover { + border-color: rgba(255, 255, 255, 0.25); + background: linear-gradient(135deg, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.4) 100%); +} + +.target-selection select:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + /* Responsive design for smaller screens */ @media (max-width: 768px) { .header { @@ -516,6 +819,52 @@ p { font-size: 0.9rem; } + /* Mobile navigation */ + .main-navigation { + flex-direction: column; + gap: 0.25rem; + padding: 0.75rem; + } + + .nav-tab { + padding: 0.75rem 1rem; + font-size: 0.9rem; + border-radius: 10px; + text-align: center; + } + + /* Mobile firmware stats */ + .firmware-stats { + grid-template-columns: 1fr; + gap: 1rem; + } + + .stat-card { + padding: 1.5rem 1rem; + } + + .stat-value { + font-size: 2rem; + } + + .firmware-actions { + grid-template-columns: 1fr; + gap: 2rem; + } + + .action-group { + padding: 1.5rem; + } + + .upload-area-large { + padding: 2rem 1.5rem; + } + + .upload-btn-large { + padding: 1rem 2rem; + font-size: 1rem; + } + /* Mobile tab responsiveness */ .tabs-header { flex-wrap: wrap;