From 8791b3bf078919edd8cbf97cf54108129ed7b863 Mon Sep 17 00:00:00 2001 From: nfebe Date: Fri, 25 Sep 2026 00:10:50 +0100 Subject: [PATCH] ui(backups): Show incomplete backup protection Operators can see partial and local-only backups, inspect individual source and destination failures, and retry failed publication from the existing archive. --- src/components/BackupsTab.test.ts | 33 +++++++++++ src/components/BackupsTab.vue | 96 ++++++++++++++++++++++++++++++- src/services/api.ts | 27 ++++++++- 3 files changed, 151 insertions(+), 5 deletions(-) diff --git a/src/components/BackupsTab.test.ts b/src/components/BackupsTab.test.ts index c7fb178..a6c5249 100644 --- a/src/components/BackupsTab.test.ts +++ b/src/components/BackupsTab.test.ts @@ -14,6 +14,7 @@ vi.mock("@/services/api", () => ({ getJob: vi.fn().mockResolvedValue({ data: { job: { id: "job-123", status: "completed", type: "backup" } }, }), + retryPublication: vi.fn().mockResolvedValue({ data: { backup: { status: "completed" } } }), }, schedulerApi: { listTasks: vi.fn().mockResolvedValue({ data: { tasks: [] } }), @@ -28,6 +29,7 @@ const mockGetDeploymentBackups = backupsApi.getDeploymentBackups as ReturnType; const mockDeleteBackup = backupsApi.delete as ReturnType; const mockRestoreBackup = backupsApi.restore as ReturnType; +const mockRetryPublication = backupsApi.retryPublication as ReturnType; const mockListTasks = schedulerApi.listTasks as ReturnType; const mockCreateTask = schedulerApi.createTask as ReturnType; @@ -178,6 +180,37 @@ describe("BackupsTab", () => { }); describe("Backups list", () => { + it("shows incomplete protection and retries publication", async () => { + const incomplete = [ + { + ...mockBackups[0], + status: "local_only", + destination_results: [{ name: "archive", status: "failed", error: "upload failed" }], + }, + ]; + const wrapper = mountBackupsTab({ backups: incomplete as typeof mockBackups }); + await new Promise((resolve) => setTimeout(resolve, 10)); + const retry = wrapper.findAll("button").find((button) => button.text().includes("Retry publication")); + expect(retry).toBeTruthy(); + expect(wrapper.text()).toContain("archive: failed"); + await retry!.trigger("click"); + expect(mockRetryPublication).toHaveBeenCalledWith("my-app", mockBackups[0].id); + }); + + it("does not offer publication retry for cleanup-only failures", async () => { + const cleanupFailure = [ + { + ...mockBackups[0], + status: "partial", + cleanup_results: [{ name: "post_hooks", kind: "cleanup", required: true, status: "failed" }], + destination_results: [{ name: "archive", status: "completed" }], + }, + ]; + const wrapper = mountBackupsTab({ backups: cleanupFailure as typeof mockBackups }); + await new Promise((resolve) => setTimeout(resolve, 10)); + expect(wrapper.text()).not.toContain("Retry publication"); + }); + it("renders backup items when backups exist", async () => { const wrapper = mountBackupsTab({ backups: mockBackups }); await wrapper.vm.$nextTick(); diff --git a/src/components/BackupsTab.vue b/src/components/BackupsTab.vue index 2b5e25e..bcb70e3 100644 --- a/src/components/BackupsTab.vue +++ b/src/components/BackupsTab.vue @@ -54,10 +54,36 @@ {{ comp }} +
+
+ {{ result.name }}: {{ result.error || "Failed" }} +
+
+
+ + {{ destination.name }}: {{ destination.status }} + +
+