From a437fb0eea92e4552bc845c87c2b08f5a7e0c38c Mon Sep 17 00:00:00 2001 From: Patrick Balsiger Date: Sun, 24 Aug 2025 21:20:18 +0200 Subject: [PATCH] feat: show member details --- README.md | 3 +- index.js | 19 ++++ public/index.html | 239 +++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 249 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index b753462..780cc50 100644 --- a/README.md +++ b/README.md @@ -4,4 +4,5 @@ - [x] bootstrap an express.js app that serves a simple html page - [x] generate js client from api/openapi.yaml -- [x] use getClusterStatus client function to get all members and display these members on the html page, use only vanilla JS \ No newline at end of file +- [x] use getClusterStatus client function to get all members and display these members on the html page, use only vanilla JS +- [x] when clicking on one of the members in the UI, it should expand and display all informations from /api/node/status \ No newline at end of file diff --git a/index.js b/index.js index c35220f..5ef6eaf 100644 --- a/index.js +++ b/index.js @@ -58,6 +58,25 @@ app.get('/api/node/status', async (req, res) => { } }); +// Proxy endpoint to get status from a specific node +app.get('/api/node/status/:ip', async (req, res) => { + try { + const nodeIp = req.params.ip; + + // Create a temporary client for the specific node + const nodeClient = new SporeApiClient(`http://${nodeIp}`); + const nodeStatus = await nodeClient.getSystemStatus(); + + res.json(nodeStatus); + } catch (error) { + console.error(`Error fetching status from node ${req.params.ip}:`, error); + res.status(500).json({ + error: `Failed to fetch status from node ${req.params.ip}`, + message: error.message + }); + } +}); + // Start the server app.listen(PORT, () => { console.log(`Server is running on http://localhost:${PORT}`); diff --git a/public/index.html b/public/index.html index 5efbe6c..31d3c98 100644 --- a/public/index.html +++ b/public/index.html @@ -105,6 +105,7 @@ border-radius: 12px; padding: 1.5rem; transition: all 0.3s ease; + cursor: pointer; } .member-card:hover { @@ -112,6 +113,102 @@ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2); } + .member-card.expanded { + background: rgba(255, 255, 255, 0.15); + border-color: rgba(255, 255, 255, 0.4); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); + transform: scale(1.02); + } + + .member-card.expanded:hover { + transform: none; + } + + .member-card.expanded .expand-icon { + transform: rotate(180deg); + } + + .member-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 1rem; + } + + .member-info { + flex: 1; + } + + .expand-icon { + font-size: 1.2rem; + opacity: 0.7; + transition: transform 0.3s ease; + } + + .member-card.expanded .expand-icon { + transform: rotate(180deg); + } + + .member-details { + display: none; + opacity: 0; + margin-top: 0; + padding-top: 0; + border-top: 1px solid transparent; + transition: opacity 0.3s ease; + } + + .member-card.expanded .member-details { + display: block; + opacity: 1; + margin-top: 1rem; + padding-top: 1rem; + border-top: 1px solid rgba(255, 255, 255, 0.2); + } + + .detail-row { + display: flex; + justify-content: space-between; + margin-bottom: 0.5rem; + font-size: 0.9rem; + } + + .detail-label { + opacity: 0.7; + font-weight: 500; + } + + .detail-value { + font-family: 'Courier New', monospace; + opacity: 0.9; + } + + .api-endpoints { + margin-top: 1rem; + } + + .api-endpoints h4 { + margin-bottom: 0.5rem; + font-size: 0.9rem; + opacity: 0.8; + } + + .endpoint-item { + background: rgba(255, 255, 255, 0.1); + padding: 0.5rem; + border-radius: 6px; + margin-bottom: 0.5rem; + font-size: 0.8rem; + font-family: 'Courier New', monospace; + } + + .loading-details { + text-align: center; + padding: 1rem; + opacity: 0.7; + font-size: 0.9rem; + } + .member-name { font-size: 1.2rem; font-weight: 600; @@ -196,6 +293,7 @@
Loading cluster members...
+ @@ -224,6 +322,26 @@ throw new Error(`Request failed: ${error.message}`); } } + + async getNodeStatus(ip) { + try { + // Create a proxy endpoint that forwards the request to the specific node + const response = await fetch(`/api/node/status/${encodeURIComponent(ip)}`, { + 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 @@ -255,6 +373,60 @@ } } + // Function to load detailed node information + async function loadNodeDetails(card, memberIp) { + console.log('Loading node details for IP:', memberIp); + const memberDetails = card.querySelector('.member-details'); + console.log('Member details element:', memberDetails); + + try { + console.log('Fetching node status...'); + const nodeStatus = await client.getNodeStatus(memberIp); + console.log('Node status received:', nodeStatus); + displayNodeDetails(memberDetails, nodeStatus); + } catch (error) { + console.error('Failed to load node details:', error); + memberDetails.innerHTML = ` +
+ Error loading node details:
+ ${error.message} +
+ `; + } + } + + // Function to display node details + function displayNodeDetails(container, 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
'} +
+ `; + } + // Function to display cluster members function displayClusterMembers(members) { const container = document.getElementById('cluster-members-container'); @@ -278,29 +450,74 @@ const statusIcon = member.status === 'active' ? '🟢' : '🔴'; return ` -
-
${member.hostname || 'Unknown Device'}
-
${member.ip || 'No IP'}
-
- ${statusIcon} ${statusText} +
+
+
+
${member.hostname || 'Unknown Device'}
+
${member.ip || 'No IP'}
+
+ ${statusIcon} ${statusText} +
+
+
▶️
-
- Latency: ${member.latency}ms | Heap: ${Math.round(member.resources?.freeHeap / 1024)}KB +
+
Loading detailed information...
`; }).join(''); container.innerHTML = membersHTML; - } + // Add event listeners for expand/collapse + console.log('Setting up event listeners for', members.length, 'member cards'); + + // Small delay to ensure DOM is ready + setTimeout(() => { + document.querySelectorAll('.member-card').forEach((card, index) => { + const expandIcon = card.querySelector('.expand-icon'); + const memberDetails = card.querySelector('.member-details'); + const memberIp = card.dataset.memberIp; + + console.log(`Setting up card ${index} with IP: ${memberIp}`); + + if (expandIcon) { + expandIcon.addEventListener('click', async (e) => { + e.stopPropagation(); + console.log('Expand icon clicked for IP:', memberIp); + + const isExpanding = !card.classList.contains('expanded'); + console.log('Is expanding:', isExpanding); + + if (isExpanding) { + // Expanding - fetch detailed status + console.log('Starting to expand...'); + await loadNodeDetails(card, memberIp); + card.classList.add('expanded'); + expandIcon.classList.add('expanded'); + console.log('Card expanded successfully'); + } else { + // Collapsing + console.log('Collapsing...'); + card.classList.remove('expanded'); + expandIcon.classList.remove('expanded'); + console.log('Card collapsed successfully'); + } + }); + + console.log(`Event listener added for card ${index}`); + } else { + console.error(`No expand icon found for card ${index}`); + } + }); + }, 100); + } + // Load cluster members when page loads document.addEventListener('DOMContentLoaded', function() { refreshClusterMembers(); }); - - // Auto-refresh every 30 seconds - setInterval(refreshClusterMembers, 30000); \ No newline at end of file