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