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
-
-
-
-
-
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;
}
}