From cba3db1508b33666762e5fbda3dba65daff0c31e Mon Sep 17 00:00:00 2001 From: Denozordec Date: Thu, 1 Oct 2026 04:11:43 +0700 Subject: [PATCH] =?UTF-8?q?fix(ui):=20=D1=81=D0=BE=D1=85=D1=80=D0=B0=D0=BD?= =?UTF-8?q?=D1=8F=D1=82=D1=8C=20=D0=BA=D0=B0=D1=82=D0=B0=D0=BB=D0=BE=D0=B3?= =?UTF-8?q?=20=D1=83=D1=81=D1=82=D1=80=D0=BE=D0=B9=D1=81=D1=82=D0=B2=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=20=D0=BF=D1=80=D0=B8=20=D0=BE=D1=82=D0=BA=D0=B0=D0=B7?= =?UTF-8?q?=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(main)/probes/page.tsx | 75 ++++++++++++++------------------ docs/ux-ui-progress.md | 9 +++- docs/ux-ui-stage-10.md | 8 ++++ lib/probe-response.test.ts | 3 ++ lib/probe-response.ts | 7 +++ tests/e2e/probes-context.spec.ts | 2 +- tests/e2e/probes-data.spec.ts | 20 ++++++++- 7 files changed, 78 insertions(+), 46 deletions(-) diff --git a/app/(main)/probes/page.tsx b/app/(main)/probes/page.tsx index 9b762bb7..c9003651 100644 --- a/app/(main)/probes/page.tsx +++ b/app/(main)/probes/page.tsx @@ -20,7 +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 { readProbeOutput, probeServersSchema } from "@/lib/probe-response" import { useDataSource } from "@/lib/data-source" import { PlayIcon, SquareIcon, CopyIcon, Trash2Icon, PlusIcon, @@ -94,18 +94,6 @@ function parseProbeOutput(text: string): OutputLine[] { return text.split("\n").map(t => ({ text: t || " ", kind: inferProbeLineKind(t) })) } -interface BackendServerRow { - id: number - name: string - host: string - site: string - country: string - asn: string - type: Server["type"] - enabled: boolean - status?: Server["status"] - latency?: number | null -} // SpeedProbeApiRow imported from probes-speed-probes-data-grid @@ -589,6 +577,9 @@ function ProbesContent() { const [liveServers, setLiveServers] = useState([]) const [liveLoad, setLiveLoad] = useState<"idle" | "loading" | "error">(() => (isLive ? "loading" : "idle")) + const [catalogError, setCatalogError] = useState(null) + const [catalogRevision, setCatalogRevision] = useState(0) + const [catalogReadAt, setCatalogReadAt] = useState(null) const [greByServer, setGreByServer] = useState>({}) const [tool, setTool] = useState("ping") @@ -609,36 +600,31 @@ function ProbesContent() { const [rosSrcV4, setRosSrcV4] = useState(undefined) useEffect(() => { - if (!isLive) { - queueMicrotask(() => setLiveLoad("idle")) - return - } - setLiveLoad("loading") - void apiFetch("/api/servers") - .then(rows => { - const mapped: Server[] = rows.map(s => ({ - id: String(s.id), - name: s.name || s.host, - host: s.host, - model: "—", - os: "—", - site: s.site, - country: s.country || "UN", - asn: s.asn, - type: s.type, - enabled: s.enabled, - status: (s.status ?? "offline") as Server["status"], - latency: s.latency != null ? Math.round(s.latency) : null, - sessions: 0, - })) - setLiveServers(mapped) + if (!isLive) return + let cancelled = false + const ctrl = new AbortController() + queueMicrotask(() => { if (!cancelled) { setLiveLoad("loading"); setCatalogError(null) } }) + void apiFetch("/api/servers", { signal: ctrl.signal }) + .then(raw => { + const result = probeServersSchema.safeParse(raw) + if (!result.success) throw new Error("Некорректный ответ API: каталог устройств не подтверждён.") + if (cancelled) return + setLiveServers(result.data.map(s => ({ + id: String(s.id), name: s.name || s.host, host: s.host, model: "—", os: "—", + site: s.site ?? "", country: s.country ?? "UN", asn: s.asn ?? "", type: s.type, + enabled: s.enabled, status: s.status ?? "unknown", + latency: s.latency != null ? Math.round(s.latency) : null, sessions: 0, + }))) + setCatalogReadAt(Date.now()) setLiveLoad("idle") }) - .catch(() => { - setLiveServers([]) + .catch(error => { + if (cancelled) return + setCatalogError(error instanceof Error ? error.message : "Не удалось прочитать устройства.") setLiveLoad("error") }) - }, [isLive, apiFetch]) + return () => { cancelled = true; ctrl.abort() } + }, [isLive, apiFetch, catalogRevision]) const allServers = useMemo(() => { if (!isLive) return servers @@ -921,7 +907,7 @@ function ProbesContent() { const running = tests.filter(t => t.status === "running") - if (isLive && liveLoad === "loading") { + if (isLive && liveLoad === "loading" && catalogReadAt === null) { return (
@@ -946,6 +932,7 @@ function ProbesContent() { actions={ <> + {isLive && } {running.length > 0 ?