Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d2a9eb3
feat(db): add dokploy update to notif model and last update to organi…
Bima42 Nov 6, 2025
c915969
feat: create the update notification sender with mail template
Bima42 Nov 6, 2025
ec98b5e
feat(server): add cron job for checking update every ten minutes
Bima42 Nov 6, 2025
7088fa9
feat(ui): allow update dokploy to be toggled in notification actions
Bima42 Nov 6, 2025
b633836
chore: regenerate migration after merge
Bima42 Nov 9, 2025
4cbbbfd
Merge branch 'canary' into feat/2663-notification-for-dokploy-update
Bima42 Dec 5, 2025
5bdb98e
chore: merge and update migration
Bima42 Dec 5, 2025
fd4e997
Merge branch 'canary' into feat/2663-notification-for-dokploy-update
Bima42 Dec 14, 2025
2f24256
chore: merge and update migration
Bima42 Dec 14, 2025
d2950de
[autofix.ci] apply automated fixes
autofix-ci[bot] Dec 14, 2025
a3fa25f
chore: clear old migration
Bima42 Dec 14, 2025
2d0c486
Merge branch 'canary' into feat/2663-notification-for-dokploy-update
Siumauricio Apr 3, 2026
e077be4
Update Node.js version and dependencies; enhance documentation and Do…
Siumauricio Apr 3, 2026
af1d47a
Update Node.js version and dependencies; enhance documentation and Do…
Siumauricio Apr 3, 2026
f3988ba
chore: clear migration
Bima42 Apr 13, 2026
8088a12
Merge branch 'canary' into feat/2663-notification-for-dokploy-update
Bima42 Apr 13, 2026
898b716
chore: regenerate migration
Bima42 Apr 13, 2026
289119e
Merge branch 'canary' into feat/2663-notification-for-dokploy-update
Bima42 Apr 14, 2026
f3c8262
fix: ts errors
Bima42 Apr 14, 2026
f3308bc
chore: remove old files
Bima42 Apr 14, 2026
9fcb076
chore: remove old files
Bima42 Apr 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { expect, test } from "vitest";
import { parse } from "yaml";

test("Generate random hash with 8 characters", () => {
const hash = generateRandomHash();

expect(hash).toBeDefined();
expect(hash.length).toBe(8);
});

const composeFile3 = `
version: "3.8"

services:
web:
image: nginx:latest
volumes_from:
- shared

api:
image: myapi:latest
volumes_from:
- shared

shared:
image: busybox
volumes:
- /data

networks:
default:
driver: bridge
`;

test("Add suffix to service names with volumes_from in compose file", () => {
const composeData = parse(composeFile3) as ComposeSpecification;

const suffix = generateRandomHash();

if (!composeData.services) {
return;
}
const updatedComposeData = addSuffixToServiceNames(
composeData.services,
suffix,
);
const actualComposeData = { ...composeData, services: updatedComposeData };

// Verificar que la nueva clave del servicio tiene el prefijo y la vieja clave no existe
expect(actualComposeData.services).toHaveProperty(`web-${suffix}`);
expect(actualComposeData.services).not.toHaveProperty("web");

// Verificar que la configuración de la imagen sigue igual
expect(actualComposeData.services?.[`web-${suffix}`]?.image).toBe(
"nginx:latest",
);
expect(actualComposeData.services?.[`api-${suffix}`]?.image).toBe(
"myapi:latest",
);

// Verificar que los nombres en volumes_from tienen el prefijo
expect(actualComposeData.services?.[`web-${suffix}`]?.volumes_from).toContain(
`shared-${suffix}`,
);
expect(actualComposeData.services?.[`api-${suffix}`]?.volumes_from).toContain(
`shared-${suffix}`,
);

// Verificar que el servicio shared también tiene el prefijo
expect(actualComposeData.services).toHaveProperty(`shared-${suffix}`);
expect(actualComposeData.services).not.toHaveProperty("shared");
expect(actualComposeData.services?.[`shared-${suffix}`]?.image).toBe(
"busybox",
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const notificationBaseSchema = z.object({
dokployRestart: z.boolean().default(false),
dockerCleanup: z.boolean().default(false),
serverThreshold: z.boolean().default(false),
dokployUpdate: z.boolean().default(false),
});

export const notificationSchema = z.discriminatedUnion("type", [
Expand Down Expand Up @@ -364,6 +365,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
name: notification.name,
type: notification.notificationType,
serverThreshold: notification.serverThreshold,
dokployUpdate: notification.dokployUpdate,
});
} else if (notification.notificationType === "telegram") {
form.reset({
Expand All @@ -380,6 +382,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
name: notification.name,
dockerCleanup: notification.dockerCleanup,
serverThreshold: notification.serverThreshold,
dokployUpdate: notification.dokployUpdate,
});
} else if (notification.notificationType === "discord") {
form.reset({
Expand All @@ -395,6 +398,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
name: notification.name,
dockerCleanup: notification.dockerCleanup,
serverThreshold: notification.serverThreshold,
dokployUpdate: notification.dokployUpdate,
});
} else if (notification.notificationType === "email") {
form.reset({
Expand All @@ -414,6 +418,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
name: notification.name,
dockerCleanup: notification.dockerCleanup,
serverThreshold: notification.serverThreshold,
dokployUpdate: notification.dokployUpdate,
});
} else if (notification.notificationType === "resend") {
form.reset({
Expand All @@ -430,6 +435,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
name: notification.name,
dockerCleanup: notification.dockerCleanup,
serverThreshold: notification.serverThreshold,
dokployUpdate: notification.dokployUpdate,
});
} else if (notification.notificationType === "gotify") {
form.reset({
Expand All @@ -446,6 +452,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
serverUrl: notification.gotify?.serverUrl,
name: notification.name,
dockerCleanup: notification.dockerCleanup,
dokployUpdate: notification.dokployUpdate,
});
} else if (notification.notificationType === "ntfy") {
form.reset({
Expand All @@ -463,6 +470,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
name: notification.name,
dockerCleanup: notification.dockerCleanup,
serverThreshold: notification.serverThreshold,
dokployUpdate: notification.dokployUpdate,
});
} else if (notification.notificationType === "mattermost") {
form.reset({
Expand All @@ -479,6 +487,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
name: notification.name,
dockerCleanup: notification.dockerCleanup,
serverThreshold: notification.serverThreshold,
dokployUpdate: notification.dokployUpdate,
});
} else if (notification.notificationType === "lark") {
form.reset({
Expand All @@ -493,6 +502,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
dockerCleanup: notification.dockerCleanup,
volumeBackup: notification.volumeBackup,
serverThreshold: notification.serverThreshold,
dokployUpdate: notification.dokployUpdate,
});
} else if (notification.notificationType === "teams") {
form.reset({
Expand All @@ -507,6 +517,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
name: notification.name,
dockerCleanup: notification.dockerCleanup,
serverThreshold: notification.serverThreshold,
dokployUpdate: notification.dokployUpdate,
});
} else if (notification.notificationType === "custom") {
form.reset({
Expand All @@ -529,6 +540,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
volumeBackup: notification.volumeBackup,
dockerCleanup: notification.dockerCleanup,
serverThreshold: notification.serverThreshold,
dokployUpdate: notification.dokployUpdate,
});
} else if (notification.notificationType === "pushover") {
form.reset({
Expand All @@ -547,6 +559,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
name: notification.name,
dockerCleanup: notification.dockerCleanup,
serverThreshold: notification.serverThreshold,
dokployUpdate: notification.dokployUpdate,
});
}
} else {
Expand Down Expand Up @@ -579,6 +592,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
volumeBackup,
dockerCleanup,
serverThreshold,
dokployUpdate,
} = data;
let promise: Promise<unknown> | null = null;
if (data.type === "slack") {
Expand All @@ -596,6 +610,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
slackId: notification?.slackId || "",
notificationId: notificationId || "",
serverThreshold: serverThreshold,
dokployUpdate: dokployUpdate,
});
} else if (data.type === "telegram") {
promise = telegramMutation.mutateAsync({
Expand All @@ -613,6 +628,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
notificationId: notificationId || "",
telegramId: notification?.telegramId || "",
serverThreshold: serverThreshold,
dokployUpdate: dokployUpdate,
});
} else if (data.type === "discord") {
promise = discordMutation.mutateAsync({
Expand All @@ -629,6 +645,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
notificationId: notificationId || "",
discordId: notification?.discordId || "",
serverThreshold: serverThreshold,
dokployUpdate: dokployUpdate,
});
} else if (data.type === "email") {
promise = emailMutation.mutateAsync({
Expand All @@ -649,6 +666,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
notificationId: notificationId || "",
emailId: notification?.emailId || "",
serverThreshold: serverThreshold,
dokployUpdate: dokployUpdate,
});
} else if (data.type === "resend") {
promise = resendMutation.mutateAsync({
Expand All @@ -666,6 +684,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
notificationId: notificationId || "",
resendId: notification?.resendId || "",
serverThreshold: serverThreshold,
dokployUpdate: dokployUpdate,
});
} else if (data.type === "gotify") {
promise = gotifyMutation.mutateAsync({
Expand All @@ -683,6 +702,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
decoration: data.decoration,
notificationId: notificationId || "",
gotifyId: notification?.gotifyId || "",
dokployUpdate: dokployUpdate,
});
} else if (data.type === "ntfy") {
promise = ntfyMutation.mutateAsync({
Expand All @@ -700,6 +720,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
dockerCleanup: dockerCleanup,
notificationId: notificationId || "",
ntfyId: notification?.ntfyId || "",
dokployUpdate: dokployUpdate,
});
} else if (data.type === "mattermost") {
promise = mattermostMutation.mutateAsync({
Expand All @@ -717,6 +738,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
notificationId: notificationId || "",
mattermostId: notification?.mattermostId || "",
serverThreshold: serverThreshold,
dokployUpdate: dokployUpdate,
});
} else if (data.type === "lark") {
promise = larkMutation.mutateAsync({
Expand All @@ -732,6 +754,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
notificationId: notificationId || "",
larkId: notification?.larkId || "",
serverThreshold: serverThreshold,
dokployUpdate: dokployUpdate,
});
} else if (data.type === "teams") {
promise = teamsMutation.mutateAsync({
Expand All @@ -747,6 +770,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
notificationId: notificationId || "",
teamsId: notification?.teamsId || "",
serverThreshold: serverThreshold,
dokployUpdate: dokployUpdate,
});
} else if (data.type === "custom") {
// Convert headers array to object
Expand All @@ -773,6 +797,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
name: data.name,
dockerCleanup: dockerCleanup,
serverThreshold: serverThreshold,
dokployUpdate: dokployUpdate,
notificationId: notificationId || "",
customId: notification?.customId || "",
});
Expand All @@ -796,6 +821,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
name: data.name,
dockerCleanup: dockerCleanup,
serverThreshold: serverThreshold,
dokployUpdate: dokployUpdate,
notificationId: notificationId || "",
pushoverId: notification?.pushoverId || "",
});
Expand Down Expand Up @@ -1992,6 +2018,30 @@ export const HandleNotifications = ({ notificationId }: Props) => {
)}
/>
)}

{!isCloud && (
<FormField
control={form.control}
name="dokployUpdate"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
<div className="">
<FormLabel>Dokploy Updates</FormLabel>
<FormDescription>
Get notified when a new Dokploy version is
available.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
)}
</div>
</div>
</form>
Expand Down
2 changes: 2 additions & 0 deletions apps/dokploy/drizzle/0166_lethal_karnak.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE "organization" ADD COLUMN "lastNotifiedUpdateVersion" text;--> statement-breakpoint
ALTER TABLE "notification" ADD COLUMN "dokployUpdate" boolean DEFAULT false NOT NULL;
Loading
Loading