fix(ui): сохранять согласованные данные оптимизатора

This commit is contained in:
Denozordec committed 2026-09-30 22:38:11 +07:00
1 parent 59f0f82176
commit f93dfe34c2
7 files changed
+139 -53

No files matched your search

+1 -1
View File
@@ -127,7 +127,7 @@ export interface OptimizerApiServer {
site: string
country: string
enabled: boolean
status: "online" | "offline" | null
status: "online" | "offline" | "degraded" | null
latency: number | null
model: string | null
wanUplinks?: Array<{
+18
View File
@@ -0,0 +1,18 @@
import {test} from "node:test"
import assert from "node:assert/strict"
import {optimizerServersSchema,optimizerRulesSchema,optimizerProbesSchema} from "./route-optimizer-response"
const server={id:1,name:"Router",host:"192.0.2.1",type:"home-router",country:"RU",enabled:true,latency:0,status:null}
const probe={id:"p",srcServerId:"1",dstServerId:"2",srcInterface:"ether1",dstInterface:"ether2",enabled:true,lastPingRttMs:0,lastPingLossPct:0,lastTxAvgMbps:0,lastRxAvgMbps:null,lastPingAt:"2026-09-30T00:00:00Z"}
test("optimizer observed zero/null and empty sources are distinct from missing",()=>{
assert.equal(optimizerServersSchema.parse([server])[0].latency,0)
assert.equal(optimizerProbesSchema.parse({probes:[probe]}).probes[0].lastTxAvgMbps,0)
assert.equal(optimizerProbesSchema.parse({probes:[probe]}).probes[0].lastRxAvgMbps,null)
assert.ok(optimizerProbesSchema.safeParse({probes:[]}).success)
assert.ok(!optimizerProbesSchema.safeParse({}).success)
assert.ok(optimizerRulesSchema.safeParse({rulesets:[],live:false,stale:false}).success)
assert.ok(!optimizerRulesSchema.safeParse({rulesets:[],stale:true}).success)
})
test("optimizer refuses unknown types, duplicate inventory/WAN and invalid metrics",()=>{
for(const value of [[{...server,type:undefined}],[server,server],[{...server,latency:-1}],[{...server,wanUplinks:[{id:"wan",name:"WAN",isp:"ISP",iface:"ether1",ip:"",maxDl:0,maxUl:10}]}]]) assert.ok(!optimizerServersSchema.safeParse(value).success)
for(const value of [[probe,probe],[{...probe,lastPingRttMs:-1}],[{...probe,lastPingLossPct:101}],[{...probe,lastPingAt:"yesterday"}],[{...probe,lastTxAvgMbps:undefined}]]) assert.ok(!optimizerProbesSchema.safeParse({probes:value}).success)
})
+17
View File
@@ -0,0 +1,17 @@
import {z} from "zod"
import {filtersResponseSchema} from "./filters-response"
const unique = <T,>(rows:T[],key:(row:T)=>string|number)=>new Set(rows.map(key)).size===rows.length
const measured = z.number().finite().nonnegative().nullable()
export const optimizerServersSchema=z.array(z.object({
id:z.number().int().positive(),name:z.string(),host:z.string().min(1),type:z.enum(["home-router","jump-host","exit-node"]),
enabled:z.boolean(),country:z.string(),site:z.string().optional().default(""),
status:z.enum(["online","offline","degraded"]).nullable().optional().default(null),
latency:measured.optional().default(null),model:z.string().nullable().optional().default(null),
wanUplinks:z.array(z.object({id:z.string().min(1),name:z.string(),isp:z.string(),iface:z.string(),ip:z.string(),maxDl:z.number().finite().positive(),maxUl:z.number().finite().positive()})).refine(rows=>unique(rows,row=>row.id)).optional(),
})).refine(rows=>unique(rows,row=>row.id))
export const optimizerRulesSchema=filtersResponseSchema.refine(data=>data.stale!==true)
export const optimizerProbesSchema=z.object({probes:z.array(z.object({
id:z.string().min(1),srcServerId:z.string().min(1),dstServerId:z.string().min(1),srcInterface:z.string(),dstInterface:z.string(),enabled:z.boolean(),
lastPingRttMs:measured,lastPingLossPct:z.number().finite().min(0).max(100).nullable(),
lastTxAvgMbps:measured,lastRxAvgMbps:measured,lastPingAt:z.string().datetime({offset:true}).nullable(),
})).refine(rows=>unique(rows,row=>row.id))})