feat: 自动备份与保留策略、健康持续化与状态页、指标历史与趋势图,并修隧道判定误报与仪表盘布局(v1.2.11)

- 自动备份:每日一份(按最新备份是否在 24h 内判定,重启不重复)、保留份数可配、列表/下载/恢复/删除,恢复前先校验归档;顺手修 writeArchive 逐个检查 tar/gzip/文件 Close()(审查报告 §二)

- 健康持续化:只存状态变更(up/down/unknown)并推导可用率与不可用区间;公开状态页默认关闭、访问码存 SHA-256、按 IP 限流、只暴露域名与可用率

- 隧道判定修复:RouteHealthCandidate 新增 tunnel_known,frps 管理接口不可用时不再把「判不出来」当成「不通」(此前会误报「隧道连续不通」);汇总与徽标同样区分「无法判定」

- 指标历史:metrics_samples 纵向点表(本机 CPU/内存/磁盘 + 每规则上下行,30 天保留、查询按桶平均、不做降采样表);仪表盘趋势卡支持 1h/24h/7d

- 仪表盘布局:运行状态并入顶部统计区(运行时长补进「服务」卡);性能并入系统资源那一行,整行 4 列

- 测试:backup/metrics/frp/api/app 新增用例;gofmt、go build/vet/test、vue-tsc、vite build 全绿

- agent 无改动,AgentVersion 保持 0.12.2
This commit is contained in:
2026-09-25 13:05:58 +08:00
parent c8264bf280
commit 74d731d0ac
46 changed files with 2967 additions and 175 deletions
+34
View File
@@ -349,6 +349,18 @@ export const api = {
request<import('./types').FrpAgentRouteHealth[]>(`/api/frpmulti/servers/${serverId}/agent/routes/health`),
getFrpRouteHealthSummary: () =>
request<import('./types').FrpRouteHealthSummary>('/api/frpmulti/routes/summary'),
getFrpRouteUptime: (hours = 24) =>
request<{ hours: number; items: import('./types').FrpRouteUptime[] }>(`/api/frpmulti/routes/uptime?hours=${hours}`),
// 指标历史:窗口内的序列已按桶取平均,拿到就能直接画
getMetricsHistory: (hours = 24, buckets = 48) =>
request<import('./types').MetricsHistory>(`/api/metrics/history?hours=${hours}&buckets=${buckets}`),
// 匿名状态页:未开启或访问码不对时后端回 404
getStatusPage: (code?: string) =>
request<import('./types').StatusPagePayload>(
`/api/status-page${code ? `?code=${encodeURIComponent(code)}` : ''}`,
),
removeFrpAgentRoute: (serverId: number, proxyId: number) => request<{ ok: boolean }>(`/api/frpmulti/servers/${serverId}/agent/routes/${proxyId}`, { method: 'DELETE' }),
pushFrpConfigToAgent: (id: number) => request<{ ok: boolean }>(`/api/frpmulti/servers/${id}/agent/frps-config`, { method: 'POST' }),
frpsServerAction: (id: number, action: 'start' | 'stop' | 'restart') =>
@@ -399,6 +411,28 @@ export const api = {
URL.revokeObjectURL(url)
},
listBackupArchives: () =>
request<{ archives: import('./types').BackupArchive[]; keep: number }>('/api/backup/archives'),
createBackupArchive: () =>
request<{ name: string; removed: number }>('/api/backup/archives', { method: 'POST' }),
restoreBackupArchive: (name: string) =>
request<{ message: string }>(`/api/backup/archives/${encodeURIComponent(name)}/restore`, { method: 'POST' }),
deleteBackupArchive: (name: string) =>
request<{ ok: boolean }>(`/api/backup/archives/${encodeURIComponent(name)}`, { method: 'DELETE' }),
async downloadBackupArchive(name: string) {
const response = await fetch(`/api/backup/archives/${encodeURIComponent(name)}`, { credentials: 'include' })
if (!response.ok) {
throw new Error('下载备份失败')
}
const blob = await response.blob()
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = name
link.click()
URL.revokeObjectURL(url)
},
async restoreBackup(file: File) {
const form = new FormData()
form.append('file', file)
+69
View File
@@ -720,6 +720,8 @@ export interface FrpAgentRouteHealth {
cert_ok: boolean
cert_detail?: string
tunnel_ok: boolean
/** 隧道状态是否判得出来:frps 管理接口不可用时 tunnel_ok 恒为 false,但那不代表隧道不通 */
tunnel_known?: boolean
tunnel_name?: string
tunnel_detail?: string
service_ok: boolean
@@ -732,6 +734,8 @@ export interface FrpRouteHealthSummary {
dns_failed: number
cert_missing: number
tunnel_down: number
/** frps 管理接口不可用、隧道状态判不出来的条数 */
tunnel_unknown?: number
service_down: number
failed_servers?: string[]
}
@@ -940,3 +944,68 @@ export interface ApiToken {
export interface CreatedApiToken extends ApiToken {
token: string
}
// 留在服务器上的备份(自动备份 + 手动「立即备份」)
export interface BackupArchive {
name: string
size: number
mod_time: string
}
// 规则可用率:由后台巡检落库的状态变更推导,不是页面现算
// (不可用区间与判不出来的时长分开给,unknown 不计入可用率分母)
export interface FrpRouteUptimeIncident {
from: string
to?: string
reason?: string
detail?: string
minutes: number
}
export interface FrpRouteUptime {
server_id: number
domain: string
state: string
uptime_pct: number
down_minutes: number
unknown_minutes: number
covered_minutes: number
incidents: FrpRouteUptimeIncident[]
}
// 指标历史:scope=host 是本机,scope=route:<serverID>:<domain> 是单条规则的流量
export interface MetricsPoint {
at: string
value: number
}
export interface MetricsSeries {
scope: string
metric: string
points: MetricsPoint[]
}
export interface MetricsHistory {
hours: number
buckets: number
series: MetricsSeries[]
}
// 公开状态页(匿名只读):只有域名与可用率,不含内网地址/端口/证书路径
export interface StatusPageRoute {
domain: string
state: string
uptime_pct: number
down_minutes: number
unknown_minutes: number
}
export interface StatusPagePayload {
title: string
code_required: boolean
checked_at: string
up: number
down: number
unknown: number
routes: StatusPageRoute[]
}