diff --git a/ui/social-bot/src/App.jsx b/ui/social-bot/src/App.jsx index 9b8596c..cf763c6 100644 --- a/ui/social-bot/src/App.jsx +++ b/ui/social-bot/src/App.jsx @@ -79,6 +79,9 @@ import { NodeList } from './components/NodeList.jsx'; // Gamepad teleoperation (issue #319) import { Teleop } from './components/Teleop.jsx'; +// System diagnostics (issue #340) +import { Diagnostics } from './components/Diagnostics.jsx'; + const TAB_GROUPS = [ { label: 'SOCIAL', @@ -127,6 +130,7 @@ const TAB_GROUPS = [ label: 'MONITORING', color: 'text-yellow-600', tabs: [ + { id: 'diagnostics', label: 'Diagnostics' }, { id: 'eventlog', label: 'Events' }, { id: 'bandwidth', label: 'Bandwidth' }, { id: 'nodes', label: 'Nodes' }, @@ -281,6 +285,8 @@ export default function App() { {activeTab === 'fleet' && } {activeTab === 'missions' && } + {activeTab === 'diagnostics' && } + {activeTab === 'eventlog' && } {activeTab === 'bandwidth' && } diff --git a/ui/social-bot/src/components/Diagnostics.jsx b/ui/social-bot/src/components/Diagnostics.jsx new file mode 100644 index 0000000..c657fc4 --- /dev/null +++ b/ui/social-bot/src/components/Diagnostics.jsx @@ -0,0 +1,308 @@ +/** + * 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 ( +
+ {/* Header */} +
+
+
{diagnostic.name}
+
+ {diagnostic.message} +
+
+
+ {color.label} +
+
+ + {/* Expanded details */} + {expanded && ( +
+ {diagnostic.values && diagnostic.values.length > 0 && ( +
+
KEY VALUES:
+ {diagnostic.values.slice(0, 5).map((val, i) => ( +
+ {val.key}: + {val.value} +
+ ))} + {diagnostic.values.length > 5 && ( +
+{diagnostic.values.length - 5} more values
+ )} +
+ )} + {diagnostic.hardware_id && ( +
+ Hardware: {diagnostic.hardware_id} +
+ )} +
+ )} +
+ ); +} + +function HealthTimeline({ statusHistory, maxEntries = 20 }) { + const recentHistory = statusHistory.slice(-maxEntries); + + return ( +
+
TIMELINE
+
+ {recentHistory.map((entry, i) => { + const color = getStatusColor(entry.level); + return ( +
+
+ {formatTimestamp(entry.timestamp)} +
+
+
+ {entry.name}: {entry.message} +
+
+ ); + })} +
+
+ ); +} + +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 ( +
+ {/* Health Summary */} +
+
+
+ SYSTEM HEALTH +
+
+ {overallColor.label} +
+
+ + {/* Stats Grid */} +
+
+
TOTAL
+
{stats.total}
+
+
+
OK
+
{stats.ok}
+
+
+
WARN
+
{stats.warning}
+
+
+
ERROR
+
{stats.error}
+
+
+
STALE
+
{stats.stale}
+
+
+
+ + {/* Diagnostics Grid */} +
+ {sortedDiags.length === 0 ? ( +
+
+
Waiting for diagnostics
+
+ Messages from /diagnostics will appear here +
+
+
+ ) : ( + sortedDiags.map((diag) => ( + toggleExpanded(diag.name)} + /> + )) + )} +
+ + {/* Timeline and Info */} +
+ {/* Timeline */} +
+ +
+ + {/* Legend */} +
+
STATUS LEGEND
+
+
+
+ OK — System nominal +
+
+
+ WARN — Check required +
+
+
+ ERROR — Immediate action +
+
+
+ STALE — No recent data +
+
+
+
+ + {/* Topic Info */} +
+
+ Topic: + /diagnostics (diagnostic_msgs/DiagnosticArray) +
+
+ Status Levels: + 0=OK, 1=WARN, 2=ERROR, 3=STALE +
+
+ History Limit: + {MAX_HISTORY} events +
+
+
+ ); +}