Pick and show schedules as plain time instead of cron

Schedules were entered and displayed as six field cron expressions. A
picker now offers a frequency, a weekday or day of the month and a time,
and turns that into the expression the backend stores; tables and the
dashboard show wording like "Every day at 03:00". An expression the
picker cannot express stays editable as cron.

This also fixes a broken preset: the backend's cron dialect counts Sunday
as 1 and rejects 0, so the "every Sunday" choice in the application
backup form produced an invalid expression. The picker emits weekday
names, and a test pins the dialect down.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Dennis Nemec
2026-09-03 21:47:56 +02:00
parent 1339ae864e
commit 6796634906
15 changed files with 513 additions and 62 deletions

View File

@ -31,3 +31,50 @@ fn due_when_a_fire_time_lies_between_last_run_and_now() {
assert!(!is_due(hourly, None, now + Duration::minutes(10), 300));
assert!(!is_due("bad", None, now, 300));
}
/// The UI offers simple choices ("every Sunday at 3:00") and turns them into these
/// expressions, so the dialect they have to match is pinned down here.
#[test]
fn documents_the_cron_dialect_the_ui_generates() {
let fires_at = |expr: &str, after: chrono::DateTime<Utc>| next_fire(expr, after).unwrap();
let monday = Utc.with_ymd_and_hms(2026, 9, 7, 0, 0, 0).unwrap(); // a Monday
// every day at 3:00
assert_eq!(
fires_at("0 0 3 * * *", monday),
Utc.with_ymd_and_hms(2026, 9, 7, 3, 0, 0).unwrap()
);
// every six hours, on the hour
assert_eq!(
fires_at("0 0 */6 * * *", monday),
Utc.with_ymd_and_hms(2026, 9, 7, 6, 0, 0).unwrap()
);
// every hour at half past
assert_eq!(
fires_at("0 30 * * * *", monday),
Utc.with_ymd_and_hms(2026, 9, 7, 0, 30, 0).unwrap()
);
// on the first day of the month at 3:30
assert_eq!(
fires_at("0 30 3 1 * *", monday),
Utc.with_ymd_and_hms(2026, 10, 1, 3, 30, 0).unwrap()
);
// Weekdays are 1-7 with Sunday as 1, and names work; a zero is not a valid day.
let sunday = Utc.with_ymd_and_hms(2026, 9, 13, 3, 0, 0).unwrap();
assert_eq!(fires_at("0 0 3 * * SUN", monday), sunday);
assert_eq!(
fires_at("0 0 3 * * 1", monday),
sunday,
"1 is Sunday, not Monday"
);
assert_eq!(
fires_at("0 0 3 * * MON", monday),
Utc.with_ymd_and_hms(2026, 9, 7, 3, 0, 0).unwrap()
);
assert!(
validate_cron("0 0 3 * * 0").is_err(),
"a zero weekday is rejected"
);
assert_eq!(next_fire("0 0 3 * * 0", monday), None);
}