"use client" import { Suspense, useState, useMemo, useEffect } from "react" import { useRouter, useSearchParams } from "next/navigation" import { PageHeader } from "@/components/page-header" import { DataPageToolbar } from "@/components/data-page-toolbar" import { BgpSessionsDataGrid } from "@/components/data-grids/bgp-sessions-data-grid" import { useBgpUrlState } from "@/hooks/use-bgp-url-state" import { QueryFilters } from "@/components/data-grids/query-filters" import { applyFilterQuery } from "@/lib/data-filters/apply-filter-query" import { BGP_FILTER_FIELDS } from "@/lib/data-filters/bgp-filter-fields" import type { BgpSessionRow, BgpState, BgpType } from "@/lib/bgp/types" import { bgpCatalogSchema, bgpServersSchema, type BackendBgpSession, type BgpFailure } from "@/lib/bgp/response" import { fmtBgpNum, sumBgpCounters } from "@/lib/bgp/helpers" import { BGP_AS_NAMES } from "@/lib/bgp/types" import { Frame, FramePanel } from "@/components/reui/frame" import { KpiStatGrid } from "@/components/reui-kit/kpi-stat-grid" import { OpsPanel } from "@/components/ops-panel" import { DataPageCard } from "@/components/data-page-card" import { Button } from "@/components/ui/button" import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" import { cn } from "@/lib/utils" import { RefreshCwIcon, DownloadIcon, ActivityIcon, BarChart3Icon, ArrowDownIcon, ServerIcon, AlertCircleIcon, GitMergeIcon, CheckCircleIcon, } from "lucide-react" import { useDataSource } from "@/lib/data-source" import { requestJson } from "@/shared/api/http-client" import { servers as mockServers, type Server } from "@/lib/data" import { ServerRailLayout, ServerRailMobileButton } from "@/components/server-rail-layout" import { ALL_SERVERS_ID, type ServerTileItem } from "@/components/server-tile-rail" // ─── types ──────────────────────────────────────────────────────────────────── type BgpSession = BgpSessionRow type BgpTab = "sessions" | "routers" | "analytics" const AS_NAMES = BGP_AS_NAMES // ─── mock data ──────────────────────────────────────────────────────────────── const SESSIONS: BgpSession[] = [ // ── srv1 (mt-msk-core-01) ──────────────────────────────────────────── { id: "s1", serverId: "srv1", serverLabel: "mt-msk-core-01", serverSite: "MSK", peerIp: "185.1.1.1", remoteAs: 8359, localAs: 65001, routerId: "185.1.1.1", description: "МТС — upstream transit", state: "Established", type: "eBGP", afi: "IPv4 Unicast", uptime: "14д 6ч 22м", holdTime: 90, keepalive: 30, prefixesRx: 280241, prefixesTx: 42, prefixesActive: 277894, inputMessages: 1842204, outputMessages: 14412, capabilities: ["4-byte-AS", "Route Refresh", "MP-BGP", "ADD-PATH"], lastError: null, }, { id: "s2", serverId: "srv1", serverLabel: "mt-msk-core-01", serverSite: "MSK", peerIp: "77.88.8.1", remoteAs: 13238, localAs: 65001, routerId: "77.88.44.1", description: "Яндекс — IX peering", state: "Established", type: "eBGP", afi: "IPv4 Unicast", uptime: "12д 3ч 11м", holdTime: 90, keepalive: 30, prefixesRx: 11840, prefixesTx: 42, prefixesActive: 11840, inputMessages: 184220, outputMessages: 14100, capabilities: ["4-byte-AS", "Route Refresh", "MP-BGP"], lastError: null, }, { id: "s3", serverId: "srv1", serverLabel: "mt-msk-core-01", serverSite: "MSK", peerIp: "10.200.0.1", remoteAs: 65001, localAs: 65001, routerId: "10.0.0.2", description: "iBGP → SPB-EDGE", state: "Established", type: "iBGP", afi: "IPv4 Unicast", uptime: "8д 14ч 5м", holdTime: 90, keepalive: 30, prefixesRx: 620, prefixesTx: 280283, prefixesActive: 620, inputMessages: 48210, outputMessages: 512800, capabilities: ["4-byte-AS", "Route Refresh", "MP-BGP", "Route-Target Constraint"], lastError: null, }, { id: "s4", serverId: "srv1", serverLabel: "mt-msk-core-01", serverSite: "MSK", peerIp: "10.200.1.1", remoteAs: 65001, localAs: 65001, routerId: "10.0.0.3", description: "iBGP → FRA-EDGE", state: "Established", type: "iBGP", afi: "IPv4 Unicast", uptime: "12д 2ч 18м", holdTime: 90, keepalive: 30, prefixesRx: 512, prefixesTx: 280283, prefixesActive: 512, inputMessages: 41022, outputMessages: 488200, capabilities: ["4-byte-AS", "Route Refresh", "MP-BGP"], lastError: null, }, { id: "s5", serverId: "srv1", serverLabel: "mt-msk-core-01", serverSite: "MSK", peerIp: "10.200.2.1", remoteAs: 65001, localAs: 65001, routerId: "10.0.0.4", description: "iBGP → AMS-EDGE", state: "Active", type: "iBGP", afi: "IPv4 Unicast", uptime: null, holdTime: 90, keepalive: 30, prefixesRx: 0, prefixesTx: 0, prefixesActive: 0, inputMessages: 0, outputMessages: 0, capabilities: [], lastError: "Hold timer expired", }, // ── srv2 (mt-spb-edge-01) ──────────────────────────────────────────── { id: "s6", serverId: "srv2", serverLabel: "mt-spb-edge-01", serverSite: "SPB", peerIp: "195.54.55.1", remoteAs: 12389, localAs: 65001, routerId: "195.54.55.1", description: "Ростелеком — upstream", state: "Established", type: "eBGP", afi: "IPv4 Unicast", uptime: "9д 1ч 44м", holdTime: 90, keepalive: 30, prefixesRx: 192440, prefixesTx: 42, prefixesActive: 191002, inputMessages: 982440, outputMessages: 12200, capabilities: ["4-byte-AS", "Route Refresh", "MP-BGP"], lastError: null, }, { id: "s7", serverId: "srv2", serverLabel: "mt-spb-edge-01", serverSite: "SPB", peerIp: "10.200.0.1", remoteAs: 65001, localAs: 65001, routerId: "10.0.0.1", description: "iBGP → MSK-CORE", state: "Established", type: "iBGP", afi: "IPv4 Unicast", uptime: "8д 14ч 3м", holdTime: 90, keepalive: 30, prefixesRx: 280283, prefixesTx: 620, prefixesActive: 277000, inputMessages: 512800, outputMessages: 48210, capabilities: ["4-byte-AS", "Route Refresh", "MP-BGP", "Route-Target Constraint"], lastError: null, }, // ── srv3 (mt-fra-edge-01) ──────────────────────────────────────────── { id: "s8", serverId: "srv3", serverLabel: "mt-fra-edge-01", serverSite: "FRA", peerIp: "91.108.4.1", remoteAs: 24940, localAs: 65001, routerId: "91.108.4.1", description: "Hetzner — upstream", state: "Established", type: "eBGP", afi: "IPv4 Unicast", uptime: "21д 7ч 12м", holdTime: 90, keepalive: 30, prefixesRx: 218820, prefixesTx: 42, prefixesActive: 215400, inputMessages: 1184200, outputMessages: 11800, capabilities: ["4-byte-AS", "Route Refresh", "MP-BGP", "Graceful Restart"], lastError: null, }, { id: "s9", serverId: "srv3", serverLabel: "mt-fra-edge-01", serverSite: "FRA", peerIp: "193.188.128.1", remoteAs: 6777, localAs: 65001, routerId: "193.188.128.1", description: "AMS-IX — peering (idle)", state: "Idle", type: "eBGP", afi: "IPv4 Unicast", uptime: null, holdTime: 90, keepalive: 30, prefixesRx: 0, prefixesTx: 0, prefixesActive: 0, inputMessages: 821, outputMessages: 412, capabilities: [], lastError: "Administratively down", }, { id: "s10", serverId: "srv3", serverLabel: "mt-fra-edge-01", serverSite: "FRA", peerIp: "10.200.1.1", remoteAs: 65001, localAs: 65001, routerId: "10.0.0.1", description: "iBGP → MSK-CORE", state: "Established", type: "iBGP", afi: "IPv4 Unicast", uptime: "12д 2ч 14м", holdTime: 90, keepalive: 30, prefixesRx: 280283, prefixesTx: 512, prefixesActive: 278100, inputMessages: 488200, outputMessages: 41022, capabilities: ["4-byte-AS", "Route Refresh", "MP-BGP"], lastError: null, }, // ── srv6 (mt-ams-test-01) ──────────────────────────────────────────── { id: "s11", serverId: "srv6", serverLabel: "mt-ams-test-01", serverSite: "AMS", peerIp: "80.249.208.1", remoteAs: 6777, localAs: 65001, routerId: "0.0.0.0", description: "AMS-IX — negotiating", state: "OpenSent", type: "eBGP", afi: "IPv4 Unicast", uptime: null, holdTime: 90, keepalive: 30, prefixesRx: 0, prefixesTx: 0, prefixesActive: 0, inputMessages: 2, outputMessages: 1, capabilities: [], lastError: null, }, { id: "s12", serverId: "srv6", serverLabel: "mt-ams-test-01", serverSite: "AMS", peerIp: "10.200.5.1", remoteAs: 65001, localAs: 65001, routerId: "10.0.0.1", description: "iBGP → MSK-CORE", state: "Established", type: "iBGP", afi: "IPv4 Unicast", uptime: "5д 9ч 17м", holdTime: 90, keepalive: 30, prefixesRx: 280283, prefixesTx: 14, prefixesActive: 276000, inputMessages: 184200, outputMessages: 4100, capabilities: ["4-byte-AS", "Route Refresh", "MP-BGP"], lastError: null, }, ] // ─── visual helpers ─────────────────────────────────────────────────────────── const STATE_STYLE: Record = { Unknown: { bg: "bg-muted", text: "text-muted-foreground", dot: "bg-muted-foreground", label: "Неизвестно" }, Established: { bg: "bg-emerald-500/10", text: "text-emerald-600 dark:text-emerald-400", dot: "bg-emerald-500", label: "Established" }, Active: { bg: "bg-amber-500/10", text: "text-amber-600 dark:text-amber-400", dot: "bg-amber-500", label: "Active" }, Idle: { bg: "bg-slate-500/10", text: "text-slate-500 dark:text-slate-400", dot: "bg-slate-500", label: "Idle" }, Connect: { bg: "bg-blue-500/10", text: "text-blue-600 dark:text-blue-400", dot: "bg-blue-500", label: "Connect" }, OpenSent: { bg: "bg-violet-500/10", text: "text-violet-600 dark:text-violet-400", dot: "bg-violet-500", label: "OpenSent" }, OpenConfirm: { bg: "bg-violet-500/10", text: "text-violet-600 dark:text-violet-400", dot: "bg-violet-500", label: "OpenConfirm" }, } const TYPE_STYLE: Record = { Unknown: { bg: "bg-muted", text: "text-muted-foreground" }, eBGP: { bg: "bg-blue-500/10", text: "text-blue-600 dark:text-blue-400" }, iBGP: { bg: "bg-purple-500/10", text: "text-purple-600 dark:text-purple-400" }, } function TypeBadge({ type }: { type: BgpType }) { const s = TYPE_STYLE[type] return ( {type === "Unknown" ? "Тип неизвестен" : type} ) } const fmtNum = fmtBgpNum // ─── backend mapping ────────────────────────────────────────────────────────── type BackendServer = ReturnType[number] type InventoryServer = Server & { monitoringUnknown?: boolean } function mapBackendServer(s: BackendServer): InventoryServer { return { id: String(s.id), name: s.name || s.host, host: s.host, model: "—", os: "—", site: s.site ?? "", country: s.country || "UN", asn: s.asn ?? "", type: s.type ?? "exit-node", enabled: s.enabled, status: s.status ?? "offline", monitoringUnknown: s.status == null, latency: s.latency ?? null, sessions: 0 } } function backendToFrontend(b: BackendBgpSession): BgpSession { return { id: `${b.serverId}-${b.id}`, serverId: String(b.serverId), serverLabel: b.serverName, serverSite: b.serverSite, peerIp: b.peerIp, remoteAs: b.remoteAs, localAs: b.localAs, routerId: b.remoteId || b.localId, description: b.name, state: Object.hasOwn(STATE_STYLE, b.state) ? b.state as BgpState : "Unknown", type: b.type, afi: b.afi ?? "Неизвестно", uptime: b.uptime, holdTime: b.holdTime, keepalive: b.keepalive, prefixesRx: b.prefixesRx, prefixesTx: b.prefixesTx, prefixesActive: null, inputMessages: b.inputMessages, outputMessages: b.outputMessages, capabilities: b.capabilities, lastError: b.lastError, } } // ─── sessions tab ───────────────────────────────────────────────────────────── const BGP_FILTER_ACCESSORS = { state: (s: BgpSession) => s.state, type: (s: BgpSession) => s.type, afi: (s: BgpSession) => s.afi, } function SessionsTab({ sessions, emptyTitle, selectedSessionId, onSelectSession }: { sessions: BgpSession[]; emptyTitle?: string; selectedSessionId: string | null; onSelectSession: (id: string | null) => void }) { const { search, setSearch, stateFilter, setStateFilter, typeFilter, setTypeFilter, filters: advancedFilters, setFilters: setAdvancedFilters, error: urlError, hasFilters, reset } = useBgpUrlState() const fields = useMemo(() => BGP_FILTER_FIELDS.map(field => field.id === "afi" ? { ...field, options: Array.from(new Set(sessions.map(s => s.afi))).sort().map(value => ({ value, label: value })) } : field), [sessions]) const q = search.toLowerCase() const filtered = useMemo(() => { if (urlError) return [] const base = sessions.filter((s) => { if (stateFilter !== "all" && s.state !== stateFilter) return false if (typeFilter !== "all" && s.type !== typeFilter) return false if ( q && !s.peerIp.includes(q) && !s.description.toLowerCase().includes(q) && !s.serverLabel.toLowerCase().includes(q) && !String(s.remoteAs).includes(q) && !(s.remoteAs === null ? "" : AS_NAMES[s.remoteAs] ?? "").toLowerCase().includes(q) ) { return false } return true }) return applyFilterQuery(base, advancedFilters, BGP_FILTER_ACCESSORS) }, [sessions, q, stateFilter, typeFilter, advancedFilters, urlError]) return (
{urlError && {urlError}} s.state === "Unknown").length }, { value: "Established", label: "Established", count: sessions.filter((s) => s.state === "Established").length }, { value: "Active", label: "Active", count: sessions.filter((s) => s.state === "Active").length }, { value: "Idle", label: "Idle", count: sessions.filter((s) => s.state === "Idle").length }, { value: "OpenSent", label: "OpenSent", count: sessions.filter((s) => s.state === "OpenSent").length }, ], }} filterControl={} search={search} onSearchChange={setSearch} searchPlaceholder="IP, AS, описание…" countLabel={`${filtered.length} из ${sessions.length}`} actions={
{(["all", "eBGP", "iBGP", "Unknown"] as const).map((t) => ( ))}
} /> {selectedSessionId && sessions.some(s => s.id === selectedSessionId) && !filtered.some(s => s.id === selectedSessionId) &&

Сессия из ссылки скрыта текущими фильтрами.

} {hasFilters && !urlError && }
) } // ─── routers tab ────────────────────────────────────────────────────────────── function RoutersTab({ sessions }: { sessions: BgpSession[] }) { const byRouter = useMemo(() => { const map: Record = {} sessions.forEach(s => { if (!map[s.serverId]) map[s.serverId] = { label: s.serverLabel, site: s.serverSite, sessions: [] } map[s.serverId].sessions.push(s) }) return Object.entries(map).map(([id, v]) => ({ id, ...v })) }, [sessions]) return (
{byRouter.map(router => { const estCnt = router.sessions.filter(s => s.state === "Established").length const downCnt = router.sessions.filter(s => s.state !== "Established" && s.state !== "Unknown").length const totalRx = sumBgpCounters(router.sessions.map(s => s.prefixesRx)) return ( {/* header */}

{router.label}

{router.site} · {Array.from(new Set(router.sessions.map(s => s.localAs === null ? "AS неизвестна" : "AS" + s.localAs))).join(", ")}

{estCnt > 0 && ( ✓ {estCnt} )} {downCnt > 0 && ( ⚠ {downCnt} )}
{/* session rows */}
{router.sessions.map(s => { const ss = STATE_STYLE[s.state] return (
{s.peerIp}
{s.remoteAs === null ? "AS неизвестна" : "AS" + s.remoteAs} {s.remoteAs !== null && AS_NAMES[s.remoteAs] && ( · {AS_NAMES[s.remoteAs]} )}
{s.prefixesRx !== null && s.prefixesRx > 0 && (

↓ {fmtNum(s.prefixesRx)}

)}

{ss.label}

) })}
{/* footer summary */} {totalRx !== null && totalRx > 0 && (
Всего получено: {fmtNum(totalRx)} префиксов
)}
) })}
) } // ─── analytics tab ──────────────────────────────────────────────────────────── function AnalyticsTab({ sessions }: { sessions: BgpSession[] }) { // top peers by prefixes received (eBGP only) const topPeers = useMemo(() => (sessions.some(s => s.prefixesRx === null) ? [] : [...sessions]) .filter(s => s.prefixesRx !== null && s.prefixesRx > 0) .sort((a, b) => (b.prefixesRx ?? 0) - (a.prefixesRx ?? 0)) .slice(0, 8), [sessions] ) const maxRx = topPeers[0]?.prefixesRx ?? 1 // state distribution const stateCounts = useMemo(() => { const map: Record = {} sessions.forEach(s => { map[s.state] = (map[s.state] ?? 0) + 1 }) return Object.entries(map).sort((a, b) => b[1] - a[1]) as [BgpState, number][] }, [sessions]) // total prefix stats const totalRx = sumBgpCounters(sessions.map(s => s.prefixesRx)) const totalActive = sumBgpCounters(sessions.map(s => s.prefixesActive)) const ebgpSessions = sessions.filter(s => s.type === "eBGP").length const ibgpSessions = sessions.filter(s => s.type === "iBGP").length return (
, iconClassName: "text-success", }, { id: "active", label: "Маршруты", hint: totalActive === null ? "нет данных API" : undefined, value: fmtNum(totalActive), icon: , iconClassName: "text-info", }, { id: "ebgp", label: "eBGP", value: sessions.some(s => s.type === "Unknown") ? "—" : ebgpSessions, icon: , iconClassName: "text-muted-foreground", }, { id: "ibgp", label: "iBGP", value: sessions.some(s => s.type === "Unknown") ? "—" : ibgpSessions, icon: , iconClassName: "text-muted-foreground", }, ]} />
{/* prefixes by peer — horizontal bar chart */}
{topPeers.map((s, i) => { const pct = ((s.prefixesRx ?? 0) / maxRx) * 100 const ss = STATE_STYLE[s.state] return (
{i + 1}
{s.peerIp} {s.remoteAs === null ? "AS неизвестна" : "AS" + s.remoteAs}
{fmtNum(s.prefixesRx)}

{s.serverLabel}

) })} {topPeers.length === 0 && (

Недостаточно данных о префиксах для сравнения

)}
{/* right column */}
{/* state distribution */} {stateCounts.length === 0 ? (

Нет данных

) : (
{stateCounts.map(([state, count]) => { const ss = STATE_STYLE[state] const pct = sessions.length > 0 ? (count / sessions.length) * 100 : 0 return (
{ss.label}
{count}
) })}
)} {/* eBGP vs iBGP */} {sessions.length === 0 || sessions.some(s => s.type === "Unknown") ? (

Нет данных

) : ( <> {[ { type: "eBGP" as BgpType, label: "Внешние (eBGP)", count: ebgpSessions }, { type: "iBGP" as BgpType, label: "Внутренние (iBGP)", count: ibgpSessions }, ].map(({ type, count }) => { const pct = sessions.length > 0 ? (count / sessions.length) * 100 : 0 return (
{count}
) })} {/* active AS list */}

Автономные системы

{Object.entries( sessions.filter(s => s.type === "eBGP").reduce>((acc, s) => { if (s.remoteAs !== null) acc[s.remoteAs] = (acc[s.remoteAs] ?? 0) + 1; return acc }, {}) ).sort((a, b) => b[1] - a[1]).map(([as, cnt]) => (
AS{as} {AS_NAMES[Number(as)] ?? ""} {cnt}
))}
)}
) } // ─── page ───────────────────────────────────────────────────────────────────── const TABS: Array<{ id: BgpTab; label: string; icon: React.ReactNode }> = [ { id: "sessions", label: "Сессии", icon: }, { id: "routers", label: "По роутерам", icon: }, { id: "analytics", label: "Аналитика", icon: }, ] export default function BgpPage() { return Загрузка BGP…
}> } function BgpRoute() { const { mode, backendUrl } = useDataSource() const params = useSearchParams() const router = useRouter() const selectedServerId = params.get("server") ?? ALL_SERVERS_ID const selectedSessionId = params.get("session") const tab = params.get("tab") const activeTab: BgpTab = tab === "routers" || tab === "analytics" ? tab : "sessions" function navigate(values: Record) { const next = new URLSearchParams(params.toString()) for (const [key, value] of Object.entries(values)) { if (value === null) next.delete(key) else next.set(key, value) } router.push("/bgp" + (next.size ? "?" + next.toString() : "")) } return navigate({ server: id === ALL_SERVERS_ID ? null : id, session: null })} onSelectSession={id => navigate({ session: id })} onSelectTab={tab => navigate({ tab: tab === "sessions" ? null : tab })} /> } function BgpPageContent({ selectedServerId, selectedSessionId, activeTab, onSelectServer, onSelectSession, onSelectTab }: { selectedServerId: string; selectedSessionId: string | null; activeTab: BgpTab; onSelectServer: (id: string) => void; onSelectSession: (id: string | null) => void; onSelectTab: (tab: BgpTab) => void; }) { const { mode, backendUrl } = useDataSource() const isLive = mode === "live" const [liveSessions, setLiveSessions] = useState([]) const [liveServers, setLiveServers] = useState([]) const [loading, setLoading] = useState(isLive) const [fetchedAt, setFetchedAt] = useState(null) const [liveError, setLiveError] = useState(null) const [failures, setFailures] = useState([]) const [hasLoaded, setHasLoaded] = useState(false) const [readReceipts, setReadReceipts] = useState>({}) const [fetchTick, setFetchTick] = useState(0) useEffect(() => { if (!isLive) { queueMicrotask(() => { setLiveSessions([]) setLiveServers([]) setLiveError(null) }) return } let cancelled = false queueMicrotask(() => { if (cancelled) return setLoading(true) void Promise.all([ requestJson(backendUrl, "/api/bgp/catalog"), requestJson(backendUrl, "/api/servers"), ]) .then(([data, servers]) => { if (cancelled) return const catalog = bgpCatalogSchema.parse(data) const inventory = bgpServersSchema.parse(servers) const enabled = new Set(inventory.filter(s => s.enabled).map(s => s.id)) if (catalog.sessions.some(s => !enabled.has(s.serverId)) || catalog.failures.some(f => !enabled.has(f.serverId))) throw new Error("Чужой контекст BGP-каталога") const failed = new Set(catalog.failures.map(f => String(f.serverId))) setLiveSessions(previous => [...catalog.sessions.map(backendToFrontend), ...previous.filter(s => enabled.has(Number(s.serverId)) && failed.has(s.serverId))]) setLiveServers(inventory.filter(s => s.enabled).map(mapBackendServer)) setFailures(catalog.failures) setLiveError(null) setHasLoaded(true) const receivedAt = new Date() setFetchedAt(receivedAt) setReadReceipts(previous => ({ ...Object.fromEntries(Object.entries(previous).filter(([id]) => enabled.has(Number(id)))), ...Object.fromEntries([...enabled].filter(id => !failed.has(String(id))).map(id => [String(id), receivedAt.toISOString()])) })) setLoading(false) }) .catch((err: unknown) => { if (cancelled) return setLiveError(err instanceof Error ? err.message : String(err)) setLoading(false) }) }) return () => { cancelled = true } }, [isLive, backendUrl, fetchTick]) const unknownIds = useMemo(() => new Set(isLive ? (liveError ? liveServers.map(s => s.id) : failures.map(f => String(f.serverId))) : []), [isLive, liveError, liveServers, failures]) const allSessions: BgpSession[] = isLive ? liveSessions.map(s => unknownIds.has(s.serverId) ? { ...s, state: "Unknown" } : s) : SESSIONS const unavailableContext = isLive && hasLoaded && selectedServerId !== ALL_SERVERS_ID && !liveServers.some(s => s.id === selectedServerId) const incomplete = isLive && (unavailableContext || !hasLoaded || Boolean(liveError) || (selectedServerId === ALL_SERVERS_ID ? failures.length > 0 : unknownIds.has(selectedServerId))) const displayServers = isLive ? liveServers : mockServers.filter((s) => s.enabled) const effectiveServerId = selectedServerId const sessions = useMemo(() => { if (effectiveServerId === ALL_SERVERS_ID) return allSessions return allSessions.filter((s) => s.serverId === effectiveServerId) }, [allSessions, effectiveServerId]) const railItems = useMemo(() => { const counts = new Map() for (const s of allSessions) { counts.set(s.serverId, (counts.get(s.serverId) ?? 0) + 1) } return displayServers.map((s) => ({ id: s.id, name: s.name, host: s.host, site: s.site, country: s.country, status: "monitoringUnknown" in s && s.monitoringUnknown ? undefined : s.status, meta: "monitoringUnknown" in s && s.monitoringUnknown ? "Статус неизвестен" : undefined, type: s.type, count: isLive && (!hasLoaded || unknownIds.has(s.id)) ? undefined : counts.get(s.id) ?? 0, enabled: s.enabled, title: [s.name, s.host, s.asn].filter(Boolean).join(" · "), })) }, [displayServers, allSessions, hasLoaded, isLive, unknownIds]) const established = sessions.filter(s => s.state === "Established").length const notEstab = sessions.filter(s => s.state !== "Established" && s.state !== "Unknown").length const totalRx = sumBgpCounters(sessions.map(s => s.prefixesRx)) return ( } /> } banner={
{TABS.map(t => ( ))}
} >
{unavailableContext && Устройство BGP недоступно в этом каталогеКонтекст {selectedServerId} сохранён. Выберите доступное устройство.} {selectedSessionId && hasLoaded && !sessions.some(s => s.id === selectedSessionId) && Сессия из ссылки отсутствует в выбранном снимке. Контекст ссылки сохранён.} {/* data source banner */} {isLive && loading && (
Загрузка BGP-сессий…
)} {isLive && fetchedAt && (
Ответ каталога получен . Это время получения браузером, не время измерения RouterOS. {displayServers.filter(s => effectiveServerId === ALL_SERVERS_ID || s.id === effectiveServerId).map(s =>

{s.name}: {readReceipts[s.id] ? : "успешный опрос не получен"}{unknownIds.has(s.id) ? " · данные устарели" : ""}

)}
)} {isLive && (liveError || failures.length > 0) && ( {liveError ? "Не удалось загрузить BGP" : "Не удалось опросить часть роутеров BGP"} {liveError ?? failures.map(f => f.serverName + ": " + f.error).join("; ")}. {hasLoaded ? "Показаны последние полученные данные; статус отказавших устройств неизвестен." : "Данные пока неизвестны."} )} {mode === "mock" && ( Моковые данные )} , iconClassName: "text-muted-foreground", }, { id: "established", label: "Established", value: incomplete || sessions.some(s => s.state === "Unknown") ? "—" : established, icon: , iconClassName: "text-success", }, { id: "not-estab", label: "Не поднято", value: incomplete || sessions.some(s => s.state === "Unknown") ? "—" : notEstab, icon: , iconClassName: notEstab > 0 ? "text-warning" : "text-muted-foreground", variant: notEstab > 0 ? "warning" : "default", }, { id: "prefixes", label: "Префиксы", value: incomplete ? "—" : fmtNum(totalRx), icon: , iconClassName: "text-info", }, ]} /> {/* alert: not-established sessions */} {!incomplete && notEstab > 0 && ( {notEstab} сессии не в состоянии Established Проверьте {sessions.filter(s => s.state !== "Established" && s.state !== "Unknown").map(s => s.peerIp).join(", ")} )} {/* tab content */} {activeTab === "sessions" && } {activeTab === "routers" && (incomplete ?

Сводка по роутерам неизвестна до успешного опроса. Последние сессии доступны в таблице.

: )} {activeTab === "analytics" && (incomplete ?

Аналитика неизвестна до успешного опроса. Последние сессии доступны в таблице.

: )}
) }