feat(web): 仪表盘补系统资源/中国 IP 段/通知通道与公网穿透卡,修复 frpc 版本读取
仪表盘:修「服务」卡写死的「正常」——改为按 Fonu/Nginx/DDNS/证书四项实际状态聚合,异常时点名并降级徽标 仪表盘:新增「性能」面板(今日异常/平均响应,/api/status 早已返回但从未展示)与「流量 Top 5」(复用已有逐规则流量轮询);/api/status 每 15 秒刷新并在页面隐藏时暂停(连同流量轮询) 仪表盘:新增「公网穿透」统计卡;运行状态面板增加公网反代行,数据来自新增的 GET /api/frpmulti/routes/summary(逐台服务端聚合,单台失败不中断) 仪表盘:新增一行三卡「系统资源 / 中国 IP 段 / 通知通道」;系统资源为新增跨平台采集 internal/sysinfo(Linux /proc、Windows kernel32,无新依赖) FRP:修复 frpc 版本读不出来——RuntimeStatus.Version 带 omitempty,--version 探测失败时字段被省略,前端只能显示未检测;现回落按 <FrpDir>/bin/<版本>/ 目录名推导(与「FRP 应用配置」弹窗的 active_version 同源),并逐台服务端探测版本 清理:删除 DashboardView 死样式(.dashboard/.stat-highlight/.proxy-metric*)
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
package frp
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSummarizeRouteHealth(t *testing.T) {
|
||||
items := []RouteHealthCandidate{
|
||||
{DNSOK: true, CertOK: true, TunnelOK: true, ServiceOK: true},
|
||||
{DNSOK: true, CertOK: false, TunnelOK: true, ServiceOK: false},
|
||||
{DNSOK: false, CertOK: true, TunnelOK: false, ServiceOK: true},
|
||||
}
|
||||
summary := summarizeRouteHealth(2, []string{"vps-b"}, items)
|
||||
|
||||
if summary.Servers != 2 || summary.Routes != 3 || summary.Healthy != 1 {
|
||||
t.Fatalf("unexpected summary counts: %+v", summary)
|
||||
}
|
||||
if summary.DNSFailed != 1 || summary.CertMissing != 1 || summary.TunnelDown != 1 || summary.ServiceDown != 1 {
|
||||
t.Fatalf("unexpected segment counts: %+v", summary)
|
||||
}
|
||||
if len(summary.FailedServers) != 1 || summary.FailedServers[0] != "vps-b" {
|
||||
t.Fatalf("unexpected failed servers: %+v", summary.FailedServers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSummarizeRouteHealthEmpty(t *testing.T) {
|
||||
summary := summarizeRouteHealth(0, nil, nil)
|
||||
if summary.Servers != 0 || summary.Routes != 0 || summary.Healthy != 0 {
|
||||
t.Fatalf("expected empty summary, got %+v", summary)
|
||||
}
|
||||
if len(summary.FailedServers) != 0 {
|
||||
t.Fatalf("expected no failed servers, got %+v", summary.FailedServers)
|
||||
}
|
||||
}
|
||||
+78
-2
@@ -836,6 +836,64 @@ func (s *Service) InstallAgentNginx(ctx context.Context, serverID int64) (map[st
|
||||
return client.InstallNginx(ctx)
|
||||
}
|
||||
|
||||
// RouteHealthSummary 公网反代部署健康的汇总(仪表盘用)。
|
||||
// 健康判定与规则页一致:四段(DNS/证书/隧道/内网服务)全通过才算健康;
|
||||
// 一个域名可同时命中多段失败,因此各段计数之和可能大于 Routes-Healthy。
|
||||
type RouteHealthSummary struct {
|
||||
Servers int `json:"servers"`
|
||||
Routes int `json:"routes"`
|
||||
Healthy int `json:"healthy"`
|
||||
DNSFailed int `json:"dns_failed"`
|
||||
CertMissing int `json:"cert_missing"`
|
||||
TunnelDown int `json:"tunnel_down"`
|
||||
ServiceDown int `json:"service_down"`
|
||||
FailedServers []string `json:"failed_servers,omitempty"`
|
||||
}
|
||||
|
||||
// SummarizeRouteHealth 逐台已启用服务端采集公网反代健康后汇总。
|
||||
// 单台服务端采集失败只记名并继续其它服务端,不阻断整轮汇总。
|
||||
func (s *Service) SummarizeRouteHealth(ctx context.Context) (RouteHealthSummary, error) {
|
||||
servers, err := s.store.ListEnabledServers(ctx)
|
||||
if err != nil {
|
||||
return RouteHealthSummary{}, err
|
||||
}
|
||||
var (
|
||||
items []RouteHealthCandidate
|
||||
failed []string
|
||||
)
|
||||
for _, server := range servers {
|
||||
candidates, err := s.RouteHealth(ctx, server.ID)
|
||||
if err != nil {
|
||||
failed = append(failed, server.Name)
|
||||
continue
|
||||
}
|
||||
items = append(items, candidates...)
|
||||
}
|
||||
return summarizeRouteHealth(len(servers), failed, items), nil
|
||||
}
|
||||
|
||||
func summarizeRouteHealth(servers int, failedServers []string, items []RouteHealthCandidate) RouteHealthSummary {
|
||||
summary := RouteHealthSummary{Servers: servers, Routes: len(items), FailedServers: failedServers}
|
||||
for _, item := range items {
|
||||
if !item.DNSOK {
|
||||
summary.DNSFailed++
|
||||
}
|
||||
if !item.CertOK {
|
||||
summary.CertMissing++
|
||||
}
|
||||
if !item.TunnelOK {
|
||||
summary.TunnelDown++
|
||||
}
|
||||
if !item.ServiceOK {
|
||||
summary.ServiceDown++
|
||||
}
|
||||
if item.DNSOK && item.CertOK && item.TunnelOK && item.ServiceOK {
|
||||
summary.Healthy++
|
||||
}
|
||||
}
|
||||
return summary
|
||||
}
|
||||
|
||||
// RouteHealthCandidate 公网反代规则的四段健康状态(DNS/证书/隧道/内网服务)。
|
||||
type RouteHealthCandidate struct {
|
||||
ProxyID int64 `json:"proxy_id"`
|
||||
@@ -1464,11 +1522,14 @@ func (s *Service) Status(ctx context.Context) RuntimeStatus {
|
||||
s.manager.RefreshVersion(versionCtx)
|
||||
servers, err := s.store.ListEnabledServers(ctx)
|
||||
if err != nil {
|
||||
return s.manager.Status()
|
||||
return s.statusWithVersion(s.manager)
|
||||
}
|
||||
statuses := make([]RuntimeStatus, 0, len(servers))
|
||||
for _, server := range servers {
|
||||
current := s.managerForServer(server.ID).Status()
|
||||
manager := s.managerForServer(server.ID)
|
||||
// 每个服务端各自探测版本(共用同一个 2 秒超时上下文)
|
||||
manager.RefreshVersion(versionCtx)
|
||||
current := s.statusWithVersion(manager)
|
||||
current.Configured = s.serverConfigured(server)
|
||||
if proxies, proxyErr := s.store.ListEnabledProxies(ctx, server.ID); proxyErr == nil {
|
||||
current.EnabledProxies = len(proxies)
|
||||
@@ -1478,6 +1539,21 @@ func (s *Service) Status(ctx context.Context) RuntimeStatus {
|
||||
return latestRuntimeStatus(statuses)
|
||||
}
|
||||
|
||||
// statusWithVersion 取服务端运行状态,并在版本探测失败时按二进制目录名推导版本:
|
||||
// frpc 进程未运行或 --version 探测失败时,RuntimeStatus.Version 因带 omitempty 会被省略,
|
||||
// 前端(FRP 穿透页状态卡、仪表盘)就只能显示「未检测 / 未知」。
|
||||
// 这里回落到 <FrpDir>/bin/<版本>/ 的目录约定,与「FRP 应用配置」弹窗的 active_version 同源。
|
||||
func (s *Service) statusWithVersion(manager *Manager) RuntimeStatus {
|
||||
status := manager.Status()
|
||||
if status.Version != "" {
|
||||
return status
|
||||
}
|
||||
if bin, err := manager.BinaryPath(); err == nil {
|
||||
status.Version = versionFromBinaryPath(s.cfg.FrpDir(), bin)
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
func (s *Service) ReadLogs(limit int, serverID int64) ([]string, error) {
|
||||
if limit <= 0 || limit > 500 {
|
||||
limit = 200
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package frp
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestVersionFromBinaryPath(t *testing.T) {
|
||||
frpDir := filepath.Join("data", "frp")
|
||||
cases := []struct {
|
||||
name string
|
||||
path string
|
||||
want string
|
||||
}{
|
||||
{"带 v 前缀的版本目录", filepath.Join(frpDir, "bin", "v0.71.0", "frpc.exe"), "v0.71.0"},
|
||||
{"纯数字版本目录", filepath.Join(frpDir, "bin", "0.62.1", "frpc"), "0.62.1"},
|
||||
{"不在 bin/<版本>/ 下", filepath.Join(frpDir, "frpc.exe"), ""},
|
||||
{"不在 FrpDir 下", filepath.Join("other", "bin", "v1.0.0", "frpc"), ""},
|
||||
{"多一层子目录", filepath.Join(frpDir, "bin", "v1.0.0", "extra", "frpc"), ""},
|
||||
{"目录名不是版本", filepath.Join(frpDir, "bin", "latest", "frpc"), ""},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
if got := versionFromBinaryPath(frpDir, tc.path); got != tc.want {
|
||||
t.Fatalf("%s:versionFromBinaryPath(%q) = %q,期望 %q", tc.name, tc.path, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user