fix(api): показывать подтверждённые метрики OSPF

This commit is contained in:
Denozordec committed 2026-09-30 19:49:47 +07:00
1 parent e3791f3496
commit af05ad5cda
13 files changed
+120 -55

No files matched your search

+20 -40
View File
@@ -46,13 +46,13 @@ interface OspfItem {
serverId: string
interfaceName: string
area: string
cost: number
active: boolean
cost: number | null
active: boolean | null
dataUnknown?: boolean
type?: string
useBfd?: boolean
helloInterval?: number
deadInterval?: number
type?: string | null
useBfd?: boolean | null
helloInterval?: number | null
deadInterval?: number | null
}
interface OspfNeighbor {
@@ -75,7 +75,7 @@ interface OspfRoute {
id: string
destination: string
type: "O" | "O IA" | "O E1" | "O E2"
cost: number
cost: number | null
nextHop: string
via: string
serverId: string
@@ -106,31 +106,10 @@ interface BfdSession {
// ─── backend types ────────────────────────────────────────────────────────────
interface BackendNeighbor {
id: string; serverId: number; serverName: string; serverSite: string; serverCountry: string
address: string; routerId: string; instance: string; area: string; areaId: string
interface: string; state: string; uptime: string | null; stateChanges: number | null; priority: number | null
}
interface BackendInterface {
id: string; serverId: number; serverName: string; serverSite: string; serverCountry: string
instance: string; area: string; areaId: string; interface: string; cost: number
type: string; disabled: boolean; inactive: boolean; priority: number
helloInterval: number; deadInterval: number; useBfd: boolean
}
interface BackendBfdSession {
id: string; serverId: number; serverName: string; serverSite: string; serverCountry: string
localAddr: string; remoteAddr: string; interface: string; state: string; uptime: string | null
multihop: boolean; multiplier: number | null; txInterval: number | null; rxInterval: number | null; holdTime: number | null
packetsRx: number | null; packetsTx: number | null; stateChanges: number | null
}
interface BackendOspfRoute {
id: string; serverId: number; serverName: string; serverSite: string
destination: string; type: OspfRoute["type"]; cost: number; nextHop: string; via: string; area: string
}
type BackendNeighbor = OspfCatalog["neighbors"][number]
type BackendInterface = OspfCatalog["interfaces"][number]
type BackendBfdSession = OspfCatalog["bfdSessions"][number]
type BackendOspfRoute = OspfCatalog["routes"][number]
function isRefInterfaceName(name: string): boolean {
@@ -168,7 +147,7 @@ function backendToItem(b: BackendInterface): OspfItem {
interfaceName: b.interface,
area: b.areaId,
cost: b.cost,
active: !b.disabled && !b.inactive,
active: b.disabled === true || b.inactive === true ? false : b.disabled === false && b.inactive === false ? true : null,
type: b.type,
useBfd: b.useBfd,
helloInterval: b.helloInterval,
@@ -637,7 +616,7 @@ function NodeDetailPanel({
<span className="text-[10px] font-mono text-[#475569] shrink-0">
{iface.area === "0.0.0.0" ? "backbone" : `area ${iface.area}`}
</span>
<span className="text-[10px] font-mono shrink-0" style={{ color: "#0ea5e9" }}>cost {iface.cost}</span>
<span className="text-[10px] font-mono shrink-0" style={{ color: "#0ea5e9" }}>cost {iface.cost ?? "—"}</span>
</div>
))}
</div>
@@ -696,7 +675,7 @@ function InterfacesTab({
return Object.values(byRouter).map(r => ({
...r,
areas: Object.entries(r.areas)
.map(([area, areaItems]) => ({ area, items: [...areaItems].sort((a, b) => a.cost - b.cost) }))
.map(([area, areaItems]) => ({ area, items: [...areaItems].sort((a, b) => (a.cost ?? Infinity) - (b.cost ?? Infinity)) }))
.sort((a, b) => a.area.localeCompare(b.area)),
}))
}, [items])
@@ -765,7 +744,7 @@ function InterfacesTab({
function onDrop(routerKey: string, area: string, targetKey: string) {
if (!dragging || dragging === targetKey) { setDragging(null); setDragOver(null); return }
setItems(prev => {
const areaItems = prev.filter(i => i.routerKey === routerKey && i.area === area).sort((a, b) => a.cost - b.cost)
const areaItems = prev.filter(i => i.routerKey === routerKey && i.area === area).sort((a, b) => (a.cost ?? Infinity) - (b.cost ?? Infinity))
const fromIdx = areaItems.findIndex(i => i.key === dragging)
const toIdx = areaItems.findIndex(i => i.key === targetKey)
if (fromIdx < 0 || toIdx < 0) return prev
@@ -785,6 +764,7 @@ function InterfacesTab({
return (
<div className="flex flex-col gap-4">
{isLive && filterServerId !== ALL_SERVERS_ID && !dataReady && <p role="status" className="text-xs text-muted-foreground">Оптимизация недоступна до полного успешного опроса и подтверждения costs/состояний интерфейсов с правом записи.</p>}
{operationError && <Alert variant="warning"><AlertCircleIcon /><AlertTitle>Оптимизация OSPF не подтверждена</AlertTitle><AlertDescription>{operationError}</AlertDescription></Alert>}
<div className="flex items-center gap-2 flex-wrap">
<Button variant="outline" size="sm" disabled={optimizing} onClick={() => setItems(initialItems)}>
@@ -856,7 +836,7 @@ function InterfacesTab({
<GripVerticalIcon className={cn("size-3.5 shrink-0", isLive ? "text-muted-foreground/10" : "text-muted-foreground/30")} />
<span className={cn("inline-block size-1.5 rounded-full shrink-0", !item.dataUnknown && item.active ? "bg-[var(--status-online)]" : "bg-muted-foreground/40")} />
<code className="text-xs font-mono flex-1 min-w-0 truncate">{item.interfaceName}</code>
{item.dataUnknown && <span className="text-xs text-muted-foreground">Неизвестно</span>}
{(item.dataUnknown || item.active === null) && <span className="text-xs text-muted-foreground">Неизвестно</span>}
{item.type && item.type !== "broadcast" && (
<Chip color="bg-violet-500/10 text-violet-600 dark:text-violet-400 border-violet-500/20">{item.type}</Chip>
)}
@@ -869,7 +849,7 @@ function InterfacesTab({
: "bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20"
}>opt {hint.optimalCost}</Chip>
)}
<Chip color="bg-sky-500/10 text-sky-600 dark:text-sky-400 border-sky-500/20">cur {item.cost}</Chip>
<Chip color="bg-sky-500/10 text-sky-600 dark:text-sky-400 border-sky-500/20">cur {item.cost ?? "—"}</Chip>
{liveOptimal !== undefined && !item.dataUnknown && <Chip color={costDiffers
? "bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20"
: "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20"
@@ -1193,7 +1173,7 @@ function OspfPageContent() {
const unknownIds = useMemo(() => new Set(liveError ? liveServers.map(s => s.id) : (liveData?.failures ?? []).map(f => f.serverId)), [liveError, liveServers, liveData])
const incomplete = isLive && (!liveData || Boolean(liveError) || (filterServerId === ALL_SERVERS_ID ? unknownIds.size > 0 : unknownIds.has(Number(filterServerId))))
const dataReady = !isLive || Boolean(liveData && !liveError && !loading && !writeBusy && !liveData.failures.length && liveServers.some(s => String(s.id) === filterServerId) && can("mm:servers:write"))
const dataReady = !isLive || Boolean(liveData && !liveError && !loading && !writeBusy && !liveData.failures.length && liveServers.some(s => String(s.id) === filterServerId) && can("mm:servers:write") && liveData.interfaces.filter(row => String(row.serverId) === filterServerId).every(row => row.cost !== null && row.cost >= 1 && row.disabled !== null && row.inactive !== null))
// Derive frontend types from backend data or use mocks
const { items, neighbors, graphNodes, graphEdges, routerIds, bfdSessions, routes } = useMemo(() => {
@@ -1211,7 +1191,7 @@ function OspfPageContent() {
// Build routerIds from instances
const routerIds: Record<string, string> = {}
for (const inst of liveData.instances) {
if (!routerIds[String(inst.serverId)]) {
if (inst.routerId && !routerIds[String(inst.serverId)]) {
routerIds[String(inst.serverId)] = inst.routerId
}
}