"use client" import { Fragment, useState, useMemo, useEffect } from "react" import { PageHeader } from "@/components/page-header" import { Card, CardContent } from "@/components/ui/card" import { Button } from "@/components/ui/button" import { cn } from "@/lib/utils" import { RefreshCwIcon, DownloadIcon, SearchIcon, ActivityIcon, BarChart3Icon, ChevronRightIcon, ChevronDownIcon, ArrowDownIcon, ClipboardCopyIcon, ServerIcon, XIcon, } from "lucide-react" import { useDataSource } from "@/lib/data-source" // ─── types ──────────────────────────────────────────────────────────────────── type BgpState = "Established" | "Active" | "Idle" | "Connect" | "OpenSent" | "OpenConfirm" type BgpType = "eBGP" | "iBGP" type BgpAfi = "IPv4 Unicast" | "IPv6 Unicast" | "VPNv4 Unicast" type BgpTab = "sessions" | "routers" | "analytics" type StateFilter = "all" | BgpState type TypeFilter = "all" | BgpType interface BgpSession { id: string serverId: string serverLabel: string serverSite: string peerIp: string remoteAs: number localAs: number routerId: string description: string state: BgpState type: BgpType afi: BgpAfi uptime: string | null holdTime: number keepalive: number prefixesRx: number prefixesTx: number prefixesActive: number inputMessages: number outputMessages: number capabilities: string[] lastError: string | null } // ─── AS name lookup ─────────────────────────────────────────────────────────── const AS_NAMES: Record = { 8359: "МТС / Tele2", 13238: "Яндекс", 12389: "Ростелеком", 24940: "Hetzner", 6777: "AMS-IX", 1299: "Telia", 65001: "iBGP internal", } // ─── 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 = { 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 = { 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 StateBadge({ state }: { state: BgpState }) { const s = STATE_STYLE[state] return ( {s.label} ) } function TypeBadge({ type }: { type: BgpType }) { const s = TYPE_STYLE[type] return ( {type} ) } function CapChip({ cap }: { cap: string }) { return ( {cap} ) } function fmtNum(n: number) { if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M` if (n >= 1_000) return `${(n / 1_000).toFixed(0)}k` return String(n) } function PrefixBar({ rx, tx, active }: { rx: number; tx: number; active: number }) { const max = Math.max(rx, 1) return (
{[ { label: "Получено", val: rx, color: "bg-emerald-500", w: rx / max }, { label: "Активных", val: active, color: "bg-sky-500", w: active / max }, { label: "Отправлено", val: tx, color: "bg-blue-400", w: Math.min(tx / max, 1) }, ].map(r => (
{r.label}
0 ? 2 : 0)}%` }} />
{fmtNum(r.val)}
))}
) } // ─── RSC snippet ────────────────────────────────────────────────────────────── function rscSnippet(s: BgpSession) { return `/routing bgp connection\nadd name=peer-as${s.remoteAs} remote.address=${s.peerIp}/32 \\\n remote.as=${s.remoteAs} local.role=${s.type === "eBGP" ? "ebgp" : "ibgp"} \\\n output.filter-chain=export-filter input.filter=import-filter \\\n routing-table=main` } // ─── session expanded row ───────────────────────────────────────────────────── function SessionDetail({ s }: { s: BgpSession }) { const [copied, setCopied] = useState(false) function copy() { navigator.clipboard.writeText(rscSnippet(s)).then(() => { setCopied(true); setTimeout(() => setCopied(false), 1800) }) } return (
{[ { label: "Router ID", value: s.routerId }, { label: "Hold / KA", value: `${s.holdTime}s / ${s.keepalive}s` }, { label: "AFI/SAFI", value: s.afi }, { label: "Сообщения ↓/↑", value: `${fmtNum(s.inputMessages)} / ${fmtNum(s.outputMessages)}` }, ].map(({ label, value }) => (

{label}

{value}

))}
{/* prefix bars */} {s.state === "Established" && (

Префиксы

)} {/* capabilities */} {s.capabilities.length > 0 && (

Capabilities

{s.capabilities.map(c => )}
)} {/* last error */} {s.lastError && (

{s.lastError}

)} {/* rsc export */}

RouterOS Export

            {rscSnippet(s)}
          
) } // ─── backend mapping ────────────────────────────────────────────────────────── interface BackendBgpSession { id: string; serverId: number; serverName: string; serverSite: string; serverCountry: string name: string; peerIp: string; remoteAs: number; localAs: number localId: string; remoteId: string; state: string; type: "eBGP" | "iBGP" uptime: string | null; holdTime: number; keepalive: number prefixesRx: number; prefixesTx: number inputMessages: number; outputMessages: number capabilities: string[]; lastError: string | null } 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: (b.state as BgpState) || "Idle", type: b.type, afi: "IPv4 Unicast", uptime: b.uptime, holdTime: b.holdTime, keepalive: b.keepalive, prefixesRx: b.prefixesRx, prefixesTx: b.prefixesTx, prefixesActive: b.prefixesRx, inputMessages: b.inputMessages, outputMessages: b.outputMessages, capabilities: b.capabilities, lastError: b.lastError, } } // ─── sessions tab ───────────────────────────────────────────────────────────── const STATE_FILTERS: Array<{ value: StateFilter; label: string }> = [ { value: "all", label: "Все" }, { value: "Established", label: "Established" }, { value: "Active", label: "Active" }, { value: "Idle", label: "Idle" }, { value: "OpenSent", label: "OpenSent" }, ] function SessionsTab({ sessions }: { sessions: BgpSession[] }) { const [search, setSearch] = useState("") const [stateFilter, setStateFilter] = useState("all") const [typeFilter, setTypeFilter] = useState("all") const [expandedId, setExpandedId] = useState(null) const q = search.toLowerCase() const filtered = useMemo(() => 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.includes(q) && !String(s.remoteAs).includes(q) && !(AS_NAMES[s.remoteAs] ?? "").toLowerCase().includes(q)) return false return true }), [sessions, q, stateFilter, typeFilter]) return (
{/* filter bar */}
{/* search */}
setSearch(e.target.value)} placeholder="IP, AS, описание…" className="h-8 pl-8 pr-3 w-52 text-xs bg-muted/50 border border-border rounded-md outline-none focus:border-primary transition-colors" /> {search && ( )}
{/* state filter */}
{STATE_FILTERS.map(f => ( ))}
{/* type filter */}
{(["all", "eBGP", "iBGP"] as const).map(t => ( ))}
{filtered.length} из {sessions.length}
{/* table */}
))} {filtered.map(s => { const isOpen = expandedId === s.id return ( setExpandedId(isOpen ? null : s.id)} className={cn( "cursor-pointer transition-colors", isOpen ? "bg-muted/30" : "hover:bg-muted/20", )}> {isOpen && ( )} ) })} {filtered.length === 0 && ( )}
{["Роутер", "Peer IP", "Remote AS", "Описание", "Тип", "Состояние", "Uptime", "Prefixes ↓", "Prefixes ↑"].map(h => ( {h}
{isOpen ? : } {s.serverLabel} {s.peerIp}
AS{s.remoteAs} {AS_NAMES[s.remoteAs] && ( {AS_NAMES[s.remoteAs]} )}
{s.description} {s.uptime ?? "—"} {s.prefixesRx > 0 ? {fmtNum(s.prefixesRx)} : } {s.prefixesTx > 0 ? {fmtNum(s.prefixesTx)} : }
Нет сессий по заданным фильтрам
) } // ─── 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.length - estCnt const totalRx = router.sessions.reduce((a, s) => a + s.prefixesRx, 0) return ( {/* header */}

{router.label}

{router.site} · AS65001

{estCnt > 0 && ( ✓ {estCnt} )} {downCnt > 0 && ( ⚠ {downCnt} )}
{/* session rows */}
{router.sessions.map(s => { const ss = STATE_STYLE[s.state] return (
{s.peerIp}
AS{s.remoteAs} {AS_NAMES[s.remoteAs] && ( · {AS_NAMES[s.remoteAs]} )}
{s.prefixesRx > 0 && (

↓ {fmtNum(s.prefixesRx)}

)}

{s.state}

) })}
{/* footer summary */} {totalRx > 0 && (
Всего получено: {fmtNum(totalRx)} префиксов
)}
) })}
) } // ─── analytics tab ──────────────────────────────────────────────────────────── function AnalyticsTab({ sessions }: { sessions: BgpSession[] }) { // top peers by prefixes received (eBGP only) const topPeers = useMemo(() => [...sessions] .filter(s => s.prefixesRx > 0) .sort((a, b) => b.prefixesRx - a.prefixesRx) .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 = sessions.reduce((a, s) => a + s.prefixesRx, 0) const totalActive = sessions.reduce((a, s) => a + s.prefixesActive, 0) const ebgpSessions = sessions.filter(s => s.type === "eBGP").length const ibgpSessions = sessions.filter(s => s.type === "iBGP").length return (
{/* summary row */}
{[ { label: "Всего префиксов", value: fmtNum(totalRx), color: "text-emerald-600 dark:text-emerald-400" }, { label: "Активных маршрутов", value: fmtNum(totalActive), color: "text-sky-600 dark:text-sky-400" }, { label: "eBGP сессий", value: ebgpSessions, color: "" }, { label: "iBGP сессий", value: ibgpSessions, color: "" }, ].map(s => (

{s.label}

{s.value}

))}
{/* prefixes by peer — horizontal bar chart */}

Топ-8 пиров по полученным префиксам

{topPeers.map((s, i) => { const pct = (s.prefixesRx / maxRx) * 100 const ss = STATE_STYLE[s.state] return (
{i + 1}
{s.peerIp} 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 (
{state}
{count}
) })}
)} {/* eBGP vs iBGP */}

eBGP vs iBGP

{sessions.length === 0 ? (

Нет данных

) : ( <> {[ { 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) => { 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() { const [activeTab, setActiveTab] = useState("sessions") const { mode, backendUrl, backendStatus } = useDataSource() const isLive = mode === "live" && backendStatus === true const [liveSessions, setLiveSessions] = useState([]) const [loading, setLoading] = useState(false) const [fetchedAt, setFetchedAt] = useState(null) const [liveError, setLiveError] = useState(null) const [fetchTick, setFetchTick] = useState(0) useEffect(() => { if (!isLive) { setLiveSessions([]); return } let cancelled = false setLoading(true) setLiveError(null) fetch(`${backendUrl}/api/bgp/sessions`) .then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`) return r.json() as Promise }) .then(data => { if (cancelled) return setLiveSessions(data.map(backendToFrontend)) setFetchedAt(new Date()) setLoading(false) }) .catch((err: unknown) => { if (cancelled) return setLiveError(err instanceof Error ? err.message : String(err)) setLoading(false) }) return () => { cancelled = true } }, [isLive, backendUrl, fetchTick]) // Use live or mock data for all tabs and KPI const sessions = isLive ? liveSessions : SESSIONS const established = sessions.filter(s => s.state === "Established").length const notEstab = sessions.length - established const totalRx = sessions.reduce((a, s) => a + s.prefixesRx, 0) const serverCount = useMemo( () => new Set(liveSessions.map(s => s.serverId)).size, [liveSessions], ) return (
} /> {/* tab bar */}
{TABS.map(t => ( ))}
{/* data source banner */} {isLive && loading && (
Загрузка BGP-сессий…
)} {isLive && !loading && fetchedAt && !liveError && (
Живые данные · {serverCount} серверов · обновлено {fetchedAt.toLocaleTimeString("ru")}
)} {isLive && liveError && (

Ошибка загрузки: {liveError}

)} {isLive && !loading && liveSessions.length === 0 && !liveError && fetchedAt && (
BGP не настроен ни на одном сервере
)} {mode === "mock" && ( Моковые данные )} {/* KPI strip */}
{[ { label: "Сессий всего", value: sessions.length, color: "" }, { label: "Established", value: established, color: "text-emerald-600 dark:text-emerald-400" }, { label: "Не установлено", value: notEstab, color: notEstab > 0 ? "text-amber-500" : "text-muted-foreground" }, { label: "Получено префиксов", value: fmtNum(totalRx),color: "" }, ].map(s => (

{s.label}

{s.value}

))}
{/* alert: not-established sessions */} {notEstab > 0 && (

{notEstab} сессии не в состоянии Established — проверьте {sessions.filter(s => s.state !== "Established").map(s => s.peerIp).join(", ")}

)} {/* tab content */} {activeTab === "sessions" && } {activeTab === "routers" && } {activeTab === "analytics" && }
) }