/**
* SystemHealth.jsx — System resource monitoring and ROS2 node status.
*
* Topics:
* /diagnostics (diagnostic_msgs/DiagnosticArray)
* Each DiagnosticStatus has: name, level (0=OK,1=WARN,2=ERROR,3=STALE)
* message, hardware_id, values: [{key,value}]
* Expected KeyValue sources (from tegrastats bridge / custom nodes):
* cpu_temp_c, gpu_temp_c, ram_used_mb, ram_total_mb,
* disk_used_gb, disk_total_gb, gpu_used_mb, gpu_total_mb
*/
import { useEffect, useState } from 'react';
const LEVEL_CONFIG = {
0: { label: 'OK', color: 'text-green-400', bg: 'bg-green-950', border: 'border-green-800', dot: 'bg-green-400' },
1: { label: 'WARN', color: 'text-amber-400', bg: 'bg-amber-950', border: 'border-amber-800', dot: 'bg-amber-400 animate-pulse' },
2: { label: 'ERROR', color: 'text-red-400', bg: 'bg-red-950', border: 'border-red-800', dot: 'bg-red-400 animate-pulse' },
3: { label: 'STALE', color: 'text-gray-500', bg: 'bg-gray-900', border: 'border-gray-700', dot: 'bg-gray-500' },
};
function ResourceBar({ label, used, total, unit, warnPct = 80 }) {
if (total == null || total === 0) return null;
const pct = Math.round((used / total) * 100);
const warn = pct >= warnPct;
const crit = pct >= 95;
const color = crit ? '#ef4444' : warn ? '#f59e0b' : '#06b6d4';
return (
{label}
{typeof used === 'number' ? used.toFixed(1) : used}/{typeof total === 'number' ? total.toFixed(1) : total} {unit}
({pct}%)
);
}
function TempGauge({ label, tempC }) {
if (tempC == null) return null;
const warn = tempC > 75;
const crit = tempC > 90;
const pct = Math.min(100, (tempC / 100) * 100);
const color = crit ? '#ef4444' : warn ? '#f59e0b' : '#22c55e';
return (
{label}
{tempC.toFixed(0)}°C
);
}
function NodeRow({ status }) {
const cfg = LEVEL_CONFIG[status.level] ?? LEVEL_CONFIG[3];
return (
{status.name}
{status.message || cfg.label}
);
}
export function SystemHealth({ subscribe }) {
const [resources, setResources] = useState({
cpuTemp: null, gpuTemp: null,
ramUsed: null, ramTotal: null,
diskUsed: null, diskTotal: null,
gpuUsed: null, gpuTotal: null,
});
const [nodes, setNodes] = useState([]);
const [lastTs, setLastTs] = useState(null);
useEffect(() => {
const unsub = subscribe('/diagnostics', 'diagnostic_msgs/DiagnosticArray', (msg) => {
setLastTs(Date.now());
// Parse each DiagnosticStatus
const nodeList = [];
for (const status of msg.status ?? []) {
const kv = {};
for (const pair of status.values ?? []) kv[pair.key] = pair.value;
// Resource metrics
setResources(prev => {
const next = { ...prev };
if (kv.cpu_temp_c !== undefined) next.cpuTemp = parseFloat(kv.cpu_temp_c);
if (kv.gpu_temp_c !== undefined) next.gpuTemp = parseFloat(kv.gpu_temp_c);
if (kv.ram_used_mb !== undefined) next.ramUsed = parseFloat(kv.ram_used_mb) / 1024;
if (kv.ram_total_mb !== undefined) next.ramTotal = parseFloat(kv.ram_total_mb) / 1024;
if (kv.disk_used_gb !== undefined) next.diskUsed = parseFloat(kv.disk_used_gb);
if (kv.disk_total_gb!== undefined) next.diskTotal = parseFloat(kv.disk_total_gb);
if (kv.gpu_used_mb !== undefined) next.gpuUsed = parseFloat(kv.gpu_used_mb);
if (kv.gpu_total_mb !== undefined) next.gpuTotal = parseFloat(kv.gpu_total_mb);
return next;
});
// Collect all statuses as node rows
nodeList.push({ name: status.name, level: status.level, message: status.message });
}
if (nodeList.length > 0) {
setNodes(nodeList);
}
});
return unsub;
}, [subscribe]);
const stale = lastTs && Date.now() - lastTs > 10000;
const errorCount = nodes.filter(n => n.level === 2).length;
const warnCount = nodes.filter(n => n.level === 1).length;
return (
{/* Summary banner */}
{nodes.length > 0 && (
0 ? 'bg-red-950 border-red-800' :
warnCount > 0 ? 'bg-amber-950 border-amber-800' :
'bg-green-950 border-green-800'
}`}>
0 ? 'bg-red-400 animate-pulse' :
warnCount > 0 ? 'bg-amber-400 animate-pulse' : 'bg-green-400'
}`} />
{errorCount > 0 ? `${errorCount} ERROR${errorCount > 1 ? 'S' : ''}` :
warnCount > 0 ? `${warnCount} WARNING${warnCount > 1 ? 'S' : ''}` :
'All systems nominal'}
{nodes.length} nodes
{stale &&
STALE
}
)}
{/* Temperatures */}
{(resources.cpuTemp != null || resources.gpuTemp != null) && (
)}
{/* Resource bars */}
SYSTEM RESOURCES
{resources.ramUsed == null && resources.gpuUsed == null && resources.diskUsed == null && (
Waiting for resource metrics from /diagnostics…
Expected keys: cpu_temp_c, gpu_temp_c, ram_used_mb, disk_used_gb
)}
{/* Node list */}
ROS2 NODE HEALTH
{nodes.length} statuses
{nodes.length === 0 ? (
Waiting for /diagnostics…
) : (
{/* Sort: errors first, then warns, then OK */}
{[...nodes]
.sort((a, b) => (b.level ?? 0) - (a.level ?? 0))
.map((n, i) => )
}
)}
);
}