Files
ScheduleTaskManager/web/src/components/TaskTable.tsx
T
bobban 0f961e7377 Add backup/update schedule types, copy buttons, cron next-run estimate; update dependencies
- New "backup" and "update" schedule types for manually tracked tasks,
  plumbed through the form, filters, group labels, and API validation.
- Copy buttons next to the agent token and install/uninstall commands
  on the Servers page, so they don't need to be selected by hand.
- Cron jobs now show an estimated "next run" (via cron-parser),
  computed client-side and clearly marked with "~" plus a tooltip,
  since the agent has no way to compute this for cron (unlike systemd
  timers, which report a real value from systemctl).
- Dependency updates: fixes a high-severity SQL injection advisory in
  drizzle-orm (0.38.4 -> 0.45.2, plus drizzle-kit 0.31.10), a qs
  vulnerability pinned past body-parser's own range via an npm
  "overrides" entry, a leftover vulnerable esbuild pulled in
  transitively by drizzle-kit (also via override), and cron-parser
  4 -> 5 (v4 is unmaintained; updated its call site for the new
  CronExpressionParser API). `npm audit` is now clean. Re-verified the
  DB layer end-to-end (migrations, CRUD, agent sync, stale-marking)
  and the cron estimate against the new cron-parser API after the
  upgrade.
2026-09-02 23:51:21 +02:00

115 lines
3.6 KiB
TypeScript

import cronstrue from "cronstrue";
import { CronExpressionParser } from "cron-parser";
import type { TaskRecord } from "../api/client";
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;
/** true when computed client-side from the cron expression rather than reported by the agent */
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;
}
}
interface Props {
tasks: TaskRecord[];
onEdit?: (task: TaskRecord) => void;
onDelete?: (task: TaskRecord) => void;
}
export default function TaskTable({ tasks, onEdit, onDelete }: Props) {
return (
<table className="task-table">
<thead>
<tr>
<th>Name</th>
<th>Command</th>
<th>Schedule</th>
<th>Next run</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
{tasks.map((task) => {
const nextRun = computeNextRun(task);
return (
<tr key={task.id} className={task.isStale ? "stale" : undefined}>
<td>{task.name}</td>
<td className="mono">{task.command ?? "—"}</td>
<td>
<div className="schedule-human">{describeSchedule(task)}</div>
{task.scheduleExpression && (
<span className="mono schedule-raw">{task.scheduleExpression}</span>
)}
</td>
<td>
{nextRun ? (
<span
title={
nextRun.estimated
? "Estimated from the cron expression in your browser's timezone — the server may run in a different timezone"
: undefined
}
>
{nextRun.estimated ? "~" : ""}
{nextRun.date.toLocaleString()}
</span>
) : (
"—"
)}
</td>
<td>
{task.isStale ? (
<span className="badge badge-stale">Missing</span>
) : task.enabled ? (
<span className="badge badge-ok">Enabled</span>
) : (
<span className="badge badge-disabled">Disabled</span>
)}
{task.origin === "manual" && <span className="badge badge-manual">Manual</span>}
</td>
<td className="actions">
{task.origin === "manual" && (
<>
<button className="btn btn-small" onClick={() => onEdit?.(task)}>
Edit
</button>
<button className="btn btn-small btn-danger" onClick={() => onDelete?.(task)}>
Delete
</button>
</>
)}
</td>
</tr>
);
})}
</tbody>
</table>
);
}