diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 71e0a69..8cc7b2d 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -2,7 +2,7 @@ name: Docker on: push: - branches: ["main", "legacy"] + branches: ["main", "legacy", "feature/*", "bugfix/*"] tags: ["*"] pull_request: branches: ["main"] diff --git a/.gitignore b/.gitignore index 1c4a75c..6a9609a 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ node_modules .next .vscode .DS_Store -.cursorignore \ No newline at end of file +.cursorignore +tsconfig.tsbuildinfo diff --git a/README.md b/README.md index d03bc2a..06e651d 100644 --- a/README.md +++ b/README.md @@ -49,19 +49,21 @@ If you find my projects helpful and want to fuel my late-night coding sessions w ```bash services: cronjob-manager: - image: ghcr.io/fccview/cronmaster:1.2.1 + image: ghcr.io/fccview/cronmaster:2.3.0 container_name: cronmaster user: "root" ports: # Feel free to change port, 3000 is very common so I like to map it to something else - - "40124:3000" + - "40123:3000" environment: - NODE_ENV=production - DOCKER=true - - NEXT_PUBLIC_CLOCK_UPDATE_INTERVAL=30000 + # Legacy used to be NEXT_PUBLIC_HOST_PROJECT_DIR, this was causing issues on runtime. - HOST_PROJECT_DIR=/path/to/cronmaster/directory + - NEXT_PUBLIC_CLOCK_UPDATE_INTERVAL=30000 # If docker struggles to find your crontab user, update this variable with it. # Obviously replace fccview with your user - find it with: ls -asl /var/spool/cron/crontabs/ + # For multiple users, use comma-separated values: HOST_CRONTAB_USER=fccview,root,user1,user2 # - HOST_CRONTAB_USER=fccview volumes: # Mount Docker socket to execute commands on host @@ -69,7 +71,7 @@ services: # These are needed if you want to keep your data on the host machine and not wihin the docker volume. # DO NOT change the location of ./scripts as all cronjobs that use custom scripts created via the app - # will target this foler (thanks to the HOST_PROJECT_DIR variable set above) + # will target this folder (thanks to the HOST_PROJECT_DIR variable set above) - ./scripts:/app/scripts - ./data:/app/data - ./snippets:/app/snippets @@ -235,6 +237,11 @@ I would like to thank the following members for raising issues and help test/deb
mariushosting
+ + +
DVDAndroid
+ + diff --git a/app/_components/BashEditor.tsx b/app/_components/BashEditor.tsx index 20f456b..1bad9d5 100644 --- a/app/_components/BashEditor.tsx +++ b/app/_components/BashEditor.tsx @@ -16,13 +16,13 @@ interface BashEditorProps { label?: string; } -export function BashEditor({ +export const BashEditor = ({ value, onChange, placeholder = "#!/bin/bash\n# Your bash script here\necho 'Hello World'", className = "", label = "Bash Script", -}: BashEditorProps) { +}: BashEditorProps) => { const [copied, setCopied] = useState(false); const editorRef = useRef(null); const editorViewRef = useRef(null); diff --git a/app/_components/BashSnippetHelper.tsx b/app/_components/BashSnippetHelper.tsx index e4f6e1c..1f0df4a 100644 --- a/app/_components/BashSnippetHelper.tsx +++ b/app/_components/BashSnippetHelper.tsx @@ -34,7 +34,7 @@ const categoryIcons = { "Custom Scripts": Code, }; -export function BashSnippetHelper({ onInsertSnippet }: BashSnippetHelperProps) { +export const BashSnippetHelper = ({ onInsertSnippet }: BashSnippetHelperProps) => { const [searchQuery, setSearchQuery] = useState(""); const [selectedCategory, setSelectedCategory] = useState(null); const [copiedId, setCopiedId] = useState(null); diff --git a/app/_components/CronExpressionHelper.tsx b/app/_components/CronExpressionHelper.tsx index eed220e..86904dc 100644 --- a/app/_components/CronExpressionHelper.tsx +++ b/app/_components/CronExpressionHelper.tsx @@ -27,13 +27,13 @@ interface CronExpressionHelperProps { showPatterns?: boolean; } -export function CronExpressionHelper({ +export const CronExpressionHelper = ({ value, onChange, placeholder = "* * * * *", className = "", showPatterns = true, -}: CronExpressionHelperProps) { +}: CronExpressionHelperProps) => { const [explanation, setExplanation] = useState(null); const [showPatternsPanel, setShowPatternsPanel] = useState(false); const [debouncedValue, setDebouncedValue] = useState(value); diff --git a/app/_components/CronJobList.tsx b/app/_components/CronJobList.tsx index 2deaff1..849dd4c 100644 --- a/app/_components/CronJobList.tsx +++ b/app/_components/CronJobList.tsx @@ -2,19 +2,33 @@ import { Card, CardContent, CardHeader, CardTitle } from "./ui/Card"; import { Button } from "./ui/Button"; -import { Trash2, Clock, Edit, Plus, Files } from "lucide-react"; +import { + Trash2, + Clock, + Edit, + Plus, + Files, + User, + Play, + Pause, + Code, +} from "lucide-react"; import { CronJob } from "@/app/_utils/system"; import { removeCronJob, editCronJob, createCronJob, cloneCronJob, + pauseCronJobAction, + resumeCronJobAction, + runCronJob, } from "@/app/_server/actions/cronjobs"; -import { useState } from "react"; +import { useState, useMemo, useEffect } from "react"; import { CreateTaskModal } from "./modals/CreateTaskModal"; import { EditTaskModal } from "./modals/EditTaskModal"; import { DeleteTaskModal } from "./modals/DeleteTaskModal"; import { CloneTaskModal } from "./modals/CloneTaskModal"; +import { UserFilter } from "./ui/UserFilter"; import { type Script } from "@/app/_server/actions/scripts"; import { showToast } from "./ui/Toast"; @@ -23,7 +37,7 @@ interface CronJobListProps { scripts: Script[]; } -export function CronJobList({ cronJobs, scripts }: CronJobListProps) { +export const CronJobList = ({ cronJobs, scripts }: CronJobListProps) => { const [deletingId, setDeletingId] = useState(null); const [editingJob, setEditingJob] = useState(null); const [isEditModalOpen, setIsEditModalOpen] = useState(false); @@ -33,6 +47,24 @@ export function CronJobList({ cronJobs, scripts }: CronJobListProps) { const [jobToDelete, setJobToDelete] = useState(null); const [jobToClone, setJobToClone] = useState(null); const [isCloning, setIsCloning] = useState(false); + const [runningJobId, setRunningJobId] = useState(null); + const [selectedUser, setSelectedUser] = useState(null); + + useEffect(() => { + const savedUser = localStorage.getItem("selectedCronUser"); + if (savedUser) { + setSelectedUser(savedUser); + } + }, []); + + useEffect(() => { + if (selectedUser) { + localStorage.setItem("selectedCronUser", selectedUser); + } else { + localStorage.removeItem("selectedCronUser"); + } + }, [selectedUser]); + const [editForm, setEditForm] = useState({ schedule: "", command: "", @@ -43,8 +75,14 @@ export function CronJobList({ cronJobs, scripts }: CronJobListProps) { command: "", comment: "", selectedScriptId: null as string | null, + user: "", }); + const filteredJobs = useMemo(() => { + if (!selectedUser) return cronJobs; + return cronJobs.filter((job) => job.user === selectedUser); + }, [cronJobs, selectedUser]); + const handleDelete = async (id: string) => { setDeletingId(id); try { @@ -85,6 +123,62 @@ export function CronJobList({ cronJobs, scripts }: CronJobListProps) { } }; + const handlePause = async (id: string) => { + try { + const result = await pauseCronJobAction(id); + if (result.success) { + showToast("success", "Cron job paused successfully"); + } else { + showToast("error", "Failed to pause cron job", result.message); + } + } catch (error) { + showToast("error", "Failed to pause cron job", "Please try again later."); + } + }; + + const handleResume = async (id: string) => { + try { + const result = await resumeCronJobAction(id); + if (result.success) { + showToast("success", "Cron job resumed successfully"); + } else { + showToast("error", "Failed to resume cron job", result.message); + } + } catch (error) { + showToast( + "error", + "Failed to resume cron job", + "Please try again later." + ); + } + }; + + const handleRun = async (id: string) => { + setRunningJobId(id); + try { + const result = await runCronJob(id); + if (result.success) { + showToast("success", "Cron job executed successfully"); + if (result.output) { + console.log("Command output:", result.output); + } + } else { + showToast("error", "Failed to execute cron job", result.message); + if (result.output) { + console.error("Command error:", result.output); + } + } + } catch (error) { + showToast( + "error", + "Failed to execute cron job", + "Please try again later." + ); + } finally { + setRunningJobId(null); + } + }; + const confirmDelete = (job: CronJob) => { setJobToDelete(job); setIsDeleteModalOpen(true); @@ -135,11 +229,13 @@ export function CronJobList({ cronJobs, scripts }: CronJobListProps) { const handleNewCronSubmit = async (e: React.FormEvent) => { e.preventDefault(); + try { const formData = new FormData(); formData.append("schedule", newCronForm.schedule); formData.append("command", newCronForm.command); formData.append("comment", newCronForm.comment); + formData.append("user", newCronForm.user); if (newCronForm.selectedScriptId) { formData.append("selectedScriptId", newCronForm.selectedScriptId); } @@ -152,6 +248,7 @@ export function CronJobList({ cronJobs, scripts }: CronJobListProps) { command: "", comment: "", selectedScriptId: null, + user: "", }); showToast("success", "Cron job created successfully"); } else { @@ -180,8 +277,9 @@ export function CronJobList({ cronJobs, scripts }: CronJobListProps) { Scheduled Tasks

- {cronJobs.length} scheduled job - {cronJobs.length !== 1 ? "s" : ""} + {filteredJobs.length} of {cronJobs.length} scheduled job + {filteredJobs.length !== 1 ? "s" : ""} + {selectedUser && ` for ${selectedUser}`}

@@ -195,17 +293,28 @@ export function CronJobList({ cronJobs, scripts }: CronJobListProps) { - {cronJobs.length === 0 ? ( +
+ +
+ + {filteredJobs.length === 0 ? (

- No scheduled tasks yet + {selectedUser + ? `No tasks for user ${selectedUser}` + : "No scheduled tasks yet"}

- Create your first scheduled task to automate your system - operations and boost productivity. + {selectedUser + ? `No scheduled tasks found for user ${selectedUser}. Try selecting a different user or create a new task.` + : "Create your first scheduled task to automate your system operations and boost productivity."}

) : (
- {cronJobs.map((job) => ( + {filteredJobs.map((job) => (
-
-
+
+
{job.schedule} @@ -239,6 +348,18 @@ export function CronJobList({ cronJobs, scripts }: CronJobListProps) {
+
+
+ + {job.user} +
+ {job.paused && ( + + Paused + + )} +
+ {job.comment && (

-

+
+ + {job.paused ? ( + + ) : ( + + )}