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}
+
+
+
+
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
+
+
+
+ );
+}