fix(ui): сериализовать ручной опрос и ограничить ожидание Speed-test

This commit is contained in:
Denozordec committed 2026-09-23 09:17:20 +07:00
1 parent cc40a5e33d
commit f7e0e20673
4 files changed
+121 -29

No files matched your search

+37 -27
View File
@@ -1128,8 +1128,9 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
/** Сбрасывает ответы устаревших GET /overview (гонка: ответ приходит после клика по ★ и затирает showOnDashboard). */
const overviewReqRef = useRef(0)
const loadLiveOverview = useCallback(async () => {
if (!liveApi || savingRef.current) return
const loadLiveOverview = useCallback(async (writeContext?: number) => {
if (!liveApi || (savingRef.current && writeContext !== saveContext.current)) return
const context = saveContext.current
const myReq = ++overviewReqRef.current
setOpError(null)
try {
@@ -1137,20 +1138,20 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
apiFetch<BackendServer[]>("/api/servers"),
apiFetch<{ probes: PingProbe[]; resources: ServerResource[] }>("/api/uptime/overview?range=1h"),
])
if (myReq !== overviewReqRef.current || savingRef.current) return
if (myReq !== overviewReqRef.current || context !== saveContext.current || (savingRef.current && writeContext !== context)) return
setAllServers(mapBackendServersToServers(serverRows))
setProbes(data.probes)
setResources(data.resources)
setOverviewState("ready")
} catch (e) {
if (myReq !== overviewReqRef.current || savingRef.current) return
if (myReq !== overviewReqRef.current || context !== saveContext.current || (savingRef.current && writeContext !== context)) return
setOpError(e instanceof Error ? e.message : "Не удалось загрузить uptime")
setOverviewState("error")
}
}, [apiFetch, liveApi])
const reloadSpeedData = useCallback(async () => {
if (!liveApi || savingRef.current) return
const reloadSpeedData = useCallback(async (writeContext?: number) => {
if (!liveApi || (savingRef.current && writeContext !== saveContext.current)) return
const context = saveContext.current
type RunRow = {
id: string
@@ -1177,7 +1178,7 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
apiFetch<{ probes: SpeedProbeRow[] }>("/api/uptime/speed-probes"),
apiFetch<{ runs: RunRow[] }>("/api/uptime/speed-test/runs"),
])
if (context !== saveContext.current || savingRef.current) return
if (context !== saveContext.current || (savingRef.current && writeContext !== context)) return
setSpeedProbes(sp.probes ?? [])
const rows = (runsRes.runs ?? []).map((r) => ({
id: r.id,
@@ -1202,31 +1203,37 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
}))
setSpeedRuns(rows)
} catch {
if (context !== saveContext.current || savingRef.current) return
if (context !== saveContext.current || (savingRef.current && writeContext !== context)) return
setSpeedProbes([])
setSpeedRuns([])
}
}, [apiFetch, liveApi])
const refreshUptimeLive = useCallback(async (opts?: { showSpinner?: boolean; pollDevices?: boolean }) => {
if (!liveApi) return
if (opts?.pollDevices && !requireWrite()) return
if (!liveApi || savingRef.current) return
const collect = opts?.pollDevices === true
if (collect && !requireWrite()) return
if (collect) {
savingRef.current = true; setProbeSaving(true)
overviewReqRef.current++; saveContext.current++
}
const context = saveContext.current
if (opts?.showSpinner) setUptimeRefreshBusy(true)
let collectErr: string | null = null
try {
void checkBackend()
if (opts?.pollDevices) {
if (collect) {
try {
await apiFetch<{ ok: boolean; lastError?: string | null }>("/api/uptime/collect-now", { method: "POST" })
} catch (e) {
collectErr = e instanceof Error ? e.message : "Не удалось опросить устройства (collect-now)"
collectErr = serverOperationError(e)
}
}
await Promise.all([loadLiveOverview(), reloadSpeedData()])
if (collectErr) {
setOpError((prev) => (prev ? `${prev} · ${collectErr}` : collectErr))
}
if (context !== saveContext.current) return
await Promise.all([loadLiveOverview(collect ? context : undefined), reloadSpeedData(collect ? context : undefined)])
if (context === saveContext.current && collectErr) setOpError(collectErr)
} finally {
if (collect) { savingRef.current = false; setProbeSaving(false) }
if (opts?.showSpinner) setUptimeRefreshBusy(false)
}
}, [liveApi, loadLiveOverview, reloadSpeedData, checkBackend, apiFetch, requireWrite])
@@ -1234,23 +1241,24 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
const refreshProbeGroupPings = useCallback(
async (srvId: string, group: { name: string; target: string; probes: PingProbe[] }) => {
if (!liveApi || !requireWrite()) return
savingRef.current = true; setProbeSaving(true)
overviewReqRef.current++
const context = ++saveContext.current
const key = probeGroupActionKey(srvId, group)
setProbeGroupPingBusy((m) => ({ ...m, [key]: true }))
setProbeGroupPingBusy(m => ({ ...m, [key]: true }))
setOpError(null)
try {
await apiFetch<{ ok: boolean; polled: number }>("/api/uptime/probes/collect-group", {
method: "POST",
body: JSON.stringify({ probeIds: group.probes.map((p) => p.id) }),
body: JSON.stringify({ probeIds: group.probes.map(p => p.id) }),
})
await loadLiveOverview()
if (context !== saveContext.current) return
await loadLiveOverview(context)
} catch (e) {
setOpError(e instanceof Error ? e.message : "Не удалось выполнить ping группы")
if (context === saveContext.current) setOpError(serverOperationError(e))
} finally {
setProbeGroupPingBusy((m) => {
const next = { ...m }
delete next[key]
return next
})
savingRef.current = false; setProbeSaving(false)
setProbeGroupPingBusy(m => { const next = { ...m }; delete next[key]; return next })
}
},
[liveApi, apiFetch, loadLiveOverview, requireWrite],
@@ -1773,6 +1781,8 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
}
}>("/api/uptime/speed-test", {
method: "POST",
// Includes interface lookups, bandwidth-test, post-test pause and Ping.
signal: AbortSignal.timeout(120_000),
body: JSON.stringify(payload),
})
if (context !== saveContext.current) return
@@ -1850,7 +1860,7 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
}
} catch (e) {
if (context !== saveContext.current) return
const message = e instanceof Error ? e.message : "Не удалось выполнить speed test"
const message = serverOperationError(e)
setSpeedRuns((prev) => prev.map((r) => r.id === runId ? ({
...r,
status: "error",
@@ -1983,7 +1993,7 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
: <><PauseIcon className="size-4" />Пауза</>}
</Button>
<Button variant="outline" size="sm" disabled={liveApi && uptimeRefreshBusy} onClick={() => {
<Button variant="outline" size="sm" disabled={probeSaving || (liveApi && uptimeRefreshBusy)} onClick={() => {
if (liveApi) {
void refreshUptimeLive({ showSpinner: true, pollDevices: can("mm:uptime:write") && selectedProbeId === null && (tab === "resources" || tab === "probes") })
return