From 437bdbd81f63a4dce042a825c42f666cfa3bee90 Mon Sep 17 00:00:00 2001 From: fccview Date: Wed, 19 Nov 2025 16:47:43 +0000 Subject: [PATCH] fix parenthesis issue and give immutable ids to old jobs too --- .../UIElements/DropdownMenu.tsx | 4 +- app/_consts/commands.ts | 29 +++++--- app/_utils/cronjob-utils.ts | 24 ++++++- app/_utils/line-manipulation-utils.ts | 71 +++++++++++++++---- app/login/page.tsx | 1 - 5 files changed, 98 insertions(+), 31 deletions(-) diff --git a/app/_components/GlobalComponents/UIElements/DropdownMenu.tsx b/app/_components/GlobalComponents/UIElements/DropdownMenu.tsx index 43595f1..a60dfbe 100644 --- a/app/_components/GlobalComponents/UIElements/DropdownMenu.tsx +++ b/app/_components/GlobalComponents/UIElements/DropdownMenu.tsx @@ -4,7 +4,7 @@ import { useState, useRef, useEffect, ReactNode } from "react"; import { Button } from "@/app/_components/GlobalComponents/UIElements/Button"; import { MoreVertical } from "lucide-react"; -const DROPDOWN_HEIGHT = 200; // Approximate max height of dropdown +const DROPDOWN_HEIGHT = 200; interface DropdownMenuItem { label: string; @@ -36,13 +36,11 @@ export const DropdownMenu = ({ const handleOpenChange = (open: boolean) => { if (open && triggerRef.current) { - // Calculate if dropdown should be positioned above or below const rect = triggerRef.current.getBoundingClientRect(); const viewportHeight = window.innerHeight; const spaceBelow = viewportHeight - rect.bottom; const spaceAbove = rect.top; - // Position above if there's not enough space below setPositionAbove(spaceBelow < DROPDOWN_HEIGHT && spaceAbove > spaceBelow); } setIsOpen(open); diff --git a/app/_consts/commands.ts b/app/_consts/commands.ts index 59270f8..1011ff7 100644 --- a/app/_consts/commands.ts +++ b/app/_consts/commands.ts @@ -1,23 +1,34 @@ -export const WRITE_CRONTAB = (content: string, user: string) => `echo '${content}' | crontab -u ${user} -`; +export const WRITE_CRONTAB = (content: string, user: string) => { + const escapedContent = content.replace(/'/g, "'\\''"); + return `crontab -u ${user} - << 'EOF'\n${escapedContent}\nEOF`; +}; -export const READ_CRONTAB = (user: string) => `crontab -l -u ${user} 2>/dev/null || echo ""`; +export const READ_CRONTAB = (user: string) => + `crontab -l -u ${user} 2>/dev/null || echo ""`; -export const READ_CRON_FILE = () => 'crontab -l 2>/dev/null || echo ""' +export const READ_CRON_FILE = () => 'crontab -l 2>/dev/null || echo ""'; -export const WRITE_CRON_FILE = (content: string) => `echo "${content}" | crontab -`; +export const WRITE_CRON_FILE = (content: string) => { + const escapedContent = content.replace(/"/g, '\\"'); + return `echo "${escapedContent}" | crontab -`; +}; -export const WRITE_HOST_CRONTAB = (base64Content: string, user: string) => `echo '${base64Content}' | base64 -d | crontab -u ${user} -`; +export const WRITE_HOST_CRONTAB = (base64Content: string, user: string) => { + const escapedContent = base64Content.replace(/'/g, "'\\''"); + return `echo '${escapedContent}' | base64 -d | crontab -u ${user} -`; +}; export const ID_U = (username: string) => `id -u ${username}`; export const ID_G = (username: string) => `id -g ${username}`; -export const MAKE_SCRIPT_EXECUTABLE = (scriptPath: string) => `chmod +x "${scriptPath}"`; +export const MAKE_SCRIPT_EXECUTABLE = (scriptPath: string) => + `chmod +x "${scriptPath}"`; export const RUN_SCRIPT = (scriptPath: string) => `bash "${scriptPath}"`; -export const GET_TARGET_USER = `getent passwd | grep ":/home/" | head -1 | cut -d: -f1` +export const GET_TARGET_USER = `getent passwd | grep ":/home/" | head -1 | cut -d: -f1`; -export const GET_DOCKER_SOCKET_OWNER = 'stat -c "%U" /var/run/docker.sock' +export const GET_DOCKER_SOCKET_OWNER = 'stat -c "%U" /var/run/docker.sock'; -export const READ_CRONTABS_DIRECTORY = `ls /var/spool/cron/crontabs/ 2>/dev/null || echo ''`; \ No newline at end of file +export const READ_CRONTABS_DIRECTORY = `ls /var/spool/cron/crontabs/ 2>/dev/null || echo ''`; diff --git a/app/_utils/cronjob-utils.ts b/app/_utils/cronjob-utils.ts index 89c205e..69fdbc6 100644 --- a/app/_utils/cronjob-utils.ts +++ b/app/_utils/cronjob-utils.ts @@ -251,7 +251,13 @@ export const deleteCronJob = async (id: string): Promise => { }; export const updateCronJob = async ( - jobData: { id: string; schedule: string; command: string; comment?: string; user: string }, + jobData: { + id: string; + schedule: string; + command: string; + comment?: string; + user: string; + }, schedule: string, command: string, comment: string = "", @@ -275,7 +281,12 @@ export const updateCronJob = async ( if (logsEnabled && !isWrapped) { const docker = await isDocker(); - finalCommand = await wrapCommandWithLogger(jobData.id, command, docker, comment); + finalCommand = await wrapCommandWithLogger( + jobData.id, + command, + docker, + comment + ); } else if (!logsEnabled && isWrapped) { finalCommand = unwrapCommand(command); } else if (logsEnabled && isWrapped) { @@ -390,7 +401,14 @@ export const cleanupCrontab = async (): Promise => { }; export const findJobIndex = ( - jobData: { id: string; schedule: string; command: string; comment?: string; user: string; paused?: boolean }, + jobData: { + id: string; + schedule: string; + command: string; + comment?: string; + user: string; + paused?: boolean; + }, lines: string[], user: string ): number => { diff --git a/app/_utils/line-manipulation-utils.ts b/app/_utils/line-manipulation-utils.ts index 1926654..2d76b7f 100644 --- a/app/_utils/line-manipulation-utils.ts +++ b/app/_utils/line-manipulation-utils.ts @@ -1,5 +1,20 @@ import { CronJob } from "@/app/_utils/cronjob-utils"; import { generateShortUUID } from "@/app/_utils/uuid-utils"; +import { createHash } from "crypto"; + +const generateStableJobId = ( + schedule: string, + command: string, + user: string, + comment?: string, + lineIndex?: number +): string => { + const content = `${schedule}|${command}|${user}|${comment || ""}|${ + lineIndex || 0 + }`; + const hash = createHash("md5").update(content).digest("hex"); + return hash.substring(0, 8); +}; export const pauseJobInLines = ( lines: string[], @@ -55,7 +70,11 @@ export const pauseJobInLines = ( if (currentJobIndex === targetJobIndex) { const commentText = trimmedLine.substring(1).trim(); const { comment, logsEnabled } = parseCommentMetadata(commentText); - const formattedComment = formatCommentWithMetadata(comment, logsEnabled, uuid); + const formattedComment = formatCommentWithMetadata( + comment, + logsEnabled, + uuid + ); const nextLine = lines[i + 1].trim(); const pausedEntry = `# PAUSED: ${formattedComment}\n# ${nextLine}`; newCronEntries.push(pausedEntry); @@ -128,8 +147,14 @@ export const resumeJobInLines = ( const { comment, logsEnabled } = parseCommentMetadata(commentText); if (i + 1 < lines.length && lines[i + 1].trim().startsWith("# ")) { const cronLine = lines[i + 1].trim().substring(2); - const formattedComment = formatCommentWithMetadata(comment, logsEnabled, uuid); - const resumedEntry = formattedComment ? `# ${formattedComment}\n${cronLine}` : cronLine; + const formattedComment = formatCommentWithMetadata( + comment, + logsEnabled, + uuid + ); + const resumedEntry = formattedComment + ? `# ${formattedComment}\n${cronLine}` + : cronLine; newCronEntries.push(resumedEntry); i += 2; } else { @@ -175,7 +200,9 @@ export const parseCommentMetadata = ( let uuid: string | undefined; if (parts.length > 1) { - 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 firstPartIsMetadata = + parts[0].match(/logsEnabled:\s*(true|false)/i) || + parts[0].match(/id:\s*([a-z0-9]{8}|[a-z0-9]{4}-[a-z0-9]{4})/i); if (firstPartIsMetadata) { comment = ""; @@ -186,9 +213,11 @@ export const parseCommentMetadata = ( 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(); + const uuidMatches = Array.from( + metadata.matchAll(/id:\s*([a-z0-9]{8}|[a-z0-9]{4}-[a-z0-9]{4})/gi) + ); + if (uuidMatches.length > 0) { + uuid = uuidMatches[uuidMatches.length - 1][1].toLowerCase(); } } else { comment = parts[0] || ""; @@ -199,14 +228,18 @@ export const parseCommentMetadata = ( 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(); + const uuidMatches = Array.from( + metadata.matchAll(/id:\s*([a-z0-9]{8}|[a-z0-9]{4}-[a-z0-9]{4})/gi) + ); + if (uuidMatches.length > 0) { + uuid = uuidMatches[uuidMatches.length - 1][1].toLowerCase(); } } } else { const logsMatch = commentText.match(/logsEnabled:\s*(true|false)/i); - const uuidMatch = commentText.match(/id:\s*([a-z0-9]{4}-[a-z0-9]{4})/i); + const uuidMatch = commentText.match( + /id:\s*([a-z0-9]{8}|[a-z0-9]{4}-[a-z0-9]{4})/i + ); if (logsMatch || uuidMatch) { if (logsMatch) { @@ -288,7 +321,8 @@ export const parseJobsFromLines = ( const schedule = parts.slice(0, 5).join(" "); const command = parts.slice(5).join(" "); - const jobId = uuid || generateShortUUID(); + const jobId = + uuid || generateStableJobId(schedule, command, user, comment, i); jobs.push({ id: jobId, @@ -317,7 +351,8 @@ export const parseJobsFromLines = ( lines[i + 1].trim() ) { const commentText = trimmedLine.substring(1).trim(); - const { comment, logsEnabled, uuid } = parseCommentMetadata(commentText); + const { comment, logsEnabled, uuid } = + parseCommentMetadata(commentText); currentComment = comment; currentLogsEnabled = logsEnabled; currentUuid = uuid; @@ -343,7 +378,9 @@ export const parseJobsFromLines = ( } if (schedule && command) { - const jobId = currentUuid || generateShortUUID(); + const jobId = + currentUuid || + generateStableJobId(schedule, command, user, currentComment, i); jobs.push({ id: jobId, @@ -545,7 +582,11 @@ export const updateJobInLines = ( } if (currentJobIndex === targetJobIndex) { - const formattedComment = formatCommentWithMetadata(comment, logsEnabled, uuid); + const formattedComment = formatCommentWithMetadata( + comment, + logsEnabled, + uuid + ); const newEntry = formattedComment ? `# ${formattedComment}\n${schedule} ${command}` : `${schedule} ${command}`; diff --git a/app/login/page.tsx b/app/login/page.tsx index 9f2ecb2..e05b70a 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -8,7 +8,6 @@ export default async function LoginPage() { const hasPassword = !!process.env.AUTH_PASSWORD; const hasOIDC = process.env.SSO_MODE === "oidc"; - // Read package.json to get version const packageJsonPath = path.join(process.cwd(), "package.json"); const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")); const version = packageJson.version;