diff --git a/crates/openfang-api/static/js/pages/scheduler.js b/crates/openfang-api/static/js/pages/scheduler.js index 94286cdc..8dfb4762 100644 --- a/crates/openfang-api/static/js/pages/scheduler.js +++ b/crates/openfang-api/static/js/pages/scheduler.js @@ -204,7 +204,9 @@ function schedulerPage() { var result = await OpenFangAPI.post('/api/cron/jobs/' + job.id + '/run', {}); if (result.status === 'triggered' || result.status === 'completed') { OpenFangToast.success('Job "' + (job.name || 'job') + '" triggered'); - job.last_run = new Date().toISOString(); + // Don't update job.last_run here — the job runs asynchronously in the + // background. The real last_run is set by the server on completion and + // will appear on the next data refresh. } else { OpenFangToast.error('Run failed: ' + (result.error || 'Unknown error')); } diff --git a/crates/openfang-kernel/src/cron.rs b/crates/openfang-kernel/src/cron.rs index 07c4fedf..27a47daf 100644 --- a/crates/openfang-kernel/src/cron.rs +++ b/crates/openfang-kernel/src/cron.rs @@ -387,7 +387,13 @@ impl CronScheduler { ); meta.job.enabled = false; } else { - meta.job.next_run = Some(compute_next_run_after(&meta.job.schedule, Utc::now())); + // Only recompute next_run if the job was already overdue. This + // preserves the scheduled fire time when a manual (on-demand) run + // fails before the job's natural next_run. + let now = Utc::now(); + if meta.job.next_run.map(|t| t <= now).unwrap_or(true) { + meta.job.next_run = Some(compute_next_run_after(&meta.job.schedule, now)); + } } } }