Enhance schedule initialization and management features

- Introduced a new `initSchedules` function to initialize and schedule active schedules from the database, improving the management of scheduled tasks.
- Updated the `createSchedule` and `updateSchedule` functions to handle scheduling jobs based on the enabled status of schedules, ensuring proper job management.
- Refactored the `removeScheduleJob` utility to cancel existing scheduled jobs, enhancing the flexibility of schedule updates.
- Improved the `HandleSchedules` and `ShowSchedules` components by removing unused imports and enhancing the user interface for better clarity and usability.
This commit is contained in:
Mauricio Siu
2025-05-02 23:05:39 -06:00
parent 98d0f1d5bf
commit 3072795232
7 changed files with 71 additions and 16 deletions

View File

@@ -14,8 +14,6 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
import { import {
Clock,
Terminal,
Info, Info,
PlusCircle, PlusCircle,
PenBoxIcon, PenBoxIcon,
@@ -49,7 +47,8 @@ import type { CacheType } from "../../compose/domains/add-domain";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor"; import { CodeEditor } from "@/components/shared/code-editor";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const commonCronExpressions = [
export const commonCronExpressions = [
{ label: "Every minute", value: "* * * * *" }, { label: "Every minute", value: "* * * * *" },
{ label: "Every hour", value: "0 * * * *" }, { label: "Every hour", value: "0 * * * *" },
{ label: "Every day at midnight", value: "0 0 * * *" }, { label: "Every day at midnight", value: "0 0 * * *" },
@@ -361,7 +360,6 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel className="flex items-center gap-2"> <FormLabel className="flex items-center gap-2">
<Clock className="w-4 h-4" />
Task Name Task Name
</FormLabel> </FormLabel>
<FormControl> <FormControl>
@@ -381,7 +379,6 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel className="flex items-center gap-2"> <FormLabel className="flex items-center gap-2">
<Clock className="w-4 h-4" />
Schedule Schedule
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
@@ -444,7 +441,6 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel className="flex items-center gap-2"> <FormLabel className="flex items-center gap-2">
<Terminal className="w-4 h-4" />
Shell Type Shell Type
</FormLabel> </FormLabel>
<Select <Select
@@ -474,7 +470,6 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel className="flex items-center gap-2"> <FormLabel className="flex items-center gap-2">
<Terminal className="w-4 h-4" />
Command Command
</FormLabel> </FormLabel>
<FormControl> <FormControl>

View File

@@ -75,7 +75,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
</CardHeader> </CardHeader>
<CardContent className="px-0"> <CardContent className="px-0">
{isLoadingSchedules ? ( {isLoadingSchedules ? (
<div className="flex gap-4 w-full items-center justify-center text-center mx-auto"> <div className="flex gap-4 w-full items-center justify-center text-center mx-auto min-h-[45vh]">
<Loader2 className="size-4 text-muted-foreground/70 transition-colors animate-spin self-center" /> <Loader2 className="size-4 text-muted-foreground/70 transition-colors animate-spin self-center" />
<span className="text-sm text-muted-foreground/70"> <span className="text-sm text-muted-foreground/70">
Loading scheduled tasks... Loading scheduled tasks...
@@ -146,7 +146,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<ShowSchedulesLogs <ShowSchedulesLogs
deployments={deployments || []} deployments={deployments || []}
serverId={serverId} serverId={serverId || undefined}
> >
<Button variant="ghost" size="icon"> <Button variant="ghost" size="icon">
<ClipboardList className="size-4 transition-colors " /> <ClipboardList className="size-4 transition-colors " />

View File

@@ -6,6 +6,7 @@ import {
createDefaultServerTraefikConfig, createDefaultServerTraefikConfig,
createDefaultTraefikConfig, createDefaultTraefikConfig,
initCronJobs, initCronJobs,
initSchedules,
initializeNetwork, initializeNetwork,
sendDokployRestartNotifications, sendDokployRestartNotifications,
setupDirectories, setupDirectories,
@@ -49,6 +50,7 @@ void app.prepare().then(async () => {
createDefaultServerTraefikConfig(); createDefaultServerTraefikConfig();
await migration(); await migration();
await initCronJobs(); await initCronJobs();
await initSchedules();
await sendDokployRestartNotifications(); await sendDokployRestartNotifications();
} }

View File

@@ -128,3 +128,4 @@ export {
} from "./utils/access-log/handler"; } from "./utils/access-log/handler";
export * from "./utils/schedules/utils"; export * from "./utils/schedules/utils";
export * from "./utils/schedules/index";

View File

@@ -8,8 +8,10 @@ import type {
updateScheduleSchema, updateScheduleSchema,
} from "../db/schema/schedule"; } from "../db/schema/schedule";
import { execAsync, execAsyncRemote } from "../utils/process/execAsync"; import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
import { paths } from "../constants"; import { IS_CLOUD, paths } from "../constants";
import path from "node:path"; import path from "node:path";
import { encodeBase64 } from "../utils/docker/utils";
import { scheduleJob, removeScheduleJob } from "../utils/schedules/utils";
export type ScheduleExtended = Awaited<ReturnType<typeof findScheduleById>>; export type ScheduleExtended = Awaited<ReturnType<typeof findScheduleById>>;
@@ -26,6 +28,11 @@ export const createSchedule = async (
) { ) {
await handleScript(newSchedule); await handleScript(newSchedule);
} }
if (newSchedule?.enabled) {
scheduleJob(newSchedule);
}
return newSchedule; return newSchedule;
}; };
@@ -50,12 +57,16 @@ export const findScheduleById = async (scheduleId: string) => {
export const deleteSchedule = async (scheduleId: string) => { export const deleteSchedule = async (scheduleId: string) => {
const schedule = await findScheduleById(scheduleId); const schedule = await findScheduleById(scheduleId);
const serverId =
schedule?.serverId ||
schedule?.application?.serverId ||
schedule?.compose?.serverId;
const { SCHEDULES_PATH } = paths(!!serverId);
const { SCHEDULES_PATH } = paths(!!schedule?.serverId);
const fullPath = path.join(SCHEDULES_PATH, schedule?.appName || ""); const fullPath = path.join(SCHEDULES_PATH, schedule?.appName || "");
const command = `rm -rf ${fullPath}`; const command = `rm -rf ${fullPath}`;
if (schedule.serverId) { if (serverId) {
await execAsyncRemote(schedule.serverId, command); await execAsyncRemote(serverId, command);
} else { } else {
await execAsync(command); await execAsync(command);
} }
@@ -89,19 +100,32 @@ export const updateSchedule = async (
) { ) {
await handleScript(updatedSchedule); await handleScript(updatedSchedule);
} }
console.log("updatedSchedule", updatedSchedule);
if (IS_CLOUD) {
// scheduleJob(updatedSchedule);
} else {
if (updatedSchedule?.enabled) {
removeScheduleJob(scheduleId);
scheduleJob(updatedSchedule);
} else {
removeScheduleJob(scheduleId);
}
}
return updatedSchedule; return updatedSchedule;
}; };
const handleScript = async (schedule: Schedule) => { const handleScript = async (schedule: Schedule) => {
const { SCHEDULES_PATH } = paths(!!schedule?.serverId); const { SCHEDULES_PATH } = paths(!!schedule?.serverId);
const fullPath = path.join(SCHEDULES_PATH, schedule?.appName || ""); const fullPath = path.join(SCHEDULES_PATH, schedule?.appName || "");
const encodedContent = encodeBase64(schedule?.script || "");
const script = ` const script = `
mkdir -p ${fullPath} mkdir -p ${fullPath}
rm -f ${fullPath}/script.sh rm -f ${fullPath}/script.sh
touch ${fullPath}/script.sh touch ${fullPath}/script.sh
chmod +x ${fullPath}/script.sh chmod +x ${fullPath}/script.sh
echo "${schedule?.script}" > ${fullPath}/script.sh echo "${encodedContent}" | base64 -d > ${fullPath}/script.sh
`; `;
if (schedule?.scheduleType === "dokploy-server") { if (schedule?.scheduleType === "dokploy-server") {

View File

@@ -0,0 +1,28 @@
import { db } from "../../db/index";
import { schedules } from "@dokploy/server/db/schema";
import { eq } from "drizzle-orm";
import { scheduleJob } from "./utils";
export const initSchedules = async () => {
try {
const schedulesResult = await db.query.schedules.findMany({
where: eq(schedules.enabled, true),
with: {
server: true,
application: true,
compose: true,
user: true,
},
});
console.log(`Initializing ${schedulesResult.length} schedules`);
for (const schedule of schedulesResult) {
scheduleJob(schedule);
console.log(
`Initialized schedule: ${schedule.name} ${schedule.scheduleType}`,
);
}
} catch (error) {
console.log(`Error initializing schedules: ${error}`);
}
};

View File

@@ -1,6 +1,6 @@
import type { Schedule } from "@dokploy/server/db/schema/schedule"; import type { Schedule } from "@dokploy/server/db/schema/schedule";
import { findScheduleById } from "@dokploy/server/services/schedule"; import { findScheduleById } from "@dokploy/server/services/schedule";
import { scheduleJob as scheduleJobNode } from "node-schedule"; import { scheduledJobs, scheduleJob as scheduleJobNode } from "node-schedule";
import { getComposeContainer, getServiceContainerIV2 } from "../docker/utils"; import { getComposeContainer, getServiceContainerIV2 } from "../docker/utils";
import { execAsyncRemote } from "../process/execAsync"; import { execAsyncRemote } from "../process/execAsync";
import { spawnAsync } from "../process/spawnAsync"; import { spawnAsync } from "../process/spawnAsync";
@@ -18,6 +18,11 @@ export const scheduleJob = (schedule: Schedule) => {
}); });
}; };
export const removeScheduleJob = (scheduleId: string) => {
const currentJob = scheduledJobs[scheduleId];
currentJob?.cancel();
};
export const runCommand = async (scheduleId: string) => { export const runCommand = async (scheduleId: string) => {
const { const {
application, application,