fix: persist a one-time schedule's run time so edit and disable keep its year (#1499)

PR #1497 pinned an enabled one-shot's next_run_at on creation, but the chosen
instant did not survive two paths: editing reconstructed the date from the
yearless cron (current year), and a disabled one-shot nulled next_run_at with no
other store, so enabling it later recomputed from the cron. Both moved a
future-year one-shot to a different annual occurrence than the date displayed.

Persist the one-shot's absolute fire time in a dedicated run_at column
(additive, nullable; recurring schedules leave it null). Create and update store
run_at independently of the enabled state; next_run_at is derived from it when
enabled and null while disabled, so a disabled one-shot keeps its run_at and the
enable toggle restores the exact instant from the column rather than the cron.
The editor reconstructs a one-shot's date from the persisted run_at, so opening
and re-saving without changes preserves the originally chosen year.

No behavior change for recurring schedules or fresh installs; the column is added
by an additive migration safe for upgrades from v0.92.0.
This commit is contained in:
Anso
2026-06-28 05:23:23 -04:00
committed by GitHub
parent 60536aa614
commit ba57c67048
6 changed files with 139 additions and 31 deletions
@@ -241,7 +241,15 @@ export default function ScheduledOperationsView({ filterNodeId, onClearFilter, p
setFormTargetId(task.target_id || '');
setFormNodeId(task.node_id != null ? String(task.node_id) : '');
setFormCron(task.cron_expression);
const parsed = parseCron(task.cron_expression, (task.delete_after_run ?? 0) === 1);
let parsed = parseCron(task.cron_expression, (task.delete_after_run ?? 0) === 1);
// The cron has no year field, so parseCron reconstructs a one-shot's date in
// the current year. Rebuild it from the persisted run_at instead, so editing
// (and re-saving) preserves the originally chosen instant rather than moving
// it to this year's occurrence.
if (parsed && parsed.frequency === 'once' && task.run_at != null) {
const pinned = new Date(task.run_at);
parsed = { ...parsed, date: pinned, hour: pinned.getHours(), minute: pinned.getMinutes() };
}
setScheduleMode(parsed ? 'simple' : 'advanced');
setSimpleSchedule(parsed ?? DEFAULT_SIMPLE_SCHEDULE);
setSimpleReplacedCron(false);