Initial commit: NetMesh smart ops platform with 3D topology, inspection reports, RCA and AI ops assistant
Some checks failed
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled

This commit is contained in:
2026-10-15 13:53:04 +08:00
commit 2f9f8da48e
3299 changed files with 926841 additions and 0 deletions

View File

@@ -0,0 +1,298 @@
import { Activity, AlertTriangle, Clock, Cpu, Layers, Settings2, Wifi } from "lucide-react";
import React, { useEffect, useState, useCallback } from "react";
import { Button } from "../ui/button";
import { Switch } from "../ui/switch";
import { Input } from "../ui/input";
import { Badge } from "../ui/badge";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "../ui/select";
import { Label } from "../ui/label";
import {
inspectionDeviceApi,
inspectionCatalogApi,
inspectionExecApi,
type DeviceCatalog,
type DeviceExtension,
type DeviceTierInfo,
type DeviceTier,
} from "../../infrastructure/inspection/inspectionApi";
import { toast } from "../ui/toast";
import { HostDetailsSection } from "../host-details";
interface HostInspectionSectionProps {
hostId: string;
hostLabel: string;
host: any;
keys: any[];
}
export function HostInspectionSection({ hostId, hostLabel, host, keys }: HostInspectionSectionProps) {
const [catalogs, setCatalogs] = useState<DeviceCatalog[]>([]);
const [tiers, setTiers] = useState<DeviceTierInfo[]>([]);
const [extension, setExtension] = useState<DeviceExtension | null>(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
// 本地编辑状态
const [enabled, setEnabled] = useState(false);
const [catalogId, setCatalogId] = useState("");
const [tier, setTier] = useState<string>("");
const [customInterval, setCustomInterval] = useState<string>("");
const loadData = useCallback(async () => {
setLoading(true);
try {
const [catalogList, tierList, ext] = await Promise.all([
inspectionCatalogApi.list(),
inspectionCatalogApi.getTiers(),
inspectionDeviceApi.get(hostId),
]);
const safeCatalogs = Array.isArray(catalogList) ? catalogList : [];
const safeTiers = Array.isArray(tierList) ? tierList : [];
setCatalogs(safeCatalogs);
setTiers(safeTiers);
setExtension(ext || null);
if (ext) {
setEnabled(ext.inspection_enabled);
setCatalogId(ext.catalog_id);
setTier(ext.tier || "");
setCustomInterval(ext.inspection_interval?.toString() || "");
} else {
// 默认值
setCatalogId(safeCatalogs[0]?.id || "linux");
setTier("access");
}
} catch (e: any) {
console.error("[Inspection] 加载失败:", e);
} finally {
setLoading(false);
}
}, [hostId]);
useEffect(() => {
loadData();
}, [loadData]);
const handleSave = async () => {
if (!catalogId) {
toast.error("请选择设备类型");
return;
}
setSaving(true);
try {
const data: Partial<DeviceExtension> = {
inspection_enabled: enabled,
catalog_id: catalogId,
tier: (tier as DeviceTier) || null,
inspection_interval: customInterval ? parseInt(customInterval, 10) : null,
// 存 hostname,用于调度器匹配 vault SSH 配置
hostname: host?.hostname || null,
};
const result = await inspectionDeviceApi.upsert(hostId, data);
setExtension(result);
toast.success(enabled ? "巡检已启用" : "巡检已停用");
} catch (e: any) {
toast.error("保存失败: " + e.message);
} finally {
setSaving(false);
}
};
const handleScanNow = async () => {
try {
if (!host || !host.hostname) {
toast.error("主机配置不完整,无法巡检");
return;
}
// 构造连接配置
const hostConfig: any = {
id: host.id || hostId,
label: hostLabel,
hostname: host.hostname,
port: host.port || 22,
username: host.username,
};
// 密码
if (host.password) {
hostConfig.password = host.password;
}
// 私钥
if (host.identityFileId && keys && keys.length) {
const key = keys.find((k) => k.id === host.identityFileId);
if (key && key.privateKey) {
hostConfig.privateKeyContent = key.privateKey;
if (key.passphrase) {
hostConfig.passphrase = key.passphrase;
}
}
}
toast.info(`正在对 ${hostLabel} 执行立即巡检...`);
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 || "未知错误"}`);
}
// 刷新扩展信息(更新 last_run_at)
loadData();
} catch (e: any) {
toast.error("巡检触发失败: " + e.message);
}
};
// 计算有效周期
const selectedTier = tiers.find((t) => t.key === tier);
const selectedCatalog = catalogs.find((c) => c.id === catalogId);
const effectiveInterval = customInterval
? `${customInterval} 分钟`
: selectedTier
? `${selectedTier.default_interval} 分钟(${selectedTier.label}默认)`
: "5 分钟(系统默认)";
return (
<HostDetailsSection
icon={<Activity size={16} />}
title="巡检配置"
hint={`为 ${hostLabel} 配置自动巡检`}
>
{loading ? (
<div className="text-sm text-muted-foreground">加载中...</div>
) : (
<div className="space-y-4">
{/* 启用开关 */}
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<div className="text-sm font-medium">启用自动巡检</div>
<div className="text-xs text-muted-foreground">
开启后系统将按周期自动巡检该设备
</div>
</div>
<Switch checked={enabled} onCheckedChange={setEnabled} />
</div>
{/* 设备类型 */}
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">设备类型</Label>
<Select value={catalogId} onValueChange={setCatalogId}>
<SelectTrigger>
<SelectValue placeholder="请选择设备类型" />
</SelectTrigger>
<SelectContent>
{catalogs.map((c) => (
<SelectItem key={c.id} value={c.id}>
<div className="flex items-center gap-2">
<span>{c.name}</span>
<span className="text-xs text-muted-foreground">{c.vendor}</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
{selectedCatalog && (
<p className="text-xs text-muted-foreground">{selectedCatalog.description}</p>
)}
</div>
{/* 设备分级 */}
<div className="space-y-1.5">
<Label className="text-xs text-muted-foreground">设备分级</Label>
<div className="flex gap-2">
{tiers.map((t) => (
<button
key={t.key}
onClick={() => setTier(tier === t.key ? "" : t.key)}
className={`flex-1 px-3 py-2 rounded-md border text-sm transition-colors ${
tier === t.key
? "border-primary bg-primary/10 text-primary"
: "border-border hover:border-primary/50"
}`}
>
<div className="font-medium">{t.label}</div>
<div className="text-xs text-muted-foreground">
默认 {t.default_interval} 分钟
</div>
</button>
))}
</div>
<p className="text-xs text-muted-foreground">
分级决定默认巡检周期,也可单独设置
</p>
</div>
{/* 自定义周期 */}
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">
自定义巡检周期(分钟,留空则使用默认)
</Label>
<Input
type="number"
min={1}
placeholder="留空使用默认值"
value={customInterval}
onChange={(e) => setCustomInterval(e.target.value)}
/>
<p className="text-xs text-muted-foreground">
有效周期:<span className="font-medium">{effectiveInterval}</span>
</p>
</div>
{/* 上次巡检信息 */}
{extension?.last_scan_at && (
<div className="flex items-center gap-2 p-3 bg-secondary/30 rounded-md text-sm">
<Clock size={14} className="text-muted-foreground" />
<div>
<span className="text-muted-foreground">上次巡检:</span>
<span className="font-medium">
{new Date(extension.last_scan_at).toLocaleString()}
</span>
<Badge
variant={extension.last_scan_status === "success" ? "default" : "destructive"}
className="ml-2"
>
{extension.last_scan_status === "success"
? "成功"
: extension.last_scan_status === "timeout"
? "超时"
: "失败"}
</Badge>
</div>
</div>
)}
{/* 操作按钮 */}
<div className="flex gap-2 pt-2">
<Button onClick={handleSave} disabled={saving} className="flex-1">
{saving ? "保存中..." : "保存配置"}
</Button>
<Button
variant="outline"
onClick={handleScanNow}
disabled={!enabled}
className="flex-1"
>
<Activity size={14} className="mr-2" />
立即巡检
</Button>
</div>
</div>
)}
</HostDetailsSection>
);
}