From ed2785929305c55a9339067ba9448da9984eb6e6 Mon Sep 17 00:00:00 2001 From: Dennis Nemec Date: Wed, 16 Sep 2026 18:24:17 +0300 Subject: [PATCH] fix(cron): Cron-Ausdruecke in server.timezone auswerten statt UTC Job::new_async interpretiert den Cron-Ausdruck in UTC - '0 0 17 * * *' feuerte dadurch erst um 19:00 deutscher Zeit (CEST), der 17-Uhr-Import fuer 'morgen' blieb aus. Alle drei Scheduler (ERP-Import, Report-Retry, Signatur-Cleanup) nutzen jetzt Job::new_async_tz mit der konfigurierten Fach-Zeitzone, sodass die Uhrzeit in der Config als Ortszeit gilt. Hinweis: tokio-cron-scheduler ermittelt den UTC-Offset einmalig beim Anlegen des Jobs (Serverstart); ein DST-Wechsel greift erst nach Neustart. Co-Authored-By: Claude Fable 5.1 --- crates/api/src/config.rs | 2 ++ crates/api/src/main.rs | 15 ++++++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/crates/api/src/config.rs b/crates/api/src/config.rs index f8ee9a6..327bbd5 100644 --- a/crates/api/src/config.rs +++ b/crates/api/src/config.rs @@ -383,6 +383,8 @@ pub struct ServerConfig { /// Zeitstempel werden durchgängig als UTC gespeichert (`TIMESTAMPTZ`, /// `DateTime`). Diese Zone gilt für alles, was ein **Kalenderdatum** /// oder eine **Wanduhrzeit** braucht: „heute" für Touren/Import-Cron, + /// die **Auslösezeiten aller Cron-Jobs** (Import, Report-Retry, + /// Signatur-Cleanup — `"0 0 17 * * *"` = 17:00 in dieser Zone, nicht UTC), /// Datumsangaben im Lieferbericht und in der Belegansicht sowie die /// ERP-Rückschreibung (`delivered_at`). Bewusst konfiguriert statt der /// OS-Zeitzone des Servers (`chrono::Local`), damit das Verhalten nicht diff --git a/crates/api/src/main.rs b/crates/api/src/main.rs index bcca54f..d7ddad5 100644 --- a/crates/api/src/main.rs +++ b/crates/api/src/main.rs @@ -561,7 +561,10 @@ pub(crate) async fn run_app( let import = import_erp_tours.clone(); let offset = cfg.import.date_offset_days; let tz = cfg.server.timezone; - let job = Job::new_async(cfg.import.cron.as_str(), move |_uuid, _lock| { + // `new_async_tz`: Cron-Ausdruck in der konfigurierten Zone auswerten. + // `new_async` würde ihn in UTC interpretieren — "0 0 17 * * *" feuerte + // dann erst um 19:00 deutscher Zeit. + let job = Job::new_async_tz(cfg.import.cron.as_str(), tz, move |_uuid, _lock| { let import = import.clone(); Box::pin(async move { // Kalendertag in der konfigurierten Zone (nicht OS-Local). @@ -648,7 +651,10 @@ pub(crate) async fn run_app( .context("Report-JobScheduler konnte nicht erstellt werden")?; let process = process_delivery_report.clone(); let jobs = report_job_repository.clone(); - let job = Job::new_async(cfg.report.retry_cron.as_str(), move |_uuid, _lock| { + let job = Job::new_async_tz( + cfg.report.retry_cron.as_str(), + cfg.server.timezone, + move |_uuid, _lock| { let process = process.clone(); let jobs = jobs.clone(); Box::pin(async move { @@ -687,7 +693,10 @@ pub(crate) async fn run_app( .context("Signatur-Cleanup-JobScheduler konnte nicht erstellt werden")?; let signatures = signature_storage.clone(); let max_age = Duration::from_secs(cfg.signature.retention_days as u64 * 86_400); - let job = Job::new_async(cfg.signature.cleanup_cron.as_str(), move |_uuid, _lock| { + let job = Job::new_async_tz( + cfg.signature.cleanup_cron.as_str(), + cfg.server.timezone, + move |_uuid, _lock| { let signatures = signatures.clone(); Box::pin(async move { match signatures.delete_older_than(max_age).await {