diff --git a/setup-cron.sh b/setup-cron.sh
new file mode 100644
index 0000000..3a5dcd6
--- /dev/null
+++ b/setup-cron.sh
@@ -0,0 +1,52 @@
+#!/bin/bash
+
+# Manual script to setup/restart cron jobs
+# Use this if cron wasn't started properly by the docker entrypoint
+
+echo "🔧 Setting up cron jobs..."
+
+# Ensure cron service is running
+if command -v service &> /dev/null; then
+ service cron start 2>/dev/null || true
+fi
+
+# Set up daily backup cron job (runs at 2 AM daily)
+echo "⏰ Setting up daily backup cron job (2 AM)..."
+echo "0 2 * * * cd /app && /usr/local/bin/node backup-db.mjs >> /app/data/backup.log 2>&1" > /etc/cron.d/backup-cron
+chmod 0644 /etc/cron.d/backup-cron
+
+# Set up daily due date reminders cron job (runs at 3 AM daily)
+echo "⏰ Setting up daily due date reminders cron job (3 AM)..."
+echo "0 3 * * * cd /app && /usr/local/bin/node send-due-date-reminders.mjs >> /app/data/reminders.log 2>&1" > /etc/cron.d/reminders-cron
+chmod 0644 /etc/cron.d/reminders-cron
+
+# Combine both cron jobs into crontab
+cat /etc/cron.d/backup-cron /etc/cron.d/reminders-cron > /tmp/combined-cron.tmp
+crontab /tmp/combined-cron.tmp
+rm /tmp/combined-cron.tmp
+
+# Verify cron jobs are installed
+echo ""
+echo "✅ Cron jobs installed:"
+crontab -l
+
+# Check if cron daemon is running
+echo ""
+if pgrep -x "cron" > /dev/null || pgrep -x "crond" > /dev/null; then
+ echo "✅ Cron daemon is running"
+else
+ echo "⚠️ Cron daemon is NOT running. Starting it..."
+ if command -v cron &> /dev/null; then
+ cron
+ echo "✅ Cron daemon started"
+ elif command -v crond &> /dev/null; then
+ crond
+ echo "✅ Cron daemon started"
+ else
+ echo "❌ Could not start cron daemon"
+ exit 1
+ fi
+fi
+
+echo ""
+echo "🎉 Cron setup complete!"
diff --git a/src/app/admin/settings/page.js b/src/app/admin/settings/page.js
index 65a6c14..4c2cc75 100644
--- a/src/app/admin/settings/page.js
+++ b/src/app/admin/settings/page.js
@@ -12,6 +12,9 @@ export default function AdminSettingsPage() {
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [users, setUsers] = useState([]);
+ const [cronStatus, setCronStatus] = useState(null);
+ const [cronLoading, setCronLoading] = useState(false);
+ const [cronActionLoading, setCronActionLoading] = useState(null);
// Redirect if not admin
useEffect(() => {
@@ -22,6 +25,7 @@ export default function AdminSettingsPage() {
}
fetchSettings();
fetchUsers();
+ fetchCronStatus();
}, [session, status, router]);
const fetchSettings = async () => {
@@ -50,6 +54,44 @@ export default function AdminSettingsPage() {
}
};
+ const fetchCronStatus = async () => {
+ setCronLoading(true);
+ try {
+ const response = await fetch("/api/admin/cron");
+ if (response.ok) {
+ const data = await response.json();
+ setCronStatus(data);
+ }
+ } catch (error) {
+ console.error("Error fetching cron status:", error);
+ } finally {
+ setCronLoading(false);
+ }
+ };
+
+ const handleCronAction = async (action) => {
+ setCronActionLoading(action);
+ try {
+ const response = await fetch("/api/admin/cron", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ action }),
+ });
+ const data = await response.json();
+ if (data.success) {
+ alert(data.message);
+ fetchCronStatus();
+ } else {
+ alert("Error: " + data.message);
+ }
+ } catch (error) {
+ console.error("Error performing cron action:", error);
+ alert("Error performing action");
+ } finally {
+ setCronActionLoading(null);
+ }
+ };
+
const updateSetting = async (key, value) => {
setSaving(true);
try {
@@ -166,6 +208,116 @@ export default function AdminSettingsPage() {
{/* Future settings can be added here */}
+
+
+
+ Cron Jobs Status
+
+
+
+
+ {cronLoading && !cronStatus ? (
+
Loading cron status...
+ ) : cronStatus ? (
+
+ {/* Status indicators */}
+
+
+ {cronStatus.available ? "✓ Cron Available" : "⚠ Cron Unavailable"}
+
+ {cronStatus.available && (
+
+ {cronStatus.running ? "✓ Daemon Running" : "✗ Daemon Not Running"}
+
+ )}
+ {cronStatus.available && (
+
+ {cronStatus.jobCount || 0} Job(s) Scheduled
+
+ )}
+
+
+ {/* Scheduled jobs */}
+ {cronStatus.jobs && cronStatus.jobs.length > 0 && (
+
+
Scheduled Jobs:
+
+ {cronStatus.jobs.map((job, idx) => (
+
{job}
+ ))}
+
+
+ )}
+
+ {/* Last backup info */}
+ {cronStatus.lastBackup && (
+
+ Last Backup:
+ {cronStatus.lastBackup.exists ? (
+
+ {cronStatus.lastBackup.filename} ({new Date(cronStatus.lastBackup.date).toLocaleString()})
+
+ ({cronStatus.lastBackup.count} total backups)
+
+
+ ) : (
+ {cronStatus.lastBackup.message || "No backups"}
+ )}
+
+ )}
+
+ {/* Message for non-Linux environments */}
+ {cronStatus.message && (
+
{cronStatus.message}
+ )}
+
+ {/* Action buttons */}
+
+ {cronStatus.available && (
+
+ )}
+
+
+
+
+ ) : (
+
Failed to load cron status
+ )}
+
+
+ {/* System Information */}
System Information
diff --git a/src/app/api/admin/cron/route.js b/src/app/api/admin/cron/route.js
new file mode 100644
index 0000000..14954d2
--- /dev/null
+++ b/src/app/api/admin/cron/route.js
@@ -0,0 +1,191 @@
+import { NextResponse } from "next/server";
+import { withAdminAuth } from "@/lib/middleware/auth";
+import { exec } from "child_process";
+import { promisify } from "util";
+import fs from "fs";
+import path from "path";
+
+const execAsync = promisify(exec);
+
+// Check if we're running in a Linux/Docker environment
+const isLinux = process.platform === "linux";
+
+async function getCronStatus() {
+ if (!isLinux) {
+ return {
+ available: false,
+ running: false,
+ jobs: [],
+ message: "Cron is only available in Linux/Docker environment",
+ lastBackup: getLastBackupInfo(),
+ lastReminder: getLastReminderInfo()
+ };
+ }
+
+ try {
+ // Check if cron daemon is running
+ let cronRunning = false;
+ try {
+ await execAsync("pgrep -x cron || pgrep -x crond");
+ cronRunning = true;
+ } catch {
+ cronRunning = false;
+ }
+
+ // Get current crontab
+ let jobs = [];
+ try {
+ const { stdout } = await execAsync("crontab -l 2>/dev/null");
+ jobs = stdout.trim().split("\n").filter(line => line && !line.startsWith("#"));
+ } catch {
+ jobs = [];
+ }
+
+ return {
+ available: true,
+ running: cronRunning,
+ jobs: jobs,
+ jobCount: jobs.length,
+ lastBackup: getLastBackupInfo(),
+ lastReminder: getLastReminderInfo()
+ };
+ } catch (error) {
+ return {
+ available: false,
+ running: false,
+ jobs: [],
+ error: error.message,
+ lastBackup: getLastBackupInfo(),
+ lastReminder: getLastReminderInfo()
+ };
+ }
+}
+
+function getLastBackupInfo() {
+ try {
+ const backupDir = path.join(process.cwd(), "backups");
+ if (!fs.existsSync(backupDir)) {
+ return { exists: false, message: "No backups directory" };
+ }
+
+ const files = fs.readdirSync(backupDir)
+ .filter(f => f.startsWith("backup-") && f.endsWith(".sqlite"))
+ .map(f => ({
+ name: f,
+ path: path.join(backupDir, f),
+ mtime: fs.statSync(path.join(backupDir, f)).mtime
+ }))
+ .sort((a, b) => b.mtime - a.mtime);
+
+ if (files.length === 0) {
+ return { exists: false, message: "No backups found" };
+ }
+
+ const latest = files[0];
+ return {
+ exists: true,
+ filename: latest.name,
+ date: latest.mtime.toISOString(),
+ count: files.length
+ };
+ } catch (error) {
+ return { exists: false, error: error.message };
+ }
+}
+
+function getLastReminderInfo() {
+ try {
+ const logPath = path.join(process.cwd(), "data", "reminders.log");
+ if (!fs.existsSync(logPath)) {
+ return { exists: false, message: "No reminders log" };
+ }
+
+ const stats = fs.statSync(logPath);
+ return {
+ exists: true,
+ lastModified: stats.mtime.toISOString()
+ };
+ } catch (error) {
+ return { exists: false, error: error.message };
+ }
+}
+
+async function getHandler() {
+ const status = await getCronStatus();
+ return NextResponse.json(status);
+}
+
+async function postHandler(request) {
+ const { action } = await request.json();
+
+ if (!isLinux) {
+ return NextResponse.json({
+ success: false,
+ message: "Cron operations are only available in Linux/Docker environment"
+ }, { status: 400 });
+ }
+
+ try {
+ if (action === "restart") {
+ // Run the setup-cron.sh script
+ const scriptPath = path.join(process.cwd(), "setup-cron.sh");
+
+ if (!fs.existsSync(scriptPath)) {
+ return NextResponse.json({
+ success: false,
+ message: "setup-cron.sh script not found"
+ }, { status: 500 });
+ }
+
+ // Make sure script is executable
+ await execAsync(`chmod +x ${scriptPath}`);
+
+ // Run the script
+ const { stdout, stderr } = await execAsync(`bash ${scriptPath}`);
+
+ // Get updated status
+ const status = await getCronStatus();
+
+ return NextResponse.json({
+ success: true,
+ message: "Cron jobs restarted successfully",
+ output: stdout,
+ status
+ });
+ } else if (action === "run-backup") {
+ // Manually trigger backup
+ const backupScript = path.join(process.cwd(), "backup-db.mjs");
+ const { stdout } = await execAsync(`cd ${process.cwd()} && node ${backupScript}`);
+
+ return NextResponse.json({
+ success: true,
+ message: "Backup completed",
+ output: stdout
+ });
+ } else if (action === "run-reminders") {
+ // Manually trigger reminders
+ const reminderScript = path.join(process.cwd(), "send-due-date-reminders.mjs");
+ const { stdout } = await execAsync(`cd ${process.cwd()} && node ${reminderScript}`);
+
+ return NextResponse.json({
+ success: true,
+ message: "Reminders sent",
+ output: stdout
+ });
+ } else {
+ return NextResponse.json({
+ success: false,
+ message: "Unknown action"
+ }, { status: 400 });
+ }
+ } catch (error) {
+ return NextResponse.json({
+ success: false,
+ message: error.message,
+ stderr: error.stderr
+ }, { status: 500 });
+ }
+}
+
+export const GET = withAdminAuth(getHandler);
+export const POST = withAdminAuth(postHandler);