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,467 @@
import React, { useState, useEffect } from 'react';
import {
X,
Server,
Cpu,
HardDrive,
Activity,
AlertTriangle,
CheckCircle2,
XCircle,
Clock,
Loader2,
ChevronRight,
} from 'lucide-react';
// ============ 类型定义 ============
export type PanelType =
| 'deviceList' // 设备列表(按状态筛选)
| 'alertList' // 告警列表(按级别筛选)
| 'deviceDetail' // 单设备详情
| 'taskList'; // 巡检任务列表(按状态筛选)
export interface DetailPanelProps {
open: boolean;
onClose: () => void;
panelType: PanelType;
panelTitle: string;
// 设备列表
deviceList?: Array<{
id: string;
name: string;
ip?: string;
type: string;
status: 'online' | 'offline' | 'warning' | 'critical';
cpu?: number;
memory?: number;
lastUpdate?: number;
alertCount?: number;
}>;
// 告警列表
alertList?: Array<{
id: string;
deviceId: string;
deviceName: string;
level: 'critical' | 'warning' | 'info';
message: string;
time: string;
metric?: string;
metricValue?: string;
threshold?: string;
}>;
// 设备详情
deviceDetail?: {
id: string;
name: string;
ip?: string;
type: string;
status: 'online' | 'offline' | 'warning' | 'critical';
cpu?: number;
memory?: number;
disk?: Array<{ mount: string; usage: number }>;
lastUpdate?: number;
alerts?: Array<{
id: string;
level: string;
message: string;
time: string;
}>;
};
// 任务列表
taskList?: Array<{
deviceId: string;
deviceName: string;
ip: string;
status: 'running' | 'success' | 'failed' | 'pending';
lastScanAt?: number;
durationMs?: number;
errorMessage?: string;
interval?: number;
}>;
// 点击设备回调
onDeviceClick?: (deviceId: string) => void;
// 点击告警回调
onAlertClick?: (alertId: string) => void;
// 告警阈值(从主进程告警规则动态派生)
thresholds?: {
cpuCritical: number;
cpuWarning: number;
memCritical: number;
memWarning: number;
diskCritical: number;
diskWarning: number;
};
}
// ============ 辅助函数 ============
const statusColors: Record<string, { bg: string; text: string; dot: string; label: string }> = {
online: { bg: 'bg-emerald-500/15', text: 'text-emerald-400', dot: 'bg-emerald-500', label: '在线' },
normal: { bg: 'bg-emerald-500/15', text: 'text-emerald-400', dot: 'bg-emerald-500', label: '正常' },
offline: { bg: 'bg-slate-500/15', text: 'text-slate-400', dot: 'bg-slate-500', label: '离线' },
warning: { bg: 'bg-amber-500/15', text: 'text-amber-400', dot: 'bg-amber-500', label: '警告' },
critical: { bg: 'bg-red-500/15', text: 'text-red-400', dot: 'bg-red-500', label: '严重' },
running: { bg: 'bg-sky-500/15', text: 'text-sky-400', dot: 'bg-sky-500', label: '执行中' },
success: { bg: 'bg-emerald-500/15', text: 'text-emerald-400', dot: 'bg-emerald-500', label: '成功' },
failed: { bg: 'bg-red-500/15', text: 'text-red-400', dot: 'bg-red-500', label: '失败' },
pending: { bg: 'bg-slate-500/15', text: 'text-slate-400', dot: 'bg-slate-500', label: '待执行' },
};
const levelColors: Record<string, { bg: string; text: string; dot: string; label: string }> = {
critical: { bg: 'bg-red-500/15', text: 'text-red-400', dot: 'bg-red-500', label: '严重' },
major: { bg: 'bg-orange-500/15', text: 'text-orange-400', dot: 'bg-orange-500', label: '重要' },
warning: { bg: 'bg-amber-500/15', text: 'text-amber-400', dot: 'bg-amber-500', label: '警告' },
minor: { bg: 'bg-yellow-500/15', text: 'text-yellow-400', dot: 'bg-yellow-500', label: '次要' },
info: { bg: 'bg-sky-500/15', text: 'text-sky-400', dot: 'bg-sky-500', label: '提示' },
};
function formatRelativeTime(ts?: number): string {
if (!ts) return '-';
const diff = Date.now() - ts;
if (diff < 60000) return '刚刚';
if (diff < 3600000) return `${Math.floor(diff / 60000)} 分钟前`;
if (diff < 86400000) return `${Math.floor(diff / 3600000)} 小时前`;
return `${Math.floor(diff / 86400000)} 天前`;
}
// ============ 组件 ============
export default function MonitoringDetailPanel({
open,
onClose,
panelType,
panelTitle,
deviceList = [],
alertList = [],
deviceDetail,
taskList = [],
onDeviceClick,
thresholds = { cpuCritical: 90, cpuWarning: 80, memCritical: 85, memWarning: 80, diskCritical: 90, diskWarning: 80 },
}: DetailPanelProps) {
// ============ 设备列表渲染 ============
const renderDeviceList = () => (
<div className="space-y-2">
{deviceList.length === 0 && (
<div className="text-center py-8 text-muted-foreground text-sm">暂无数据</div>
)}
{deviceList.map((d) => {
const sc = statusColors[d.status] || statusColors.online;
return (
<div
key={d.id}
className="p-3 rounded-lg border cursor-pointer transition-all hover:border-cyan-500/40 hover:bg-cyan-500/5"
style={{ background: 'rgba(15,23,42,0.6)', borderColor: 'rgba(148,163,184,0.15)' }}
onClick={() => onDeviceClick?.(d.id)}
>
<div className="flex items-center justify-between mb-1.5">
<div className="flex items-center gap-2 min-w-0">
<div className={`w-2 h-2 rounded-full ${sc.dot} flex-shrink-0`} />
<span className="text-sm font-medium text-foreground truncate">{d.name}</span>
</div>
<ChevronRight className="w-4 h-4 text-muted-foreground flex-shrink-0" />
</div>
<div className="flex items-center gap-3 text-xs text-muted-foreground">
{d.ip && <span>{d.ip}</span>}
<span>{d.type}</span>
<span className={`${sc.text}`}>{sc.label}</span>
</div>
{(d.cpu !== undefined || d.memory !== undefined) && (
<div className="flex items-center gap-4 mt-2 text-xs">
{d.cpu !== undefined && (
<div className="flex items-center gap-1">
<Cpu className="w-3 h-3 text-muted-foreground" />
<span className="text-muted-foreground">CPU</span>
<span className={d.cpu > thresholds.cpuCritical ? 'text-red-400' : d.cpu > thresholds.cpuWarning ? 'text-amber-400' : 'text-emerald-400'}>
{d.cpu}%
</span>
</div>
)}
{d.memory !== undefined && (
<div className="flex items-center gap-1">
<HardDrive className="w-3 h-3 text-muted-foreground" />
<span className="text-muted-foreground">内存</span>
<span className={d.memory > thresholds.memCritical ? 'text-red-400' : d.memory > thresholds.memWarning ? 'text-amber-400' : 'text-emerald-400'}>
{d.memory}%
</span>
</div>
)}
</div>
)}
</div>
);
})}
</div>
);
// ============ 告警列表渲染 ============
const renderAlertList = () => (
<div className="space-y-2">
{alertList.length === 0 && (
<div className="text-center py-8 text-emerald-400 text-sm">暂无告警</div>
)}
{alertList.map((a) => {
const lc = levelColors[a.level] || levelColors.info;
return (
<div
key={a.id}
className="p-3 rounded-lg border transition-all"
style={{ background: 'rgba(15,23,42,0.6)', borderColor: 'rgba(148,163,184,0.15)' }}
>
<div className="flex items-start justify-between gap-2 mb-1">
<div className="flex items-center gap-2 min-w-0">
<div className={`w-2 h-2 rounded-full ${lc.dot} flex-shrink-0 mt-0.5`} />
<span className={`text-xs font-medium ${lc.text} flex-shrink-0`}>{lc.label}</span>
</div>
<span className="text-xs text-muted-foreground flex-shrink-0">{a.time}</span>
</div>
<div className="text-sm text-foreground mb-1">{a.message}</div>
<div className="text-xs text-muted-foreground">
设备:<span className="text-cyan-400">{a.deviceName}</span>
{a.metric && a.metricValue && (
<span className="ml-3">
{a.metric}: {a.metricValue}
{a.threshold && <span className="text-muted-foreground"> (阈值 {a.threshold})</span>}
</span>
)}
</div>
</div>
);
})}
</div>
);
// ============ 设备详情渲染 ============
const renderDeviceDetail = () => {
if (!deviceDetail) return null;
const sc = statusColors[deviceDetail.status] || statusColors.online;
return (
<div className="space-y-4">
{/* 基本信息 */}
<div className="p-3 rounded-lg border" style={{ background: 'rgba(15,23,42,0.6)', borderColor: 'rgba(148,163,184,0.15)' }}>
<div className="flex items-center gap-2 mb-2">
<Server className="w-4 h-4 text-cyan-400" />
<span className="text-sm font-medium">基本信息</span>
</div>
<div className="grid grid-cols-2 gap-2 text-xs">
<div>
<div className="text-muted-foreground mb-0.5">设备名称</div>
<div className="text-foreground font-medium">{deviceDetail.name}</div>
</div>
<div>
<div className="text-muted-foreground mb-0.5">设备类型</div>
<div className="text-foreground">{deviceDetail.type}</div>
</div>
<div>
<div className="text-muted-foreground mb-0.5">IP 地址</div>
<div className="text-foreground font-mono">{deviceDetail.ip || '-'}</div>
</div>
<div>
<div className="text-muted-foreground mb-0.5">运行状态</div>
<div className={`${sc.text} font-medium`}>{sc.label}</div>
</div>
</div>
</div>
{/* 资源使用 */}
<div className="p-3 rounded-lg border" style={{ background: 'rgba(15,23,42,0.6)', borderColor: 'rgba(148,163,184,0.15)' }}>
<div className="flex items-center gap-2 mb-3">
<Activity className="w-4 h-4 text-cyan-400" />
<span className="text-sm font-medium">资源使用</span>
</div>
<div className="space-y-3">
{/* CPU */}
<div>
<div className="flex items-center justify-between text-xs mb-1">
<span className="text-muted-foreground">CPU 使用率</span>
<span className={deviceDetail.cpu && deviceDetail.cpu > thresholds.cpuCritical ? 'text-red-400' : deviceDetail.cpu && deviceDetail.cpu > thresholds.cpuWarning ? 'text-amber-400' : 'text-foreground'}>
{deviceDetail.cpu ?? '-'}%
</span>
</div>
<div className="h-1.5 rounded-full bg-slate-700/50 overflow-hidden">
<div
className="h-full rounded-full transition-all"
style={{
width: `${deviceDetail.cpu ?? 0}%`,
background: deviceDetail.cpu && deviceDetail.cpu > thresholds.cpuCritical ? '#f87171'
: deviceDetail.cpu && deviceDetail.cpu > thresholds.cpuWarning ? '#fbbf24' : '#34d399',
}}
/>
</div>
</div>
{/* 内存 */}
<div>
<div className="flex items-center justify-between text-xs mb-1">
<span className="text-muted-foreground">内存使用率</span>
<span className={deviceDetail.memory && deviceDetail.memory > thresholds.memCritical ? 'text-red-400' : deviceDetail.memory && deviceDetail.memory > thresholds.memWarning ? 'text-amber-400' : 'text-foreground'}>
{deviceDetail.memory ?? '-'}%
</span>
</div>
<div className="h-1.5 rounded-full bg-slate-700/50 overflow-hidden">
<div
className="h-full rounded-full transition-all"
style={{
width: `${deviceDetail.memory ?? 0}%`,
background: deviceDetail.memory && deviceDetail.memory > thresholds.memCritical ? '#f87171'
: deviceDetail.memory && deviceDetail.memory > thresholds.memWarning ? '#fbbf24' : '#34d399',
}}
/>
</div>
</div>
{/* 磁盘 */}
{deviceDetail.disk && deviceDetail.disk.length > 0 && (
<div className="space-y-2">
<div className="text-xs text-muted-foreground">磁盘使用率</div>
{deviceDetail.disk.map((d, i) => (
<div key={i}>
<div className="flex items-center justify-between text-xs mb-1">
<span className="text-muted-foreground font-mono">{d.mount}</span>
<span className={d.usage > thresholds.diskCritical ? 'text-red-400' : d.usage > thresholds.diskWarning ? 'text-amber-400' : 'text-foreground'}>
{d.usage}%
</span>
</div>
<div className="h-1.5 rounded-full bg-slate-700/50 overflow-hidden">
<div
className="h-full rounded-full transition-all"
style={{
width: `${d.usage}%`,
background: d.usage > thresholds.diskCritical ? '#f87171' : d.usage > thresholds.diskWarning ? '#fbbf24' : '#34d399',
}}
/>
</div>
</div>
))}
</div>
)}
</div>
</div>
{/* 更新时间 */}
<div className="text-xs text-muted-foreground flex items-center gap-1.5">
<Clock className="w-3 h-3" />
最后更新:{formatRelativeTime(deviceDetail.lastUpdate)}
</div>
{/* 告警历史 */}
{deviceDetail.alerts && deviceDetail.alerts.length > 0 && (
<div className="p-3 rounded-lg border" style={{ background: 'rgba(15,23,42,0.6)', borderColor: 'rgba(148,163,184,0.15)' }}>
<div className="flex items-center gap-2 mb-2">
<AlertTriangle className="w-4 h-4 text-amber-400" />
<span className="text-sm font-medium">告警记录</span>
</div>
<div className="space-y-2">
{deviceDetail.alerts.map((a) => {
const lc = levelColors[a.level] || levelColors.info;
return (
<div key={a.id} className="flex items-start gap-2 text-xs">
<div className={`w-1.5 h-1.5 rounded-full ${lc.dot} flex-shrink-0 mt-1`} />
<div className="flex-1 min-w-0">
<div className="text-foreground">{a.message}</div>
<div className="text-muted-foreground mt-0.5">{a.time}</div>
</div>
</div>
);
})}
</div>
</div>
)}
</div>
);
};
// ============ 任务列表渲染 ============
const renderTaskList = () => (
<div className="space-y-2">
{taskList.length === 0 && (
<div className="text-center py-8 text-muted-foreground text-sm">暂无数据</div>
)}
{taskList.map((t) => {
const sc = statusColors[t.status] || statusColors.pending;
const durationStr = t.durationMs ? `${(t.durationMs / 1000).toFixed(1)}s` : null;
const intervalStr = t.interval ? `${t.interval}秒` : null;
return (
<div
key={t.deviceId}
className="p-3 rounded-lg border transition-all"
style={{ background: 'rgba(15,23,42,0.6)', borderColor: 'rgba(148,163,184,0.15)' }}
>
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-2 min-w-0">
{t.status === 'running' && <Loader2 className="w-3.5 h-3.5 text-sky-400 animate-spin flex-shrink-0" />}
{t.status === 'success' && <CheckCircle2 className="w-3.5 h-3.5 text-emerald-400 flex-shrink-0" />}
{t.status === 'failed' && <XCircle className="w-3.5 h-3.5 text-red-400 flex-shrink-0" />}
{t.status === 'pending' && <Clock className="w-3.5 h-3.5 text-slate-400 flex-shrink-0" />}
<span className="text-sm font-medium text-foreground truncate">{t.deviceName}</span>
</div>
<span className={`text-xs font-medium ${sc.text} flex-shrink-0`}>{sc.label}</span>
</div>
<div className="text-xs text-muted-foreground font-mono mb-1">{t.ip}</div>
<div className="flex items-center gap-3 text-xs text-muted-foreground flex-wrap">
{t.lastScanAt && <span>上次 {formatRelativeTime(t.lastScanAt)}</span>}
{durationStr && <span>耗时 {durationStr}</span>}
{intervalStr && <span>间隔 {intervalStr}</span>}
</div>
{t.errorMessage && (
<div className="mt-2 text-xs text-red-400 bg-red-500/10 p-2 rounded">
{t.errorMessage}
</div>
)}
</div>
);
})}
</div>
);
// ============ 主渲染 ============
return (
<>
{/* 遮罩 */}
<div
className={`absolute inset-0 bg-black/50 z-40 transition-opacity ${
open ? 'opacity-100' : 'opacity-0 pointer-events-none'
}`}
onClick={onClose}
/>
{/* 面板 */}
<div
className={`absolute right-0 top-0 bottom-0 z-50 transition-transform duration-300 ease-out flex flex-col ${
open ? 'translate-x-0' : 'translate-x-full'
}`}
style={{
width: '420px',
background: '#0f172a',
borderLeft: '1px solid rgba(148,163,184,0.2)',
}}
>
{/* 标题栏 */}
<div
className="flex items-center justify-between px-4 py-3 flex-shrink-0"
style={{ borderBottom: '1px solid rgba(148,163,184,0.15)' }}
>
<div className="flex items-center gap-2">
<div
className="w-1 h-5 rounded-full"
style={{ background: 'linear-gradient(180deg, #06b6d4, #3b82f6)' }}
/>
<span className="text-sm font-medium text-foreground">{panelTitle}</span>
</div>
<button
onClick={onClose}
className="p-1 rounded hover:bg-slate-700/50 text-muted-foreground hover:text-foreground transition-colors"
>
<X className="w-4 h-4" />
</button>
</div>
{/* 内容区 */}
<div className="flex-1 overflow-y-auto p-3">
{panelType === 'deviceList' && renderDeviceList()}
{panelType === 'alertList' && renderAlertList()}
{panelType === 'deviceDetail' && renderDeviceDetail()}
{panelType === 'taskList' && renderTaskList()}
</div>
</div>
</>
);
}