mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-19 14:15:21 +02:00
feat(volume-backup): add volume backup email notification template and integrate with notification system
This commit is contained in:
@@ -1 +1 @@
|
||||
ALTER TABLE "notification" ADD COLUMN "volumeBackup" boolean DEFAULT false NOT NULL;
|
||||
ALTER TABLE "notification" ADD COLUMN "volumeBackup" boolean DEFAULT false NOT NULL;
|
||||
6857
apps/dokploy/drizzle/meta/0127_snapshot.json
Normal file
6857
apps/dokploy/drizzle/meta/0127_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -890,6 +890,13 @@
|
||||
"when": 1765065295708,
|
||||
"tag": "0126_nifty_monster_badoon",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 127,
|
||||
"version": "7",
|
||||
"when": 1765095189368,
|
||||
"tag": "0127_superb_alice",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
123
packages/server/src/emails/emails/volume-backup.tsx
Normal file
123
packages/server/src/emails/emails/volume-backup.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import {
|
||||
Body,
|
||||
Container,
|
||||
Head,
|
||||
Heading,
|
||||
Html,
|
||||
Img,
|
||||
Preview,
|
||||
Section,
|
||||
Tailwind,
|
||||
Text,
|
||||
} from "@react-email/components";
|
||||
|
||||
export type TemplateProps = {
|
||||
projectName: string;
|
||||
applicationName: string;
|
||||
volumeName: string;
|
||||
serviceType:
|
||||
| "application"
|
||||
| "postgres"
|
||||
| "mysql"
|
||||
| "mongodb"
|
||||
| "mariadb"
|
||||
| "redis"
|
||||
| "compose";
|
||||
type: "error" | "success";
|
||||
errorMessage?: string;
|
||||
backupSize?: string;
|
||||
date: string;
|
||||
};
|
||||
|
||||
export const VolumeBackupEmail = ({
|
||||
projectName = "dokploy",
|
||||
applicationName = "frontend",
|
||||
volumeName = "app-data",
|
||||
serviceType = "application",
|
||||
type = "success",
|
||||
errorMessage,
|
||||
backupSize,
|
||||
date = "2023-05-01T00:00:00.000Z",
|
||||
}: TemplateProps) => {
|
||||
const previewText = `Volume backup for ${applicationName} was ${type === "success" ? "successful ✅" : "failed ❌"}`;
|
||||
return (
|
||||
<Html>
|
||||
<Preview>{previewText}</Preview>
|
||||
<Tailwind
|
||||
config={{
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
brand: "#007291",
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Head />
|
||||
|
||||
<Body className="bg-white my-auto mx-auto font-sans px-2">
|
||||
<Container className="border border-solid border-[#eaeaea] rounded-lg my-[40px] mx-auto p-[20px] max-w-[465px]">
|
||||
<Section className="mt-[32px]">
|
||||
<Img
|
||||
src={
|
||||
"https://raw.githubusercontent.com/Dokploy/dokploy/refs/heads/canary/apps/dokploy/logo.png"
|
||||
}
|
||||
width="100"
|
||||
height="50"
|
||||
alt="Dokploy"
|
||||
className="my-0 mx-auto"
|
||||
/>
|
||||
</Section>
|
||||
<Heading className="text-black text-[24px] font-normal text-center p-0 my-[30px] mx-0">
|
||||
Volume backup for <strong>{applicationName}</strong>
|
||||
</Heading>
|
||||
<Text className="text-black text-[14px] leading-[24px]">
|
||||
Hello,
|
||||
</Text>
|
||||
<Text className="text-black text-[14px] leading-[24px]">
|
||||
Your volume backup for <strong>{applicationName}</strong> was{" "}
|
||||
{type === "success"
|
||||
? "successful ✅"
|
||||
: "failed. Please check the error message below. ❌"}
|
||||
.
|
||||
</Text>
|
||||
<Section className="flex text-black text-[14px] leading-[24px] bg-[#F4F4F5] rounded-lg p-2">
|
||||
<Text className="!leading-3 font-bold">Details: </Text>
|
||||
<Text className="!leading-3">
|
||||
Project Name: <strong>{projectName}</strong>
|
||||
</Text>
|
||||
<Text className="!leading-3">
|
||||
Application Name: <strong>{applicationName}</strong>
|
||||
</Text>
|
||||
<Text className="!leading-3">
|
||||
Volume Name: <strong>{volumeName}</strong>
|
||||
</Text>
|
||||
<Text className="!leading-3">
|
||||
Service Type: <strong>{serviceType}</strong>
|
||||
</Text>
|
||||
{backupSize && (
|
||||
<Text className="!leading-3">
|
||||
Backup Size: <strong>{backupSize}</strong>
|
||||
</Text>
|
||||
)}
|
||||
<Text className="!leading-3">
|
||||
Date: <strong>{date}</strong>
|
||||
</Text>
|
||||
</Section>
|
||||
{type === "error" && errorMessage ? (
|
||||
<Section className="flex text-black text-[14px] mt-4 leading-[24px] bg-[#F4F4F5] rounded-lg p-2">
|
||||
<Text className="!leading-3 font-bold">Reason: </Text>
|
||||
<Text className="text-[12px] leading-[24px]">
|
||||
{errorMessage || "Error message not provided"}
|
||||
</Text>
|
||||
</Section>
|
||||
) : null}
|
||||
</Container>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>
|
||||
);
|
||||
};
|
||||
|
||||
export default VolumeBackupEmail;
|
||||
@@ -1,5 +1,6 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { notifications } from "@dokploy/server/db/schema";
|
||||
import { VolumeBackupEmail } from "@dokploy/server/emails/emails/volume-backup";
|
||||
import { renderAsync } from "@react-email/components";
|
||||
import { format } from "date-fns";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
@@ -60,25 +61,18 @@ export const sendVolumeBackupNotifications = async ({
|
||||
|
||||
if (email) {
|
||||
const subject = `Volume Backup ${type === "success" ? "Successful" : "Failed"} - ${applicationName}`;
|
||||
const htmlContent = `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<h2 style="color: ${type === "success" ? "#00AA00" : "#AA0000"};">
|
||||
${type === "success" ? "✅" : "❌"} Volume Backup ${type === "success" ? "Successful" : "Failed"}
|
||||
</h2>
|
||||
<div style="background-color: #f5f5f5; padding: 20px; border-radius: 5px; margin: 20px 0;">
|
||||
<p><strong>Project:</strong> ${projectName}</p>
|
||||
<p><strong>Application:</strong> ${applicationName}</p>
|
||||
<p><strong>Volume Name:</strong> ${volumeName}</p>
|
||||
<p><strong>Service Type:</strong> ${serviceType}</p>
|
||||
${backupSize ? `<p><strong>Backup Size:</strong> ${backupSize}</p>` : ""}
|
||||
<p><strong>Date:</strong> ${date.toLocaleString()}</p>
|
||||
${type === "error" && errorMessage ? `<p><strong>Error:</strong> <code>${errorMessage}</code></p>` : ""}
|
||||
</div>
|
||||
<p style="color: #666; font-size: 12px;">
|
||||
This notification was sent by Dokploy Volume Backup System.
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
const htmlContent = await renderAsync(
|
||||
VolumeBackupEmail({
|
||||
projectName,
|
||||
applicationName,
|
||||
volumeName,
|
||||
serviceType,
|
||||
type,
|
||||
errorMessage,
|
||||
backupSize,
|
||||
date: date.toISOString(),
|
||||
}),
|
||||
);
|
||||
await sendEmailNotification(email, subject, htmlContent);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,51 @@ import { getS3Credentials, normalizeS3Path } from "../backups/utils";
|
||||
import { sendVolumeBackupNotifications } from "../notifications/volume-backup";
|
||||
import { backupVolume } from "./backup";
|
||||
|
||||
// Helper functions to extract project info from volume backup
|
||||
const getProjectName = (
|
||||
volumeBackup: Awaited<ReturnType<typeof findVolumeBackupById>>,
|
||||
): string => {
|
||||
const services = [
|
||||
volumeBackup.application,
|
||||
volumeBackup.compose,
|
||||
volumeBackup.postgres,
|
||||
volumeBackup.mysql,
|
||||
volumeBackup.mariadb,
|
||||
volumeBackup.mongo,
|
||||
volumeBackup.redis,
|
||||
];
|
||||
|
||||
for (const service of services) {
|
||||
if (service?.environment?.project?.name) {
|
||||
return service.environment.project.name;
|
||||
}
|
||||
}
|
||||
|
||||
return "Unknown Project";
|
||||
};
|
||||
|
||||
const getOrganizationId = (
|
||||
volumeBackup: Awaited<ReturnType<typeof findVolumeBackupById>>,
|
||||
): string => {
|
||||
const services = [
|
||||
volumeBackup.application,
|
||||
volumeBackup.compose,
|
||||
volumeBackup.postgres,
|
||||
volumeBackup.mysql,
|
||||
volumeBackup.mariadb,
|
||||
volumeBackup.mongo,
|
||||
volumeBackup.redis,
|
||||
];
|
||||
|
||||
for (const service of services) {
|
||||
if (service?.environment?.project?.organizationId) {
|
||||
return service.environment.project.organizationId;
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
export const scheduleVolumeBackup = async (volumeBackupId: string) => {
|
||||
const volumeBackup = await findVolumeBackupById(volumeBackupId);
|
||||
scheduleJob(volumeBackupId, volumeBackup.cronExpression, async () => {
|
||||
@@ -62,7 +107,8 @@ export const runVolumeBackup = async (volumeBackupId: string) => {
|
||||
title: "Volume Backup",
|
||||
description: "Volume Backup",
|
||||
});
|
||||
|
||||
const projectName = getProjectName(volumeBackup);
|
||||
const organizationId = getOrganizationId(volumeBackup);
|
||||
try {
|
||||
const command = await backupVolume(volumeBackup);
|
||||
|
||||
@@ -79,55 +125,20 @@ export const runVolumeBackup = async (volumeBackupId: string) => {
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
|
||||
// Send success notification
|
||||
try {
|
||||
const projectName =
|
||||
volumeBackup.application?.environment?.project?.name ||
|
||||
volumeBackup.compose?.environment?.project?.name ||
|
||||
volumeBackup.postgres?.environment?.project?.name ||
|
||||
volumeBackup.mysql?.environment?.project?.name ||
|
||||
volumeBackup.mariadb?.environment?.project?.name ||
|
||||
volumeBackup.mongo?.environment?.project?.name ||
|
||||
volumeBackup.redis?.environment?.project?.name ||
|
||||
"Unknown Project";
|
||||
// Map service type to match notification function expectations
|
||||
const mappedServiceType =
|
||||
volumeBackup.serviceType === "mongo"
|
||||
? "mongodb"
|
||||
: volumeBackup.serviceType;
|
||||
|
||||
const organizationId =
|
||||
volumeBackup.application?.environment?.project?.organizationId ||
|
||||
volumeBackup.compose?.environment?.project?.organizationId ||
|
||||
volumeBackup.postgres?.environment?.project?.organizationId ||
|
||||
volumeBackup.mysql?.environment?.project?.organizationId ||
|
||||
volumeBackup.mariadb?.environment?.project?.organizationId ||
|
||||
volumeBackup.mongo?.environment?.project?.organizationId ||
|
||||
volumeBackup.redis?.environment?.project?.organizationId ||
|
||||
"";
|
||||
|
||||
// Map service type to match notification function expectations
|
||||
const mappedServiceType =
|
||||
volumeBackup.serviceType === "mongo"
|
||||
? "mongodb"
|
||||
: volumeBackup.serviceType;
|
||||
|
||||
await sendVolumeBackupNotifications({
|
||||
projectName,
|
||||
applicationName: volumeBackup.name,
|
||||
volumeName: volumeBackup.volumeName,
|
||||
serviceType: mappedServiceType as
|
||||
| "application"
|
||||
| "postgres"
|
||||
| "mysql"
|
||||
| "mongodb"
|
||||
| "mariadb"
|
||||
| "redis"
|
||||
| "compose",
|
||||
type: "success",
|
||||
organizationId,
|
||||
});
|
||||
} catch (notificationError) {
|
||||
console.error(
|
||||
"Failed to send volume backup success notification:",
|
||||
notificationError,
|
||||
);
|
||||
}
|
||||
await sendVolumeBackupNotifications({
|
||||
projectName,
|
||||
applicationName: volumeBackup.name,
|
||||
volumeName: volumeBackup.volumeName,
|
||||
serviceType: mappedServiceType,
|
||||
type: "success",
|
||||
organizationId,
|
||||
});
|
||||
} catch (error) {
|
||||
const { VOLUME_BACKUPS_PATH } = paths(!!serverId);
|
||||
const volumeBackupPath = path.join(
|
||||
@@ -144,56 +155,19 @@ export const runVolumeBackup = async (volumeBackupId: string) => {
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
|
||||
// Send error notification
|
||||
try {
|
||||
const projectName =
|
||||
volumeBackup.application?.environment?.project?.name ||
|
||||
volumeBackup.compose?.environment?.project?.name ||
|
||||
volumeBackup.postgres?.environment?.project?.name ||
|
||||
volumeBackup.mysql?.environment?.project?.name ||
|
||||
volumeBackup.mariadb?.environment?.project?.name ||
|
||||
volumeBackup.mongo?.environment?.project?.name ||
|
||||
volumeBackup.redis?.environment?.project?.name ||
|
||||
"Unknown Project";
|
||||
const mappedServiceType =
|
||||
volumeBackup.serviceType === "mongo"
|
||||
? "mongodb"
|
||||
: volumeBackup.serviceType;
|
||||
|
||||
const organizationId =
|
||||
volumeBackup.application?.environment?.project?.organizationId ||
|
||||
volumeBackup.compose?.environment?.project?.organizationId ||
|
||||
volumeBackup.postgres?.environment?.project?.organizationId ||
|
||||
volumeBackup.mysql?.environment?.project?.organizationId ||
|
||||
volumeBackup.mariadb?.environment?.project?.organizationId ||
|
||||
volumeBackup.mongo?.environment?.project?.organizationId ||
|
||||
volumeBackup.redis?.environment?.project?.organizationId ||
|
||||
"";
|
||||
|
||||
// Map service type to match notification function expectations
|
||||
const mappedServiceType =
|
||||
volumeBackup.serviceType === "mongo"
|
||||
? "mongodb"
|
||||
: volumeBackup.serviceType;
|
||||
|
||||
await sendVolumeBackupNotifications({
|
||||
projectName,
|
||||
applicationName: volumeBackup.name,
|
||||
volumeName: volumeBackup.volumeName,
|
||||
serviceType: mappedServiceType as
|
||||
| "application"
|
||||
| "postgres"
|
||||
| "mysql"
|
||||
| "mongodb"
|
||||
| "mariadb"
|
||||
| "redis"
|
||||
| "compose",
|
||||
type: "error",
|
||||
organizationId,
|
||||
errorMessage: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
} catch (notificationError) {
|
||||
console.error(
|
||||
"Failed to send volume backup error notification:",
|
||||
notificationError,
|
||||
);
|
||||
}
|
||||
|
||||
console.error(error);
|
||||
await sendVolumeBackupNotifications({
|
||||
projectName,
|
||||
applicationName: volumeBackup.name,
|
||||
volumeName: volumeBackup.volumeName,
|
||||
serviceType: mappedServiceType,
|
||||
type: "error",
|
||||
organizationId,
|
||||
errorMessage: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user