Files
spore-ledlab/public/scripts/node-discovery.js
2025-10-11 17:46:32 +02:00

179 lines
5.5 KiB
JavaScript

// 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 = '<div class="empty-state">No nodes discovered</div>';
return;
}
const html = this.nodes.map(node => `
<div class="node-item ${node.status} ${node.ip === this.currentTarget ? 'selected' : ''}" data-ip="${this.escapeHtml(node.ip)}" style="cursor: pointer;">
<div class="node-indicator ${node.status}"></div>
<div class="node-info">
<div class="node-ip">${this.escapeHtml(node.ip === 'broadcast' ? 'Broadcast' : node.ip)}</div>
<div class="node-status">${node.status} • Port ${node.port}</div>
</div>
</div>
`).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 = `<div class="error">${this.escapeHtml(message)}</div>`;
}
}
// Public method to select broadcast (called from outside)
selectBroadcastTarget() {
this.selectBroadcast();
}
escapeHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
}
// Export for use in other modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = NodeDiscovery;
}