package remnawave import ( "context" "fmt" "net/http" "strings" ) type CheckItem struct { Name string OK bool Status int Detail string } type HealthReport struct { PanelName string PanelURL string Checks []CheckItem Users int Nodes int AllOK bool } func (c *Client) FullCheck(ctx context.Context, subscriptionURL string) HealthReport { report := HealthReport{ PanelName: "", PanelURL: c.baseURL, } probes := []struct { name string path string }{ {"Панель (веб)", "/"}, {"API (статистика)", "/api/system/stats/recap"}, {"API (пользователи)", "/api/users"}, {"API (ноды)", "/api/nodes"}, {"Подписка (настройки)", "/api/subscription-settings"}, {"Подписка (API список)", "/api/subscriptions"}, } for _, p := range probes { item := c.probe(ctx, p.name, p.path) report.Checks = append(report.Checks, item) if p.path == "/api/users" && item.OK { if n, err := c.countFromEndpoint(ctx, "/api/users", "users"); err == nil { report.Users = n } } if p.path == "/api/nodes" && item.OK { if n, err := c.countFromEndpoint(ctx, "/api/nodes", "nodes"); err == nil { report.Nodes = n } } } subURL := strings.TrimRight(strings.TrimSpace(subscriptionURL), "/") if subURL != "" { report.Checks = append(report.Checks, c.probePublic(ctx, "Страница подписки", subURL)) } else { report.Checks = append(report.Checks, CheckItem{ Name: "Страница подписки", OK: false, Status: 0, Detail: "не задана (REMNAWAVE_SUBSCRIPTION_URL)", }) } report.AllOK = true for _, ch := range report.Checks { if !ch.OK { report.AllOK = false break } } return report } func (c *Client) probe(ctx context.Context, name, path string) CheckItem { item := CheckItem{Name: name} resp, body, err := c.get(ctx, path) if err != nil { item.Detail = err.Error() return item } item.Status = resp.StatusCode switch resp.StatusCode { case http.StatusOK: item.OK = true item.Detail = "OK" case http.StatusUnauthorized, http.StatusForbidden: item.Detail = fmt.Sprintf("HTTP %d — неверный токен или нет прав", resp.StatusCode) default: item.Detail = fmt.Sprintf("HTTP %d: %s", resp.StatusCode, trimBody(body, 120)) } return item } func (c *Client) probePublic(ctx context.Context, name, url string) CheckItem { item := CheckItem{Name: name} req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { item.Detail = err.Error() return item } req.Header.Set("Accept", "text/html,application/json,*/*") if c.caddyToken != "" { req.Header.Set("X-Api-Key", c.caddyToken) } resp, err := c.http.Do(req) if err != nil { item.Detail = fmt.Sprintf("нет связи: %v", err) return item } defer resp.Body.Close() item.Status = resp.StatusCode if resp.StatusCode >= 200 && resp.StatusCode < 400 { item.OK = true item.Detail = fmt.Sprintf("OK (HTTP %d)", resp.StatusCode) } else { item.Detail = fmt.Sprintf("HTTP %d", resp.StatusCode) } return item } func FormatReport(r HealthReport, panelName, panelURL string) string { var b strings.Builder if panelName != "" { b.WriteString(fmt.Sprintf("Панель: *%s*\nURL: `%s`\n\n", panelName, panelURL)) } icon := func(ok bool) string { if ok { return "✅" } return "❌" } for _, ch := range r.Checks { line := fmt.Sprintf("%s *%s*", icon(ch.OK), ch.Name) if ch.Status > 0 { line += fmt.Sprintf(" — HTTP %d", ch.Status) } if ch.Detail != "" { line += ": " + ch.Detail } b.WriteString(line + "\n") } if r.Users > 0 || r.Nodes > 0 { b.WriteString(fmt.Sprintf("\n👥 Пользователей: %d\n📡 Нод: %d", r.Users, r.Nodes)) } if r.AllOK { b.WriteString("\n\n✅ *Все проверки пройдены*") } else { b.WriteString("\n\n⚠️ *Есть ошибки* — проверьте токен, URL и страницу подписки") } return b.String() }