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:
2026-09-23 19:10:43 +08:00
parent 1b30be45a0
commit 8f6fac802f
14 changed files with 1168 additions and 124 deletions
+78 -2
View File
@@ -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