From b23025a6f8673bcf1bbbe64908b6ae2eee9e4334 Mon Sep 17 00:00:00 2001 From: Patrick Balsiger Date: Tue, 26 Aug 2025 13:19:46 +0200 Subject: [PATCH] feat: make upload stuff more compact --- public/index.html | 55 +++-- public/script.js | 87 ++++++- public/styles.css | 576 ++++++++++++++++++++++++++++------------------ 3 files changed, 461 insertions(+), 257 deletions(-) diff --git a/public/index.html b/public/index.html index c7c6ea1..c60274d 100644 --- a/public/index.html +++ b/public/index.html @@ -82,28 +82,41 @@
-

📁 Upload New Firmware

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

🎯 Target Selection

-
- - -
diff --git a/public/script.js b/public/script.js index edd23e0..f30ebb4 100644 --- a/public/script.js +++ b/public/script.js @@ -705,7 +705,7 @@ function setupFirmwareView() { // Setup global firmware file input const globalFirmwareFile = document.getElementById('global-firmware-file'); if (globalFirmwareFile) { - globalFirmwareFile.addEventListener('change', handleGlobalFirmwareUpload); + globalFirmwareFile.addEventListener('change', handleGlobalFirmwareFileSelect); } // Setup target selection @@ -720,36 +720,105 @@ function setupFirmwareView() { } else { specificNodeSelect.style.display = 'none'; } + updateDeployButton(); }); }); + + // Setup specific node select change handler + if (specificNodeSelect) { + specificNodeSelect.addEventListener('change', updateDeployButton); + } + + // Setup deploy button + const deployBtn = document.getElementById('deploy-btn'); + if (deployBtn) { + deployBtn.addEventListener('click', handleDeployFirmware); + } + + // Initial button state + updateDeployButton(); } -// Handle global firmware upload -async function handleGlobalFirmwareUpload(event) { +// Handle file selection for the compact interface +function handleGlobalFirmwareFileSelect(event) { const file = event.target.files[0]; - if (!file) return; + const fileInfo = document.getElementById('file-info'); + const deployBtn = document.getElementById('deploy-btn'); + if (file) { + fileInfo.textContent = `${file.name} (${(file.size / 1024).toFixed(1)}KB)`; + fileInfo.classList.add('has-file'); + deployBtn.disabled = false; + } else { + fileInfo.textContent = 'No file selected'; + fileInfo.classList.remove('has-file'); + deployBtn.disabled = true; + } +} + +// Update deploy button state +function updateDeployButton() { + const deployBtn = document.getElementById('deploy-btn'); + const fileInput = document.getElementById('global-firmware-file'); + const targetType = document.querySelector('input[name="target-type"]:checked'); + const specificNodeSelect = document.getElementById('specific-node-select'); + + if (!deployBtn || !fileInput) return; + + const hasFile = fileInput.files && fileInput.files.length > 0; + const isValidTarget = targetType.value === 'all' || + (targetType.value === 'specific' && specificNodeSelect.value); + + deployBtn.disabled = !hasFile || !isValidTarget; +} + +// Handle deploy firmware button click +async function handleDeployFirmware() { + const fileInput = document.getElementById('global-firmware-file'); const targetType = document.querySelector('input[name="target-type"]:checked').value; const specificNode = document.getElementById('specific-node-select').value; + if (!fileInput.files || !fileInput.files[0]) { + alert('Please select a firmware file first.'); + return; + } + + const file = fileInput.files[0]; + if (targetType === 'specific' && !specificNode) { alert('Please select a specific node to update.'); return; } try { + // Disable deploy button during upload + const deployBtn = document.getElementById('deploy-btn'); + deployBtn.disabled = true; + deployBtn.classList.add('loading'); + deployBtn.textContent = '⏳ Deploying...'; + if (targetType === 'all') { await uploadFirmwareToAllNodes(file); } else { await uploadFirmwareToSpecificNode(file, specificNode); } + + // Reset interface after successful upload + fileInput.value = ''; + document.getElementById('file-info').textContent = 'No file selected'; + document.getElementById('file-info').classList.remove('has-file'); + } catch (error) { - console.error('Global firmware upload failed:', error); - alert(`Upload failed: ${error.message}`); + console.error('Firmware deployment failed:', error); + alert(`Deployment failed: ${error.message}`); + } finally { + // Re-enable deploy button + const deployBtn = document.getElementById('deploy-btn'); + deployBtn.disabled = false; + deployBtn.classList.remove('loading'); + deployBtn.textContent = '🚀 Deploy Firmware'; + updateDeployButton(); } - - // Clear file input - event.target.value = ''; } // Upload firmware to all nodes diff --git a/public/styles.css b/public/styles.css index d23bf86..903ba35 100644 --- a/public/styles.css +++ b/public/styles.css @@ -791,15 +791,15 @@ p { /* Firmware Actions */ .firmware-actions { display: grid; - grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); - gap: 2.5rem; - margin-bottom: 3rem; + grid-template-columns: 1fr; + gap: 1.5rem; + margin-bottom: 2.5rem; } .action-group { background: rgba(0, 0, 0, 0.2); - border-radius: 20px; - padding: 2rem; + border-radius: 16px; + padding: 1.5rem; border: 1px solid rgba(255, 255, 255, 0.06); position: relative; overflow: hidden; @@ -817,42 +817,261 @@ p { .action-group h3 { color: #ffffff; - margin-bottom: 1.5rem; - font-size: 1.2rem; + margin-bottom: 1.25rem; + font-size: 1.1rem; 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%); +/* Compact Firmware Upload Interface */ +.firmware-upload-compact { + display: flex; + flex-direction: column; + gap: 1.25rem; +} + +.upload-target-row { + display: flex; + gap: 1.5rem; + align-items: stretch; + position: relative; +} + +.upload-target-row::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 1px; + height: 60%; + background: linear-gradient(180deg, transparent, rgba(255, 255, 255, 0.1), transparent); + pointer-events: none; +} + +.upload-section { + flex: 1; + display: flex; + align-items: center; + gap: 0.75rem; + padding: 1.25rem; + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 10px; transition: all 0.3s ease; + min-height: 100px; } -.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-section:hover { + background: rgba(255, 255, 255, 0.05); + border-color: rgba(255, 255, 255, 0.15); } -.upload-btn-large { +.target-section { + flex: 1; + display: flex; + flex-direction: column; + gap: 0.75rem; + padding: 1.25rem; + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 10px; + transition: all 0.3s ease; + min-height: 100px; + justify-content: center; +} + +.target-section:hover { + background: rgba(255, 255, 255, 0.05); + border-color: rgba(255, 255, 255, 0.15); +} + +.file-input-wrapper { + display: flex; + align-items: center; + gap: 1rem; + flex: 1; +} + +.upload-btn-compact { 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; + padding: 0.6rem 1.25rem; + border-radius: 8px; cursor: pointer; - font-size: 1.1rem; + font-size: 0.9rem; + font-weight: 500; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + white-space: nowrap; +} + +.upload-btn-compact:hover { + background: linear-gradient(135deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0.12) 100%); + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); +} + +.file-info { + color: rgba(255, 255, 255, 0.7); + font-size: 0.85rem; + font-style: italic; + transition: all 0.3s ease; + padding: 0.4rem 0.6rem; + background: rgba(255, 255, 255, 0.05); + border-radius: 6px; + border: 1px solid transparent; +} + +.file-info.has-file { + color: #4ade80; + background: rgba(74, 222, 128, 0.1); + border-color: rgba(74, 222, 128, 0.2); + font-weight: 500; + font-style: normal; +} + +.target-options { + display: flex; + gap: 1.25rem; +} + +.target-option { + display: flex; + align-items: center; + gap: 0.4rem; + cursor: pointer; + padding: 0.4rem; + border-radius: 6px; + transition: all 0.2s ease; +} + +.target-option:hover { + background: rgba(255, 255, 255, 0.05); +} + +.target-option input[type="radio"] { + display: none; +} + +.radio-custom { + width: 16px; + height: 16px; + border: 2px solid rgba(255, 255, 255, 0.3); + border-radius: 50%; + position: relative; + transition: all 0.2s ease; + background: rgba(255, 255, 255, 0.05); +} + +.target-option input[type="radio"]:checked + .radio-custom { + border-color: #667eea; + background: #667eea; + box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2); +} + +.target-option input[type="radio"]:checked + .radio-custom::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 6px; + height: 6px; + background: white; + border-radius: 50%; + animation: radioPop 0.2s ease-out; +} + +@keyframes radioPop { + 0% { + transform: translate(-50%, -50%) scale(0); + opacity: 0; + } + 100% { + transform: translate(-50%, -50%) scale(1); + opacity: 1; + } +} + +.target-label { + color: rgba(255, 255, 255, 0.9); + font-size: 0.9rem; + font-weight: 500; +} + +.node-select { + background: linear-gradient(135deg, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0.7) 100%); + border: 1px solid rgba(255, 255, 255, 0.3); + color: #ffffff; + padding: 0.6rem 0.9rem; + border-radius: 8px; + font-size: 0.9rem; + transition: all 0.2s ease; + cursor: pointer; + min-width: 180px; + font-weight: 500; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8); +} + +.node-select:hover { + border-color: rgba(255, 255, 255, 0.4); + background: linear-gradient(135deg, rgba(0, 0, 0, 0.9) 0%, rgba(0, 0, 0, 0.8) 100%); +} + +.node-select:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2); + background: linear-gradient(135deg, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0.9) 100%); +} + +/* Style the dropdown options */ +.node-select option { + background: #1a202c; + color: #ffffff; + padding: 0.75rem 1rem; + font-size: 0.9rem; + font-weight: 500; + border: none; +} + +.node-select option:hover { + background: #2d3748; +} + +.node-select option:checked { + background: #667eea; + color: #ffffff; + font-weight: 600; +} + +/* Ensure the select field text is always visible */ +.node-select:invalid { + color: rgba(255, 255, 255, 0.8); +} + +.node-select:valid { + color: #ffffff; +} + +.deploy-btn { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border: none; + color: #ffffff; + padding: 0.875rem 1.75rem; + border-radius: 10px; + cursor: pointer; + font-size: 0.95rem; font-weight: 600; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - margin-bottom: 1.5rem; + align-self: center; + min-width: 180px; position: relative; overflow: hidden; } -.upload-btn-large::before { +.deploy-btn::before { content: ''; position: absolute; top: 0; @@ -863,245 +1082,148 @@ p { 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 { +.deploy-btn:hover:not(:disabled)::before { left: 100%; } -.upload-info-large { - font-size: 0.9rem; - color: rgba(255, 255, 255, 0.7); - line-height: 1.5; +.deploy-btn:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3); } -/* Target Selection */ -.target-selection { - display: flex; - flex-direction: column; - gap: 1.25rem; +.deploy-btn:disabled { + background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.05) 100%); + color: rgba(255, 255, 255, 0.4); + cursor: not-allowed; + transform: none; + box-shadow: none; } -.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; +.deploy-btn.loading { + background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%); + color: #1f2937; + cursor: not-allowed; } -.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.6) 0%, rgba(0, 0, 0, 0.5) 100%); - border: 1px solid rgba(255, 255, 255, 0.2); - color: #ffffff; - padding: 0.75rem 1rem; - border-radius: 10px; - margin-top: 0.75rem; - font-size: 0.95rem; - transition: all 0.2s ease; - cursor: pointer; - font-weight: 500; - text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); -} - -.target-selection select:hover { - border-color: rgba(255, 255, 255, 0.3); - background: linear-gradient(135deg, rgba(0, 0, 0, 0.7) 0%, rgba(0, 0, 0, 0.6) 100%); -} - -.target-selection select:focus { - outline: none; - border-color: #667eea; - box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); - background: linear-gradient(135deg, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0.7) 100%); -} - -/* Style the dropdown options */ -.target-selection select option { - background: #2c3e50; - color: #ffffff; - padding: 0.5rem; - font-size: 0.9rem; -} - -.target-selection select option:hover { - background: #34495e; -} - -.target-selection select option:checked { - background: #667eea; - color: #ffffff; -} - -/* Ensure the select field text is always visible */ -.target-selection select:invalid { - color: rgba(255, 255, 255, 0.7); -} - -.target-selection select:valid { - color: #ffffff; -} - -/* Text shadow already added to main select rule above */ - /* Responsive design for smaller screens */ @media (max-width: 768px) { - .header { - flex-direction: column; - gap: 1rem; - text-align: center; - } - - h1 { - font-size: 2rem; - } - - .status { - padding: 0.5rem 1rem; - font-size: 0.9rem; - } - - /* Mobile navigation */ - .main-navigation { - flex-direction: column; - gap: 1rem; - padding: 1rem; - } - - .nav-left { - flex-direction: column; - gap: 0.25rem; - } - - .nav-tab { - padding: 0.75rem 1rem; - font-size: 0.9rem; - border-radius: 10px; - text-align: center; - } - - .cluster-status { - padding: 0.5rem 1rem; - font-size: 0.85rem; - } - - /* 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; + gap: 1rem; } .action-group { - padding: 1.5rem; + padding: 1rem; } - .upload-area-large { - padding: 2rem 1.5rem; + .upload-target-row { + flex-direction: column; + gap: 1rem; } - .upload-btn-large { - padding: 1rem 2rem; + .upload-target-row::after { + display: none; + } + + .upload-section { + flex: none; + flex-direction: column; + align-items: stretch; + gap: 0.75rem; + padding: 1rem; + min-height: auto; + } + + .file-input-wrapper { + flex-direction: column; + align-items: stretch; + gap: 0.5rem; + } + + .upload-btn-compact { + width: 100%; + padding: 0.75rem; + font-size: 0.9rem; + } + + .target-section { + flex: none; + padding: 1rem; + min-height: auto; + } + + .target-options { + justify-content: center; + gap: 0.75rem; + } + + .node-select { + min-width: auto; + width: 100%; + padding: 0.5rem 0.75rem; + font-size: 0.9rem; + } + + .deploy-btn { + min-width: auto; + width: 100%; + padding: 1rem 1.5rem; font-size: 1rem; } - /* Mobile tab responsiveness */ - .tabs-header { - flex-wrap: wrap; - gap: 0.25rem; - padding-bottom: 0.5rem; + .file-info { + text-align: center; + padding: 0.5rem; + font-size: 0.85rem; } - .tab-button { - flex: 1 1 auto; - min-width: 0; - padding: 0.4rem 0.75rem; - font-size: 0.8rem; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + .target-label { + font-size: 0.9rem; } - .tabs-container { - padding: 0.25rem; - margin-top: 0.75rem; + .radio-custom { + width: 16px; + height: 16px; } - - .tab-content { - padding: 0.75rem 0; - } - - /* Mobile member card improvements */ - .member-card { - padding: 1rem; - margin-bottom: 0.75rem; - } - - .member-info { - gap: 0.25rem; - } - - .member-name { - font-size: 1.1rem; - } - - .member-ip { - font-size: 0.8rem; - } - - .member-latency { - margin-top: 0.4rem; - font-size: 0.8rem; - } -} +} /* Extra small screens */ @media (max-width: 480px) { - .tabs-header { - gap: 0.2rem; + .action-group { + padding: 0.75rem; } - .tab-button { - padding: 0.35rem 0.6rem; - font-size: 0.75rem; - border-radius: 6px 6px 0 0; + .upload-section, + .target-section { + padding: 0.75rem; } - .tabs-container { - padding: 0.2rem; - margin-top: 0.5rem; + .upload-btn-compact { + padding: 0.6rem; + font-size: 0.85rem; + } + + .deploy-btn { + padding: 0.75rem 1.25rem; + font-size: 0.95rem; + } + + .file-info { + padding: 0.4rem; + font-size: 0.8rem; + } + + .target-label { + font-size: 0.85rem; + } + + .radio-custom { + width: 14px; + height: 14px; + } + + .node-select { + padding: 0.4rem 0.6rem; + font-size: 0.85rem; } }