"use client"; import { useEffect, useState, useCallback } from "react"; import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Switch } from "@/components/ui/switch"; import { Skeleton } from "@/components/ui/skeleton"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription, } from "@/components/ui/dialog"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Clock, Plus, MoreHorizontal, Trash2, Edit, RefreshCw } from "lucide-react"; import { toast } from "sonner"; import { formatDistanceToNow } from "date-fns"; interface Task { id: string; name: string; description: string | null; cronExpression: string; command: string; isEnabled: boolean; lastRun: number | null; nextRun: number | null; createdAt: number; } const CRON_PRESETS = [ { label: "Every minute", value: "* * * * *" }, { label: "Every 5 minutes", value: "*/5 * * * *" }, { label: "Every hour", value: "0 * * * *" }, { label: "Every day at midnight", value: "0 0 * * *" }, { label: "Every Sunday at 3am", value: "0 3 * * 0" }, ]; function TaskForm({ initial, onSubmit, onCancel, loading, }: { initial?: Partial; onSubmit: (data: Omit) => void; onCancel: () => void; loading: boolean; }) { const [name, setName] = useState(initial?.name ?? ""); const [description, setDescription] = useState(initial?.description ?? ""); const [cronExpression, setCronExpression] = useState(initial?.cronExpression ?? "0 0 * * *"); const [command, setCommand] = useState(initial?.command ?? ""); const [isEnabled, setIsEnabled] = useState(initial?.isEnabled ?? true); return (
setName(e.target.value)} placeholder="e.g. Daily restart" className="mt-1 bg-zinc-800 border-zinc-700 text-white" />
setDescription(e.target.value)} placeholder="Brief description" className="mt-1 bg-zinc-800 border-zinc-700 text-white" />
setCronExpression(e.target.value)} placeholder="* * * * *" className="mt-1 bg-zinc-800 border-zinc-700 text-white font-mono" />
{CRON_PRESETS.map((p) => ( ))}
setCommand(e.target.value)} placeholder="e.g. say Server restart in 5 minutes" className="mt-1 bg-zinc-800 border-zinc-700 text-white font-mono" />

Enter a Minecraft command (without leading /) to execute via RCON.

); } export default function SchedulerPage() { const [tasks, setTasks] = useState([]); const [loading, setLoading] = useState(true); const [dialogOpen, setDialogOpen] = useState(false); const [editTask, setEditTask] = useState(null); const [deleteId, setDeleteId] = useState(null); const [saving, setSaving] = useState(false); const fetchTasks = useCallback(async () => { try { const res = await fetch("/api/scheduler"); if (res.ok) setTasks((await res.json()).tasks); } finally { setLoading(false); } }, []); useEffect(() => { fetchTasks(); }, [fetchTasks]); const handleCreate = async (data: Parameters[0]["onSubmit"] extends (d: infer D) => void ? D : never) => { setSaving(true); try { const res = await fetch("/api/scheduler", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); if (!res.ok) throw new Error((await res.json()).error); toast.success("Task created"); setDialogOpen(false); fetchTasks(); } catch (err) { toast.error(err instanceof Error ? err.message : "Failed to create task"); } finally { setSaving(false); } }; const handleUpdate = async (data: Parameters[0]["onSubmit"] extends (d: infer D) => void ? D : never) => { if (!editTask) return; setSaving(true); try { const res = await fetch(`/api/scheduler/${editTask.id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); if (!res.ok) throw new Error((await res.json()).error); toast.success("Task updated"); setEditTask(null); fetchTasks(); } catch (err) { toast.error(err instanceof Error ? err.message : "Failed to update task"); } finally { setSaving(false); } }; const handleDelete = async (id: string) => { try { const res = await fetch(`/api/scheduler/${id}`, { method: "DELETE" }); if (!res.ok) throw new Error((await res.json()).error); toast.success("Task deleted"); setTasks((p) => p.filter((t) => t.id !== id)); } catch (err) { toast.error(err instanceof Error ? err.message : "Failed to delete"); } setDeleteId(null); }; const toggleTask = async (task: Task) => { try { const res = await fetch(`/api/scheduler/${task.id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ isEnabled: !task.isEnabled }), }); if (!res.ok) throw new Error((await res.json()).error); fetchTasks(); } catch (err) { toast.error(err instanceof Error ? err.message : "Failed to toggle"); } }; return (

Scheduler

Automated recurring tasks

{loading ? (
{[1,2,3].map(i => )}
) : tasks.length === 0 ? (

No scheduled tasks

Create a task to automate server commands

) : (
{tasks.map(task => (
toggleTask(task)} className="shrink-0" />

{task.name}

{task.description && ( — {task.description} )}
{task.cronExpression} {task.command}
{task.lastRun ? (

Last: {formatDistanceToNow(new Date(task.lastRun), { addSuffix: true })}

) : (

Never run

)}
setEditTask(task)} className="text-zinc-300 focus:text-white focus:bg-zinc-800"> Edit setDeleteId(task.id)} className="text-red-400 focus:text-red-300 focus:bg-zinc-800"> Delete
))}
)}
{/* Create dialog */} New Scheduled Task Schedule a Minecraft command to run automatically. setDialogOpen(false)} loading={saving} /> {/* Edit dialog */} !o && setEditTask(null)}> Edit Task {editTask && setEditTask(null)} loading={saving} />} {/* Delete confirm */} setDeleteId(null)}> Delete task? This will stop and remove the scheduled task permanently. Cancel deleteId && handleDelete(deleteId)} className="bg-red-600 hover:bg-red-500 text-white">Delete
); }