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