diff --git a/app/(main)/ospf/page.tsx b/app/(main)/ospf/page.tsx
index c9825098..1fa7eddf 100644
--- a/app/(main)/ospf/page.tsx
+++ b/app/(main)/ospf/page.tsx
@@ -2,7 +2,9 @@
import { buildOspfCostIndex, ospfCostKey } from "@/lib/ospf-cost"
import { normalizeOspfState, normalizeBfdState, ospfNodeState, type OspfNeighborState, type BfdState } from "@/lib/ospf-state"
-import { useMemo, useState, useEffect, useRef } from "react"
+import { useMemo, useState, useEffect, useRef, Suspense } from "react"
+import { useSearchParams } from "next/navigation"
+import { readOspfUrl, patchOspfUrl, type OspfTab } from "@/lib/ospf-url-state"
import { ospfCatalogSchema, ospfServersSchema, ospfOptimizeSchema, type OspfCatalog } from "@/lib/ospf-response"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { PageHeader } from "@/components/page-header"
@@ -30,7 +32,6 @@ import { readStoredRouteOptimizerSettings } from "@/lib/route-optimizer-data"
// ─── types ────────────────────────────────────────────────────────────────────
-type OspfTab = "interfaces" | "neighbors" | "routes" | "bfd"
interface OspfServerInfo {
id: string // String(serverId) in live mode, routerKey in mock
@@ -632,7 +633,6 @@ function InterfacesTab({
isLive,
filterServerId,
backendUrl,
- onLiveDataRefresh,
dataReady,
dataIncomplete,
onOperationBusy,
@@ -642,7 +642,6 @@ function InterfacesTab({
isLive: boolean
filterServerId: string
backendUrl: string
- onLiveDataRefresh: () => void
dataReady: boolean
dataIncomplete: boolean
onOperationConfirmed: (id: number) => void
@@ -720,24 +719,23 @@ function InterfacesTab({
body: JSON.stringify({ pingWeight: ra.pingWeight }),
},
)
- if (!mounted.current) return
const data = ospfOptimizeSchema.parse(response)
if (String(data.serverId) !== filterServerId) throw new Error("API вернул другой контекст OSPF")
const byKey: Record = {}
data.interfaces.forEach((row) => {
byKey[`${data.serverId}-${row.id}`] = row.optimalCost
})
- setLiveOptimalCost(byKey)
onOperationConfirmed(data.serverId)
+ if (!mounted.current) return
+ setLiveOptimalCost(byKey)
toast.success(`API подтвердил оптимизацию OSPF: ${data.optimizedCount} интерфейсов`)
- onLiveDataRefresh()
} catch (err) {
if (!mounted.current) return
setOperationError(`API не подтвердил завершение оптимизации: ${err instanceof Error ? err.message : String(err)}. Часть costs могла измениться; выполнен повторный опрос перед новой попыткой.`)
- onLiveDataRefresh()
} finally {
operationLock.current = false
- if (mounted.current) { setOptimizing(false); onOperationBusy(false) }
+ if (mounted.current) setOptimizing(false)
+ onOperationBusy(false)
}
}
@@ -1112,13 +1110,19 @@ const TABS: Array<{ id: OspfTab; label: string; icon: React.ReactNode }> = [
]
export default function OspfPage() {
- const { mode, backendUrl } = useDataSource()
- return
+ return Загрузка контекста OSPF…
}>
}
-function OspfPageContent() {
- const [activeTab, setActiveTab] = useState("interfaces")
- const [filterServerId, setFilterServerId] = useState(ALL_SERVERS_ID)
-
+function OspfRoute() {
+ const { mode, backendUrl } = useDataSource()
+ const params = useSearchParams()
+ const state = readOspfUrl(new URLSearchParams(params.toString()))
+ function navigate(values: Record) {
+ const href = patchOspfUrl(new URLSearchParams(window.location.search), values)
+ if (href !== window.location.pathname + window.location.search) window.history.pushState(null, "", href)
+ }
+ return navigate({ server: id === ALL_SERVERS_ID ? null : id })} onSelectTab={tab => navigate({ tab: tab === "interfaces" ? null : tab })} onResetContext={() => navigate({ server: null, tab: null })} />
+}
+function OspfPageContent({ filterServerId, activeTab, contextError, onSelectServer, onSelectTab, onResetContext }: { filterServerId: string; activeTab: OspfTab; contextError: string | null; onSelectServer: (id: string) => void; onSelectTab: (tab: OspfTab) => void; onResetContext: () => void }) {
const { mode, backendUrl } = useDataSource()
const isLive = mode === "live"
@@ -1130,15 +1134,12 @@ function OspfPageContent() {
const [fetchTick, setFetchTick] = useState(0)
const [readReceipts, setReadReceipts] = useState>({})
const [writeReceipt, setWriteReceipt] = useState<{ serverId: number; at: string } | null>(null)
+ const pageMounted = useRef(true)
+ useEffect(() => { pageMounted.current = true; return () => { pageMounted.current = false } }, [])
const operationBusy = useRef(false)
const [writeBusy, setWriteBusy] = useState(false)
function refresh() { if (operationBusy.current) return; setLoading(true); setFetchTick(t => t + 1) }
- // Reset server filter when switching live ↔ mock
- useEffect(() => {
- queueMicrotask(() => setFilterServerId("all"))
- }, [isLive])
-
useEffect(() => {
if (!isLive) {
queueMicrotask(() => setLiveData(null))
@@ -1172,8 +1173,8 @@ function OspfPageContent() {
}, [isLive, backendUrl, fetchTick])
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") && liveData.interfaces.filter(row => String(row.serverId) === filterServerId).every(row => row.cost !== null && row.cost >= 1 && row.disabled !== null && row.inactive !== null))
+ const incomplete = Boolean(contextError) || isLive && (!liveData || Boolean(liveError) || (filterServerId === ALL_SERVERS_ID ? unknownIds.size > 0 : unknownIds.has(Number(filterServerId))))
+ const dataReady = !contextError && (!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(() => {
@@ -1246,10 +1247,13 @@ function OspfPageContent() {
}, [items, neighbors, bfdSessions])
// ── filtered display data ─────────────────────────────────────────────────────
- const displayItems = filterServerId === ALL_SERVERS_ID ? items : items.filter(i => i.routerKey === filterServerId)
- const displayNeighbors = filterServerId === ALL_SERVERS_ID ? neighbors : neighbors.filter(n => n.localRouter === filterServerId)
- const displayBfdSessions = filterServerId === ALL_SERVERS_ID ? bfdSessions : bfdSessions.filter(b => b.serverId === filterServerId)
- const displayRoutes = filterServerId === ALL_SERVERS_ID ? routes : routes.filter(r => r.serverId === filterServerId)
+ const missingServer = filterServerId !== ALL_SERVERS_ID && (!isLive || Boolean(liveData)) && !ospfServers.some(server => server.id === filterServerId)
+ const unavailable = Boolean(contextError) || missingServer
+ const viewIncomplete = incomplete || unavailable
+ const displayItems = unavailable ? [] : filterServerId === ALL_SERVERS_ID ? items : items.filter(i => i.routerKey === filterServerId)
+ const displayNeighbors = unavailable ? [] : filterServerId === ALL_SERVERS_ID ? neighbors : neighbors.filter(n => n.localRouter === filterServerId)
+ const displayBfdSessions = unavailable ? [] : filterServerId === ALL_SERVERS_ID ? bfdSessions : bfdSessions.filter(b => b.serverId === filterServerId)
+ const displayRoutes = unavailable ? [] : filterServerId === ALL_SERVERS_ID ? routes : routes.filter(r => r.serverId === filterServerId)
const ospfRailItems = useMemo(() => (
ospfServers.map((s) => {
@@ -1280,7 +1284,7 @@ function OspfPageContent() {
preserveSelection
items={ospfRailItems}
selectedId={filterServerId}
- onSelect={id => { if (!operationBusy.current) setFilterServerId(id) }}
+ onSelect={id => { if (!operationBusy.current) onSelectServer(id) }}
showAll
allCount={ospfServers.length}
loading={isLive && loading && ospfServers.length === 0}
@@ -1302,7 +1306,7 @@ function OspfPageContent() {
{TABS.map(t => (
-