// 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 `
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);