fix(api): сохранять каталог speed-проб атомарно

This commit is contained in:
Denozordec committed 2026-10-04 10:41:03 +07:00
1 parent d4f2961f76
commit 908974302e
9 files changed
+204 -86

No files matched your search

+15 -24
View File
@@ -1,6 +1,6 @@
"use client"
import { speedHistoryResponseSchema, speedProbesResponseSchema } from "@mmapp/contracts"
import { speedHistoryResponseSchema, speedProbesResponseSchema, speedProbesWriteSchema, speedProbesSavedSchema } from "@mmapp/contracts"
import probeStyles from "./probe-rows.module.css"
@@ -1074,6 +1074,7 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
const [deleteTarget, setDeleteTarget] = useState<{ kind: "ping" | "speed"; id: string; label: string } | null>(null)
const draftProbeId = useRef("")
const [probeSaving, setProbeSaving] = useState(false)
const [probeSaveWarning, setProbeSaveWarning] = useState<string | null>(null)
const [probeSaveError, setProbeSaveError] = useState<string | null>(null)
const savingRef = useRef(false)
const saveContext = useRef(0)
@@ -1524,34 +1525,23 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
const persistSpeedProbes = useCallback(async (rows: SpeedProbeRow[]) => {
if (!liveApi) return
if (!speedCatalogReady) throw new Error("Каталог speed-проб не подтверждён. Обновите данные перед сохранением.")
await apiFetch("/api/uptime/speed-probes", {
method: "PUT",
signal: AbortSignal.timeout(30_000),
body: JSON.stringify({
probes: rows.map((p) => ({
id: p.id,
srcServerId: p.srcServerId,
dstServerId: p.dstServerId,
srcInterface: p.srcInterface || "",
dstInterface: p.dstInterface || "",
protocol: p.protocol,
direction: p.direction,
durationSec: Number.parseInt(p.durationSec, 10) || 10,
enabled: p.enabled,
lastRunAt: p.lastRunAt ?? null,
lastTxAvgMbps: p.lastTxAvgMbps ?? null,
lastRxAvgMbps: p.lastRxAvgMbps ?? null,
lastStatus: p.lastStatus ?? null,
lastError: p.lastError ?? null,
})),
}),
})
const payload = speedProbesWriteSchema.safeParse({ probes: rows.map(p => ({
id: p.id, srcServerId: p.srcServerId, dstServerId: p.dstServerId,
srcInterface: p.srcInterface || "", dstInterface: p.dstInterface || "",
protocol: p.protocol, direction: p.direction, durationSec: p.durationSec, enabled: p.enabled,
})) })
if (!payload.success) throw new Error("Проверьте speed-пробы: источник и назначение должны отличаться, длительность — целое число от 3 секунд.")
const saved = speedProbesSavedSchema.safeParse(await apiFetch<unknown>("/api/uptime/speed-probes", {
method: "PUT", signal: AbortSignal.timeout(30_000), body: JSON.stringify(payload.data),
}))
if (!saved.success) throw new Error("Ответ о сохранении speed-проб не подтверждён. Обновите список перед повторной попыткой.")
setProbeSaveWarning(saved.data.warning ?? null)
}, [apiFetch, liveApi, speedCatalogReady])
async function saveProbeForm(save: () => Promise<void>, confirm: () => void) {
if (!requireWrite()) return
savingRef.current = true
setProbeSaving(true); setProbeSaveError(null)
setProbeSaving(true); setProbeSaveError(null); setProbeSaveWarning(null)
overviewReqRef.current++
const context = ++saveContext.current
try {
@@ -2014,6 +2004,7 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
{!canWrite && <ContextAlert variant="info" role="status"><ContextTitle>Только просмотр</ContextTitle><ContextDescription>Изменение проб и запуск измерений требуют разрешения на запись. Обновление данных загружает сохранённые результаты.</ContextDescription></ContextAlert>}
{permissionError && !sheetOpen && !speedSheetOpen && !deleteTarget && <ContextAlert variant="destructive" role="alert"><ContextDescription>{permissionError}</ContextDescription></ContextAlert>}
{probeSaveWarning && <ContextAlert variant="warning" role="status"><ContextDescription>{probeSaveWarning}</ContextDescription></ContextAlert>}
{probeSaveError && !sheetOpen && !speedSheetOpen && !deleteTarget && <ContextAlert variant="destructive" role="alert"><ContextDescription>{probeSaveError}</ContextDescription></ContextAlert>}
{urlError && <ContextAlert variant="warning" role="alert"><ContextTitle>Параметры ссылки требуют внимания</ContextTitle><ContextDescription>{urlError}<Button type="button" variant="outline" size="sm" onClick={resetUrl}>Сбросить параметры мониторинга</Button></ContextDescription></ContextAlert>}