import type { MonitorTunnelApiItem, NodeMetricApiItem, TunnelMetricApiItem, ServiceMonitorApiItem, ServiceMonitorResultApiItem, ServiceMonitorMutationPayload, ServiceMonitorLimitsApiData, } from "@/api/types"; import { useCallback, useEffect, useMemo, useState } from "react"; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, } from "recharts"; import { MoreVertical, Plus, RefreshCw, Trash2, Edit, Activity, Play, Server, Zap, HardDrive, Cpu, Clock, Globe, } from "lucide-react"; import toast from "react-hot-toast"; import { getMonitorTunnels, getNodeMetrics, getTunnelMetrics, getServiceMonitorList, getServiceMonitorLimits, getServiceMonitorResults, getServiceMonitorLatestResults, createServiceMonitor, updateServiceMonitor, deleteServiceMonitor, runServiceMonitor, } from "@/api"; import { Button } from "@/shadcn-bridge/heroui/button"; import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, } from "@/shadcn-bridge/heroui/modal"; import { Input } from "@/shadcn-bridge/heroui/input"; import { Switch } from "@/shadcn-bridge/heroui/switch"; import { Select, SelectItem } from "@/shadcn-bridge/heroui/select"; import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell, } from "@/shadcn-bridge/heroui/table"; import { Chip } from "@/shadcn-bridge/heroui/chip"; import { Dropdown, DropdownTrigger, DropdownMenu, DropdownItem, } from "@/shadcn-bridge/heroui/dropdown"; import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card"; import { Progress } from "@/shadcn-bridge/heroui/progress"; import { useNodeRealtime } from "@/pages/node/use-node-realtime"; interface MonitorViewProps { nodeMap: Map; } type RealtimeNodeMetric = { receivedAt: number; cpuUsage: number; memoryUsage: number; diskUsage: number; netInBytes: number; netOutBytes: number; netInSpeed: number; netOutSpeed: number; load1: number; load5: number; load15: number; tcpConns: number; udpConns: number; uptime: number; }; const isSupportedMonitorType = (t: string): t is "tcp" | "icmp" => t === "tcp" || t === "icmp"; const formatTimestamp = (ts: number, rangeMs?: number): string => { const date = new Date(ts); const includeDate = (rangeMs ?? 0) >= 24 * 60 * 60 * 1000; if (includeDate) { return date.toLocaleString("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", }); } return date.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit", }); }; const formatDateTime = (ts: number): string => { return new Date(ts).toLocaleString("zh-CN", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", }); }; const formatBytes = (bytes: number): string => { if (!Number.isFinite(bytes) || bytes <= 0) return "0 B"; const k = 1024; const sizes = ["B", "KB", "MB", "GB", "TB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; }; const formatBytesPerSecond = (bytesPerSecond: number): string => { if (!Number.isFinite(bytesPerSecond) || bytesPerSecond <= 0) return "0 B/s"; const k = 1024; const sizes = ["B/s", "KB/s", "MB/s", "GB/s", "TB/s"]; const i = Math.floor(Math.log(bytesPerSecond) / Math.log(k)); return `${parseFloat((bytesPerSecond / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; }; const formatUptime = (seconds: number) => { if (!seconds) return "-"; const days = Math.floor(seconds / 86400); const hours = Math.floor((seconds % 86400) / 3600); if (days > 0) return `${days} 天 ${hours} 小时`; return `${hours} 小时`; }; const getColorByUsage = (usage?: number) => { if (usage === undefined || usage === null) return "default"; if (usage >= 90) return "danger"; if (usage >= 75) return "warning"; if (usage >= 50) return "primary"; return "success"; }; function ServerCard({ node, metric }: { node: any; metric: RealtimeNodeMetric | null }) { const isOnline = node.connectionStatus === "online"; return (
{node.name}
{isOnline ? "在线" : "离线"}
运行
{metric ? formatUptime(metric.uptime) : "-"}
负载
{metric ? metric.load1.toFixed(2) : "-"}
连接
{metric ? metric.tcpConns : "-"}
UDP
{metric ? metric.udpConns : "-"}
CPU
{metric ? metric.cpuUsage.toFixed(1) : "0.0"}%
RAM
{metric ? metric.memoryUsage.toFixed(1) : "0.0"}%
Disk
{metric ? metric.diskUsage.toFixed(1) : "0.0"}%
网络 ↓ {metric ? formatBytesPerSecond(metric.netInSpeed) : "0 B/s"}
总计 {metric ? formatBytes(metric.netInBytes) : "0 B"}
网络 ↑ {metric ? formatBytesPerSecond(metric.netOutSpeed) : "0 B/s"}
总计 {metric ? formatBytes(metric.netOutBytes) : "0 B"}
); } type MetricType = | "cpu" | "memory" | "disk" | "network" | "load" | "connections"; type TunnelMetricType = "traffic"; const METRICS_MAX_ROWS = 5000; const DEFAULT_SERVICE_MONITOR_LIMITS: ServiceMonitorLimitsApiData = { checkerScanIntervalSec: 30, minIntervalSec: 30, defaultIntervalSec: 60, minTimeoutSec: 1, defaultTimeoutSec: 5, maxTimeoutSec: 60, }; export function MonitorView({ nodeMap }: MonitorViewProps) { const [selectedNodeId, setSelectedNodeId] = useState(null); const [metrics, setMetrics] = useState([]); const [metricsLoading, setMetricsLoading] = useState(false); const [metricsTruncated, setMetricsTruncated] = useState(false); const [metricsError, setMetricsError] = useState(null); const [activeMetricType, setActiveMetricType] = useState("cpu"); const [metricsRangeMs, setMetricsRangeMs] = useState(60 * 60 * 1000); const [tunnels, setTunnels] = useState([]); const [tunnelsLoading, setTunnelsLoading] = useState(false); const [tunnelsError, setTunnelsError] = useState(null); const [selectedTunnelId, setSelectedTunnelId] = useState(null); const [tunnelMetrics, setTunnelMetrics] = useState([]); const [tunnelMetricsLoading, setTunnelMetricsLoading] = useState(false); const [tunnelMetricsTruncated, setTunnelMetricsTruncated] = useState(false); const [tunnelMetricsError, setTunnelMetricsError] = useState( null, ); const [tunnelRangeMs, setTunnelRangeMs] = useState(60 * 60 * 1000); const [activeTunnelMetricType, setActiveTunnelMetricType] = useState("traffic"); const [serviceMonitors, setServiceMonitors] = useState< ServiceMonitorApiItem[] >([]); const [monitorsLoading, setMonitorsLoading] = useState(false); const [monitorsError, setMonitorsError] = useState(null); const [latestResultsError, setLatestResultsError] = useState( null, ); const [monitorResults, setMonitorResults] = useState< Record >({}); const [serviceMonitorLimits, setServiceMonitorLimits] = useState(null); const [editModalOpen, setEditModalOpen] = useState(false); const [editingMonitor, setEditingMonitor] = useState(null); const [monitorForm, setMonitorForm] = useState({ name: "", type: "tcp" as "tcp" | "icmp", target: "", intervalSec: DEFAULT_SERVICE_MONITOR_LIMITS.defaultIntervalSec, timeoutSec: DEFAULT_SERVICE_MONITOR_LIMITS.defaultTimeoutSec, nodeId: 0, enabled: true, }); const [submitLoading, setSubmitLoading] = useState(false); const [accessDenied, setAccessDenied] = useState(null); const [resultsModalOpen, setResultsModalOpen] = useState(false); const [resultsMonitorId, setResultsMonitorId] = useState(null); const [resultsLimit, setResultsLimit] = useState(50); const [resultsLoading, setResultsLoading] = useState(false); const [realtimeNodeStatus, setRealtimeNodeStatus] = useState< Record >({}); const [realtimeNodeMetrics, setRealtimeNodeMetrics] = useState< Record >({}); const nodes = useMemo(() => { return Array.from(nodeMap.values()).map((n) => { const status = realtimeNodeStatus[n.id]; if (!status) { return n; } return { ...n, connectionStatus: status, }; }); }, [nodeMap, realtimeNodeStatus]); const onlineNodes = nodes.filter((n) => n.connectionStatus === "online"); const preferredNodeId = onlineNodes[0]?.id ?? nodes[0]?.id ?? 0; const resolvedServiceMonitorLimits = serviceMonitorLimits ?? DEFAULT_SERVICE_MONITOR_LIMITS; const handleRealtimeMessage = useCallback((message: any) => { const nodeId = Number(message?.id ?? 0); if (!nodeId || Number.isNaN(nodeId)) { return; } const type = String(message?.type ?? ""); const payload = message?.data; if (type === "status") { const status = Number(payload); setRealtimeNodeStatus((prev) => ({ ...prev, [nodeId]: status === 1 ? "online" : "offline", })); return; } if (type === "metric") { let raw = payload; if (typeof raw === "string") { try { raw = JSON.parse(raw); } catch { return; } } if (!raw || typeof raw !== "object") { return; } const metric = raw as Record; const receivedAt = Date.now(); const normalized: RealtimeNodeMetric = { receivedAt, cpuUsage: Number(metric.cpuUsage ?? metric.cpu_usage ?? 0), memoryUsage: Number(metric.memoryUsage ?? metric.memory_usage ?? 0), diskUsage: Number(metric.diskUsage ?? metric.disk_usage ?? 0), netInBytes: Number(metric.netInBytes ?? metric.bytes_received ?? 0), netOutBytes: Number(metric.netOutBytes ?? metric.bytes_transmitted ?? 0), netInSpeed: Number(metric.netInSpeed ?? metric.net_in_speed ?? 0), netOutSpeed: Number(metric.netOutSpeed ?? metric.net_out_speed ?? 0), load1: Number(metric.load1 ?? 0), load5: Number(metric.load5 ?? 0), load15: Number(metric.load15 ?? 0), tcpConns: Number(metric.tcpConns ?? metric.tcp_conns ?? 0), udpConns: Number(metric.udpConns ?? metric.udp_conns ?? 0), uptime: Number(metric.uptime ?? 0), }; setRealtimeNodeMetrics((prev) => ({ ...prev, [nodeId]: normalized, })); setRealtimeNodeStatus((prev) => ({ ...prev, [nodeId]: "online", })); return; } if (type === "info") { setRealtimeNodeStatus((prev) => ({ ...prev, [nodeId]: "online", })); } }, []); const { wsConnected, wsConnecting } = useNodeRealtime({ onMessage: handleRealtimeMessage, enabled: !accessDenied, }); useEffect(() => { if (!selectedNodeId && preferredNodeId > 0) { setSelectedNodeId(preferredNodeId); } }, [preferredNodeId, selectedNodeId]); const loadTunnels = useCallback(async () => { setTunnelsLoading(true); try { const response = await getMonitorTunnels(); if (response.code === 0 && response.data) { setAccessDenied(null); setTunnelsError(null); setTunnels(response.data); return; } if (response.code === 403) { setAccessDenied(response.msg || "暂无监控权限,请联系管理员授权"); setTunnelsError(null); setTunnels([]); return; } setTunnelsError(response.msg || "加载隧道列表失败"); toast.error(response.msg || "加载隧道列表失败"); } catch { setTunnelsError("加载隧道列表失败"); toast.error("加载隧道列表失败"); } finally { setTunnelsLoading(false); } }, []); useEffect(() => { void loadTunnels(); }, [loadTunnels]); useEffect(() => { const timer = window.setInterval(() => { void loadTunnels(); }, 60_000); return () => window.clearInterval(timer); }, [loadTunnels]); useEffect(() => { if (!selectedTunnelId && tunnels.length > 0) { setSelectedTunnelId(tunnels[0].id); } }, [selectedTunnelId, tunnels]); const loadTunnelMetrics = useCallback( async (tunnelId: number) => { setTunnelMetricsLoading(true); try { const end = Date.now(); const start = end - tunnelRangeMs; const response = await getTunnelMetrics(tunnelId, start, end); if (response.code === 0 && Array.isArray(response.data)) { setAccessDenied(null); setTunnelMetricsError(null); setTunnelMetricsTruncated(response.data.length >= METRICS_MAX_ROWS); const ordered = [...response.data].sort( (a, b) => a.timestamp - b.timestamp, ); setTunnelMetrics(ordered); return; } if (response.code === 403) { setAccessDenied(response.msg || "暂无监控权限,请联系管理员授权"); setTunnelMetricsTruncated(false); setTunnelMetricsError(null); return; } setTunnelMetricsTruncated(false); setTunnelMetricsError(response.msg || "加载隧道指标失败"); toast.error(response.msg || "加载隧道指标失败"); } catch { setTunnelMetricsTruncated(false); setTunnelMetricsError("加载隧道指标失败"); } finally { setTunnelMetricsLoading(false); } }, [tunnelRangeMs], ); useEffect(() => { if (selectedTunnelId) { void loadTunnelMetrics(selectedTunnelId); } }, [selectedTunnelId, loadTunnelMetrics]); useEffect(() => { if (!selectedTunnelId) return; const timer = window.setInterval(() => { void loadTunnelMetrics(selectedTunnelId); }, 30_000); return () => window.clearInterval(timer); }, [selectedTunnelId, loadTunnelMetrics]); const loadMetrics = useCallback( async (nodeId: number) => { setMetricsLoading(true); try { const end = Date.now(); const start = end - metricsRangeMs; const response = await getNodeMetrics(nodeId, start, end); if (response.code === 0 && Array.isArray(response.data)) { setAccessDenied(null); setMetricsError(null); setMetricsTruncated(response.data.length >= METRICS_MAX_ROWS); const ordered = [...response.data].sort( (a, b) => a.timestamp - b.timestamp, ); setMetrics(ordered); return; } if (response.code === 403) { setAccessDenied(response.msg || "暂无监控权限,请联系管理员授权"); setMetricsTruncated(false); setMetricsError(null); return; } setMetricsTruncated(false); setMetricsError(response.msg || "加载指标失败"); toast.error(response.msg || "加载指标失败"); } catch { setMetricsTruncated(false); setMetricsError("加载指标失败"); } finally { setMetricsLoading(false); } }, [metricsRangeMs], ); const loadServiceMonitors = useCallback(async () => { setMonitorsLoading(true); try { const response = await getServiceMonitorList(); if (response.code === 0 && response.data) { setAccessDenied(null); setMonitorsError(null); setServiceMonitors(response.data); return; } if (response.code === 403) { setAccessDenied(response.msg || "暂无监控权限,请联系管理员授权"); setMonitorsError(null); return; } setMonitorsError(response.msg || "加载服务监控失败"); toast.error(response.msg || "加载服务监控失败"); } catch { setMonitorsError("加载服务监控失败"); } finally { setMonitorsLoading(false); } }, []); const loadServiceMonitorLimits = useCallback(async () => { try { const response = await getServiceMonitorLimits(); if (response.code === 0 && response.data) { setAccessDenied(null); setServiceMonitorLimits(response.data); return; } if (response.code === 403) { setAccessDenied(response.msg || "暂无监控权限,请联系管理员授权"); return; } } catch { toast.error("加载服务监控限制失败,已使用默认值"); } }, []); const loadMonitorResults = useCallback( async (monitorId: number, limit = 20) => { try { const response = await getServiceMonitorResults(monitorId, limit); if (response.code === 0 && response.data) { setMonitorResults((prev) => ({ ...prev, [monitorId]: response.data, })); return; } if (response.code === 403) { setAccessDenied(response.msg || "暂无监控权限,请联系管理员授权"); } } catch { toast.error("加载监控记录失败"); } }, [], ); const loadLatestMonitorResults = useCallback(async () => { try { const response = await getServiceMonitorLatestResults(); if (response.code === 0 && Array.isArray(response.data)) { setAccessDenied(null); setLatestResultsError(null); setMonitorResults((prev) => { const next: Record = { ...prev, }; response.data.forEach((r) => { const monitorId = Number(r?.monitorId ?? 0); if (monitorId <= 0) return; const existing = next[monitorId]; if (existing && existing.length > 1) { const rest = existing.filter((x) => x.id !== r.id); next[monitorId] = [r, ...rest]; return; } next[monitorId] = [r]; }); return next; }); return; } if (response.code === 403) { setAccessDenied(response.msg || "暂无监控权限,请联系管理员授权"); setLatestResultsError(null); return; } setLatestResultsError(response.msg || "加载最新监控结果失败"); } catch { setLatestResultsError("加载最新监控结果失败"); } }, []); const loadResultsForModal = useCallback(async () => { if (!resultsMonitorId) return; setResultsLoading(true); try { await loadMonitorResults(resultsMonitorId, resultsLimit); } finally { setResultsLoading(false); } }, [loadMonitorResults, resultsLimit, resultsMonitorId]); useEffect(() => { void loadServiceMonitors(); void loadServiceMonitorLimits(); void loadLatestMonitorResults(); }, [loadLatestMonitorResults, loadServiceMonitorLimits, loadServiceMonitors]); useEffect(() => { const timer = window.setInterval(() => { void loadServiceMonitors(); void loadLatestMonitorResults(); }, 30_000); return () => window.clearInterval(timer); }, [loadLatestMonitorResults, loadServiceMonitors]); useEffect(() => { if (selectedNodeId) { loadMetrics(selectedNodeId); } }, [selectedNodeId, loadMetrics]); useEffect(() => { if (!selectedNodeId) return; const timer = window.setInterval(() => { void loadMetrics(selectedNodeId); }, 15_000); return () => window.clearInterval(timer); }, [selectedNodeId, loadMetrics]); useEffect(() => { if (!resultsModalOpen || !resultsMonitorId) return; void loadResultsForModal(); }, [resultsModalOpen, resultsMonitorId, resultsLimit, loadResultsForModal]); const chartData = metrics.map((m) => ({ time: formatTimestamp(m.timestamp, metricsRangeMs), cpu: m.cpuUsage, memory: m.memoryUsage, disk: m.diskUsage, netIn: m.netInSpeed, netOut: m.netOutSpeed, load1: m.load1, load5: m.load5, load15: m.load15, tcp: m.tcpConns, udp: m.udpConns, })); const getChartConfig = () => { switch (activeMetricType) { case "cpu": return { lines: [{ dataKey: "cpu", color: "#3b82f6", name: "CPU %" }], yAxisLabel: "使用率 (%)", }; case "memory": return { lines: [{ dataKey: "memory", color: "#8b5cf6", name: "内存 %" }], yAxisLabel: "使用率 (%)", }; case "disk": return { lines: [{ dataKey: "disk", color: "#f59e0b", name: "磁盘 %" }], yAxisLabel: "使用率 (%)", }; case "network": return { lines: [ { dataKey: "netIn", color: "#10b981", name: "入站速度" }, { dataKey: "netOut", color: "#ef4444", name: "出站速度" }, ], yAxisLabel: "速度 (bytes/s)", }; case "load": return { lines: [ { dataKey: "load1", color: "#3b82f6", name: "负载 1m" }, { dataKey: "load5", color: "#8b5cf6", name: "负载 5m" }, { dataKey: "load15", color: "#f59e0b", name: "负载 15m" }, ], yAxisLabel: "负载值", }; case "connections": return { lines: [ { dataKey: "tcp", color: "#3b82f6", name: "TCP 连接" }, { dataKey: "udp", color: "#10b981", name: "UDP 连接" }, ], yAxisLabel: "连接数", }; } }; const tunnelChartData = tunnelMetrics.map((m) => ({ time: formatTimestamp(m.timestamp, tunnelRangeMs), bytesIn: m.bytesIn, bytesOut: m.bytesOut, })); const getTunnelChartConfig = () => { return { lines: [ { dataKey: "bytesIn", color: "#10b981", name: "入站流量" }, { dataKey: "bytesOut", color: "#ef4444", name: "出站流量" }, ], yAxisLabel: "流量", }; }; const handleOpenEditModal = (monitor?: ServiceMonitorApiItem) => { if (monitor) { if (!isSupportedMonitorType(monitor.type)) { toast.error("该监控类型已不支持,仅支持删除"); return; } setEditingMonitor(monitor); setMonitorForm({ name: monitor.name, type: monitor.type as "tcp" | "icmp", target: monitor.target, intervalSec: monitor.intervalSec, timeoutSec: monitor.timeoutSec, nodeId: monitor.nodeId, enabled: monitor.enabled === 1, }); } else { setEditingMonitor(null); setMonitorForm({ name: "", type: "tcp", target: "", intervalSec: resolvedServiceMonitorLimits.defaultIntervalSec, timeoutSec: resolvedServiceMonitorLimits.defaultTimeoutSec, nodeId: 0, enabled: true, }); } setEditModalOpen(true); }; const handleSubmitMonitor = async () => { if (!monitorForm.name || !monitorForm.target) { toast.error("请填写完整信息"); return; } if ( monitorForm.type === "icmp" && (!monitorForm.nodeId || monitorForm.nodeId <= 0) ) { toast.error("ICMP 监控必须选择执行节点"); return; } if (monitorForm.intervalSec < resolvedServiceMonitorLimits.minIntervalSec) { toast.error( `检查间隔不能小于 ${resolvedServiceMonitorLimits.minIntervalSec}s`, ); return; } if ( monitorForm.timeoutSec < resolvedServiceMonitorLimits.minTimeoutSec || monitorForm.timeoutSec > resolvedServiceMonitorLimits.maxTimeoutSec ) { toast.error( `超时时间需在 ${resolvedServiceMonitorLimits.minTimeoutSec}-${resolvedServiceMonitorLimits.maxTimeoutSec}s 范围内`, ); return; } setSubmitLoading(true); try { const payload: ServiceMonitorMutationPayload = { name: monitorForm.name, type: monitorForm.type, target: monitorForm.target, intervalSec: monitorForm.intervalSec, timeoutSec: monitorForm.timeoutSec, nodeId: monitorForm.nodeId, enabled: monitorForm.enabled ? 1 : 0, }; if (editingMonitor) { const response = await updateServiceMonitor({ ...payload, id: editingMonitor.id, }); if (response.code === 0) { toast.success("更新成功"); } else { toast.error(response.msg || "更新失败"); } } else { const response = await createServiceMonitor(payload); if (response.code === 0) { toast.success("创建成功"); } else { toast.error(response.msg || "创建失败"); } } setEditModalOpen(false); void loadServiceMonitors(); void loadLatestMonitorResults(); } catch { toast.error("操作失败"); } finally { setSubmitLoading(false); } }; const handleDeleteMonitor = async (id: number) => { if (!confirm("确定删除该监控项?")) return; try { const response = await deleteServiceMonitor(id); if (response.code === 0) { toast.success("删除成功"); void loadServiceMonitors(); void loadLatestMonitorResults(); } else { toast.error(response.msg || "删除失败"); } } catch { toast.error("删除失败"); } }; const handleRunMonitor = async (id: number) => { try { const response = await runServiceMonitor(id); if (response.code === 0) { const ok = response.data?.success === 1; if (ok) { toast.success("检查成功"); } else { toast.error(response.data?.errorMessage || "检查失败"); } if (response.data) { const latest = response.data; setMonitorResults((prev) => { const next = { ...prev }; const existing = next[id]; if (existing && existing.length > 1) { next[id] = [ latest, ...existing.filter((x) => x.id !== latest.id), ]; } else { next[id] = [latest]; } return next; }); } void loadLatestMonitorResults(); if (resultsMonitorId === id && resultsModalOpen) { void loadResultsForModal(); } } else if (response.code === 403) { setAccessDenied(response.msg || "暂无监控权限,请联系管理员授权"); } else { toast.error(response.msg || "执行失败"); } } catch { toast.error("执行失败"); } }; const openResultsModal = (monitorId: number) => { setResultsMonitorId(monitorId); setResultsModalOpen(true); }; const getLatestResult = ( monitorId: number, ): ServiceMonitorResultApiItem | null => { const results = monitorResults[monitorId]; return results && results.length > 0 ? results[0] : null; }; const resultsMonitor = resultsMonitorId != null ? serviceMonitors.find((m) => m.id === resultsMonitorId) || null : null; const modalResults = resultsMonitorId != null ? monitorResults[resultsMonitorId] || [] : []; const selectedRealtimeMetric = selectedNodeId != null ? realtimeNodeMetrics[selectedNodeId] || null : null; const latestPersistedNodeTs = metrics.length > 0 ? metrics[metrics.length - 1]?.timestamp : null; const latestPersistedTunnelTs = tunnelMetrics.length > 0 ? tunnelMetrics[tunnelMetrics.length - 1]?.timestamp : null; const resolveMonitorIntervalSec = useCallback( (monitor: ServiceMonitorApiItem) => { let interval = Number(monitor.intervalSec ?? 0); if (!interval || interval <= 0) { interval = resolvedServiceMonitorLimits.defaultIntervalSec; } return Math.max(interval, resolvedServiceMonitorLimits.minIntervalSec); }, [resolvedServiceMonitorLimits.defaultIntervalSec, resolvedServiceMonitorLimits.minIntervalSec], ); const isResultStale = useCallback( (monitor: ServiceMonitorApiItem, latestResult: ServiceMonitorResultApiItem | null) => { if (monitor.enabled !== 1) { return false; } if (!latestResult || !latestResult.timestamp) { return false; } const intervalMs = resolveMonitorIntervalSec(monitor) * 1000; const budgetMs = intervalMs + resolvedServiceMonitorLimits.checkerScanIntervalSec * 1000 + 5000; return Date.now() - latestResult.timestamp > budgetMs; }, [resolveMonitorIntervalSec, resolvedServiceMonitorLimits.checkerScanIntervalSec], ); const monitorSummary = useMemo(() => { let disabled = 0; let ok = 0; let fail = 0; let unknown = 0; let stale = 0; serviceMonitors.forEach((m) => { if (m.enabled !== 1) { disabled += 1; return; } const latest = getLatestResult(m.id); if (!latest) { unknown += 1; return; } if (isResultStale(m, latest)) { stale += 1; } if (latest.success === 1) { ok += 1; } else { fail += 1; } }); return { disabled, ok, fail, unknown, stale }; }, [getLatestResult, isResultStale, serviceMonitors]); const chartConfig = getChartConfig(); const tunnelChartConfig = getTunnelChartConfig(); const nodeYAxisTickFormatter = (value: unknown) => { const n = Number(value); if (!Number.isFinite(n)) return ""; switch (activeMetricType) { case "network": return formatBytesPerSecond(n); case "cpu": case "memory": case "disk": return `${n.toFixed(0)}%`; case "load": return n.toFixed(1); case "connections": return String(Math.round(n)); } }; const nodeTooltipFormatter = (value: unknown) => { const n = Number(value); if (!Number.isFinite(n)) return "-"; switch (activeMetricType) { case "network": return formatBytesPerSecond(n); case "cpu": case "memory": case "disk": return `${n.toFixed(1)}%`; case "load": return n.toFixed(2); case "connections": return String(Math.round(n)); } }; const tunnelYAxisTickFormatter = (value: unknown) => { const n = Number(value); if (!Number.isFinite(n)) return ""; return formatBytes(n); }; const tunnelTooltipFormatter = (value: unknown) => { const n = Number(value); if (!Number.isFinite(n)) return "-"; return formatBytes(n); }; return (
{accessDenied && (

监控权限

{accessDenied}
如需使用监控功能,请联系管理员在用户页面授予监控权限。
)} {!accessDenied && ( <>
{nodes.map((node) => { const metric = realtimeNodeMetrics[node.id] || null; return ; })}

监控概览

{wsConnected ? "实时已连接" : wsConnecting ? "实时连接中" : "实时未连接"}
节点在线 {onlineNodes.length}/{nodes.length} 服务监控 成功 {monitorSummary.ok} / 失败 {monitorSummary.fail} 禁用 {monitorSummary.disabled} {monitorSummary.unknown > 0 ? ( 未检查 {monitorSummary.unknown} ) : null} {monitorSummary.stale > 0 ? ( 陈旧 {monitorSummary.stale} ) : null}
历史图表来自数据库,入库批量刷新约 30s(可能存在 0-30s 延迟)
节点最近入库: {latestPersistedNodeTs ? ( {formatDateTime(latestPersistedNodeTs)}(约{ Math.max( 0, Math.round((Date.now() - latestPersistedNodeTs) / 1000), )} s前) ) : ( "-" )}
实时数据更新时间: {selectedRealtimeMetric ? ( {formatDateTime(selectedRealtimeMetric.receivedAt)} ) : ( "-" )}

节点指标图表

{selectedNodeId && ( )}
{selectedNodeId && ( <>
实时
{selectedRealtimeMetric ? ( CPU {selectedRealtimeMetric.cpuUsage.toFixed(1)}% · 内存{" "} {selectedRealtimeMetric.memoryUsage.toFixed(1)}% · ↓{" "} {formatBytesPerSecond(selectedRealtimeMetric.netInSpeed)} · ↑{" "} {formatBytesPerSecond(selectedRealtimeMetric.netOutSpeed)} · 更新{" "} {formatDateTime(selectedRealtimeMetric.receivedAt)} ) : ( 暂无实时数据 )}
最近入库: {latestPersistedNodeTs ? ( {formatDateTime(latestPersistedNodeTs)}(约 {Math.max( 0, Math.round((Date.now() - latestPersistedNodeTs) / 1000), )} s前) ) : ( "-" )}
{( [ { key: "cpu", label: "CPU" }, { key: "memory", label: "内存" }, { key: "disk", label: "磁盘" }, { key: "network", label: "网络" }, { key: "load", label: "负载" }, { key: "connections", label: "连接" }, ] as { key: MetricType; label: string }[] ).map((item) => ( ))}
{metricsLoading ? (
) : metricsError ? (
{metricsError}
) : metrics.length > 0 ? ( <>
{chartConfig.lines.map((line) => ( ))}
{metricsTruncated && (
数据点过多,已截断为最近 {METRICS_MAX_ROWS}{" "} 条,建议缩小时间范围。
)} ) : (
暂无指标数据
)} )} {!selectedNodeId && (
请选择一个在线节点查看指标
)}

隧道流量趋势

{tunnelsError ? (
{tunnelsError}
) : null}
{selectedTunnelId && ( )}
{selectedTunnelId && ( <>
最近入库: {latestPersistedTunnelTs ? ( {formatDateTime(latestPersistedTunnelTs)}(约 {Math.max( 0, Math.round((Date.now() - latestPersistedTunnelTs) / 1000), )} s前) ) : ( "-" )}
{( [{ key: "traffic", label: "流量" }] as { key: TunnelMetricType; label: string; }[] ).map((item) => ( ))}
{tunnelMetricsLoading ? (
) : tunnelMetricsError ? (
{tunnelMetricsError}
) : tunnelMetrics.length > 0 ? ( <>
{tunnelChartConfig.lines.map((line) => ( ))}
{tunnelMetricsTruncated && (
数据点过多,已截断为最近 {METRICS_MAX_ROWS}{" "} 条,建议缩小时间范围。
)} ) : (
暂无指标数据
)} )} {!selectedTunnelId && (
请选择一个隧道查看指标
)}

服务监控

{monitorsError ? (
{monitorsError}
) : null} {latestResultsError ? (
{latestResultsError}
) : null} {monitorsLoading ? (
) : serviceMonitors.length > 0 ? ( 名称 类型 目标 间隔 启用 最新结果 操作 {serviceMonitors.map((monitor) => { const latestResult = getLatestResult(monitor.id); const stale = isResultStale(monitor, latestResult); return (
{monitor.name}
{monitor.nodeId > 0 && (
节点:{" "} {nodeMap.get(monitor.nodeId)?.name || monitor.nodeId}
)}
{monitor.type.toUpperCase()} {monitor.target} {monitor.intervalSec}s {monitor.enabled === 1 ? ( 启用 ) : ( 禁用 )} {latestResult ? (
{latestResult.success === 1 ? ( 成功 ) : ( 失败 )} {stale ? ( 陈旧 ) : null} {Number.isFinite(latestResult.latencyMs) ? `${latestResult.latencyMs.toFixed(0)}ms` : "-"} {formatDateTime(latestResult.timestamp)}
{latestResult.errorMessage ? (
{latestResult.errorMessage}
) : null}
) : monitor.enabled === 1 ? ( 未检查 ) : ( - )}
} onPress={() => handleRunMonitor(monitor.id)} > 立即检查 } onPress={() => openResultsModal(monitor.id)} > 查看记录 } onPress={() => handleOpenEditModal(monitor)} > 编辑 } onPress={() => handleDeleteMonitor(monitor.id) } > 删除
); })}
) : (
暂无服务监控,点击"添加监控"创建
)}
)} { setResultsModalOpen(false); setResultsMonitorId(null); }} >
监控记录 {resultsMonitor?.name ? ` - ${resultsMonitor.name}` : ""}
{resultsMonitor?.nodeId && resultsMonitor.nodeId > 0 ? (
节点:{" "} {nodeMap.get(resultsMonitor.nodeId)?.name || resultsMonitor.nodeId}
) : (
面板执行
)} {modalResults.length > 0 ? (
最新检查:{" "} {formatDateTime(modalResults[0].timestamp)}
) : null}
{resultsMonitorId != null ? (
) : null}
{resultsLoading ? (
) : modalResults.length > 0 ? ( 时间 结果 延迟 错误 {modalResults.map((r) => ( {formatDateTime(r.timestamp)} {r.success === 1 ? ( 成功 ) : ( 失败 )} {Number.isFinite(r.latencyMs) ? `${r.latencyMs.toFixed(0)}ms` : "-"} {r.errorMessage || "-"} ))}
) : (
暂无监控记录
)}
setEditModalOpen(false)}> {editingMonitor ? "编辑监控" : "添加监控"} setMonitorForm((f) => ({ ...f, name: e.target.value })) } /> setMonitorForm((f) => ({ ...f, target: e.target.value })) } />
setMonitorForm((f) => ({ ...f, intervalSec: Number(e.target.value) || resolvedServiceMonitorLimits.defaultIntervalSec, })) } /> resolvedServiceMonitorLimits.maxTimeoutSec ? `需在 ${resolvedServiceMonitorLimits.minTimeoutSec}-${resolvedServiceMonitorLimits.maxTimeoutSec}s 范围内` : undefined } isInvalid={ monitorForm.timeoutSec < resolvedServiceMonitorLimits.minTimeoutSec || monitorForm.timeoutSec > resolvedServiceMonitorLimits.maxTimeoutSec } label="超时时间(秒)" type="number" value={String(monitorForm.timeoutSec)} onChange={(e) => setMonitorForm((f) => ({ ...f, timeoutSec: Number(e.target.value) || resolvedServiceMonitorLimits.defaultTimeoutSec, })) } />
setMonitorForm((f) => ({ ...f, enabled: v })) } /> 启用
); }