// ===============================
// 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 += '| Key | \n';
// host_html += 'Value | \n';
// host_html += '
\n';
// host_html += '\n';
host_html += '\n';
for (let key in hostinfo) {
host_html += '\n';
host_html += '| ' + key + ' | \n';
host_html += '' + hostinfo[key] + ' | \n';
host_html += '
\n';
}
host_html += '\n';
host_html += '
\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 Version |
v${msg.msg.sdl.version} |
`;
sdl_html += `
| Uptime (d:h:m:s) |
${msg.msg.sdl.uptime} |
`;
// sdl_html += `
//
// | Cluster ID |
// ${msg.msg.cluster.id} |
//
// `;
sdl_html += `
| Name |
${msg.msg.cluster.name} |
`;
sdl_html += `
| Description |
${msg.msg.cluster.desc} |
`;
// Workers stats
const workers = msg.msg.workers || { allocated: 0, available: 0, used: 0 };
sdl_html += `
| SDL Workers |
${workers.available}
|
`;
// sdl_html += `
//
// | SDL Workers (active/total) |
//
// ${workers.available} / ${workers.allocated}
// |
//
// `;
// CPU resources
const cpus = msg.msg.resources?.cpus || { allocated: 0, available: 0, used: 0 };
sdl_html += `
| CPU Cores |
${cpus.available}
|
`;
// sdl_html += `
//
// | CPU Cores |
//
// ${cpus.available} / ${cpus.allocated}
// ${cpus.used > 0 ? `(${cpus.used} in use)` : ''}
// |
//
// `;
// 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 += `
| RAM |
${memAvailGB} GB
|
`;
// sdl_html += `
//
// | RAM (GB) |
//
// ${memAvailGB} / ${memAllocGB} GB
// ${memUsedGB > 0 ? `(${memUsedGB} GB in use)` : ''}
// |
//
// `;
// GPU resources
const gpus = msg.msg.resources?.gpus || { allocated: 0, available: 0, used: 0 };
sdl_html += `
| GPU (VRAM) |
${gpus.available}
|
`;
// sdl_html += `
//
// | GPU |
//
// ${gpus.available} / ${gpus.allocated}
// ${gpus.used > 0 ? `(${gpus.used} in use)` : ''}
// |
//
// `;
sdl_html += `
`;
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 += `
| ${name} |
${icon} |
`;
}
html += `
`;
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 += '| System | ';
html += 'Status | ';
html += ' CPU | ';
html += ' RAM | ';
html += ' GPU | ';
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.last_seen_utime;
const isActive = ageSec <= staleThresholdSec;
// Status badge
const statusBadge = isActive
? 'Active'
: 'Inactive'; // ✅ Red instead of grey
// Hardware info
const hwType = worker.hardware?.type || 'unknown';
const hwIcon = hwType === 'hw' ? '🖥️' : hwType === 'vm' ? '💠' : '❓';
const hwManuf = worker.hardware?.manufacturer || 'Unknown';
const hwModel = worker.hardware?.model || 'Unknown';
// ✅ Full hardware display with model
const hardwareDisplay = hwType === 'vm'
? `${hwIcon} ${hwModel}`
: `${hwIcon} ${hwManuf} ${hwModel}`;
// ✅ OS info
const osDistro = worker.distro || 'Unknown';
const osArch = worker.arch || 'unknown';
const osPlatform = worker.platform || 'unknown';
// CPU
const cpuAvail = worker.resources?.cpus?.available || 0;
const cpuTotal = worker.resources?.cpus?.allocated || 0;
const cpuUsed = worker.usage?.cpu?.total || 0;
// Memory
const memAvail = worker.resources?.memory?.available || 0;
const memTotal = worker.resources?.memory?.allocated || 0;
const memUsed = worker.usage?.memory?.used || 0;
const memAvailGB = Math.round(memAvail / (1024 * 1024 * 1024));
const memTotalGB = Math.round(memTotal / (1024 * 1024 * 1024));
const memPercent = worker.usage?.memory?.percent_used || 0;
// GPU
const gpuAvail = worker.resources?.gpus?.available || 0;
const gpuTotal = worker.resources?.gpus?.allocated || 0;
const gpuData = worker.usage?.gpu;
const gpuUsed = gpuData?.total_utilization || 0;
const gpuMemMB = gpuData?.total_memory_total_mb || 0;
const gpuMemGB = Math.round(gpuMemMB / 1024);
// Uptime
const procUptime = worker.uptime?.proc_dhms || 'N/A';
const sysUptime = worker.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 += '';
// ✅ Hostname column with hardware underneath
html += `
${worker.hostname}
${worker.sdl_id}
${hardwareDisplay}
${osDistro} ${osArch} ${osPlatform}
| `;
// ✅ Status column with uptime and last seen
html += `
${statusBadge}
SDL: ${procUptime}
OS: ${sysUptime}
${lastSeenAge}
| `;
// CPU
html += `
${cpuAvail} / ${cpuTotal}
${cpuUsed > 0 ? ` ${cpuUsed}% load` : ''}
| `;
// Memory
html += `
${memAvailGB} / ${memTotalGB} GB
${memPercent > 0 ? ` ${memPercent}% used` : ''}
| `;
// GPU
html += `
${gpuAvail} / ${gpuTotal}
${gpuMemGB > 0 ? ` (${gpuMemGB} GB)` : ''}
${gpuUsed > 0 ? ` ${gpuUsed}% load` : ''}
| `;
html += '
';
}
html += '';
html += '
';
document.getElementById('dash-sdl-wkrs').innerHTML = html;
}