// Frontend API client - calls our Express backend class FrontendApiClient { constructor() { this.baseUrl = ''; // Same origin as the current page } async getClusterMembers() { try { const response = await fetch('/api/cluster/members', { 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}`); } } 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 const client = new FrontendApiClient(); // Function to refresh cluster members async function refreshClusterMembers() { const container = document.getElementById('cluster-members-container'); // Show loading state container.innerHTML = `
Loading cluster members...
`; try { const response = await client.getClusterMembers(); console.log(response); displayClusterMembers(response.members); } catch (error) { console.error('Failed to fetch cluster members:', error); container.innerHTML = `
Error loading cluster members:
${error.message}
`; } } // 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) { console.log('Displaying node details in container:', container); 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
'}
`; console.log('Node details HTML set successfully'); } // Function to display cluster members function displayClusterMembers(members) { const container = document.getElementById('cluster-members-container'); if (!members || members.length === 0) { container.innerHTML = `
🌐
No cluster members found
The cluster might be empty or not yet discovered
`; return; } const membersHTML = members.map(member => { const statusClass = member.status === 'active' ? 'status-online' : 'status-offline'; const statusText = member.status === 'active' ? 'Online' : 'Offline'; const statusIcon = member.status === 'active' ? '🟢' : '🔴'; return `
${member.hostname || 'Unknown Device'}
${member.ip || 'No IP'}
${statusIcon} ${statusText}
▶️
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}`); // Make the entire card clickable card.addEventListener('click', async (e) => { // Don't trigger if clicking on the expand icon (to avoid double-triggering) if (e.target === expandIcon) { return; } console.log('Card 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'); } }); // Keep the expand icon click handler for visual feedback 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 expand icon on card ${index}`); } else { console.error(`No expand icon found for card ${index}`); } console.log(`Event listener added for card ${index}`); }); }, 100); } // Load cluster members when page loads document.addEventListener('DOMContentLoaded', function() { refreshClusterMembers(); }); // Auto-refresh every 30 seconds setInterval(refreshClusterMembers, 30000);