diff --git a/app/_components/FeatureComponents/Cronjobs/CronJobList.tsx b/app/_components/FeatureComponents/Cronjobs/CronJobList.tsx
index 6fe6459..4ba8091 100644
--- a/app/_components/FeatureComponents/Cronjobs/CronJobList.tsx
+++ b/app/_components/FeatureComponents/Cronjobs/CronJobList.tsx
@@ -215,7 +215,7 @@ export const CronJobList = ({ cronJobs, scripts }: CronJobListProps) => {
onNewTaskClick={() => setIsNewCronModalOpen(true)}
/>
) : (
-
+
{filteredJobs.map((job) => (
{
+export const handleDelete = async (job: CronJob, props: HandlerProps) => {
const {
setDeletingId,
setIsDeleteModalOpen,
@@ -77,19 +77,25 @@ export const handleDelete = async (id: string, props: HandlerProps) => {
refreshJobErrors,
} = props;
- setDeletingId(id);
+ setDeletingId(job.id);
try {
- const result = await removeCronJob(id);
+ const result = await removeCronJob({
+ id: job.id,
+ schedule: job.schedule,
+ command: job.command,
+ comment: job.comment,
+ user: job.user,
+ });
if (result.success) {
showToast("success", "Cron job deleted successfully");
} else {
- const errorId = `delete-${id}-${Date.now()}`;
+ const errorId = `delete-${job.id}-${Date.now()}`;
const jobError: JobError = {
id: errorId,
title: "Failed to delete cron job",
message: result.message,
timestamp: new Date().toISOString(),
- jobId: id,
+ jobId: job.id,
};
setJobError(jobError);
refreshJobErrors();
@@ -107,14 +113,14 @@ export const handleDelete = async (id: string, props: HandlerProps) => {
);
}
} catch (error: any) {
- const errorId = `delete-${id}-${Date.now()}`;
+ const errorId = `delete-${job.id}-${Date.now()}`;
const jobError: JobError = {
id: errorId,
title: "Failed to delete cron job",
message: error.message || "Please try again later.",
details: error.stack,
timestamp: new Date().toISOString(),
- jobId: id,
+ jobId: job.id,
};
setJobError(jobError);
showToast(
@@ -158,9 +164,15 @@ export const handleClone = async (newComment: string, props: HandlerProps) => {
}
};
-export const handlePause = async (id: string) => {
+export const handlePause = async (job: any) => {
try {
- const result = await pauseCronJobAction(id);
+ const result = await pauseCronJobAction({
+ id: job.id,
+ schedule: job.schedule,
+ command: job.command,
+ comment: job.comment,
+ user: job.user,
+ });
if (result.success) {
showToast("success", "Cron job paused successfully");
} else {
@@ -171,9 +183,16 @@ export const handlePause = async (id: string) => {
}
};
-export const handleToggleLogging = async (id: string) => {
+export const handleToggleLogging = async (job: any) => {
try {
- const result = await toggleCronJobLogging(id);
+ const result = await toggleCronJobLogging({
+ id: job.id,
+ schedule: job.schedule,
+ command: job.command,
+ comment: job.comment,
+ user: job.user,
+ logsEnabled: job.logsEnabled,
+ });
if (result.success) {
showToast("success", result.message);
} else {
@@ -185,9 +204,15 @@ export const handleToggleLogging = async (id: string) => {
}
};
-export const handleResume = async (id: string) => {
+export const handleResume = async (job: any) => {
try {
- const result = await resumeCronJobAction(id);
+ const result = await resumeCronJobAction({
+ id: job.id,
+ schedule: job.schedule,
+ command: job.command,
+ comment: job.comment,
+ user: job.user,
+ });
if (result.success) {
showToast("success", "Cron job resumed successfully");
} else {
@@ -401,9 +426,9 @@ export const handleNewCronSubmit = async (
}
};
-export const handleBackup = async (id: string) => {
+export const handleBackup = async (job: any) => {
try {
- const result = await backupCronJob(id);
+ const result = await backupCronJob(job);
if (result.success) {
showToast("success", "Job backed up successfully");
} else {
diff --git a/app/_hooks/useCronJobState.ts b/app/_hooks/useCronJobState.ts
index f6eaf35..dd5ddb6 100644
--- a/app/_hooks/useCronJobState.ts
+++ b/app/_hooks/useCronJobState.ts
@@ -127,7 +127,10 @@ export const useCronJobState = ({ cronJobs, scripts }: CronJobListProps) => {
};
const handleDeleteLocal = async (id: string) => {
- await handleDelete(id, getHelperState());
+ const job = cronJobs.find(j => j.id === id);
+ if (job) {
+ await handleDelete(job, getHelperState());
+ }
};
const handleCloneLocal = async (newComment: string) => {
@@ -135,11 +138,17 @@ export const useCronJobState = ({ cronJobs, scripts }: CronJobListProps) => {
};
const handlePauseLocal = async (id: string) => {
- await handlePause(id);
+ const job = cronJobs.find(j => j.id === id);
+ if (job) {
+ await handlePause(job);
+ }
};
const handleResumeLocal = async (id: string) => {
- await handleResume(id);
+ const job = cronJobs.find(j => j.id === id);
+ if (job) {
+ await handleResume(job);
+ }
};
const handleRunLocal = async (id: string) => {
@@ -149,7 +158,10 @@ export const useCronJobState = ({ cronJobs, scripts }: CronJobListProps) => {
};
const handleToggleLoggingLocal = async (id: string) => {
- await handleToggleLogging(id);
+ const job = cronJobs.find(j => j.id === id);
+ if (job) {
+ await handleToggleLogging(job);
+ }
};
const handleViewLogs = (job: CronJob) => {
@@ -187,7 +199,10 @@ export const useCronJobState = ({ cronJobs, scripts }: CronJobListProps) => {
};
const handleBackupLocal = async (id: string) => {
- await handleBackup(id);
+ const job = cronJobs.find(j => j.id === id);
+ if (job) {
+ await handleBackup(job);
+ }
};
return {
diff --git a/app/_server/actions/cronjobs/index.ts b/app/_server/actions/cronjobs/index.ts
index b611adc..a4788f2 100644
--- a/app/_server/actions/cronjobs/index.ts
+++ b/app/_server/actions/cronjobs/index.ts
@@ -3,25 +3,27 @@
import {
getCronJobs,
addCronJob,
- deleteCronJob,
- updateCronJob,
- pauseCronJob,
- resumeCronJob,
cleanupCrontab,
+ readUserCrontab,
+ writeUserCrontab,
+ findJobIndex,
+ updateCronJob,
type CronJob,
} from "@/app/_utils/cronjob-utils";
-import { getAllTargetUsers, getUserInfo } from "@/app/_utils/crontab-utils";
+import { getAllTargetUsers } from "@/app/_utils/crontab-utils";
import { revalidatePath } from "next/cache";
import { getScriptPathForCron } from "@/app/_server/actions/scripts";
-import { exec } from "child_process";
-import { promisify } from "util";
import { isDocker } from "@/app/_server/actions/global";
import {
runJobSynchronously,
runJobInBackground,
} from "@/app/_utils/job-execution-utils";
-
-const execAsync = promisify(exec);
+import {
+ pauseJobInLines,
+ resumeJobInLines,
+ deleteJobInLines,
+} from "@/app/_utils/line-manipulation-utils";
+import { cleanCrontabContent } from "@/app/_utils/files-manipulation-utils";
export const fetchCronJobs = async (): Promise => {
try {
@@ -90,10 +92,22 @@ export const createCronJob = async (
};
export const removeCronJob = async (
- id: string
+ jobData: { id: string; schedule: string; command: string; comment?: string; user: string }
): Promise<{ success: boolean; message: string; details?: string }> => {
try {
- const success = await deleteCronJob(id);
+ const cronContent = await readUserCrontab(jobData.user);
+ const lines = cronContent.split("\n");
+
+ const jobIndex = findJobIndex(jobData, lines, jobData.user);
+
+ if (jobIndex === -1) {
+ return { success: false, message: "Cron job not found in crontab" };
+ }
+
+ const newCronEntries = deleteJobInLines(lines, jobIndex);
+ const newCron = await cleanCrontabContent(newCronEntries.join("\n"));
+ const success = await writeUserCrontab(jobData.user, newCron);
+
if (success) {
revalidatePath("/");
return { success: true, message: "Cron job deleted successfully" };
@@ -124,8 +138,15 @@ export const editCronJob = async (
return { success: false, message: "Missing required fields" };
}
+ const cronJobs = await getCronJobs(false);
+ const job = cronJobs.find((j) => j.id === id);
+
+ if (!job) {
+ return { success: false, message: "Cron job not found" };
+ }
+
const success = await updateCronJob(
- id,
+ job,
schedule,
command,
comment,
@@ -152,7 +173,7 @@ export const cloneCronJob = async (
newComment: string
): Promise<{ success: boolean; message: string; details?: string }> => {
try {
- const cronJobs = await getCronJobs();
+ const cronJobs = await getCronJobs(false);
const originalJob = cronJobs.find((job) => job.id === id);
if (!originalJob) {
@@ -183,10 +204,22 @@ export const cloneCronJob = async (
};
export const pauseCronJobAction = async (
- id: string
+ jobData: { id: string; schedule: string; command: string; comment?: string; user: string }
): Promise<{ success: boolean; message: string; details?: string }> => {
try {
- const success = await pauseCronJob(id);
+ const cronContent = await readUserCrontab(jobData.user);
+ const lines = cronContent.split("\n");
+
+ const jobIndex = findJobIndex(jobData, lines, jobData.user);
+
+ if (jobIndex === -1) {
+ return { success: false, message: "Cron job not found in crontab" };
+ }
+
+ const newCronEntries = pauseJobInLines(lines, jobIndex, jobData.id);
+ const newCron = await cleanCrontabContent(newCronEntries.join("\n"));
+ const success = await writeUserCrontab(jobData.user, newCron);
+
if (success) {
revalidatePath("/");
return { success: true, message: "Cron job paused successfully" };
@@ -204,10 +237,22 @@ export const pauseCronJobAction = async (
};
export const resumeCronJobAction = async (
- id: string
+ jobData: { id: string; schedule: string; command: string; comment?: string; user: string }
): Promise<{ success: boolean; message: string; details?: string }> => {
try {
- const success = await resumeCronJob(id);
+ const cronContent = await readUserCrontab(jobData.user);
+ const lines = cronContent.split("\n");
+
+ const jobIndex = findJobIndex(jobData, lines, jobData.user);
+
+ if (jobIndex === -1) {
+ return { success: false, message: "Cron job not found in crontab" };
+ }
+
+ const newCronEntries = resumeJobInLines(lines, jobIndex, jobData.id);
+ const newCron = await cleanCrontabContent(newCronEntries.join("\n"));
+ const success = await writeUserCrontab(jobData.user, newCron);
+
if (success) {
revalidatePath("/");
return { success: true, message: "Cron job resumed successfully" };
@@ -257,23 +302,16 @@ export const cleanupCrontabAction = async (): Promise<{
};
export const toggleCronJobLogging = async (
- id: string
+ jobData: { id: string; schedule: string; command: string; comment?: string; user: string; logsEnabled?: boolean }
): Promise<{ success: boolean; message: string; details?: string }> => {
try {
- const cronJobs = await getCronJobs();
- const job = cronJobs.find((j) => j.id === id);
-
- if (!job) {
- return { success: false, message: "Cron job not found" };
- }
-
- const newLogsEnabled = !job.logsEnabled;
+ const newLogsEnabled = !jobData.logsEnabled;
const success = await updateCronJob(
- id,
- job.schedule,
- job.command,
- job.comment || "",
+ jobData,
+ jobData.schedule,
+ jobData.command,
+ jobData.comment || "",
newLogsEnabled
);
@@ -309,7 +347,7 @@ export const runCronJob = async (
mode?: "sync" | "async";
}> => {
try {
- const cronJobs = await getCronJobs();
+ const cronJobs = await getCronJobs(false);
const job = cronJobs.find((j) => j.id === id);
if (!job) {
@@ -356,7 +394,7 @@ export const executeJob = async (
mode?: "sync" | "async";
}> => {
try {
- const cronJobs = await getCronJobs();
+ const cronJobs = await getCronJobs(false);
const job = cronJobs.find((j) => j.id === id);
if (!job) {
@@ -388,13 +426,13 @@ export const executeJob = async (
};
export const backupCronJob = async (
- id: string
+ job: CronJob
): Promise<{ success: boolean; message: string; details?: string }> => {
try {
const {
backupJobToFile,
} = await import("@/app/_utils/backup-utils");
- const success = await backupJobToFile(id);
+ const success = await backupJobToFile(job);
if (success) {
return { success: true, message: "Cron job backed up successfully" };
} else {
diff --git a/app/_utils/backup-utils.ts b/app/_utils/backup-utils.ts
index f3e2cae..c4cdb89 100644
--- a/app/_utils/backup-utils.ts
+++ b/app/_utils/backup-utils.ts
@@ -17,18 +17,10 @@ const sanitizeFilename = (id: string): string => {
return id.replace(/[^a-zA-Z0-9_-]/g, "_");
};
-export const backupJobToFile = async (id: string): Promise => {
+export const backupJobToFile = async (job: CronJob): Promise => {
try {
await ensureBackupDirectoryExists();
- const cronJobs = await getCronJobs(false);
- const job = cronJobs.find((j) => j.id === id);
-
- if (!job) {
- console.error(`Job with id ${id} not found`);
- return false;
- }
-
const jobData = {
id: job.id,
schedule: job.schedule,
@@ -40,14 +32,14 @@ export const backupJobToFile = async (id: string): Promise => {
backedUpAt: new Date().toISOString(),
};
- const filename = `${sanitizeFilename(id)}.job`;
+ const filename = `${sanitizeFilename(job.id)}.job`;
const filepath = path.join(BACKUP_DIR, filename);
await fs.writeFile(filepath, JSON.stringify(jobData, null, 2), "utf8");
return true;
} catch (error) {
- console.error(`Error backing up job ${id}:`, error);
+ console.error(`Error backing up job ${job.id}:`, error);
return false;
}
};
@@ -64,7 +56,7 @@ export const backupAllJobsToFiles = async (): Promise<{
let successCount = 0;
for (const job of cronJobs) {
- const success = await backupJobToFile(job.id);
+ const success = await backupJobToFile(job);
if (success) {
successCount++;
}
diff --git a/app/_utils/cronjob-utils.ts b/app/_utils/cronjob-utils.ts
index f70de39..89c205e 100644
--- a/app/_utils/cronjob-utils.ts
+++ b/app/_utils/cronjob-utils.ts
@@ -46,7 +46,7 @@ export interface CronJob {
};
}
-const readUserCrontab = async (user: string): Promise => {
+export const readUserCrontab = async (user: string): Promise => {
const docker = await isDocker();
if (docker) {
@@ -59,7 +59,7 @@ const readUserCrontab = async (user: string): Promise => {
}
};
-const writeUserCrontab = async (
+export const writeUserCrontab = async (
user: string,
content: string
): Promise => {
@@ -115,33 +115,6 @@ export const getCronJobs = async (
const lines = content.split("\n");
const jobs = parseJobsFromLines(lines, user);
- let needsUpdate = false;
- let updatedLines = [...lines];
-
- for (let jobIndex = 0; jobIndex < jobs.length; jobIndex++) {
- const job = jobs[jobIndex];
-
- const cronContent = lines.join("\n");
- if (!cronContent.includes(`id: ${job.id}`)) {
- needsUpdate = true;
-
- updatedLines = updateJobInLines(
- updatedLines,
- jobIndex,
- job.schedule,
- job.command,
- job.comment || "",
- job.logsEnabled || false,
- job.id
- );
- }
- }
-
- if (needsUpdate) {
- const newCron = await cleanCrontabContent(updatedLines.join("\n"));
- await writeUserCrontab(user, newCron);
- }
-
allJobs.push(...jobs);
}
@@ -278,46 +251,38 @@ export const deleteCronJob = async (id: string): Promise => {
};
export const updateCronJob = async (
- id: string,
+ jobData: { id: string; schedule: string; command: string; comment?: string; user: string },
schedule: string,
command: string,
comment: string = "",
logsEnabled: boolean = false
): Promise => {
try {
- const allJobs = await getCronJobs(false);
- const targetJob = allJobs.find((j) => j.id === id);
-
- if (!targetJob) {
- console.error(`Job with id ${id} not found`);
- return false;
- }
-
- const user = targetJob.user;
+ const user = jobData.user;
const cronContent = await readUserCrontab(user);
const lines = cronContent.split("\n");
- const userJobs = parseJobsFromLines(lines, user);
- const jobIndex = userJobs.findIndex((j) => j.id === id);
+
+ const jobIndex = findJobIndex(jobData, lines, user);
if (jobIndex === -1) {
- console.error(`Job with id ${id} not found in parsed jobs`);
+ console.error(`Job not found in crontab`);
return false;
}
- const isWrappd = isCommandWrapped(command);
+ const isWrapped = isCommandWrapped(command);
let finalCommand = command;
- if (logsEnabled && !isWrappd) {
+ if (logsEnabled && !isWrapped) {
const docker = await isDocker();
- finalCommand = await wrapCommandWithLogger(id, command, docker, comment);
- } else if (!logsEnabled && isWrappd) {
+ finalCommand = await wrapCommandWithLogger(jobData.id, command, docker, comment);
+ } else if (!logsEnabled && isWrapped) {
finalCommand = unwrapCommand(command);
- } else if (logsEnabled && isWrappd) {
+ } else if (logsEnabled && isWrapped) {
const unwrapped = unwrapCommand(command);
const docker = await isDocker();
finalCommand = await wrapCommandWithLogger(
- id,
+ jobData.id,
unwrapped,
docker,
comment
@@ -333,7 +298,7 @@ export const updateCronJob = async (
finalCommand,
comment,
logsEnabled,
- id
+ jobData.id
);
const newCron = await cleanCrontabContent(newCronEntries.join("\n"));
@@ -423,3 +388,24 @@ export const cleanupCrontab = async (): Promise => {
return false;
}
};
+
+export const findJobIndex = (
+ jobData: { id: string; schedule: string; command: string; comment?: string; user: string; paused?: boolean },
+ lines: string[],
+ user: string
+): number => {
+ const cronContentStr = lines.join("\n");
+ const userJobs = parseJobsFromLines(lines, user);
+
+ if (cronContentStr.includes(`id: ${jobData.id}`)) {
+ return userJobs.findIndex((j) => j.id === jobData.id);
+ }
+
+ return userJobs.findIndex(
+ (j) =>
+ j.schedule === jobData.schedule &&
+ j.command === jobData.command &&
+ j.user === jobData.user &&
+ (j.comment || "") === (jobData.comment || "")
+ );
+};
diff --git a/app/_utils/line-manipulation-utils.ts b/app/_utils/line-manipulation-utils.ts
index 8673a89..1926654 100644
--- a/app/_utils/line-manipulation-utils.ts
+++ b/app/_utils/line-manipulation-utils.ts
@@ -175,17 +175,34 @@ export const parseCommentMetadata = (
let uuid: string | undefined;
if (parts.length > 1) {
- comment = parts[0] || "";
- const metadata = parts.slice(1).join("|").trim();
+ const firstPartIsMetadata = parts[0].match(/logsEnabled:\s*(true|false)/i) || parts[0].match(/id:\s*([a-z0-9]{4}-[a-z0-9]{4})/i);
- const logsMatch = metadata.match(/logsEnabled:\s*(true|false)/i);
- if (logsMatch) {
- logsEnabled = logsMatch[1].toLowerCase() === "true";
- }
+ if (firstPartIsMetadata) {
+ comment = "";
+ const metadata = parts.join("|").trim();
- const uuidMatch = metadata.match(/id:\s*([a-z0-9]{4}-[a-z0-9]{4})/i);
- if (uuidMatch) {
- uuid = uuidMatch[1].toLowerCase();
+ const logsMatch = metadata.match(/logsEnabled:\s*(true|false)/i);
+ if (logsMatch) {
+ logsEnabled = logsMatch[1].toLowerCase() === "true";
+ }
+
+ const uuidMatch = metadata.match(/id:\s*([a-z0-9]{4}-[a-z0-9]{4})/i);
+ if (uuidMatch) {
+ uuid = uuidMatch[1].toLowerCase();
+ }
+ } else {
+ comment = parts[0] || "";
+ const metadata = parts.slice(1).join("|").trim();
+
+ const logsMatch = metadata.match(/logsEnabled:\s*(true|false)/i);
+ if (logsMatch) {
+ logsEnabled = logsMatch[1].toLowerCase() === "true";
+ }
+
+ const uuidMatch = metadata.match(/id:\s*([a-z0-9]{4}-[a-z0-9]{4})/i);
+ if (uuidMatch) {
+ uuid = uuidMatch[1].toLowerCase();
+ }
}
} else {
const logsMatch = commentText.match(/logsEnabled:\s*(true|false)/i);
diff --git a/app/api/cronjobs/[id]/route.ts b/app/api/cronjobs/[id]/route.ts
index 32d7f53..a35fbe6 100644
--- a/app/api/cronjobs/[id]/route.ts
+++ b/app/api/cronjobs/[id]/route.ts
@@ -87,7 +87,7 @@ export async function DELETE(
if (authError) return authError;
try {
- const result = await removeCronJob(params.id);
+ const result = await removeCronJob({ id: params.id, schedule: "", command: "", user: "" });
if (result.success) {
return NextResponse.json(result);
diff --git a/app/globals.css b/app/globals.css
index a04ca01..a83756b 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -76,6 +76,24 @@
scrollbar-width: none;
}
+.overflow-y-auto {
+ padding-right: 1em;
+}
+
+.overflow-y-auto::-webkit-scrollbar {
+ width: 4px;
+}
+.overflow-y-auto::-webkit-scrollbar-track {
+ background: transparent;
+}
+.overflow-y-auto::-webkit-scrollbar-thumb {
+ background-color: hsl(var(--primary) / 0.8);
+ border-radius: 3px;
+}
+.overflow-y-auto::-webkit-scrollbar-thumb:hover {
+ background-color: hsl(var(--primary) / 0.1);
+}
+
@layer base {
* {
@apply border-border;
diff --git a/app/login/page.tsx b/app/login/page.tsx
index 29f0cfc..8971d38 100644
--- a/app/login/page.tsx
+++ b/app/login/page.tsx
@@ -1,4 +1,4 @@
-'use server';
+export const dynamic = "force-dynamic";
import { LoginForm } from "@/app/_components/FeatureComponents/LoginForm/LoginForm";