Enhance /admin with full panel and subscription health checks
This commit is contained in:
@@ -17,14 +17,6 @@ type Client struct {
|
||||
http *http.Client
|
||||
}
|
||||
|
||||
type PanelStatus struct {
|
||||
OK bool
|
||||
StatusCode int
|
||||
Users int
|
||||
Nodes int
|
||||
Detail string
|
||||
}
|
||||
|
||||
func NewClient(baseURL, apiToken, caddyToken string) *Client {
|
||||
return &Client{
|
||||
baseURL: strings.TrimRight(baseURL, "/"),
|
||||
@@ -36,38 +28,6 @@ func NewClient(baseURL, apiToken, caddyToken string) *Client {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Check(ctx context.Context) (PanelStatus, error) {
|
||||
st := PanelStatus{}
|
||||
|
||||
resp, body, err := c.get(ctx, "/api/system/stats/recap")
|
||||
if err != nil {
|
||||
return st, err
|
||||
}
|
||||
st.StatusCode = resp.StatusCode
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
st.OK = true
|
||||
st.Detail = "API панели отвечает"
|
||||
case http.StatusUnauthorized, http.StatusForbidden:
|
||||
return st, fmt.Errorf("доступ запрещён (HTTP %d): проверьте REMNAWAVE_API_TOKEN", resp.StatusCode)
|
||||
default:
|
||||
return st, fmt.Errorf("панель вернула HTTP %d: %s", resp.StatusCode, trimBody(body, 200))
|
||||
}
|
||||
|
||||
users, err := c.countFromEndpoint(ctx, "/api/users", "users")
|
||||
if err == nil {
|
||||
st.Users = users
|
||||
}
|
||||
|
||||
nodes, err := c.countFromEndpoint(ctx, "/api/nodes", "nodes")
|
||||
if err == nil {
|
||||
st.Nodes = nodes
|
||||
}
|
||||
|
||||
return st, nil
|
||||
}
|
||||
|
||||
func (c *Client) countFromEndpoint(ctx context.Context, path, arrayKey string) (int, error) {
|
||||
resp, body, err := c.get(ctx, path)
|
||||
if err != nil {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user