// Node Discovery Component class NodeDiscovery extends Component { constructor(container, viewModel, eventBus) { super(container, viewModel, eventBus); this.nodes = []; this.currentTarget = null; } mount() { super.mount(); this.setupEventListeners(); this.setupViewModelListeners(); this.loadNodes(); this.startPeriodicRefresh(); } setupEventListeners() { // Broadcast button const broadcastBtn = this.findElement('#broadcast-btn'); if (broadcastBtn) { this.addEventListener(broadcastBtn, 'click', () => { this.selectBroadcastTarget(); }); } } setupViewModelListeners() { this.subscribeToEvent('nodeDiscovered', (data) => { this.addOrUpdateNode(data.node); }); this.subscribeToEvent('nodeLost', (data) => { this.removeNode(data.node.ip); }); this.subscribeToEvent('status', (data) => { // Update UI to reflect current server state if (data.data.nodes) { this.nodes = data.data.nodes; this.currentTarget = data.data.currentTarget; this.renderNodeList(); } }); } async loadNodes() { try { const response = await fetch('/api/nodes'); const data = await response.json(); this.nodes = data.nodes || []; this.renderNodeList(); } catch (error) { console.error('Error loading nodes:', error); this.showError('Failed to load nodes'); } } startPeriodicRefresh() { // Refresh node list every 5 seconds setInterval(() => { this.loadNodes(); }, 5000); } addOrUpdateNode(node) { const existingIndex = this.nodes.findIndex(n => n.ip === node.ip); if (existingIndex >= 0) { // Update existing node this.nodes[existingIndex] = { ...node, lastSeen: Date.now() }; } else { // Add new node this.nodes.push({ ...node, lastSeen: Date.now() }); } this.renderNodeList(); } removeNode(nodeIp) { this.nodes = this.nodes.filter(node => node.ip !== nodeIp); this.renderNodeList(); } renderNodeList() { const nodeListContainer = this.findElement('#node-list'); if (!nodeListContainer) return; if (this.nodes.length === 0) { nodeListContainer.innerHTML = '
No nodes discovered
'; return; } const html = this.nodes.map(node => `
${this.escapeHtml(node.ip === 'broadcast' ? 'Broadcast' : node.ip)}
${node.status} • Port ${node.port}
`).join(''); nodeListContainer.innerHTML = html; // Add click handlers for node selection this.nodes.forEach(node => { const nodeElement = nodeListContainer.querySelector(`[data-ip="${node.ip}"]`); if (nodeElement) { this.addEventListener(nodeElement, 'click', () => { this.selectNode(node.ip); }); } }); } selectNode(nodeIp) { this.currentTarget = nodeIp; this.viewModel.publish('selectNode', { nodeIp }); // Update visual selection const nodeListContainer = this.findElement('#node-list'); if (nodeListContainer) { nodeListContainer.querySelectorAll('.node-item').forEach(item => { item.classList.remove('selected'); }); const selectedNode = nodeListContainer.querySelector(`[data-ip="${nodeIp}"]`); if (selectedNode) { selectedNode.classList.add('selected'); } } } selectBroadcast() { this.currentTarget = 'broadcast'; this.viewModel.publish('selectBroadcast', {}); // Update visual selection const nodeListContainer = this.findElement('#node-list'); if (nodeListContainer) { nodeListContainer.querySelectorAll('.node-item').forEach(item => { item.classList.remove('selected'); }); const broadcastNode = nodeListContainer.querySelector(`[data-ip="broadcast"]`); if (broadcastNode) { broadcastNode.classList.add('selected'); } } } showError(message) { const nodeListContainer = this.findElement('#node-list'); if (nodeListContainer) { nodeListContainer.innerHTML = `
${this.escapeHtml(message)}
`; } } // Public method to select broadcast (called from outside) selectBroadcastTarget() { this.selectBroadcast(); } escapeHtml(str) { return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } } // Export for use in other modules if (typeof module !== 'undefined' && module.exports) { module.exports = NodeDiscovery; }