Merge branch 'canary' into 187-backups-for-docker-compose

This commit is contained in:
Mauricio Siu
2025-05-03 09:48:24 -06:00
37 changed files with 2133 additions and 169 deletions

View File

@@ -9,12 +9,13 @@ import {
CardTitle,
} from "@/components/ui/card";
import { type RouterOutputs, api } from "@/utils/api";
import { RocketIcon } from "lucide-react";
import { RocketIcon, Clock } from "lucide-react";
import React, { useEffect, useState } from "react";
import { CancelQueues } from "./cancel-queues";
import { RefreshToken } from "./refresh-token";
import { ShowDeployment } from "./show-deployment";
import { Badge } from "@/components/ui/badge";
import { formatDuration } from "../schedules/show-schedules-logs";
interface Props {
applicationId: string;
}
@@ -96,8 +97,23 @@ export const ShowDeployments = ({ applicationId }: Props) => {
)}
</div>
<div className="flex flex-col items-end gap-2">
<div className="text-sm capitalize text-muted-foreground">
<div className="text-sm capitalize text-muted-foreground flex items-center gap-2">
<DateTooltip date={deployment.createdAt} />
{deployment.startedAt && deployment.finishedAt && (
<Badge
variant="outline"
className="text-[10px] gap-1 flex items-center"
>
<Clock className="size-3" />
{formatDuration(
Math.floor(
(new Date(deployment.finishedAt).getTime() -
new Date(deployment.startedAt).getTime()) /
1000,
),
)}
</Badge>
)}
</div>
<Button

View File

@@ -0,0 +1,538 @@
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
FormDescription,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import {
Info,
PlusCircle,
PenBoxIcon,
RefreshCw,
DatabaseZap,
} from "lucide-react";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Switch } from "@/components/ui/switch";
import { useEffect, useState } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { toast } from "sonner";
import type { CacheType } from "../../compose/domains/add-domain";
import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor";
import { cn } from "@/lib/utils";
export const commonCronExpressions = [
{ label: "Every minute", value: "* * * * *" },
{ label: "Every hour", value: "0 * * * *" },
{ label: "Every day at midnight", value: "0 0 * * *" },
{ label: "Every Sunday at midnight", value: "0 0 * * 0" },
{ label: "Every month on the 1st at midnight", value: "0 0 1 * *" },
{ label: "Every 15 minutes", value: "*/15 * * * *" },
{ label: "Every weekday at midnight", value: "0 0 * * 1-5" },
];
const formSchema = z
.object({
name: z.string().min(1, "Name is required"),
cronExpression: z.string().min(1, "Cron expression is required"),
shellType: z.enum(["bash", "sh"]).default("bash"),
command: z.string(),
enabled: z.boolean().default(true),
serviceName: z.string(),
scheduleType: z.enum([
"application",
"compose",
"server",
"dokploy-server",
]),
script: z.string(),
})
.superRefine((data, ctx) => {
if (data.scheduleType === "compose" && !data.serviceName) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Service name is required",
path: ["serviceName"],
});
}
if (
(data.scheduleType === "dokploy-server" ||
data.scheduleType === "server") &&
!data.script
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Script is required",
path: ["script"],
});
}
if (
(data.scheduleType === "application" ||
data.scheduleType === "compose") &&
!data.command
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Command is required",
path: ["command"],
});
}
});
interface Props {
id?: string;
scheduleId?: string;
scheduleType?: "application" | "compose" | "server" | "dokploy-server";
}
export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const [cacheType, setCacheType] = useState<CacheType>("cache");
const utils = api.useUtils();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
name: "",
cronExpression: "",
shellType: "bash",
command: "",
enabled: true,
serviceName: "",
scheduleType: scheduleType || "application",
script: "",
},
});
const scheduleTypeForm = form.watch("scheduleType");
const { data: schedule } = api.schedule.one.useQuery(
{ scheduleId: scheduleId || "" },
{ enabled: !!scheduleId },
);
const {
data: services,
isFetching: isLoadingServices,
error: errorServices,
refetch: refetchServices,
} = api.compose.loadServices.useQuery(
{
composeId: id || "",
type: cacheType,
},
{
retry: false,
refetchOnWindowFocus: false,
enabled: !!id && scheduleType === "compose",
},
);
useEffect(() => {
if (scheduleId && schedule) {
form.reset({
name: schedule.name,
cronExpression: schedule.cronExpression,
shellType: schedule.shellType,
command: schedule.command,
enabled: schedule.enabled,
serviceName: schedule.serviceName || "",
scheduleType: schedule.scheduleType,
script: schedule.script || "",
});
}
}, [form, schedule, scheduleId]);
const { mutateAsync, isLoading } = scheduleId
? api.schedule.update.useMutation()
: api.schedule.create.useMutation();
const onSubmit = async (values: z.infer<typeof formSchema>) => {
if (!id && !scheduleId) return;
await mutateAsync({
...values,
scheduleId: scheduleId || "",
...(scheduleType === "application" && {
applicationId: id || "",
}),
...(scheduleType === "compose" && {
composeId: id || "",
}),
...(scheduleType === "server" && {
serverId: id || "",
}),
...(scheduleType === "dokploy-server" && {
userId: id || "",
}),
})
.then(() => {
toast.success(
`Schedule ${scheduleId ? "updated" : "created"} successfully`,
);
utils.schedule.list.invalidate({
id,
scheduleType,
});
setIsOpen(false);
})
.catch((error) => {
toast.error(
error instanceof Error ? error.message : "An unknown error occurred",
);
});
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
{scheduleId ? (
<Button
variant="ghost"
size="icon"
className="group hover:bg-blue-500/10"
>
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
</Button>
) : (
<Button>
<PlusCircle className="w-4 h-4 mr-2" />
Add Schedule
</Button>
)}
</DialogTrigger>
<DialogContent
className={cn(
"max-h-screen overflow-y-auto",
scheduleTypeForm === "dokploy-server" || scheduleTypeForm === "server"
? "max-h-[95vh] sm:max-w-2xl"
: " sm:max-w-lg",
)}
>
<DialogHeader>
<DialogTitle>{scheduleId ? "Edit" : "Create"} Schedule</DialogTitle>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
{scheduleTypeForm === "compose" && (
<div className="flex flex-col w-full gap-4">
{errorServices && (
<AlertBlock
type="warning"
className="[overflow-wrap:anywhere]"
>
{errorServices?.message}
</AlertBlock>
)}
<FormField
control={form.control}
name="serviceName"
render={({ field }) => (
<FormItem className="w-full">
<FormLabel>Service Name</FormLabel>
<div className="flex gap-2">
<Select
onValueChange={field.onChange}
defaultValue={field.value || ""}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a service name" />
</SelectTrigger>
</FormControl>
<SelectContent>
{services?.map((service, index) => (
<SelectItem
value={service}
key={`${service}-${index}`}
>
{service}
</SelectItem>
))}
<SelectItem value="none" disabled>
Empty
</SelectItem>
</SelectContent>
</Select>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="secondary"
type="button"
isLoading={isLoadingServices}
onClick={() => {
if (cacheType === "fetch") {
refetchServices();
} else {
setCacheType("fetch");
}
}}
>
<RefreshCw className="size-4 text-muted-foreground" />
</Button>
</TooltipTrigger>
<TooltipContent
side="left"
sideOffset={5}
className="max-w-[10rem]"
>
<p>
Fetch: Will clone the repository and load the
services
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="secondary"
type="button"
isLoading={isLoadingServices}
onClick={() => {
if (cacheType === "cache") {
refetchServices();
} else {
setCacheType("cache");
}
}}
>
<DatabaseZap className="size-4 text-muted-foreground" />
</Button>
</TooltipTrigger>
<TooltipContent
side="left"
sideOffset={5}
className="max-w-[10rem]"
>
<p>
Cache: If you previously deployed this compose,
it will read the services from the last
deployment/fetch from the repository
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormMessage />
</FormItem>
)}
/>
</div>
)}
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center gap-2">
Task Name
</FormLabel>
<FormControl>
<Input placeholder="Daily Database Backup" {...field} />
</FormControl>
<FormDescription>
A descriptive name for your scheduled task
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="cronExpression"
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center gap-2">
Schedule
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
<p>
Cron expression format: minute hour day month
weekday
</p>
<p>Example: 0 0 * * * (daily at midnight)</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</FormLabel>
<div className="flex flex-col gap-2">
<Select
onValueChange={(value) => {
field.onChange(value);
}}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a predefined schedule" />
</SelectTrigger>
</FormControl>
<SelectContent>
{commonCronExpressions.map((expr) => (
<SelectItem key={expr.value} value={expr.value}>
{expr.label} ({expr.value})
</SelectItem>
))}
</SelectContent>
</Select>
<div className="relative">
<FormControl>
<Input
placeholder="Custom cron expression (e.g., 0 0 * * *)"
{...field}
/>
</FormControl>
</div>
</div>
<FormDescription>
Choose a predefined schedule or enter a custom cron
expression
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{(scheduleTypeForm === "application" ||
scheduleTypeForm === "compose") && (
<>
<FormField
control={form.control}
name="shellType"
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center gap-2">
Shell Type
</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select shell type" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="bash">Bash</SelectItem>
<SelectItem value="sh">Sh</SelectItem>
</SelectContent>
</Select>
<FormDescription>
Choose the shell to execute your command
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="command"
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center gap-2">
Command
</FormLabel>
<FormControl>
<Input placeholder="npm run backup" {...field} />
</FormControl>
<FormDescription>
The command to execute in your container
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{(scheduleTypeForm === "dokploy-server" ||
scheduleTypeForm === "server") && (
<FormField
control={form.control}
name="script"
render={({ field }) => (
<FormItem>
<FormLabel>Script</FormLabel>
<FormControl>
<FormControl>
<CodeEditor
language="shell"
placeholder={`# This is a comment
echo "Hello, world!"
`}
className="h-96 font-mono"
{...field}
/>
</FormControl>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="enabled"
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center gap-2">
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
Enabled
</FormLabel>
</FormItem>
)}
/>
<Button type="submit" isLoading={isLoading} className="w-full">
{scheduleId ? "Update" : "Create"} Schedule
</Button>
</form>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,131 @@
import { DateTooltip } from "@/components/shared/date-tooltip";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import type { RouterOutputs } from "@/utils/api";
import { useState } from "react";
import { ShowDeployment } from "../deployments/show-deployment";
import { ClipboardList, Clock } from "lucide-react";
import { Badge } from "@/components/ui/badge";
interface Props {
deployments: RouterOutputs["deployment"]["all"];
serverId?: string;
children?: React.ReactNode;
}
export const formatDuration = (seconds: number) => {
if (seconds < 60) return `${seconds}s`;
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes}m ${remainingSeconds}s`;
};
export const ShowSchedulesLogs = ({
deployments,
serverId,
children,
}: Props) => {
const [activeLog, setActiveLog] = useState<
RouterOutputs["deployment"]["all"][number] | null
>(null);
const [isOpen, setIsOpen] = useState(false);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
{children ? (
children
) : (
<Button className="sm:w-auto w-full" size="sm" variant="outline">
View Logs
</Button>
)}
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl">
<DialogHeader>
<DialogTitle>Logs</DialogTitle>
<DialogDescription>
See all the logs for this schedule
</DialogDescription>
</DialogHeader>
{deployments.length > 0 ? (
<div className="grid gap-4">
{deployments.map((deployment, index) => (
<div
key={deployment.deploymentId}
className="flex items-center justify-between rounded-lg border p-4 gap-2"
>
<div className="flex flex-col">
<span className="flex items-center gap-4 font-medium capitalize text-foreground">
{index + 1} {deployment.status}
<StatusTooltip
status={deployment?.status}
className="size-2.5"
/>
</span>
<span className="text-sm text-muted-foreground">
{deployment.title}
</span>
{deployment.description && (
<span className="break-all text-sm text-muted-foreground">
{deployment.description}
</span>
)}
</div>
<div className="flex flex-col items-end gap-2">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<DateTooltip date={deployment.createdAt} />
{deployment.startedAt && deployment.finishedAt && (
<Badge
variant="outline"
className="text-[10px] gap-1 flex items-center"
>
<Clock className="size-3" />
{formatDuration(
Math.floor(
(new Date(deployment.finishedAt).getTime() -
new Date(deployment.startedAt).getTime()) /
1000,
),
)}
</Badge>
)}
</div>
<Button
onClick={() => {
setActiveLog(deployment);
}}
>
View
</Button>
</div>
</div>
))}
</div>
) : (
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
<ClipboardList className="size-12 mb-4" />
<p className="text-lg font-medium">No logs found</p>
<p className="text-sm">This schedule hasn't been executed yet</p>
</div>
)}
</DialogContent>
<ShowDeployment
serverId={serverId || ""}
open={Boolean(activeLog && activeLog.logPath !== null)}
onClose={() => setActiveLog(null)}
logPath={activeLog?.logPath || ""}
errorMessage={activeLog?.errorMessage || ""}
/>
</Dialog>
);
};

View File

@@ -0,0 +1,243 @@
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { HandleSchedules } from "./handle-schedules";
import {
Clock,
Play,
Terminal,
Trash2,
ClipboardList,
Loader2,
} from "lucide-react";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { toast } from "sonner";
import { ShowSchedulesLogs } from "./show-schedules-logs";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { DialogAction } from "@/components/shared/dialog-action";
interface Props {
id: string;
scheduleType?: "application" | "compose" | "server" | "dokploy-server";
}
export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
const {
data: schedules,
isLoading: isLoadingSchedules,
refetch: refetchSchedules,
} = api.schedule.list.useQuery(
{
id: id || "",
scheduleType,
},
{
enabled: !!id,
},
);
const utils = api.useUtils();
const { mutateAsync: deleteSchedule, isLoading: isDeleting } =
api.schedule.delete.useMutation();
const { mutateAsync: runManually, isLoading } =
api.schedule.runManually.useMutation();
return (
<Card className="border px-6 shadow-none bg-transparent h-full min-h-[50vh]">
<CardHeader className="px-0">
<div className="flex justify-between items-center">
<div className="flex flex-col gap-2">
<CardTitle className="text-xl font-bold flex items-center gap-2">
Scheduled Tasks
</CardTitle>
<CardDescription>
Schedule tasks to run automatically at specified intervals.
</CardDescription>
</div>
{schedules && schedules.length > 0 && (
<HandleSchedules id={id} scheduleType={scheduleType} />
)}
</div>
</CardHeader>
<CardContent className="px-0">
{isLoadingSchedules ? (
<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" />
<span className="text-sm text-muted-foreground/70">
Loading scheduled tasks...
</span>
</div>
) : schedules && schedules.length > 0 ? (
<div className="grid xl:grid-cols-2 gap-4 grid-cols-1 h-full">
{schedules.map((schedule) => {
const serverId =
schedule.serverId ||
schedule.application?.serverId ||
schedule.compose?.serverId;
const deployments = schedule.deployments;
return (
<div
key={schedule.scheduleId}
className=" flex items-center justify-between rounded-lg border p-3 transition-colors bg-muted/50"
>
<div className="flex items-start gap-3">
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-primary/5">
<Clock className="size-4 text-primary/70" />
</div>
<div className="space-y-1.5">
<div className="flex items-center gap-2">
<h3 className="text-sm font-medium leading-none">
{schedule.name}
</h3>
<Badge
variant={schedule.enabled ? "default" : "secondary"}
className="text-[10px] px-1 py-0"
>
{schedule.enabled ? "Enabled" : "Disabled"}
</Badge>
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Badge
variant="outline"
className="font-mono text-[10px] bg-transparent"
>
Cron: {schedule.cronExpression}
</Badge>
{schedule.scheduleType !== "server" &&
schedule.scheduleType !== "dokploy-server" && (
<>
<span className="text-xs text-muted-foreground/50">
</span>
<Badge
variant="outline"
className="font-mono text-[10px] bg-transparent"
>
{schedule.shellType}
</Badge>
</>
)}
</div>
{schedule.command && (
<div className="flex items-center gap-2">
<Terminal className="size-3.5 text-muted-foreground/70" />
<code className="font-mono text-[10px] text-muted-foreground/70">
{schedule.command}
</code>
</div>
)}
</div>
</div>
<div className="flex items-center gap-1.5">
<ShowSchedulesLogs
deployments={deployments || []}
serverId={serverId || undefined}
>
<Button variant="ghost" size="icon">
<ClipboardList className="size-4 transition-colors " />
</Button>
</ShowSchedulesLogs>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon"
isLoading={isLoading}
onClick={async () => {
await runManually({
scheduleId: schedule.scheduleId,
})
.then(async () => {
toast.success("Schedule run successfully");
await new Promise((resolve) =>
setTimeout(resolve, 1500),
);
refetchSchedules();
})
.catch(() => {
toast.error("Error running schedule:");
});
}}
>
<Play className="size-4 transition-colors" />
</Button>
</TooltipTrigger>
<TooltipContent>Run Manual Schedule</TooltipContent>
</Tooltip>
</TooltipProvider>
<HandleSchedules
scheduleId={schedule.scheduleId}
id={id}
scheduleType={scheduleType}
/>
<DialogAction
title="Delete Schedule"
description="Are you sure you want to delete this schedule?"
type="destructive"
onClick={async () => {
await deleteSchedule({
scheduleId: schedule.scheduleId,
})
.then(() => {
utils.schedule.list.invalidate({
id,
scheduleType,
});
toast.success("Schedule deleted successfully");
})
.catch(() => {
toast.error("Error deleting schedule");
});
}}
>
<Button
variant="ghost"
size="icon"
className="group hover:bg-red-500/10 "
isLoading={isDeleting}
>
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
</Button>
</DialogAction>
</div>
</div>
);
})}
</div>
) : (
<div className="flex flex-col gap-2 items-center justify-center py-12 rounded-lg">
<Clock className="size-8 mb-4 text-muted-foreground" />
<p className="text-lg font-medium text-muted-foreground">
No scheduled tasks
</p>
<p className="text-sm text-muted-foreground mt-1">
Create your first scheduled task to automate your workflows
</p>
<HandleSchedules id={id} scheduleType={scheduleType} />
</div>
)}
</CardContent>
</Card>
);
};

View File

@@ -9,12 +9,13 @@ import {
CardTitle,
} from "@/components/ui/card";
import { type RouterOutputs, api } from "@/utils/api";
import { RocketIcon } from "lucide-react";
import { RocketIcon, Clock } from "lucide-react";
import React, { useEffect, useState } from "react";
import { CancelQueuesCompose } from "./cancel-queues-compose";
import { RefreshTokenCompose } from "./refresh-token-compose";
import { ShowDeploymentCompose } from "./show-deployment-compose";
import { Badge } from "@/components/ui/badge";
import { formatDuration } from "@/components/dashboard/application/schedules/show-schedules-logs";
interface Props {
composeId: string;
}
@@ -96,8 +97,23 @@ export const ShowDeploymentsCompose = ({ composeId }: Props) => {
)}
</div>
<div className="flex flex-col items-end gap-2">
<div className="text-sm capitalize text-muted-foreground">
<div className="text-sm capitalize text-muted-foreground flex items-center gap-2">
<DateTooltip date={deployment.createdAt} />
{deployment.startedAt && deployment.finishedAt && (
<Badge
variant="outline"
className="text-[10px] gap-1 flex items-center"
>
<Clock className="size-3" />
{formatDuration(
Math.floor(
(new Date(deployment.finishedAt).getTime() -
new Date(deployment.startedAt).getTime()) /
1000,
),
)}
</Badge>
)}
</div>
<Button

View File

@@ -0,0 +1,28 @@
import { useState } from "react";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
interface Props {
serverId: string;
}
export const ShowSchedulesModal = ({ serverId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<DropdownMenuItem
className="w-full cursor-pointer "
onSelect={(e) => e.preventDefault()}
>
Show Schedules
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="sm:max-w-5xl overflow-y-auto max-h-screen ">
<ShowSchedules id={serverId} scheduleType="server" />
</DialogContent>
</Dialog>
);
};

View File

@@ -43,6 +43,7 @@ import { ShowMonitoringModal } from "./show-monitoring-modal";
import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal";
import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription";
import { ShowSchedulesModal } from "./show-schedules-modal";
export const ShowServers = () => {
const { t } = useTranslation("settings");
@@ -332,6 +333,10 @@ export const ShowServers = () => {
<ShowNodesModal
serverId={server.serverId}
/>
<ShowSchedulesModal
serverId={server.serverId}
/>
</>
)}
</DropdownMenuContent>

View File

@@ -10,6 +10,7 @@ import {
ChevronRight,
ChevronsUpDown,
CircleHelp,
Clock,
CreditCard,
Database,
Folder,
@@ -158,6 +159,14 @@ const MENU: Menu = {
// Only enabled in non-cloud environments
isEnabled: ({ isCloud }) => !isCloud,
},
{
isSingle: true,
title: "Schedules",
url: "/dashboard/schedules",
icon: Clock,
// Only enabled in non-cloud environments
isEnabled: ({ isCloud, auth }) => !isCloud && auth?.role === "owner",
},
{
isSingle: true,
title: "Traefik File System",

View File

@@ -0,0 +1,28 @@
CREATE TYPE "public"."scheduleType" AS ENUM('application', 'compose', 'server', 'dokploy-server');--> statement-breakpoint
CREATE TYPE "public"."shellType" AS ENUM('bash', 'sh');--> statement-breakpoint
CREATE TABLE "schedule" (
"scheduleId" text PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"cronExpression" text NOT NULL,
"appName" text NOT NULL,
"serviceName" text,
"shellType" "shellType" DEFAULT 'bash' NOT NULL,
"scheduleType" "scheduleType" DEFAULT 'application' NOT NULL,
"command" text NOT NULL,
"script" text,
"applicationId" text,
"composeId" text,
"serverId" text,
"userId" text,
"enabled" boolean DEFAULT true NOT NULL,
"createdAt" text NOT NULL
);
--> statement-breakpoint
ALTER TABLE "deployment" ADD COLUMN "startedAt" text;--> statement-breakpoint
ALTER TABLE "deployment" ADD COLUMN "finishedAt" text;--> statement-breakpoint
ALTER TABLE "deployment" ADD COLUMN "scheduleId" text;--> statement-breakpoint
ALTER TABLE "schedule" ADD CONSTRAINT "schedule_applicationId_application_applicationId_fk" FOREIGN KEY ("applicationId") REFERENCES "public"."application"("applicationId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "schedule" ADD CONSTRAINT "schedule_composeId_compose_composeId_fk" FOREIGN KEY ("composeId") REFERENCES "public"."compose"("composeId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "schedule" ADD CONSTRAINT "schedule_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "schedule" ADD CONSTRAINT "schedule_userId_user_temp_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "deployment" ADD CONSTRAINT "deployment_scheduleId_schedule_scheduleId_fk" FOREIGN KEY ("scheduleId") REFERENCES "public"."schedule"("scheduleId") ON DELETE cascade ON UPDATE no action;

View File

@@ -1,5 +1,5 @@
{
"id": "7fea81ef-e2a7-4a8b-b755-e98903a08b57",
"id": "c7eae4ce-5acc-439b-962f-bb2ef8922187",
"prevId": "7fb3716c-3cc6-4b18-b8d1-762844da26be",
"version": "7",
"dialect": "postgresql",
@@ -1770,12 +1770,6 @@
"primaryKey": false,
"notNull": true
},
"serviceName": {
"name": "serviceName",
"type": "text",
"primaryKey": false,
"notNull": false
},
"destinationId": {
"name": "destinationId",
"type": "text",
@@ -1788,14 +1782,6 @@
"primaryKey": false,
"notNull": false
},
"backupType": {
"name": "backupType",
"type": "backupType",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'database'"
},
"databaseType": {
"name": "databaseType",
"type": "databaseType",
@@ -1803,12 +1789,6 @@
"primaryKey": false,
"notNull": true
},
"composeId": {
"name": "composeId",
"type": "text",
"primaryKey": false,
"notNull": false
},
"postgresId": {
"name": "postgresId",
"type": "text",
@@ -1855,19 +1835,6 @@
"onDelete": "cascade",
"onUpdate": "no action"
},
"backup_composeId_compose_composeId_fk": {
"name": "backup_composeId_compose_composeId_fk",
"tableFrom": "backup",
"tableTo": "compose",
"columnsFrom": [
"composeId"
],
"columnsTo": [
"composeId"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"backup_postgresId_postgres_postgresId_fk": {
"name": "backup_postgresId_postgres_postgresId_fk",
"tableFrom": "backup",
@@ -2101,11 +2068,29 @@
"primaryKey": false,
"notNull": true
},
"startedAt": {
"name": "startedAt",
"type": "text",
"primaryKey": false,
"notNull": false
},
"finishedAt": {
"name": "finishedAt",
"type": "text",
"primaryKey": false,
"notNull": false
},
"errorMessage": {
"name": "errorMessage",
"type": "text",
"primaryKey": false,
"notNull": false
},
"scheduleId": {
"name": "scheduleId",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
@@ -2161,6 +2146,19 @@
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"deployment_scheduleId_schedule_scheduleId_fk": {
"name": "deployment_scheduleId_schedule_scheduleId_fk",
"tableFrom": "deployment",
"tableTo": "schedule",
"columnsFrom": [
"scheduleId"
],
"columnsTo": [
"scheduleId"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
@@ -5255,6 +5253,167 @@
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.schedule": {
"name": "schedule",
"schema": "",
"columns": {
"scheduleId": {
"name": "scheduleId",
"type": "text",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"cronExpression": {
"name": "cronExpression",
"type": "text",
"primaryKey": false,
"notNull": true
},
"appName": {
"name": "appName",
"type": "text",
"primaryKey": false,
"notNull": true
},
"serviceName": {
"name": "serviceName",
"type": "text",
"primaryKey": false,
"notNull": false
},
"shellType": {
"name": "shellType",
"type": "shellType",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'bash'"
},
"scheduleType": {
"name": "scheduleType",
"type": "scheduleType",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'application'"
},
"command": {
"name": "command",
"type": "text",
"primaryKey": false,
"notNull": true
},
"script": {
"name": "script",
"type": "text",
"primaryKey": false,
"notNull": false
},
"applicationId": {
"name": "applicationId",
"type": "text",
"primaryKey": false,
"notNull": false
},
"composeId": {
"name": "composeId",
"type": "text",
"primaryKey": false,
"notNull": false
},
"serverId": {
"name": "serverId",
"type": "text",
"primaryKey": false,
"notNull": false
},
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": false
},
"enabled": {
"name": "enabled",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"createdAt": {
"name": "createdAt",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"schedule_applicationId_application_applicationId_fk": {
"name": "schedule_applicationId_application_applicationId_fk",
"tableFrom": "schedule",
"tableTo": "application",
"columnsFrom": [
"applicationId"
],
"columnsTo": [
"applicationId"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"schedule_composeId_compose_composeId_fk": {
"name": "schedule_composeId_compose_composeId_fk",
"tableFrom": "schedule",
"tableTo": "compose",
"columnsFrom": [
"composeId"
],
"columnsTo": [
"composeId"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"schedule_serverId_server_serverId_fk": {
"name": "schedule_serverId_server_serverId_fk",
"tableFrom": "schedule",
"tableTo": "server",
"columnsFrom": [
"serverId"
],
"columnsTo": [
"serverId"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"schedule_userId_user_temp_id_fk": {
"name": "schedule_userId_user_temp_id_fk",
"tableFrom": "schedule",
"tableTo": "user_temp",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {
@@ -5292,14 +5451,6 @@
"preview"
]
},
"public.backupType": {
"name": "backupType",
"schema": "public",
"values": [
"database",
"compose"
]
},
"public.databaseType": {
"name": "databaseType",
"schema": "public",
@@ -5433,6 +5584,24 @@
"active",
"inactive"
]
},
"public.scheduleType": {
"name": "scheduleType",
"schema": "public",
"values": [
"application",
"compose",
"server",
"dokploy-server"
]
},
"public.shellType": {
"name": "shellType",
"schema": "public",
"values": [
"bash",
"sh"
]
}
},
"schemas": {},

View File

@@ -621,15 +621,8 @@
{
"idx": 88,
"version": "7",
"when": 1745801614194,
"tag": "0088_same_ezekiel",
"breakpoints": true
},
{
"idx": 89,
"version": "7",
"when": 1745812150155,
"tag": "0089_dazzling_marrow",
"when": 1746256928101,
"tag": "0088_illegal_ma_gnuci",
"breakpoints": true
}
]

View File

@@ -12,6 +12,7 @@ import { ShowEnvironment } from "@/components/dashboard/application/environment/
import { ShowGeneralApplication } from "@/components/dashboard/application/general/show";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { ShowPreviewDeployments } from "@/components/dashboard/application/preview-deployments/show-preview-deployments";
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
import { UpdateApplication } from "@/components/dashboard/application/update-application";
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
@@ -232,6 +233,7 @@ const Service = (
<TabsTrigger value="preview-deployments">
Preview Deployments
</TabsTrigger>
<TabsTrigger value="schedules">Schedules</TabsTrigger>
<TabsTrigger value="deployments">Deployments</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger>
{((data?.serverId && isCloud) || !data?.server) && (
@@ -308,6 +310,14 @@ const Service = (
/>
</div>
</TabsContent>
<TabsContent value="schedules">
<div className="flex flex-col gap-4 pt-2.5">
<ShowSchedules
id={applicationId}
scheduleType="application"
/>
</div>
</TabsContent>
<TabsContent value="deployments" className="w-full">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDeployments applicationId={applicationId} />

View File

@@ -1,6 +1,7 @@
import { ShowImport } from "@/components/dashboard/application/advanced/import/show-import";
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
import { AddCommandCompose } from "@/components/dashboard/compose/advanced/add-command";
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ShowDeploymentsCompose } from "@/components/dashboard/compose/deployments/show-deployments-compose";
@@ -230,6 +231,7 @@ const Service = (
<TabsTrigger value="deployments">Deployments</TabsTrigger>
<TabsTrigger value="backups">Backups</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="schedules">Schedules</TabsTrigger>
{((data?.serverId && isCloud) || !data?.server) && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
@@ -253,6 +255,12 @@ const Service = (
</div>
</TabsContent>
<TabsContent value="schedules">
<div className="flex flex-col gap-4 pt-2.5">
<ShowSchedules id={composeId} scheduleType="compose" />
</div>
</TabsContent>
<TabsContent value="monitoring">
<div className="pt-2.5">
<div className="flex flex-col border rounded-lg ">

View File

@@ -0,0 +1,54 @@
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import type { ReactElement } from "react";
import type { GetServerSidePropsContext } from "next";
import { validateRequest } from "@dokploy/server/lib/auth";
import { IS_CLOUD } from "@dokploy/server/constants";
import { api } from "@/utils/api";
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
import { Card } from "@/components/ui/card";
function SchedulesPage() {
const { data: user } = api.user.get.useQuery();
return (
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-8xl mx-auto min-h-[45vh]">
<div className="rounded-xl bg-background shadow-md h-full">
<ShowSchedules
scheduleType="dokploy-server"
id={user?.user.id || ""}
/>
</div>
</Card>
</div>
);
}
export default SchedulesPage;
SchedulesPage.getLayout = (page: ReactElement) => {
return <DashboardLayout>{page}</DashboardLayout>;
};
export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
if (IS_CLOUD) {
return {
redirect: {
permanent: true,
destination: "/dashboard/projects",
},
};
}
const { user } = await validateRequest(ctx.req);
if (!user || user.role !== "owner") {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
return {
props: {},
};
}

View File

@@ -35,6 +35,7 @@ import { sshRouter } from "./routers/ssh-key";
import { stripeRouter } from "./routers/stripe";
import { swarmRouter } from "./routers/swarm";
import { userRouter } from "./routers/user";
import { scheduleRouter } from "./routers/schedule";
/**
* This is the primary router for your server.
*
@@ -78,6 +79,7 @@ export const appRouter = createTRPCRouter({
swarm: swarmRouter,
ai: aiRouter,
organization: organizationRouter,
schedule: scheduleRouter,
});
// export type definition of API

View File

@@ -0,0 +1,142 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import {
createScheduleSchema,
schedules,
updateScheduleSchema,
} from "@dokploy/server/db/schema/schedule";
import { desc, eq } from "drizzle-orm";
import { db } from "@dokploy/server/db";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { runCommand } from "@dokploy/server/index";
import { deployments } from "@dokploy/server/db/schema/deployment";
import {
deleteSchedule,
findScheduleById,
createSchedule,
updateSchedule,
} from "@dokploy/server/services/schedule";
import { IS_CLOUD, scheduleJob } from "@dokploy/server";
import { removeJob, schedule } from "@/server/utils/backup";
import { removeScheduleJob } from "@dokploy/server";
export const scheduleRouter = createTRPCRouter({
create: protectedProcedure
.input(createScheduleSchema)
.mutation(async ({ input }) => {
const newSchedule = await createSchedule(input);
if (newSchedule?.enabled) {
if (IS_CLOUD) {
schedule({
scheduleId: newSchedule.scheduleId,
type: "schedule",
cronSchedule: newSchedule.cronExpression,
});
} else {
scheduleJob(newSchedule);
}
}
return newSchedule;
}),
update: protectedProcedure
.input(updateScheduleSchema)
.mutation(async ({ input }) => {
const updatedSchedule = await updateSchedule(input);
if (IS_CLOUD) {
if (updatedSchedule?.enabled) {
schedule({
scheduleId: updatedSchedule.scheduleId,
type: "schedule",
cronSchedule: updatedSchedule.cronExpression,
});
} else {
await removeJob({
cronSchedule: updatedSchedule.cronExpression,
scheduleId: updatedSchedule.scheduleId,
type: "schedule",
});
}
} else {
if (updatedSchedule?.enabled) {
removeScheduleJob(updatedSchedule.scheduleId);
scheduleJob(updatedSchedule);
} else {
removeScheduleJob(updatedSchedule.scheduleId);
}
}
return updatedSchedule;
}),
delete: protectedProcedure
.input(z.object({ scheduleId: z.string() }))
.mutation(async ({ input }) => {
const schedule = await findScheduleById(input.scheduleId);
await deleteSchedule(input.scheduleId);
if (IS_CLOUD) {
await removeJob({
cronSchedule: schedule.cronExpression,
scheduleId: schedule.scheduleId,
type: "schedule",
});
} else {
removeScheduleJob(schedule.scheduleId);
}
return true;
}),
list: protectedProcedure
.input(
z.object({
id: z.string(),
scheduleType: z.enum([
"application",
"compose",
"server",
"dokploy-server",
]),
}),
)
.query(async ({ input }) => {
const where = {
application: eq(schedules.applicationId, input.id),
compose: eq(schedules.composeId, input.id),
server: eq(schedules.serverId, input.id),
"dokploy-server": eq(schedules.userId, input.id),
};
return db.query.schedules.findMany({
where: where[input.scheduleType],
with: {
application: true,
server: true,
compose: true,
deployments: {
orderBy: [desc(deployments.createdAt)],
},
},
});
}),
one: protectedProcedure
.input(z.object({ scheduleId: z.string() }))
.query(async ({ input }) => {
return await findScheduleById(input.scheduleId);
}),
runManually: protectedProcedure
.input(z.object({ scheduleId: z.string().min(1) }))
.mutation(async ({ input }) => {
try {
await runCommand(input.scheduleId);
return true;
} catch (error) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message:
error instanceof Error ? error.message : "Error running schedule",
});
}
}),
});

View File

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

View File

@@ -14,6 +14,11 @@ type QueueJob =
type: "server";
cronSchedule: string;
serverId: string;
}
| {
type: "schedule";
cronSchedule: string;
scheduleId: string;
};
export const schedule = async (job: QueueJob) => {
try {

View File

@@ -34,8 +34,8 @@ app.use(async (c, next) => {
app.post("/create-backup", zValidator("json", jobQueueSchema), async (c) => {
const data = c.req.valid("json");
scheduleJob(data);
logger.info({ data }, "Backup created successfully");
return c.json({ message: "Backup created successfully" });
logger.info({ data }, `[${data.type}] created successfully`);
return c.json({ message: `[${data.type}] created successfully` });
});
app.post("/update-backup", zValidator("json", jobQueueSchema), async (c) => {
@@ -55,6 +55,12 @@ app.post("/update-backup", zValidator("json", jobQueueSchema), async (c) => {
type: "server",
cronSchedule: job.pattern,
});
} else if (data.type === "schedule") {
result = await removeJob({
scheduleId: data.scheduleId,
type: "schedule",
cronSchedule: job.pattern,
});
}
logger.info({ result }, "Job removed");
}

View File

@@ -36,6 +36,12 @@ export const scheduleJob = (job: QueueJob) => {
pattern: job.cronSchedule,
},
});
} else if (job.type === "schedule") {
jobQueue.add(job.scheduleId, job, {
repeat: {
pattern: job.cronSchedule,
},
});
}
};
@@ -54,7 +60,13 @@ export const removeJob = async (data: QueueJob) => {
});
return result;
}
if (data.type === "schedule") {
const { scheduleId, cronSchedule } = data;
const result = await jobQueue.removeRepeatable(scheduleId, {
pattern: cronSchedule,
});
return result;
}
return false;
};
@@ -72,6 +84,11 @@ export const getJobRepeatable = async (
const job = repeatableJobs.find((j) => j.name === `${serverId}-cleanup`);
return job ? job : null;
}
if (data.type === "schedule") {
const { scheduleId } = data;
const job = repeatableJobs.find((j) => j.name === scheduleId);
return job ? job : null;
}
return null;
};

View File

@@ -11,6 +11,11 @@ export const jobQueueSchema = z.discriminatedUnion("type", [
type: z.literal("server"),
serverId: z.string(),
}),
z.object({
cronSchedule: z.string(),
type: z.literal("schedule"),
scheduleId: z.string(),
}),
]);
export type QueueJob = z.infer<typeof jobQueueSchema>;

View File

@@ -3,8 +3,10 @@ import {
cleanUpSystemPrune,
cleanUpUnusedImages,
findBackupById,
findScheduleById,
findServerById,
keepLatestNBackups,
runCommand,
runMariadbBackup,
runMongoBackup,
runMySqlBackup,
@@ -12,7 +14,7 @@ import {
runComposeBackup,
} from "@dokploy/server";
import { db } from "@dokploy/server/dist/db";
import { backups, server } from "@dokploy/server/dist/db/schema";
import { backups, schedules, server } from "@dokploy/server/dist/db/schema";
import { and, eq } from "drizzle-orm";
import { logger } from "./logger.js";
import { scheduleJob } from "./queue.js";
@@ -75,8 +77,7 @@ export const runJobs = async (job: QueueJob) => {
}
await runComposeBackup(compose, backup);
}
}
if (job.type === "server") {
} else if (job.type === "server") {
const { serverId } = job;
const server = await findServerById(serverId);
if (server.serverStatus === "inactive") {
@@ -86,6 +87,12 @@ export const runJobs = async (job: QueueJob) => {
await cleanUpUnusedImages(serverId);
await cleanUpDockerBuilder(serverId);
await cleanUpSystemPrune(serverId);
} else if (job.type === "schedule") {
const { scheduleId } = job;
const schedule = await findScheduleById(scheduleId);
if (schedule.enabled) {
await runCommand(schedule.scheduleId);
}
}
} catch (error) {
logger.error(error);
@@ -134,4 +141,17 @@ export const initializeJobs = async () => {
});
}
logger.info({ Quantity: backupsResult.length }, "Backups Initialized");
const schedulesResult = await db.query.schedules.findMany({
where: eq(schedules.enabled, true),
});
for (const schedule of schedulesResult) {
scheduleJob({
scheduleId: schedule.scheduleId,
type: "schedule",
cronSchedule: schedule.cronExpression,
});
}
logger.info({ Quantity: schedulesResult.length }, "Schedules Initialized");
};