// =============================== // 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 = { "hostname": '' + config.host.hostname + '', "sdl_id": '' + config.identity.sdl_id + '', "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'; // host_html += '\n'; // host_html += '\n'; // host_html += '\n'; // host_html += '\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 += '
KeyValue
' + 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); } else if (msg.type === 'cluster-workers') { lastWorkers = msg; renderWorkersTable(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 += ` `; // sdl_html += ` // // // // // `; // CPU resources const cpus = msg.msg.resources?.cpus || { allocated: 0, available: 0, used: 0 }; sdl_html += ` `; // 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 += ` `; // sdl_html += ` // // // // // `; // GPU resources const gpus = msg.msg.resources?.gpus || { allocated: 0, available: 0, used: 0 }; sdl_html += ` `; // 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}
SDL Workers (active/total) // ${workers.available} / ${workers.allocated} //
CPU Cores ${cpus.available}
CPU Cores // ${cpus.available} / ${cpus.allocated} // ${cpus.used > 0 ? `(${cpus.used} in use)` : ''} //
RAM ${memAvailGB} GB
RAM (GB) // ${memAvailGB} / ${memAllocGB} GB // ${memUsedGB > 0 ? `(${memUsedGB} GB in use)` : ''} //
GPU (VRAM) ${gpus.available}
GPU // ${gpus.available} / ${gpus.allocated} // ${gpus.used > 0 ? `(${gpus.used} in use)` : ''} //
`; document.getElementById('dash-sdl-info').innerHTML = sdl_html; 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) `; } }, 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; }