From 11e9fbcc441f135cf7e55454194e0c1af301464b Mon Sep 17 00:00:00 2001 From: Nico <47644445+nicotsx@users.noreply.github.com> Date: Wed, 29 Apr 2026 23:48:58 +0200 Subject: [PATCH] feat: pre/post backup webhooks (#835) * feat: pre/post backup webhooks * fix(hooks): run post when cancelled * refactor(webhooks): headers as array * refactor: pr feedback * refactor: simplify hooks ceremonies * chore: pr feedbacks * chore: re-gen migration --- app/client/api-client/types.gen.ts | 84 + .../components/webhook-request-preview.tsx | 25 + .../create-schedule-form/advanced-section.tsx | 82 +- .../components/create-schedule-form/index.tsx | 16 +- .../components/create-schedule-form/types.ts | 33 +- .../components/create-schedule-form/utils.ts | 36 +- .../routes/__tests__/backup-details.test.tsx | 1 + .../routes/__tests__/edit-backup.test.tsx | 26 + .../modules/backups/routes/backup-details.tsx | 1 + .../modules/backups/routes/create-backup.tsx | 1 + .../notification-forms/generic-form.tsx | 19 +- .../migration.sql | 1 + .../snapshot.json | 2341 +++++++++++++++++ app/server/db/schema.ts | 2 + .../modules/agents/__tests__/session.test.ts | 1 + .../backups.service.execution.test.ts | 35 + .../backups/__tests__/backups.service.test.ts | 56 + app/server/modules/backups/backup-executor.ts | 52 +- .../modules/backups/backups.controller.ts | 9 +- app/server/modules/backups/backups.dto.ts | 10 +- app/server/modules/backups/backups.service.ts | 9 +- .../backups/helpers/backup-lifecycle.ts | 2 +- .../src/__tests__/controller-session.test.ts | 1 + apps/agent/src/commands/backup-run.test.ts | 258 +- apps/agent/src/commands/backup-run.ts | 113 +- packages/contracts/src/agent-protocol.ts | 2 + packages/core/package.json | 5 + .../src/backup-hooks/__tests__/hooks.test.ts | 383 +++ packages/core/src/backup-hooks/index.ts | 280 ++ 29 files changed, 3762 insertions(+), 122 deletions(-) create mode 100644 app/client/components/webhook-request-preview.tsx create mode 100644 app/drizzle/20260429214453_classy_namora/migration.sql create mode 100644 app/drizzle/20260429214453_classy_namora/snapshot.json create mode 100644 packages/core/src/backup-hooks/__tests__/hooks.test.ts create mode 100644 packages/core/src/backup-hooks/index.ts diff --git a/app/client/api-client/types.gen.ts b/app/client/api-client/types.gen.ts index ddf355f3..e6e5f670 100644 --- a/app/client/api-client/types.gen.ts +++ b/app/client/api-client/types.gen.ts @@ -2434,6 +2434,18 @@ export type ListBackupSchedulesResponses = { includePatterns: Array | null; oneFileSystem: boolean; customResticParams: Array | null; + backupWebhooks: { + pre: { + url: string; + headers?: Array; + body?: string; + } | null; + post: { + url: string; + headers?: Array; + body?: string; + } | null; + } | null; maxRetries: number; retryDelay: number; lastBackupAt: number | null; @@ -2712,6 +2724,18 @@ export type CreateBackupScheduleData = { oneFileSystem?: boolean; tags?: Array; customResticParams?: Array; + backupWebhooks?: { + pre: { + url: string; + headers?: Array; + body?: string; + } | null; + post: { + url: string; + headers?: Array; + body?: string; + } | null; + } | null; maxRetries?: number; retryDelay?: number; }; @@ -2747,6 +2771,18 @@ export type CreateBackupScheduleResponses = { includePatterns: Array | null; oneFileSystem: boolean; customResticParams: Array | null; + backupWebhooks: { + pre: { + url: string; + headers?: Array; + body?: string; + } | null; + post: { + url: string; + headers?: Array; + body?: string; + } | null; + } | null; maxRetries: number; retryDelay: number; lastBackupAt: number | null; @@ -2816,6 +2852,18 @@ export type GetBackupScheduleResponses = { includePatterns: Array | null; oneFileSystem: boolean; customResticParams: Array | null; + backupWebhooks: { + pre: { + url: string; + headers?: Array; + body?: string; + } | null; + post: { + url: string; + headers?: Array; + body?: string; + } | null; + } | null; maxRetries: number; retryDelay: number; lastBackupAt: number | null; @@ -3093,6 +3141,18 @@ export type UpdateBackupScheduleData = { oneFileSystem?: boolean; tags?: Array; customResticParams?: Array; + backupWebhooks?: { + pre: { + url: string; + headers?: Array; + body?: string; + } | null; + post: { + url: string; + headers?: Array; + body?: string; + } | null; + } | null; maxRetries?: number; retryDelay?: number; }; @@ -3130,6 +3190,18 @@ export type UpdateBackupScheduleResponses = { includePatterns: Array | null; oneFileSystem: boolean; customResticParams: Array | null; + backupWebhooks: { + pre: { + url: string; + headers?: Array; + body?: string; + } | null; + post: { + url: string; + headers?: Array; + body?: string; + } | null; + } | null; maxRetries: number; retryDelay: number; lastBackupAt: number | null; @@ -3179,6 +3251,18 @@ export type GetBackupScheduleForVolumeResponses = { includePatterns: Array | null; oneFileSystem: boolean; customResticParams: Array | null; + backupWebhooks: { + pre: { + url: string; + headers?: Array; + body?: string; + } | null; + post: { + url: string; + headers?: Array; + body?: string; + } | null; + } | null; maxRetries: number; retryDelay: number; lastBackupAt: number | null; diff --git a/app/client/components/webhook-request-preview.tsx b/app/client/components/webhook-request-preview.tsx new file mode 100644 index 00000000..1b1c132f --- /dev/null +++ b/app/client/components/webhook-request-preview.tsx @@ -0,0 +1,25 @@ +import { CodeBlock } from "~/client/components/ui/code-block"; +import { Label } from "~/client/components/ui/label"; + +type WebhookRequestPreviewProps = { + method: string; + url?: string; + contentType?: string; + headers?: string[]; + body: string; +}; + +export const WebhookRequestPreview = ({ method, url, contentType, headers, body }: WebhookRequestPreviewProps) => { + const headerLines = [contentType ? `Content-Type: ${contentType}` : undefined, ...(headers ?? [])].filter(Boolean); + const previewCode = `${method} ${url || "https://api.example.com/webhook"}${headerLines.length > 0 ? `\n${headerLines.join("\n")}` : ""} + +${body}`; + + return ( +
+ + +

This is a preview of the HTTP request that will be sent.

+
+ ); +}; diff --git a/app/client/modules/backups/components/create-schedule-form/advanced-section.tsx b/app/client/modules/backups/components/create-schedule-form/advanced-section.tsx index a6d865a3..0b6c3d33 100644 --- a/app/client/modules/backups/components/create-schedule-form/advanced-section.tsx +++ b/app/client/modules/backups/components/create-schedule-form/advanced-section.tsx @@ -1,6 +1,6 @@ import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/client/components/ui/form"; import { Textarea } from "~/client/components/ui/textarea"; -import type { UseFormReturn } from "react-hook-form"; +import { type UseFormReturn } from "react-hook-form"; import type { InternalFormValues } from "./types"; import { Input } from "~/client/components/ui/input"; @@ -8,6 +8,72 @@ type AdvancedSectionProps = { form: UseFormReturn; }; +type WebhookFieldsProps = { + form: UseFormReturn; + phase: "pre" | "post"; + urlPlaceholder: string; + bodyPlaceholder: string; + description: string; +}; + +const WebhookFields = ({ form, phase, urlPlaceholder, bodyPlaceholder, description }: WebhookFieldsProps) => { + const labelPrefix = phase === "pre" ? "Pre-backup" : "Post-backup"; + const fieldPrefix = phase === "pre" ? "preBackupWebhook" : "postBackupWebhook"; + + return ( + <> + ( + + {labelPrefix} webhook + + + + {description} + + + )} + /> + ( + + {labelPrefix} webhook headers + +