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."}
setIsNewCronModalOpen(true)}
@@ -218,13 +327,13 @@ export function CronJobList({ cronJobs, scripts }: CronJobListProps) {
) : (
- {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 && (
-
+
+
handleRun(job.id)}
+ disabled={runningJobId === job.id || job.paused}
+ className="btn-outline h-8 px-3"
+ title="Run cron job manually"
+ aria-label="Run cron job manually"
+ >
+ {runningJobId === job.id ? (
+
+ ) : (
+
+ )}
+
+ {job.paused ? (
+
handleResume(job.id)}
+ className="btn-outline h-8 px-3"
+ title="Resume cron job"
+ aria-label="Resume cron job"
+ >
+
+
+ ) : (
+
handlePause(job.id)}
+ className="btn-outline h-8 px-3"
+ title="Pause cron job"
+ aria-label="Pause cron job"
+ >
+
+
+ )}
{
const [scripts, setScripts] = useState