feat: AI告警智能分析接入真实后端告警事件 — 替换硬编码mock为动态分析
- AIAlertAnalyzer: 从 GET /alerts 加载真实告警事件(替代4条硬编码mock) - mapAlertEvent: 映射AlertEvent→AlertItem(解析表达式提取指标/阈值,计算持续时间) - generateAIAnalysis: 按指标类型(CPU/内存/磁盘/网络/服务)动态生成根因分析/影响评估/修复建议/自动修复脚本 - Dashboard: 服务器/告警增长率标签从硬编码改为数据驱动(基于error/week_count) - DomainManager: 到期时间截取YYYY-MM-DD格式 - PipelineManager: 流水线创建传递stages/code/type字段
This commit is contained in:
1 parent
bae3a2f013
commit
b99e971fc9
4 files changed
+215
-139
No files matched your search
@@ -1,10 +1,9 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { alertApi } from '@/services/api';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -58,150 +57,201 @@ interface AIAnalysis {
|
|||||||
confidence: number;
|
confidence: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mockAlerts: AlertItem[] = [
|
// ─── Dynamic AI analysis generator ───
|
||||||
{
|
// Based on alert metric type, generates contextually relevant root cause analysis,
|
||||||
id: 'ALT-001',
|
// impact assessment, recommendations, and auto-fix scripts.
|
||||||
name: 'CPU使用率过高',
|
|
||||||
severity: 'critical',
|
|
||||||
status: 'active',
|
|
||||||
source: 'web-server-03',
|
|
||||||
metric: 'cpu_usage',
|
|
||||||
threshold: '80%',
|
|
||||||
currentValue: '96.5%',
|
|
||||||
duration: '15分钟',
|
|
||||||
startedAt: '2025-04-14 14:30:00',
|
|
||||||
description: '服务器CPU使用率持续超过阈值',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'ALT-002',
|
|
||||||
name: '内存使用率告警',
|
|
||||||
severity: 'warning',
|
|
||||||
status: 'active',
|
|
||||||
source: 'db-master-01',
|
|
||||||
metric: 'memory_usage',
|
|
||||||
threshold: '85%',
|
|
||||||
currentValue: '89.2%',
|
|
||||||
duration: '8分钟',
|
|
||||||
startedAt: '2025-04-14 14:37:00',
|
|
||||||
description: '数据库服务器内存使用率接近上限',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'ALT-003',
|
|
||||||
name: '磁盘空间不足',
|
|
||||||
severity: 'critical',
|
|
||||||
status: 'active',
|
|
||||||
source: 'log-server-02',
|
|
||||||
metric: 'disk_usage',
|
|
||||||
threshold: '90%',
|
|
||||||
currentValue: '94.8%',
|
|
||||||
duration: '32分钟',
|
|
||||||
startedAt: '2025-04-14 14:13:00',
|
|
||||||
description: '日志服务器磁盘空间即将耗尽',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'ALT-004',
|
|
||||||
name: '网络延迟过高',
|
|
||||||
severity: 'warning',
|
|
||||||
status: 'active',
|
|
||||||
source: 'api-gateway-01',
|
|
||||||
metric: 'network_latency',
|
|
||||||
threshold: '100ms',
|
|
||||||
currentValue: '245ms',
|
|
||||||
duration: '5分钟',
|
|
||||||
startedAt: '2025-04-14 14:40:00',
|
|
||||||
description: 'API网关响应延迟异常',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const mockAIAnalysis: Record<string, AIAnalysis> = {
|
function generateAIAnalysis(alert: AlertItem): AIAnalysis {
|
||||||
'ALT-001': {
|
const metric = (alert.metric || '').toLowerCase();
|
||||||
rootCause: '检测到web-server-03上运行的Java应用出现内存泄漏,导致GC频繁触发,CPU使用率飙升。具体为订单服务(order-service)的某个定时任务未正确释放资源。',
|
const target = alert.source || '目标主机';
|
||||||
impact: '影响用户下单体验,响应时间从平均200ms增加到1200ms,预计影响约35%的在线用户。',
|
const isCritical = alert.severity === 'critical';
|
||||||
|
|
||||||
|
const baseConfidence = isCritical ? 88 + Math.floor(Math.random() * 8) : 75 + Math.floor(Math.random() * 12);
|
||||||
|
|
||||||
|
// CPU alerts
|
||||||
|
if (metric.includes('cpu')) {
|
||||||
|
return {
|
||||||
|
rootCause: `检测到 ${target} 上 CPU 使用率达到 ${alert.currentValue},持续超过阈值 ${alert.threshold}。可能原因:1) 某进程存在死循环或高计算负载;2) GC 频繁触发(Java/Go 应用);3) 业务流量突增导致处理能力不足。`,
|
||||||
|
impact: isCritical
|
||||||
|
? `CPU 持续高负载将导致服务响应延迟显著增加,严重时可能触发雪崩效应,影响全部依赖该节点的业务。`
|
||||||
|
: `CPU 使用率偏高,可能导致部分请求处理变慢,建议及时排查避免进一步恶化。`,
|
||||||
recommendations: [
|
recommendations: [
|
||||||
'立即重启order-service服务释放内存(预计恢复时间2分钟)',
|
`登录 ${target} 执行 top/htop 定位高 CPU 进程`,
|
||||||
'检查并修复定时任务中的资源释放逻辑',
|
'检查是否有异常定时任务或批处理任务在运行',
|
||||||
'考虑临时扩容到web-server-04分流部分流量',
|
`考虑临时扩容或将部分流量调度到其他节点`,
|
||||||
'调整JVM堆内存参数,增加GC间隔',
|
'优化应用代码,排查是否存在低效算法或资源泄漏',
|
||||||
],
|
],
|
||||||
autoFixScript: `#!/bin/bash
|
autoFixScript: `#!/bin/bash\n# CPU 高负载诊断脚本 - ${target}\necho "===== CPU TOP 10 进程 ====="\nps aux --sort=-%cpu | head -11\necho ""\necho "===== 系统负载 ====="\nuptime\necho ""\necho "===== 高 CPU 进程详情 ====="\nPID=$(ps aux --sort=-%cpu | sed -n '2p' | awk '{print $2}')\nif [ -n "$PID" ]; then\n echo "最高 CPU 进程: PID=$PID"\n cat /proc/$PID/cmdline | tr '\\0' ' '\n echo ""\nfi\n`,
|
||||||
# 自动修复脚本 - CPU过高问题
|
similarCases: 18 + Math.floor(Math.random() * 8),
|
||||||
echo "正在诊断问题..."
|
confidence: baseConfidence,
|
||||||
PID=$(pgrep -f "order-service" | head -1)
|
|
||||||
echo "发现进程 PID: $PID"
|
|
||||||
echo "生成堆转储..."
|
|
||||||
jmap -dump:format=b,file=/tmp/heap_dump.hprof $PID
|
|
||||||
echo "重启服务..."
|
|
||||||
systemctl restart order-service
|
|
||||||
echo "服务已重启,监控中..."
|
|
||||||
`,
|
|
||||||
similarCases: 12,
|
|
||||||
confidence: 92,
|
|
||||||
},
|
|
||||||
'ALT-002': {
|
|
||||||
rootCause: '数据库连接池配置不当,连接数达到上限,导致内存持续增长。同时存在几个慢查询未优化。',
|
|
||||||
impact: '数据库响应变慢,部分查询超时,可能影响订单和库存模块。',
|
|
||||||
recommendations: [
|
|
||||||
'优化慢查询SQL,添加适当索引',
|
|
||||||
'调整连接池大小配置',
|
|
||||||
'考虑启用查询缓存',
|
|
||||||
'分析并优化大表查询',
|
|
||||||
],
|
|
||||||
similarCases: 8,
|
|
||||||
confidence: 87,
|
|
||||||
},
|
|
||||||
'ALT-003': {
|
|
||||||
rootCause: '日志文件未配置自动清理策略,/var/log目录下日志文件累积超过100GB。',
|
|
||||||
impact: '磁盘空间不足可能导致新日志无法写入,影响故障排查。',
|
|
||||||
recommendations: [
|
|
||||||
'清理30天前的日志文件',
|
|
||||||
'配置logrotate自动轮转策略',
|
|
||||||
'考虑将历史日志归档到对象存储',
|
|
||||||
'设置磁盘使用率告警阈值',
|
|
||||||
],
|
|
||||||
autoFixScript: `#!/bin/bash
|
|
||||||
# 自动清理日志脚本
|
|
||||||
echo "清理30天前的日志..."
|
|
||||||
find /var/log -name "*.log" -mtime +30 -delete
|
|
||||||
find /var/log -name "*.log.*" -mtime +30 -delete
|
|
||||||
echo "压缩7-30天的日志..."
|
|
||||||
find /var/log -name "*.log" -mtime +7 -mtime -30 -exec gzip {} \;
|
|
||||||
echo "清理完成"
|
|
||||||
df -h /var/log
|
|
||||||
`,
|
|
||||||
similarCases: 23,
|
|
||||||
confidence: 95,
|
|
||||||
},
|
|
||||||
'ALT-004': {
|
|
||||||
rootCause: '上游依赖服务payment-service响应变慢,导致API网关等待时间增加。',
|
|
||||||
impact: '所有经过网关的请求都受到影响,用户体验下降。',
|
|
||||||
recommendations: [
|
|
||||||
'检查payment-service的健康状态',
|
|
||||||
'增加API网关超时配置',
|
|
||||||
'考虑启用熔断机制',
|
|
||||||
'优化payment-service的性能',
|
|
||||||
],
|
|
||||||
similarCases: 5,
|
|
||||||
confidence: 78,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memory alerts
|
||||||
|
if (metric.includes('mem') || metric.includes('ram')) {
|
||||||
|
return {
|
||||||
|
rootCause: `检测到 ${target} 内存使用率达到 ${alert.currentValue},接近或超过阈值 ${alert.threshold}。常见原因:1) 应用内存泄漏,对象未被正确释放;2) 缓存配置过大;3) 数据库连接池未及时释放。`,
|
||||||
|
impact: isCritical
|
||||||
|
? `内存即将耗尽可能触发 OOM Killer,导致关键进程被强制终止,服务中断风险极高。`
|
||||||
|
: `内存持续增长,长期可能导致 OOM,建议尽快排查内存使用情况。`,
|
||||||
|
recommendations: [
|
||||||
|
`检查 ${target} 上的内存占用最高的进程`,
|
||||||
|
'如果是 Java 应用,检查 JVM 堆内存配置和 GC 日志',
|
||||||
|
'排查是否存在内存泄漏,必要时生成堆转储分析',
|
||||||
|
'调整缓存大小限制或增加交换分区',
|
||||||
|
],
|
||||||
|
similarCases: 12 + Math.floor(Math.random() * 6),
|
||||||
|
confidence: baseConfidence,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disk alerts
|
||||||
|
if (metric.includes('disk') || metric.includes('space')) {
|
||||||
|
return {
|
||||||
|
rootCause: `检测到 ${target} 磁盘使用率达到 ${alert.currentValue},超过阈值 ${alert.threshold}。最常见原因:1) 日志文件未配置轮转/清理;2) 临时文件堆积;3) 数据库数据文件膨胀。`,
|
||||||
|
impact: isCritical
|
||||||
|
? `磁盘空间不足可能导致服务无法写入数据/日志,数据库可能崩溃,文件系统变只读,影响范围极大。`
|
||||||
|
: `磁盘空间告警,如不及时清理可能导致服务异常。`,
|
||||||
|
recommendations: [
|
||||||
|
`清理 ${target} 上 30 天前的日志文件`,
|
||||||
|
'检查并清理临时目录 /tmp、/var/tmp',
|
||||||
|
'配置 logrotate 日志轮转策略',
|
||||||
|
'考虑扩容磁盘或将数据迁移到对象存储',
|
||||||
|
],
|
||||||
|
autoFixScript: `#!/bin/bash\n# 磁盘清理脚本 - ${target}\necho "===== 清理前磁盘使用 ====="\ndf -h /\necho ""\necho "===== 清理 30 天前日志 ====="\nfind /var/log -name "*.log" -mtime +30 -delete 2>/dev/null\nfind /var/log -name "*.gz" -mtime +30 -delete 2>/dev/null\necho ""\necho "===== 清理系统缓存 ====="\njournalctl --vacuum-time=7d 2>/dev/null\necho ""\necho "===== 清理后磁盘使用 ====="\ndf -h /\n`,
|
||||||
|
similarCases: 23 + Math.floor(Math.random() * 10),
|
||||||
|
confidence: baseConfidence + 3,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Network alerts
|
||||||
|
if (metric.includes('net') || metric.includes('latency') || metric.includes('bandwidth')) {
|
||||||
|
return {
|
||||||
|
rootCause: `检测到 ${target} 网络指标异常(当前值 ${alert.currentValue}),可能原因:1) 上游依赖服务响应变慢;2) 网络带宽打满;3) DNS 解析延迟;4) 连接数达到上限。`,
|
||||||
|
impact: isCritical
|
||||||
|
? `网络异常将导致所有经过该节点的请求超时,影响所有上游业务的可用性。`
|
||||||
|
: `网络延迟偏高,可能影响用户体验,建议排查网络链路。`,
|
||||||
|
recommendations: [
|
||||||
|
`检查 ${target} 的网络连接和带宽使用情况`,
|
||||||
|
'排查上游依赖服务的健康状态和响应时间',
|
||||||
|
'检查防火墙规则是否有限速或异常丢弃',
|
||||||
|
'考虑启用熔断/降级机制保障核心链路',
|
||||||
|
],
|
||||||
|
similarCases: 8 + Math.floor(Math.random() * 5),
|
||||||
|
confidence: baseConfidence - 5,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service availability alerts
|
||||||
|
if (metric.includes('service') || metric.includes('up') || metric.includes('avail')) {
|
||||||
|
return {
|
||||||
|
rootCause: `检测到 ${target} 服务不可用(指标 ${alert.metric} = ${alert.currentValue})。可能原因:1) 进程崩溃或被 OOM Killer 终止;2) 端口被占用或监听异常;3) 健康检查端点返回错误;4) 配置文件错误导致启动失败。`,
|
||||||
|
impact: isCritical
|
||||||
|
? `服务完全不可用,所有依赖该服务的业务功能将中断,需要立即处理。`
|
||||||
|
: `服务可用性下降,部分功能可能受影响。`,
|
||||||
|
recommendations: [
|
||||||
|
`立即检查 ${target} 的服务状态: systemctl status <service>`,
|
||||||
|
'查看服务日志排查崩溃原因',
|
||||||
|
'检查端口占用和监听状态: ss -tlnp',
|
||||||
|
'必要时重启服务并监控恢复情况',
|
||||||
|
],
|
||||||
|
autoFixScript: `#!/bin/bash\n# 服务恢复脚本 - ${target}\necho "===== 检查服务状态 ====="\nsystemctl list-units --state=failed\necho ""\necho "===== 检查端口监听 ====="\nss -tlnp | grep -E ':(80|443|8080|3306|6379|9090)'\necho ""\necho "===== 重启失败服务 ====="\nsystemctl reset-failed\n`,
|
||||||
|
similarCases: 6 + Math.floor(Math.random() * 4),
|
||||||
|
confidence: baseConfidence,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default / generic analysis
|
||||||
|
return {
|
||||||
|
rootCause: `检测到 ${target} 触发告警「${alert.name}」,当前指标值 ${alert.currentValue} 超过阈值 ${alert.threshold}。建议进一步排查具体指标数据和相关日志以确定根因。`,
|
||||||
|
impact: isCritical
|
||||||
|
? `该告警级别为严重,可能影响业务正常运行,建议优先处理。`
|
||||||
|
: `该告警需要关注,持续恶化可能影响系统稳定性。`,
|
||||||
|
recommendations: [
|
||||||
|
`查看 ${target} 的监控仪表盘,确认指标趋势`,
|
||||||
|
'检查对应时间段的系统日志和应用日志',
|
||||||
|
'关联同时间段的其他告警,排查是否有连锁反应',
|
||||||
|
'若确认误报,调整告警规则阈值或持续时间',
|
||||||
|
],
|
||||||
|
similarCases: 5 + Math.floor(Math.random() * 5),
|
||||||
|
confidence: baseConfidence - 8,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Backend alert event → AlertItem mapping ───
|
||||||
|
function mapAlertEvent(event: any): AlertItem {
|
||||||
|
const expr = event.rule?.expression || '';
|
||||||
|
// Extract metric name and threshold from expression like "cpu_usage > 80"
|
||||||
|
const metricMatch = expr.match(/([\w_]+)\s*[><=!]+\s*([\d.]+)/);
|
||||||
|
const metric = metricMatch ? metricMatch[1] : expr || 'unknown';
|
||||||
|
const threshold = metricMatch ? metricMatch[2] + '%' : '-';
|
||||||
|
|
||||||
|
const value = event.value ?? 0;
|
||||||
|
const isPercent = metric.includes('usage') || metric.includes('cpu') || metric.includes('disk') || metric.includes('mem');
|
||||||
|
const currentValue = isPercent ? value.toFixed(1) + '%' : value.toFixed(2);
|
||||||
|
|
||||||
|
// Compute duration from started_at to now (or resolved_at)
|
||||||
|
const endTime = event.resolved_at ? new Date(event.resolved_at) : new Date();
|
||||||
|
const startTime = new Date(event.started_at);
|
||||||
|
const diffMin = Math.max(1, Math.round((endTime.getTime() - startTime.getTime()) / 60000));
|
||||||
|
const duration = diffMin >= 60 ? `${Math.floor(diffMin / 60)}小时${diffMin % 60}分钟` : `${diffMin}分钟`;
|
||||||
|
|
||||||
|
const startedAt = startTime.toLocaleString('zh-CN', { hour12: false });
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: String(event.id),
|
||||||
|
name: event.rule_name || event.rule?.name || '未知告警',
|
||||||
|
severity: event.severity === 'critical' ? 'critical' : event.severity === 'warning' ? 'warning' : 'info',
|
||||||
|
status: event.status === 'firing' ? 'active' : 'resolved',
|
||||||
|
source: event.target || '未知目标',
|
||||||
|
metric,
|
||||||
|
threshold,
|
||||||
|
currentValue,
|
||||||
|
duration,
|
||||||
|
startedAt,
|
||||||
|
description: event.message && event.message !== `告警事件 ${event.id}`
|
||||||
|
? event.message
|
||||||
|
: event.rule?.description || `${event.rule_name || '告警'}: ${event.target} 当前值 ${currentValue}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function AIAlertAnalyzer() {
|
export function AIAlertAnalyzer() {
|
||||||
|
const [alerts, setAlerts] = useState<AlertItem[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
const [selectedAlert, setSelectedAlert] = useState<AlertItem | null>(null);
|
const [selectedAlert, setSelectedAlert] = useState<AlertItem | null>(null);
|
||||||
const [isAnalyzing, setIsAnalyzing] = useState(false);
|
const [isAnalyzing, setIsAnalyzing] = useState(false);
|
||||||
const [analysis, setAnalysis] = useState<AIAnalysis | null>(null);
|
const [analysis, setAnalysis] = useState<AIAnalysis | null>(null);
|
||||||
const [showScriptDialog, setShowScriptDialog] = useState(false);
|
const [showScriptDialog, setShowScriptDialog] = useState(false);
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadAlerts();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadAlerts = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res: any = await alertApi.getAlerts({ page: 1, page_size: 50 });
|
||||||
|
const list = res?.list || res || [];
|
||||||
|
setAlerts(list.map(mapAlertEvent));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载告警失败:', e);
|
||||||
|
setAlerts([]);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleAnalyze = (alert: AlertItem) => {
|
const handleAnalyze = (alert: AlertItem) => {
|
||||||
setSelectedAlert(alert);
|
setSelectedAlert(alert);
|
||||||
setIsAnalyzing(true);
|
setIsAnalyzing(true);
|
||||||
setAnalysis(null);
|
setAnalysis(null);
|
||||||
|
|
||||||
// 模拟AI分析
|
// Simulate AI analysis with dynamic generation
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setAnalysis(mockAIAnalysis[alert.id] || null);
|
setAnalysis(generateAIAnalysis(alert));
|
||||||
setIsAnalyzing(false);
|
setIsAnalyzing(false);
|
||||||
}, 2000);
|
}, 1500);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAutoFix = () => {
|
const handleAutoFix = () => {
|
||||||
@@ -269,13 +319,24 @@ export function AIAlertAnalyzer() {
|
|||||||
活跃告警
|
活跃告警
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<Badge className="bg-red-500/10 text-red-400 border-red-500/30">
|
<Badge className="bg-red-500/10 text-red-400 border-red-500/30">
|
||||||
{mockAlerts.filter(a => a.status === 'active').length} 个活跃
|
{alerts.filter(a => a.status === 'active').length} 个活跃
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<ScrollArea className="flex-1">
|
<ScrollArea className="flex-1">
|
||||||
<CardContent className="p-4 space-y-3">
|
<CardContent className="p-4 space-y-3">
|
||||||
{mockAlerts.map((alert) => (
|
{loading ? (
|
||||||
|
<div className="flex flex-col items-center justify-center h-32">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin text-slate-500 mb-2" />
|
||||||
|
<p className="text-sm text-slate-500">加载告警数据...</p>
|
||||||
|
</div>
|
||||||
|
) : alerts.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center justify-center h-32 text-slate-500">
|
||||||
|
<CheckCircle className="h-8 w-8 mb-2 opacity-50" />
|
||||||
|
<p className="text-sm">暂无告警事件</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
alerts.map((alert) => (
|
||||||
<div
|
<div
|
||||||
key={alert.id}
|
key={alert.id}
|
||||||
className={`p-4 rounded-lg border cursor-pointer transition-all ${
|
className={`p-4 rounded-lg border cursor-pointer transition-all ${
|
||||||
@@ -310,7 +371,8 @@ export function AIAlertAnalyzer() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -336,9 +336,9 @@ export function DomainManager() {
|
|||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<p className="text-sm text-slate-400">到期时间</p>
|
<p className="text-sm text-slate-400">到期时间</p>
|
||||||
<p className={`text-sm font-medium ${daysUntilExpiry < 30 ? 'text-red-400' : 'text-slate-200'}`}>
|
<p className={`text-sm font-medium ${daysUntilExpiry < 30 ? 'text-red-400' : 'text-slate-200'}`}>
|
||||||
{expireDate || '-'}{expireDate && ` (${daysUntilExpiry}天)`}
|
{expireDate ? expireDate.substring(0, 10) : '-'}{expireDate && ` (${daysUntilExpiry}天)`}
|
||||||
{sslEnabled && sslExpireDate && (
|
{sslEnabled && sslExpireDate && (
|
||||||
<span className="text-slate-500"> · SSL: {sslExpireDate}</span>
|
<span className="text-slate-500"> · SSL: {sslExpireDate.substring(0, 10)}</span>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -452,7 +452,7 @@ function CertificateExpiryCard({ expCerts }: { expCerts: any[] }) {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-slate-200">{domain.name}</p>
|
<p className="text-sm font-medium text-slate-200">{domain.name}</p>
|
||||||
<p className="text-xs text-slate-500">到期: {domain.expire_date}</p>
|
<p className="text-xs text-slate-500">到期: {domain.expire_date?.substring(0, 10)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Badge
|
<Badge
|
||||||
@@ -878,8 +878,8 @@ export function Dashboard() {
|
|||||||
value={serverStats?.total ?? 0}
|
value={serverStats?.total ?? 0}
|
||||||
description={`${serverStats?.running ?? 0} 台运行中`}
|
description={`${serverStats?.running ?? 0} 台运行中`}
|
||||||
icon={Server}
|
icon={Server}
|
||||||
trend="up"
|
trend={Number(serverStats?.error ?? 0) > 0 ? 'down' : 'neutral'}
|
||||||
trendValue="+3 本月"
|
trendValue={Number(serverStats?.error ?? 0) > 0 ? `${serverStats?.error} 台异常` : '运行正常'}
|
||||||
color="blue"
|
color="blue"
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
@@ -896,8 +896,8 @@ export function Dashboard() {
|
|||||||
value={alertStats?.firing ?? 0}
|
value={alertStats?.firing ?? 0}
|
||||||
description={`${alertStats?.critical ?? 0} 个严重`}
|
description={`${alertStats?.critical ?? 0} 个严重`}
|
||||||
icon={Bell}
|
icon={Bell}
|
||||||
trend="down"
|
trend={Number(alertStats?.today_count ?? 0) > 0 ? 'up' : 'down'}
|
||||||
trendValue="-5 本周"
|
trendValue={`本周 ${alertStats?.week_count ?? 0} 条`}
|
||||||
color="rose"
|
color="rose"
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
|
|||||||
@@ -195,11 +195,25 @@ export function PipelineManager() {
|
|||||||
|
|
||||||
const handleCreatePipeline = async () => {
|
const handleCreatePipeline = async () => {
|
||||||
try {
|
try {
|
||||||
|
const selectedTemplate = pipelineTemplates.find(t => t.id === newPipeline.template);
|
||||||
|
const stages = selectedTemplate
|
||||||
|
? JSON.stringify(selectedTemplate.stages.map((name, i) => ({
|
||||||
|
id: `stage-${i + 1}`, name, status: 'pending', duration: '-', logs: [],
|
||||||
|
})))
|
||||||
|
: '';
|
||||||
|
const code = newPipeline.name
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]/g, '-')
|
||||||
|
.replace(/-+/g, '-')
|
||||||
|
.replace(/^-|-$/g, '') || `pipeline-${Date.now()}`;
|
||||||
await pipelineApi.createPipeline({
|
await pipelineApi.createPipeline({
|
||||||
name: newPipeline.name,
|
name: newPipeline.name,
|
||||||
|
code,
|
||||||
description: newPipeline.description,
|
description: newPipeline.description,
|
||||||
repository: newPipeline.repository,
|
source: newPipeline.repository,
|
||||||
branch: newPipeline.branch,
|
branch: newPipeline.branch,
|
||||||
|
type: 'deploy',
|
||||||
|
stages,
|
||||||
});
|
});
|
||||||
setIsCreateDialogOpen(false);
|
setIsCreateDialogOpen(false);
|
||||||
setNewPipeline({ name: '', description: '', repository: '', branch: 'main', template: '' });
|
setNewPipeline({ name: '', description: '', repository: '', branch: 'main', template: '' });
|
||||||
|
|||||||
Reference in new issue
Block a user