// =============================== // SDL Dashboard MQTT Client // =============================== import { getConfig } from './weblib.js'; const config = await getConfig(); // console.log('config:', JSON.stringify(config, null, 2)); let mqttClient = null; let mqttConfig = null; let sdlConfig = null; let lastSdlStatus = null; let lastSdlTimestamp = null; let statusCheckTimer = null; const STALE_GRACE_MS = 2000; let lastWorkers = null; // ------------------------------- // GPU Summary (for dashboard) // ------------------------------- let gpuSummary = 'None'; if (Array.isArray(config.host.gpu) && config.host.gpu.length > 0) { gpuSummary = config.host.gpu.map(gpu => { const displayName = gpu.vendor === 'nvidia' ? `NVIDIA ${shortGpuName(gpu)}` : `Intel ${shortGpuName(gpu)}`; const fullName = gpu.model; let memLabel = ''; // if (gpu.memory?.type === 'dedicated' && gpu.memory.total_mb) { if ( gpu.memory?.type === 'dedicated' && Number.isFinite(gpu.memory.total_mb) ) { memLabel = `(${Math.round(gpu.memory.total_mb / 1024)} GB VRAM)`; } else if (gpu.memory?.type === 'shared') { memLabel = '(shared VRAM)'; } else { console.log('ERR: gpu.memory:', JSON.stringify(gpu.memory)); } return ` ${displayName} ${memLabel} `; }).join('
'); } // Hostinfo: hostname, cpu, cores, ram, os const hostinfo = { "sdl_id": '' + config.identity.sdl_id + '', "hostname": '' + config.host.hostname + '', // "cpu cores": '' + config.host.cpu.cores_logical + ' ' + config.host.cpu.model + '', // // "cores": config.host.cpu.cores_logical, // "ram": '' + config.host.memory.total_gb + ' GB ', // "gpu": gpuSummary, // "net": "", // "os": config.host.os.pretty_name } for (let intf in config.host.network) { // console.log(intf, JSON.stringify(config.host.network[intf], null, 2)); if (intf != 'lo') { for (let addr in config.host.network[intf]) { if (config.host.network[intf][addr].family == 'IPv4') { hostinfo["net"] += '' + config.host.network[intf][addr].cidr + ' '; } } } } let host_html = '
Manager
\n'; host_html += '\n'; host_html += '\n'; for (let key in hostinfo) { host_html += '\n'; host_html += '\n'; host_html += '\n'; host_html += '\n'; } host_html += '\n'; host_html += '
' + key + '' + hostinfo[key] + '
\n'; // document.getElementById('dash-sdl-mgr-info').innerHTML = host_html; initMqtt(); // function shortGpuName(gpu) { // let name = gpu.model; // // Strip vendor boilerplate // name = name // .replace(/^NVIDIA Corporation\s+/i, '') // .replace(/^Intel Corporation\s+/i, ''); // // NVIDIA cleanup // if (gpu.vendor === 'nvidia') { // name = name.replace(/^GeForce\s+/i, 'RTX '); // } // // Intel cleanup // if (gpu.vendor === 'intel') { // name = name.replace(/\s*\[.*?\]\s*/g, ''); // } // return name.trim(); // } function shortGpuName(gpu) { const full = gpu.model; // NVIDIA: extract RTX/GTX number from brackets if (gpu.vendor === 'nvidia') { const m = full.match(/\[(?:GeForce\s+)?(RTX|GTX)\s+(\d+)/i); if (m) { return `${m[1].toUpperCase()} ${m[2]}`; } // fallback return 'NVIDIA GPU'; } // Intel: strip bracketed suffix, keep platform if (gpu.vendor === 'intel') { return full .replace(/^Intel Corporation\s+/i, '') .replace(/\s*\[.*?\]\s*/g, '') .trim(); } return full; } async function initMqtt() { // 1) Load config (no cache – must be authoritative) // const res = await fetch('/api/config', { cache: 'no-store' }); // const config = await res.json(); mqttConfig = config.modules?.mqtt; sdlConfig = config.modules?.['sdl-mgr']; // console.log('config:', JSON.stringify(config.modules, null, 2)); // console.log('MQTT config:', JSON.stringify(mqttConfig, null, 2)); // console.log('SDL config:', JSON.stringify(sdlConfig, null, 2)); // 2) Hard requirement: embedded MQTT must be enabled if (!mqttConfig) { throw new Error('MQTT module not defined in config'); } if (mqttConfig.enabled !== true) { throw new Error('MQTT module is disabled; dashboard requires MQTT'); } if (!mqttConfig.ws_port) { throw new Error('MQTT ws_port not configured'); } if (!mqttConfig.topics?.['sdl-web']?.sub?.['sdl_cluster-status']) { throw new Error('MQTT topic sdl_status not configured'); } // console.log('MQTT config:', JSON.stringify(mqttConfig, null, 2)); // console.log('SDL config:', JSON.stringify(sdlConfig, null, 2)); // 3) Derive broker WS URL from page origin const wsProto = window.location.protocol === 'https:' ? 'wss' : 'ws'; const wsHost = window.location.hostname; // const wsHost = // mqttConfig.ws_host // ?? config.host.ips.find(ip => ip !== '127.0.0.1') // ?? window.location.hostname; const wsPort = mqttConfig.ws_port; const wsUrl = `${wsProto}://${wsHost}:${wsPort}`; console.log('Connecting to embedded MQTT broker:', wsUrl); // 4) Connect (IMPORTANT: force path '/') mqttClient = mqtt.connect(wsUrl, { path: '/', reconnectPeriod: 2000, connectTimeout: 5000 }); // 5) Register handlers mqttClient.on('connect', () => { console.log('MQTT connected (dashboard)'); const statusTopic = mqttConfig.topics['sdl-web'].sub['sdl_cluster-status']; const workersTopic = mqttConfig.topics['sdl-web'].sub['sdl_cluster-workers']; console.log('Subscribing to:', statusTopic); console.log('Subscribing to:', workersTopic); mqttClient.subscribe(statusTopic, { qos: 1 }, err => { if (err) { console.error('MQTT subscribe error (status):', err); } else { console.log('Subscribed to', statusTopic); } }); mqttClient.subscribe(workersTopic, { qos: 1 }, err => { if (err) { console.error('MQTT subscribe error (workers):', err); } else { console.log('Subscribed to', workersTopic); } }); startStatusAgeMonitor(); }); mqttClient.on('message', (topic, payload, packet) => { try { const text = new TextDecoder().decode(payload); const msg = JSON.parse(text); console.log( 'MQTT RX:', topic, msg, packet.retain ? 'retained' : 'live' ); if (msg.type === 'cluster-status') { lastSdlStatus = msg; lastSdlTimestamp = Date.parse(msg.ts); renderModulesTable(msg); // renderClusterSummary(msg); } else if (msg.type === 'cluster-workers') { lastWorkers = msg; renderWorkersTable(msg); renderClusterSummary(msg); } } catch (e) { console.warn( 'MQTT parse error:', e, payload ); } }); mqttClient.on('error', err => { console.error('MQTT error:', err); }); mqttClient.on('close', () => { console.warn('MQTT connection closed'); }); mqttClient.on('reconnect', () => { console.warn('MQTT reconnecting...'); }); } function renderModulesTable(msg) { // console.log('renderModulesTable()'); // console.log(JSON.stringify(msg, null, 2)); let sdl_html = '
Cluster
'; sdl_html += ''; sdl_html += ` `; sdl_html += ` `; sdl_html += ` `; // sdl_html += ` // // // // // `; // sdl_html += ` // // // // // `; // sdl_html += ` // // // // // `; // Workers stats // const workers = msg.msg.workers || { allocated: 0, available: 0, used: 0 }; // sdl_html += ` // // // // // `; // CPU resources // const cpus = msg.msg.resources?.cpus || { allocated: 0, available: 0, used: 0 }; // sdl_html += ` // // // // // `; // Memory resources // const memory = msg.msg.resources?.memory || { allocated: 0, available: 0, used: 0 }; // const memAllocGB = Math.round(memory.allocated / (1024 * 1024 * 1024)); // const memAvailGB = Math.round(memory.available / (1024 * 1024 * 1024)); // const memUsedGB = memory.used > 0 ? Math.round(memory.used / 1024) : 0; // sdl_html += ` // // // // // `; // GPU resources // const gpus = msg.msg.resources?.gpus || { allocated: 0, available: 0, used: 0 }; // sdl_html += ` // // // // // `; sdl_html += `
SDL Version v${msg.msg.sdl.version}
Uptime (d:h:m:s) ${msg.msg.sdl.uptime}
Cluster ID${msg.msg.cluster.id}
Name${msg.msg.cluster.name}
Description${msg.msg.cluster.desc}
SDL Workers // ${workers.available} //
CPU Cores // ${cpus.available} //
RAM // ${memAvailGB} GB //
GPU (VRAM) // ${gpus.available} //
`; sessionStorage.setItem('dash-sdl-uptime', msg.msg.sdl.uptime); const modules = msg.msg.modules; let html = '
Service Modules
'; html += ''; html += ` `; for (const [name, info] of Object.entries(modules)) { const ok = info.enabled === true; const icon = ok ? '' : ''; html += ` `; } html += `
${name} ${icon}
`; // document.getElementById('dash-modules-list').innerHTML = html; } function startStatusAgeMonitor() { if (!sdlConfig?.update_interval.cluster_status) return; const maxAgeMs = sdlConfig.update_interval.cluster_status + STALE_GRACE_MS; if (statusCheckTimer) { clearInterval(statusCheckTimer); } statusCheckTimer = setInterval(() => { if (!lastSdlTimestamp) return; const ageMs = Date.now() - lastSdlTimestamp; // const ageEl = document.getElementById('dash-modules-age'); // if (ageMs > maxAgeMs) { // ageEl.innerHTML = // ` // // SDL status stale (${Math.round(ageMs / 1000)}s) // `; // markAllModulesDown(); // } else { // ageEl.innerHTML = // ` // // SDL online (${Math.round(ageMs / 1000)}s ago) // `; // } // Save dash-sdl-status to Session Storage if (ageMs > maxAgeMs) { sessionStorage.setItem('dash-sdl-status', ' ' + Math.round(ageMs / 1000) + 's ago' + ''); } else { sessionStorage.setItem('dash-sdl-status', ' ' + Math.round(ageMs / 1000) + 's ago'); } // sessionStorage.setItem('dash-sdl-status', Math.round(ageMs / 1000) + 's ago'); }, 1000); } function markAllModulesDown() { if (!lastSdlStatus?.modules) return; const downModules = {}; downModules.msg = {} for (const name of Object.keys(lastSdlStatus.modules)) { downModules.msg[name] = { enabled: false }; } renderModulesTable(downModules); } function renderWorkersTable(msg) { const workers = msg.msg?.workers || {}; const workerCount = Object.keys(workers).length; let html = `
Workers (${workerCount})
`; if (workerCount === 0) { html += '

No workers connected

'; document.getElementById('dash-sdl-wkrs').innerHTML = html; return; } html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; const nowSec = Math.floor(Date.now() / 1000); const staleThresholdSec = 60; for (const [sdl_id, worker] of Object.entries(workers)) { const ageSec = nowSec - worker.ts_utime; // ✅ Use ts_utime from telemetry const isActive = ageSec <= staleThresholdSec; // Status badge const statusBadge = isActive ? 'Active' : 'Inactive'; // ✅ Hardware info from nested msg.system.hardware const hwType = worker.msg?.system?.hardware?.type || 'unknown'; // const hwIcon = hwType === 'hw' ? '🖥️' : hwType === 'vm' ? '💠' : '❓'; const hwIcon = hwType === 'hw' ? '' : hwType === 'vm' ? '' : ''; const hwManuf = worker.msg?.system?.hardware?.manufacturer || 'Unknown'; const hwModel = worker.msg?.system?.hardware?.model || 'Unknown'; const hwCPU = worker.msg?.system?.hardware?.cpu || 'Unknown'; const hardwareDisplay = hwType === 'vm' ? `${hwIcon} ${hwModel}` : `${hwIcon} ${hwManuf} ${hwModel}`; // ✅ OS info from nested msg.system const osDistro = worker.msg?.system?.distro.replace(/\(\S+\)/, '') || 'Unknown'; const osArch = worker.msg?.system?.arch || 'unknown'; const osPlatform = worker.msg?.system?.platform || 'unknown'; const platformIcon = osPlatform === 'linux' ? '' : osPlatform === 'win32' ? '' : osPlatform === 'darwin' ? '' : ''; const osDisplay = `${platformIcon} ${osDistro} (${osArch})`; // ✅ CPU from nested msg.resources and msg.usage const cpuAvail = worker.msg?.resources?.cpus?.available || 0; const cpuTotal = worker.msg?.resources?.cpus?.allocated || 0; const cpuUsed = worker.msg?.usage?.cpu?.total || 0; // ✅ Memory from nested msg const memAvail = worker.msg?.resources?.memory?.available || 0; const memTotal = worker.msg?.resources?.memory?.allocated || 0; const memAvailGB = Math.round(memAvail / (1024 * 1024 * 1024)); const memTotalGB = Math.round(memTotal / (1024 * 1024 * 1024)); const memPercent = worker.msg?.usage?.memory?.percent_used || 0; // ✅ GPU from nested msg const gpuAvail = worker.msg?.resources?.gpus?.available || 0; const gpuTotal = worker.msg?.resources?.gpus?.allocated || 0; const gpuData = worker.msg?.usage?.gpu; const gpuUsed = gpuData?.total_utilization || 0; const gpuMemMB = gpuData?.total_memory_total_mb || 0; const gpuMemGB = Math.round(gpuMemMB / 1024); // ✅ Uptime from nested msg.uptime const procUptime = worker.msg?.uptime?.proc_dhms || 'N/A'; const sysUptime = worker.msg?.uptime?.sys_dhms || 'N/A'; // Last seen age const days = Math.floor(ageSec / 86400); const hours = Math.floor((ageSec % 86400) / 3600); const minutes = Math.floor((ageSec % 3600) / 60); const seconds = Math.floor(ageSec % 60); let lastSeenAge = ''; if (days > 0) lastSeenAge += `${days}d `; if (hours > 0 || days > 0) lastSeenAge += `${hours}h `; if (minutes > 0 || hours > 0 || days > 0) lastSeenAge += `${minutes}m `; lastSeenAge += `${seconds}s ago`; html += ''; // System column html += ``; // Status column html += ``; // CPU html += ``; // Memory html += ``; // GPU html += ``; html += ''; } html += ''; html += '
SystemStatus CPU RAM GPU
${worker.host}
${worker.sdl_id.substring(0, 8)}...
${hardwareDisplay}
${memTotalGB} GB | ${cpuTotal}x ${hwCPU}}
${osDisplay}
${statusBadge}
SDL: ${procUptime}
OS: ${sysUptime}
${lastSeenAge}
${cpuAvail} / ${cpuTotal} ${cpuUsed > 0 ? `
${cpuUsed}% load` : ''}
${memAvailGB} / ${memTotalGB} GB ${memPercent > 0 ? `
${memPercent}% used` : ''}
${gpuAvail} / ${gpuTotal} ${gpuMemGB > 0 ? `
(${gpuMemGB} GB)` : ''} ${gpuUsed > 0 ? `
${gpuUsed}% load` : ''}
'; document.getElementById('dash-sdl-wkrs').innerHTML = html; } function renderClusterSummary(msg) { const workers = msg.msg?.workers || {}; const workerCount = Object.keys(workers).length; // Aggregate resources from all workers let totalCpus = 0; let totalMemory = 0; let totalGpus = 0; let activeCpus = 0; let activeMemory = 0; let activeGpus = 0; let activeWorkers = 0; const nowSec = Math.floor(Date.now() / 1000); const staleThresholdSec = 60; for (const worker of Object.values(workers)) { const ageSec = nowSec - worker.ts_utime; const isActive = ageSec <= staleThresholdSec; // Totals (all workers) totalCpus += worker.msg?.resources?.cpus?.allocated || 0; totalMemory += worker.msg?.resources?.memory?.allocated || 0; totalGpus += worker.msg?.resources?.gpus?.allocated || 0; // Active workers only if (isActive) { activeWorkers++; activeCpus += worker.msg?.resources?.cpus?.available || 0; activeMemory += worker.msg?.resources?.memory?.allocated || 0; activeGpus += worker.msg?.resources?.gpus?.available || 0; } } // Convert memory to GB const totalMemoryGB = Math.round(totalMemory / (1024 * 1024 * 1024)); const activeMemoryGB = Math.round(activeMemory / (1024 * 1024 * 1024)); // Get cluster info from last status message const clusterName = lastSdlStatus?.msg?.cluster?.name || 'Unknown'; const clusterDesc = lastSdlStatus?.msg?.cluster?.desc || ''; let html = `
Cluster: ${clusterName}
${clusterDesc}
`; html += '
'; // SDL card html += `
SDL
Version: v${config?.sdl?.version}
Uptime:
Status:
`; // Workers card html += `
Workers

${activeWorkers}

of ${workerCount} total
`; // CPU card html += `
CPU Cores

${activeCpus}

of ${totalCpus} total
`; // Memory card html += `
RAM

${activeMemoryGB} GB

of ${totalMemoryGB} GB total
`; // GPU card html += `
GPUs

${activeGpus}

of ${totalGpus} total
`; html += '
'; // end row document.getElementById('dash-sdl-clstr').innerHTML = html; // Read dash-sdl-uptime from Session Storage const dashSdlUptime = sessionStorage.getItem('dash-sdl-uptime'); if (dashSdlUptime) { document.getElementById('dash-sdl-uptime').innerHTML = dashSdlUptime; } // Read dash-sdl-status from Session Storage const dashSdlStatus = sessionStorage.getItem('dash-sdl-status'); if (dashSdlStatus) { document.getElementById('dash-sdl-status').innerHTML = dashSdlStatus; } }