fix(backups): сохранять неизвестный результат восстановления

This commit is contained in:
Denozordec committed 2026-10-01 16:32:06 +07:00
1 parent 39570656c9
commit 6f152c8dfe
11 files changed
+159 -11

No files matched your search

+26 -3
View File
@@ -37,6 +37,9 @@ import {
} from "@/shared/api/backups"
import { ApiClientError, requestJson } from "@/shared/api/http-client"
import { toast } from "sonner"
import { useBackupRestoreGuard } from "@/lib/use-backup-restore-guard"
import { restoreWasNotApplied } from "@/lib/backup-restore-checkpoint"
import { Checkbox } from "@/components/ui/checkbox"
import { useBackupJob } from "@/lib/use-backup-job"
import { backupJobOutcome } from "@/lib/backup-job-poll"
import { backupSchedulePayload, backupStoragePayload } from "@/lib/backup-settings"
@@ -91,6 +94,8 @@ function BackupsContent() {
const [liveServers, setLiveServers] = useState<Server[]>(isLive ? [] : mockServers)
const baseCanCreate = canWrite && canReadServers && liveServers.length > 0 && current("servers") && current("storage")
const [loading, setLoading] = useState(false)
const restoreGuard = useBackupRestoreGuard(backendUrl, isLive)
const [restoreReviewed, setRestoreReviewed] = useState(false)
const [opBusy, setOpBusy] = useState(false)
const [opError, setOpError] = useState<string | null>(null)
const [scheduleSaveBusy, setScheduleSaveBusy] = useState(false)
@@ -292,15 +297,23 @@ function BackupsContent() {
finally { finishOperation(abort) }
}
async function handleRestore() {
if (!restoreTarget || !restoreTarget.serverId) return
if (!restoreTarget || !restoreTarget.serverId || !restoreGuard.canStart) return
const target = restoreTarget, abort = startOperation(); if (!abort) return
let sent = false
try {
if (!restoreGuard.begin({ backupId: target.id, serverId: Number(target.serverId), filename: target.filename, serverName: target.server })) return
sent = true
const result = await restoreBackup(backendUrl, target.id, { signal: abort.signal }, Number(target.serverId))
if (!alive.current || abort.signal.aborted) return
if (result.filename !== target.filename) throw new Error("Ответ восстановления относится к другому файлу")
toast.success("API подтвердил импорт " + result.filename + " на " + result.serverName)
setRestoreTarget(null)
} catch (error) { if (alive.current && !abort.signal.aborted) setOpError(userApiError(error)) }
restoreGuard.clear(); setRestoreTarget(null)
} catch (error) {
if (alive.current && !abort.signal.aborted) {
if (sent && restoreWasNotApplied(error)) restoreGuard.clear()
setOpError(userApiError(error) + (sent && !restoreWasNotApplied(error) ? ". Результат импорта неизвестен; проверьте роутер перед повтором" : ""))
}
}
finally { finishOperation(abort) }
}
async function handleDownload(id: string, filename: string) {
@@ -399,6 +412,14 @@ function BackupsContent() {
<AlertDescription>{({ backups: "История", servers: "Серверы", schedule: "Расписание", storage: "Хранилище" })[resource]}: {resources[resource].error}</AlertDescription>
</Alert>)}
{isLive && (restoreGuard.checkpoint || restoreGuard.error) && <Alert variant="warning" aria-label="Подтверждение восстановления">
<AlertTitle>{opBusy && restoreTarget ? "Импорт ожидает подтверждения" : "Проверьте результат восстановления"}</AlertTitle>
<AlertDescription className="flex flex-col gap-3">
<span>{restoreGuard.error || (restoreGuard.checkpoint ? restoreGuard.checkpoint.filename + " → " + restoreGuard.checkpoint.serverName + ". Ответ ещё не подтверждён. Закрытие страницы не отменяет импорт." : "")}</span>
<label className="flex items-start gap-2"><Checkbox checked={restoreReviewed} disabled={opBusy || !canWrite} onCheckedChange={value => setRestoreReviewed(value === true)} />Я проверил состояние роутера и понимаю последствия повторного импорта</label>
<Button type="button" variant="outline" disabled={!restoreReviewed || opBusy || !canWrite} onClick={() => { if (restoreGuard.clear()) { setRestoreReviewed(false); setOpError(null) } }}>Разрешить новое восстановление</Button>
</AlertDescription>
</Alert>}
{opError && <Alert variant="warning" aria-label="Операция резервных копий"><AlertTitle>Операция не подтверждена</AlertTitle><AlertDescription>{opError}</AlertDescription></Alert>}
{(job.error || job.storageError) && <Alert variant="warning" aria-label="Задача резервного копирования"><AlertTitle>{jobTerminal ? "Задача завершена с ограничениями" : "Результат задачи пока неизвестен"}</AlertTitle><AlertDescription>{job.error || job.storageError}</AlertDescription></Alert>}
<KpiStatGrid
@@ -476,6 +497,7 @@ function BackupsContent() {
onCreate={() => openCreate(false)}
canCreate={canCreate}
canWrite={canWrite && !opBusy && !scheduleSaveBusy && !backupJobId}
canRestore={restoreGuard.canStart}
canDownload={isLive && canRead}
/>}
</TabsContent>
@@ -562,6 +584,7 @@ function BackupsContent() {
<BackupRestoreDialog
backup={restoreTarget}
busy={opBusy}
blocked={!restoreGuard.canStart}
error={opError}
onConfirm={() => void handleRestore()}
onCancel={() => setRestoreTarget(null)}