import { Activity, CheckCircle2, XCircle, Clock, RefreshCw, Play, Search, Server, ListIcon, AlertTriangle, Settings, ChevronDown, ChevronRight, } from "lucide-react"; import React, { useEffect, useState, useCallback, useMemo } from "react"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/collapsible"; import { Button } from "../ui/button"; import { Input } from "../ui/input"; import { Badge } from "../ui/badge"; import { Switch } from "../ui/switch"; import { Label } from "../ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "../ui/select"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from "../ui/dialog"; import { inspectionDeviceApi, inspectionCatalogApi, inspectionResultApi, inspectionExecApi, inspectionAlertApi, type DeviceExtension, type DeviceCatalog, type InspectionResult, type AlertStats, } from "../../infrastructure/inspection/inspectionApi"; import { useVaultState } from "../../application/state/useVaultState"; import { toast } from "../ui/toast"; const TIER_LABELS: Record = { core: "核心级", aggregation: "重要级", access: "一般", }; interface DeviceRow { hostId: string; hostName: string; hostAddress: string; hostPlatform: string; catalogId: string; catalogName: string; tier: string; enabled: boolean; interval: number; intervalLabel: string; lastStatus: "success" | "failed" | "never"; lastTime: number | null; lastResult?: InspectionResult; } export function InspectionCenterPage() { const { hosts, keys } = useVaultState(); const [extensions, setExtensions] = useState>({}); const [catalogs, setCatalogs] = useState([]); const [latestResults, setLatestResults] = useState>({}); const [alertStats, setAlertStats] = useState(null); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); // 筛选 const [searchText, setSearchText] = useState(""); const [filterCatalog, setFilterCatalog] = useState("all"); const [filterTier, setFilterTier] = useState("all"); const [filterStatus, setFilterStatus] = useState("all"); // 详情抽屉 const [detailHostId, setDetailHostId] = useState(null); const [detailResults, setDetailResults] = useState([]); const [detailLoading, setDetailLoading] = useState(false); // 配置弹窗 const [configOpen, setConfigOpen] = useState(false); const [configHostId, setConfigHostId] = useState(null); const [configSaving, setConfigSaving] = useState(false); const [configForm, setConfigForm] = useState({ inspection_enabled: true, catalog_id: "", tier: "", inspection_interval: "" as string, }); const loadData = useCallback(async () => { setRefreshing(true); try { const [extMap, catList, results, stats] = await Promise.all([ inspectionDeviceApi.getAll(), inspectionCatalogApi.list(), inspectionResultApi.getLatestAll(), inspectionAlertApi.getStats(), ]); setExtensions(extMap || {}); setCatalogs(Array.isArray(catList) ? catList : []); if (Array.isArray(results)) { const map: Record = {}; results.forEach((r) => { if (r.device_id) map[r.device_id] = r; }); setLatestResults(map); } setAlertStats(stats); } catch (e: any) { console.error("[InspectionCenter] 加载失败:", e); } finally { setLoading(false); setRefreshing(false); } }, []); useEffect(() => { loadData(); }, [loadData]); // 构建设备行数据 const deviceRows = useMemo(() => { return (hosts || []) .filter((h) => h && h.id) .map((host) => { const ext = extensions[host.id]; const catalog = catalogs.find((c) => c.id === ext?.catalog_id); const lastResult = latestResults[host.id]; const lastStatus: "success" | "failed" | "never" = lastResult ? lastResult.status === "success" ? "success" : "failed" : "never"; // 计算有效周期 let interval = 5; let intervalLabel = "5 分钟(系统默认)"; if (ext?.inspection_interval) { interval = ext.inspection_interval; intervalLabel = `${interval} 分钟(自定义)`; } else if (ext?.tier) { const tierDefault = ext.tier === "core" ? 5 : ext.tier === "aggregation" ? 10 : 15; interval = tierDefault; intervalLabel = `${tierDefault} 分钟(${TIER_LABELS[ext.tier] || ext.tier}默认)`; } return { hostId: host.id, hostName: host.label || host.hostname || host.address || "未命名", hostAddress: host.address || "", hostPlatform: host.platform || "ssh", catalogId: ext?.catalog_id || "", catalogName: catalog?.name || ext?.catalog_id || "未配置", tier: ext?.tier || "", enabled: ext?.inspection_enabled || false, interval, intervalLabel, lastStatus, lastTime: lastResult?.timestamp || null, lastResult, }; }); }, [hosts, extensions, catalogs, latestResults]); // 筛选 const filteredRows = useMemo(() => { return deviceRows.filter((row) => { if (searchText) { const s = searchText.toLowerCase(); if ( !row.hostName.toLowerCase().includes(s) && !row.hostAddress.toLowerCase().includes(s) && !row.catalogName.toLowerCase().includes(s) ) { return false; } } if (filterCatalog !== "all" && row.catalogId !== filterCatalog) return false; if (filterTier !== "all" && row.tier !== filterTier) return false; if (filterStatus === "enabled" && !row.enabled) return false; if (filterStatus === "disabled" && row.enabled) return false; return true; }); }, [deviceRows, searchText, filterCatalog, filterTier, filterStatus]); const stats = useMemo(() => { const total = deviceRows.length; const enabled = deviceRows.filter((r) => r.enabled).length; const success = deviceRows.filter((r) => r.lastStatus === "success").length; const failed = deviceRows.filter((r) => r.lastStatus === "failed").length; return { total, enabled, success, failed }; }, [deviceRows]); const handleScanNow = async (hostId: string, hostName: string) => { try { const host = hosts.find((h: any) => h.id === hostId); if (!host) { toast.error("未找到主机配置"); return; } // 构造连接配置 const hostConfig: any = { id: host.id, label: host.label, hostname: host.hostname, port: host.port || 22, username: host.username, }; // 密码 if (host.password) { hostConfig.password = host.password; } // 私钥 if (host.identityFileId) { const key = keys.find((k: any) => k.id === host.identityFileId); if (key && key.privateKey) { hostConfig.privateKeyContent = key.privateKey; if (key.passphrase) { hostConfig.passphrase = key.passphrase; } } } const catalogId = extensions[hostId]?.catalog_id || "linux_server"; toast.info(`正在对 ${hostName} 执行立即巡检...`); const result = await inspectionExecApi.runInspection(hostId, hostConfig, catalogId); if (result?.success) { toast.success(`巡检完成,耗时 ${(result.duration / 1000).toFixed(1)} 秒`); if (result.alerts && result.alerts.length > 0) { toast.warning(`触发 ${result.alerts.length} 条告警`); } } else { toast.error(`巡检失败: ${result?.error || "未知错误"}`); } setTimeout(() => loadData(), 1000); } catch (e: any) { toast.error("巡检触发失败: " + e.message); } }; const openDetail = async (hostId: string) => { setDetailHostId(hostId); setDetailLoading(true); try { const results = await inspectionResultApi.getLatestByDevice(hostId, 20); setDetailResults(Array.isArray(results) ? results : []); } catch (e: any) { console.error("[InspectionCenter] 加载详情失败:", e); setDetailResults([]); } finally { setDetailLoading(false); } }; // 打开配置弹窗 const openConfig = (row: DeviceRow) => { setConfigHostId(row.hostId); setConfigForm({ inspection_enabled: row.enabled, catalog_id: row.catalogId || "linux", tier: row.tier || "access", inspection_interval: extensions[row.hostId]?.inspection_interval?.toString() || "", }); setConfigOpen(true); }; // 保存配置 const saveConfig = async () => { if (!configHostId) return; setConfigSaving(true); try { const data: any = { inspection_enabled: configForm.inspection_enabled, catalog_id: configForm.catalog_id, tier: configForm.tier || null, inspection_interval: configForm.inspection_interval ? parseInt(configForm.inspection_interval, 10) : null, }; await inspectionDeviceApi.upsert(configHostId, data); toast.success("配置已保存"); setConfigOpen(false); loadData(); } catch (e: any) { toast.error("保存失败: " + e.message); } finally { setConfigSaving(false); } }; const formatTime = (ts: number | null) => { if (!ts) return "-"; const d = new Date(ts); return `${d.getMonth() + 1}/${d.getDate()} ${d.getHours().toString().padStart(2, "0")}:${d.getMinutes().toString().padStart(2, "0")}:${d.getSeconds().toString().padStart(2, "0")}`; }; const formatDuration = (ms: number | undefined) => { if (!ms) return "-"; if (ms < 1000) return `${ms}ms`; return `${(ms / 1000).toFixed(1)}s`; }; const getStatusBadge = (status: "success" | "failed" | "never") => { switch (status) { case "success": return ( 成功 ); case "failed": return ( 失败 ); default: return ( 未执行 ); } }; return (
{/* 顶部标题栏 */}

巡检中心

所有主机巡检状态概览与执行记录

{/* 统计卡片 */}
设备总数
{stats.total}
已启用巡检: {stats.enabled}
巡检成功
{stats.success}
上次巡检结果
巡检失败
{stats.failed}
需关注设备
活跃告警
{(alertStats?.critical || 0) + (alertStats?.warning || 0) + (alertStats?.info || 0)}
严重 {alertStats?.critical || 0} / 警告 {alertStats?.warning || 0}
{/* 筛选栏 */}
setSearchText(e.target.value)} className="pl-8 h-8 text-xs" />
类型:
分级:
状态:
{/* 设备列表 */}
{loading ? (
加载中...
) : filteredRows.length === 0 ? (

暂无符合条件的设备

请在主机管理中添加设备并配置巡检

) : (
{/* 表头 */}
设备
设备类型
分级
巡检周期
状态
上次时间
操作
{filteredRows.map((row) => (
openDetail(row.hostId)} >
{row.hostName}
{row.hostAddress}
{row.catalogName}
{row.tier ? ( {TIER_LABELS[row.tier] || row.tier} ) : ( - )}
{row.enabled ? row.intervalLabel : "未启用"}
{getStatusBadge(row.lastStatus)}
{formatTime(row.lastTime)}
e.stopPropagation()}>
))}
)}
{/* 巡检记录详情弹窗 */} !open && setDetailHostId(null)} > 巡检记录 ({deviceRows.find((r) => r.hostId === detailHostId)?.hostName || ""}) {detailLoading ? (
加载中...
) : detailResults.length === 0 ? (

暂无巡检记录

) : (
{detailResults.map((result, idx) => (
{result.status === "success" ? ( ) : ( )} {result.status === "success" ? "巡检成功" : "巡检失败"}
{formatTime(result.timestamp)} 耗时 {formatDuration(result.duration_ms)}
{result.metrics && Object.keys(result.metrics).length > 0 && ( <> {/* 格式化指标(精简展示) */}
{Object.entries(result.metrics) .filter(([key]) => !["load_avg", "process_count", "uptime", "load_1m", "load_5m", "load_15m"].includes(key)) .map(([key, val]) => (
{formatMetricLabel(key)} {formatMetricValue(key, val)}
))}
{/* 原始数据(可折叠) */} 查看完整数据
                            {JSON.stringify(result.metrics, null, 2)}
                          
)} {result.error_message && (
{result.error_message}
)}
))}
)}
{/* 配置弹窗 */} 巡检配置 ({deviceRows.find((r) => r.hostId === configHostId)?.hostName || ""})
{/* 启用开关 */}

开启后将按周期自动执行巡检

setConfigForm({ ...configForm, inspection_enabled: v }) } />
{/* 设备类型 */}
{/* 设备分级 */}
{/* 巡检周期 */}
setConfigForm({ ...configForm, inspection_interval: e.target.value }) } />

优先级:自定义周期 → 分级默认 → 系统默认(5 分钟)

有效周期: {configForm.inspection_interval ? `${configForm.inspection_interval} 分钟(自定义)` : configForm.tier ? `${configForm.tier === "core" ? 5 : configForm.tier === "aggregation" ? 10 : 15} 分钟(${TIER_LABELS[configForm.tier] || configForm.tier}默认)` : "5 分钟(系统默认)"}
); } // ---------- 工具函数 ---------- function formatMetricLabel(key: string): string { const map: Record = { cpu_usage: "CPU 使用率", memory_usage: "内存使用率", disk_usage: "磁盘使用率", load_1m: "负载 (1分钟)", load_5m: "负载 (5分钟)", load_15m: "负载 (15分钟)", online: "在线状态", temperature: "设备温度", power_failure_count: "电源故障数", fan_failure_count: "风扇故障数", port_down_count: "端口 Down 数", port_total_count: "端口总数", port_status: "端口状态", power_status: "电源状态", fan_status: "风扇状态", }; return map[key] || key; } function formatMetricValue(key: string, val: any): string { if (val === null || val === undefined) return "-"; if (key === "online") return val ? "在线" : "离线"; if (key === "cpu_usage" || key === "memory_usage") { return `${val}%`; } if (key === "temperature") return `${val}°C`; // disk_usage 是数组 { filesystem, usage_percent, mount } if (key === "disk_usage") { if (Array.isArray(val) && val.length > 0) { return val.map((d: any) => `${d.mount || d.filesystem}: ${d.usage_percent}%`).join(" | "); } return "-"; } // 网络设备: port_status { up_count, down_count, ports: [{name, phy}] } if (key === "port_status") { if (typeof val !== "object") return String(val); const up = val.up_count ?? 0; const down = val.down_count ?? 0; const total = up + down; // 简写端口名: GigabitEthernet0/0/1 → GE0/0/1, TenGigE → XGE 等 const shortName = (n: string) => n .replace(/GigabitEthernet/g, "GE") .replace(/TenGigabitEthernet/g, "XGE") .replace(/TwentyFiveGigE/g, "25GE") .replace(/FortyGigE/g, "40GE") .replace(/HundredGigE/g, "100GE") .replace(/Ethernet/g, "Eth"); if (Array.isArray(val.ports) && val.ports.length > 0) { const upList = val.ports.filter((p: any) => p.phy === "up").map((p: any) => shortName(p.name)); const downList = val.ports.filter((p: any) => p.phy === "down").map((p: any) => shortName(p.name)); const parts: string[] = [`${up}/${total} UP`]; if (upList.length > 0) parts.push(`↑${upList.join(",")}`); if (downList.length > 0 && downList.length <= 12) parts.push(`↓${downList.join(",")}`); else if (downList.length > 12) parts.push(`↓${downList.length}个down`); return parts.join(" "); } return `${up}/${total} UP`; } // 网络设备: power_status / fan_status { normal_count, abnormal_count } if (key === "power_status" || key === "fan_status") { if (typeof val !== "object") return String(val); const normal = val.normal_count ?? 0; const abnormal = val.abnormal_count ?? 0; if (abnormal > 0) return `⚠ ${abnormal}异常 / ${normal}正常`; return `${normal}个正常`; } return String(val); } export default InspectionCenterPage;