feat: ledlab
This commit is contained in:
178
public/scripts/node-discovery.js
Normal file
178
public/scripts/node-discovery.js
Normal file
@@ -0,0 +1,178 @@
|
||||
// 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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = NodeDiscovery;
|
||||
}
|
||||
Reference in New Issue
Block a user