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.
This commit is contained in:
1 parent
329dcab936
commit
0f961e7377
15 files changed
+541
-1819
No files matched your search
@@ -1,4 +1,5 @@
|
||||
import cronstrue from "cronstrue";
|
||||
import { CronExpressionParser } from "cron-parser";
|
||||
import type { TaskRecord } from "../api/client";
|
||||
|
||||
function describeSchedule(task: TaskRecord): string {
|
||||
@@ -13,6 +14,27 @@ function describeSchedule(task: TaskRecord): string {
|
||||
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;
|
||||
@@ -33,39 +55,59 @@ export default function TaskTable({ tasks, onEdit, onDelete }: Props) {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tasks.map((task) => (
|
||||
<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>{task.nextRunAt ? new Date(task.nextRunAt).toLocaleString() : "—"}</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>
|
||||
))}
|
||||
{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>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user