diff --git a/atsf_server/web/features/nodes/components/node-detail-page.tsx b/atsf_server/web/features/nodes/components/node-detail-page.tsx index 7f8f6862..2a372ae5 100644 --- a/atsf_server/web/features/nodes/components/node-detail-page.tsx +++ b/atsf_server/web/features/nodes/components/node-detail-page.tsx @@ -23,6 +23,7 @@ import { getApplyLogs } from '@/features/apply-logs/api/apply-logs'; import { deleteNode, getNodeAgentRelease, + getNodeObservability, getNodes, requestNodeOpenrestyRestart, requestNodeAgentUpdate, @@ -31,6 +32,7 @@ import { import type { NodeAgentReleaseInfo, NodeMutationPayload, + NodeObservability, } from '@/features/nodes/types'; import { CodeBlock, @@ -94,6 +96,112 @@ async function copyToClipboard(value: string) { await navigator.clipboard.writeText(value); } +function formatBytes(value?: number | null) { + if (!value || value <= 0) { + return '—'; + } + + const units = ['B', 'KB', 'MB', 'GB', 'TB']; + let current = value; + let index = 0; + while (current >= 1024 && index < units.length - 1) { + current /= 1024; + index += 1; + } + return `${current.toFixed(current >= 100 || index === 0 ? 0 : 1)} ${units[index]}`; +} + +function formatPercent(value?: number | null) { + if (value === undefined || value === null || Number.isNaN(value)) { + return '—'; + } + return `${value.toFixed(1)}%`; +} + +function formatUsageRatio(used?: number | null, total?: number | null) { + if (!used || !total || total <= 0) { + return null; + } + return Math.max(0, Math.min(100, (used / total) * 100)); +} + +function formatUptime(seconds?: number | null) { + if (!seconds || seconds <= 0) { + return '—'; + } + + const days = Math.floor(seconds / 86400); + const hours = Math.floor((seconds % 86400) / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + + if (days > 0) { + return `${days} 天 ${hours} 小时`; + } + if (hours > 0) { + return `${hours} 小时 ${minutes} 分钟`; + } + return `${minutes} 分钟`; +} + +function getHealthEventVariant( + event: NodeObservability['health_events'][number], +): 'success' | 'warning' | 'danger' | 'info' { + if (event.status === 'resolved') { + return 'success'; + } + if (event.severity === 'critical') { + return 'danger'; + } + if (event.severity === 'warning') { + return 'warning'; + } + return 'info'; +} + +function getHealthEventLabel( + event: NodeObservability['health_events'][number], +) { + return event.event_type.replaceAll('_', ' '); +} + +function MetricBar({ + label, + value, + progress, + hint, +}: { + label: string; + value: string; + progress?: number | null; + hint?: string; +}) { + return ( +
+ {label} +
+ {hint ? ( ++ {hint} +
+ ) : null} ++ {value} +
+- 最近心跳: - {isMeaningfulTime(node.last_seen_at) - ? ` ${formatRelativeTime( - node.last_seen_at, - )} · ${formatDateTime(node.last_seen_at)}` - : ' 暂无'} -
-+ 主机名 +
++ {observability.profile.hostname || node.name} +
++ 操作系统 +
++ {observability.profile.os_name || 'unknown'} + {observability.profile.os_version + ? ` ${observability.profile.os_version}` + : ''} +
++ 内核 / 架构 +
++ {observability.profile.kernel_version || 'unknown'} ·{' '} + {observability.profile.architecture || 'unknown'} +
++ 在线时长 +
++ {formatUptime(observability.profile.uptime_seconds)} +
++ CPU +
++ {observability.profile.cpu_model || 'unknown'} +
++ {observability.profile.cpu_cores || 0} 核 +
++ 总内存 +
++ {formatBytes(observability.profile.total_memory_bytes)} +
++ 总存储 +
++ {formatBytes(observability.profile.total_disk_bytes)} +
++ 上报时间 +
++ {isMeaningfulTime(observability.profile.reported_at) + ? formatDateTime(observability.profile.reported_at) + : '—'} +
+Agent:{node.agent_version || 'unknown'}
-Nginx:{node.nginx_version || 'unknown'}
-当前配置:{node.current_version || '未应用'}
-+ 网络流量 +
++ 入站: + {formatBytes(latestMetricSnapshot.network_rx_bytes)} +
++ 出站: + {formatBytes(latestMetricSnapshot.network_tx_bytes)} +
++ 磁盘 IO +
++ 读取: + {formatBytes(latestMetricSnapshot.disk_read_bytes)} +
++ 写入: + {formatBytes(latestMetricSnapshot.disk_write_bytes)} +
++ 最近心跳: + {isMeaningfulTime(node.last_seen_at) + ? ` ${formatRelativeTime(node.last_seen_at)} · ${formatDateTime(node.last_seen_at)}` + : ' 暂无'} +
+IP:{node.ip || '暂无'}
+当前配置:{node.current_version || '未应用'}
+ {latestTrafficReport ? ( ++ 最近窗口请求:{latestTrafficReport.request_count} · UV{' '} + {latestTrafficReport.unique_visitor_count} · 错误{' '} + {latestTrafficReport.error_count} +
+ ) : null} ++ {event.message || '暂无详细消息'} +
+Agent:{node.agent_version || 'unknown'}
+Nginx:{node.nginx_version || 'unknown'}
+当前配置:{node.current_version || '未应用'}
+