fix(ui): сохранять каталог устройств диагностики при отказе

This commit is contained in:
Denozordec committed 2026-10-01 04:11:43 +07:00
1 parent 6fa8e5bfd1
commit cba3db1508
7 files changed
+78 -46

No files matched your search

+32 -43
View File
@@ -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<Server[]>([])
const [liveLoad, setLiveLoad] = useState<"idle" | "loading" | "error">(() => (isLive ? "loading" : "idle"))
const [catalogError, setCatalogError] = useState<string | null>(null)
const [catalogRevision, setCatalogRevision] = useState(0)
const [catalogReadAt, setCatalogReadAt] = useState<number | null>(null)
const [greByServer, setGreByServer] = useState<Record<string, GreTunnel[]>>({})
const [tool, setTool] = useState<DiagTool>("ping")
@@ -609,36 +600,31 @@ function ProbesContent() {
const [rosSrcV4, setRosSrcV4] = useState<string | null | undefined>(undefined)
useEffect(() => {
if (!isLive) {
queueMicrotask(() => setLiveLoad("idle"))
return
}
setLiveLoad("loading")
void apiFetch<BackendServerRow[]>("/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<unknown>("/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 (
<div className="flex flex-col h-full">
<PageHeader crumbs={[{ label: "Инструменты" }, { label: "Диагностика" }]} />
@@ -946,6 +932,7 @@ function ProbesContent() {
actions={
<>
<ServerRailMobileButton />
{isLive && <Button type="button" size="sm" variant="outline" aria-label="Обновить устройства диагностики" disabled={liveLoad === "loading"} onClick={() => setCatalogRevision(n => n + 1)}>Обновить</Button>}
{running.length > 0
? <Button variant="outline" size="sm" onClick={() => {
running.forEach(test => stopTest(test.id))
@@ -961,11 +948,13 @@ function ProbesContent() {
<div className="flex flex-col gap-4">
{isLive && liveLoad === "error" && (
<div className="rounded-lg border border-destructive/30 bg-destructive/10 px-4 py-2.5 text-xs text-destructive flex items-center gap-2">
<AlertCircleIcon className="size-3.5 shrink-0" />
Бекенд недоступен — переключитесь в режим «Демо» в настройках источника данных или проверьте URL API.
<div role="alert" aria-label="Каталог диагностики" className="rounded-lg border border-destructive/30 bg-destructive/10 px-4 py-2.5 text-sm text-destructive">
<p>{catalogReadAt === null ? "Устройства пока неизвестны." : "Не удалось обновить устройства. Последний список сохранён; запуск приостановлен."}</p>
<p>{catalogError}</p>
</div>
)}
{isLive && catalogReadAt !== null && <p role="status" className="text-xs text-muted-foreground">Каталог получен: {new Date(catalogReadAt).toLocaleTimeString("ru-RU")}{liveLoad === "loading" ? " · обновляется" : ""}</p>}
{isLive && liveLoad === "idle" && allServers.length === 0 && <p role="status">В каталоге нет устройств. Добавьте устройство в разделе «Серверы».</p>}
{isLive && (
<p className="text-xs text-muted-foreground">