From a3b84f79c7910645651f0cec48c159119b9f6ebc Mon Sep 17 00:00:00 2001 From: attacker Date: Mon, 15 Jun 2026 03:05:26 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BB=AA=E8=A1=A8=E7=9B=98=E8=B5=84?= =?UTF-8?q?=E6=BA=90=E8=B6=8B=E5=8A=BF=E5=9B=BE=E4=BD=BF=E7=94=A8=E7=9C=9F?= =?UTF-8?q?=E5=AE=9EAgent=E6=8C=87=E6=A0=87=E6=9B=BF=E4=BB=A3=E9=9A=8F?= =?UTF-8?q?=E6=9C=BA=E5=81=87=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端 GetMetrics 修复 strftime 字符串无法扫入 time.Time 导致 timestamp 为零值的 bug (改为 string 类型 + time.ParseInLocation 手动解析) - 前端 Dashboard.tsx 新增 getMetrics(24) 调用,获取真实按小时聚合的 Agent 数据 - DashboardCharts.tsx 新增 buildMetricsFromReal() 将 MetricData[] 转换为图表数据 - ResourceUsageCard 优先使用真实时间序列,无数据时回退到平均值生成 --- backend/internal/api/handlers/dashboard.go | 17 ++++++---- .../src/components/dashboard/Dashboard.tsx | 10 +++++- .../components/dashboard/DashboardCharts.tsx | 31 ++++++++++++++++++- 3 files changed, 50 insertions(+), 8 deletions(-) diff --git a/backend/internal/api/handlers/dashboard.go b/backend/internal/api/handlers/dashboard.go index 9312b5f..c9d73e1 100644 --- a/backend/internal/api/handlers/dashboard.go +++ b/backend/internal/api/handlers/dashboard.go @@ -294,12 +294,12 @@ func (h *DashboardHandler) GetMetrics(c *gin.Context) { } } - // 从 agent_reports 表查最近 N 小时的真实数据 + // 从 agent_reports 表查最近 N 小时的真实数据 // Logan.Li since := time.Now().Add(-time.Duration(hours) * time.Hour) type avgPoint struct { - Bucket time.Time - AvgCPU float64 - AvgMem float64 + Bucket string // strftime 返回字符串 "2006-01-02 15:00",不能直接扫入 time.Time(会得零值) + AvgCPU float64 + AvgMem float64 } var rows []avgPoint h.db.Model(&models.AgentReport{}). @@ -311,9 +311,14 @@ func (h *DashboardHandler) GetMetrics(c *gin.Context) { var metrics []models.MetricData for _, r := range rows { + // 手动解析 SQLite strftime 字符串为 time.Time(GORM 无法自动转换) // Logan.Li + ts, err := time.ParseInLocation("2006-01-02 15:00", r.Bucket, time.Local) + if err != nil { + ts = time.Now() + } metrics = append(metrics, - models.MetricData{Name: "cpu", Value: r.AvgCPU, Timestamp: r.Bucket}, - models.MetricData{Name: "memory", Value: r.AvgMem, Timestamp: r.Bucket}, + models.MetricData{Name: "cpu", Value: r.AvgCPU, Timestamp: ts}, + models.MetricData{Name: "memory", Value: r.AvgMem, Timestamp: ts}, ) } diff --git a/frontend/src/components/dashboard/Dashboard.tsx b/frontend/src/components/dashboard/Dashboard.tsx index 7a2193e..ee1604f 100644 --- a/frontend/src/components/dashboard/Dashboard.tsx +++ b/frontend/src/components/dashboard/Dashboard.tsx @@ -20,12 +20,13 @@ export function Dashboard() { const [alerts, setAlerts] = useState([]); const [releases, setReleases] = useState([]); const [expCerts, setExpCerts] = useState([]); + const [metrics, setMetrics] = useState([]); // 真实 Agent 指标时间序列 // Logan.Li useEffect(() => { const fetchData = async () => { setLoading(true); try { - const [statsRes, serversRes, clustersRes, alertsRes, releasesRes] = await Promise.all([ + const [statsRes, serversRes, clustersRes, alertsRes, releasesRes, metricsRes] = await Promise.all([ dashboardStatsApi.getStats().catch((e) => { console.error('Failed to fetch dashboard stats:', e); return null; @@ -46,6 +47,11 @@ export function Dashboard() { console.error('Failed to fetch dashboard releases:', e); return null; }), + // 获取真实 Agent 指标时间序列(过去24小时按小时聚合) // Logan.Li + dashboardStatsApi.getMetrics(24).catch((e) => { + console.error('Failed to fetch metrics:', e); + return []; + }), ]); setStats(statsRes); @@ -54,6 +60,7 @@ export function Dashboard() { setAlerts(Array.isArray(alertsRes) ? alertsRes : []); setReleases(Array.isArray(releasesRes) ? releasesRes : []); setExpCerts(statsRes?.expiring_certs ?? []); + setMetrics(Array.isArray(metricsRes) ? metricsRes : []); } catch (error) { console.error('Failed to load dashboard data:', error); } finally { @@ -142,6 +149,7 @@ export function Dashboard() { avgCpu={Number(serverStats?.avg_cpu ?? 0)} avgMemory={Number(serverStats?.avg_memory ?? 0)} avgDisk={Number(serverStats?.avg_disk ?? 0)} + metrics={metrics} /> {/* Cost & Top Resource Usage */} diff --git a/frontend/src/components/dashboard/DashboardCharts.tsx b/frontend/src/components/dashboard/DashboardCharts.tsx index c705eb2..53d53e5 100644 --- a/frontend/src/components/dashboard/DashboardCharts.tsx +++ b/frontend/src/components/dashboard/DashboardCharts.tsx @@ -248,17 +248,46 @@ export function TopResourceUsageCard({ topUsage }: { topUsage: any }) { ); } +// 将后端 MetricData[] 时间序列转换为图表所需格式 { time, cpu, memory } // Logan.Li +function buildMetricsFromReal(metrics: any[], avgDisk: number): { time: string; cpu: number; memory: number; disk: number }[] { + if (!metrics || metrics.length === 0) return []; + // 按 timestamp 分组:同一时刻有 cpu + memory 两条记录 + const buckets: Record = {}; + for (const m of metrics) { + const ts = m.timestamp; + if (!ts) continue; + // 提取 HH:MM 格式作为 X 轴标签 + const d = new Date(ts); + const label = `${d.getHours().toString().padStart(2, '0')}:00`; + if (!buckets[label]) buckets[label] = {}; + if (m.name === 'cpu') buckets[label].cpu = Number(m.value) || 0; + if (m.name === 'memory') buckets[label].memory = Number(m.value) || 0; + } + return Object.entries(buckets) + .sort((a, b) => a[0].localeCompare(b[0])) + .map(([time, v]) => ({ + time, + cpu: Number((v.cpu ?? 0).toFixed(1)), + memory: Number((v.memory ?? 0).toFixed(1)), + disk: Number(avgDisk.toFixed(1)), // 磁盘无历史序列,用平均值 + })); +} + // 资源使用率趋势卡片 export function ResourceUsageCard({ avgCpu, avgMemory, avgDisk, + metrics, }: { avgCpu: number; avgMemory: number; avgDisk: number; + metrics?: any[]; }) { - const data = generateMetricsData(avgCpu, avgMemory, avgDisk, 24); + // 优先使用真实 Agent 指标时间序列;无数据时回退到平均值生成(带随机波动) // Logan.Li + const realData = buildMetricsFromReal(metrics ?? [], avgDisk); + const data = realData.length > 0 ? realData : generateMetricsData(avgCpu, avgMemory, avgDisk, 24); return (