'use client'; import {useMemo, useState} from 'react'; import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'; import {Loader2, Trash2} from 'lucide-react'; import {toast} from 'sonner'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import {Button} from '@/components/ui/button'; import {Card, CardContent, CardDescription, CardHeader, CardTitle} from '@/components/ui/card'; import {formatDateTime} from '@/lib/utils'; import {NodeService} from '@/lib/services/openflare'; import {NodeStatusBadge} from './node-status-badge'; import { formatBytes, formatRelativeTime, formatUptime, formatUsageRatio, getErrorMessage, getHealthEventLabel, getHealthEventTone, isMeaningfulTime, } from './node-utils'; type HealthEventFilter = 'all' | 'active' | 'resolved'; function MetricBar({ label, value, progress, hint, }: { label: string; value: string; progress?: number | null; hint?: string; }) { return (

{label}

{hint ?

{hint}

: null}

{value}

{progress !== null && progress !== undefined ? (
) : null}
); } export function NodeObservability({ nodeId, connectionHint = '活动连接数', }: { nodeId: number; connectionHint?: string; }) { const queryClient = useQueryClient(); const [healthEventFilter, setHealthEventFilter] = useState('all'); const [cleanupOpen, setCleanupOpen] = useState(false); const observabilityQuery = useQuery({ queryKey: ['openflare', 'node-observability', nodeId], queryFn: () => NodeService.getObservability(nodeId, { hours: 24, limit: 48 }), refetchInterval: 10000, }); const cleanupMutation = useMutation({ mutationFn: () => NodeService.cleanupHealthEvents(nodeId), onSuccess: async (result) => { toast.success( result.deleted_count > 0 ? `已清理 ${result.deleted_count} 条健康事件日志` : '当前没有可清理的健康事件日志', ); setCleanupOpen(false); await queryClient.invalidateQueries({ queryKey: ['openflare', 'node-observability', nodeId], }); }, onError: (error) => toast.error(getErrorMessage(error)), }); const observability = observabilityQuery.data ?? null; const profile = observability?.profile ?? null; const latestMetric = observability?.metric_snapshots?.[0] ?? null; const activeHealthEvents = useMemo( () => observability?.health_events.filter((event) => event.status === 'active') ?? [], [observability?.health_events], ); const resolvedHealthEvents = useMemo( () => observability?.health_events.filter((event) => event.status === 'resolved') ?? [], [observability?.health_events], ); const filteredHealthEvents = useMemo(() => { switch (healthEventFilter) { case 'active': return activeHealthEvents; case 'resolved': return resolvedHealthEvents; default: return observability?.health_events ?? []; } }, [activeHealthEvents, healthEventFilter, observability?.health_events, resolvedHealthEvents]); const memoryUsageRatio = formatUsageRatio( latestMetric?.memory_used_bytes, latestMetric?.memory_total_bytes, ); const storageUsageRatio = formatUsageRatio( latestMetric?.storage_used_bytes, latestMetric?.storage_total_bytes, ); if (observabilityQuery.isLoading) { return ( 加载运行观测数据中... ); } if (observabilityQuery.isError) { return (

{getErrorMessage(observabilityQuery.error)}

); } return ( <>
运行诊断 {activeHealthEvents.length ? `${activeHealthEvents.length} 个活动异常` : '运行稳定'} 系统核心 {profile?.hostname || '—'} 在线时长 {formatUptime(profile?.uptime_seconds)}
系统画像 节点上报的主机与硬件信息 {profile ? ( <>
操作系统 {profile.os_name || 'unknown'} {profile.os_version ? ` ${profile.os_version}` : ''}
内核 / 架构 {profile.kernel_version || 'unknown'} · {profile.architecture || 'unknown'}
CPU {profile.cpu_model || 'unknown'}({profile.cpu_cores || 0} 核)
总内存 {formatBytes(profile.total_memory_bytes)}
总存储 {formatBytes(profile.total_disk_bytes)}
上报时间 {isMeaningfulTime(profile.reported_at) ? formatDateTime(profile.reported_at) : '—'}
) : (

节点已接入,但尚未上报完整系统画像。

)}
实时资源快照 最近一次 metrics 上报 {latestMetric ? (
) : (

节点已接入,但尚未上报资源快照。

)}
健康事件时间线 保留活动与已恢复事件,用于运行异常诊断
{observability?.health_events.length ? ( <>
{(['all', 'active', 'resolved'] as const).map((filter) => ( ))}
{filteredHealthEvents.slice(0, 8).map((event) => (

{event.message || '暂无详细消息'}

首次触发: {isMeaningfulTime(event.first_triggered_at) ? ` ${formatDateTime(event.first_triggered_at)}` : ' —'}

最近触发: {isMeaningfulTime(event.last_triggered_at) ? ` ${formatDateTime(event.last_triggered_at)}` : ' —'}

恢复时间: {isMeaningfulTime(event.resolved_at) && event.resolved_at ? ` ${formatDateTime(event.resolved_at)}` : ' —'}

))} {filteredHealthEvents.length === 0 ? (

当前筛选下没有健康事件。

) : null} ) : (

节点当前还没有上报健康事件记录。

)}
清理健康事件日志 该操作会删除此节点在控制端记录的所有健康诊断事件历史,不会影响后续新事件上报。 取消 cleanupMutation.mutate()} > {cleanupMutation.isPending ? '清理中...' : '确认清理'} ); }