diff --git a/app/(main)/uptime/page.tsx b/app/(main)/uptime/page.tsx index c93d1d10..3b2edddf 100644 --- a/app/(main)/uptime/page.tsx +++ b/app/(main)/uptime/page.tsx @@ -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("/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 : <>Пауза} -