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
+63 -30
View File
@@ -8,6 +8,7 @@ import (
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/fonu/fonu/internal/acme"
@@ -17,6 +18,7 @@ import (
"github.com/fonu/fonu/internal/logstore"
"github.com/fonu/fonu/internal/publicip"
"github.com/fonu/fonu/internal/service"
"github.com/fonu/fonu/internal/sysinfo"
)
type StatusHandler struct {
@@ -26,9 +28,13 @@ type StatusHandler struct {
acmeSvc *acme.Service
startedAt string
nginxPIDFile string
cpuMu sync.Mutex
cpuSample sysinfo.CPUSample
}
func NewStatusHandler(cfg config.Config, proxySvc *service.ProxyService, ddnsSvc *ddns.Service, acmeSvc *acme.Service, startedAt, nginxPIDFile string) *StatusHandler {
// 先采一次 CPU 计数器,使首次请求就能算出占用率(否则只能返回 0)
_, sample := sysinfo.Read(sysinfo.CPUSample{})
return &StatusHandler{
cfg: cfg,
proxySvc: proxySvc,
@@ -36,31 +42,37 @@ func NewStatusHandler(cfg config.Config, proxySvc *service.ProxyService, ddnsSvc
acmeSvc: acmeSvc,
startedAt: startedAt,
nginxPIDFile: nginxPIDFile,
cpuSample: sample,
}
}
type statusResponse struct {
PublicIPv4 string `json:"public_ipv4"`
PublicIPv6 string `json:"public_ipv6"`
PublicIPSource string `json:"public_ip_source"`
DDNSStatus string `json:"ddns_status"`
DDNSCount int `json:"ddns_count"`
DDNSLastUpdated string `json:"ddns_last_updated,omitempty"`
CertificateStatus string `json:"certificate_status"`
CertificateDays int `json:"certificate_days"`
CertificateCount int `json:"certificate_count"`
CertificateSummary string `json:"certificate_summary,omitempty"`
ProxyCount int `json:"proxy_count"`
NginxStatus string `json:"nginx_status"`
PublicIPv4 string `json:"public_ipv4"`
PublicIPv6 string `json:"public_ipv6"`
PublicIPSource string `json:"public_ip_source"`
DDNSStatus string `json:"ddns_status"`
DDNSCount int `json:"ddns_count"`
DDNSLastUpdated string `json:"ddns_last_updated,omitempty"`
CertificateStatus string `json:"certificate_status"`
CertificateDays int `json:"certificate_days"`
CertificateCount int `json:"certificate_count"`
CertificateSummary string `json:"certificate_summary,omitempty"`
ProxyCount int `json:"proxy_count"`
NginxStatus string `json:"nginx_status"`
RequestToday int `json:"request_today"`
RequestYesterday int `json:"request_yesterday"`
RequestTrend *int `json:"request_trend,omitempty"`
RequestsHourly []int `json:"requests_hourly"`
RequestsHourlyLabels []string `json:"requests_hourly_labels"`
ErrorToday int `json:"error_today"`
AvgResponseMs float64 `json:"avg_response_ms"`
StartedAt string `json:"started_at"`
UptimeSeconds int64 `json:"uptime_seconds"`
AvgResponseMs float64 `json:"avg_response_ms"`
CPUPercent float64 `json:"cpu_percent"`
MemUsedBytes uint64 `json:"mem_used_bytes"`
MemTotalBytes uint64 `json:"mem_total_bytes"`
DiskUsedBytes uint64 `json:"disk_used_bytes"`
DiskTotalBytes uint64 `json:"disk_total_bytes"`
StartedAt string `json:"started_at"`
UptimeSeconds int64 `json:"uptime_seconds"`
}
func (h *StatusHandler) Get(w http.ResponseWriter, r *http.Request) {
@@ -83,6 +95,9 @@ func (h *StatusHandler) Get(w http.ResponseWriter, r *http.Request) {
records, _ := h.acmeSvc.List(r.Context())
certStatus, certDays, certCount, certSummary := summarizeCertificates(records)
resources, cpuSample := sysinfo.Read(h.currentCPUSample())
h.setCPUSample(cpuSample)
accessLog := filepath.Join(h.cfg.LogsDir(), "access.log")
total, errors, avgMs := logstore.CountTodayAccess(accessLog)
yesterday, _, _ := logstore.CountAccessForDay(accessLog, time.Now().AddDate(0, 0, -1))
@@ -92,30 +107,48 @@ func (h *StatusHandler) Get(w http.ResponseWriter, r *http.Request) {
uptime := time.Since(started).Seconds()
writeJSON(w, http.StatusOK, statusResponse{
PublicIPv4: ipv4,
PublicIPv6: ipv6,
PublicIPSource: ipSource,
DDNSStatus: ddnsStatus,
DDNSCount: ddnsCount,
DDNSLastUpdated: ddnsLastUpdated,
CertificateStatus: certStatus,
CertificateDays: certDays,
CertificateCount: certCount,
CertificateSummary: certSummary,
ProxyCount: len(rules),
NginxStatus: nginxStatus,
PublicIPv4: ipv4,
PublicIPv6: ipv6,
PublicIPSource: ipSource,
DDNSStatus: ddnsStatus,
DDNSCount: ddnsCount,
DDNSLastUpdated: ddnsLastUpdated,
CertificateStatus: certStatus,
CertificateDays: certDays,
CertificateCount: certCount,
CertificateSummary: certSummary,
ProxyCount: len(rules),
NginxStatus: nginxStatus,
RequestToday: total,
RequestYesterday: yesterday,
RequestTrend: requestTrendPercent(total, yesterday),
RequestsHourly: hourly,
RequestsHourlyLabels: hourlyLabels,
ErrorToday: errors,
AvgResponseMs: avgMs,
StartedAt: h.startedAt,
UptimeSeconds: int64(uptime),
AvgResponseMs: avgMs,
CPUPercent: resources.CPUPercent,
MemUsedBytes: resources.MemUsedBytes,
MemTotalBytes: resources.MemTotalBytes,
DiskUsedBytes: resources.DiskUsedBytes,
DiskTotalBytes: resources.DiskTotalBytes,
StartedAt: h.startedAt,
UptimeSeconds: int64(uptime),
})
}
// currentCPUSample 与 setCPUSample 用锁保护上次采样,/api/status 可能被并发调用
func (h *StatusHandler) currentCPUSample() sysinfo.CPUSample {
h.cpuMu.Lock()
defer h.cpuMu.Unlock()
return h.cpuSample
}
func (h *StatusHandler) setCPUSample(sample sysinfo.CPUSample) {
h.cpuMu.Lock()
defer h.cpuMu.Unlock()
h.cpuSample = sample
}
func (h *StatusHandler) resolvePublicIPs(ctx context.Context) (ipv4, ipv6, source string) {
if ipv4, ipv6, ok := h.ddnsSvc.PublicIPs(ctx); ok {
return ipv4, ipv6, "ddns"