diff --git a/README.md b/README.md
index 56f9fe7..b563d1e 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
| **Author** | John Haverlack |
| **Copyright** | 2026 John Haverlack |
| **License** | MIT |
-| **Version** | 0.3.5 |
+| **Version** | 0.3.6 |
| **Date** | 2026-02-01 |
## Overview
diff --git a/changelog.json b/changelog.json
index bf1e67e..1428b1b 100644
--- a/changelog.json
+++ b/changelog.json
@@ -1,5 +1,12 @@
{
"releases": [
+ {
+ "version": "0.3.6",
+ "date": "2026-02-01",
+ "maturity": "ALPHA",
+ "summary": "Light/Dark Mode, Wkr Install, Cluster Stats",
+ "notes": ""
+ },
{
"version": "0.3.5",
"date": "2026-02-01",
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 17bdf31..8c7bb7d 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -5,14 +5,26 @@
| **Author** | John Haverlack |
| **Copyright** | 2026 John Haverlack |
| **License** | MIT |
-| **Version** | 0.3.5 |
+| **Version** | 0.3.6 |
| **Date** | 2026-02-01 |
-## v0.3.5 - 2026-02-01 (ALPHA)
+## v0.3.6 - 2026-02-01 (ALPHA)
**Summary**
Light/Dark Mode, Wkr Install, Cluster Stats
+- Adding Cluster Telemetry
+- WIP: Worker Display
+- Tweaks
+- Start v0.3.6
+- CP Updates
+- Dash: Total Cluster Stats
+- Publishing to cluster/workers
+- Light/Dark Mode Toggle
+- Added light/dark theme CSS
+
+## v0.3.5 - 2026-02-01 (ALPHA)
+
- v0.3.5
- Fixing sdl-wkr install script
- Start v0.3.5
diff --git a/docs/DESIGN.md b/docs/DESIGN.md
index dccda85..6179362 100644
--- a/docs/DESIGN.md
+++ b/docs/DESIGN.md
@@ -5,7 +5,7 @@
| **Author** | John Haverlack |
| **Copyright** | 2026 John Haverlack |
| **License** | MIT |
-| **Version** | 0.3.5 |
+| **Version** | 0.3.6 |
| **Date** | 2026-02-01 |
## Overview
diff --git a/metadata.json b/metadata.json
index 9903564..3ad3fde 100644
--- a/metadata.json
+++ b/metadata.json
@@ -3,7 +3,7 @@
"name": "sdl",
"description": "Software Defined Laboratory",
"abbr": "SDL",
- "version": "0.3.5",
+ "version": "0.3.6",
"version_date": "2026-02-01",
"maturity": "ALPHA",
"author": "John Haverlack",
diff --git a/sdl-mgr/app/modules/sdl-mgr/index.js b/sdl-mgr/app/modules/sdl-mgr/index.js
index 715a07b..fb4f7f7 100644
--- a/sdl-mgr/app/modules/sdl-mgr/index.js
+++ b/sdl-mgr/app/modules/sdl-mgr/index.js
@@ -621,12 +621,121 @@ function startUdpBeacon() {
}
+// Update startHeartbeatListener to use telemetry topic
+function startTelemetryListener() { // ✅ Renamed function
+ const sdlCfg = config.modules[module];
+ const mqttCfg = config.modules.mqtt;
-// -------------------------------
-// Entry point (ESM-safe)
-// -------------------------------
+ if (!sdlCfg?.enabled) {
+ log(`${module}: disabled, not listening for telemetry`);
+ return;
+ }
+
+ if (!mqttCfg?.enabled) {
+ log(`${module}: MQTT disabled, cannot listen for telemetry`);
+ return;
+ }
+
+ let clusterState = loadClusterState(config);
+
+ const telemetryTopic = mqttCfg.topics?.[module]?.sub?.['sdl_cluster-telemetry']; // ✅ Changed
+ if (!telemetryTopic) {
+ log(`${module}: cluster-telemetry topic not configured`);
+ return;
+ }
+
+ const mqttUrl = `mqtt://127.0.0.1:${mqttCfg.mqtt_port}`;
+ const client = mqtt.connect(mqttUrl);
+
+ client.on('connect', () => {
+ log(`${module}: telemetry listener connected to MQTT at ${mqttUrl}`);
+
+ client.subscribe(telemetryTopic, { qos: 1 }, err => {
+ if (err) {
+ log(`${module}: failed to subscribe to ${telemetryTopic}: ${err}`);
+ } else {
+ log(`${module}: subscribed to ${telemetryTopic}`);
+ }
+ });
+ });
+
+ client.on('message', (topic, message) => {
+ if (topic !== telemetryTopic) return;
+
+ try {
+ const telemetry = JSON.parse(message.toString());
+ const sdl_id = telemetry.msg.sdl_id;
+
+ // Update worker's last_seen timestamp
+ if (clusterState.workers[sdl_id]) {
+ clusterState.workers[sdl_id].last_seen = new Date().toISOString();
+ clusterState.workers[sdl_id].status = 'active';
+
+ // Update resources if provided
+ if (telemetry.msg.resources) {
+ clusterState.workers[sdl_id].resources = telemetry.msg.resources;
+ }
+
+ clusterState = computeStats(clusterState);
+ saveClusterState(config, clusterState);
+ }
+ } catch (err) {
+ log(`${module}: failed to process telemetry: ${err}`);
+ }
+ });
+
+ client.on('error', err => {
+ log(`${module}: telemetry listener MQTT error: ${err}`);
+ });
+}
+
+// Add Stale Worker Detection
+function startStaleWorkerDetection() {
+ const sdlCfg = config.modules[module];
+
+ if (!sdlCfg?.enabled) {
+ log(`${module}: disabled, not starting stale worker detection`);
+ return;
+ }
+
+ const checkInterval = 10000; // Check every 10 seconds
+ const staleThreshold =
+ Number.isInteger(sdlCfg.worker_stale_threshold) &&
+ sdlCfg.worker_stale_threshold > 0
+ ? sdlCfg.worker_stale_threshold
+ : 30000; // Default 30 seconds
+
+ setInterval(() => {
+ let clusterState = loadClusterState(config);
+ const now = Date.now();
+ let changed = false;
+
+ for (const [sdl_id, worker] of Object.entries(clusterState.workers)) {
+ const lastSeenMs = Date.parse(worker.last_seen);
+ const ageMs = now - lastSeenMs;
+
+ if (ageMs > staleThreshold && worker.status === 'active') {
+ log(`${module}: worker ${worker.hostname} (${sdl_id}) marked as inactive (last seen ${Math.round(ageMs / 1000)}s ago)`);
+ clusterState.workers[sdl_id].status = 'inactive';
+ changed = true;
+ }
+ }
+
+ if (changed) {
+ clusterState = computeStats(clusterState);
+ saveClusterState(config, clusterState);
+ }
+ }, checkInterval);
+
+ log(`${module}: stale worker detection active (threshold: ${staleThreshold}ms, check interval: ${checkInterval}ms)`);
+}
+
+// Update entry point
startSDLStatusPub();
-startWorkersPub(); // ✅ Start workers publisher
+startWorkersPub();
+startTelemetryListener();
startUdpBeacon();
loadClusterState(config);
-startJoinHandler();
\ No newline at end of file
+startJoinHandler();
+startStaleWorkerDetection();
+
diff --git a/sdl-mgr/app/package.json b/sdl-mgr/app/package.json
index 504c250..417c095 100644
--- a/sdl-mgr/app/package.json
+++ b/sdl-mgr/app/package.json
@@ -1,6 +1,6 @@
{
"name": "sdl-mgr",
- "version": "0.3.5",
+ "version": "0.3.6",
"version_date": "2026-02-01",
"description": "Software Defined Laboratory: Manager (sdl-mgr)",
"main": "index.js",
diff --git a/sdl-mgr/conf/modules/sdl-mgr.json b/sdl-mgr/conf/modules/sdl-mgr.json
index 049e810..d4f7d6d 100644
--- a/sdl-mgr/conf/modules/sdl-mgr.json
+++ b/sdl-mgr/conf/modules/sdl-mgr.json
@@ -1,6 +1,7 @@
{
"enabled": true,
"beacon_udp_port":10101,
+ "worker_stale_threshold": 15000,
"update_interval": {
"udp_beacon": 2000,
"cluster_status": 10000,
diff --git a/sdl-mgr/html/conf/nav-sidepanel.json b/sdl-mgr/html/conf/nav-sidepanel.json
index e8fa3c0..cd19180 100644
--- a/sdl-mgr/html/conf/nav-sidepanel.json
+++ b/sdl-mgr/html/conf/nav-sidepanel.json
@@ -20,7 +20,7 @@
"desc": "Workers",
"icon": "fa-solid fa-server",
"page": "wkrs",
- "enabled": true,
+ "enabled": false,
"items": []
},
"proj": {
diff --git a/sdl-mgr/html/js/dash.js b/sdl-mgr/html/js/dash.js
index e6cc551..caa76a2 100644
--- a/sdl-mgr/html/js/dash.js
+++ b/sdl-mgr/html/js/dash.js
@@ -13,7 +13,7 @@ let lastSdlStatus = null;
let lastSdlTimestamp = null;
let statusCheckTimer = null;
const STALE_GRACE_MS = 2000;
-
+let lastWorkers = null;
// -------------------------------
// GPU Summary (for dashboard)
@@ -44,7 +44,7 @@ if (Array.isArray(config.host.gpu) && config.host.gpu.length > 0) {
return `
${displayName} ${memLabel}
@@ -55,7 +55,7 @@ if (Array.isArray(config.host.gpu) && config.host.gpu.length > 0) {
// Hostinfo: hostname, cpu, cores, ram, os
const hostinfo = {
"hostname": '' + config.host.hostname + '',
- "sdl_id": '' + config.identity.sdl_id + '',
+ "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 ',
@@ -70,7 +70,7 @@ for (let intf in config.host.network) {
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 + '
';
+ hostinfo["net"] += '' + config.host.network[intf][addr].cidr + '
';
}
}
}
@@ -201,22 +201,32 @@ async function initMqtt() {
});
// 5) Register handlers
- mqttClient.on('connect', () => {
+ mqttClient.on('connect', () => {
console.log('MQTT connected (dashboard)');
- const topic = mqttConfig.topics['sdl-web'].sub['sdl_cluster-status'];
- console.log('Subscribing to:', topic);
+ 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(topic, { qos: 1 }, err => {
+ mqttClient.subscribe(statusTopic, { qos: 1 }, err => {
if (err) {
- console.error('MQTT subscribe error:', err);
+ console.error('MQTT subscribe error (status):', err);
} else {
- console.log('Subscribed to', topic);
+ 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) => {
@@ -235,6 +245,9 @@ async function initMqtt() {
lastSdlStatus = msg;
lastSdlTimestamp = Date.parse(msg.ts);
renderModulesTable(msg);
+ } else if (msg.type === 'cluster-workers') {
+ lastWorkers = msg;
+ renderWorkersTable(msg);
}
} catch (e) {
@@ -278,7 +291,7 @@ function renderModulesTable(msg) {
sdl_html += `
No workers connected
'; + document.getElementById('dash-sdl-wkrs').innerHTML = html; + return; + } + + html += '| Hostname | '; + html += 'Status | '; + html += 'CPU | '; + html += 'RAM | '; + html += 'GPU | '; + html += 'Platform | '; + html += 'Last Seen | '; + html += '
|---|---|---|---|---|---|---|
| ${worker.hostname} ${worker.sdl_id} | `;
+ html += `${status} | `; + html += `${cpuAvail} / ${cpuTotal}${cpuUsed > 0 ? ` (${cpuUsed} used)` : ''} | `;
+ html += `${memAvailGB} / ${memTotalGB} GB${memUsedGB > 0 ? ` (${memUsedGB} GB used)` : ''} | `;
+ html += `${gpuAvail} / ${gpuTotal}${gpuUsed > 0 ? ` (${gpuUsed} used)` : ''} | `;
+ html += `${platform} | `; + html += `${lastSeen} | `; + html += '