feat: 监控告警体系统一重构 + 自动化任务调度引擎 + 监控大屏拆分

告警中心
- 新增统一告警(alert_unified)、监控告警规则(monitor_alert_rule)、指标目录(alert_metric_catalog)
- 通知策略与消息分发(alert_notify_policy/dispatch/message)、静默规则
- 告警引擎按阈值/SNMP 拆分(alert_engine_monitor/snmp)

监控采集
- SNMP 常量/模板与历史指标(snmp templates/constants、snmp_metrics_history、snmp_collect)
- 中间件监控 Redis 采集与种子(middleware_monitor_redis/seed)
- 站点探测 pkg(siteprobe) + 站点视图(site_view)

自动化任务
- cron 调度器(cronscheduler)、脚本运行器(scriptrunner)、系统任务(systask)及运行记录(cron_job_run、system_task)

审计与凭证
- 审计设置(audit_setting) + 历史清理(auditcleaner)

前端
- 监控页面(host/middleware/site/snmp)拆分为子模块并大屏化
- 告警中心多 Tab(规则/指标目录/通知策略/静默)、任务管理组件重构、Agent CLI Markdown 渲染
- 移除旧脚本卡片/详情/日志组件,统一为 ScriptTable/ScriptCodeEditor/TerminalOutput

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
LoganandClaude committed 2026-07-01 23:44:45 +08:00
1 parent 092d0e4b9e
commit 471353e3d7
190 files changed
+16640 -3484

No files matched your search

@@ -0,0 +1,225 @@
// 站点拨测 — 按协议可观测图表(拖拽选区 + 暗黑 Tooltip)// Logan.Li
import {
Area, Bar, BarChart, CartesianGrid, Cell, ComposedChart, Line,
ReferenceArea, ReferenceLine, Tooltip, XAxis, YAxis,
} from 'recharts';
import { Globe, Network, Radio, Shield, Wifi } from 'lucide-react';
import type { SiteResult, SiteTask } from '../helpers';
import { fmtMs, fmtSslDays, normalizeProbeType } from '../helpers';
import {
analyzeDnsResults, analyzeHttpResults, analyzePingResults,
analyzeSslResults, analyzeTcpResults,
} from './analytics';
import { SiteEventTimeline } from './SiteEventTimeline';
import { ChartExpandShell } from '../../shared/ChartExpandShell';
import { MonitorRangeChartFrame } from '../../shared/MonitorRangeChartFrame';
import { MonitorChartTooltip, MONITOR_CHART_GRID, MONITOR_CHART_TICK } from '../../shared/monitorChartTheme';
interface Props {
task: SiteTask;
results: SiteResult[];
rangeLabel?: string;
}
function SelectionArea({ left, right }: { left: string; right: string }) {
if (!left || !right || left === right) return null;
return (
<ReferenceArea
x1={left}
x2={right}
strokeOpacity={0.4}
fill="var(--dg-blue)"
fillOpacity={0.12}
stroke="var(--dg-blue)"
/>
);
}
function HttpObs({ task, results, rangeLabel = '近 6 小时' }: Props) {
const s = analyzeHttpResults(results, task);
return (
<div className="space-y-3">
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
{[
{ label: `期望 ${s.expectedCode}`, value: s.okCount, tone: 'dg-text-green' },
{ label: '4xx', value: s.err4xx, tone: 'dg-text-yellow' },
{ label: '5xx', value: s.err5xx, tone: 'dg-text-red' },
{ label: '超时/其他', value: s.otherErr, tone: 'dg-text-red' },
].map((item) => (
<div key={item.label} className="bg-[var(--dg-bg-secondary)] rounded-lg px-3 py-2 text-center">
<p className="text-[10px] text-[var(--dg-text-muted)]">{item.label}</p>
<p className={`text-base font-bold ${item.tone}`}>{item.value}</p>
</div>
))}
</div>
<ChartExpandShell
icon={<Globe className="h-3.5 w-3.5 text-[var(--dg-text-muted)]" />}
title={`HTTP 状态码时序(${rangeLabel})`}
empty={s.chart.length === 0}
>
{(h) => (
<MonitorRangeChartFrame data={s.chart} xKey="time" height={h}>
{({ displayData, chartHandlers, refLeft, refRight }) => (
<BarChart data={displayData} margin={{ top: 4, right: 8, left: 0, bottom: 4 }} {...chartHandlers}>
<CartesianGrid strokeDasharray="3 3" stroke={MONITOR_CHART_GRID} vertical={false} />
<XAxis dataKey="time" tick={MONITOR_CHART_TICK} tickLine={false} axisLine={{ stroke: MONITOR_CHART_GRID }} interval="preserveStartEnd" />
<YAxis tick={MONITOR_CHART_TICK} tickLine={false} axisLine={false} width={36} domain={[0, 'auto']} />
<Tooltip content={<MonitorChartTooltip valueFormatter={(v) => `HTTP ${v}`} nameFormatter={() => '状态码'} />} />
<ReferenceLine y={s.expectedCode} stroke="var(--dg-green)" strokeDasharray="4 4" label={{ value: `期望${s.expectedCode}`, fontSize: 10, fill: 'var(--dg-text-muted)' }} />
<Bar dataKey="code" radius={[2, 2, 0, 0]} maxBarSize={14} isAnimationActive={false}>
{displayData.map((d, i) => (
<Cell key={`${d.time}-${i}`} fill={d.ok ? 'var(--dg-green)' : d.code >= 500 ? 'var(--dg-red)' : d.code >= 400 ? '#eab308' : 'var(--dg-red)'} />
))}
</Bar>
<SelectionArea left={refLeft} right={refRight} />
</BarChart>
)}
</MonitorRangeChartFrame>
)}
</ChartExpandShell>
<SiteEventTimeline title="4xx / 5xx / 超时事件" events={s.events} />
</div>
);
}
function SslObs({ task, results }: Props) {
const s = analyzeSslResults(results, task);
const sslInfo = fmtSslDays(s.days);
return (
<div className="space-y-3">
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
{[
{ label: '证书到期', value: s.expiry || '--', tone: 'text-[var(--dg-text-primary)]' },
{ label: '剩余天数', value: sslInfo.text, tone: sslInfo.tone },
{ label: '签发者', value: s.issuer ? (s.issuer.length > 16 ? `${s.issuer.slice(0, 16)}…` : s.issuer) : '--', tone: 'text-[var(--dg-text-primary)]' },
{ label: '握手延迟', value: fmtMs(s.handshakeMs), tone: 'text-[var(--dg-text-primary)]' },
].map((item) => (
<div key={item.label} className="bg-[var(--dg-bg-secondary)] rounded-lg px-3 py-2 text-center">
<p className="text-[10px] text-[var(--dg-text-muted)]">{item.label}</p>
<p className={`text-sm font-bold truncate ${item.tone}`} title={String(item.value)}>{item.value}</p>
</div>
))}
</div>
<ChartExpandShell
icon={<Shield className="h-3.5 w-3.5 text-[var(--dg-text-muted)]" />}
title="证书剩余天数趋势"
compactHeight={160}
empty={s.chart.length === 0}
>
{(h) => (
<MonitorRangeChartFrame data={s.chart} xKey="time" height={h}>
{({ displayData, chartHandlers, refLeft, refRight }) => (
<ComposedChart data={displayData} margin={{ top: 4, right: 8, left: 0, bottom: 4 }} {...chartHandlers}>
<CartesianGrid strokeDasharray="3 3" stroke={MONITOR_CHART_GRID} vertical={false} />
<XAxis dataKey="time" tick={MONITOR_CHART_TICK} tickLine={false} axisLine={{ stroke: MONITOR_CHART_GRID }} />
<YAxis tick={MONITOR_CHART_TICK} tickLine={false} axisLine={false} width={32} />
<Tooltip content={<MonitorChartTooltip valueFormatter={(v) => `${v} 天`} nameFormatter={() => '剩余'} />} />
<ReferenceLine y={30} stroke="#eab308" strokeDasharray="4 4" label={{ value: '30天', fontSize: 10, fill: 'var(--dg-text-muted)' }} />
<ReferenceLine y={7} stroke="var(--dg-red)" strokeDasharray="4 4" label={{ value: '7天', fontSize: 10, fill: 'var(--dg-text-muted)' }} />
<Line type="monotone" dataKey="days" stroke="var(--dg-blue)" strokeWidth={2} dot={false} isAnimationActive={false} />
<SelectionArea left={refLeft} right={refRight} />
</ComposedChart>
)}
</MonitorRangeChartFrame>
)}
</ChartExpandShell>
<SiteEventTimeline title="证书 / 握手异常" events={s.events} />
</div>
);
}
function LatencyObs({ results, mode, rangeLabel = '近 6 小时' }: Props & { mode: 'PING' | 'DNS' }) {
const s = mode === 'PING' ? analyzePingResults(results) : analyzeDnsResults(results);
const label = mode === 'PING' ? 'PING 延迟波动' : 'DNS 解析延迟';
const Icon = mode === 'PING' ? Wifi : Radio;
return (
<div className="space-y-3">
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
{[
{ label: '平均', value: fmtMs(s.avg) },
{ label: '最小', value: fmtMs(s.min) },
{ label: '最大', value: fmtMs(s.max) },
{ label: 'P95', value: fmtMs(s.p95) },
{ label: '波动次数', value: String(s.spikeCount), tone: s.spikeCount > 0 ? 'dg-text-yellow' : undefined },
].map((item) => (
<div key={item.label} className="bg-[var(--dg-bg-secondary)] rounded-lg px-3 py-2 text-center">
<p className="text-[10px] text-[var(--dg-text-muted)]">{item.label}</p>
<p className={`text-sm font-bold ${item.tone || 'text-[var(--dg-text-primary)]'}`}>{item.value}</p>
</div>
))}
</div>
<ChartExpandShell
icon={<Icon className="h-3.5 w-3.5 text-[var(--dg-text-muted)]" />}
title={`${label}(${rangeLabel})`}
empty={s.chart.length === 0}
>
{(h) => (
<MonitorRangeChartFrame data={s.chart} xKey="time" height={h}>
{({ displayData, chartHandlers, refLeft, refRight }) => (
<ComposedChart data={displayData} margin={{ top: 4, right: 8, left: 0, bottom: 4 }} {...chartHandlers}>
<CartesianGrid strokeDasharray="3 3" stroke={MONITOR_CHART_GRID} vertical={false} />
<XAxis dataKey="time" tick={MONITOR_CHART_TICK} tickLine={false} axisLine={{ stroke: MONITOR_CHART_GRID }} interval="preserveStartEnd" />
<YAxis tick={MONITOR_CHART_TICK} tickLine={false} axisLine={false} width={42} tickFormatter={(v: number) => (v >= 1000 ? `${(v / 1000).toFixed(1)}s` : `${v}ms`)} />
<Tooltip content={<MonitorChartTooltip valueFormatter={(v, name) => (name === 'latency' ? fmtMs(v) : String(v))} nameFormatter={(name) => (name === 'latency' ? '延迟' : name)} />} />
<ReferenceLine y={s.avg} stroke="var(--dg-blue)" strokeDasharray="4 4" label={{ value: '均值', fontSize: 10, fill: 'var(--dg-text-muted)' }} />
<Line type="monotone" dataKey="latency" stroke="var(--dg-blue)" strokeWidth={2} dot={false} isAnimationActive={false} />
<SelectionArea left={refLeft} right={refRight} />
</ComposedChart>
)}
</MonitorRangeChartFrame>
)}
</ChartExpandShell>
<SiteEventTimeline title="延迟尖峰 / 失败事件" events={s.events} />
</div>
);
}
function TcpObs({ results, rangeLabel = '近 6 小时' }: Props) {
const s = analyzeTcpResults(results);
return (
<div className="space-y-3">
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
{[
{ label: `连通率(${rangeLabel})`, value: s.upRate, tone: 'dg-text-green' },
{ label: '不通次数', value: String(s.failCount), tone: s.failCount > 0 ? 'dg-text-red' : 'text-[var(--dg-text-primary)]' },
{ label: '最近失败', value: s.lastFailAt || '--', tone: 'text-[var(--dg-text-primary)]' },
].map((item) => (
<div key={item.label} className="bg-[var(--dg-bg-secondary)] rounded-lg px-3 py-2 text-center">
<p className="text-[10px] text-[var(--dg-text-muted)]">{item.label}</p>
<p className={`text-sm font-bold ${item.tone}`}>{item.value}</p>
</div>
))}
</div>
<ChartExpandShell
icon={<Network className="h-3.5 w-3.5 text-[var(--dg-text-muted)]" />}
title="TCP 连通状态时序"
compactHeight={120}
empty={s.chart.length === 0}
>
{(h) => (
<MonitorRangeChartFrame data={s.chart} xKey="time" height={h}>
{({ displayData, chartHandlers, refLeft, refRight }) => (
<ComposedChart data={displayData} margin={{ top: 4, right: 8, left: 0, bottom: 4 }} {...chartHandlers}>
<XAxis dataKey="time" tick={MONITOR_CHART_TICK} tickLine={false} axisLine={{ stroke: MONITOR_CHART_GRID }} interval="preserveStartEnd" />
<YAxis hide domain={[0, 100]} />
<Tooltip content={<MonitorChartTooltip valueFormatter={(v) => (v >= 100 ? '连通' : '不通')} nameFormatter={() => '状态'} />} />
<Area type="stepAfter" dataKey="up" stroke="var(--dg-green)" fill="var(--dg-green)" fillOpacity={0.2} isAnimationActive={false} />
<SelectionArea left={refLeft} right={refRight} />
</ComposedChart>
)}
</MonitorRangeChartFrame>
)}
</ChartExpandShell>
<SiteEventTimeline title="TCP 不通时间点" events={s.events} emptyText="近期 TCP 均连通" />
</div>
);
}
export function SiteProtocolObs({ task, results, rangeLabel }: Props) {
const type = normalizeProbeType(task.type);
if (type === 'SSL') return <SslObs task={task} results={results} rangeLabel={rangeLabel} />;
if (type === 'PING') return <LatencyObs task={task} results={results} mode="PING" rangeLabel={rangeLabel} />;
if (type === 'DNS') return <LatencyObs task={task} results={results} mode="DNS" rangeLabel={rangeLabel} />;
if (type === 'TCP') return <TcpObs task={task} results={results} rangeLabel={rangeLabel} />;
return <HttpObs task={task} results={results} rangeLabel={rangeLabel} />;
}