Enhance /admin with full panel and subscription health checks

This commit is contained in:
tgvpn
2026-05-21 00:47:13 +03:00
parent 1fb512163b
commit f360d53614
7 changed files with 284 additions and 91 deletions
+168
View File
@@ -0,0 +1,168 @@
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()
}