Compare commits
No commits in common. "51561001973a59ae5cae3a02c4eb09d2d783915c" and "a7d95315373693fd05a094c0df9916f6e3d84501" have entirely different histories.
5156100197
...
a7d9531537
@ -79,9 +79,6 @@ import { NodeList } from './components/NodeList.jsx';
|
|||||||
// Gamepad teleoperation (issue #319)
|
// Gamepad teleoperation (issue #319)
|
||||||
import { Teleop } from './components/Teleop.jsx';
|
import { Teleop } from './components/Teleop.jsx';
|
||||||
|
|
||||||
// System diagnostics (issue #340)
|
|
||||||
import { Diagnostics } from './components/Diagnostics.jsx';
|
|
||||||
|
|
||||||
const TAB_GROUPS = [
|
const TAB_GROUPS = [
|
||||||
{
|
{
|
||||||
label: 'SOCIAL',
|
label: 'SOCIAL',
|
||||||
@ -130,7 +127,6 @@ const TAB_GROUPS = [
|
|||||||
label: 'MONITORING',
|
label: 'MONITORING',
|
||||||
color: 'text-yellow-600',
|
color: 'text-yellow-600',
|
||||||
tabs: [
|
tabs: [
|
||||||
{ id: 'diagnostics', label: 'Diagnostics' },
|
|
||||||
{ id: 'eventlog', label: 'Events' },
|
{ id: 'eventlog', label: 'Events' },
|
||||||
{ id: 'bandwidth', label: 'Bandwidth' },
|
{ id: 'bandwidth', label: 'Bandwidth' },
|
||||||
{ id: 'nodes', label: 'Nodes' },
|
{ id: 'nodes', label: 'Nodes' },
|
||||||
@ -285,8 +281,6 @@ export default function App() {
|
|||||||
{activeTab === 'fleet' && <FleetPanel />}
|
{activeTab === 'fleet' && <FleetPanel />}
|
||||||
{activeTab === 'missions' && <MissionPlanner />}
|
{activeTab === 'missions' && <MissionPlanner />}
|
||||||
|
|
||||||
{activeTab === 'diagnostics' && <Diagnostics subscribe={subscribe} />}
|
|
||||||
|
|
||||||
{activeTab === 'eventlog' && <EventLog subscribe={subscribe} />}
|
{activeTab === 'eventlog' && <EventLog subscribe={subscribe} />}
|
||||||
|
|
||||||
{activeTab === 'bandwidth' && <BandwidthMonitor />}
|
{activeTab === 'bandwidth' && <BandwidthMonitor />}
|
||||||
|
|||||||
@ -1,308 +0,0 @@
|
|||||||
/**
|
|
||||||
* Diagnostics.jsx — System diagnostics panel with hardware status monitoring
|
|
||||||
*
|
|
||||||
* Features:
|
|
||||||
* - Subscribes to /diagnostics (diagnostic_msgs/DiagnosticArray)
|
|
||||||
* - Hardware status cards per subsystem (color-coded health)
|
|
||||||
* - Real-time error and warning counts
|
|
||||||
* - Diagnostic status timeline
|
|
||||||
* - Error/warning history with timestamps
|
|
||||||
* - Aggregated system health summary
|
|
||||||
* - Status indicators: OK (green), WARNING (yellow), ERROR (red), STALE (gray)
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
|
||||||
|
|
||||||
const MAX_HISTORY = 100; // Keep last 100 diagnostic messages
|
|
||||||
const STATUS_COLORS = {
|
|
||||||
0: { bg: 'bg-green-950', border: 'border-green-800', text: 'text-green-400', label: 'OK' },
|
|
||||||
1: { bg: 'bg-yellow-950', border: 'border-yellow-800', text: 'text-yellow-400', label: 'WARN' },
|
|
||||||
2: { bg: 'bg-red-950', border: 'border-red-800', text: 'text-red-400', label: 'ERROR' },
|
|
||||||
3: { bg: 'bg-gray-900', border: 'border-gray-700', text: 'text-gray-400', label: 'STALE' },
|
|
||||||
};
|
|
||||||
|
|
||||||
function getStatusColor(level) {
|
|
||||||
return STATUS_COLORS[level] || STATUS_COLORS[3];
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatTimestamp(timestamp) {
|
|
||||||
const date = new Date(timestamp);
|
|
||||||
return date.toLocaleTimeString('en-US', {
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
second: '2-digit',
|
|
||||||
hour12: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function DiagnosticCard({ diagnostic, expanded, onToggle }) {
|
|
||||||
const color = getStatusColor(diagnostic.level);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`rounded-lg border p-3 space-y-2 cursor-pointer transition-all ${
|
|
||||||
color.bg
|
|
||||||
} ${color.border} ${expanded ? 'ring-2 ring-offset-2 ring-cyan-500' : ''}`}
|
|
||||||
onClick={onToggle}
|
|
||||||
>
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-start justify-between gap-2">
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="text-xs font-bold text-gray-400 truncate">{diagnostic.name}</div>
|
|
||||||
<div className={`text-sm font-mono font-bold ${color.text}`}>
|
|
||||||
{diagnostic.message}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={`px-2 py-1 rounded text-xs font-bold whitespace-nowrap flex-shrink-0 ${
|
|
||||||
color.bg
|
|
||||||
} ${color.border} border ${color.text}`}
|
|
||||||
>
|
|
||||||
{color.label}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Expanded details */}
|
|
||||||
{expanded && (
|
|
||||||
<div className="border-t border-gray-700 pt-2 space-y-2 text-xs">
|
|
||||||
{diagnostic.values && diagnostic.values.length > 0 && (
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="text-gray-500 font-bold">KEY VALUES:</div>
|
|
||||||
{diagnostic.values.slice(0, 5).map((val, i) => (
|
|
||||||
<div key={i} className="flex justify-between gap-2 text-gray-400">
|
|
||||||
<span className="font-mono truncate">{val.key}:</span>
|
|
||||||
<span className="font-mono text-gray-300 truncate">{val.value}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{diagnostic.values.length > 5 && (
|
|
||||||
<div className="text-gray-500">+{diagnostic.values.length - 5} more values</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{diagnostic.hardware_id && (
|
|
||||||
<div className="text-gray-500">
|
|
||||||
<span className="font-bold">Hardware:</span> {diagnostic.hardware_id}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function HealthTimeline({ statusHistory, maxEntries = 20 }) {
|
|
||||||
const recentHistory = statusHistory.slice(-maxEntries);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="text-xs font-bold text-gray-400 tracking-widest">TIMELINE</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
{recentHistory.map((entry, i) => {
|
|
||||||
const color = getStatusColor(entry.level);
|
|
||||||
return (
|
|
||||||
<div key={i} className="flex items-center gap-2">
|
|
||||||
<div className="text-xs text-gray-500 font-mono whitespace-nowrap">
|
|
||||||
{formatTimestamp(entry.timestamp)}
|
|
||||||
</div>
|
|
||||||
<div className={`w-2 h-2 rounded-full flex-shrink-0 ${color.text}`} />
|
|
||||||
<div className="text-xs text-gray-400 truncate">
|
|
||||||
{entry.name}: {entry.message}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Diagnostics({ subscribe }) {
|
|
||||||
const [diagnostics, setDiagnostics] = useState({});
|
|
||||||
const [statusHistory, setStatusHistory] = useState([]);
|
|
||||||
const [expandedDiags, setExpandedDiags] = useState(new Set());
|
|
||||||
const diagRef = useRef({});
|
|
||||||
|
|
||||||
// Subscribe to diagnostics
|
|
||||||
useEffect(() => {
|
|
||||||
const unsubscribe = subscribe(
|
|
||||||
'/diagnostics',
|
|
||||||
'diagnostic_msgs/DiagnosticArray',
|
|
||||||
(msg) => {
|
|
||||||
try {
|
|
||||||
const diags = {};
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
// Process each diagnostic status
|
|
||||||
(msg.status || []).forEach((status) => {
|
|
||||||
const name = status.name || 'unknown';
|
|
||||||
diags[name] = {
|
|
||||||
name,
|
|
||||||
level: status.level,
|
|
||||||
message: status.message || '',
|
|
||||||
hardware_id: status.hardware_id || '',
|
|
||||||
values: status.values || [],
|
|
||||||
timestamp: now,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add to history
|
|
||||||
setStatusHistory((prev) => [
|
|
||||||
...prev,
|
|
||||||
{
|
|
||||||
name,
|
|
||||||
level: status.level,
|
|
||||||
message: status.message || '',
|
|
||||||
timestamp: now,
|
|
||||||
},
|
|
||||||
].slice(-MAX_HISTORY));
|
|
||||||
});
|
|
||||||
|
|
||||||
setDiagnostics(diags);
|
|
||||||
diagRef.current = diags;
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error parsing diagnostics:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return unsubscribe;
|
|
||||||
}, [subscribe]);
|
|
||||||
|
|
||||||
// Calculate statistics
|
|
||||||
const stats = {
|
|
||||||
total: Object.keys(diagnostics).length,
|
|
||||||
ok: Object.values(diagnostics).filter((d) => d.level === 0).length,
|
|
||||||
warning: Object.values(diagnostics).filter((d) => d.level === 1).length,
|
|
||||||
error: Object.values(diagnostics).filter((d) => d.level === 2).length,
|
|
||||||
stale: Object.values(diagnostics).filter((d) => d.level === 3).length,
|
|
||||||
};
|
|
||||||
|
|
||||||
const overallHealth =
|
|
||||||
stats.error > 0 ? 2 : stats.warning > 0 ? 1 : stats.stale > 0 ? 3 : 0;
|
|
||||||
const overallColor = getStatusColor(overallHealth);
|
|
||||||
|
|
||||||
const sortedDiags = Object.values(diagnostics).sort((a, b) => {
|
|
||||||
// Sort by level (errors first), then by name
|
|
||||||
if (a.level !== b.level) return b.level - a.level;
|
|
||||||
return a.name.localeCompare(b.name);
|
|
||||||
});
|
|
||||||
|
|
||||||
const toggleExpanded = (name) => {
|
|
||||||
const updated = new Set(expandedDiags);
|
|
||||||
if (updated.has(name)) {
|
|
||||||
updated.delete(name);
|
|
||||||
} else {
|
|
||||||
updated.add(name);
|
|
||||||
}
|
|
||||||
setExpandedDiags(updated);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col h-full space-y-3">
|
|
||||||
{/* Health Summary */}
|
|
||||||
<div className={`rounded-lg border p-3 space-y-3 ${overallColor.bg} ${overallColor.border}`}>
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<div className={`text-xs font-bold tracking-widest ${overallColor.text}`}>
|
|
||||||
SYSTEM HEALTH
|
|
||||||
</div>
|
|
||||||
<div className={`text-xs font-bold px-3 py-1 rounded ${overallColor.text}`}>
|
|
||||||
{overallColor.label}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Stats Grid */}
|
|
||||||
<div className="grid grid-cols-5 gap-2">
|
|
||||||
<div className="bg-gray-900 rounded p-2">
|
|
||||||
<div className="text-gray-600 text-xs">TOTAL</div>
|
|
||||||
<div className="text-lg font-mono text-cyan-300 font-bold">{stats.total}</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-green-950 rounded p-2">
|
|
||||||
<div className="text-gray-600 text-xs">OK</div>
|
|
||||||
<div className="text-lg font-mono text-green-400 font-bold">{stats.ok}</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-yellow-950 rounded p-2">
|
|
||||||
<div className="text-gray-600 text-xs">WARN</div>
|
|
||||||
<div className="text-lg font-mono text-yellow-400 font-bold">{stats.warning}</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-red-950 rounded p-2">
|
|
||||||
<div className="text-gray-600 text-xs">ERROR</div>
|
|
||||||
<div className="text-lg font-mono text-red-400 font-bold">{stats.error}</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-gray-900 rounded p-2">
|
|
||||||
<div className="text-gray-600 text-xs">STALE</div>
|
|
||||||
<div className="text-lg font-mono text-gray-400 font-bold">{stats.stale}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Diagnostics Grid */}
|
|
||||||
<div className="flex-1 overflow-y-auto space-y-2">
|
|
||||||
{sortedDiags.length === 0 ? (
|
|
||||||
<div className="flex items-center justify-center h-32 text-gray-600">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="text-sm mb-2">Waiting for diagnostics</div>
|
|
||||||
<div className="text-xs text-gray-700">
|
|
||||||
Messages from /diagnostics will appear here
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
sortedDiags.map((diag) => (
|
|
||||||
<DiagnosticCard
|
|
||||||
key={diag.name}
|
|
||||||
diagnostic={diag}
|
|
||||||
expanded={expandedDiags.has(diag.name)}
|
|
||||||
onToggle={() => toggleExpanded(diag.name)}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Timeline and Info */}
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
{/* Timeline */}
|
|
||||||
<div className="bg-gray-950 rounded-lg border border-cyan-950 p-3">
|
|
||||||
<HealthTimeline statusHistory={statusHistory} maxEntries={10} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Legend */}
|
|
||||||
<div className="bg-gray-950 rounded-lg border border-cyan-950 p-3 space-y-2">
|
|
||||||
<div className="text-xs font-bold text-gray-400 tracking-widest mb-2">STATUS LEGEND</div>
|
|
||||||
<div className="space-y-1 text-xs">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="w-2 h-2 rounded-full bg-green-500" />
|
|
||||||
<span className="text-gray-400">OK — System nominal</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="w-2 h-2 rounded-full bg-yellow-500" />
|
|
||||||
<span className="text-gray-400">WARN — Check required</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="w-2 h-2 rounded-full bg-red-500" />
|
|
||||||
<span className="text-gray-400">ERROR — Immediate action</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="w-2 h-2 rounded-full bg-gray-500" />
|
|
||||||
<span className="text-gray-400">STALE — No recent data</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Topic Info */}
|
|
||||||
<div className="bg-gray-950 rounded border border-gray-800 p-2 text-xs text-gray-600 space-y-1">
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span>Topic:</span>
|
|
||||||
<span className="text-gray-500">/diagnostics (diagnostic_msgs/DiagnosticArray)</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span>Status Levels:</span>
|
|
||||||
<span className="text-gray-500">0=OK, 1=WARN, 2=ERROR, 3=STALE</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span>History Limit:</span>
|
|
||||||
<span className="text-gray-500">{MAX_HISTORY} events</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user