From 6fa8e5bfd17d5ad6e971d36a9c50b40cc1a90b9c Mon Sep 17 00:00:00 2001 From: Denozordec Date: Thu, 1 Oct 2026 04:07:08 +0700 Subject: [PATCH] =?UTF-8?q?fix(ui):=20=D1=80=D0=B0=D0=B7=D0=BB=D0=B8=D1=87?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D1=80=D0=B5=D0=B7=D1=83=D0=BB=D1=8C=D1=82?= =?UTF-8?q?=D0=B0=D1=82=20=D0=B8=20=D0=BE=D1=82=D0=BC=D0=B5=D0=BD=D1=83=20?= =?UTF-8?q?=D0=B4=D0=B8=D0=B0=D0=B3=D0=BD=D0=BE=D1=81=D1=82=D0=B8=D0=BA?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(main)/probes/page.tsx | 60 ++++++++++++++++++++--------- docs/ux-ui-progress.md | 10 ++++- docs/ux-ui-stage-10.md | 9 +++++ lib/probe-response.test.ts | 5 +++ lib/probe-response.ts | 8 ++++ playwright.config.ts | 2 +- playwright.probes-context.config.ts | 8 ++++ tests/e2e/probes-context.spec.ts | 15 ++++++++ tests/e2e/probes-data.spec.ts | 35 +++++++++++++++++ 9 files changed, 132 insertions(+), 20 deletions(-) create mode 100644 lib/probe-response.test.ts create mode 100644 lib/probe-response.ts create mode 100644 playwright.probes-context.config.ts create mode 100644 tests/e2e/probes-context.spec.ts create mode 100644 tests/e2e/probes-data.spec.ts diff --git a/app/(main)/probes/page.tsx b/app/(main)/probes/page.tsx index bbdbf709..9b762bb7 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 { readProbeOutput } from "@/lib/probe-response" import { useDataSource } from "@/lib/data-source" import { PlayIcon, SquareIcon, CopyIcon, Trash2Icon, PlusIcon, @@ -33,7 +34,7 @@ import { requestJson } from "@/shared/api/http-client" // ─── types ──────────────────────────────────────────────────────────────────── type DiagTool = "ping" | "traceroute" | "bandwidth" | "dns" | "route" | "mtu" -type RunStatus = "running" | "done" | "error" +type RunStatus = "running" | "done" | "error" | "cancelled" type TraceProto = "icmp" | "udp" | "tcp" type DnsType = "A" | "AAAA" | "MX" | "NS" | "TXT" | "CNAME" | "PTR" @@ -576,6 +577,12 @@ function ScheduleTab({ // ─── page ───────────────────────────────────────────────────────────────────── export default function ProbesPage() { + const { mode, backendUrl, prefsHydrated } = useDataSource() + if (!prefsHydrated) return

Загрузка настроек диагностики…

+ return +} + +function ProbesContent() { const { mode, backendUrl } = useDataSource() const isLive = mode === "live" const apiFetch = useMemo(() => makeApiFetch(backendUrl), [backendUrl]) @@ -716,6 +723,13 @@ export default function ProbesPage() { /** Живой POST /probes/run — отмена через fetch AbortSignal */ const liveProbeRunRef = useRef<{ testId: string; ctrl: AbortController } | null>(null) + const runLock = useRef(false) + const alive = useRef(true) + useEffect(() => { + alive.current = true + return () => { alive.current = false; liveProbeRunRef.current?.ctrl.abort() } + }, []) + const bwTunnels = tunnelsForSrc const srcServer = useMemo(() => allServers.find(s => s.id === srcId), [allServers, srcId]) const bwTun = useMemo(() => bwTunnels.find(t => t.id === bwTunId), [bwTunnels, bwTunId]) @@ -748,15 +762,18 @@ export default function ProbesPage() { // ── run test ── const runTest = useCallback(async () => { const srv = allServers.find(s => s.id === srcId) - if (!srv) return + if (!srv || !srv.enabled || runLock.current || (isLive && liveLoad !== "idle")) return const id = String(nextId.current++) const numericServerId = Number.parseInt(srcId, 10) if (isLive) { if (!Number.isFinite(numericServerId)) return + runLock.current = true setRunBusy(true) const abortCtrl = new AbortController() liveProbeRunRef.current = { testId: id, ctrl: abortCtrl } + let timedOut = false + const timeout = setTimeout(() => { timedOut = true; abortCtrl.abort() }, 60_000) setTests(p => [{ id, tool, @@ -793,32 +810,34 @@ export default function ProbesPage() { bwProto, bwDuration, } - const res = await apiFetch<{ output: string }>(`/api/servers/${numericServerId}/probes/run`, { + const res = await apiFetch(`/api/servers/${numericServerId}/probes/run`, { method: "POST", body: JSON.stringify(body), signal: abortCtrl.signal, }) - const lines = parseProbeOutput(res.output ?? "") - const looksErr = (res.output ?? "").trim().toLowerCase().startsWith("error:") + if (!alive.current || abortCtrl.signal.aborted) return + const output = readProbeOutput(res) + const lines = parseProbeOutput(output || "Запрос выполнен: API вернул пустой вывод.") setTests(p => p.map(t => (t.id === id ? { ...t, lines, totalLines: lines.length, - status: looksErr ? "error" : "done", + status: "done", } : t))) } catch (e) { - const aborted = + if (!alive.current) return + const aborted = abortCtrl.signal.aborted || (typeof DOMException !== "undefined" && e instanceof DOMException && e.name === "AbortError") || (e instanceof Error && e.name === "AbortError") if (aborted) { setTests(p => p.map(t => (t.id === id ? { ...t, - lines: [{ text: "Запрос отменён (стоп или закрытие запроса).", kind: "dim" }], + lines: [{ text: timedOut ? "Время ожидания истекло. Результат на устройстве не подтверждён; повтор выполняется вручную." : "Ожидание отменено. Остановка на устройстве не подтверждена.", kind: timedOut ? "err" : "dim" }], totalLines: 1, - status: "done", + status: timedOut ? "error" : "cancelled", } : t))) } else { @@ -833,8 +852,10 @@ export default function ProbesPage() { : t))) } } finally { + clearTimeout(timeout) if (liveProbeRunRef.current?.testId === id) liveProbeRunRef.current = null - setRunBusy(false) + runLock.current = false + if (alive.current) setRunBusy(false) } return } @@ -867,6 +888,7 @@ export default function ProbesPage() { setTab("history") }, [ isLive, + liveLoad, allServers, srcId, tool, @@ -887,12 +909,15 @@ export default function ProbesPage() { ]) const stopTest = (id: string) => { - if (liveProbeRunRef.current?.testId === id) liveProbeRunRef.current.ctrl.abort() + if (liveProbeRunRef.current?.testId === id) { liveProbeRunRef.current.ctrl.abort(); return } setTests(p => p.map(t => (t.id === id && t.status === "running" - ? { ...t, status: "done" as const, totalLines: t.lines.length } + ? { ...t, status: "cancelled" as const, totalLines: t.lines.length } : t))) } - const clearTest = (id: string) => setTests(p => p.filter(t => t.id !== id)) + const clearTest = (id: string) => { + if (liveProbeRunRef.current?.testId === id) liveProbeRunRef.current.ctrl.abort() + setTests(p => p.filter(t => t.id !== id)) + } const running = tests.filter(t => t.status === "running") @@ -923,10 +948,7 @@ export default function ProbesPage() { {running.length > 0 ? @@ -984,6 +1006,7 @@ export default function ProbesPage() { setTarget(e.target.value)} placeholder={tool === "dns" ? "google.com" : tool === "route" ? "8.8.8.8" : "8.8.8.8"} + aria-label={tool === "dns" ? "Домен" : "Цель диагностики"} className="font-mono text-sm" onKeyDown={e => e.key === "Enter" && runTest()} /> @@ -1066,7 +1089,7 @@ export default function ProbesPage() {