feat(mcp): подготовить сервер и доступ к приёмке

This commit is contained in:
Denozordec committed 2026-11-03 03:12:03 +07:00
commit bf3d50e1ff
3835 files changed
+537122

No files matched your search

+351
View File
@@ -0,0 +1,351 @@
"use client"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import Link from "next/link"
import {
ActivityIcon,
BellIcon,
CableIcon,
FilterIcon,
GitMergeIcon,
HardDriveIcon,
HeartPulseIcon,
MapIcon,
ServerIcon,
ShieldIcon,
} from "lucide-react"
import { PageHeader } from "@/components/page-header"
import { OpsPanel } from "@/components/ops-panel"
import { KpiStatGrid } from "@/components/reui-kit/kpi-stat-grid"
import { QuickActionGrid } from "@/components/reui-kit/quick-action-grid"
import { AttentionQueue } from "@/components/reui-kit/attention-queue"
import { TrafficRxTxChart } from "@/components/reui-kit/traffic-rx-tx-chart"
import { LatencyChart } from "@/components/dashboard/latency-chart"
import { DashboardEventsTimeline } from "@/components/dashboard/dashboard-events-timeline"
import { InternetPathPanel } from "@/components/dashboard/internet-path-panel"
import { DashboardActiveProbesDataGrid } from "@/components/data-grids/dashboard-active-probes-data-grid"
import { EmptyState } from "@/components/empty-state"
import { DashboardDataStatus } from "@/components/dashboard/dashboard-data-status"
import { Badge } from "@/components/reui/badge"
import { buttonVariants } from "@/components/ui/button"
import { cn } from "@/lib/utils"
import { fmtRate } from "@/lib/fmt-rate"
import { useDashboardLive, type AttentionRow } from "@/hooks/use-dashboard-live"
function fmtIntRu(n: number): string {
return n.toLocaleString("ru-RU")
}
function AttentionRows({ rows }: { rows: AttentionRow[] }) {
const [expanded,setExpanded]=useState(false)
const [visible,setVisible]=useState(55)
function renderRow(row: AttentionRow) {
return (
<Link
key={row.id}
href={row.href}
className="hover:bg-muted/40 focus-visible:ring-ring flex min-w-0 items-start justify-between gap-2 rounded-md px-1 py-1.5 focus-visible:ring-2"
>
<span className="min-w-0 wrap-anywhere text-sm font-medium">{row.title}</span>
<Badge
size="sm"
variant={row.tone === "warning" ? "warning-light" : "destructive-light"}
className="shrink-0"
>
{row.hint}
</Badge>
</Link>
)
}
return (
<div className="flex flex-col gap-0.5">
{rows.slice(0, 5).map(renderRow)}
{rows.length > 5 && (
<details onToggle={event=>{setExpanded(event.currentTarget.open);if(!event.currentTarget.open)setVisible(55)}}>
<summary className="cursor-pointer rounded-md px-1 py-2 text-xs text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring">Ещё {rows.length - 5}</summary>
{expanded && rows.slice(5,visible).map(renderRow)}
{expanded && rows.length>visible && <Button type="button" variant="ghost" size="sm" onClick={()=>setVisible(count=>Math.min(count+50,rows.length))}>Показать ещё {Math.min(50,rows.length-visible)}</Button>}
</details>
)}
</div>
)
}
export default function DashboardPage() {
const dash = useDashboardLive()
const offline = dash.offlineCount
const health = dash.probeDown + dash.probeWarn
return (
<div className="flex h-full flex-col">
<PageHeader crumbs={[{ label: "Обзор" }, { label: "Дашборд" }]} />
<div className="flex-1 overflow-y-auto">
<div className="flex flex-col gap-4 px-4 py-4 md:gap-6 md:px-6 md:py-5">
{dash.isLive && <DashboardDataStatus sources={dash.sources} refreshing={dash.refreshing} onRefresh={dash.retry} />}
<KpiStatGrid
aria-label="Сводка дашборда"
isLoading={dash.loading}
items={[
{
id: "fleet",
label: "Серверы онлайн",
value: dash.availability.servers ? `${dash.onlineCount} / ${dash.totalServers}` : "—",
hint: !dash.availability.servers ? "нет данных" : !dash.isLive
? "демо"
: offline > 0 || dash.unknownServerCount > 0 || dash.degradedServerCount > 0
? [offline > 0 ? `${offline} не в сети` : null,dash.unknownServerCount > 0 ? `Статус неизвестен: ${dash.unknownServerCount}` : null,dash.degradedServerCount > 0 ? `Ухудшение: ${dash.degradedServerCount}` : null].filter(Boolean).join(" · ")
: dash.totalServers > 0
? "все в сети"
: "нет серверов",
href: "/servers",
icon: <ServerIcon />,
iconClassName: "text-muted-foreground",
variant: offline > 0 || dash.unknownServerCount > 0 || dash.degradedServerCount > 0 ? "warning" : "default",
},
{
id: "overlay",
label: "Туннели работают",
value: dash.availability.overlay ? String(dash.overlay.up) : "—",
hint: !dash.availability.overlay ? "не все источники доступны" : !dash.reliable.overlay ? "Последний успешный снимок; не все источники обновлены" : dash.overlay.total > 0 ? `Не работают: ${dash.overlay.down}` : "нет туннелей",
href: "/gre",
icon: <CableIcon />,
iconClassName: "text-info",
variant: dash.overlay.down > 0 ? "warning" : "default",
},
{
id: "bgp",
label: "Префиксы BGP",
value: dash.bgp?.prefixSum != null ? fmtIntRu(dash.bgp.prefixSum) : "—",
hint: dash.bgp ? `Сессий установлено: ${dash.bgp.establishedCount ?? "неизвестно"}${dash.bgp.prefixSum === null ? " · префиксы неизвестны" : ""}` : "нет данных",
href: "/bgp",
icon: <GitMergeIcon />,
iconClassName: "text-primary",
},
{
id: "health",
label: "Проверки с проблемами",
value: dash.availability.probes && dash.unknownProbeCount===0 ? String(health) : "—",
hint: dash.availability.probes ? `${dash.probeDown} ошибок · ${dash.probeWarn} предупреждений${dash.unknownProbeCount>0?` · неизвестны: ${dash.unknownProbeCount}`:""}` : "нет данных",
href: "/uptime",
icon: <BellIcon />,
iconClassName: health > 0 ? "text-destructive" : "text-muted-foreground",
variant: health > 0 ? "destructive" : "default",
},
]}
/>
<section aria-labelledby="dashboard-attention-title" className="flex min-w-0 flex-col gap-2">
<h2 id="dashboard-attention-title" className="text-sm font-semibold">Требует внимания</h2>
<AttentionQueue
className="lg:grid-cols-3"
columns={[
{
id: "nodes",
title: "Серверы",
icon: ServerIcon,
count: dash.attentionServers.length,
countVariant: "warning-light",
emptyTitle: dash.reliable.servers ? (dash.totalServers > 0 ? "Серверы без проблем" : "Нет серверов") : "Состояние не подтверждено",
emptyDescription: dash.reliable.servers ? "Нет включённых серверов с ошибками доступности." : "Проверьте состояние источников выше.",
children: <AttentionRows rows={dash.attentionServers} />,
},
{
id: "overlay",
title: "Туннели",
icon: CableIcon,
iconClassName: "text-info",
count: dash.attentionOverlay.length,
countVariant: "destructive-light",
emptyTitle: dash.reliable.overlay ? (dash.overlay.total > 0 ? "Туннели работают" : "Нет туннелей") : "Состояние не подтверждено",
emptyDescription: dash.reliable.overlay ? "В GRE, WireGuard и VXLAN проблем не обнаружено." : "Проверьте состояние источников выше.",
children: <AttentionRows rows={dash.attentionOverlay} />,
},
{
id: "probes",
title: "Проверки",
icon: HeartPulseIcon,
iconClassName: "text-destructive",
count: dash.attentionProbes.length,
countVariant: "destructive-light",
emptyTitle: dash.reliable.probes ? (dash.probeCount > 0 ? "Проверки без ошибок" : "Нет активных проверок") : "Состояние не подтверждено",
emptyDescription: dash.reliable.probes ? "Нет ошибок и предупреждений активных проверок." : "Проверьте состояние источников выше.",
children: <AttentionRows rows={dash.attentionProbes} />,
},
]}
/>
</section>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-[2fr_1fr]">
<OpsPanel
title="Задержка до серверов"
description={
dash.latency.kind === "mock" || dash.latency.kind === "live"
? dash.latency.subtitle
: dash.latency.kind === "loading"
? "Загрузка…"
: "Нет серии RTT"
}
contentClassName="px-3 pb-3"
>
{dash.latency.kind === "loading" && (
<div className="bg-muted/50 h-[220px] w-full animate-pulse rounded-md" />
)}
{dash.latency.kind === "empty" && (
<p className="text-muted-foreground px-4 py-10 text-center text-sm">{dash.latency.message}</p>
)}
{(dash.latency.kind === "mock" || dash.latency.kind === "live") && (
<LatencyChart series={dash.latency.series} labels={dash.latency.labels} />
)}
</OpsPanel>
<OpsPanel
title="Последние события"
description="Система и BGP"
headerRight={
<Link href="/alerts" className={cn(buttonVariants({ variant: "ghost", size: "sm" }), "h-7 text-xs")}>
Все →
</Link>
}
>
<DashboardEventsTimeline
events={dash.recentEvents}
loading={dash.eventsLoading}
error={dash.eventsError}
/>
</OpsPanel>
</div>
<div>
<OpsPanel
title="Пропускная способность"
description={dash.traffic?.demo ? "Суммарный RX / TX · демо" : "Суммарная история RX / TX · 1 ч; значения — последний измеренный снимок"}
headerRight={<Link href="/traffic" className={cn(buttonVariants({ variant: "outline", size: "sm" }), "h-7 text-xs")}>Трафик →</Link>}
contentClassName="px-3 pb-3"
>
{dash.loading && !dash.traffic ? (
<div className="bg-muted/50 h-[220px] w-full animate-pulse rounded-md" />
) : !dash.traffic ? (
<p className="text-muted-foreground px-4 py-10 text-center text-sm">
Нет данных трафика за последний час.{" "}
<Link href="/traffic" className="underline underline-offset-2">
Открыть трафик
</Link>
</p>
) : (
<>
<div className="flex flex-wrap items-center gap-3 px-2 py-3 text-xs">
{dash.traffic.demo ? <Badge variant="outline" size="sm">демо</Badge> : !dash.traffic.observationsKnown ? <Badge variant="outline" size="sm">Измерения не подтверждены API</Badge> : null}
<span className="tabular-nums">RX {dash.traffic.rxNow===null?"—":fmtRate(dash.traffic.rxNow)}</span>
<span className="tabular-nums">TX {dash.traffic.txNow===null?"—":fmtRate(dash.traffic.txNow)}</span>
</div>
<TrafficRxTxChart rx={dash.traffic.rx} tx={dash.traffic.tx} range="1h" embedded />
</>
)}
</OpsPanel>
</div>
<InternetPathPanel
model={dash.internetPath}
loading={dash.internetPathLoading}
error={dash.isLive ? dash.internetPathError : null}
/>
<OpsPanel
title="Активные пробы"
description={
<span className={dash.error && dash.isLive ? "text-destructive" : undefined}>{dash.probesSubtitle}</span>
}
headerRight={
<Link href="/uptime" className={cn(buttonVariants({ variant: "outline", size: "sm" }), "h-7 text-xs")}>
Мониторинг →
</Link>
}
contentClassName="px-0 pb-0"
>
{dash.activeProbes.length === 0 && !dash.probesLoading ? (
<EmptyState
className="py-10"
title={dash.availability.probes ? "Нет проб на дашборде" : "Данные проб недоступны"}
description={dash.availability.probes ? "Включите пробу и отметьте ★ на странице мониторинга." : "Повторите обновление данных."}
action={
<Link href="/uptime" className={cn(buttonVariants({ variant: "outline", size: "sm" }))}>
Открыть мониторинг
</Link>
}
/>
) : (
<DashboardActiveProbesDataGrid
probes={dash.activeProbes}
catalog={dash.servers}
isLoading={dash.probesLoading}
emptyDescription="Нет проб с звездой на дашборде."
/>
)}
</OpsPanel>
<QuickActionGrid
title="Быстрые переходы"
description="Другие разделы управления сетью"
actions={[
{
id: "traffic",
title: "Трафик",
description: "Приём, передача и потоки IPFIX",
href: "/traffic",
icon: <ActivityIcon />,
iconClassName: "text-info",
},
{
id: "map",
title: "Карта сети",
description: "Топология GRE и WAN",
href: "/network-map",
icon: <MapIcon />,
iconClassName: "text-primary",
},
{
id: "filters",
title: "Фильтры",
description: "Правила обхода и списки",
href: "/filters",
icon: <FilterIcon />,
iconClassName: "text-info",
},
{
id: "firewall",
title: "Firewall",
description: "Цепочки filter / nat",
href: "/firewall",
icon: <ShieldIcon />,
iconClassName: "text-warning",
},
{
id: "backups",
title: "Бэкапы",
description: "Снапшоты и расписание",
href: "/backups",
icon: <HardDriveIcon />,
iconClassName: "text-muted-foreground",
},
{
id: "bgp",
title: "BGP",
description: "Сессии и префиксы",
href: "/bgp",
icon: <GitMergeIcon />,
iconClassName: "text-primary",
},
]}
/>
</div>
</div>
</div>
)
}