document.addEventListener('DOMContentLoaded', () => { const nodeGridContainer = document.getElementById('node-grid-container'); const nodeCountSpan = document.getElementById('node-count'); const POLLING_INTERVAL_MS = 3000; // Poll every 3 seconds async function fetchNodeData() { try { // Use window.API_BASE_PATH for dynamic base URL const response = await fetch(`${window.API_BASE_PATH}/nodes/status`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); renderNodeGrid(data.nodes); } catch (error) { console.error("Error fetching node data:", error); nodeGridContainer.innerHTML = '

Error loading node data. Please check server connection.

'; } } function renderNodeGrid(nodes) { nodeGridContainer.innerHTML = ''; // Clear existing content nodeCountSpan.textContent = nodes.length; // Update total node count if (nodes.length === 0) { nodeGridContainer.innerHTML = '

No nodes reporting yet. Start a client!

'; return; } // Create a dynamic grid container const grid = document.createElement('div'); grid.classList.add('connection-grid'); // Dynamically set grid columns based on number of nodes + 1 for the header column // minmax(100px, 1fr) for the row header, then repeat for each node column grid.style.gridTemplateColumns = `minmax(100px, 1fr) repeat(${nodes.length}, minmax(100px, 1fr))`; // Add header row for column UUIDs const headerRow = document.createElement('div'); headerRow.classList.add('grid-row', 'header-row'); headerRow.appendChild(createEmptyCell()); // Top-left corner nodes.forEach(node => { const headerCell = document.createElement('div'); headerCell.classList.add('grid-cell', 'header-cell'); const displayUuid = node.uuid.substring(0, 8) + '...'; headerCell.innerHTML = `
${displayUuid}
`; headerRow.appendChild(headerCell); }); grid.appendChild(headerRow); // Create rows for the grid nodes.forEach((rowNode, rowIndex) => { const row = document.createElement('div'); row.classList.add('grid-row'); // Add row header (UUID) const rowHeader = document.createElement('div'); rowHeader.classList.add('grid-cell', 'header-cell'); const displayUuid = rowNode.uuid.substring(0, 8) + '...'; rowHeader.innerHTML = `
${displayUuid}
`; row.appendChild(rowHeader); // Add cells for connections nodes.forEach((colNode, colIndex) => { const cell = document.createElement('div'); cell.classList.add('grid-cell'); if (rowIndex === colIndex) { // Diagonal: show node health status cell.classList.add(`node-${rowNode.health_status}`); cell.innerHTML = `
Status: ${rowNode.health_status.toUpperCase()}

UUID: ${rowNode.uuid}

IP: ${rowNode.ip}

Last Seen: ${new Date(rowNode.last_seen).toLocaleTimeString()}

Uptime: ${rowNode.uptime_seconds =! null ? formatUptime(rowNode.uptime_seconds) : 'N/A'}

Load Avg (1m, 5m, 15m): ${ Array.isArray(rowNode.load_avg) ? rowNode.load_avg.map(l => l.toFixed(2)).join(', ') : 'N/A' }

Memory Usage: ${ rowNode.memory_usage_percent != null ? rowNode.memory_usage_percent.toFixed(2) + '%' : 'N/A' }

`; } else { // Off-diagonal: show ping latency const latency = ( rowNode.connections && colNode.uuid in rowNode.connections && rowNode.connections[colNode.uuid] !== null ) ? rowNode.connections[colNode.uuid] : null; const displayLatency = (latency != null && !isNaN(latency)) ? `${latency.toFixed(1)} ms` : 'N/A'; const latencyClass = (latency != null && !isNaN(latency)) ? getLatencyClass(latency) : 'latency-unavailable'; cell.classList.add(latencyClass); cell.innerHTML = `
Ping: ${displayLatency}

From: ${rowNode.uuid.substring(0, 8)}...

to: ${colNode.uuid.substring(0, 8)}...

Latency: ${displayLatency}

`; } row.appendChild(cell); }); grid.appendChild(row); }); nodeGridContainer.appendChild(grid); } function getLatencyClass(latency) { if (latency <= 200) return 'latency-low'; if (latency <= 1000) return 'latency-medium'; return 'latency-high'; } function createEmptyCell() { const cell = document.createElement('div'); cell.classList.add('grid-cell', 'empty-cell'); return cell; } function formatUptime(seconds) { const days = Math.floor(seconds / (3600 * 24)); seconds %= (3600 * 24); const hours = Math.floor(seconds / 3600); seconds %= 3600; const minutes = Math.floor(seconds / 60); const remainingSeconds = seconds % 60; let parts = []; if (days > 0) parts.push(`${days}d`); if (hours > 0) parts.push(`${hours}h`); if (minutes > 0) parts.push(`${minutes}m`); if (remainingSeconds > 0 || parts.length === 0) parts.push(`${remainingSeconds}s`); return parts.join(' '); } // Initial fetch and then set up polling fetchNodeData(); setInterval(fetchNodeData, POLLING_INTERVAL_MS); });