"use client" import { useCallback, useEffect, useRef, useState } from "react" import { can } from "@/lib/auth" import { Button } from "@/components/ui/button" import { Alert, AlertTitle, AlertDescription } from "@/components/reui/alert" import { AlertCircleIcon } from "lucide-react" import { getTrafficFlowHostStatus, reconcileTrafficFlowHost } from "@/shared/api/traffic-flow" import type { TrafficFlowHostStatus } from "@mmapp/contracts/traffic-flow" export function FlowHostPanel({ backendUrl, enabled = true, onConfigured }: { backendUrl: string; enabled?: boolean; onConfigured?: (configured: boolean) => void }) { const [status, setStatus] = useState(null) const [receivedAt, setReceivedAt] = useState(null) const [error, setError] = useState(null) const [operationError, setOperationError] = useState(null) const [busy, setBusy] = useState(false) const operation = useRef(null) const [revision, setRevision] = useState(0) const accept = useCallback((value: TrafficFlowHostStatus) => { setStatus(value); setReceivedAt(Date.now()); setError(null); onConfigured?.(value.configured) }, [onConfigured]) useEffect(() => () => { operation.current?.abort() }, []) useEffect(() => { if (!enabled || !can("mm:traffic:read") || busy) return const controller = new AbortController() let timer: ReturnType | undefined async function refresh() { try { const value = await getTrafficFlowHostStatus(backendUrl, { signal: controller.signal }); if (!controller.signal.aborted) accept(value) } catch (reason) { if (!controller.signal.aborted) setError(reason instanceof Error ? reason.message : "Статус хоста не получен") } finally { if (!controller.signal.aborted) timer = setTimeout(() => { void refresh() }, 10_000) } } void refresh() return () => { controller.abort(); if (timer) clearTimeout(timer) } }, [accept, backendUrl, enabled, busy, revision]) async function apply() { if (operation.current) return const controller = new AbortController(); operation.current = controller setBusy(true); setOperationError(null) try { const value = await reconcileTrafficFlowHost(backendUrl, { signal: controller.signal }); if (!controller.signal.aborted) accept(value) } catch (reason) { if (!controller.signal.aborted) setOperationError(reason instanceof Error ? reason.message : "Применение не подтверждено; обновите состояние хоста") } finally { operation.current = null; if (!controller.signal.aborted) setBusy(false) } } if (!enabled || !can("mm:traffic:read")) return null return

WireGuard на сервере MM

{!status ? "Получение состояния агента…" : !status.configured ? "Ручной режим. Установите агент из раздела «Файлы хоста»." : !status.reachable ? "Агент недоступен" : status.configMatches ? "Настройки применены" : "Настройки требуют проверки или применения"}

{error && receivedAt ?

Показано последнее состояние: {new Date(receivedAt).toLocaleString("ru-RU")}

: null} {status?.configured && status.reachable ? <>

Интерфейс: {status.interfaceUp ? status.addresses.join(", ") : "не поднят"} · UDP {status.listenPort}

    {status.peers.map(peer =>
  • {peer.allowedIps.join(", ") || "Без адреса"} · {peer.lastHandshakeAt ? "Handshake: " + new Date(peer.lastHandshakeAt * 1000).toLocaleString("ru-RU") + ((receivedAt ?? 0) / 1000 - peer.lastHandshakeAt > 180 ? " (давно)" : "") : "Ожидание handshake"}
  • )}
: null} {operationError || error || status?.error ? Состояние хоста требует внимания{operationError || error || status?.error} : null}
{status?.configured && can("mm:traffic:write") ? : null}
}