fix(ui): проверять контекст расчёта OSPF оптимизатора

This commit is contained in:
Denozordec committed 2026-09-30 22:47:04 +07:00
1 parent f93dfe34c2
commit bf2815b5a4
9 files changed
+131 -108

No files matched your search

+53 -99
View File
@@ -2,7 +2,8 @@
import { useCallback, useEffect, useState, useMemo, useRef } from "react"
import Link from "next/link"
import {optimizerServersSchema,optimizerRulesSchema,optimizerProbesSchema} from "@/lib/route-optimizer-response"
import {ospfCatalogSchema} from "@/lib/ospf-response"
import {optimizerServersSchema,optimizerRulesSchema,optimizerProbesSchema,optimizerOspfPreviewSchema,type OptimizerOspfPreview} from "@/lib/route-optimizer-response"
import { PageHeader } from "@/components/page-header"
import { RouteOptimizerWanMatrixDataGrid } from "@/components/data-grids/route-optimizer-wan-matrix-data-grid"
import { RouteOptimizerFullRoutesDataGrid } from "@/components/data-grids/route-optimizer-full-routes-data-grid"
@@ -451,30 +452,16 @@ function RouteOptimizerContent() {
const [ospfApplying, setOspfApplying] = useState(false)
const [ospfApplyResult, setOspfApplyResult] = useState<{ serverName: string; optimizedCount: number } | null>(null)
const [ospfApplyError, setOspfApplyError] = useState("")
const [ospfMeta, setOspfMeta] = useState<{ interfaces: number; areas: number; serverName: string } | null>(null)
const [ospfPreviewError, setOspfPreviewError] = useState("")
const [ospfPreview, setOspfPreview] = useState<{
changedCount: number
interfacesTotal: number
interfaces: Array<{
interface: string
currentCost: number
optimalCost: number
score: number
pingMs: number
dlMbps: number
ulMbps: number
}>
changes: Array<{
interface: string
currentCost: number
optimalCost: number
score: number
pingMs: number
dlMbps: number
ulMbps: number
}>
} | null>(null)
const [metaState,setMetaState]=useState<{owner:string;data:{interfaces:number;areas:number;serverName:string}|null;error:string}>({owner:"",data:null,error:""})
const [previewState,setPreviewState]=useState<{owner:string;data:OptimizerOspfPreview|null;error:string;pending:boolean}>({owner:"",data:null,error:"",pending:false})
const [previewTick,setPreviewTick]=useState(0)
const previewKey=JSON.stringify([ospfServerId,settings.pingWeight])
const ospfMeta=metaState.owner===ospfServerId?metaState.data:null
const ospfMetaError=metaState.owner===ospfServerId?metaState.error:""
const ospfPreview=previewState.owner===previewKey?previewState.data:null
const ospfPreviewError=previewState.owner===previewKey?previewState.error:""
const ospfPreviewLoading=useLiveData && Boolean(ospfServerId) && (previewState.owner!==previewKey || previewState.pending)
const previewReady=Boolean(ospfPreview) && !ospfPreviewLoading && !ospfPreviewError
const load = useCallback(async (override?: OptimizerSettings) => {
const epoch = ++reader.current.epoch
@@ -621,7 +608,7 @@ function RouteOptimizerContent() {
}
async function applyOspfOptimization() {
if (!ospfServerId || loading || error || !data) return
if (!ospfServerId || loading || error || !data || !previewReady) return
setOspfApplying(true)
setOspfApplyError("")
setOspfApplyResult(null)
@@ -638,7 +625,7 @@ function RouteOptimizerContent() {
optimizedCount: res.optimizedCount,
})
await load()
setOspfPreview((prev) => (prev ? { ...prev, changedCount: 0, changes: [] } : prev))
setPreviewTick(value=>value+1)
} catch (e) {
setOspfApplyError(e instanceof Error ? e.message : "Ошибка применения OSPF-оптимизации")
} finally {
@@ -649,74 +636,42 @@ function RouteOptimizerContent() {
const set = <K extends keyof OptimizerSettings>(k: K, v: OptimizerSettings[K]) =>
setSettings(prev => ({ ...prev, [k]: v }))
useEffect(() => {
if (!useLiveData || !ospfServerId) return
let cancelled = false
void apiFetch<{
interfaces: Array<{ areaId: string; interface: string }>
instances: Array<unknown>
neighbors: Array<unknown>
bfdSessions: Array<unknown>
}>(`/api/servers/${ospfServerId}/ospf`)
.then((data) => {
if (cancelled) return
const selected = liveServers.find((s) => String(s.id) === ospfServerId)
const visibleInterfaces = data.interfaces.filter((i) => !/^\(ref\s+\*.+\)$/.test(i.interface.trim()))
setOspfMeta({
interfaces: visibleInterfaces.length,
areas: new Set(visibleInterfaces.map((i) => i.areaId)).size,
serverName: selected?.name || selected?.host || ospfServerId,
})
})
.catch(() => {
if (cancelled) return
setOspfMeta(null)
})
return () => { cancelled = true }
}, [apiFetch, liveServers, ospfServerId, useLiveData])
useEffect(()=>{
if(!useLiveData||!ospfServerId)return
let cancelled=false
void apiFetch<unknown>("/api/ospf/catalog").then(raw=>{
if(cancelled)return
const parsed=ospfCatalogSchema.safeParse(raw)
if(!parsed.success)throw new Error("Недостоверный каталог OSPF")
const catalog=parsed.data
const failure=catalog.failures.find(item=>String(item.serverId)===ospfServerId)
if(failure)throw new Error(failure.error)
const interfaces=catalog.interfaces.filter(item=>String(item.serverId)===ospfServerId && !/^\(ref\s+\*.+\)$/.test(item.interface.trim()))
const selected=liveServers.find(item=>String(item.id)===ospfServerId)
setMetaState({owner:ospfServerId,data:{interfaces:interfaces.length,areas:new Set(interfaces.map(item=>item.areaId)).size,serverName:selected?.name||selected?.host||ospfServerId},error:""})
}).catch(error=>{
if(!cancelled)setMetaState(previous=>({owner:ospfServerId,data:previous.owner===ospfServerId?previous.data:null,error:error instanceof Error?error.message:"Ошибка каталога OSPF"}))
})
return()=>{cancelled=true}
},[apiFetch,liveServers,ospfServerId,useLiveData,previewTick])
useEffect(() => {
if (!useLiveData || !ospfServerId) return
let cancelled = false
void apiFetch<{
changedCount: number
interfacesTotal: number
interfaces: Array<{
interface: string
currentCost: number
optimalCost: number
score: number
pingMs: number
dlMbps: number
ulMbps: number
}>
changes: Array<{
interface: string
currentCost: number
optimalCost: number
score: number
pingMs: number
dlMbps: number
ulMbps: number
}>
}>(
`/api/servers/${ospfServerId}/ospf/optimize/preview`,
{ method: "POST", body: JSON.stringify({ pingWeight: settings.pingWeight }) },
)
.then((data) => {
if (cancelled) return
setOspfPreviewError("")
setOspfPreview(data)
useEffect(()=>{
if(!useLiveData||!ospfServerId)return
let cancelled=false
queueMicrotask(()=>{
if(cancelled)return
setPreviewState(previous=>({owner:previewKey,data:previous.owner===previewKey?previous.data:null,error:"",pending:true}))
void apiFetch<unknown>("/api/servers/"+ospfServerId+"/ospf/optimize/preview",{method:"POST",body:JSON.stringify({pingWeight:settings.pingWeight})}).then(raw=>{
if(cancelled)return
const preview=optimizerOspfPreviewSchema.safeParse(raw)
if(!preview.success||String(preview.data.serverId)!==ospfServerId||preview.data.pingWeight!==settings.pingWeight)throw new Error("Недостоверный preview OSPF или другой контекст")
setPreviewState({owner:previewKey,data:preview.data,error:"",pending:false})
}).catch(error=>{
if(!cancelled)setPreviewState(previous=>({owner:previewKey,data:previous.owner===previewKey?previous.data:null,error:error instanceof Error?error.message:"Ошибка preview OSPF",pending:false}))
})
.catch((e) => {
if (cancelled) return
setOspfPreview(null)
setOspfPreviewError(e instanceof Error ? e.message : "Ошибка preview OSPF")
})
return () => { cancelled = true }
}, [apiFetch, ospfServerId, settings.pingWeight, useLiveData])
const ospfPreviewLoading = useLiveData && Boolean(ospfServerId) && !ospfPreview && !ospfPreviewError
})
return()=>{cancelled=true}
},[apiFetch,ospfServerId,settings.pingWeight,useLiveData,previewKey,readAt,previewTick])
return (
<div className="flex flex-col h-full">
@@ -862,6 +817,7 @@ function RouteOptimizerContent() {
</FramePanel>
</Frame>
{ospfMetaError && <p role="alert" className="text-sm text-warning">Каталог OSPF: {ospfMetaError}. Актуальность неизвестна.</p>}
{/* OSPF optimization from Route Optimizer */}
<OpsPanel
title="OSPF"
@@ -892,13 +848,13 @@ function RouteOptimizerContent() {
))}
</select>
<span className="text-[11px] text-muted-foreground">
{ospfMeta ? `${ospfMeta.interfaces} iface · ${ospfMeta.areas} area` : "сбор OSPF-метрик…"}
{ospfMetaError ? "Каталог OSPF недостоверен" : ospfMeta ? `${ospfMeta.interfaces} iface · ${ospfMeta.areas} area` : "сбор OSPF-метрик…"}
</span>
<Button
size="sm"
className="ml-auto h-7 text-xs"
onClick={() => void applyOspfOptimization()}
disabled={loading || Boolean(error) || !data || !ospfServerId || ospfApplying}
disabled={loading || Boolean(error) || !data || !ospfServerId || ospfApplying || !previewReady || Boolean(ospfMetaError)}
>
<ZapIcon className={cn("size-3.5", ospfApplying && "animate-pulse")} />
{ospfApplying ? "Оптимизация…" : "Оптимизировать OSPF"}
@@ -917,6 +873,7 @@ function RouteOptimizerContent() {
<div className="rounded-lg border bg-background/70 overflow-hidden">
<div className="px-3 py-2 border-b text-[11px] text-muted-foreground flex items-center justify-between">
<span>Preview изменений OSPF cost (до применения)</span>
<Button type="button" variant="outline" size="sm" disabled={ospfApplying || ospfPreviewLoading} onClick={()=>setPreviewTick(value=>value+1)}>Обновить preview OSPF</Button>
<span>
{ospfPreviewLoading
? "расчёт…"
@@ -926,10 +883,7 @@ function RouteOptimizerContent() {
</span>
</div>
<RouteOptimizerOspfPreviewDataGrid
rows={(ospfPreview?.interfaces ?? []).map((row) => ({
id: `${row.interface}-${row.currentCost}-${row.optimalCost}`,
...row,
}))}
rows={ospfPreview?.interfaces ?? []}
error={ospfPreviewError || null}
loading={ospfPreviewLoading}
/>