import { useCallback, useEffect, useMemo, useState } from "react"; import cronstrue from "cronstrue"; import { CronExpressionParser } from "cron-parser"; import { api, type CurrentUser, type ManualTaskInput, type ScheduleType, type ServerRecord, type TaskRecord, } from "../api/client"; import CopyButton from "../components/CopyButton"; import { formatDateTime } from "../utils/date"; const SCHEDULE_TYPE_LABELS: Record = { cron: "Cron jobs", systemd_timer: "systemd timers", docker: "Docker jobs", backup: "Backups", update: "Updates", n8n_workflow: "n8n workflows", manual: "Other", }; const SCHEDULE_TYPE_OPTIONS: { value: ScheduleType; label: string }[] = [ { value: "docker", label: "Docker" }, { value: "cron", label: "Cron" }, { value: "systemd_timer", label: "systemd timer" }, { value: "backup", label: "Backup" }, { value: "update", label: "Update" }, { value: "n8n_workflow", label: "n8n workflow" }, { value: "manual", label: "Other / manual" }, ]; function installCommand(token: string): string { return `curl -fsSL ${window.location.origin}/agent/linux/install.sh | sudo API_URL=${window.location.origin} API_TOKEN=${token} bash`; } function uninstallCommand(): string { return `curl -fsSL ${window.location.origin}/agent/linux/uninstall.sh | sudo bash`; } function describeSchedule(task: TaskRecord): string { if (!task.scheduleExpression) return "—"; if (task.scheduleType === "cron") { try { return cronstrue.toString(task.scheduleExpression, { verbose: false }); } catch { return task.scheduleExpression; } } return task.scheduleExpression; } interface NextRun { date: Date; estimated: boolean; } function computeNextRun(task: TaskRecord): NextRun | null { if (task.nextRunAt) return { date: new Date(task.nextRunAt), estimated: false }; if (task.scheduleType !== "cron" || !task.scheduleExpression || task.isStale || !task.enabled) return null; try { const date = CronExpressionParser.parse(task.scheduleExpression).next().toDate(); return { date, estimated: true }; } catch { return null; } } const emptyTaskForm = { scheduleType: "docker" as ScheduleType, name: "", command: "", scheduleExpression: "", enabled: true, }; export default function ServersTasks({ user }: { user: CurrentUser }) { const isAdmin = user.role === "admin"; const canEditTasks = user.role === "admin" || user.role === "operator"; const [servers, setServers] = useState([]); const [tasks, setTasks] = useState(null); const [error, setError] = useState(null); const [managingServers, setManagingServers] = useState(false); const [filters, setFilters] = useState({ serverId: undefined as number | undefined, scheduleType: undefined as string | undefined, search: "", includeStale: false, }); const [taskForm, setTaskForm] = useState(null); const [editingTask, setEditingTask] = useState(null); const [newTaskServerId, setNewTaskServerId] = useState(undefined); const [savingTask, setSavingTask] = useState(false); const [serverName, setServerName] = useState(""); const [serverHostname, setServerHostname] = useState(""); const [serverDescription, setServerDescription] = useState(""); const [newToken, setNewToken] = useState<{ server: ServerRecord; token: string } | null>(null); const [uninstallFor, setUninstallFor] = useState(null); const loadServers = useCallback(() => { return api.servers .list() .then((res) => setServers(res.servers)) .catch((err) => setError(String(err))); }, []); const loadTasks = useCallback(() => { return api.tasks .list(filters) .then((res) => setTasks(res.tasks)) .catch((err) => setError(String(err))); }, [filters]); useEffect(() => { loadServers(); }, [loadServers]); useEffect(() => { const handle = setTimeout(loadTasks, 200); return () => clearTimeout(handle); }, [loadTasks]); const groups = useMemo(() => { const byServer = new Map(); for (const task of tasks ?? []) { const entry = byServer.get(task.serverId) ?? { serverId: task.serverId, serverName: task.serverName, tasks: [] }; entry.tasks.push(task); byServer.set(task.serverId, entry); } return [...byServer.values()]; }, [tasks]); function openAddTask() { setEditingTask(null); setNewTaskServerId(filters.serverId ?? servers[0]?.id); setTaskForm({ ...emptyTaskForm }); } function openEditTask(task: TaskRecord) { setEditingTask(task); setTaskForm({ scheduleType: task.scheduleType, name: task.name, command: task.command ?? "", scheduleExpression: task.scheduleExpression ?? "", enabled: task.enabled, }); } async function submitTask(e: React.FormEvent) { e.preventDefault(); if (!taskForm) return; setSavingTask(true); setError(null); try { const serverId = editingTask?.serverId ?? newTaskServerId; if (!serverId) { setError("Choose a server first."); return; } const data: ManualTaskInput = { serverId, scheduleType: taskForm.scheduleType, name: taskForm.name, command: taskForm.command || undefined, scheduleExpression: taskForm.scheduleExpression || undefined, enabled: taskForm.enabled, }; if (editingTask) { await api.tasks.update(editingTask.id, data); } else { await api.tasks.create(data); } setTaskForm(null); setEditingTask(null); await loadTasks(); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setSavingTask(false); } } async function deleteTask(task: TaskRecord) { if (!confirm(`Delete "${task.name}"?`)) return; try { await api.tasks.remove(task.id); await loadTasks(); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } } async function createServer(e: React.FormEvent) { e.preventDefault(); setError(null); try { const result = await api.servers.create({ name: serverName, hostname: serverHostname || undefined, description: serverDescription || undefined, }); setNewToken(result); setServerName(""); setServerHostname(""); setServerDescription(""); await loadServers(); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } } async function rotateToken(id: number) { try { const result = await api.servers.rotateToken(id); setNewToken(result); await loadServers(); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } } async function deleteServer(id: number) { if (!confirm("Remove this server and all its tracked tasks?")) return; try { await api.servers.remove(id); await loadServers(); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } } return ( <>

Servers & Tasks

{isAdmin && ( )}
{error &&
{error}
} {managingServers ? ( <>

Add a server

setServerName(e.target.value)} />
setServerHostname(e.target.value)} />
setServerDescription(e.target.value)} />
{newToken && (

Agent token for {newToken.server.name}

This token is only shown once — copy it now.

{newToken.token}

Install the agent on the server (run as root — put sudo right after the pipe, not before curl):

{installCommand(newToken.token)}
)} {uninstallFor && (

Uninstall agent from {uninstallFor.name}

Run this on the server as root to stop and remove the agent (its entry here is kept):

{uninstallCommand()}
)}
{servers.map((s) => ( ))} {servers.length === 0 && ( )}
Name Hostname Token Last seen Actions
{s.name} {s.hostname ?? "—"} {s.apiTokenPrefix}… {s.lastSeenAt ? formatDateTime(new Date(s.lastSeenAt)) : "never"}
No servers registered yet.
) : ( <>
setFilters({ ...filters, search: e.target.value })} />
{canEditTasks && ( )}
{taskForm && (

{editingTask ? "Edit task" : "Add a task manually"}

{!editingTask && (
)}
setTaskForm({ ...taskForm, name: e.target.value })} />
setTaskForm({ ...taskForm, command: e.target.value })} />
setTaskForm({ ...taskForm, scheduleExpression: e.target.value })} />
)} {tasks && groups.length === 0 && (
No scheduled tasks found yet. Install the agent on a server to auto-report cron/systemd tasks, or use "Add task" above for things like Docker-based backup jobs.
)} {groups.map((group) => { const byType = new Map(); for (const task of group.tasks) { const list = byType.get(task.scheduleType) ?? []; list.push(task); byType.set(task.scheduleType, list); } return (

{group.serverName}

{[...byType.entries()].map(([scheduleType, groupTasks]) => (
{SCHEDULE_TYPE_LABELS[scheduleType] ?? scheduleType}
{canEditTasks && } {groupTasks.map((task) => { const nextRun = computeNextRun(task); return ( {canEditTasks && ( )} ); })}
Name Command Schedule Next run StatusActions
{task.name} {task.command ?? "—"}
{describeSchedule(task)}
{task.scheduleExpression && ( {task.scheduleExpression} )}
{nextRun ? ( {nextRun.estimated ? "~" : ""} {formatDateTime(nextRun.date)} ) : ( "—" )} {task.isStale ? ( Missing ) : task.enabled ? ( Enabled ) : ( Disabled )} {task.origin === "manual" && Manual} {task.origin === "manual" && (
)}
))}
); })} )} ); }