feat(signature): Signaturen beim Report-Upload behalten + Cron-Cleanup nach Frist

Bisher loeschte die Report-Pipeline die Unterschriften nach erfolgreichem
DOCUframe-Upload. Wir brauchen die Signatur-Dateien aber weiterhin, daher:

- ProcessDeliveryReportUseCase: Signatur-Loeschung (delete_for_delivery) aus dem
  Cleanup entfernt + SignatureStorage-Dependency raus (Report-PDF/Bild-Notiz-
  Cleanup bleibt).
- SignatureStorage: neue Methode delete_older_than(max_age) -> Anzahl; lokaler
  Adapter loescht PNGs aelter als die Frist (per mtime).
- Config [signature]: retention_days (Default 90, 0 = aus) + cleanup_cron
  (Default taeglich 04:00).
- main.rs: Signatur-Cleanup-Scheduler (gated retention_days > 0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dennis Nemec
2026-06-18 16:16:12 +02:00
parent 2f4368ec52
commit 47eb8ec57d
6 changed files with 140 additions and 12 deletions

View File

@ -46,6 +46,7 @@ use holzleitner_application::usecases::{
UpdateServiceUseCase, UploadDeliveryNoteImageUseCase,
};
use holzleitner_application::ports::DeliveryReportJobRepository;
use holzleitner_application::ports::SignatureStorage;
use holzleitner_application::ports::{SyncRunRepository, SyncTrigger};
use holzleitner_infrastructure::auth::{
KeycloakAdapterConfig, KeycloakAdminClient, KeycloakAdminConfig, KeycloakAuthService,
@ -271,7 +272,6 @@ pub(crate) async fn run_app(
gsd_service.clone(),
attachment_repository.clone(),
attachment_storage.clone(),
signature_storage.clone(),
report_sink.clone(),
));
@ -368,7 +368,7 @@ pub(crate) async fn run_app(
Arc::new(ApplyDeliveryActionUseCase::new(delivery_repository.clone()));
let complete_delivery = Arc::new(CompleteDeliveryUseCase::new(
delivery_completion_repository,
signature_storage,
signature_storage.clone(),
car_repository.clone(),
if cfg.erp.writeback_enabled {
Some(push_completion_to_erp.clone())
@ -631,6 +631,51 @@ pub(crate) async fn run_app(
None
};
// --- Signatur-Cleanup-Scheduler ------------------------------------
// Unterschriften bleiben nach dem Report-Upload bewusst erhalten (wir
// brauchen die Dateien) und werden hier periodisch gelöscht, sobald sie
// älter als die Aufbewahrungsfrist sind. retention_days = 0 ⇒ deaktiviert.
let _signature_cleanup_scheduler = if cfg.signature.retention_days > 0 {
let scheduler = JobScheduler::new()
.await
.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 signatures = signatures.clone();
Box::pin(async move {
match signatures.delete_older_than(max_age).await {
Ok(0) => {}
Ok(n) => tracing::info!(
deleted = n,
"signature_cleanup: abgelaufene Unterschriften gelöscht"
),
Err(e) => tracing::error!(error = %e, "signature_cleanup fehlgeschlagen"),
}
})
})
.context("Signatur-Cleanup-Job konnte nicht erstellt werden")?;
scheduler
.add(job)
.await
.context("Signatur-Cleanup-Job add fehlgeschlagen")?;
scheduler
.start()
.await
.context("Signatur-Cleanup-Scheduler-Start fehlgeschlagen")?;
tracing::info!(
cron = %cfg.signature.cleanup_cron,
retention_days = cfg.signature.retention_days,
"signature_cleanup scheduler gestartet"
);
Some(scheduler)
} else {
tracing::info!(
"signature_cleanup deaktiviert (signature.retention_days = 0) — Unterschriften werden nie automatisch gelöscht"
);
None
};
let addr: SocketAddr = format!("{}:{}", cfg.server.host, cfg.server.port)
.parse()
.with_context(|| format!("ungültige Adresse {}:{}", cfg.server.host, cfg.server.port))?;