fix(ui): сохранить неизвестные данные и ошибки BGP на дашборде

This commit is contained in:
Denozordec committed 2026-10-03 22:55:15 +07:00
1 parent c343ce62c8
commit f0eaa94e02
11 files changed
+78 -24

No files matched your search

+15
View File
@@ -0,0 +1,15 @@
import assert from "node:assert/strict"
import {test} from "node:test"
import {parseDashboardBgp} from "./dashboard-bgp"
import {bgpSession} from "../tests/e2e/fixtures/bgp"
test("BGP global totals distinguish observed zero, unknown counters and unknown session state",()=>{
assert.deepEqual(parseDashboardBgp({sessions:[],failures:[]}),{prefixSum:0,establishedCount:0})
assert.deepEqual(parseDashboardBgp({sessions:[bgpSession()],failures:[]}),{prefixSum:321,establishedCount:1})
assert.deepEqual(parseDashboardBgp({sessions:[bgpSession(1,null)],failures:[]}),{prefixSum:null,establishedCount:1})
assert.deepEqual(parseDashboardBgp({sessions:[{...bgpSession(1,null),state:"Idle"}],failures:[]}),{prefixSum:0,establishedCount:0})
for(const state of ["Unknown","NotEstablished","custom-status"])assert.deepEqual(parseDashboardBgp({sessions:[{...bgpSession(),state}],failures:[]}),{prefixSum:null,establishedCount:null})
})
test("BGP rejects partial catalogs, malformed counters, duplicates and overflow before publication",()=>{
for(const raw of [{sessions:[]},{sessions:[{}],failures:[]},{sessions:[bgpSession(),bgpSession()],failures:[]},{sessions:[bgpSession(1,-1)],failures:[]},{sessions:[bgpSession(1,Number.MAX_SAFE_INTEGER),bgpSession(2,1)],failures:[]}])assert.throws(()=>parseDashboardBgp(raw))
assert.throws(()=>parseDashboardBgp({sessions:[bgpSession()],failures:[{serverId:2,serverName:"Недоступный узел",error:"timeout"}]}),/Недоступный узел: timeout/)
})
+16
View File
@@ -0,0 +1,16 @@
import {bgpCatalogSchema} from "@/lib/bgp/response"
export interface DashboardBgp {prefixSum:number|null;establishedCount:number|null}
/** A failed device cannot be counted as having no sessions in the global KPI. */
export function parseDashboardBgp(raw:unknown):DashboardBgp {
const catalog=bgpCatalogSchema.safeParse(raw)
if(!catalog.success)throw Error("Некорректный ответ каталога BGP")
if(catalog.data.failures.length)throw Error("Не опрошены устройства BGP: "+catalog.data.failures.map(f=>f.serverName+": "+f.error).join("; "))
let prefixSum=0,establishedCount=0,missingPrefix=false,unknownState=false
for(const session of catalog.data.sessions){
const state=session.state.toLowerCase()
if(state==="established"){establishedCount++;if(session.prefixesRx===null)missingPrefix=true;else prefixSum+=session.prefixesRx}
else if(!["active","idle","connect","opensent","openconfirm"].includes(state))unknownState=true
}
if(!Number.isSafeInteger(prefixSum))throw Error("Суммарный счётчик BGP превышает допустимый диапазон")
return {prefixSum:unknownState||missingPrefix?null:prefixSum,establishedCount:unknownState?null:establishedCount}
}
+1 -1
View File
@@ -3,6 +3,6 @@ import {test} from "node:test"
import {dashboardRequestPermission} from "./dashboard-permissions"
import {permissionForRequest} from "../backend/src/lib/permissions"
test("dashboard reader gates match real backend permissions including fallback diagnostic POST",()=>{
for(const [method,path] of [["GET","/api/uptime/overview?range=1h"],["GET","/api/servers"],["GET","/api/filters/rules"],["GET","/api/bgp/sessions"],["GET","/api/internet-path/latest"],["GET","/api/filters/gre-tunnels"],["GET","/api/wireguard"],["GET","/api/traffic/servers?range=1h"],["GET","/api/vxlan"],["GET","/api/events?limit=6"],["GET","/api/servers/1/wan-runtime"],["POST","/api/servers/1/probes/run"],["GET","/api/uptime/speed-probes"]])assert.equal(dashboardRequestPermission(method,path),permissionForRequest(method,path))
for(const [method,path] of [["GET","/api/uptime/overview?range=1h"],["GET","/api/servers"],["GET","/api/filters/rules"],["GET","/api/bgp/catalog"],["GET","/api/internet-path/latest"],["GET","/api/filters/gre-tunnels"],["GET","/api/wireguard"],["GET","/api/traffic/servers?range=1h"],["GET","/api/vxlan"],["GET","/api/events?limit=6"],["GET","/api/servers/1/wan-runtime"],["POST","/api/servers/1/probes/run"],["GET","/api/uptime/speed-probes"]])assert.equal(dashboardRequestPermission(method,path),permissionForRequest(method,path))
assert.throws(()=>dashboardRequestPermission("GET","/api/unmapped"));assert.throws(()=>dashboardRequestPermission("POST","/api/events"))
})