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

+32 -27
View File
@@ -4,20 +4,21 @@ import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { Clock, Server, ArrowRight } from 'lucide-react';
import { dashboardStatsApi, serverApi, kubernetesApi } from '@/api';
import { dashboardStatsApi, kubernetesApi } from '@/api';
import { StatStrip } from '@/components/ui/stat-strip';
import { CostOverviewCard, TopResourceUsageCard, ResourceUsageCard } from './DashboardCharts';
import {
AlertMessagesCard,
CertificateExpiryCard,
ReleaseHistoryCard,
ServerStatusCard,
OpsOverviewCard,
KubernetesClusterCard,
} from './DashboardRecent';
import type {
DashboardRelease,
DashboardServer,
DashboardCluster,
DashboardAutomationStats,
DashboardOpsStats,
} from './DashboardRecent';
import { SkeletonDashboard } from '@/components/layout/SkeletonCard';
import { PageShell } from '@/components/layout/PageShell';
@@ -41,7 +42,6 @@ export function Dashboard() {
const [initialLoading, setInitialLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [stats, setStats] = useState<JsonData>(null);
const [servers, setServers] = useState<DashboardServer[]>([]);
const [clusters, setClusters] = useState<DashboardCluster[]>([]);
const [alerts, setAlerts] = useState<AlertEventRecord[]>([]);
const [releases, setReleases] = useState<DashboardRelease[]>([]);
@@ -51,15 +51,11 @@ export function Dashboard() {
const refreshData = useCallback(async () => {
setRefreshing(true);
try {
const [statsRes, serversRes, clustersRes, alertsRes, releasesRes, metricsRes] = await Promise.all([
const [statsRes, clustersRes, alertsRes, releasesRes, metricsRes] = await Promise.all([
dashboardStatsApi.getStats().catch((e) => {
console.error('Failed to fetch dashboard stats:', e);
return null;
}),
serverApi.getServers({ page: 1, page_size: 100 }).catch((e) => {
console.error('Failed to fetch servers:', e);
return null;
}),
kubernetesApi.getClusters().catch((e) => {
console.error('Failed to fetch k8s clusters:', e);
return null;
@@ -79,7 +75,6 @@ export function Dashboard() {
]);
setStats(statsRes);
setServers(serversRes?.list ?? []);
setClusters(clustersRes?.list ?? []);
setAlerts(Array.isArray(alertsRes) ? alertsRes : []);
setReleases(Array.isArray(releasesRes) ? releasesRes : []);
@@ -95,8 +90,8 @@ export function Dashboard() {
useEffect(() => {
refreshData();
// 每 5 秒自动刷新
const interval = setInterval(refreshData, 5000);
// 每 30 秒自动刷新(集群列表含实时统计,不宜过频)// Logan.Li
const interval = setInterval(refreshData, 30_000);
return () => clearInterval(interval);
}, [refreshData]);
@@ -108,6 +103,9 @@ export function Dashboard() {
const k8sStats = stats?.kubernetes_stats;
const alertStats = stats?.alert_stats;
const costStats = stats?.cost_stats;
const automationStats = stats?.automation_stats as DashboardAutomationStats | undefined;
const opsStats = stats?.ops_stats as DashboardOpsStats | undefined;
return (
<PageShell
@@ -136,18 +134,19 @@ export function Dashboard() {
<StatStrip
items={[
{ label: t('dashboard.stat.servers'), value: serverStats?.total ?? 0, color: 'blue' },
{ label: t('dashboard.stat.kubernetes'), value: k8sStats?.total ?? 0, color: 'purple' },
{ label: t('dashboard.stat.running'), value: serverStats?.running ?? 0, color: 'emerald' },
{ label: t('dashboard.stat.k8sClusters'), value: k8sStats?.total ?? 0, color: 'purple' },
{ label: t('dashboard.stat.healthyClusters'), value: k8sStats?.healthy ?? 0, color: 'cyan' },
{ label: t('dashboard.stat.cronJobs'), value: automationStats?.cron_job_count ?? 0, color: 'cyan' },
{ label: t('dashboard.stat.siteProbes'), value: opsStats?.site_task_count ?? 0, color: 'green' },
{ label: t('dashboard.stat.alerts'), value: alertStats?.firing ?? 0, color: alertStats?.firing > 0 ? 'red' : 'muted' },
{ label: t('dashboard.stat.critical'), value: alertStats?.critical ?? 0, color: 'red' },
{ label: t('dashboard.stat.monthlyCost'), value: `¥${Number(costStats?.month_cost ?? 0).toLocaleString()}`, color: 'amber' },
]}
/>
)}
contentClassName="space-y-6"
>
<div>
{/* 资源使用率趋势 — 第一行 */}
<div className="animate-fade-in-delay-1">
<ResourceUsageCard
avgCpu={Number(serverStats?.avg_cpu ?? 0)}
avgMemory={Number(serverStats?.avg_memory ?? 0)}
@@ -161,23 +160,29 @@ export function Dashboard() {
/>
</div>
{/* Cost & Top Resource Usage */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 animate-fade-in-delay-3">
<CostOverviewCard costStats={costStats} />
{/* 运维概览 → K8s 集群 → Top5 资源使用 */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 animate-fade-in-delay-2">
<OpsOverviewCard
automation={automationStats}
ops={opsStats}
serverCount={serverStats?.total}
hostMonitorCount={serverStats?.running}
firingAlerts={alertStats?.firing}
/>
<KubernetesClusterCard clusters={clusters} />
<TopResourceUsageCard topUsage={stats?.top_resource_usage} />
</div>
{/* Alert Messages & Certificate Expiry */}
{/* 费用概览 & 发布历史 */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 animate-fade-in-delay-3">
<AlertMessagesCard alerts={alerts} />
<CertificateExpiryCard expCerts={expCerts} />
<CostOverviewCard costStats={costStats} />
<ReleaseHistoryCard releases={releases} />
</div>
{/* Bottom Row */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 animate-fade-in-delay-4">
<ReleaseHistoryCard releases={releases} />
<ServerStatusCard servers={servers} />
<KubernetesClusterCard clusters={clusters} />
{/* 告警消息 & 证书到期提醒 — 置底 */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 animate-fade-in-delay-4">
<AlertMessagesCard alerts={alerts} />
<CertificateExpiryCard expCerts={expCerts} />
</div>
</PageShell>
);
@@ -10,8 +10,12 @@ import {
ArrowRight,
Cloud,
Shield,
Clock3,
Globe,
Database,
Server,
Circle,
Monitor,
Activity,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import type { AlertEventRecord, CertExpiryRecord } from '@/types';
@@ -27,14 +31,6 @@ export interface DashboardRelease {
[key: string]: unknown;
}
export interface DashboardServer {
id: number | string;
status: string;
hostname?: string;
ip?: string;
[key: string]: unknown;
}
export interface DashboardCluster {
id: number | string;
code?: string;
@@ -44,9 +40,34 @@ export interface DashboardCluster {
memory_usage?: number;
node_count?: number;
pod_count?: number;
ingress_count?: number;
deployment_count?: number;
service_count?: number;
[key: string]: unknown;
}
export interface DashboardAutomationStats {
cron_job_count?: number;
script_count?: number;
today_executions?: number;
success_rate?: number;
}
export interface DashboardOpsStats {
domain_count?: number;
site_task_count?: number;
middleware_count?: number;
snmp_device_count?: number;
monitor_target_count?: number;
}
export interface DashboardPipelineStats {
today_count?: number;
success?: number;
failed?: number;
running?: number;
}
/**
* DashboardRecent - 优化后的仪表盘底部卡片组件
*
@@ -336,84 +357,70 @@ export function ReleaseHistoryCard({ releases }: { releases: DashboardRelease[]
);
}
export function ServerStatusCard({ servers }: { servers: DashboardServer[] }) {
/** 运维概览 — 资产 / 监控 / 任务快捷入口 // Logan.Li */
export function OpsOverviewCard({
automation,
ops,
serverCount,
hostMonitorCount,
firingAlerts,
}: {
automation?: DashboardAutomationStats;
ops?: DashboardOpsStats;
serverCount?: number;
hostMonitorCount?: number;
firingAlerts?: number;
}) {
const { t } = useTranslation();
const navigate = useNavigate();
const runningServers = servers.filter((s) => s.status === 'running').length;
const stoppedServers = servers.filter((s) => s.status === 'stopped').length;
const maintenanceServers = servers.filter((s) => s.status === 'maintenance').length;
const tiles = [
{ key: 'servers', label: t('dashboard.ops.servers'), value: serverCount ?? 0, icon: Server, path: '/servers', color: 'text-[var(--dg-blue)]' },
{ key: 'domains', label: t('dashboard.ops.domains'), value: ops?.domain_count ?? 0, icon: Globe, path: '/domains', color: 'text-[var(--dg-cyan)]' },
{ key: 'host', label: t('dashboard.ops.hostMonitor'), value: hostMonitorCount ?? serverCount ?? 0, icon: Monitor, path: '/host-monitor', color: 'text-[var(--dg-green)]' },
{ key: 'mw', label: t('dashboard.ops.middleware'), value: ops?.middleware_count ?? 0, icon: Database, path: '/monitor-center?tab=middleware', color: 'text-[var(--dg-red)]' },
{ key: 'site', label: t('dashboard.ops.siteProbes'), value: ops?.site_task_count ?? 0, icon: Activity, path: '/monitor-center?tab=site', color: 'text-[var(--dg-yellow)]' },
{ key: 'cron', label: t('dashboard.ops.cronJobs'), value: automation?.cron_job_count ?? 0, icon: Clock3, path: '/cronjobs', color: 'text-[var(--dg-purple)]' },
];
return (
<Card className="dg-card dg-card-hover">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-[var(--dg-text-primary)]">{t('dashboard.serversStatus.title')}</CardTitle>
<CardDescription className="text-[var(--dg-text-muted)]">{t('dashboard.serversStatus.description')}</CardDescription>
<CardTitle className="text-[var(--dg-text-primary)]">{t('dashboard.ops.title')}</CardTitle>
<CardDescription className="text-[var(--dg-text-muted)]">{t('dashboard.ops.description')}</CardDescription>
</div>
<Button
variant="ghost"
size="sm"
className="dg-btn-ghost text-xs"
onClick={() => navigate('/servers')}
>
{t('dashboard.serversStatus.viewAll')}
<Button variant="ghost" size="sm" className="dg-btn-ghost text-xs" onClick={() => navigate('/dashboards')}>
{t('dashboard.ops.monitorScreen')}
<ArrowRight className="h-3.5 w-3.5 ml-1" />
</Button>
</div>
</CardHeader>
<CardContent>
{/* Status Summary */}
<div className="grid grid-cols-3 gap-3 mb-4">
<div className="text-center p-3 rounded-xl bg-[var(--dg-green)]/8 border border-[var(--dg-green)]/15">
<div className="text-2xl font-bold text-[var(--dg-green)]">{runningServers}</div>
<div className="text-xs text-[var(--dg-text-muted)] mt-1">{t('dashboard.serversStatus.running')}</div>
</div>
<div className="text-center p-3 rounded-xl bg-[var(--dg-yellow)]/8 border border-[var(--dg-yellow)]/15">
<div className="text-2xl font-bold text-[var(--dg-yellow)]">{maintenanceServers}</div>
<div className="text-xs text-[var(--dg-text-muted)] mt-1">{t('dashboard.serversStatus.maintenance')}</div>
</div>
<div className="text-center p-3 rounded-xl bg-[var(--dg-text-muted)]/8 border border-[var(--dg-border)]">
<div className="text-2xl font-bold text-[var(--dg-text-muted)]">{stoppedServers}</div>
<div className="text-xs text-[var(--dg-text-muted)] mt-1">{t('dashboard.serversStatus.stopped')}</div>
</div>
</div>
{/* Server List */}
<div className="space-y-1.5">
{servers.slice(0, 4).map((server) => (
<div
key={server.id}
className="flex items-center gap-3 p-2.5 rounded-lg bg-[var(--dg-bg-tertiary)]/50 hover:bg-[var(--dg-bg-hover)] transition-all duration-200"
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2 mb-3">
{tiles.map((tile) => (
<button
key={tile.key}
type="button"
onClick={() => navigate(tile.path)}
className="flex items-center gap-2.5 p-2.5 rounded-xl bg-[var(--dg-bg-tertiary)]/50 hover:bg-[var(--dg-bg-hover)] transition-all text-left"
>
<Circle
className={cn(
'h-2.5 w-2.5 flex-shrink-0 fill-current',
server.status === 'running' ? 'text-[var(--dg-green)]' :
server.status === 'maintenance' ? 'text-[var(--dg-yellow)]' :
'text-[var(--dg-text-muted)]'
)}
/>
<div className="flex-1 min-w-0">
<p className="text-sm text-[var(--dg-text-primary)] truncate">{server.hostname}</p>
<p className="text-xs text-[var(--dg-text-muted)]">{server.ip}</p>
<tile.icon className={cn('h-4 w-4 shrink-0', tile.color)} />
<div className="min-w-0">
<div className="text-base font-bold text-[var(--dg-text-primary)] leading-none">{tile.value}</div>
<div className="text-[10px] text-[var(--dg-text-muted)] mt-0.5 truncate">{tile.label}</div>
</div>
<div className="text-right flex-shrink-0">
<p className="text-xs text-[var(--dg-text-muted)]">
{t('dashboard.serversStatus.cpu')} <span className="text-[var(--dg-text-secondary)] font-medium">{Number(server.cpu_usage ?? 0).toFixed(1)}%</span>
</p>
<p className="text-xs text-[var(--dg-text-muted)]">
{t('dashboard.serversStatus.memory')} <span className="text-[var(--dg-text-secondary)] font-medium">{Number(server.memory_usage ?? 0).toFixed(1)}%</span>
</p>
</div>
</div>
</button>
))}
{servers.length === 0 && (
<div className="flex flex-col items-center justify-center py-6 text-[var(--dg-text-muted)]">
<Server className="h-8 w-8 mb-2 opacity-30" />
<p className="text-sm">{t('dashboard.serversStatus.empty')}</p>
</div>
)}
</div>
<div className="flex items-center justify-between text-xs px-1 pt-1 border-t border-[var(--dg-border)]">
<span className="text-[var(--dg-text-muted)]">
{t('dashboard.ops.todayExecutions')}: <span className="text-[var(--dg-text-primary)] font-medium">{automation?.today_executions ?? 0}</span>
</span>
<button type="button" className="text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]" onClick={() => navigate('/alert-center')}>
{t('dashboard.ops.firingAlerts')}: <span className={cn('font-medium', (firingAlerts ?? 0) > 0 ? 'text-[var(--dg-red)]' : 'text-[var(--dg-text-primary)]')}>{firingAlerts ?? 0}</span>
</button>
</div>
</CardContent>
</Card>
@@ -454,6 +461,20 @@ export function KubernetesClusterCard({ clusters }: { clusters: DashboardCluster
{clusters.map((cluster) => {
const cpuUsage = Number(cluster.cpu_usage ?? 0);
const memoryUsage = Number(cluster.memory_usage ?? 0);
const hasUsage = cpuUsage > 0 || memoryUsage > 0;
const metrics = hasUsage
? [
{ label: t('dashboard.clusters.nodes'), value: cluster.node_count ?? 0 },
{ label: t('dashboard.clusters.pods'), value: cluster.pod_count ?? 0 },
{ label: t('dashboard.clusters.cpu'), value: `${cpuUsage.toFixed(1)}%` },
{ label: t('dashboard.clusters.memory'), value: `${memoryUsage.toFixed(1)}%` },
]
: [
{ label: t('dashboard.clusters.nodes'), value: cluster.node_count ?? 0 },
{ label: t('dashboard.clusters.pods'), value: cluster.pod_count ?? 0 },
{ label: t('dashboard.clusters.ingress'), value: cluster.ingress_count ?? 0 },
{ label: t('dashboard.clusters.deployments'), value: cluster.deployment_count ?? 0 },
];
return (
<div
key={cluster.id}
@@ -478,12 +499,7 @@ export function KubernetesClusterCard({ clusters }: { clusters: DashboardCluster
</Badge>
</div>
<div className="grid grid-cols-4 gap-2 text-center">
{[
{ label: t('dashboard.clusters.nodes'), value: cluster.node_count ?? 0 },
{ label: t('dashboard.clusters.pods'), value: cluster.pod_count ?? 0 },
{ label: t('dashboard.clusters.cpu'), value: `${cpuUsage.toFixed(1)}%` },
{ label: t('dashboard.clusters.memory'), value: `${memoryUsage.toFixed(1)}%` },
].map((item) => (
{metrics.map((item) => (
<div key={item.label} className="p-1.5 rounded-lg bg-[var(--dg-bg-primary)]/50">
<div className="text-sm font-semibold text-[var(--dg-text-primary)]">{item.value}</div>
<div className="text-[10px] text-[var(--dg-text-muted)] mt-0.5">{item.label}</div>