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 <noreply@anthropic.com>
This commit is contained in:
Dennis Nemec
2026-09-16 18:24:17 +03:00
parent f74b6c608a
commit ed27859293
2 changed files with 14 additions and 3 deletions

View File

@ -383,6 +383,8 @@ pub struct ServerConfig {
/// Zeitstempel werden durchgängig als UTC gespeichert (`TIMESTAMPTZ`,
/// `DateTime<Utc>`). 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

View File

@ -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 {