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
+93
View File
@@ -0,0 +1,93 @@
//go:build windows
package sysinfo
import (
"os"
"syscall"
"unsafe"
)
var (
kernel32 = syscall.NewLazyDLL("kernel32.dll")
procGetSystemTimes = kernel32.NewProc("GetSystemTimes")
procGlobalMemoryStatusEx = kernel32.NewProc("GlobalMemoryStatusEx")
procGetDiskFreeSpaceExW = kernel32.NewProc("GetDiskFreeSpaceExW")
)
type memoryStatusEx struct {
Length uint32
MemoryLoad uint32
TotalPhys uint64
AvailPhys uint64
TotalPageFile uint64
AvailPageFile uint64
TotalVirtual uint64
AvailVirtual uint64
AvailExtendedVirtual uint64
}
func readPlatform() (Stats, CPUSample) {
var (
stats Stats
sample CPUSample
)
var idle, kernel, user syscall.Filetime
if ret, _, _ := procGetSystemTimes.Call(
uintptr(unsafe.Pointer(&idle)),
uintptr(unsafe.Pointer(&kernel)),
uintptr(unsafe.Pointer(&user)),
); ret != 0 {
// kernel 时间包含 idle,因此总时间 = kernel + user
sample = CPUSample{
Total: filetimeTicks(kernel) + filetimeTicks(user),
Idle: filetimeTicks(idle),
}
}
var mem memoryStatusEx
mem.Length = uint32(unsafe.Sizeof(mem))
if ret, _, _ := procGlobalMemoryStatusEx.Call(uintptr(unsafe.Pointer(&mem))); ret != 0 {
stats.MemTotalBytes = mem.TotalPhys
stats.MemUsedBytes = mem.TotalPhys - mem.AvailPhys
}
stats.DiskTotalBytes, stats.DiskUsedBytes = diskUsage(volumeRoot())
return stats, sample
}
// volumeRoot 取当前工作目录所在盘符(Fonu 的数据目录与工作目录同盘),失败时回落到 C:\
func volumeRoot() string {
wd, err := os.Getwd()
if err != nil || len(wd) < 2 || wd[1] != ':' {
return `C:\`
}
return wd[:3]
}
func diskUsage(root string) (total, used uint64) {
rootPtr, err := syscall.UTF16PtrFromString(root)
if err != nil {
return 0, 0
}
var freeToCaller, totalBytes, totalFree uint64
ret, _, _ := procGetDiskFreeSpaceExW.Call(
uintptr(unsafe.Pointer(rootPtr)),
uintptr(unsafe.Pointer(&freeToCaller)),
uintptr(unsafe.Pointer(&totalBytes)),
uintptr(unsafe.Pointer(&totalFree)),
)
if ret == 0 || totalBytes == 0 {
return 0, 0
}
if totalFree > totalBytes {
return totalBytes, 0
}
return totalBytes, totalBytes - totalFree
}
// filetimeTicks 把 FILETIME(100ns 单位)折算为 10ms 刻度,避免累加时溢出
func filetimeTicks(ft syscall.Filetime) uint64 {
return (uint64(ft.HighDateTime)<<32 | uint64(ft.LowDateTime)) / 10000
}