fix(backups): продолжать наблюдение без повторного создания

This commit is contained in:
Denozordec committed 2026-10-01 15:58:47 +07:00
1 parent 4aee2a34f1
commit 714af7e872
9 files changed
+265 -74

No files matched your search

+32 -66
View File
@@ -23,9 +23,7 @@ import { can } from "@/lib/auth"
import { userApiError } from "@/shared/api/users"
import { toFrontendServer } from "@/entities/server/model/mappers"
import {
createBackupsAsync,
deleteBackup,
getBackupJob,
getBackupScheduleSettings,
getBackupStorageSettings,
listBackups,
@@ -38,6 +36,8 @@ import {
} from "@/shared/api/backups"
import { ApiClientError, requestJson, requestBlob } from "@/shared/api/http-client"
import { toast } from "sonner"
import { useBackupJob } from "@/lib/use-backup-job"
import { backupJobOutcome } from "@/lib/backup-job-poll"
import { backupSchedulePayload, backupStoragePayload } from "@/lib/backup-settings"
import { BackupsHistory } from "@/components/backups/backups-history"
import {
@@ -85,11 +85,10 @@ function BackupsContent() {
const [search, setSearch] = useState("")
const [advancedFilters, setAdvancedFilters] = useState<FilterQuery>(() => createFilterQuery())
const [liveServers, setLiveServers] = useState<Server[]>(isLive ? [] : mockServers)
const canCreate = canWrite && canReadServers && liveServers.length > 0 && current("servers") && current("storage")
const baseCanCreate = canWrite && canReadServers && liveServers.length > 0 && current("servers") && current("storage")
const [loading, setLoading] = useState(false)
const [opBusy, setOpBusy] = useState(false)
const [opError, setOpError] = useState<string | null>(null)
const [backupJobId, setBackupJobId] = useState<string | null>(null)
const [scheduleSaveBusy, setScheduleSaveBusy] = useState(false)
const [testBusy, setTestBusy] = useState(false)
const [syncBusy, setSyncBusy] = useState(false)
@@ -165,6 +164,12 @@ function BackupsContent() {
return () => { alive.current = false; reader.current?.abort(); settingsController.current?.abort() }
}, [loadLive])
const job = useBackupJob(backendUrl, isLive, canRead, baseCanCreate && !opBusy && !scheduleSaveBusy, loadLive)
const checkpoint = job.checkpoint
const jobTerminal = !!checkpoint?.job && ["done", "failed"].includes(checkpoint.job.status)
const backupJobId = checkpoint && !jobTerminal ? checkpoint.request.requestId : null
const canCreate = baseCanCreate && job.hydrated && !checkpoint && !job.storageError
function startSettingsMutation() {
if (!alive.current || settingsLock.current || opBusy || backupJobId) return null
settingsLock.current = true
@@ -246,60 +251,12 @@ function BackupsContent() {
}
async function handleManualBackup() {
if (!canCreate || manualServers.size === 0 || opBusy) return
setOpError(null)
try {
const res = await createBackupsAsync(backendUrl, {
serverIds: [...manualServers],
notes: manualNotes || undefined,
})
setBackupJobId(res.jobId)
setManualOpen(false)
setManualServers(new Set())
setManualNotes("")
setTab("history")
} catch (e) {
setOpError(e instanceof Error ? e.message : "Ошибка создания бэкапа")
}
if (checkpoint) { await job.run(); return }
if (!canCreate || !manualServers.size || settingsLock.current || opBusy) return
if ([...manualServers].some(id => !liveServers.some(server => server.id === id))) { setOpError("Выбранный сервер отсутствует; обновите выбор"); return }
await job.run({ serverIds: [...manualServers], notes: manualNotes || undefined })
}
useEffect(() => {
if (!backupJobId) return
toast.info("Бэкап выполняется в фоне...")
let cancelled = false
const timer = setInterval(() => {
void (async () => {
try {
const job = await getBackupJob(backendUrl, backupJobId)
if (cancelled) return
if (job.status === "done") {
clearInterval(timer)
setBackupJobId(null)
if (job.failures.length > 0) {
setOpError(`Часть бэкапов не создалась: ${job.failures.map((f) => `${f.serverId}: ${f.error}`).join("; ")}`)
} else {
toast.success("Бэкап успешно завершён")
}
await loadLive()
} else if (job.status === "failed") {
clearInterval(timer)
setBackupJobId(null)
setOpError(job.failures.map((f) => `${f.serverId}: ${f.error}`).join("; ") || "Ошибка фоновой задачи бэкапа")
await loadLive()
}
} catch (e) {
clearInterval(timer)
setBackupJobId(null)
setOpError(e instanceof Error ? e.message : "Ошибка опроса задачи бэкапа")
}
})()
}, 1200)
return () => {
cancelled = true
clearInterval(timer)
}
}, [backendUrl, backupJobId, loadLive])
useEffect(() => {
if (opError) toast.error(opError)
}, [opError])
@@ -377,6 +334,7 @@ function BackupsContent() {
: "default"
function openCreate(all: boolean) {
if (checkpoint) { setManualStep(3); setManualOpen(true); return }
if (!canCreate) return
setManualServers(all ? new Set(liveServers.map((s) => s.id)) : new Set())
setManualNotes("")
@@ -395,7 +353,7 @@ function BackupsContent() {
variant="outline"
size="sm"
onClick={() => openCreate(true)}
disabled={!canCreate || loading}
disabled={!canCreate || loading || job.busy}
>
<RefreshCwIcon className="size-4" />
Снять со всех
@@ -404,10 +362,10 @@ function BackupsContent() {
type="button"
size="sm"
onClick={() => openCreate(false)}
disabled={!canCreate || loading}
disabled={checkpoint ? !canRead : !canCreate || loading || job.busy}
>
<PlusIcon className="size-4" />
Новый бэкап
{checkpoint ? "Задача резервного копирования" : "Новый бэкап"}
</Button>
</>
}
@@ -428,6 +386,7 @@ function BackupsContent() {
</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
aria-label="Сводка бэкапов"
items={[
@@ -465,11 +424,11 @@ function BackupsContent() {
<Alert variant={alertVariant}>
<ClockIcon />
<AlertTitle>
{backupJobId ? "Задача выполняется" : schedSummary}
{backupJobId ? (checkpoint?.accepted ? "Задача наблюдается" : "Результат запроса пока неизвестен") : schedSummary}
</AlertTitle>
<AlertDescription>
{backupJobId
? "Снимок конфигурации снимается в фоне. Список обновится после завершения."
? "Закрытие формы не отменяет задачу. Проверка продолжает чтение того же ID."
: statusDetail}
</AlertDescription>
<AlertAction>
@@ -532,8 +491,8 @@ function BackupsContent() {
scheduleKnown={known("schedule")}
storageKnown={known("storage")}
storageDirty={storageIsDirty}
scheduleWritable={canWrite && canReadServers && current("schedule") && current("servers")}
storageWritable={canWrite && current("storage")}
scheduleWritable={canWrite && canReadServers && current("schedule") && current("servers") && !backupJobId}
storageWritable={canWrite && current("storage") && !backupJobId}
allEnabled={allEnabled}
onAllEnabledChange={value => { scheduleDirty.current = true; setAllEnabled(value) }}
onTest={handleTestStorage}
@@ -554,7 +513,7 @@ function BackupsContent() {
step={manualStep}
onStepChange={setManualStep}
servers={liveServers}
selected={manualServers}
selected={checkpoint ? new Set(checkpoint.request.serverIds.map(String)) : manualServers}
onToggle={(id) => {
setManualServers((prev) => {
const next = new Set(prev)
@@ -565,10 +524,17 @@ function BackupsContent() {
}}
onSelectAll={() => setManualServers(new Set(liveServers.map((s) => s.id)))}
onClear={() => setManualServers(new Set())}
notes={manualNotes}
notes={checkpoint?.request.notes ?? manualNotes}
onNotesChange={setManualNotes}
destinationLabel={destinationLabel}
busy={backupJobId !== null}
busy={job.busy}
locked={!!checkpoint}
terminal={jobTerminal}
canObserve={canRead}
jobId={checkpoint?.request.requestId}
jobStatus={checkpoint?.job ? ({ queued: "В очереди", running: "Выполняется", done: "Завершена", failed: "Прервана" })[checkpoint.job.status] + ": " + checkpoint.job.completed + "/" + checkpoint.job.total : checkpoint?.accepted ? "Запрос принят, результат ещё неизвестен" : "Результат запроса ещё неизвестен"}
result={checkpoint?.job && jobTerminal ? "Создано " + backupJobOutcome(checkpoint.job).created + " из " + checkpoint.job.total : undefined}
onReset={() => { if (job.reset()) { setManualServers(new Set()); setManualNotes(""); setManualStep(1) } }}
onSubmit={handleManualBackup}
/>