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

@@ -0,0 +1,140 @@
"use client"
import { useState } from "react"
import type { WireGuardPeer } from "@/lib/data"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
ArrowDownIcon,
ArrowUpIcon,
CodeXmlIcon,
KeyRoundIcon,
Trash2Icon,
} from "lucide-react"
function fmtBytes(n: number | undefined): string {
if (n === undefined || !Number.isFinite(n) || n < 0) return "—"
if (n >= 1_000_000_000) return `${(n / 1_000_000_000).toFixed(1)} ГБ`
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)} МБ`
if (n >= 1_000) return `${(n / 1_000).toFixed(0)} КБ`
return `${n} Б`
}
function truncKey(key: string): string {
if (key.length <= 20) return key
return `${key.slice(0, 8)}…${key.slice(-8)}`
}
function peerKey(peer: WireGuardPeer, index: number): string {
return peer.id ?? peer.rosId ?? peer.publicKey ?? String(index)
}
function WireGuardPeersDetail({
peers,
interfaceName,
onDeletePeer,
onExportPeer,
}: {
peers: WireGuardPeer[]
interfaceName: string
onDeletePeer?: (peerId: string) => void
onExportPeer?: (peerId: string) => void
}) {
const [page, setPage] = useState(0)
const lastPage = Math.max(0, Math.ceil(peers.length / 50) - 1)
const currentPage = Math.min(page, lastPage)
const offset = currentPage * 50
if (page > lastPage) setPage(lastPage)
if (peers.length === 0) {
return (
<div className="px-5 py-4 text-xs text-muted-foreground text-center border-t border-border/50">
Нет пиров
</div>
)
}
return (
<section aria-label={"Пиры: " + interfaceName} className="sticky left-0 w-full max-w-[calc(100vw-2rem)] border-t border-border/50 md:max-w-none">
<div className="flex flex-wrap items-center justify-between gap-2 px-4 py-2 bg-muted/10">
<p className="text-xs text-muted-foreground" aria-live="polite">Пиры {offset + 1}–{Math.min(offset + 50, peers.length)} из {peers.length}</p>
{peers.length > 50 && <div className="flex gap-1">
<Button type="button" variant="outline" className="min-h-11" disabled={currentPage === 0} aria-label="Предыдущая страница пиров" onClick={() => setPage(currentPage - 1)}>Назад</Button>
<Button type="button" variant="outline" className="min-h-11" disabled={currentPage >= lastPage} aria-label="Следующая страница пиров" onClick={() => setPage(currentPage + 1)}>Далее</Button>
</div>}
</div>
{peers.slice(offset, offset + 50).map((peer, index) => {
const id = peerKey(peer, offset + index)
return (
<div
key={id}
data-wireguard-peer={id}
className="grid grid-cols-2 gap-3 px-4 py-3 items-center text-xs border-t border-border/50 bg-muted/20 md:grid-cols-[1fr_1fr_auto_auto_auto_auto]"
>
<div className="flex items-center gap-1.5 min-w-0">
<KeyRoundIcon className="size-3 text-muted-foreground shrink-0" />
<span className="font-mono text-muted-foreground break-all" title={peer.publicKey}>
{peer.name || truncKey(peer.publicKey)}
</span>
</div>
<div className="min-w-0 break-all font-mono text-muted-foreground">
<span className="sr-only">Allowed IPs: </span><span>{peer.allowedIps.join(", ")}</span>
</div>
<span
className={cn(
"font-mono text-[11px] whitespace-nowrap",
"text-muted-foreground",
)}
>
<span className="sr-only">Последнее рукопожатие: </span><span>{peer.latestHandshake?.trim() || "Нет данных"}</span>
</span>
<div className="flex items-center gap-2 text-muted-foreground whitespace-nowrap">
<span aria-label="RX" className="flex items-center gap-0.5">
<ArrowDownIcon aria-hidden="true" className="size-3 text-success" />
{fmtBytes(peer.transferRx)}
</span>
<span aria-label="TX" className="flex items-center gap-0.5">
<ArrowUpIcon aria-hidden="true" className="size-3 text-info" />
{fmtBytes(peer.transferTx)}
</span>
</div>
<span className="min-w-0 break-all font-mono text-muted-foreground/60 text-[11px]"><span>Endpoint: </span>{peer.endpoint ?? "—"}</span>
<div className="flex items-center gap-1 justify-end">
{onExportPeer && (
<Button
type="button"
variant="ghost"
size="icon"
className="size-11"
aria-label="Экспорт peer .conf"
onClick={(e) => {
e.stopPropagation()
onExportPeer(id)
}}
>
<CodeXmlIcon className="size-3.5" />
</Button>
)}
{onDeletePeer && (
<Button
type="button"
variant="ghost"
size="icon"
className="size-11 text-destructive"
aria-label="Удалить пира"
onClick={(e) => {
e.stopPropagation()
onDeletePeer(id)
}}
>
<Trash2Icon className="size-3.5" />
</Button>
)}
</div>
</div>
)
})}
</section>
)
}
export { WireGuardPeersDetail, fmtBytes, truncKey }