From 875969efffc049daf19ee9b3127e8ee50ad4fc54 Mon Sep 17 00:00:00 2001 From: dushixiang Date: Wed, 19 Nov 2025 20:18:21 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=9C=8D=E5=8A=A1=E7=9B=91?= =?UTF-8?q?=E6=8E=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 3 +- go.sum | 2 + internal/app.go | 6 +- internal/handler/monitor_handler.go | 18 +- internal/models/metric.go | 4 +- internal/models/monitor_stats.go | 38 +-- internal/protocol/message.go | 2 +- internal/protocol/monitor.go | 2 +- internal/repo/metric_repo.go | 30 +-- internal/repo/monitor_stats_repo.go | 36 +-- internal/service/agent_service.go | 6 +- internal/service/monitor_service.go | 268 +++++++++++++++++--- pkg/agent/collector/monitor.go | 6 +- pkg/agent/service/agent.go | 2 +- web/src/api/monitor.ts | 16 +- web/src/pages/Public/MonitorDetail.tsx | 75 +++--- web/src/pages/Public/MonitorList.tsx | 336 +++++++++++++------------ web/src/router/index.tsx | 2 +- web/src/types/index.ts | 25 +- 19 files changed, 556 insertions(+), 321 deletions(-) diff --git a/go.mod b/go.mod index 7844289..a64e33a 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/docker/docker v28.5.2+incompatible github.com/go-errors/errors v1.5.1 github.com/go-orz/orz v0.2.9 + github.com/go-orz/toolkit v0.1.0 github.com/go-playground/locales v0.14.1 github.com/go-playground/universal-translator v0.18.1 github.com/go-playground/validator/v10 v10.28.0 @@ -21,6 +22,7 @@ require ( go.uber.org/zap v1.27.0 golang.org/x/crypto v0.43.0 gopkg.in/yaml.v3 v3.0.1 + gorm.io/datatypes v1.2.7 gorm.io/gorm v1.31.1 ) @@ -94,7 +96,6 @@ require ( golang.org/x/text v0.30.0 // indirect golang.org/x/time v0.11.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect - gorm.io/datatypes v1.2.7 // indirect gorm.io/driver/mysql v1.6.0 // indirect gorm.io/driver/postgres v1.6.0 // indirect gotest.tools/v3 v3.5.2 // indirect diff --git a/go.sum b/go.sum index 79cf0b3..e418fb6 100644 --- a/go.sum +++ b/go.sum @@ -51,6 +51,8 @@ github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-orz/orz v0.2.9 h1:9n3yl+N450nDxEfbfE6OX8muQmWorNp9E7YIpvx0SYQ= github.com/go-orz/orz v0.2.9/go.mod h1:KMrLiZ5R2ZyTho4VHWCJ7z5ff2dGYXCFOu6+lCNm++4= +github.com/go-orz/toolkit v0.1.0 h1:u7jx6wuwgzTSMudiyqdFedgP1VmhZzUO+1r5F1Yw6mc= +github.com/go-orz/toolkit v0.1.0/go.mod h1:y7MySpBCa+GpGh+l83WoaSzFBCfVGTObKa0fd1DhP0s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= diff --git a/internal/app.go b/internal/app.go index 599e63c..f49e1e0 100644 --- a/internal/app.go +++ b/internal/app.go @@ -141,12 +141,12 @@ func setupApi(app *orz.App, components *AppComponents) { // Agent 版本和下载 publicApi.GET("/agent/version", components.AgentHandler.GetAgentVersion) - publicApi.GET("/agent/downloads/:filename", components.AgentHandler.DownloadAgent) + publicApi.GET("/agent/downloads/:fi1lename", components.AgentHandler.DownloadAgent) // 监控统计数据(公开访问)- 用于公共展示页面 publicApi.GET("/monitors/stats", components.MonitorHandler.GetAllStats) - publicApi.GET("/monitors/:name/stats", components.MonitorHandler.GetStatsByName) - publicApi.GET("/monitors/:name/history", components.MonitorHandler.GetHistoryByName) + publicApi.GET("/monitors/:id/stats", components.MonitorHandler.GetStatsByID) + publicApi.GET("/monitors/:id/history", components.MonitorHandler.GetHistoryByID) } // WebSocket 路由(探针连接) diff --git a/internal/handler/monitor_handler.go b/internal/handler/monitor_handler.go index baf62a7..f444d5d 100644 --- a/internal/handler/monitor_handler.go +++ b/internal/handler/monitor_handler.go @@ -121,7 +121,7 @@ func (h *MonitorHandler) Delete(c echo.Context) error { // GetAllStats 获取所有监控统计数据 func (h *MonitorHandler) GetAllStats(c echo.Context) error { ctx := c.Request().Context() - stats, err := h.monitorService.GetAllMonitorStats(ctx) + stats, err := h.monitorService.GetPublicMonitorOverview(ctx) if err != nil { return err } @@ -129,12 +129,12 @@ func (h *MonitorHandler) GetAllStats(c echo.Context) error { return orz.Ok(c, stats) } -// GetStatsByName 获取指定监控任务的统计数据(所有探针) -func (h *MonitorHandler) GetStatsByName(c echo.Context) error { - name := c.Param("name") +// GetStatsByID 获取指定监控任务的统计数据(所有探针) +func (h *MonitorHandler) GetStatsByID(c echo.Context) error { + id := c.Param("id") ctx := c.Request().Context() - stats, err := h.monitorService.GetMonitorStatsByName(ctx, name) + stats, err := h.monitorService.GetMonitorStatsByID(ctx, id) if err != nil { return err } @@ -142,9 +142,9 @@ func (h *MonitorHandler) GetStatsByName(c echo.Context) error { return orz.Ok(c, stats) } -// GetHistoryByName 获取指定监控任务的历史响应时间数据 -func (h *MonitorHandler) GetHistoryByName(c echo.Context) error { - name := c.Param("name") +// GetHistoryByID 获取指定监控任务的历史响应时间数据 +func (h *MonitorHandler) GetHistoryByID(c echo.Context) error { + id := c.Param("id") timeRange := c.QueryParam("range") // 默认时间范围为 5 分钟 @@ -153,7 +153,7 @@ func (h *MonitorHandler) GetHistoryByName(c echo.Context) error { } ctx := c.Request().Context() - history, err := h.monitorService.GetMonitorHistory(ctx, name, timeRange) + history, err := h.monitorService.GetMonitorHistory(ctx, id, timeRange) if err != nil { return err } diff --git a/internal/models/metric.go b/internal/models/metric.go index fe921b5..9bf4dab 100644 --- a/internal/models/metric.go +++ b/internal/models/metric.go @@ -180,8 +180,8 @@ func (DockerMetric) TableName() string { // MonitorMetric 监控指标 type MonitorMetric struct { ID uint `gorm:"primaryKey;autoIncrement" json:"id"` - AgentID string `gorm:"index:idx_agent_name_time;index:idx_agent_time" json:"agentId"` // 探针ID - Name string `gorm:"index:idx_agent_name_time;index" json:"name"` // 监控项名称 + AgentId string `json:"agentId"` // 探针ID + MonitorId string `json:"monitorId"` // 监控项ID Type string `json:"type"` // 监控类型: http, tcp Target string `json:"target"` // 监控目标 Status string `json:"status"` // 状态: up, down diff --git a/internal/models/monitor_stats.go b/internal/models/monitor_stats.go index 830c204..c91dec4 100644 --- a/internal/models/monitor_stats.go +++ b/internal/models/monitor_stats.go @@ -2,24 +2,26 @@ package models // MonitorStats 监控统计数据 type MonitorStats struct { - ID uint `gorm:"primaryKey;autoIncrement" json:"id"` - AgentID string `gorm:"index:idx_agent_name;uniqueIndex:idx_agent_name_unique" json:"agentId"` // 探针ID - MonitorName string `gorm:"index:idx_agent_name;uniqueIndex:idx_agent_name_unique" json:"name"` // 监控项名称 - MonitorType string `json:"type"` // 监控类型 - Target string `json:"target"` // 目标地址 - CurrentResponse int64 `json:"currentResponse"` // 当前响应时间(ms) - AvgResponse24h int64 `json:"avgResponse24h"` // 24小时平均响应时间(ms) - Uptime24h float64 `json:"uptime24h"` // 24小时在线率(百分比) - Uptime30d float64 `json:"uptime30d"` // 30天在线率(百分比) - CertExpiryDate int64 `json:"certExpiryDate"` // 证书过期时间(毫秒时间戳),0表示无证书 - CertExpiryDays int `json:"certExpiryDays"` // 证书剩余天数 - TotalChecks24h int64 `json:"totalChecks24h"` // 24小时总检测次数 - SuccessChecks24h int64 `json:"successChecks24h"` // 24小时成功次数 - TotalChecks30d int64 `json:"totalChecks30d"` // 30天总检测次数 - SuccessChecks30d int64 `json:"successChecks30d"` // 30天成功次数 - LastCheckTime int64 `json:"lastCheckTime"` // 最后检测时间 - LastCheckStatus string `json:"lastCheckStatus"` // 最后检测状态: up/down - UpdatedAt int64 `gorm:"autoUpdateTime:milli" json:"updatedAt"` // 更新时间 + ID string `gorm:"primaryKey" json:"id"` // ID + AgentID string `json:"agentId"` // 探针ID + AgentName string `gorm:"-" json:"agentName,omitempty"` // 探针名称(不存储在数据库,仅用于 API 返回) + MonitorId string `json:"monitorId"` // 监控项ID + MonitorName string `gorm:"-" json:"name"` // 监控项名称(不存储在数据库,仅用于 API 返回) + MonitorType string `json:"type"` // 监控类型 + Target string `json:"target"` // 目标地址 + CurrentResponse int64 `json:"currentResponse"` // 当前响应时间(ms) + AvgResponse24h int64 `json:"avgResponse24h"` // 24小时平均响应时间(ms) + Uptime24h float64 `json:"uptime24h"` // 24小时在线率(百分比) + Uptime30d float64 `json:"uptime30d"` // 30天在线率(百分比) + CertExpiryDate int64 `json:"certExpiryDate"` // 证书过期时间(毫秒时间戳),0表示无证书 + CertExpiryDays int `json:"certExpiryDays"` // 证书剩余天数 + TotalChecks24h int64 `json:"totalChecks24h"` // 24小时总检测次数 + SuccessChecks24h int64 `json:"successChecks24h"` // 24小时成功次数 + TotalChecks30d int64 `json:"totalChecks30d"` // 30天总检测次数 + SuccessChecks30d int64 `json:"successChecks30d"` // 30天成功次数 + LastCheckTime int64 `json:"lastCheckTime"` // 最后检测时间 + LastCheckStatus string `json:"lastCheckStatus"` // 最后检测状态: up/down + UpdatedAt int64 `gorm:"autoUpdateTime:milli" json:"updatedAt"` // 更新时间 } func (MonitorStats) TableName() string { diff --git a/internal/protocol/message.go b/internal/protocol/message.go index 9bd6ddb..75f1696 100644 --- a/internal/protocol/message.go +++ b/internal/protocol/message.go @@ -267,7 +267,7 @@ type DockerContainerData struct { // MonitorData 监控数据 type MonitorData struct { - Name string `json:"name"` // 监控项名称 + ID string `json:"id"` // 监控项ID Type string `json:"type"` // 监控类型: http, tcp Target string `json:"target"` // 监控目标 Status string `json:"status"` // 状态: up, down diff --git a/internal/protocol/monitor.go b/internal/protocol/monitor.go index 7e1150e..7a2b31e 100644 --- a/internal/protocol/monitor.go +++ b/internal/protocol/monitor.go @@ -8,7 +8,7 @@ type MonitorConfigPayload struct { // MonitorItem 监控项配置 type MonitorItem struct { - Name string `json:"name"` + ID string `json:"id"` Type string `json:"type"` Target string `json:"target"` HTTPConfig *HTTPMonitorConfig `json:"httpConfig,omitempty"` diff --git a/internal/repo/metric_repo.go b/internal/repo/metric_repo.go index 818ed9d..41f2881 100644 --- a/internal/repo/metric_repo.go +++ b/internal/repo/metric_repo.go @@ -435,14 +435,14 @@ func (r *MetricRepo) SaveMonitorMetric(ctx context.Context, metric *models.Monit } // GetMonitorMetrics 获取监控指标列表 -func (r *MetricRepo) GetMonitorMetrics(ctx context.Context, agentID, monitorName string, start, end int64) ([]models.MonitorMetric, error) { +func (r *MetricRepo) GetMonitorMetrics(ctx context.Context, agentID, monitorID string, start, end int64) ([]models.MonitorMetric, error) { var metrics []models.MonitorMetric query := r.db.WithContext(ctx). Where("agent_id = ? AND timestamp >= ? AND timestamp <= ?", agentID, start, end) - // 如果指定了监控项名称,则只查询该监控项 - if monitorName != "" { - query = query.Where("name = ?", monitorName) + // 如果指定了监控项ID,则只查询该监控项 + if monitorID != "" { + query = query.Where("monitor_id = ?", monitorID) } err := query.Order("timestamp ASC").Find(&metrics).Error @@ -451,23 +451,23 @@ func (r *MetricRepo) GetMonitorMetrics(ctx context.Context, agentID, monitorName // GetLatestMonitorMetrics 获取最新的监控指标(每个监控项的最新一条) func (r *MetricRepo) GetLatestMonitorMetrics(ctx context.Context, agentID string) ([]models.MonitorMetric, error) { - // 先获取该 agent 下所有监控项名称 - var names []string + // 先获取该 agent 下所有监控项ID + var monitorIDs []string err := r.db.WithContext(ctx). Model(&models.MonitorMetric{}). Where("agent_id = ?", agentID). - Distinct("name"). - Pluck("name", &names).Error + Distinct("monitor_id"). + Pluck("monitor_id", &monitorIDs).Error if err != nil { return nil, err } // 对每个监控项获取最新的一条记录 var metrics []models.MonitorMetric - for _, name := range names { + for _, monitorID := range monitorIDs { var metric models.MonitorMetric err := r.db.WithContext(ctx). - Where("agent_id = ? AND name = ?", agentID, name). + Where("agent_id = ? AND monitor_id = ?", agentID, monitorID). Order("timestamp DESC"). First(&metric).Error if err == nil { @@ -479,10 +479,10 @@ func (r *MetricRepo) GetLatestMonitorMetrics(ctx context.Context, agentID string } // GetMonitorMetricsByName 获取指定监控项的历史数据 -func (r *MetricRepo) GetMonitorMetricsByName(ctx context.Context, agentID, monitorName string, start, end int64, limit int) ([]models.MonitorMetric, error) { +func (r *MetricRepo) GetMonitorMetricsByName(ctx context.Context, agentID, monitorID string, start, end int64, limit int) ([]models.MonitorMetric, error) { var metrics []models.MonitorMetric query := r.db.WithContext(ctx). - Where("agent_id = ? AND name = ? AND timestamp >= ? AND timestamp <= ?", agentID, monitorName, start, end). + Where("agent_id = ? AND monitor_id = ? AND timestamp >= ? AND timestamp <= ?", agentID, monitorID, start, end). Order("timestamp DESC") if limit > 0 { @@ -508,7 +508,7 @@ type AggregatedMonitorMetric struct { } // GetAggregatedMonitorMetrics 获取聚合后的监控指标(按探针和时间间隔聚合) -func (r *MetricRepo) GetAggregatedMonitorMetrics(ctx context.Context, monitorName string, start, end int64, interval int) ([]AggregatedMonitorMetric, error) { +func (r *MetricRepo) GetAggregatedMonitorMetrics(ctx context.Context, monitorID string, start, end int64, interval int) ([]AggregatedMonitorMetric, error) { var metrics []AggregatedMonitorMetric query := ` @@ -522,7 +522,7 @@ func (r *MetricRepo) GetAggregatedMonitorMetrics(ctx context.Context, monitorNam timestamp, ROW_NUMBER() OVER (PARTITION BY agent_id, CAST(FLOOR(timestamp / ?) * ? AS BIGINT) ORDER BY timestamp DESC) as rn FROM monitor_metrics - WHERE name = ? AND timestamp >= ? AND timestamp <= ? + WHERE monitor_id = ? AND timestamp >= ? AND timestamp <= ? ) SELECT time_bucket as timestamp, @@ -542,7 +542,7 @@ func (r *MetricRepo) GetAggregatedMonitorMetrics(ctx context.Context, monitorNam intervalMs := int64(interval * 1000) err := r.db.WithContext(ctx). - Raw(query, intervalMs, intervalMs, intervalMs, intervalMs, monitorName, start, end). + Raw(query, intervalMs, intervalMs, intervalMs, intervalMs, monitorID, start, end). Scan(&metrics).Error return metrics, err diff --git a/internal/repo/monitor_stats_repo.go b/internal/repo/monitor_stats_repo.go index 47b766e..a7e5ced 100644 --- a/internal/repo/monitor_stats_repo.go +++ b/internal/repo/monitor_stats_repo.go @@ -9,49 +9,29 @@ import ( ) type MonitorStatsRepo struct { - orz.Repository[models.MonitorStats, uint] + orz.Repository[models.MonitorStats, string] db *gorm.DB } func NewMonitorStatsRepo(db *gorm.DB) *MonitorStatsRepo { return &MonitorStatsRepo{ - Repository: orz.NewRepository[models.MonitorStats, uint](db), + Repository: orz.NewRepository[models.MonitorStats, string](db), db: db, } } -// FindByAgentAndName 根据探针ID和监控名称查找统计数据 -func (r *MonitorStatsRepo) FindByAgentAndName(ctx context.Context, agentID, monitorName string) (*models.MonitorStats, error) { - var stats models.MonitorStats - err := r.db.WithContext(ctx). - Where("agent_id = ? AND monitor_name = ?", agentID, monitorName). - First(&stats).Error - if err != nil { - return nil, err - } - return &stats, nil -} - -// UpsertStats 插入或更新统计数据 -func (r *MonitorStatsRepo) UpsertStats(ctx context.Context, stats *models.MonitorStats) error { - return r.db.WithContext(ctx). - Where("agent_id = ? AND monitor_name = ?", stats.AgentID, stats.MonitorName). - Assign(stats). - FirstOrCreate(stats).Error -} - -// ListByMonitorName 根据监控名称列出所有探针的统计数据 -func (r *MonitorStatsRepo) ListByMonitorName(ctx context.Context, monitorName string) ([]models.MonitorStats, error) { +func (r *MonitorStatsRepo) FindByMonitorId(ctx context.Context, monitorId string) ([]models.MonitorStats, error) { var statsList []models.MonitorStats err := r.db.WithContext(ctx). - Where("monitor_name = ?", monitorName). + Where("monitor_id = ?", monitorId). Find(&statsList).Error return statsList, err } -// ListAll 列出所有统计数据 -func (r *MonitorStatsRepo) ListAll(ctx context.Context) ([]models.MonitorStats, error) { +func (r *MonitorStatsRepo) FindByMonitorIdIn(ctx context.Context, monitorIds []string) ([]models.MonitorStats, error) { var statsList []models.MonitorStats - err := r.db.WithContext(ctx).Find(&statsList).Error + err := r.db.WithContext(ctx). + Where("monitor_id in ?", monitorIds). + Find(&statsList).Error return statsList, err } diff --git a/internal/service/agent_service.go b/internal/service/agent_service.go index 9086bae..53b0374 100644 --- a/internal/service/agent_service.go +++ b/internal/service/agent_service.go @@ -379,8 +379,8 @@ func (s *AgentService) HandleMetricData(ctx context.Context, agentID string, met // 保存每个监控项的数据 for _, monitorData := range monitorDataList { metric := &models.MonitorMetric{ - AgentID: agentID, - Name: monitorData.Name, + AgentId: agentID, + MonitorId: monitorData.ID, Type: monitorData.Type, Target: monitorData.Target, Status: monitorData.Status, @@ -397,7 +397,7 @@ func (s *AgentService) HandleMetricData(ctx context.Context, agentID string, met s.logger.Error("failed to save monitor metric", zap.Error(err), zap.String("agentID", agentID), - zap.String("name", monitorData.Name)) + zap.String("MonitorId", monitorData.ID)) } } return nil diff --git a/internal/service/monitor_service.go b/internal/service/monitor_service.go index f4e3c07..9fa051b 100644 --- a/internal/service/monitor_service.go +++ b/internal/service/monitor_service.go @@ -11,6 +11,7 @@ import ( "github.com/dushixiang/pika/internal/repo" ws "github.com/dushixiang/pika/internal/websocket" "github.com/go-orz/orz" + "github.com/go-orz/toolkit" "github.com/google/uuid" "go.uber.org/zap" "gorm.io/datatypes" @@ -50,6 +51,27 @@ type MonitorTaskRequest struct { AgentIds []string `json:"agentIds,omitempty"` } +// PublicMonitorOverview 用于公开展示的监控配置及汇总数据 +type PublicMonitorOverview struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Target string `json:"target"` + Description string `json:"description"` + Enabled bool `json:"enabled"` + Interval int `json:"interval"` + AgentIds []string `json:"agentIds"` + AgentCount int `json:"agentCount"` + LastCheckStatus string `json:"lastCheckStatus"` + CurrentResponse int64 `json:"currentResponse"` + AvgResponse24h int64 `json:"avgResponse24h"` + Uptime24h float64 `json:"uptime24h"` + Uptime30d float64 `json:"uptime30d"` + CertExpiryDate int64 `json:"certExpiryDate"` + CertExpiryDays int `json:"certExpiryDays"` + LastCheckTime int64 `json:"lastCheckTime"` +} + func (s *MonitorService) CreateMonitor(ctx context.Context, req *MonitorTaskRequest) (*models.MonitorTask, error) { // 设置默认检测频率 interval := req.Interval @@ -117,6 +139,155 @@ func (s *MonitorService) DeleteMonitor(ctx context.Context, id string) error { }) } +// GetPublicMonitorOverview 返回公开展示所需的监控配置和汇总统计 +func (s *MonitorService) GetPublicMonitorOverview(ctx context.Context) ([]PublicMonitorOverview, error) { + var monitors []models.MonitorTask + if err := s.MonitorRepo.GetDB(ctx). + Where("enabled = ?", true). + Order("name ASC"). + Find(&monitors).Error; err != nil { + return nil, err + } + + monitorIds := make([]string, 0, len(monitors)) + for _, monitor := range monitors { + monitorIds = append(monitorIds, monitor.ID) + } + + statsList, err := s.monitorStatsRepo.FindByMonitorIdIn(ctx, monitorIds) + if err != nil { + return nil, err + } + + statsMap := make(map[string][]models.MonitorStats, len(monitors)) + for _, stats := range statsList { + statsMap[stats.MonitorId] = append(statsMap[stats.MonitorId], stats) + } + + items := make([]PublicMonitorOverview, 0, len(monitors)) + for _, monitor := range monitors { + summary := aggregateMonitorStats(statsMap[monitor.ID]) + item := PublicMonitorOverview{ + ID: monitor.ID, + Name: monitor.Name, + Type: monitor.Type, + Target: monitor.Target, + Description: monitor.Description, + Enabled: monitor.Enabled, + Interval: monitor.Interval, + AgentIds: cloneAgentIDs(monitor.AgentIds), + AgentCount: summary.AgentCount, + LastCheckStatus: summary.LastCheckStatus, + CurrentResponse: summary.CurrentResponse, + AvgResponse24h: summary.AvgResponse24h, + Uptime24h: summary.Uptime24h, + Uptime30d: summary.Uptime30d, + CertExpiryDate: summary.CertExpiryDate, + CertExpiryDays: summary.CertExpiryDays, + LastCheckTime: summary.LastCheckTime, + } + + items = append(items, item) + } + + return items, nil +} + +type monitorOverviewSummary struct { + AgentCount int + LastCheckStatus string + CurrentResponse int64 + AvgResponse24h int64 + Uptime24h float64 + Uptime30d float64 + CertExpiryDate int64 + CertExpiryDays int + LastCheckTime int64 +} + +func aggregateMonitorStats(stats []models.MonitorStats) monitorOverviewSummary { + summary := monitorOverviewSummary{ + LastCheckStatus: "unknown", + } + + if len(stats) == 0 { + return summary + } + + var totalCurrentResponse int64 + var totalAvgResponse24h int64 + var totalUptime24h float64 + var totalUptime30d float64 + var lastCheckTime int64 + var certExpiryDate int64 + var certExpiryDays int + hasCert := false + hasUp := false + hasDown := false + + for _, stat := range stats { + totalCurrentResponse += stat.CurrentResponse + totalAvgResponse24h += stat.AvgResponse24h + totalUptime24h += stat.Uptime24h + totalUptime30d += stat.Uptime30d + + if stat.LastCheckTime > lastCheckTime { + lastCheckTime = stat.LastCheckTime + } + + switch stat.LastCheckStatus { + case "up": + hasUp = true + case "down": + hasDown = true + } + + if stat.CertExpiryDate > 0 { + if !hasCert || stat.CertExpiryDate < certExpiryDate { + certExpiryDate = stat.CertExpiryDate + certExpiryDays = stat.CertExpiryDays + hasCert = true + } + } + } + + count := len(stats) + summary.AgentCount = count + if count > 0 { + summary.CurrentResponse = totalCurrentResponse / int64(count) + summary.AvgResponse24h = totalAvgResponse24h / int64(count) + summary.Uptime24h = totalUptime24h / float64(count) + summary.Uptime30d = totalUptime30d / float64(count) + } + summary.LastCheckTime = lastCheckTime + + switch { + case hasUp: + summary.LastCheckStatus = "up" + case hasDown: + summary.LastCheckStatus = "down" + default: + summary.LastCheckStatus = "unknown" + } + + if hasCert { + summary.CertExpiryDate = certExpiryDate + summary.CertExpiryDays = certExpiryDays + } + + return summary +} + +func cloneAgentIDs(ids datatypes.JSONSlice[string]) []string { + if len(ids) == 0 { + return []string{} + } + + copied := make([]string, len(ids)) + copy(copied, []string(ids)) + return copied +} + // BroadcastMonitorConfig 向所有在线探针广播监控配置 func (s *MonitorService) BroadcastMonitorConfig(ctx context.Context) error { // 获取所有启用的监控任务 @@ -172,7 +343,7 @@ func (s *MonitorService) BroadcastMonitorConfig(ctx context.Context) error { items := make([]protocol.MonitorItem, 0, len(tasks)) for _, task := range tasks { item := protocol.MonitorItem{ - Name: task.Name, + ID: task.ID, Type: task.Type, Target: task.Target, } @@ -236,36 +407,47 @@ func (s *MonitorService) sendMonitorConfigToAgent(agentID string, payload protoc // SendMonitorTaskToAgents 向指定探针发送单个监控任务(公开方法) func (s *MonitorService) SendMonitorTaskToAgents(ctx context.Context, monitor models.MonitorTask, agentIDs []string) error { - // 获取所有在线探针 - agents, err := s.agentRepo.FindOnlineAgents(ctx) - if err != nil { - return err + // 实时获取所有在线探针,避免依赖数据库状态 + onlineIDs := s.wsManager.GetAllClients() + if len(onlineIDs) == 0 { + return nil } - // 确定要发送的探针列表 - var targetAgents []models.Agent + onlineSet := make(map[string]struct{}, len(onlineIDs)) + for _, id := range onlineIDs { + onlineSet[id] = struct{}{} + } + + // 确定要发送的探针ID列表 + targetAgentIDs := make([]string, 0) if len(agentIDs) == 0 { - // 发送给所有在线探针 - targetAgents = agents + // 发送给所有当前在线的探针 + targetAgentIDs = append(targetAgentIDs, onlineIDs...) } else { - // 只发送给指定的在线探针 - for _, agent := range agents { - for _, agentID := range agentIDs { - if agent.ID == agentID { - targetAgents = append(targetAgents, agent) - break - } + // 只发送给指定且当前在线的探针 + for _, id := range agentIDs { + if _, ok := onlineSet[id]; ok { + targetAgentIDs = append(targetAgentIDs, id) } } } + if len(targetAgentIDs) == 0 { + return nil + } + + // 查询探针基础信息(用于保持原有逻辑的一致性) + targetAgents, err := s.agentRepo.ListByIDs(ctx, targetAgentIDs) + if err != nil { + return err + } if len(targetAgents) == 0 { return nil } // 构建监控项 item := protocol.MonitorItem{ - Name: monitor.Name, + ID: monitor.ID, Type: monitor.Type, Target: monitor.Target, } @@ -337,7 +519,7 @@ func (s *MonitorService) CalculateMonitorStats(ctx context.Context) error { } for _, agent := range targetAgents { - stats, err := s.calculateStatsForAgentMonitor(ctx, agent.ID, monitor.Name, monitor.Type, monitor.Target, now) + stats, err := s.calculateStatsForAgentMonitor(ctx, agent.ID, monitor.ID, monitor.Type, monitor.Target, now) if err != nil { s.logger.Error("计算监控统计失败", zap.String("agentID", agent.ID), @@ -346,7 +528,7 @@ func (s *MonitorService) CalculateMonitorStats(ctx context.Context) error { continue } - if err := s.monitorStatsRepo.UpsertStats(ctx, stats); err != nil { + if err := s.monitorStatsRepo.Save(ctx, stats); err != nil { s.logger.Error("保存监控统计失败", zap.String("agentID", agent.ID), zap.String("monitorName", monitor.Name), @@ -359,10 +541,11 @@ func (s *MonitorService) CalculateMonitorStats(ctx context.Context) error { } // calculateStatsForAgentMonitor 计算单个探针单个监控任务的统计数据 -func (s *MonitorService) calculateStatsForAgentMonitor(ctx context.Context, agentID, monitorName, monitorType, target string, now time.Time) (*models.MonitorStats, error) { +func (s *MonitorService) calculateStatsForAgentMonitor(ctx context.Context, agentID, monitorId, monitorType, target string, now time.Time) (*models.MonitorStats, error) { stats := &models.MonitorStats{ + ID: toolkit.Sign("monitor_stats", agentID, monitorId, monitorType, target), AgentID: agentID, - MonitorName: monitorName, + MonitorId: monitorId, MonitorType: monitorType, Target: target, } @@ -370,14 +553,14 @@ func (s *MonitorService) calculateStatsForAgentMonitor(ctx context.Context, agen // 计算24小时数据 start24h := now.Add(-24 * time.Hour).UnixMilli() end := now.UnixMilli() - metrics24h, err := s.metricRepo.GetMonitorMetrics(ctx, agentID, monitorName, start24h, end) + metrics24h, err := s.metricRepo.GetMonitorMetrics(ctx, agentID, monitorId, start24h, end) if err != nil { return nil, err } // 计算30天数据 start30d := now.Add(-30 * 24 * time.Hour).UnixMilli() - metrics30d, err := s.metricRepo.GetMonitorMetrics(ctx, agentID, monitorName, start30d, end) + metrics30d, err := s.metricRepo.GetMonitorMetrics(ctx, agentID, monitorId, start30d, end) if err != nil { return nil, err } @@ -435,18 +618,39 @@ func (s *MonitorService) calculateStatsForAgentMonitor(ctx context.Context, agen return stats, nil } -// GetMonitorStatsByName 获取监控任务的统计数据(所有探针) -func (s *MonitorService) GetMonitorStatsByName(ctx context.Context, monitorName string) ([]models.MonitorStats, error) { - return s.monitorStatsRepo.ListByMonitorName(ctx, monitorName) -} +// GetMonitorStatsByID 获取监控任务的统计数据(所有探针) +func (s *MonitorService) GetMonitorStatsByID(ctx context.Context, monitorID string) ([]models.MonitorStats, error) { + monitor, err := s.MonitorRepo.FindById(ctx, monitorID) + if err != nil { + return nil, err + } -// GetAllMonitorStats 获取所有监控统计数据 -func (s *MonitorService) GetAllMonitorStats(ctx context.Context) ([]models.MonitorStats, error) { - return s.monitorStatsRepo.ListAll(ctx) + statsList, err := s.monitorStatsRepo.FindByMonitorId(ctx, monitor.ID) + if err != nil { + return nil, err + } + + // 填充监控名称和探针名称 + for i := range statsList { + statsList[i].MonitorName = monitor.Name + + // 查询探针名称 + agent, err := s.agentRepo.FindById(ctx, statsList[i].AgentID) + if err == nil { + statsList[i].AgentName = agent.Name + } + } + + return statsList, nil } // GetMonitorHistory 获取监控任务的历史响应时间数据 -func (s *MonitorService) GetMonitorHistory(ctx context.Context, monitorName, timeRange string) ([]repo.AggregatedMonitorMetric, error) { +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 // 聚合间隔(秒) @@ -473,5 +677,5 @@ func (s *MonitorService) GetMonitorHistory(ctx context.Context, monitorName, tim end := now.UnixMilli() start := now.Add(-duration).UnixMilli() - return s.metricRepo.GetAggregatedMonitorMetrics(ctx, monitorName, start, end, interval) + return s.metricRepo.GetAggregatedMonitorMetrics(ctx, monitor.ID, start, end, interval) } diff --git a/pkg/agent/collector/monitor.go b/pkg/agent/collector/monitor.go index 61a6b82..2fc54a9 100644 --- a/pkg/agent/collector/monitor.go +++ b/pkg/agent/collector/monitor.go @@ -60,7 +60,7 @@ func (c *MonitorCollector) Collect(items []protocol.MonitorItem) []protocol.Moni result = c.checkTCP(item) default: result = protocol.MonitorData{ - Name: item.Name, + ID: item.ID, Type: item.Type, Target: item.Target, Status: "down", @@ -78,7 +78,7 @@ func (c *MonitorCollector) Collect(items []protocol.MonitorItem) []protocol.Moni // checkHTTP 检查 HTTP/HTTPS 服务 func (c *MonitorCollector) checkHTTP(item protocol.MonitorItem) protocol.MonitorData { result := protocol.MonitorData{ - Name: item.Name, + ID: item.ID, Type: item.Type, Target: item.Target, CheckedAt: time.Now().UnixMilli(), @@ -200,7 +200,7 @@ func (c *MonitorCollector) checkHTTP(item protocol.MonitorItem) protocol.Monitor // checkTCP 检查 TCP 端口 func (c *MonitorCollector) checkTCP(item protocol.MonitorItem) protocol.MonitorData { result := protocol.MonitorData{ - Name: item.Name, + ID: item.ID, Type: item.Type, Target: item.Target, CheckedAt: time.Now().UnixMilli(), diff --git a/pkg/agent/service/agent.go b/pkg/agent/service/agent.go index 229aebb..931c8f2 100644 --- a/pkg/agent/service/agent.go +++ b/pkg/agent/service/agent.go @@ -320,7 +320,7 @@ func (a *Agent) registerAgent(conn *safeConn) error { return fmt.Errorf("解析注册响应失败: %w", err) } - log.Printf("注册成功: AgentID=%s, Status=%s", registerResp.AgentID, registerResp.Status) + log.Printf("注册成功: AgentId=%s, Status=%s", registerResp.AgentID, registerResp.Status) return nil } diff --git a/web/src/api/monitor.ts b/web/src/api/monitor.ts index 4121089..652910a 100644 --- a/web/src/api/monitor.ts +++ b/web/src/api/monitor.ts @@ -1,5 +1,5 @@ import {get, post, put, del} from './request'; -import type {MonitorListResponse, MonitorTask, MonitorTaskRequest, MonitorStats} from '../types'; +import type {MonitorListResponse, MonitorTask, MonitorTaskRequest, MonitorStats, PublicMonitor} from '../types'; export const listMonitors = (page: number = 1, pageSize: number = 10, keyword?: string) => { const params = new URLSearchParams(); @@ -27,14 +27,14 @@ export const deleteMonitor = (id: number) => { return del(`/admin/monitors/${id}`); }; -// 公开接口 - 获取所有监控统计数据 -export const getAllMonitorStats = () => { - return get('/monitors/stats'); +// 公开接口 - 获取监控配置及聚合统计 +export const getPublicMonitors = () => { + return get('/monitors/stats'); }; // 公开接口 - 获取指定监控的统计数据 -export const getMonitorStatsByName = (name: string) => { - return get(`/monitors/${encodeURIComponent(name)}/stats`); +export const getMonitorStatsById = (id: string) => { + return get(`/monitors/${encodeURIComponent(id)}/stats`); }; // 聚合的监控历史数据 @@ -52,6 +52,6 @@ export interface AggregatedMonitorMetric { } // 公开接口 - 获取指定监控的历史数据 -export const getMonitorHistory = (name: string, range: string = '5m') => { - return get(`/monitors/${encodeURIComponent(name)}/history?range=${range}`); +export const getMonitorHistory = (id: string, range: string = '5m') => { + return get(`/monitors/${encodeURIComponent(id)}/history?range=${range}`); }; diff --git a/web/src/pages/Public/MonitorDetail.tsx b/web/src/pages/Public/MonitorDetail.tsx index 3da5ab8..049c846 100644 --- a/web/src/pages/Public/MonitorDetail.tsx +++ b/web/src/pages/Public/MonitorDetail.tsx @@ -23,7 +23,7 @@ import { XAxis, YAxis, } from 'recharts'; -import {getMonitorStatsByName, getMonitorHistory, type AggregatedMonitorMetric} from '../../api/monitor'; +import {getMonitorStatsById, getMonitorHistory, type AggregatedMonitorMetric} from '../../api/monitor'; import type {MonitorStats} from '../../types'; import PublicHeader from '../../components/PublicHeader'; import PublicFooter from '../../components/PublicFooter'; @@ -69,19 +69,24 @@ const LoadingSpinner = () => ( ); const StatusBadge = ({status}: { status: string }) => { - const isUp = status === 'up'; + let containerClass = 'bg-slate-100 text-slate-600'; + let label = '未知'; + let icon = ; + + if (status === 'up') { + containerClass = 'bg-emerald-50 text-emerald-700'; + label = '正常'; + icon = ; + } else if (status === 'down') { + containerClass = 'bg-red-50 text-red-700'; + label = '异常'; + icon = ; + } + return ( -
- {isUp ? ( - - ) : ( - - )} - {isUp ? '正常' : '异常'} +
+ {icon} + {label}
); }; @@ -196,31 +201,31 @@ type TimeRange = typeof timeRangeOptions[number]['value']; const MonitorDetail = () => { const navigate = useNavigate(); - const {name} = useParams<{ name: string }>(); + const {id} = useParams<{ id: string }>(); const [selectedAgent, setSelectedAgent] = useState('all'); const [timeRange, setTimeRange] = useState('5m'); const {data: monitorStats = [], isLoading} = useQuery({ - queryKey: ['monitorStats', name], + queryKey: ['monitorStats', id], queryFn: async () => { - if (!name) return []; - const response = await getMonitorStatsByName(name); + if (!id) return []; + const response = await getMonitorStatsById(id); return response.data || []; }, refetchInterval: 30000, - enabled: !!name, + enabled: !!id, }); // 获取历史数据 const {data: historyData = []} = useQuery({ - queryKey: ['monitorHistory', name, timeRange], + queryKey: ['monitorHistory', id, timeRange], queryFn: async () => { - if (!name) return []; - const response = await getMonitorHistory(name, timeRange); + if (!id) return []; + const response = await getMonitorHistory(id, timeRange); return response.data || []; }, refetchInterval: 30000, - enabled: !!name, + enabled: !!id, }); // 获取所有可用的探针列表 @@ -228,7 +233,7 @@ const MonitorDetail = () => { if (monitorStats.length === 0) return []; return monitorStats.map(stat => ({ id: stat.agentId, - label: stat.agentId.substring(0, 8), + label: stat.agentName || stat.agentId.substring(0, 8), })); }, [monitorStats]); @@ -308,6 +313,7 @@ const MonitorDetail = () => { } 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; @@ -315,7 +321,7 @@ const MonitorDetail = () => { return (
- +
@@ -492,7 +498,7 @@ const MonitorDetail = () => { const originalIndex = monitorStats.findIndex(s => s.agentId === stat.agentId); const agentKey = `agent_${stat.agentId}`; const color = AGENT_COLORS[originalIndex % AGENT_COLORS.length]; - const agentLabel = stat.agentId.substring(0, 8); + const agentLabel = stat.agentName || stat.agentId.substring(0, 8); return ( { className="inline-block h-3 w-3 rounded-full" style={{backgroundColor: color}} /> - - {stats.agentId.substring(0, 8)}... - +
+ {stats.agentName ? ( + <> + + {stats.agentName} + + + {stats.agentId.substring(0, 8)}... + + + ) : ( + + {stats.agentId.substring(0, 8)}... + + )} +
diff --git a/web/src/pages/Public/MonitorList.tsx b/web/src/pages/Public/MonitorList.tsx index 81693fd..c53a426 100644 --- a/web/src/pages/Public/MonitorList.tsx +++ b/web/src/pages/Public/MonitorList.tsx @@ -1,9 +1,9 @@ import {useState} from 'react'; import {useNavigate} from 'react-router-dom'; import {useQuery} from '@tanstack/react-query'; -import {AlertCircle, CheckCircle2, Clock, Eye, Loader2, Shield} from 'lucide-react'; -import {getAllMonitorStats} from '../../api/monitor'; -import type {MonitorStats} from '../../types'; +import {AlertCircle, CheckCircle2, Clock, Loader2, Shield} from 'lucide-react'; +import {getPublicMonitors} from '../../api/monitor'; +import type {PublicMonitor} from '../../types'; import PublicHeader from '../../components/PublicHeader'; import PublicFooter from '../../components/PublicFooter'; @@ -37,19 +37,25 @@ const LoadingSpinner = () => ( ); const StatusBadge = ({status}: { status: string }) => { - const isUp = status === 'up'; + let containerClass = 'bg-slate-100 text-slate-600'; + let label = '未知'; + let icon = ; + + if (status === 'up') { + containerClass = 'bg-emerald-50 text-emerald-700'; + label = '正常'; + icon = ; + } else if (status === 'down') { + containerClass = 'bg-red-50 text-red-700'; + label = '异常'; + icon = ; + } + return ( -
- {isUp ? ( - - ) : ( - - )} - {isUp ? '正常' : '异常'} +
+ {icon} + {label}
); }; @@ -80,41 +86,16 @@ const MonitorList = () => { const navigate = useNavigate(); const [viewMode, setViewMode] = useState('grid'); - const {data: monitorStats = [], isLoading, dataUpdatedAt} = useQuery({ - queryKey: ['monitorStats'], + const {data: monitors = [], isLoading, dataUpdatedAt} = useQuery({ + queryKey: ['publicMonitors'], queryFn: async () => { - const response = await getAllMonitorStats(); + const response = await getPublicMonitors(); return response.data || []; }, refetchInterval: 30000, // 30秒刷新一次 }); - // 按监控名称分组 - const groupedStats = monitorStats.reduce((acc, stats) => { - if (!acc[stats.name]) { - acc[stats.name] = []; - } - acc[stats.name].push(stats); - return acc; - }, {} as Record); - - // 计算每个监控的汇总状态 - const monitorSummaries = Object.entries(groupedStats).map(([name, statsList]) => { - const allDown = statsList.every(s => s.lastCheckStatus === 'down'); - const avgUptime24h = statsList.reduce((sum, s) => sum + s.uptime24h, 0) / statsList.length; - const avgUptime30d = statsList.reduce((sum, s) => sum + s.uptime30d, 0) / statsList.length; - - // 使用第一个探针的数据作为代表 - const representative = statsList[0]; - - return { - ...representative, - lastCheckStatus: allDown ? 'down' : 'up', - uptime24h: avgUptime24h, - uptime30d: avgUptime30d, - agentCount: statsList.length, - }; - }); + const monitorSummaries = monitors; const lastUpdatedDisplay = dataUpdatedAt && dataUpdatedAt > 0 @@ -136,11 +117,11 @@ const MonitorList = () => { key={stats.id} role="button" tabIndex={0} - onClick={() => navigate(`/monitors/${encodeURIComponent(stats.name)}`)} + onClick={() => navigate(`/monitors/${encodeURIComponent(stats.id)}`)} onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); - navigate(`/monitors/${encodeURIComponent(stats.name)}`); + navigate(`/monitors/${encodeURIComponent(stats.id)}`); } }} className="group relative flex h-full cursor-pointer flex-col gap-4 rounded-2xl border border-slate-200 bg-white p-5 transition duration-200 hover:border-blue-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-300" @@ -151,7 +132,7 @@ const MonitorList = () => {

{stats.name}

- +

{stats.target} @@ -165,10 +146,12 @@ const MonitorList = () => {

-
+
-
- +
+
当前响应
@@ -177,10 +160,12 @@ const MonitorList = () => {
-
+
-
- +
+
24h 平均
@@ -192,30 +177,36 @@ const MonitorList = () => {
24h 在线率 - {formatPercentValue(stats.uptime24h)}% + {formatPercentValue(stats.uptime24h)}%
- +
30d 在线率 - {formatPercentValue(stats.uptime30d)}% + {formatPercentValue(stats.uptime30d)}%
- +
{hasCert && ( -
+
- + 证书到期
-
+
{formatDate(stats.certExpiryDate)}
-
+
剩余 {stats.certExpiryDays} 天
@@ -241,11 +232,11 @@ const MonitorList = () => { key={stats.id} role="button" tabIndex={0} - onClick={() => navigate(`/monitors/${encodeURIComponent(stats.name)}`)} + onClick={() => navigate(`/monitors/${encodeURIComponent(stats.id)}`)} onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); - navigate(`/monitors/${encodeURIComponent(stats.name)}`); + navigate(`/monitors/${encodeURIComponent(stats.id)}`); } }} className="cursor-pointer rounded-xl border border-slate-200 bg-white p-4 transition hover:border-blue-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-300" @@ -256,7 +247,7 @@ const MonitorList = () => { {stats.name} - +

{stats.target}

{stats.agentCount > 1 && ( @@ -268,39 +259,49 @@ const MonitorList = () => {
-
+
当前响应 - {formatTime(stats.currentResponse)} + {formatTime(stats.currentResponse)}
-
+
24h 平均响应 - {formatTime(stats.avgResponse24h)} + {formatTime(stats.avgResponse24h)}
24h 在线率 - {formatPercentValue(stats.uptime24h)}% + {formatPercentValue(stats.uptime24h)}%
- +
30d 在线率 - {formatPercentValue(stats.uptime30d)}% + {formatPercentValue(stats.uptime30d)}%
- +
{hasCert && ( -
+
- + 证书到期
-
+
{formatDate(stats.certExpiryDate)}
-
+
剩余 {stats.certExpiryDays} 天
@@ -316,109 +317,112 @@ const MonitorList = () => {
- - - - - - - - - + + + + + + + + + - {monitorSummaries.map((stats) => { - const hasCert = stats.certExpiryDate > 0; - const certExpiringSoon = hasCert && stats.certExpiryDays < 30; + {monitorSummaries.map((stats) => { + const hasCert = stats.certExpiryDate > 0; + const certExpiringSoon = hasCert && stats.certExpiryDays < 30; - return ( - navigate(`/monitors/${encodeURIComponent(stats.name)}`)} - onKeyDown={(event) => { - if (event.key === 'Enter' || event.key === ' ') { - event.preventDefault(); - navigate(`/monitors/${encodeURIComponent(stats.name)}`); - } - }} - className="cursor-pointer transition hover:bg-blue-50 focus-within:bg-blue-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-200" - > - navigate(`/monitors/${encodeURIComponent(stats.id)}`)} + onKeyDown={(event) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + navigate(`/monitors/${encodeURIComponent(stats.id)}`); + } + }} + className="cursor-pointer transition hover:bg-blue-50 focus-within:bg-blue-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-200" + > + - - + + - + - + - + - + - - ); - })} + + ) : ( + - + )} + + + ); + })}
监控项状态当前响应24h 平均响应24h 在线率30d 在线率证书信息
监控项状态当前响应24h 平均响应24h 在线率30d 在线率证书信息
-
-
- {stats.name} -
-
- {stats.target} -
- {stats.agentCount > 1 && ( -
- {stats.agentCount} 个探针 -
- )} + return ( +
+
+
+ {stats.name}
-
- - -
- - +
+ {stats.target} +
+ {stats.agentCount > 1 && ( +
+ {stats.agentCount} 个探针 +
+ )} +
+
+ + +
+ + {formatTime(stats.currentResponse)} -
-
+ + {formatTime(stats.avgResponse24h)} - -
-
-
- -
- +
+
+
+
+ +
+ {formatPercentValue(stats.uptime24h)}% -
-
-
-
-
- -
- +
+
+
+
+
+ +
+ {formatPercentValue(stats.uptime30d)}% -
-
- {hasCert ? ( -
- -
-
- {formatDate(stats.certExpiryDate)} -
-
- 剩余 {stats.certExpiryDays} 天 -
+
+
+ {hasCert ? ( +
+ +
+
+ {formatDate(stats.certExpiryDate)} +
+
+ 剩余 {stats.certExpiryDays} 天
- ) : ( - - - )} -
@@ -437,10 +441,10 @@ const MonitorList = () => { />
- +
- +
); } @@ -458,7 +462,7 @@ const MonitorList = () => {
{monitorSummaries.length === 0 ? ( - + ) : viewMode === 'grid' ? ( renderGridView() ) : ( @@ -467,7 +471,7 @@ const MonitorList = () => {
- +
); }; diff --git a/web/src/router/index.tsx b/web/src/router/index.tsx index ff1ded9..478935b 100644 --- a/web/src/router/index.tsx +++ b/web/src/router/index.tsx @@ -49,7 +49,7 @@ const router = createBrowserRouter([ element: lazyLoad(PublicMonitorListPage), }, { - path: '/monitors/:name', + path: '/monitors/:id', element: lazyLoad(PublicMonitorDetailPage), }, // 管理员页面 - 需要登录 diff --git a/web/src/types/index.ts b/web/src/types/index.ts index c53a5ab..2f1245d 100644 --- a/web/src/types/index.ts +++ b/web/src/types/index.ts @@ -293,11 +293,34 @@ export interface MonitorListResponse { pageSize: number; } +// 面向公开页面的监控项及聚合统计 +export interface PublicMonitor { + id: string; + name: string; + type: 'http' | 'https' | 'tcp'; + target: string; + description?: string; + enabled: boolean; + interval: number; + agentIds: string[]; + agentCount: number; + lastCheckStatus: string; + currentResponse: number; + avgResponse24h: number; + uptime24h: number; + uptime30d: number; + certExpiryDate: number; + certExpiryDays: number; + lastCheckTime: number; +} + // 监控统计数据 export interface MonitorStats { id: number; agentId: string; - name: string; + agentName?: string; // 探针名称 + monitorId: string; // 监控项ID + name: string; // 监控项名称 type: string; target: string; currentResponse: number; // 当前响应时间(ms)