"use client"; import { useState } from "react"; import { CRON_PRESETS, SHELL_COMMAND_ALLOWLIST, type CreateScheduledTaskInput, type ScheduledTaskConfig, type ScheduledTaskRecord, type ScheduledTaskType, } from "@/lib/shared/contracts/scheduled-tasks"; import { useScheduledTasks } from "@/modules/settings/hooks/useScheduledTasks"; import { useInstalledApps } from "@/modules/apps/hooks/useInstalledApps"; import { SectionDivider } from "@/modules/settings/components/panel/controls"; import { SETTINGS_PANEL_INSET } from "@/modules/settings/components/panel/surface"; import { AlertTriangle, Check, ChevronDownIcon as ChevronDown, Clock, Play, Plus, Trash2, } from "@/components/icons/platform-icons"; import { cn } from "@/lib/utils"; type TaskFormState = { label: string; taskType: ScheduledTaskType; shellCommand: string; script: string; appId: string; appName: string; cronPreset: string; customCron: string; }; const TASK_TYPE_LABELS: Record = { shell: "Shell Command", script: "Custom Script", "restart-app": "Restart App", backup: "Run Backup", "pull-images": "Pull Docker Images", }; const DEFAULT_FORM: TaskFormState = { label: "", taskType: "shell", shellCommand: SHELL_COMMAND_ALLOWLIST[0], script: "", appId: "", appName: "", cronPreset: CRON_PRESETS[1].value, customCron: "0 3 * * *", }; function formatRelative(iso: string | null, future = false): string { if (!iso) return "Never"; const diff = new Date(iso).getTime() - Date.now(); const absDiff = Math.abs(diff); const minutes = Math.floor(absDiff / 60_000); const suffix = future ? (diff > 0 ? " from now" : " ago") : " ago"; if (minutes < 1) return future && diff > 0 ? "< 1 min" : "Just now"; if (minutes < 60) return `${minutes}m${suffix}`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}h${suffix}`; return `${Math.floor(hours / 24)}d${suffix}`; } function buildConfig(form: TaskFormState): ScheduledTaskConfig { if (form.taskType === "shell") return { type: "shell", command: form.shellCommand }; if (form.taskType === "script") return { type: "script", script: form.script }; if (form.taskType === "restart-app") return { type: "restart-app", appId: form.appId, appName: form.appName }; if (form.taskType === "backup") return { type: "backup" }; return { type: "pull-images" }; } function buildCron(form: TaskFormState): string { return form.cronPreset === "custom" ? form.customCron : form.cronPreset; } // ── Field helpers ──────────────────────────────────────────────────────────── function Field({ label, children }: { label: string; children: React.ReactNode }) { return (
{children}
); } const inputCls = "h-8 w-full rounded-lg border border-glass-border bg-background/55 px-3 text-xs text-foreground placeholder:text-muted-foreground/50 focus:border-primary/40 focus:outline-none"; const selectCls = "h-8 w-full cursor-pointer appearance-none rounded-lg border border-glass-border bg-background/55 px-3 text-xs text-foreground focus:border-primary/40 focus:outline-none"; // ── Task row ───────────────────────────────────────────────────────────────── function TaskRow({ task, onToggle, onRunNow, onDelete, isRunning, }: { task: ScheduledTaskRecord; onToggle: () => void; onRunNow: () => void; onDelete: () => void; isRunning: boolean; }) { const [expanded, setExpanded] = useState(false); return (
{/* Header */} {/* Expanded detail */} {expanded && (
{/* Info rows */}
Last run {formatRelative(task.lastRunAt)}
Next run {task.nextRunAt ? formatRelative(task.nextRunAt, true) : "—"}
{task.lastRunOutput && (
Output
{task.lastRunOutput}
)}
{/* Actions */}
)}
); } // ── Create form ────────────────────────────────────────────────────────────── function CreateTaskForm({ onSubmit, onCancel, }: { onSubmit: (input: CreateScheduledTaskInput) => Promise; onCancel: () => void; }) { const [form, setForm] = useState(DEFAULT_FORM); const [submitting, setSubmitting] = useState(false); const { data: installedApps = [] } = useInstalledApps(); const patch = (u: Partial) => setForm((f) => ({ ...f, ...u })); const cronExpression = buildCron(form); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!form.label.trim()) return; if (form.taskType === "script" && !form.script.trim()) return; if (form.taskType === "restart-app" && !form.appId) return; setSubmitting(true); try { await onSubmit({ label: form.label.trim(), taskType: form.taskType, taskConfig: buildConfig(form), cronExpression, enabled: true, }); } finally { setSubmitting(false); } } return (
New Task
patch({ label: e.target.value })} placeholder="e.g. Clean Docker images" className={inputCls} /> {form.taskType === "shell" && ( )} {form.taskType === "script" && (