diff --git a/internal/app.go b/internal/app.go index c9413b5..0f3b5b6 100644 --- a/internal/app.go +++ b/internal/app.go @@ -193,6 +193,7 @@ func setupApi(app *orz.App, components *AppComponents) { // 监控统计数据(公开访问,支持可选认证)- 用于公共展示页面 publicApiWithOptionalAuth.GET("/monitors", components.MonitorHandler.GetMonitors) publicApiWithOptionalAuth.GET("/monitors/:id/stats", components.MonitorHandler.GetStatsByID) + publicApiWithOptionalAuth.GET("/monitors/:id/agents", components.MonitorHandler.GetAgentStatsByID) publicApiWithOptionalAuth.GET("/monitors/:id/history", components.MonitorHandler.GetHistoryByID) // Logo(公开访问)- 用于公共页面只获取 Logo @@ -309,6 +310,7 @@ func autoMigrate(database *gorm.DB) error { &models.AggregatedDiskIOMetricModel{}, &models.AggregatedGPUMetricModel{}, &models.AggregatedTemperatureMetricModel{}, + &models.AggregatedMonitorMetricModel{}, &models.AggregationProgress{}, ) } diff --git a/internal/handler/monitor_handler.go b/internal/handler/monitor_handler.go index abc017f..cb3624a 100644 --- a/internal/handler/monitor_handler.go +++ b/internal/handler/monitor_handler.go @@ -158,6 +158,24 @@ func (h *MonitorHandler) GetStatsByID(c echo.Context) error { return orz.Ok(c, stats) } +// GetAgentStatsByID 获取指定监控任务各探针的统计数据(公开接口,已登录返回全部,未登录返回公开可见) +func (h *MonitorHandler) GetAgentStatsByID(c echo.Context) error { + id := c.Param("id") + ctx := c.Request().Context() + + // 验证监控任务访问权限 + if _, err := h.monitorService.GetMonitorByAuth(ctx, id, utils.IsAuthenticated(c)); err != nil { + return err + } + + stats, err := h.monitorService.GetMonitorAgentStats(ctx, id) + if err != nil { + return err + } + + return orz.Ok(c, stats) +} + // GetHistoryByID 获取指定监控任务的历史响应时间数据(公开接口,已登录返回全部,未登录返回公开可见) func (h *MonitorHandler) GetHistoryByID(c echo.Context) error { id := c.Param("id") diff --git a/internal/models/metric.go b/internal/models/metric.go index 2326517..cee30c6 100644 --- a/internal/models/metric.go +++ b/internal/models/metric.go @@ -331,3 +331,24 @@ type AggregationProgress struct { func (AggregationProgress) TableName() string { return "aggregation_progress" } + +// AggregatedMonitorMetricModel 监控指标聚合表 +type AggregatedMonitorMetricModel struct { + ID uint `gorm:"primaryKey;autoIncrement" json:"id"` + MonitorID string `gorm:"index:idx_monitoragg_monitor_agent_bucket,priority:1;uniqueIndex:ux_monitoragg_bucket,priority:1" json:"monitorId"` + AgentID string `gorm:"index:idx_monitoragg_monitor_agent_bucket,priority:2;uniqueIndex:ux_monitoragg_bucket,priority:2" json:"agentId"` + BucketSeconds int `gorm:"index:idx_monitoragg_monitor_agent_bucket,priority:3;uniqueIndex:ux_monitoragg_bucket,priority:3" json:"bucketSeconds"` + BucketStart int64 `gorm:"index:idx_monitoragg_monitor_agent_bucket,priority:4;uniqueIndex:ux_monitoragg_bucket,priority:4" json:"bucketStart"` // 毫秒 + AvgResponse float64 `json:"avgResponse"` // 平均响应时间(ms) + MaxResponse int64 `json:"maxResponse"` // 最大响应时间(ms) + MinResponse int64 `json:"minResponse"` // 最小响应时间(ms) + SuccessCount int64 `json:"successCount"` // 成功次数 + TotalCount int64 `json:"totalCount"` // 总次数 + SuccessRate float64 `json:"successRate"` // 成功率(百分比) + LastStatus string `json:"lastStatus"` // 最后状态 + LastError string `json:"lastError"` // 最后错误信息 +} + +func (AggregatedMonitorMetricModel) TableName() string { + return "monitor_metrics_aggs" +} diff --git a/internal/repo/metric_repo.go b/internal/repo/metric_repo.go index 961641d..1d01b31 100644 --- a/internal/repo/metric_repo.go +++ b/internal/repo/metric_repo.go @@ -923,3 +923,70 @@ func (r *MetricRepo) GetAggregationProgress(ctx context.Context, metricType stri } return &progress, nil } + +// ----------- 监控指标聚合表操作 ----------- + +// AggregateMonitorMetricsToAgg 将原始监控数据聚合到聚合表 +func (r *MetricRepo) AggregateMonitorMetricsToAgg(ctx context.Context, bucketSeconds int, start, end int64) error { + bucketMs := int64(bucketSeconds * 1000) + return r.db.WithContext(ctx).Exec(` + INSERT INTO monitor_metrics_aggs ( + monitor_id, agent_id, bucket_seconds, bucket_start, + avg_response, max_response, min_response, + success_count, total_count, success_rate, + last_status, last_error + ) + WITH ranked_metrics AS ( + SELECT + monitor_id, + agent_id, + (timestamp / ?) * ? as bucket_start, + response_time, + status, + error, + timestamp, + ROW_NUMBER() OVER (PARTITION BY monitor_id, agent_id, (timestamp / ?) * ? ORDER BY timestamp DESC) as rn + FROM monitor_metrics + WHERE timestamp >= ? AND timestamp < ? + ) + SELECT + monitor_id, + agent_id, + ? as bucket_seconds, + bucket_start, + AVG(response_time) as avg_response, + MAX(response_time) as max_response, + MIN(response_time) as min_response, + SUM(CASE WHEN status = 'up' THEN 1 ELSE 0 END) as success_count, + COUNT(*) as total_count, + CAST(SUM(CASE WHEN status = 'up' THEN 1 ELSE 0 END) AS REAL) / CAST(COUNT(*) AS REAL) * 100 as success_rate, + MAX(CASE WHEN rn = 1 THEN status END) as last_status, + MAX(CASE WHEN rn = 1 THEN error END) as last_error + FROM ranked_metrics + GROUP BY monitor_id, agent_id, bucket_start + ON CONFLICT (monitor_id, agent_id, bucket_seconds, bucket_start) DO UPDATE SET + avg_response = EXCLUDED.avg_response, + max_response = EXCLUDED.max_response, + min_response = EXCLUDED.min_response, + success_count = EXCLUDED.success_count, + total_count = EXCLUDED.total_count, + success_rate = EXCLUDED.success_rate, + last_status = EXCLUDED.last_status, + last_error = EXCLUDED.last_error + `, bucketMs, bucketMs, bucketMs, bucketMs, start, end, bucketSeconds).Error +} + +// GetMonitorMetricsAgg 从聚合表获取监控指标(按探针和时间间隔聚合) +func (r *MetricRepo) GetMonitorMetricsAgg(ctx context.Context, monitorID string, start, end int64, bucketSeconds int) ([]AggregatedMonitorMetric, error) { + var metrics []AggregatedMonitorMetric + err := r.db.WithContext(ctx). + Table("monitor_metrics_aggs"). + Select(`bucket_start as timestamp, agent_id, + avg_response, max_response, min_response, + success_count, total_count, success_rate, + last_status, last_error as last_error_msg`). + Where("monitor_id = ? AND bucket_seconds = ? AND bucket_start >= ? AND bucket_start <= ?", monitorID, bucketSeconds, start, end). + Order("bucket_start, agent_id"). + Scan(&metrics).Error + return metrics, err +} diff --git a/internal/service/metric_service.go b/internal/service/metric_service.go index 1fa755f..5888c40 100644 --- a/internal/service/metric_service.go +++ b/internal/service/metric_service.go @@ -676,6 +676,7 @@ func (s *MetricService) runAggregation(ctx context.Context) { s.aggregateMetric(ctx, "disk_io", bucket, retention, s.metricRepo.AggregateDiskIOToAgg) s.aggregateMetric(ctx, "gpu", bucket, retention, s.metricRepo.AggregateGPUToAgg) s.aggregateMetric(ctx, "temperature", bucket, retention, s.metricRepo.AggregateTemperatureToAgg) + s.aggregateMetric(ctx, "monitor", bucket, retention, s.metricRepo.AggregateMonitorMetricsToAgg) } } diff --git a/internal/service/monitor_service.go b/internal/service/monitor_service.go index 4937392..0a92263 100644 --- a/internal/service/monitor_service.go +++ b/internal/service/monitor_service.go @@ -11,6 +11,7 @@ import ( "github.com/dushixiang/pika/internal/protocol" "github.com/dushixiang/pika/internal/repo" ws "github.com/dushixiang/pika/internal/websocket" + "github.com/go-orz/cache" "github.com/go-orz/orz" "github.com/go-orz/toolkit" "github.com/google/uuid" @@ -27,6 +28,11 @@ type MonitorService struct { metricRepo *repo.MetricRepo monitorStatsRepo *repo.MonitorStatsRepo wsManager *ws.Manager + + // 监控概览缓存:缓存监控任务列表(使用不同的 key 区分 public 和 private) + overviewCache cache.Cache[string, []PublicMonitorOverview] + // 监控统计缓存:缓存单个监控任务的统计数据 + statsCache cache.Cache[string, []models.MonitorStats] } func NewMonitorService(logger *zap.Logger, db *gorm.DB, wsManager *ws.Manager) *MonitorService { @@ -37,8 +43,11 @@ func NewMonitorService(logger *zap.Logger, db *gorm.DB, wsManager *ws.Manager) * agentRepo: repo.NewAgentRepo(db), metricRepo: repo.NewMetricRepo(db), monitorStatsRepo: repo.NewMonitorStatsRepo(db), + wsManager: wsManager, - wsManager: wsManager, + // 缓存 5 分钟,避免频繁查询 + overviewCache: cache.New[string, []PublicMonitorOverview](5 * time.Minute), + statsCache: cache.New[string, []models.MonitorStats](5 * time.Minute), } } @@ -178,6 +187,18 @@ func (s *MonitorService) DeleteMonitor(ctx context.Context, id string) error { // ListByAuth 返回公开展示所需的监控配置和汇总统计 func (s *MonitorService) ListByAuth(ctx context.Context, isAuthenticated bool) ([]PublicMonitorOverview, error) { + // 构建缓存键:根据认证状态使用不同的 key + cacheKey := "overview:public" + if isAuthenticated { + cacheKey = "overview:private" + } + + // 尝试从缓存获取 + if cachedResult, ok := s.overviewCache.Get(cacheKey); ok { + return cachedResult, nil + } + + // 缓存未命中,查询数据库 // 获取符合权限的监控任务列表 monitors, err := s.FindByAuth(ctx, isAuthenticated) if err != nil { @@ -185,7 +206,10 @@ func (s *MonitorService) ListByAuth(ctx context.Context, isAuthenticated bool) ( } if len(monitors) == 0 { - return []PublicMonitorOverview{}, nil + emptyResult := []PublicMonitorOverview{} + // 缓存空结果 + s.overviewCache.Set(cacheKey, emptyResult, 5*time.Minute) + return emptyResult, nil } // 提取监控任务ID列表 @@ -194,7 +218,7 @@ func (s *MonitorService) ListByAuth(ctx context.Context, isAuthenticated bool) ( monitorIds = append(monitorIds, monitor.ID) } - // 批量获取统计数据 + // 批量获取统计数据(已经由后台任务预计算好) statsList, err := s.monitorStatsRepo.FindByMonitorIdIn(ctx, monitorIds) if err != nil { return nil, err @@ -206,55 +230,23 @@ func (s *MonitorService) ListByAuth(ctx context.Context, isAuthenticated bool) ( statsMap[stats.MonitorId] = append(statsMap[stats.MonitorId], stats) } - // 获取所有探针列表,用于过滤有效的统计数据 - agents, err := s.agentRepo.FindAll(ctx) - if err != nil { - return nil, err - } - // 构建监控概览列表 items := make([]PublicMonitorOverview, 0, len(monitors)) for _, monitor := range monitors { - // 计算当前监控任务关联的目标探针 - targetAgents := s.resolveTargetAgents(monitor, agents) - - // 过滤出目标探针的统计数据 - filteredStats := s.filterStatsByAgents(statsMap[monitor.ID], targetAgents) - - // 聚合统计数据 - summary := aggregateMonitorStats(filteredStats) + // 直接聚合预计算的统计数据 + summary := aggregateMonitorStats(statsMap[monitor.ID]) // 构建监控概览对象 item := s.buildMonitorOverview(monitor, summary) items = append(items, item) } + // 缓存结果 + s.overviewCache.Set(cacheKey, items, 5*time.Minute) + return items, nil } -// filterStatsByAgents 过滤出指定探针的统计数据 -func (s *MonitorService) filterStatsByAgents(stats []models.MonitorStats, targetAgents []models.Agent) []models.MonitorStats { - if len(stats) == 0 || len(targetAgents) == 0 { - return []models.MonitorStats{} - } - - // 构建目标探针ID映射 - targetAgentsMap := make(map[string]bool, len(targetAgents)) - for _, agent := range targetAgents { - targetAgentsMap[agent.ID] = true - } - - // 过滤统计数据 - filteredStats := make([]models.MonitorStats, 0, len(stats)) - for _, stat := range stats { - if targetAgentsMap[stat.AgentID] { - filteredStats = append(filteredStats, stat) - } - } - - return filteredStats -} - // buildMonitorOverview 构建监控概览对象 func (s *MonitorService) buildMonitorOverview(monitor models.MonitorTask, summary monitorOverviewSummary) PublicMonitorOverview { // 根据 ShowTargetPublic 字段决定是否返回真实的 Target @@ -652,8 +644,40 @@ func (s *MonitorService) calculateStatsForAgentMonitor(ctx context.Context, agen return stats, nil } -// GetMonitorStatsByID 获取监控任务的统计数据(所有探针) -func (s *MonitorService) GetMonitorStatsByID(ctx context.Context, monitorID string) ([]models.MonitorStats, error) { +// GetMonitorStatsByID 获取监控任务的统计数据(聚合后的单个监控详情) +func (s *MonitorService) GetMonitorStatsByID(ctx context.Context, monitorID string) (*PublicMonitorOverview, error) { + // 查询监控任务 + monitor, err := s.MonitorRepo.FindById(ctx, monitorID) + if err != nil { + return nil, err + } + + // 获取统计数据 + statsList, err := s.monitorStatsRepo.FindByMonitorId(ctx, monitor.ID) + if err != nil { + return nil, err + } + + // 聚合统计数据 + summary := aggregateMonitorStats(statsList) + + // 构建监控概览对象 + overview := s.buildMonitorOverview(monitor, summary) + + return &overview, nil +} + +// GetMonitorAgentStats 获取监控任务各探针的统计数据(详细列表) +func (s *MonitorService) GetMonitorAgentStats(ctx context.Context, monitorID string) ([]models.MonitorStats, error) { + // 构建缓存键 + cacheKey := fmt.Sprintf("agents:%s", monitorID) + + // 尝试从缓存获取 + if cachedResult, ok := s.statsCache.Get(cacheKey); ok { + return cachedResult, nil + } + + // 缓存未命中,查询数据库 monitor, err := s.MonitorRepo.FindById(ctx, monitorID) if err != nil { return nil, err @@ -695,43 +719,69 @@ func (s *MonitorService) GetMonitorStatsByID(ctx context.Context, monitorID stri filteredStatsList = append(filteredStatsList, stats) } + // 缓存结果 + s.statsCache.Set(cacheKey, filteredStatsList, 5*time.Minute) + return filteredStatsList, nil } // GetMonitorHistory 获取监控任务的历史响应时间数据 +// 根据前端时间范围选择,直接使用聚合表数据 +// 支持时间范围:15m, 30m, 1h, 3h, 6h, 12h, 1d, 3d, 7d func (s *MonitorService) GetMonitorHistory(ctx context.Context, monitorID, timeRange string) ([]repo.AggregatedMonitorMetric, error) { monitor, err := s.MonitorRepo.FindById(ctx, monitorID) if err != nil { return nil, err } - // 解析时间范围 + // 根据前端时间范围,选择最优的聚合粒度 var duration time.Duration - var interval int // 聚合间隔(秒) + var bucketSeconds int // 使用的聚合 bucket(秒) switch timeRange { - case "5m": - duration = 5 * time.Minute - interval = 15 // 15秒聚合一次 case "15m": duration = 15 * time.Minute - interval = 30 // 30秒聚合一次 + bucketSeconds = 60 // 1分钟聚合,约15个点 case "30m": duration = 30 * time.Minute - interval = 60 // 1分钟聚合一次 + bucketSeconds = 60 // 1分钟聚合,约30个点 case "1h": duration = 1 * time.Hour - interval = 120 // 2分钟聚合一次 + bucketSeconds = 300 // 5分钟聚合,约12个点 + case "3h": + duration = 3 * time.Hour + bucketSeconds = 300 // 5分钟聚合,约36个点 + case "6h": + duration = 6 * time.Hour + bucketSeconds = 900 // 15分钟聚合,约24个点 + case "12h": + duration = 12 * time.Hour + bucketSeconds = 900 // 15分钟聚合,约48个点 + case "1d", "24h": + duration = 24 * time.Hour + bucketSeconds = 1800 // 30分钟聚合,约48个点 + case "3d": + duration = 3 * 24 * time.Hour + bucketSeconds = 3600 // 1小时聚合,约72个点 + case "7d": + duration = 7 * 24 * time.Hour + bucketSeconds = 7200 // 2小时聚合,约84个点 default: - duration = 5 * time.Minute - interval = 15 + // 默认 15 分钟 + duration = 15 * time.Minute + bucketSeconds = 60 } now := time.Now() end := now.UnixMilli() start := now.Add(-duration).UnixMilli() - return s.metricRepo.GetAggregatedMonitorMetrics(ctx, monitor.ID, start, end, interval) + // 对齐时间范围到 bucket 边界 + bucketMs := int64(bucketSeconds * 1000) + start, end = alignTimeRangeToBucket(start, end, bucketMs) + + // 直接使用聚合表查询 + return s.metricRepo.GetMonitorMetricsAgg(ctx, monitor.ID, start, end, bucketSeconds) } // GetMonitorByAuth 根据认证状态获取监控任务(已登录返回全部,未登录返回公开可见) diff --git a/web/src/api/monitor.ts b/web/src/api/monitor.ts index 7148924..f73700c 100644 --- a/web/src/api/monitor.ts +++ b/web/src/api/monitor.ts @@ -34,9 +34,14 @@ export const getPublicMonitors = () => { return get('/monitors'); }; -// 公开接口 - 获取指定监控的统计数据 +// 公开接口 - 获取指定监控的统计数据(聚合后的单个监控详情) export const getMonitorStatsById = (id: string) => { - return get(`/monitors/${encodeURIComponent(id)}/stats`); + return get(`/monitors/${encodeURIComponent(id)}/stats`); +}; + +// 公开接口 - 获取指定监控各探针的统计数据(详细列表) +export const getMonitorAgentStats = (id: string) => { + return get(`/monitors/${encodeURIComponent(id)}/agents`); }; // 聚合的监控历史数据 diff --git a/web/src/pages/Public/MonitorDetail.tsx b/web/src/pages/Public/MonitorDetail.tsx index 07e4865..d9be788 100644 --- a/web/src/pages/Public/MonitorDetail.tsx +++ b/web/src/pages/Public/MonitorDetail.tsx @@ -15,8 +15,8 @@ import { } from 'lucide-react'; import type {TooltipProps} from 'recharts'; import {Area, AreaChart, CartesianGrid, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis,} from 'recharts'; -import {type AggregatedMonitorMetric, getMonitorHistory, getMonitorStatsById} from '@/api/monitor.ts'; -import type {MonitorStats} from '@/types'; +import {type AggregatedMonitorMetric, getMonitorHistory, getMonitorStatsById, getMonitorAgentStats} from '@/api/monitor.ts'; +import type {MonitorStats, PublicMonitor} from '@/types'; const formatTime = (ms: number): string => { if (!ms || ms <= 0) return '0 ms'; @@ -74,12 +74,12 @@ const timeRangeOptions = [ {label: '15分钟', value: '15m'}, {label: '30分钟', value: '30m'}, {label: '1小时', value: '1h'}, - // {label: '3小时', value: '3h'}, - // {label: '6小时', value: '6h'}, - // {label: '12小时', value: '12h'}, - // {label: '1天', value: '1d'}, - // {label: '3天', value: '3d'}, - // {label: '7天', value: '7d'}, + {label: '3小时', value: '3h'}, + {label: '6小时', value: '6h'}, + {label: '12小时', value: '12h'}, + {label: '1天', value: '1d'}, + {label: '3天', value: '3d'}, + {label: '7天', value: '7d'}, ] const ChartPlaceholder = ({ @@ -318,11 +318,24 @@ const MonitorDetail = () => { const [selectedAgent, setSelectedAgent] = useState('all'); const [timeRange, setTimeRange] = useState('15m'); - const {data: monitorStats = [], isLoading} = useQuery({ - queryKey: ['monitorStats', id], + // 获取监控详情(聚合数据) + const {data: monitorDetail, isLoading} = useQuery({ + queryKey: ['monitorDetail', id], + queryFn: async () => { + if (!id) throw new Error('Monitor ID is required'); + const response = await getMonitorStatsById(id); + return response.data; + }, + refetchInterval: 30000, + enabled: !!id, + }); + + // 获取各探针的统计数据 + const {data: monitorStats = []} = useQuery({ + queryKey: ['monitorAgentStats', id], queryFn: async () => { if (!id) return []; - const response = await getMonitorStatsById(id); + const response = await getMonitorAgentStats(id); return response.data || []; }, refetchInterval: 30000, @@ -392,23 +405,20 @@ const MonitorDetail = () => { return ; } - if (monitorStats.length === 0) { + if (!monitorDetail) { return ; } - const firstStat = monitorStats[0]; - const monitorTitle = firstStat?.name ?? '监控详情'; - const avgUptime24h = monitorStats.reduce((sum, s) => sum + s.uptime24h, 0) / monitorStats.length; - const avgUptime30d = monitorStats.reduce((sum, s) => sum + s.uptime30d, 0) / monitorStats.length; - const hasCert = firstStat.certExpiryDate > 0; - const certExpired = hasCert && firstStat.certExpiryDays < 0; - const certExpiringSoon = hasCert && firstStat.certExpiryDays >= 0 && firstStat.certExpiryDays < 30; + const monitorTitle = monitorDetail.name ?? '监控详情'; + const hasCert = monitorDetail.certExpiryDate > 0; + const certExpired = hasCert && monitorDetail.certExpiryDays < 0; + const certExpiringSoon = hasCert && monitorDetail.certExpiryDays >= 0 && monitorDetail.certExpiryDays < 30; const heroStats = [ - {label: '监控类型', value: firstStat.type.toUpperCase()}, - {label: '探针数量', value: `${monitorStats.length} 个`}, - {label: '24h在线率', value: `${formatPercentValue(avgUptime24h)}%`}, - {label: '30d在线率', value: `${formatPercentValue(avgUptime30d)}%`}, + {label: '监控类型', value: monitorDetail.type.toUpperCase()}, + {label: '探针数量', value: `${monitorDetail.agentCount} 个`}, + {label: '24h在线率', value: `${formatPercentValue(monitorDetail.uptime24h)}%`}, + {label: '30d在线率', value: `${formatPercentValue(monitorDetail.uptime30d)}%`}, ]; return ( @@ -433,7 +443,7 @@ const MonitorDetail = () => {
- {firstStat.type === 'tcp' ? ( + {monitorDetail.type === 'tcp' ? ( ) : ( @@ -444,7 +454,7 @@ const MonitorDetail = () => {

{monitorTitle}

- {firstStat.target} + {monitorDetail.target}

公共视图 · 实时监控概览

@@ -464,11 +474,11 @@ const MonitorDetail = () => {
- 监控 ID:{firstStat.id || id} + 监控 ID:{monitorDetail.id || id} - 探针数量:{monitorStats.length} 个 + 探针数量:{monitorDetail.agentCount} 个 - 目标:{firstStat.target} + 目标:{monitorDetail.target}
@@ -480,26 +490,26 @@ const MonitorDetail = () => { } label="当前响应" - value={formatTime(firstStat.currentResponse)} + value={formatTime(monitorDetail.currentResponse)} color="blue" /> } label="24h 平均响应" - value={formatTime(firstStat.avgResponse24h)} + value={formatTime(monitorDetail.avgResponse24h)} color="blue" /> } label="24h 在线率" - value={`${formatPercentValue(avgUptime24h)}%`} - color={avgUptime24h >= 99 ? 'emerald' : avgUptime24h >= 95 ? 'amber' : 'rose'} + value={`${formatPercentValue(monitorDetail.uptime24h)}%`} + color={monitorDetail.uptime24h >= 99 ? 'emerald' : monitorDetail.uptime24h >= 95 ? 'amber' : 'rose'} /> } label="30d 在线率" - value={`${formatPercentValue(avgUptime30d)}%`} - color={avgUptime30d >= 99 ? 'emerald' : avgUptime30d >= 95 ? 'amber' : 'rose'} + value={`${formatPercentValue(monitorDetail.uptime30d)}%`} + color={monitorDetail.uptime30d >= 99 ? 'emerald' : monitorDetail.uptime30d >= 95 ? 'amber' : 'rose'} /> @@ -537,12 +547,12 @@ const MonitorDetail = () => { ? 'text-amber-700 dark:text-amber-200' : 'text-slate-600 dark:text-slate-400' }`}> - 证书到期时间: {formatDate(firstStat.certExpiryDate)} + 证书到期时间: {formatDate(monitorDetail.certExpiryDate)} {certExpired ? ( (已过期 {Math.abs(firstStat.certExpiryDays)} 天) + className="ml-1">(已过期 {Math.abs(monitorDetail.certExpiryDays)} 天) ) : ( - (剩余 {firstStat.certExpiryDays} 天) + (剩余 {monitorDetail.certExpiryDays} 天) )}

{certExpired && (