package service import ( "hy2xs-admin/dao" "hy2xs-admin/model/vo" "strings" "time" ) func DashboardSummary() (vo.DashboardSummaryVo, error) { nowMs := time.Now().UnixMilli() summary := vo.DashboardSummaryVo{CollectedAt: nowMs} summary.Health.Collector = vo.DataHealthVo{Status: "stale", MessageKey: "dashboard.error.collectorStale"} summary.Health.Hysteria = vo.DataHealthVo{Status: "ok"} // До первого снимка сборщика о службе не известно ничего. Нулевое значение // `Running = false` прочиталось бы как «остановлена» — то самое смешение // «неизвестно» и «выключено», от которого избавляет HysteriaServiceState. summary.Hysteria.ServiceState = string(HysteriaServiceUnknown) snapshot := DashboardSnapshot() if snapshot.CollectedAt > 0 { summary.CollectedAt = snapshot.CollectedAt summary.System = snapshot.System summary.Hysteria = snapshot.Hysteria summary.Peers.OnlinePeers = snapshot.OnlinePeers summary.Peers.OnlineDevices = snapshot.OnlineDevices summary.Health.Collector = snapshot.CollectorState summary.Health.Hysteria = snapshot.HysteriaState if nowMs-snapshot.CollectedAt > int64(90*time.Second/time.Millisecond) { summary.Health.Collector = vo.DataHealthVo{Status: "stale", MessageKey: "dashboard.error.collectorStale", LastSuccessAt: snapshot.CollectedAt} } } if peerSummary, err := dao.DashboardPeerSummary(nowMs); err == nil { summary.Peers.Total = peerSummary.Total summary.Peers.Enabled = peerSummary.Enabled summary.Peers.Disabled = peerSummary.Disabled summary.Peers.Expired = peerSummary.Expired } if trafficSummary, err := dao.DashboardTrafficSummary(); err == nil { summary.Traffic = trafficSummary } summary.SecurityRisks = DashboardSecurityRisks(summary) return summary, nil } func DashboardTimeseries(rangeKey string) (vo.DashboardTimeseriesVo, error) { nowMs := time.Now().UnixMilli() fromMs := nowMs - int64(24*time.Hour/time.Millisecond) bucketMs := int64(5 * time.Minute / time.Millisecond) source := "sample" r := "24h" switch strings.TrimSpace(rangeKey) { case "1h": fromMs = nowMs - int64(time.Hour/time.Millisecond) bucketMs = int64(time.Minute / time.Millisecond) source = "sample" r = "1h" case "7d": fromMs = nowMs - int64(7*24*time.Hour/time.Millisecond) bucketMs = int64(time.Hour / time.Millisecond) source = "hourly" r = "7d" case "24h", "": fromMs = nowMs - int64(24*time.Hour/time.Millisecond) bucketMs = int64(5 * time.Minute / time.Millisecond) source = "sample" r = "24h" default: fromMs = nowMs - int64(24*time.Hour/time.Millisecond) bucketMs = int64(5 * time.Minute / time.Millisecond) source = "sample" r = "24h" } traffic, err := dao.DashboardTrafficTimeseries(fromMs, nowMs, bucketMs, source) if err != nil { return vo.DashboardTimeseriesVo{}, err } systemRows, err := dao.DashboardSystemTimeseries(fromMs, nowMs, bucketMs) if err != nil { return vo.DashboardTimeseriesVo{}, err } return vo.DashboardTimeseriesVo{ Range: r, Traffic: traffic, System: systemRows, CollectedAt: nowMs, }, nil } func DashboardTopPeers(rangeKey string, limit int) ([]vo.DashboardTopPeerVo, error) { nowMs := time.Now().UnixMilli() normalizedRange := strings.TrimSpace(strings.ToLower(rangeKey)) fromMs := nowMs - int64(24*time.Hour/time.Millisecond) source := "sample" switch normalizedRange { case "1h": fromMs = nowMs - int64(time.Hour/time.Millisecond) source = "sample" case "7d": fromMs = nowMs - int64(7*24*time.Hour/time.Millisecond) source = "hourly" case "24h", "": fromMs = nowMs - int64(24*time.Hour/time.Millisecond) source = "sample" default: fromMs = nowMs - int64(24*time.Hour/time.Millisecond) source = "sample" } return dao.DashboardTopPeers(fromMs, nowMs, limit, source) } func DashboardSecurity() ([]vo.SecurityRiskVo, error) { summary, err := DashboardSummary() if err != nil { return []vo.SecurityRiskVo{}, err } return summary.SecurityRisks, nil } // DashboardSecurityRisks называет ровно то, что известно. // // «Служба остановлена» и «состояние службы неизвестно» — разные сообщения с // разными действиями оператора. Пока состояние было булевым, второе выдавалось // за первое: сломанный systemctl при живой Hysteria показывал критическую // плашку «служба остановлена», и оператор шёл перезапускать работающий // туннель. func DashboardSecurityRisks(summary vo.DashboardSummaryVo) []vo.SecurityRiskVo { risks := make([]vo.SecurityRiskVo, 0) switch summary.Hysteria.ServiceState { case string(HysteriaServiceActive): // Обслуживание идёт — сообщать нечего. case string(HysteriaServiceUnknown): risks = append(risks, vo.SecurityRiskVo{Key: "dashboard.security.hysteriaStateUnknown", Severity: "warning", Dismissible: false}) default: risks = append(risks, vo.SecurityRiskVo{Key: "dashboard.security.hysteriaStopped", Severity: "critical", Dismissible: false}) } if !summary.Hysteria.ApiReachable { risks = append(risks, vo.SecurityRiskVo{Key: "dashboard.security.trafficApiUnavailable", Severity: "warning", Dismissible: false}) } return risks }