send fixes to develop for automatic ID assignation

This commit is contained in:
fccview
2025-11-12 20:56:54 +00:00
parent 1fd2689296
commit 8faf4d26d0
10 changed files with 218 additions and 127 deletions

View File

@@ -215,7 +215,7 @@ export const CronJobList = ({ cronJobs, scripts }: CronJobListProps) => {
onNewTaskClick={() => setIsNewCronModalOpen(true)}
/>
) : (
<div className="space-y-3">
<div className="space-y-3 max-h-[60vh] overflow-y-auto">
{filteredJobs.map((job) => (
<CronJobItem
key={job.id}

View File

@@ -69,7 +69,7 @@ export const refreshJobErrors = (
setJobErrors(errors);
};
export const handleDelete = async (id: string, props: HandlerProps) => {
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 {

View File

@@ -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 {

View File

@@ -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<CronJob[]> => {
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 {

View File

@@ -17,18 +17,10 @@ const sanitizeFilename = (id: string): string => {
return id.replace(/[^a-zA-Z0-9_-]/g, "_");
};
export const backupJobToFile = async (id: string): Promise<boolean> => {
export const backupJobToFile = async (job: CronJob): Promise<boolean> => {
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<boolean> => {
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++;
}

View File

@@ -46,7 +46,7 @@ export interface CronJob {
};
}
const readUserCrontab = async (user: string): Promise<string> => {
export const readUserCrontab = async (user: string): Promise<string> => {
const docker = await isDocker();
if (docker) {
@@ -59,7 +59,7 @@ const readUserCrontab = async (user: string): Promise<string> => {
}
};
const writeUserCrontab = async (
export const writeUserCrontab = async (
user: string,
content: string
): Promise<boolean> => {
@@ -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<boolean> => {
};
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<boolean> => {
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<boolean> => {
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 || "")
);
};

View File

@@ -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);

View File

@@ -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);

View File

@@ -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;

View File

@@ -1,4 +1,4 @@
'use server';
export const dynamic = "force-dynamic";
import { LoginForm } from "@/app/_components/FeatureComponents/LoginForm/LoginForm";