mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-27 10:46:51 +00:00
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:
@@ -546,6 +546,11 @@ export interface ScheduledTask {
|
||||
target_services: string | null;
|
||||
prune_label_filter: string | null;
|
||||
delete_after_run?: number;
|
||||
// Absolute epoch-ms fire time for a one-time ('once') schedule. A 5-field
|
||||
// cron has no year field, so the chosen instant (including year) is persisted
|
||||
// here and survives disable/enable and edit, where the cron alone would
|
||||
// collapse to the next annual occurrence. Null for recurring schedules.
|
||||
run_at?: number | null;
|
||||
}
|
||||
|
||||
export interface ScheduledTaskRun {
|
||||
@@ -1558,6 +1563,7 @@ export class DatabaseService {
|
||||
maybeAddCol('scheduled_tasks', 'target_services', 'TEXT DEFAULT NULL');
|
||||
maybeAddCol('scheduled_tasks', 'prune_label_filter', 'TEXT DEFAULT NULL');
|
||||
maybeAddCol('scheduled_tasks', 'delete_after_run', 'INTEGER DEFAULT 0');
|
||||
maybeAddCol('scheduled_tasks', 'run_at', 'INTEGER DEFAULT NULL');
|
||||
|
||||
// Recreate stack_update_status with composite PK (node_id, stack_name).
|
||||
// Original table had stack_name as sole PK which breaks when multiple nodes share stack names.
|
||||
@@ -4295,13 +4301,13 @@ export class DatabaseService {
|
||||
|
||||
public createScheduledTask(task: Omit<ScheduledTask, 'id'>): number {
|
||||
const result = this.db.prepare(
|
||||
'INSERT INTO scheduled_tasks (name, target_type, target_id, node_id, action, cron_expression, enabled, created_by, created_at, updated_at, last_run_at, next_run_at, last_status, last_error, prune_targets, target_services, prune_label_filter, delete_after_run) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
||||
'INSERT INTO scheduled_tasks (name, target_type, target_id, node_id, action, cron_expression, enabled, created_by, created_at, updated_at, last_run_at, next_run_at, last_status, last_error, prune_targets, target_services, prune_label_filter, delete_after_run, run_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
||||
).run(
|
||||
task.name, task.target_type, task.target_id, task.node_id,
|
||||
task.action, task.cron_expression, task.enabled, task.created_by,
|
||||
task.created_at, task.updated_at, task.last_run_at, task.next_run_at,
|
||||
task.last_status, task.last_error, task.prune_targets, task.target_services,
|
||||
task.prune_label_filter, task.delete_after_run ?? 0
|
||||
task.prune_label_filter, task.delete_after_run ?? 0, task.run_at ?? null
|
||||
);
|
||||
return result.lastInsertRowid as number;
|
||||
}
|
||||
@@ -4319,8 +4325,13 @@ export class DatabaseService {
|
||||
prune_targets: updates.prune_targets, target_services: updates.target_services,
|
||||
prune_label_filter: updates.prune_label_filter,
|
||||
delete_after_run: updates.delete_after_run,
|
||||
run_at: updates.run_at,
|
||||
};
|
||||
|
||||
// `undefined` means "leave this column unchanged"; an explicit `null`
|
||||
// writes SQL NULL. Callers rely on this distinction (e.g. run_at: null
|
||||
// clears a one-shot's pin while an omitted run_at preserves it), so do
|
||||
// not relax this guard to a truthy or `!= null` check.
|
||||
for (const [col, val] of Object.entries(map)) {
|
||||
if (val !== undefined) {
|
||||
fields.push(`${col} = ?`);
|
||||
|
||||
Reference in New Issue
Block a user