Perfect. Let’s finish this so your dashboard actually looks like it was designed by a human instead of a CLI zombie. We’ll make DeviceList.jsx + DeviceCard.jsx fully functional with:
49f26b8 verified | // WebSocket connection | |
| let socket; | |
| const syncStatus = document.getElementById('sync-status'); | |
| function updateSyncStatus(message) { | |
| const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19); | |
| syncStatus.innerHTML += `<div class="mb-1"><span class="text-secondary-500">[${timestamp}]</span> ${message}</div>`; | |
| syncStatus.scrollTop = syncStatus.scrollHeight; | |
| } | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // Initialize WebSocket | |
| function connectWebSocket() { | |
| socket = new WebSocket('wss://your-websocket-server.com/ble-mesh'); | |
| socket.onopen = () => { | |
| updateSyncStatus('WebSocket connected'); | |
| }; | |
| socket.onmessage = (event) => { | |
| updateSyncStatus(`Received data: ${event.data}`); | |
| // Handle incoming sync data | |
| }; | |
| socket.onclose = () => { | |
| updateSyncStatus('WebSocket disconnected - attempting reconnect...'); | |
| setTimeout(connectWebSocket, 5000); | |
| }; | |
| } | |
| connectWebSocket(); | |
| // Enhanced device data with metrics | |
| const devices = [ | |
| { | |
| id: 'node-042', | |
| type: 'ESP32 Mesh Node', | |
| status: 'active', | |
| lastSeen: new Date().toISOString(), | |
| lastRtt: 24, | |
| aggregated: { | |
| summary: 'Stable connection, low latency', | |
| metrics: { | |
| health: 'ok', | |
| packetLoss: '0.2%', | |
| uptime: '99.8%' | |
| } | |
| } | |
| }, | |
| { | |
| id: 'imu-015', | |
| type: 'IMU Sensor', | |
| status: 'active', | |
| lastSeen: new Date(Date.now() - 120000).toISOString(), | |
| lastRtt: 58, | |
| aggregated: { | |
| summary: 'Higher than usual latency', | |
| metrics: { | |
| health: 'warn', | |
| packetLoss: '1.8%', | |
| uptime: '98.1%' | |
| } | |
| } | |
| }, | |
| { | |
| id: 'proxy-007', | |
| type: 'GATT Proxy', | |
| status: 'inactive', | |
| lastSeen: new Date(Date.now() - 900000).toISOString(), | |
| lastRtt: null, | |
| aggregated: { | |
| summary: 'Device unreachable', | |
| metrics: { | |
| health: 'critical', | |
| packetLoss: '100%', | |
| uptime: '0%' | |
| } | |
| } | |
| }, | |
| { | |
| id: 'gateway-001', | |
| type: 'Manabox Gateway', | |
| status: 'active', | |
| lastSeen: new Date().toISOString(), | |
| lastRtt: 32, | |
| aggregated: { | |
| summary: 'Normal operation', | |
| metrics: { | |
| health: 'ok', | |
| packetLoss: '0.1%', | |
| uptime: '99.9%' | |
| } | |
| } | |
| }, | |
| ]; | |
| // Populate device table | |
| const tableBody = document.getElementById('device-table-body'); | |
| devices.forEach(device => { | |
| const row = document.createElement('tr'); | |
| row.className = 'border-b border-stone-700 hover:bg-stone-700/50 transition-colors'; | |
| const health = device.aggregated?.metrics?.health || ''; | |
| const statusClass = health === 'ok' ? 'text-green-400' : | |
| health === 'warn' ? 'text-amber-400' : 'text-red-400'; | |
| row.innerHTML = ` | |
| <td class="py-3 px-4 font-mono">${device.id}</td> | |
| <td class="py-3 px-4">${device.type}</td> | |
| <td class="py-3 px-4 ${statusClass} flex items-center gap-2"> | |
| <span class="h-2 w-2 rounded-full ${health === 'ok' ? 'bg-green-400' : | |
| health === 'warn' ? 'bg-amber-400' : 'bg-red-400'}"></span> | |
| ${device.status} | |
| </td> | |
| <td class="py-3 px-4">${new Date(device.lastSeen).toLocaleTimeString()}</td> | |
| <td class="py-3 px-4">${device.lastRtt ? device.lastRtt + 'ms' : '-'}</td> | |
| `; | |
| tableBody.appendChild(row); | |
| }); | |
| // Telemetry stream simulation | |
| const telemetryStream = document.getElementById('telemetry-stream'); | |
| const startBtn = document.getElementById('start-stream'); | |
| const clearBtn = document.getElementById('clear-stream'); | |
| let streamInterval; | |
| // Bluetooth sync handler | |
| document.getElementById('sync-bluetooth').addEventListener('click', async () => { | |
| updateSyncStatus('Initiating BLE Mesh sync...'); | |
| try { | |
| // Mock BLE sync - in real implementation this would use Web Bluetooth API | |
| updateSyncStatus('Discovering nearby mesh nodes...'); | |
| await new Promise(resolve => setTimeout(resolve, 1000)); | |
| updateSyncStatus('Connected to 3 mesh nodes'); | |
| await new Promise(resolve => setTimeout(resolve, 500)); | |
| updateSyncStatus('Syncing configuration data...'); | |
| await new Promise(resolve => setTimeout(resolve, 1500)); | |
| updateSyncStatus('Sync complete! Updated 12 devices'); | |
| } catch (error) { | |
| updateSyncStatus(`BLE Sync failed: ${error.message}`); | |
| } | |
| }); | |
| // WebSocket sync handler | |
| document.getElementById('sync-websocket').addEventListener('click', () => { | |
| if (socket.readyState === WebSocket.OPEN) { | |
| updateSyncStatus('Sending sync request via WebSocket...'); | |
| socket.send(JSON.stringify({ | |
| type: 'sync_request', | |
| devices: devices // Send current device state | |
| })); | |
| } else { | |
| updateSyncStatus('WebSocket not connected - attempting reconnect'); | |
| connectWebSocket(); | |
| } | |
| }); | |
| // Data export handler | |
| document.getElementById('export-data').addEventListener('click', () => { | |
| const config = { | |
| timestamp: new Date().toISOString(), | |
| devices: devices, | |
| settings: { | |
| meshId: 'nexus-glow-001', | |
| networkKey: '********', | |
| appKey: '********' | |
| } | |
| }; | |
| const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `ble-mesh-config-${new Date().toISOString().split('T')[0]}.json`; | |
| a.click(); | |
| updateSyncStatus('Configuration exported as JSON'); | |
| }); | |
| startBtn.addEventListener('click', function() { | |
| if (streamInterval) { | |
| clearInterval(streamInterval); | |
| startBtn.innerHTML = '<i data-feather="play"></i> Start Stream'; | |
| feather.replace(); | |
| return; | |
| } | |
| startBtn.innerHTML = '<i data-feather="pause"></i> Stop Stream'; | |
| feather.replace(); | |
| streamInterval = setInterval(() => { | |
| const now = new Date(); | |
| const timestamp = now.toISOString().replace('T', ' ').substring(0, 19); | |
| const telemetryData = { | |
| timestamp, | |
| device_id: 'node-' + Math.floor(Math.random() * 100).toString().padStart(3, '0'), | |
| accel: { | |
| x: (Math.random() * 2 - 1).toFixed(2), | |
| y: (Math.random() * 2 - 1).toFixed(2), | |
| z: (9.8 + Math.random() * 0.2 - 0.1).toFixed(2) | |
| }, | |
| buttons: { | |
| A: Math.random() > 0.7 ? 1 : 0, | |
| B: Math.random() > 0.8 ? 1 : 0, | |
| Guard: Math.random() > 0.9 ? 1 : 0 | |
| } | |
| }; | |
| const entry = document.createElement('div'); | |
| entry.className = 'mb-2 p-3 bg-stone-700/50 rounded-lg font-mono text-sm'; | |
| entry.innerHTML = `<span class="text-secondary-500">${timestamp}</span> ${JSON.stringify(telemetryData)}`; | |
| telemetryStream.appendChild(entry); | |
| // Auto-scroll to bottom | |
| telemetryStream.scrollTop = telemetryStream.scrollHeight; | |
| }, 1000); | |
| }); | |
| clearBtn.addEventListener('click', function() { | |
| telemetryStream.innerHTML = ''; | |
| }); | |
| }); |