feat: 网络双线图+告警联系人/联系组+Agent v2.0.0 + 一键报警阈值

- HostMonitor: 网络改为双线图 rx/tx 网卡带宽吞吐(双Area线), 移除单rx图
- Agent: 默认版本号从 dev 改为 v2.0.0, 重建二进制 5.2MB
- 告警中心: 新增报警联系人/报警联系组 Tab, ContactDialog/ContactGroupDialog
- 后端: AlertContact/AlertContactGroup 模型+CRUD, monitor_alert_contacts/groups 表
- 路由: /alerts/contacts + /alerts/contact-groups 8个REST端点
- 一键报警: 更新描述为6条规则(含TCP+进程), 切换到 /alerts/quick-setup
- 测试规则: 内存>10% 触发告警已创建, 飞书通知验证通过
This commit is contained in:
attacker committed 2026-06-24 08:47:40 +00:00
1 parent 5333f6f04a
commit e1cfbaf45e
11 files changed
+810 -16

No files matched your search

+81 -8
View File
@@ -272,14 +272,22 @@ export function HostMonitor() {
</div>
</TabsContent>
{/* 网络监控 — 增加 TIME_WAIT + FD 图表 */}
<TabsContent value="network" className="mt-3 grid grid-cols-1 md:grid-cols-3 gap-3">
<MetricChartCard title="TCP 连接数(EST)" value={currentVal('tcp')} icon={<Network className="h-4 w-4 text-purple-400" />}
data={metrics.tcpEst} color="#8b5cf6" unit="个" domain={[0, 'auto']} aggType={aggType} />
<MetricChartCard title="TCP TIME_WAIT" value={currentVal('tw')} icon={<Clock className="h-4 w-4 text-amber-400" />}
data={metrics.tcpTW} color="#f59e0b" unit="个" domain={[0, 'auto']} aggType={aggType} />
<MetricChartCard title="网络接收(KB/s)" value={currentVal('rx')} icon={<Network className="h-4 w-4 text-blue-400" />}
data={metrics.rx} color="#3b82f6" unit="KB/s" domain={[0, 'auto']} aggType={aggType} />
{/* 网络监控 — 双线网卡带宽 + TIME_WAIT + TCP连接 */}
<TabsContent value="network" className="mt-3 space-y-3">
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<MetricChartCard title="TCP 连接数(EST)" value={currentVal('tcp')} icon={<Network className="h-4 w-4 text-purple-400" />}
data={metrics.tcpEst} color="#8b5cf6" unit="个" domain={[0, 'auto']} aggType={aggType} />
<MetricChartCard title="TCP TIME_WAIT" value={currentVal('tw')} icon={<Clock className="h-4 w-4 text-amber-400" />}
data={metrics.tcpTW} color="#f59e0b" unit="个" domain={[0, 'auto']} aggType={aggType} />
</div>
{/* 网卡带宽吞吐 — 双线 rx/tx */}
<DualLineChart
title="网卡带宽吞吐"
currentRx={netInfo?.net_rx_bps != null ? formatBPS(netInfo.net_rx_bps) : '--'}
currentTx={netInfo?.net_tx_bps != null ? formatBPS(netInfo.net_tx_bps) : '--'}
rxData={metrics.rx} txData={metrics.tx}
aggType={aggType} unit="KB/s"
/>
</TabsContent>
{/* ═══ 进程监控 — v2.0 新增 ═══ */}
@@ -505,3 +513,68 @@ function MetricChartCard({
</Card>
);
}
// ═══════ 双线图表(网卡带宽 rx+tx)═══════
function DualLineChart({
title, currentRx, currentTx, rxData, txData, aggType, unit,
}: {
title: string; currentRx: string; currentTx: string;
rxData: BucketPoint[]; txData: BucketPoint[];
aggType: AggType; unit: string;
}) {
// 合并 rx/tx 为统一时间序列
const chartData = rxData.map((d, i) => ({
time: d.time,
timestamp: d.timestamp,
rx: d[aggType] ?? 0,
tx: txData[i]?.[aggType] ?? 0,
}));
const rxGradId = `grad-rx-${title.replace(/\s/g, '')}`;
const txGradId = `grad-tx-${title.replace(/\s/g, '')}`;
return (
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-3">
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-1.5">
<Network className="h-4 w-4 text-blue-400" />
<span className="text-xs text-[var(--dg-text-primary)] font-medium">{title}</span>
</div>
<div className="flex items-center gap-3 text-xs">
<span><span className="inline-block w-2.5 h-2.5 rounded-sm bg-blue-400 mr-1 align-middle" />接收 <b className="text-blue-300">{currentRx}</b></span>
<span><span className="inline-block w-2.5 h-2.5 rounded-sm bg-emerald-400 mr-1 align-middle" />发送 <b className="text-emerald-300">{currentTx}</b></span>
</div>
</div>
<div className="h-56">
{chartData.length === 0 ? (
<div className="h-full flex items-center justify-center text-xs text-[var(--dg-text-muted)] border border-dashed border-[var(--dg-border)] rounded-md">暂无数据</div>
) : (
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={chartData} margin={{ top: 2, right: 2, bottom: 0, left: -10 }}>
<defs>
<linearGradient id={rxGradId} x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.3} />
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0} />
</linearGradient>
<linearGradient id={txGradId} x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#10b981" stopOpacity={0.3} />
<stop offset="95%" stopColor="#10b981" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" strokeOpacity={0.5} />
<XAxis dataKey="time" hide />
<YAxis hide domain={[0, 'auto']} />
<Tooltip contentStyle={{ backgroundColor: '#0f172a', border: '1px solid #1e293b', borderRadius: '4px', color: '#f8fafc', fontSize: '11px', padding: '4px 8px' }}
formatter={(v: number, name: string) => [`${v.toFixed(2)} ${unit}`, name === 'rx' ? '接收' : '发送']}
labelFormatter={(label) => `时间: ${label}`} />
<Area type="monotone" dataKey="rx" stroke="#3b82f6" strokeWidth={1.5} fill={`url(#${rxGradId})`} name="rx" />
<Area type="monotone" dataKey="tx" stroke="#10b981" strokeWidth={1.5} fill={`url(#${txGradId})`} name="tx" />
<Brush dataKey="time" height={24} stroke="#334155" fill="#0f172a"
startIndex={Math.max(0, chartData.length - Math.min(20, chartData.length))} />
</AreaChart>
</ResponsiveContainer>
)}
</div>
</CardContent>
</Card>
);
}