show the real backup error when bulk snapshot deletion fails

This commit is contained in:
Jarek Krochmalski committed 2026-09-14 11:53:29 +02:00
1 parent ab855f2b0c
commit a7e4ce9bda
6 files changed
+131 -13

No files matched your search

@@ -0,0 +1,37 @@
<script lang="ts">
import * as Dialog from '$lib/components/ui/dialog';
import { Button } from '$lib/components/ui/button';
import { AlertTriangle } from 'lucide-svelte';
interface Props {
open: boolean;
/** Raw restic stderr for the failed batch (shown verbatim). */
error: string;
/** How many snapshots were still deleted before the failure. */
deleted?: number;
}
let { open = $bindable(), error, deleted = 0 }: Props = $props();
</script>
<Dialog.Root bind:open>
<Dialog.Content class="max-w-2xl">
<Dialog.Header>
<Dialog.Title class="flex items-center gap-2 text-destructive">
<AlertTriangle class="h-5 w-5" />
Snapshots could not be deleted
</Dialog.Title>
</Dialog.Header>
{#if deleted > 0}
<p class="text-sm text-muted-foreground">
Deleted {deleted} snapshot{deleted === 1 ? '' : 's'}, but the rest failed. The backup
tool reported:
</p>
{:else}
<p class="text-sm text-muted-foreground">The backup tool reported:</p>
{/if}
<pre class="max-h-[50vh] overflow-auto rounded-md bg-muted p-3 text-xs whitespace-pre-wrap break-words font-mono">{error}</pre>
<Dialog.Footer>
<Button onclick={() => (open = false)}>OK</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
+23 -3
View File
@@ -458,6 +458,9 @@ export interface BulkDeleteResult {
skipped: number;
/** true if any destination's batch call returned a non-2xx. */
failed: boolean;
/** Raw restic error from the first failing destination (repo locked, timeout, etc.),
* surfaced verbatim so the user sees the real reason instead of a generic message. */
error?: string;
}
/**
@@ -469,7 +472,7 @@ export interface BulkDeleteResult {
export async function bulkDeleteSnapshots(
targets: { id: string; _destinationId: number }[]
): Promise<BulkDeleteResult> {
let deleted = 0, skipped = 0, failed = false;
let deleted = 0, skipped = 0, failed = false, error: string | undefined;
for (const { destinationId, snapshotIds } of groupSnapshotIdsByDestination(targets)) {
try {
const res = await fetch('/api/backup/snapshots/batch-delete', {
@@ -480,17 +483,34 @@ export async function bulkDeleteSnapshots(
const data = await res.json().catch(() => ({}));
deleted += data.deleted?.length ?? 0;
if (res.ok) skipped += data.skipped?.length ?? 0;
else failed = true;
else {
failed = true;
if (!error && data.error) error = String(data.error);
}
} catch {
failed = true;
}
}
return { deleted, skipped, failed };
return { deleted, skipped, failed, error };
}
/**
* A hard restic failure that warrants the full-error dialog (repo locked, timeout,
* etc.) rather than a toast. Returns the raw restic error and how many were still
* deleted, or null when there is no such failure (toast handles success/skipped).
* A `failed` with no `error` (e.g. a thrown fetch) also returns null - nothing
* verbatim to show - so the caller falls back to the toast.
*/
export function bulkDeleteFailure(r: BulkDeleteResult): { error: string; deleted: number } | null {
if (r.failed && r.error) return { error: r.error, deleted: r.deleted };
return null;
}
/**
* Consistent toast wording for a bulk-delete result, shared by every snapshot list.
* Returns { type, message } so each caller fires its own toast (import stays local).
* A hard restic failure with a verbatim error is handled by the caller via
* bulkDeleteFailure + the error dialog, not here.
*/
export function bulkDeleteToast(r: BulkDeleteResult): { type: 'success' | 'error'; message: string } {
if (r.failed) return { type: 'error', message: r.deleted > 0 ? `Deleted ${r.deleted}, but some snapshots could not be deleted` : 'Snapshots could not be deleted' };