From 155dfc5716d13e078dce4da7912be0bb9b3e666e Mon Sep 17 00:00:00 2001 From: Denozordec Date: Thu, 1 Oct 2026 04:20:19 +0700 Subject: [PATCH] =?UTF-8?q?fix(ui):=20=D0=BF=D0=BE=D0=B4=D1=82=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D0=B6=D0=B4=D0=B0=D1=82=D1=8C=20GRE=20=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=B4=20=D1=82=D0=B5=D1=81=D1=82=D0=BE=D0=BC=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=BF=D1=83=D1=81=D0=BA=D0=BD=D0=BE=D0=B9=20=D1=81?= =?UTF-8?q?=D0=BF=D0=BE=D1=81=D0=BE=D0=B1=D0=BD=D0=BE=D1=81=D1=82=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(main)/probes/page.tsx | 50 +++++++++++++++++++++++------------ docs/ux-ui-progress.md | 10 ++++++- docs/ux-ui-stage-10.md | 9 +++++++ tests/e2e/probes-data.spec.ts | 24 +++++++++++++++++ 4 files changed, 75 insertions(+), 18 deletions(-) diff --git a/app/(main)/probes/page.tsx b/app/(main)/probes/page.tsx index a6ff87c8..41cc5e96 100644 --- a/app/(main)/probes/page.tsx +++ b/app/(main)/probes/page.tsx @@ -20,6 +20,7 @@ import { Input } from "@/components/ui/input" import { ServerRailLayout, ServerRailMobileButton } from "@/components/server-rail-layout" import { type ServerTileItem } from "@/components/server-tile-rail" import { servers, greTunnels, type GreTunnel, type Server } from "@/lib/data" +import { filtersGreResponseSchema } from "@/lib/filters-response" import { can } from "@/lib/auth" import { readProbeOutput, probeServersSchema } from "@/lib/probe-response" import { useDataSource } from "@/lib/data-source" @@ -584,6 +585,8 @@ function ProbesContent() { const [catalogError, setCatalogError] = useState(null) const [catalogRevision, setCatalogRevision] = useState(0) const [catalogReadAt, setCatalogReadAt] = useState(null) + const [greState, setGreState] = useState<{ id: string; state: "loading" | "ready" | "error"; error?: string } | null>(null) + const [greRevision, setGreRevision] = useState(0) const [greByServer, setGreByServer] = useState>({}) const [tool, setTool] = useState("ping") @@ -671,22 +674,25 @@ function ProbesContent() { return () => { cancelled = true } }, [isLive, srcId, apiFetch]) - const ensureGre = useCallback(async (serverId: string) => { - if (!isLive || !canReadGre) return - try { - const d = await apiFetch<{ tunnels: GreTunnel[] }>( - `/api/filters/gre-tunnels?serverId=${encodeURIComponent(serverId)}`, - ) - setGreByServer(prev => ({ ...prev, [serverId]: d.tunnels })) - } catch { - setGreByServer(prev => ({ ...prev, [serverId]: [] })) - } - }, [isLive, canReadGre, apiFetch]) - useEffect(() => { - if (!isLive || !srcId) return - void ensureGre(srcId) - }, [isLive, srcId, ensureGre]) + if (!isLive || !canReadGre || liveLoad !== "idle" || !allServers.some(s => s.id === srcId && s.enabled)) return + let cancelled = false + const ctrl = new AbortController() + queueMicrotask(() => { if (!cancelled) setGreState({ id: srcId, state: "loading" }) }) + void apiFetch("/api/filters/gre-tunnels?serverId=" + encodeURIComponent(srcId), { signal: ctrl.signal }) + .then(raw => { + const result = filtersGreResponseSchema.safeParse(raw) + if (!result.success || result.data.tunnels.some(tunnel => tunnel.serverId !== srcId)) throw new Error("Некорректный ответ API: GRE выбранного устройства не подтверждены.") + if (cancelled) return + setGreByServer(prev => ({ ...prev, [srcId]: result.data.tunnels })) + setGreState({ id: srcId, state: "ready" }) + }) + .catch(error => { + if (cancelled) return + setGreState({ id: srcId, state: "error", error: error instanceof Error ? error.message : "Не удалось прочитать GRE." }) + }) + return () => { cancelled = true; ctrl.abort() } + }, [isLive, canReadGre, liveLoad, allServers, srcId, apiFetch, greRevision]) const tunnelsForSrc = useMemo(() => { if (isLive) return greByServer[srcId] ?? [] @@ -722,6 +728,7 @@ function ProbesContent() { const bwTunnels = tunnelsForSrc const srcServer = useMemo(() => allServers.find(s => s.id === srcId), [allServers, srcId]) + const greReady = !isLive || (greState?.id === srcId && greState.state === "ready") const bwTun = useMemo(() => bwTunnels.find(t => t.id === bwTunId), [bwTunnels, bwTunId]) // current command preview @@ -752,7 +759,7 @@ function ProbesContent() { // ── run test ── const runTest = useCallback(async () => { const srv = allServers.find(s => s.id === srcId) - if (!canRun || (tool === "bandwidth" && isLive && !canReadGre) || !srv || !srv.enabled || runLock.current || (isLive && liveLoad !== "idle")) return + if (!canRun || (tool === "bandwidth" && (!bwTun?.enabled || !bwTun.remoteAddress.trim() || !greReady || (isLive && !canReadGre))) || !srv || !srv.enabled || runLock.current || (isLive && liveLoad !== "idle")) return const id = String(nextId.current++) const numericServerId = Number.parseInt(srcId, 10) @@ -881,6 +888,7 @@ function ProbesContent() { liveLoad, canRun, canReadGre, + greReady, allServers, srcId, tool, @@ -973,6 +981,14 @@ function ProbesContent() { {!canRun &&

Для запуска диагностики требуется mm:servers:write.

} {isLive && tool === "bandwidth" && !canReadGre &&

Для выбора GRE требуется mm:filters:read.

} + {isLive && tool === "bandwidth" && canReadGre && ( +
+ {greState?.id === srcId && greState.state === "error" ?

GRE недоступны. Последний список сохранён; BW-тест приостановлен. {greState.error}

+ : !greReady ?

GRE выбранного устройства загружаются…

+ : !bwTunnels.length ?

У выбранного устройства нет GRE-туннелей.

: !bwTun?.enabled || !bwTun.remoteAddress.trim() ?

Выберите включённый GRE с адресом назначения.

: null} +
+ )} + {/* ── tool selector + config ── */} @@ -1089,7 +1105,7 @@ function ProbesContent() {