import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema"; import { AlertTriangle, Mail, PenBoxIcon, PlusIcon, Trash2, } from "lucide-react"; import { useEffect, useState } from "react"; import { useFieldArray, useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; import { DiscordIcon, GotifyIcon, LarkIcon, NtfyIcon, PushoverIcon, ResendIcon, SlackIcon, TeamsIcon, TelegramIcon, } from "@/components/icons/notification-icons"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Switch } from "@/components/ui/switch"; import { api } from "@/utils/api"; const notificationBaseSchema = z.object({ name: z.string().min(1, { message: "Name is required", }), appDeploy: z.boolean().default(false), appBuildError: z.boolean().default(false), databaseBackup: z.boolean().default(false), volumeBackup: z.boolean().default(false), dokployRestart: z.boolean().default(false), dockerCleanup: z.boolean().default(false), serverThreshold: z.boolean().default(false), }); export const notificationSchema = z.discriminatedUnion("type", [ z .object({ type: z.literal("slack"), webhookUrl: z.string().min(1, { message: "Webhook URL is required" }), channel: z.string(), }) .merge(notificationBaseSchema), z .object({ type: z.literal("telegram"), botToken: z.string().min(1, { message: "Bot Token is required" }), chatId: z.string().min(1, { message: "Chat ID is required" }), messageThreadId: z.string().optional(), }) .merge(notificationBaseSchema), z .object({ type: z.literal("discord"), webhookUrl: z.string().min(1, { message: "Webhook URL is required" }), decoration: z.boolean().default(true), }) .merge(notificationBaseSchema), z .object({ type: z.literal("email"), smtpServer: z.string().min(1, { message: "SMTP Server is required" }), smtpPort: z.number().min(1, { message: "SMTP Port is required" }), username: z.string().min(1, { message: "Username is required" }), password: z.string().min(1, { message: "Password is required" }), fromAddress: z.string().min(1, { message: "From Address is required" }), toAddresses: z .array( z.string().min(1, { message: "Email is required" }).email({ message: "Email is invalid", }), ) .min(1, { message: "At least one email is required" }), }) .merge(notificationBaseSchema), z .object({ type: z.literal("resend"), apiKey: z.string().min(1, { message: "API Key is required" }), fromAddress: z .string() .min(1, { message: "From Address is required" }) .email({ message: "Email is invalid" }), toAddresses: z .array( z.string().min(1, { message: "Email is required" }).email({ message: "Email is invalid", }), ) .min(1, { message: "At least one email is required" }), }) .merge(notificationBaseSchema), z .object({ type: z.literal("gotify"), serverUrl: z.string().min(1, { message: "Server URL is required" }), appToken: z.string().min(1, { message: "App Token is required" }), priority: z.number().min(1).max(10).default(5), decoration: z.boolean().default(true), }) .merge(notificationBaseSchema), z .object({ type: z.literal("ntfy"), serverUrl: z.string().min(1, { message: "Server URL is required" }), topic: z.string().min(1, { message: "Topic is required" }), accessToken: z.string().optional(), priority: z.number().min(1).max(5).default(3), }) .merge(notificationBaseSchema), z .object({ type: z.literal("pushover"), userKey: z.string().min(1, { message: "User Key is required" }), apiToken: z.string().min(1, { message: "API Token is required" }), priority: z.number().min(-2).max(2).default(0), retry: z.number().min(30).nullish(), expire: z.number().min(1).max(10800).nullish(), }) .merge(notificationBaseSchema), z .object({ type: z.literal("custom"), endpoint: z.string().min(1, { message: "Endpoint URL is required" }), headers: z .array( z.object({ key: z.string(), value: z.string(), }), ) .optional() .default([]), }) .merge(notificationBaseSchema), z .object({ type: z.literal("lark"), webhookUrl: z.string().min(1, { message: "Webhook URL is required" }), }) .merge(notificationBaseSchema), z .object({ type: z.literal("teams"), webhookUrl: z.string().min(1, { message: "Webhook URL is required" }), }) .merge(notificationBaseSchema), ]); export const notificationsMap = { slack: { icon: , label: "Slack", }, telegram: { icon: , label: "Telegram", }, discord: { icon: , label: "Discord", }, lark: { icon: , label: "Lark", }, teams: { icon: , label: "Microsoft Teams", }, email: { icon: , label: "Email", }, resend: { icon: , label: "Resend", }, gotify: { icon: , label: "Gotify", }, ntfy: { icon: , label: "ntfy", }, pushover: { icon: , label: "Pushover", }, custom: { icon: , label: "Custom", }, }; export type NotificationSchema = z.infer; interface Props { notificationId?: string; } export const HandleNotifications = ({ notificationId }: Props) => { const utils = api.useUtils(); const [visible, setVisible] = useState(false); const { data: isCloud } = api.settings.isCloud.useQuery(); const { data: notification } = api.notification.one.useQuery( { notificationId: notificationId || "", }, { enabled: !!notificationId, }, ); const { mutateAsync: testSlackConnection, isPending: isLoadingSlack } = api.notification.testSlackConnection.useMutation(); const { mutateAsync: testTelegramConnection, isPending: isLoadingTelegram } = api.notification.testTelegramConnection.useMutation(); const { mutateAsync: testDiscordConnection, isPending: isLoadingDiscord } = api.notification.testDiscordConnection.useMutation(); const { mutateAsync: testEmailConnection, isPending: isLoadingEmail } = api.notification.testEmailConnection.useMutation(); const { mutateAsync: testResendConnection, isPending: isLoadingResend } = api.notification.testResendConnection.useMutation(); const { mutateAsync: testGotifyConnection, isPending: isLoadingGotify } = api.notification.testGotifyConnection.useMutation(); const { mutateAsync: testNtfyConnection, isPending: isLoadingNtfy } = api.notification.testNtfyConnection.useMutation(); const { mutateAsync: testLarkConnection, isPending: isLoadingLark } = api.notification.testLarkConnection.useMutation(); const { mutateAsync: testTeamsConnection, isPending: isLoadingTeams } = api.notification.testTeamsConnection.useMutation(); const { mutateAsync: testCustomConnection, isPending: isLoadingCustom } = api.notification.testCustomConnection.useMutation(); const { mutateAsync: testPushoverConnection, isPending: isLoadingPushover } = api.notification.testPushoverConnection.useMutation(); const customMutation = notificationId ? api.notification.updateCustom.useMutation() : api.notification.createCustom.useMutation(); const slackMutation = notificationId ? api.notification.updateSlack.useMutation() : api.notification.createSlack.useMutation(); const telegramMutation = notificationId ? api.notification.updateTelegram.useMutation() : api.notification.createTelegram.useMutation(); const discordMutation = notificationId ? api.notification.updateDiscord.useMutation() : api.notification.createDiscord.useMutation(); const emailMutation = notificationId ? api.notification.updateEmail.useMutation() : api.notification.createEmail.useMutation(); const resendMutation = notificationId ? api.notification.updateResend.useMutation() : api.notification.createResend.useMutation(); const gotifyMutation = notificationId ? api.notification.updateGotify.useMutation() : api.notification.createGotify.useMutation(); const ntfyMutation = notificationId ? api.notification.updateNtfy.useMutation() : api.notification.createNtfy.useMutation(); const larkMutation = notificationId ? api.notification.updateLark.useMutation() : api.notification.createLark.useMutation(); const teamsMutation = notificationId ? api.notification.updateTeams.useMutation() : api.notification.createTeams.useMutation(); const pushoverMutation = notificationId ? api.notification.updatePushover.useMutation() : api.notification.createPushover.useMutation(); const form = useForm({ defaultValues: { type: "slack", webhookUrl: "", channel: "", name: "", }, resolver: zodResolver(notificationSchema), }); const type = form.watch("type"); const { fields, append, remove } = useFieldArray({ control: form.control, name: "toAddresses" as never, }); const { fields: headerFields, append: appendHeader, remove: removeHeader, } = useFieldArray({ control: form.control, name: "headers" as never, }); useEffect(() => { if ((type === "email" || type === "resend") && fields.length === 0) { append(""); } }, [type, append, fields.length]); useEffect(() => { if (notification) { if (notification.notificationType === "slack") { form.reset({ appBuildError: notification.appBuildError, appDeploy: notification.appDeploy, dokployRestart: notification.dokployRestart, databaseBackup: notification.databaseBackup, volumeBackup: notification.volumeBackup, dockerCleanup: notification.dockerCleanup, webhookUrl: notification.slack?.webhookUrl, channel: notification.slack?.channel || "", name: notification.name, type: notification.notificationType, serverThreshold: notification.serverThreshold, }); } else if (notification.notificationType === "telegram") { form.reset({ appBuildError: notification.appBuildError, appDeploy: notification.appDeploy, dokployRestart: notification.dokployRestart, databaseBackup: notification.databaseBackup, volumeBackup: notification.volumeBackup, botToken: notification.telegram?.botToken, messageThreadId: notification.telegram?.messageThreadId || "", chatId: notification.telegram?.chatId, type: notification.notificationType, name: notification.name, dockerCleanup: notification.dockerCleanup, serverThreshold: notification.serverThreshold, }); } else if (notification.notificationType === "discord") { form.reset({ appBuildError: notification.appBuildError, appDeploy: notification.appDeploy, dokployRestart: notification.dokployRestart, databaseBackup: notification.databaseBackup, volumeBackup: notification.volumeBackup, type: notification.notificationType, webhookUrl: notification.discord?.webhookUrl, decoration: notification.discord?.decoration ?? undefined, name: notification.name, dockerCleanup: notification.dockerCleanup, serverThreshold: notification.serverThreshold, }); } else if (notification.notificationType === "email") { form.reset({ appBuildError: notification.appBuildError, appDeploy: notification.appDeploy, dokployRestart: notification.dokployRestart, databaseBackup: notification.databaseBackup, volumeBackup: notification.volumeBackup, type: notification.notificationType, smtpServer: notification.email?.smtpServer, smtpPort: notification.email?.smtpPort, username: notification.email?.username, password: notification.email?.password, toAddresses: notification.email?.toAddresses, fromAddress: notification.email?.fromAddress, name: notification.name, dockerCleanup: notification.dockerCleanup, serverThreshold: notification.serverThreshold, }); } else if (notification.notificationType === "resend") { form.reset({ appBuildError: notification.appBuildError, appDeploy: notification.appDeploy, dokployRestart: notification.dokployRestart, databaseBackup: notification.databaseBackup, volumeBackup: notification.volumeBackup, type: notification.notificationType, apiKey: notification.resend?.apiKey, toAddresses: notification.resend?.toAddresses, fromAddress: notification.resend?.fromAddress, name: notification.name, dockerCleanup: notification.dockerCleanup, serverThreshold: notification.serverThreshold, }); } else if (notification.notificationType === "gotify") { form.reset({ appBuildError: notification.appBuildError, appDeploy: notification.appDeploy, dokployRestart: notification.dokployRestart, databaseBackup: notification.databaseBackup, volumeBackup: notification.volumeBackup, type: notification.notificationType, appToken: notification.gotify?.appToken, decoration: notification.gotify?.decoration ?? undefined, priority: notification.gotify?.priority, serverUrl: notification.gotify?.serverUrl, name: notification.name, dockerCleanup: notification.dockerCleanup, }); } else if (notification.notificationType === "ntfy") { form.reset({ appBuildError: notification.appBuildError, appDeploy: notification.appDeploy, dokployRestart: notification.dokployRestart, databaseBackup: notification.databaseBackup, volumeBackup: notification.volumeBackup, type: notification.notificationType, accessToken: notification.ntfy?.accessToken || "", topic: notification.ntfy?.topic, priority: notification.ntfy?.priority, serverUrl: notification.ntfy?.serverUrl, name: notification.name, dockerCleanup: notification.dockerCleanup, serverThreshold: notification.serverThreshold, }); } else if (notification.notificationType === "lark") { form.reset({ appBuildError: notification.appBuildError, appDeploy: notification.appDeploy, dokployRestart: notification.dokployRestart, databaseBackup: notification.databaseBackup, type: notification.notificationType, webhookUrl: notification.lark?.webhookUrl, name: notification.name, dockerCleanup: notification.dockerCleanup, volumeBackup: notification.volumeBackup, serverThreshold: notification.serverThreshold, }); } else if (notification.notificationType === "teams") { form.reset({ appBuildError: notification.appBuildError, appDeploy: notification.appDeploy, dokployRestart: notification.dokployRestart, databaseBackup: notification.databaseBackup, volumeBackup: notification.volumeBackup, type: notification.notificationType, webhookUrl: notification.teams?.webhookUrl, name: notification.name, dockerCleanup: notification.dockerCleanup, serverThreshold: notification.serverThreshold, }); } else if (notification.notificationType === "custom") { form.reset({ appBuildError: notification.appBuildError, appDeploy: notification.appDeploy, dokployRestart: notification.dokployRestart, databaseBackup: notification.databaseBackup, type: notification.notificationType, endpoint: notification.custom?.endpoint || "", headers: notification.custom?.headers ? Object.entries(notification.custom.headers).map( ([key, value]) => ({ key, value, }), ) : [], name: notification.name, volumeBackup: notification.volumeBackup, dockerCleanup: notification.dockerCleanup, serverThreshold: notification.serverThreshold, }); } else if (notification.notificationType === "pushover") { form.reset({ appBuildError: notification.appBuildError, appDeploy: notification.appDeploy, dokployRestart: notification.dokployRestart, databaseBackup: notification.databaseBackup, volumeBackup: notification.volumeBackup, type: notification.notificationType, userKey: notification.pushover?.userKey, apiToken: notification.pushover?.apiToken, priority: notification.pushover?.priority, retry: notification.pushover?.retry ?? undefined, expire: notification.pushover?.expire ?? undefined, name: notification.name, dockerCleanup: notification.dockerCleanup, serverThreshold: notification.serverThreshold, }); } } else { form.reset(); } }, [form, form.reset, form.formState.isSubmitSuccessful, notification]); const activeMutation = { slack: slackMutation, telegram: telegramMutation, discord: discordMutation, email: emailMutation, resend: resendMutation, gotify: gotifyMutation, ntfy: ntfyMutation, lark: larkMutation, teams: teamsMutation, custom: customMutation, pushover: pushoverMutation, }; const onSubmit = async (data: NotificationSchema) => { const { appBuildError, appDeploy, dokployRestart, databaseBackup, volumeBackup, dockerCleanup, serverThreshold, } = data; let promise: Promise | null = null; if (data.type === "slack") { promise = slackMutation.mutateAsync({ appBuildError: appBuildError, appDeploy: appDeploy, dokployRestart: dokployRestart, databaseBackup: databaseBackup, volumeBackup: volumeBackup, webhookUrl: data.webhookUrl, channel: data.channel, name: data.name, dockerCleanup: dockerCleanup, slackId: notification?.slackId || "", notificationId: notificationId || "", serverThreshold: serverThreshold, }); } else if (data.type === "telegram") { promise = telegramMutation.mutateAsync({ appBuildError: appBuildError, appDeploy: appDeploy, dokployRestart: dokployRestart, databaseBackup: databaseBackup, volumeBackup: volumeBackup, botToken: data.botToken, messageThreadId: data.messageThreadId || "", chatId: data.chatId, name: data.name, dockerCleanup: dockerCleanup, notificationId: notificationId || "", telegramId: notification?.telegramId || "", serverThreshold: serverThreshold, }); } else if (data.type === "discord") { promise = discordMutation.mutateAsync({ appBuildError: appBuildError, appDeploy: appDeploy, dokployRestart: dokployRestart, databaseBackup: databaseBackup, volumeBackup: volumeBackup, webhookUrl: data.webhookUrl, decoration: data.decoration, name: data.name, dockerCleanup: dockerCleanup, notificationId: notificationId || "", discordId: notification?.discordId || "", serverThreshold: serverThreshold, }); } else if (data.type === "email") { promise = emailMutation.mutateAsync({ appBuildError: appBuildError, appDeploy: appDeploy, dokployRestart: dokployRestart, databaseBackup: databaseBackup, volumeBackup: volumeBackup, smtpServer: data.smtpServer, smtpPort: data.smtpPort, username: data.username, password: data.password, fromAddress: data.fromAddress, toAddresses: data.toAddresses, name: data.name, dockerCleanup: dockerCleanup, notificationId: notificationId || "", emailId: notification?.emailId || "", serverThreshold: serverThreshold, }); } else if (data.type === "resend") { promise = resendMutation.mutateAsync({ appBuildError: appBuildError, appDeploy: appDeploy, dokployRestart: dokployRestart, databaseBackup: databaseBackup, volumeBackup: volumeBackup, apiKey: data.apiKey, fromAddress: data.fromAddress, toAddresses: data.toAddresses, name: data.name, dockerCleanup: dockerCleanup, notificationId: notificationId || "", resendId: notification?.resendId || "", serverThreshold: serverThreshold, }); } else if (data.type === "gotify") { promise = gotifyMutation.mutateAsync({ appBuildError: appBuildError, appDeploy: appDeploy, dokployRestart: dokployRestart, databaseBackup: databaseBackup, volumeBackup: volumeBackup, serverUrl: data.serverUrl, appToken: data.appToken, priority: data.priority, name: data.name, dockerCleanup: dockerCleanup, decoration: data.decoration, notificationId: notificationId || "", gotifyId: notification?.gotifyId || "", }); } else if (data.type === "ntfy") { promise = ntfyMutation.mutateAsync({ appBuildError: appBuildError, appDeploy: appDeploy, dokployRestart: dokployRestart, databaseBackup: databaseBackup, volumeBackup: volumeBackup, serverUrl: data.serverUrl, accessToken: data.accessToken || "", topic: data.topic, priority: data.priority, name: data.name, dockerCleanup: dockerCleanup, notificationId: notificationId || "", ntfyId: notification?.ntfyId || "", }); } else if (data.type === "lark") { promise = larkMutation.mutateAsync({ appBuildError: appBuildError, appDeploy: appDeploy, dokployRestart: dokployRestart, databaseBackup: databaseBackup, volumeBackup: volumeBackup, webhookUrl: data.webhookUrl, name: data.name, dockerCleanup: dockerCleanup, notificationId: notificationId || "", larkId: notification?.larkId || "", serverThreshold: serverThreshold, }); } else if (data.type === "teams") { promise = teamsMutation.mutateAsync({ appBuildError: appBuildError, appDeploy: appDeploy, dokployRestart: dokployRestart, databaseBackup: databaseBackup, volumeBackup: volumeBackup, webhookUrl: data.webhookUrl, name: data.name, dockerCleanup: dockerCleanup, notificationId: notificationId || "", teamsId: notification?.teamsId || "", serverThreshold: serverThreshold, }); } else if (data.type === "custom") { // Convert headers array to object const headersRecord = data.headers && data.headers.length > 0 ? data.headers.reduce( (acc, { key, value }) => { if (key.trim()) acc[key] = value; return acc; }, {} as Record, ) : undefined; promise = customMutation.mutateAsync({ appBuildError: appBuildError, appDeploy: appDeploy, dokployRestart: dokployRestart, databaseBackup: databaseBackup, volumeBackup: volumeBackup, endpoint: data.endpoint, headers: headersRecord, name: data.name, dockerCleanup: dockerCleanup, serverThreshold: serverThreshold, notificationId: notificationId || "", customId: notification?.customId || "", }); } else if (data.type === "pushover") { if (data.priority === 2 && (data.retry == null || data.expire == null)) { toast.error("Retry and expire are required for emergency priority (2)"); return; } promise = pushoverMutation.mutateAsync({ appBuildError: appBuildError, appDeploy: appDeploy, dokployRestart: dokployRestart, databaseBackup: databaseBackup, volumeBackup: volumeBackup, userKey: data.userKey, apiToken: data.apiToken, priority: data.priority, retry: data.priority === 2 ? data.retry : undefined, expire: data.priority === 2 ? data.expire : undefined, name: data.name, dockerCleanup: dockerCleanup, serverThreshold: serverThreshold, notificationId: notificationId || "", pushoverId: notification?.pushoverId || "", }); } if (promise) { await promise .then(async () => { toast.success( notificationId ? "Notification Updated" : "Notification Created", ); form.reset({ type: "slack", webhookUrl: "", }); setVisible(false); await utils.notification.all.invalidate(); if (notificationId) { await utils.notification.one.invalidate({ notificationId }); } }) .catch(() => { toast.error( notificationId ? "Error updating a notification" : "Error creating a notification", ); }); } }; return ( {notificationId ? ( ) : ( )} {notificationId ? "Update" : "Add"} Notification {notificationId ? "Update your notification providers for multiple channels." : "Create new notification providers for multiple channels."}
( Select a provider {Object.entries(notificationsMap).map(([key, value]) => (
))}
{activeMutation[field.value].isError && (
{activeMutation[field.value].error?.message}
)}
)} />
Fill the next fields.
( Name )} /> {type === "slack" && ( <> ( Webhook URL )} /> ( Channel )} /> )} {type === "telegram" && ( <> ( Bot Token )} /> ( Chat ID )} /> ( Message Thread ID Optional. Use it when you want to send notifications to a specific topic in a group. )} /> )} {type === "discord" && ( <> ( Webhook URL )} /> (
Decoration Decorate the notification with emojis.
)} /> )} {type === "email" && ( <>
( SMTP Server )} /> ( SMTP Port { const value = e.target.value; if (value === "") { field.onChange(undefined); } else { const port = Number.parseInt(value); if (port > 0 && port < 65536) { field.onChange(port); } } }} value={field.value || ""} type="number" /> )} />
( Username )} /> ( Password )} />
( From Address )} />
To Addresses {fields.map((field, index) => (
( )} />
))} {type === "email" && "toAddresses" in form.formState.errors && (
{form.formState?.errors?.toAddresses?.root?.message}
)}
)} {type === "resend" && ( <> ( API Key )} /> ( From Address )} />
To Addresses {fields.map((field, index) => (
( )} />
))} {type === "resend" && "toAddresses" in form.formState.errors && (
{form.formState?.errors?.toAddresses?.root?.message}
)}
)} {type === "gotify" && ( <> ( Server URL )} /> ( App Token )} /> ( Priority { const value = e.target.value; if (value) { const port = Number.parseInt(value); if (port > 0 && port < 10) { field.onChange(port); } } }} type="number" /> Message priority (1-10, default: 5) )} /> (
Decoration Decorate the notification with emojis.
)} /> )} {type === "ntfy" && ( <> ( Server URL )} /> ( Topic )} /> ( Access Token Optional. Leave blank for public topics. )} /> ( Priority { const value = e.target.value; if (value) { const port = Number.parseInt(value); if (port > 0 && port <= 5) { field.onChange(port); } } }} type="number" /> Message priority (1-5, default: 3) )} /> )} {type === "custom" && (
( Webhook URL The URL where POST requests will be sent with notification data. )} />
Headers Optional. Custom headers for your POST request (e.g., Authorization, Content-Type).
{headerFields.map((field, index) => (
( )} /> ( )} />
))}
)} {type === "lark" && ( <> ( Webhook URL )} /> )} {type === "teams" && ( <> ( Webhook URL Incoming Webhook URL from a Teams channel. Add an Incoming Webhook in your channel settings to get the URL. )} /> )} {type === "pushover" && ( <> ( User Key )} /> ( API Token )} /> ( Priority { const value = e.target.value; if (value === "" || value === "-") { field.onChange(0); } else { const priority = Number.parseInt(value); if ( !Number.isNaN(priority) && priority >= -2 && priority <= 2 ) { field.onChange(priority); } } }} type="number" min={-2} max={2} /> Message priority (-2 to 2, default: 0, emergency: 2) )} /> {form.watch("priority") === 2 && ( <> ( Retry (seconds) { const value = e.target.value; if (value === "") { field.onChange(undefined); } else { const retry = Number.parseInt(value); if (!Number.isNaN(retry)) { field.onChange(retry); } } }} type="number" min={30} /> How often (in seconds) to retry. Minimum 30 seconds. )} /> ( Expire (seconds) { const value = e.target.value; if (value === "") { field.onChange(undefined); } else { const expire = Number.parseInt(value); if (!Number.isNaN(expire)) { field.onChange(expire); } } }} type="number" min={1} max={10800} /> How long to keep retrying (max 10800 seconds / 3 hours). )} /> )} )}
Select the actions.
(
App Deploy Trigger the action when a app is deployed.
)} /> (
App Build Error Trigger the action when the build fails.
)} /> (
Database Backup Trigger the action when a database backup is created.
)} /> (
Volume Backup Trigger the action when a volume backup is created.
)} /> (
Docker Cleanup Trigger the action when the docker cleanup is performed.
)} /> {!isCloud && ( (
Dokploy Restart Trigger the action when dokploy is restarted.
)} /> )} {isCloud && ( (
Server Threshold Trigger the action when the server threshold is reached.
)} /> )}
); };