/** * HealthPanel — Read-only dashboard for all subsystem health status * * Displays: * - Agent Heartbeat engine status (running, config, alerts) * - Connection status (mode, SaaS reachability) * - SaaS device heartbeat status * - Memory pipeline status * - Recent alerts history * * No config editing (that's HeartbeatConfig tab). * Uses useState (not Zustand) — component-scoped state. */ import { useState, useEffect, useCallback, useRef } from 'react'; import { Activity, RefreshCw, Wifi, WifiOff, Cloud, CloudOff, Database, AlertTriangle, CheckCircle, XCircle, Clock, } from 'lucide-react'; import { intelligenceClient, type HeartbeatResult } from '../lib/intelligence-client'; import { useConnectionStore } from '../store/connectionStore'; import { useSaaSStore } from '../store/saasStore'; import { isTauriRuntime } from '../lib/tauri-gateway'; import { safeListen } from '../lib/safe-tauri'; import { createLogger } from '../lib/logger'; const log = createLogger('HealthPanel'); // === Types === interface HealthSnapshotData { timestamp: string; intelligence: { engineRunning: boolean; config: { enabled: boolean; interval_minutes: number; proactivity_level: string; }; lastTick: string | null; alertCount24h: number; totalChecks: number; }; memory: { totalEntries: number; storageSizeBytes: number; lastExtraction: string | null; }; } interface HealthCardProps { title: string; icon: React.ReactNode; status: 'green' | 'yellow' | 'gray' | 'red'; children: React.ReactNode; } const STATUS_COLORS = { green: 'text-green-500', yellow: 'text-yellow-500', gray: 'text-gray-400', red: 'text-red-500', }; const STATUS_BG = { green: 'bg-green-50 dark:bg-green-900/20', yellow: 'bg-yellow-50 dark:bg-yellow-900/20', gray: 'bg-gray-50 dark:bg-gray-800/50', red: 'bg-red-50 dark:bg-red-900/20', }; function HealthCard({ title, icon, status, children }: HealthCardProps) { return (
{icon}

{title}

{status === 'green' ? '正常' : status === 'yellow' ? '降级' : status === 'red' ? '异常' : '未启用'}
{children}
); } function formatBytes(bytes: number): string { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`; } function formatTime(isoString: string | null): string { if (!isoString) return '从未'; try { const date = new Date(isoString); return date.toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', }); } catch { return isoString; } } function formatUrgency(urgency: string): { label: string; color: string } { switch (urgency) { case 'high': return { label: '高', color: 'text-red-500' }; case 'medium': return { label: '中', color: 'text-yellow-500' }; case 'low': return { label: '低', color: 'text-blue-500' }; default: return { label: urgency, color: 'text-gray-500' }; } } // === Main Component === export function HealthPanel() { const [snapshot, setSnapshot] = useState(null); const [alerts, setAlerts] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const alertsEndRef = useRef(null); // Get live connection and SaaS state const connectionState = useConnectionStore((s) => s.connectionState); const gatewayVersion = useConnectionStore((s) => s.gatewayVersion); const connectionMode = useSaaSStore((s) => s.connectionMode); const saasReachable = useSaaSStore((s) => s.saasReachable); const consecutiveFailures = useSaaSStore((s) => s._consecutiveFailures); const isLoggedIn = useSaaSStore((s) => s.isLoggedIn); // Fetch health snapshot const fetchSnapshot = useCallback(async () => { if (!isTauriRuntime()) return; setLoading(true); setError(null); try { const { invoke } = await import('@tauri-apps/api/core'); const data = await invoke('health_snapshot', { agentId: 'zclaw-main', }); setSnapshot(data); } catch (err) { log.warn('Failed to fetch health snapshot:', err); setError(String(err)); } finally { setLoading(false); } }, []); // Fetch alert history const fetchAlerts = useCallback(async () => { if (!isTauriRuntime()) return; try { const history = await intelligenceClient.heartbeat.getHistory('zclaw-main', 100); setAlerts(history); } catch (err) { log.warn('Failed to fetch alert history:', err); } }, []); // Initial load useEffect(() => { fetchSnapshot(); fetchAlerts(); }, [fetchSnapshot, fetchAlerts]); // Subscribe to real-time alerts useEffect(() => { if (!isTauriRuntime()) return; let unlisten: (() => void) | null = null; const subscribe = async () => { unlisten = await safeListen>( 'heartbeat:alert', (newAlerts) => { // Prepend new alerts to history setAlerts((prev) => { const result: HeartbeatResult[] = [ { status: 'alert', alerts: newAlerts.map((a) => ({ title: a.title, content: a.content, urgency: a.urgency as 'low' | 'medium' | 'high', source: a.source, timestamp: a.timestamp, })), checked_items: 0, timestamp: new Date().toISOString(), }, ...prev, ]; // Keep max 100 return result.slice(0, 100); }); }, ); }; subscribe(); return () => { if (unlisten) unlisten(); }; }, []); // Auto-scroll alerts to show latest useEffect(() => { alertsEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [alerts]); // Determine SaaS card status const saasStatus: 'green' | 'yellow' | 'gray' | 'red' = !isLoggedIn ? 'gray' : saasReachable ? 'green' : 'red'; // Determine connection card status const isActuallyConnected = connectionState === 'connected'; const connectionStatus: 'green' | 'yellow' | 'gray' | 'red' = isActuallyConnected ? 'green' : connectionState === 'connecting' || connectionState === 'reconnecting' ? 'yellow' : 'red'; // Determine heartbeat card status const heartbeatStatus: 'green' | 'yellow' | 'gray' | 'red' = !snapshot ? 'gray' : snapshot.intelligence.engineRunning ? 'green' : snapshot.intelligence.config.enabled ? 'yellow' : 'gray'; // Determine memory card status const memoryStatus: 'green' | 'yellow' | 'gray' | 'red' = !snapshot ? 'gray' : snapshot.memory.totalEntries === 0 ? 'gray' : snapshot.memory.storageSizeBytes > 50 * 1024 * 1024 ? 'yellow' : 'green'; return (
{/* Header */}

系统健康

{/* Content */}
{error && (
加载失败: {error}
)} {/* Health Cards Grid */}
{/* Agent Heartbeat Card */} } status={heartbeatStatus} >
引擎状态 {snapshot?.intelligence.engineRunning ? '运行中' : '已停止'}
检查间隔 {snapshot?.intelligence.config.interval_minutes ?? '-'} 分钟
上次检查 {formatTime(snapshot?.intelligence.lastTick ?? null)}
24h 告警数 {snapshot?.intelligence.alertCount24h ?? 0}
主动性级别 {snapshot?.intelligence.config.proactivity_level ?? '-'}
{/* Connection Card */} : } status={connectionStatus} >
连接模式 {connectionMode === 'saas' ? 'SaaS 云端' : connectionMode === 'tauri' ? '本地模式' : connectionMode}
连接状态 {connectionState === 'connected' ? '已连接' : connectionState === 'connecting' ? '连接中...' : connectionState === 'reconnecting' ? '重连中...' : '未连接'}
网关版本 {gatewayVersion ?? '-'}
SaaS 可达 {saasReachable ? '是' : '否'}
{/* SaaS Device Card */} : } status={saasStatus} >
设备注册 {isLoggedIn ? '已注册' : '未注册'}
连续失败 0 ? 'text-yellow-500' : 'text-green-600'}> {consecutiveFailures}
服务状态 {saasReachable ? '在线' : isLoggedIn ? '离线 (已降级)' : '未连接'}
{/* Memory Card */} } status={memoryStatus} >
记忆条目 {snapshot?.memory.totalEntries ?? 0}
存储大小 {formatBytes(snapshot?.memory.storageSizeBytes ?? 0)}
上次提取 {formatTime(snapshot?.memory.lastExtraction ?? null)}
{/* Alerts History */}

最近告警

{alerts.reduce((sum, r) => sum + r.alerts.length, 0)} 条
{alerts.length === 0 ? (
暂无告警记录
) : ( alerts.map((result, ri) => result.alerts.map((alert, ai) => (
{alert.urgency === 'high' ? ( ) : alert.urgency === 'medium' ? ( ) : ( )}
{alert.title} {formatUrgency(alert.urgency).label}

{alert.content}

{formatTime(alert.timestamp)}
)) ) )}
); }