From 0390b714a2c0c61f8e240f29edfa9c47428d2357 Mon Sep 17 00:00:00 2001 From: Patrick Balsiger Date: Mon, 25 Aug 2025 08:31:52 +0200 Subject: [PATCH] feat: introduce tabs in member cards --- public/script.js | 199 ++++++++++++++++++++++++++++++++++++++++------ public/styles.css | 148 ++++++++++++++++++++++++++++++++++ 2 files changed, 322 insertions(+), 25 deletions(-) diff --git a/public/script.js b/public/script.js index 04af788..9d0db02 100644 --- a/public/script.js +++ b/public/script.js @@ -42,6 +42,25 @@ class FrontendApiClient { throw new Error(`Request failed: ${error.message}`); } } + + async getTasksStatus() { + try { + const response = await fetch('/api/tasks/status', { + method: 'GET', + headers: { + 'Accept': 'application/json' + } + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + return await response.json(); + } catch (error) { + throw new Error(`Request failed: ${error.message}`); + } + } } // Global client instance @@ -101,37 +120,167 @@ function displayNodeDetails(container, nodeStatus) { console.log('Node status data:', nodeStatus); container.innerHTML = ` -
- Free Heap: - ${Math.round(nodeStatus.freeHeap / 1024)}KB -
-
- Chip ID: - ${nodeStatus.chipId} -
-
- SDK Version: - ${nodeStatus.sdkVersion} -
-
- CPU Frequency: - ${nodeStatus.cpuFreqMHz}MHz -
-
- Flash Size: - ${Math.round(nodeStatus.flashChipSize / 1024)}KB -
-
-

Available API Endpoints:

- ${nodeStatus.api ? nodeStatus.api.map(endpoint => - `
${endpoint.method === 1 ? 'GET' : 'POST'} ${endpoint.uri}
` - ).join('') : '
No API endpoints available
'} +
+
+ + + + +
+ +
+
+ Free Heap: + ${Math.round(nodeStatus.freeHeap / 1024)}KB +
+
+ Chip ID: + ${nodeStatus.chipId} +
+
+ SDK Version: + ${nodeStatus.sdkVersion} +
+
+ CPU Frequency: + ${nodeStatus.cpuFreqMHz}MHz +
+
+ Flash Size: + ${Math.round(nodeStatus.flashChipSize / 1024)}KB +
+
+ +
+

Available API Endpoints:

+ ${nodeStatus.api ? nodeStatus.api.map(endpoint => + `
${endpoint.method === 1 ? 'GET' : 'POST'} ${endpoint.uri}
` + ).join('') : '
No API endpoints available
'} +
+ +
+
Loading tasks...
+
+ +
+
+

Firmware Update

+
+ + +
Select a .bin or .hex file to upload
+
+
+
`; + // Set up tab switching + setupTabs(container); + + // Load tasks data for the tasks tab + loadTasksData(container, nodeStatus); + console.log('Node details HTML set successfully'); } +// Function to set up tab switching +function setupTabs(container) { + const tabButtons = container.querySelectorAll('.tab-button'); + const tabContents = container.querySelectorAll('.tab-content'); + + tabButtons.forEach(button => { + button.addEventListener('click', (e) => { + // Prevent the click event from bubbling up to the card + e.stopPropagation(); + + const targetTab = button.dataset.tab; + + // Remove active class from all buttons and contents + tabButtons.forEach(btn => btn.classList.remove('active')); + tabContents.forEach(content => content.classList.remove('active')); + + // Add active class to clicked button and corresponding content + button.classList.add('active'); + const targetContent = container.querySelector(`#${targetTab}-tab`); + if (targetContent) { + targetContent.classList.add('active'); + } + }); + }); + + // Also prevent event propagation on tab content areas + tabContents.forEach(content => { + content.addEventListener('click', (e) => { + e.stopPropagation(); + }); + }); + + // Set up firmware upload button + const uploadBtn = container.querySelector('.upload-btn[data-action="select-file"]'); + if (uploadBtn) { + uploadBtn.addEventListener('click', (e) => { + e.stopPropagation(); + const fileInput = container.querySelector('#firmware-file'); + if (fileInput) { + fileInput.click(); + } + }); + } +} + +// Function to load tasks data +async function loadTasksData(container, nodeStatus) { + const tasksTab = container.querySelector('#tasks-tab'); + if (!tasksTab) return; + + try { + const response = await client.getTasksStatus(); + console.log('Tasks data received:', response); + + if (response && response.length > 0) { + const tasksHTML = response.map(task => ` +
+
+ ${task.name || 'Unknown Task'} + + ${task.running ? '🟢 Running' : '🔴 Stopped'} + +
+
+ Interval: ${task.interval}ms + ${task.enabled ? '🟢 Enabled' : '🔴 Disabled'} +
+
+ `).join(''); + + tasksTab.innerHTML = ` +

Active Tasks

+ ${tasksHTML} + `; + } else { + tasksTab.innerHTML = ` +
+
📋 No active tasks found
+
+ This node has no running tasks +
+
+ `; + } + } catch (error) { + console.error('Failed to load tasks:', error); + tasksTab.innerHTML = ` +
+ Error loading tasks:
+ ${error.message} +
+ `; + } +} + // Function to display cluster members function displayClusterMembers(members) { const container = document.getElementById('cluster-members-container'); diff --git a/public/styles.css b/public/styles.css index 7c3614b..bc26dbe 100644 --- a/public/styles.css +++ b/public/styles.css @@ -283,6 +283,154 @@ p { border: 1px solid rgba(255, 255, 255, 0.1); } +/* Tab Styles */ +.tabs-container { + margin-top: 1rem; +} + +.tabs-header { + display: flex; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + margin-bottom: 1rem; + gap: 0.5rem; +} + +.tab-button { + background: rgba(0, 0, 0, 0.3); + border: 1px solid rgba(255, 255, 255, 0.1); + color: rgba(255, 255, 255, 0.7); + padding: 0.5rem 1rem; + border-radius: 8px 8px 0 0; + cursor: pointer; + font-size: 0.9rem; + transition: all 0.3s ease; + border-bottom: none; +} + +.tab-button:hover { + background: rgba(0, 0, 0, 0.5); + color: rgba(255, 255, 255, 0.9); +} + +.tab-button.active { + background: rgba(255, 255, 255, 0.1); + color: #ecf0f1; + border-color: rgba(255, 255, 255, 0.2); +} + +.tab-content { + display: none; + padding: 1rem 0; +} + +.tab-content.active { + display: block; +} + +/* Task Styles */ +.task-item { + background: rgba(0, 0, 0, 0.2); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + padding: 1rem; + margin-bottom: 0.75rem; +} + +.task-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; +} + +.task-name { + font-weight: 600; + color: #ecf0f1; +} + +.task-status { + font-size: 0.85rem; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-weight: 500; +} + +.task-status.running { + background: rgba(76, 175, 80, 0.2); + color: #4caf50; + border: 1px solid rgba(76, 175, 80, 0.3); +} + +.task-status.stopped { + background: rgba(244, 67, 54, 0.2); + color: #f44336; + border: 1px solid rgba(244, 67, 54, 0.3); +} + +.task-details { + display: flex; + gap: 1rem; + font-size: 0.8rem; + opacity: 0.8; +} + +.task-interval, .task-enabled { + background: rgba(0, 0, 0, 0.2); + padding: 0.2rem 0.5rem; + border-radius: 4px; + border: 1px solid rgba(255, 255, 255, 0.05); +} + +/* Firmware Upload Styles */ +.firmware-upload h4 { + margin-bottom: 1rem; + color: #ecf0f1; +} + +.upload-area { + text-align: center; + padding: 2rem; + border: 2px dashed rgba(255, 255, 255, 0.2); + border-radius: 12px; + background: rgba(0, 0, 0, 0.2); +} + +.upload-btn { + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + color: #ecf0f1; + padding: 0.75rem 1.5rem; + border-radius: 8px; + cursor: pointer; + font-size: 1rem; + transition: all 0.3s ease; + margin-bottom: 1rem; +} + +.upload-btn:hover { + background: rgba(255, 255, 255, 0.2); + transform: translateY(-2px); +} + +.upload-info { + font-size: 0.9rem; + opacity: 0.7; + color: rgba(255, 255, 255, 0.8); +} + +.no-tasks { + text-align: center; + padding: 2rem; + opacity: 0.7; +} + +.loading-tasks { + text-align: center; + padding: 1rem; + opacity: 0.7; + font-style: italic; +} + .loading { text-align: center; padding: 2rem;