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:
@@ -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)
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user