import { useCallback, useEffect, useState } from "react"; import { Clock, Pause, Play, Plus, Trash2, Zap } from "lucide-react"; import { Badge } from "@nous-research/ui/ui/components/badge"; import { Button } from "@nous-research/ui/ui/components/button"; import { Select, SelectOption } from "@nous-research/ui/ui/components/select"; import { Spinner } from "@nous-research/ui/ui/components/spinner"; import { H2 } from "@/components/NouiTypography"; import { api } from "@/lib/api"; import type { CronJob } from "@/lib/api"; import { DeleteConfirmDialog } from "@/components/DeleteConfirmDialog"; import { useToast } from "@/hooks/useToast"; import { useConfirmDelete } from "@/hooks/useConfirmDelete"; import { Toast } from "@/components/Toast"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { useI18n } from "@/i18n"; import { PluginSlot } from "@/plugins"; function formatTime(iso?: string | null): string { if (!iso) return "—"; const d = new Date(iso); return d.toLocaleString(); } function asText(value: unknown): string { return typeof value === "string" ? value : ""; } function truncateText(value: string, maxLength: number): string { return value.length > maxLength ? value.slice(0, maxLength) + "..." : value; } function getJobPrompt(job: CronJob): string { return asText(job.prompt); } function getJobName(job: CronJob): string { return asText(job.name).trim(); } function getJobTitle(job: CronJob): string { const name = getJobName(job); if (name) return name; const prompt = getJobPrompt(job); if (prompt) return truncateText(prompt, 60); const script = asText(job.script); if (script) return truncateText(script, 60); return job.id || "Cron job"; } function getJobScheduleDisplay(job: CronJob): string { return ( asText(job.schedule_display) || asText(job.schedule?.display) || asText(job.schedule?.expr) || "—" ); } function getJobState(job: CronJob): string { return asText(job.state) || (job.enabled === false ? "disabled" : "scheduled"); } const STATUS_TONE: Record = { enabled: "success", scheduled: "success", paused: "warning", error: "destructive", completed: "destructive", }; export default function CronPage() { const [jobs, setJobs] = useState([]); const [loading, setLoading] = useState(true); const { toast, showToast } = useToast(); const { t } = useI18n(); // New job form state const [prompt, setPrompt] = useState(""); const [schedule, setSchedule] = useState(""); const [name, setName] = useState(""); const [deliver, setDeliver] = useState("local"); const [creating, setCreating] = useState(false); const loadJobs = useCallback(() => { api .getCronJobs() .then(setJobs) .catch(() => showToast(t.common.loading, "error")) .finally(() => setLoading(false)); }, [showToast, t.common.loading]); useEffect(() => { loadJobs(); }, [loadJobs]); const handleCreate = async () => { if (!prompt.trim() || !schedule.trim()) { showToast(`${t.cron.prompt} & ${t.cron.schedule} required`, "error"); return; } setCreating(true); try { await api.createCronJob({ prompt: prompt.trim(), schedule: schedule.trim(), name: name.trim() || undefined, deliver, }); showToast(t.common.create + " ✓", "success"); setPrompt(""); setSchedule(""); setName(""); setDeliver("local"); loadJobs(); } catch (e) { showToast(`${t.config.failedToSave}: ${e}`, "error"); } finally { setCreating(false); } }; const handlePauseResume = async (job: CronJob) => { try { const isPaused = getJobState(job) === "paused"; if (isPaused) { await api.resumeCronJob(job.id); showToast( `${t.cron.resume}: "${truncateText(getJobTitle(job), 30)}"`, "success", ); } else { await api.pauseCronJob(job.id); showToast( `${t.cron.pause}: "${truncateText(getJobTitle(job), 30)}"`, "success", ); } loadJobs(); } catch (e) { showToast(`${t.status.error}: ${e}`, "error"); } }; const handleTrigger = async (job: CronJob) => { try { await api.triggerCronJob(job.id); showToast( `${t.cron.triggerNow}: "${truncateText(getJobTitle(job), 30)}"`, "success", ); loadJobs(); } catch (e) { showToast(`${t.status.error}: ${e}`, "error"); } }; const jobDelete = useConfirmDelete({ onDelete: useCallback( async (id: string) => { const job = jobs.find((j) => j.id === id); try { await api.deleteCronJob(id); showToast( `${t.common.delete}: "${job ? truncateText(getJobTitle(job), 30) : id}"`, "success", ); loadJobs(); } catch (e) { showToast(`${t.status.error}: ${e}`, "error"); throw e; } }, [jobs, loadJobs, showToast, t.common.delete, t.status.error], ), }); if (loading) { return (
); } const pendingJob = jobDelete.pendingId ? jobs.find((j) => j.id === jobDelete.pendingId) : null; return (
{t.cron.newJob}
setName(e.target.value)} />