cleanup von abgeschlossenen jobs

This commit is contained in:
2026-05-22 09:14:31 +02:00
parent d61dbd46fd
commit 848e48b0dd
7 changed files with 689 additions and 31 deletions
+120 -31
View File
@@ -14,16 +14,18 @@ Zielumgebung: internes LAN, hinter zentralem ReverseProxy (TLSTerminierung
4. [Komponenten im Detail](#komponenten-im-detail)
5. [Verzeichnisstruktur](#verzeichnisstruktur)
6. [Datenfluss eines SerienbriefJobs](#datenfluss-eines-serienbriefjobs)
7. [Datenmodell](#datenmodell)
8. [Sicherheitskonzept](#sicherheitskonzept)
9. [Netzwerk & Ports](#netzwerk--ports)
10. [Volumes & Persistenz](#volumes--persistenz)
11. [Konfiguration](#konfiguration)
12. [Inbetriebnahme](#inbetriebnahme)
13. [Entwicklung in VS Code](#entwicklung-in-vs-code)
14. [Betrieb & Wartung](#betrieb--wartung)
15. [Datenschutz & Compliance](#datenschutz--compliance)
16. [Bekannte Einschränkungen & Roadmap](#bekannte-einschränkungen--roadmap)
7. [SingleLetterPreview](#singleletterpreview)
8. [Datenmodell](#datenmodell)
9. [Sicherheitskonzept](#sicherheitskonzept)
10. [Netzwerk & Ports](#netzwerk--ports)
11. [Volumes & Persistenz](#volumes--persistenz)
12. [Konfiguration](#konfiguration)
13. [Inbetriebnahme](#inbetriebnahme)
14. [Entwicklung in VS Code](#entwicklung-in-vs-code)
15. [Tests](#tests)
16. [Betrieb & Wartung](#betrieb--wartung)
17. [Datenschutz & Compliance](#datenschutz--compliance)
18. [Bekannte Einschränkungen & Roadmap](#bekannte-einschränkungen--roadmap)
---
@@ -31,13 +33,16 @@ Zielumgebung: internes LAN, hinter zentralem ReverseProxy (TLSTerminierung
- DOCXVorlagen mit Jinjaartigen Platzhaltern (`{{ feldname }}`) via **docxtpl**
- CSVUpload als Empfängerliste (UTF8, KommaTrenner)
- Asynchrone Verarbeitung über Celery — UI bleibt responsiv, StatusPolling per HTMX
- **SingleLetterPreview** vor dem KomplettRender: rendert nur die erste CSVZeile synchron als PDF und liefert es inline aus — kein Job, keine Persistenz, kein QueueRoundtrip
- Asynchrone Verarbeitung des KomplettJobs über Celery — UI bleibt responsiv, StatusPolling per HTMX
- DOCX → PDF Konversion mit headless **LibreOffice**
- Zusammenführung aller EinzelPDFs via **pypdf**
- Authentifizierung über Djangos AuthSystem, BruteForceSchutz mit **djangoaxes**
- Logout per POSTFormular (Django 5 Pflicht)
- Geschütztes Ausliefern generierter PDFs via **XAccelRedirect** (Nginx)
- AuditLog pro Job (StatusWechsel, Fehler, abgearbeitete Zeilen)
- Tägliches Backup (pg_dump + MediaTar, 14 Tage Retention)
- **TestSuite** mit pytestdjango (ServiceUnit, ViewLayer, Integration mit echtem LibreOffice) — siehe [TEST.md](TEST.md)
---
@@ -96,6 +101,7 @@ Zwei BridgeNetze trennen Verantwortlichkeiten:
| UIInteraktion | djangohtmx | aktuell |
| DBAdapter | psycopg | 3.2.3 (binary) |
| Python | CPython | 3.12 (slimbookworm) |
| Tests | pytest, pytestdjango | 8.3 / 4.9 |
---
@@ -114,6 +120,7 @@ Zwei BridgeNetze trennen Verantwortlichkeiten:
- SettingsSplit: `base.py`, `dev.py`, `production.py`
- Entrypoint wartet auf DBReadiness, führt `migrate` und (in Prod) `collectstatic` aus
- Healthcheck via `/healthz`
- **Synchron**er PreviewEndpoint (`/jobs/preview/`) rendert über LibreOffice direkt im `web`Container — kein CeleryRoundtrip
### `worker` Celery
- Verarbeitet `process_mailmerge_job`Tasks
@@ -122,8 +129,9 @@ Zwei BridgeNetze trennen Verantwortlichkeiten:
### `beat` Celery Scheduler
- Nutzt `DatabaseScheduler` von djangocelerybeat
- Geplant für: RetentionCleanup (alte Jobs/PDFs), AuditLogRotation
- *Hinweis: Konkrete Periodic Tasks aktuell noch nicht registriert (siehe Roadmap)*
- Periodic Tasks werden über DjangoMigrationen registriert (reproduzierbar, single source of truth)
- Aktiv:
- **RetentionCleanup** (`mailmerge.cleanup_expired_jobs`) — täglich 03:15 (`CELERY_TIMEZONE`)
### `db` PostgreSQL 16
- Eigenes Volume `postgres_data`
@@ -165,20 +173,28 @@ serienbrief/
│ │ └── production.py
│ ├── mailmerge/ # Hauptapp
│ │ ├── models.py # LetterTemplate, MailMergeJob, JobLogEntry
│ │ ├── views.py
│ │ ├── views.py # incl. job_preview (synchron)
│ │ ├── admin.py
│ │ ├── forms.py
│ │ ├── urls.py
│ │ ├── urls.py # incl. "jobs/preview/"
│ │ ├── tasks.py # Celery tasks
│ │ ├── services/
│ │ │ ├── docx_renderer.py # docxtpl + LibreOffice
│ │ │ ── pdf_merge.py # pypdf
│ │ │ ── pdf_merge.py # pypdf
│ │ │ └── preview.py # Single-Letter-Preview (synchron)
│ │ ├── tests/ # pytest-django Tests
│ │ │ ├── conftest.py
│ │ │ ├── test_preview_service.py
│ │ │ ├── test_preview_view.py
│ │ │ └── test_preview_integration.py # marker: integration
│ │ └── management/commands/
│ │ └── wait_for_db.py
│ ├── pyproject.toml # pytest config (markers, addopts)
│ └── templates/
│ ├── base.html
│ ├── base.html # Logout via POST-Form (Django 5)
│ ├── registration/login.html
│ └── mailmerge/
│ └── mailmerge/
│ └── job_form.html # Form + Preview-Button + iframe
├── nginx/
│ ├── nginx.conf
│ └── conf.d/serienbrief.conf
@@ -200,16 +216,50 @@ serienbrief/
## Datenfluss eines SerienbriefJobs
1. **Upload Vorlage:** `LetterTemplate` mit DOCXDatei wird angelegt (Admin oder UI)
2. **JobErstellung:** User wählt Vorlage + lädt CSV hoch `MailMergeJob(status=queued)`
3. **Enqueue:** View löst `process_mailmerge_job.delay(job.pk)` aus
4. **Worker holt Task:**
2. **JobErstellung:** User wählt Vorlage + lädt CSV hoch im `job_form.html`
3. **Optional: Preview** — Button »Vorschau (erste Zeile)« löst synchronen POST an `/jobs/preview/` aus, ErgebnisPDF erscheint inline im iframe (siehe nächster Abschnitt)
4. **Submit:** Klick auf »Job starten« → `MailMergeJob(status=queued)` angelegt
5. **Enqueue:** View löst `process_mailmerge_job.delay(job.pk)` aus
6. **Worker holt Task:**
- Liest CSV (UTF8, validiert Header gegen Platzhalter)
- Iteriert Zeilen → docxtpl rendert pro Zeile eine temporäre DOCX in `/tmp`
- LibreOffice (`soffice --headless --convert-to pdf …`) erzeugt pro DOCX ein PDF
- pypdf merged alle PDFs in eine Ausgabedatei unter `media_files`
- `JobLogEntry`s pro Schritt; Status `running → done` bzw. `failed`
5. **UIPolling:** HTMX fragt `/jobs/<id>/status/` alle ~2 s ab → Fortschrittsbalken
6. **Download:** Nutzer klickt Download → View prüft Auth → Antwort enthält `XAccelRedirect: /protected/…` → Nginx streamt das PDF
7. **UIPolling:** HTMX fragt `/jobs/<id>/status/` alle ~2 s ab → Fortschrittsbalken
8. **Download:** Nutzer klickt Download → View prüft Auth → Antwort enthält `XAccelRedirect: /protected/…` → Nginx streamt das PDF
---
## SingleLetterPreview
Vor dem KomplettRender kann eine Vorschau auf Basis der ersten CSVZeile erstellt werden. Spart Zeit bei der TemplateEntwicklung und verhindert, dass fehlerhafte Templates 200 PDFs ungenutzt produzieren.
### DesignEntscheidungen
| Aspekt | Wahl | Begründung |
|---|---|---|
| Ausführung | **synchron im `web`Container** | Renderzeit < 5 s, kein CeleryRoundtrip nötig, kein State |
| Persistenz | **keine** — kein `MailMergeJob`, keine Datei auf `media_files` | Preview ist ephemer, hinterlässt keine DSGVOrelevanten Artefakte |
| RenderPfad | tmpfs `/tmp` (sizelimitiert, kein DiskI/O) | LibreOfficeUserProfil und ZwischenDOCX werden mit RequestEnde verworfen |
| Validierung | strikt: fehlende Spalten → `PreviewError` | Im KomplettJob aktuell warnonly — PreviewPfad gibt klare Fehlermeldung ins UI |
| Auth | `@login_required`, POSTonly | gleiche Schutzklasse wie KomplettJob |
| Response | `ContentType: application/pdf`, `ContentDisposition: inline` | iframe im Form zeigt das PDF direkt |
### ResponseHeader
Die PreviewView setzt DiagnoseHeader, die das FrontEnd auswertet:
- `X-Preview-Placeholders` — KommaListe der im Template gefundenen Platzhalter
- `X-Preview-Extra-Columns` — KommaListe der CSVSpalten, die das Template nicht verwendet (UI zeigt Hinweis)
### Fehlerfälle
| Auslöser | Verhalten |
|---|---|
| Form ungültig (fehlende Datei, falsche Extension) | Formular wird mit DjangoFormErrors neu gerendert |
| `PreviewError` (z.B. Spalte fehlt) | Formular mit `preview_error`Message neu gerendert |
| LibreOfficeCrash / Timeout | Formular mit generischer Fehlermeldung; Stacktrace im ServerLog |
---
@@ -518,6 +568,8 @@ docker compose up -d
# Debugging: F5 in VS Code → attach an debugpy:5678
```
**Hinweis zum ComposeAufruf:** Niemals `docker compose -f docker-compose.yml up -d` verwenden — das ignoriert die OverrideDatei und führt zu NetzwerkRaceConditions sowie zur falschen UID im Container. Compose merged `docker-compose.yml` + `docker-compose.override.yml` automatisch, wenn keine `-f`Flags gesetzt sind.
### Wichtige Unterschiede Dev ↔ Prod
| Aspekt | Dev | Prod |
@@ -532,6 +584,38 @@ docker compose up -d
---
## Tests
Die TestSuite liegt unter `app/mailmerge/tests/` und ist in drei Schichten organisiert:
| Layer | Datei | Was wird geprüft | LibreOffice |
|---|---|---|---|
| ServiceUnit | `test_preview_service.py` | CSVParsing, HeaderValidierung, ResultAufbau, Fehlerpfade | gemockt |
| View | `test_preview_view.py` | HTTPLayer: Auth, MethodRestriction, FormValidation, ResponseHeader, FehlerRendering | gemockt |
| Integration | `test_preview_integration.py` | EndtoEnd mit echtem `soffice --headless` | **real** (Marker `integration`) |
**PytestKonfiguration** (`app/pyproject.toml`):
- `--reuse-db` für schnelle ReRuns
- DefaultSelektor `-m 'not integration'` — IntegrationTests laufen nur auf Anforderung
- `python_files = ["test_*.py", "*_test.py", "tests.py"]`
**Schnellstart:**
```bash
# Erster Lauf (DB anlegen)
docker compose exec web pytest mailmerge/tests/ -v --create-db
# Normale ReRuns
docker compose exec web pytest mailmerge/tests/ -v
# Inkl. Integration (langsamer, benötigt LibreOffice im Image)
docker compose exec web pytest mailmerge/tests/ -m integration -v
```
Detaillierte Anleitung inkl. Coverage, Fixtures, Debugging und CIIntegration: siehe **[TEST.md](TEST.md)**.
---
## Betrieb & Wartung
### Häufige Operationen
@@ -596,7 +680,7 @@ docker compose exec -T db pg_restore -U serienbrief -d serienbrief --clean < bac
- **Rechtsgrundlage:** Verarbeitung personenbezogener Adress/Empfängerdaten je nach Anwendungsfall Art. 6 Abs. 1 lit. b/c/e DSGVO; bei besonderen Datenkategorien (Art. 9 DSGVO) gesonderte Rechtsgrundlage erforderlich
- **Speicherort:** Innerbetrieblicher Server, kein CloudTransfer
- **Zugriffssteuerung:** Nur authentifizierte User; Trennung Templates/Jobs aktuell flach — Mandanten/Rollenmodell ggf. ergänzen (siehe Roadmap)
- **Aufbewahrung:** Jobs + CSVs werden nach `JOB_RETENTION_DAYS` (default 30) gelöscht — CleanupTask noch zu implementieren (siehe Roadmap)
- **Aufbewahrung:** Jobs + CSVs + OutputPDFs werden nach `JOB_RETENTION_DAYS` (default 30) automatisch gelöscht. Der RetentionCleanup läuft als CeleryBeatPeriodicTask einmal täglich. Manuelle Ausführung/DryRun: `docker compose exec web python manage.py cleanup_jobs --dry-run`
- **Audit:** Jeder Job hat ein `JobLogEntry`Protokoll (wer, wann, was, Fehler)
- **BackupVerschlüsselung:** BindMount `./backups` liegt aktuell unverschlüsselt — für produktiven Einsatz LUKS/FilesystemEncryption am Host **dringend empfohlen**
- **DSFARelevanz:** Bei Verarbeitung besonderer Datenkategorien oder umfangreicher Profilbildung ist eine DatenschutzFolgenabschätzung gemäß Art. 35 DSGVO durchzuführen, bevor produktiver Echtbetrieb startet
@@ -608,17 +692,22 @@ docker compose exec -T db pg_restore -U serienbrief -d serienbrief --clean < bac
### TechDebt
- PostgresPasswort doppelt gehalten (`DATABASE_URL` + SecretDatei) → refactor zu Single Source via Entrypoint, der `DATABASE_URL` aus `*_FILE`Env zusammenbaut
- Keine LDAP/ADAnbindung — aktuell lokale DjangoUser
- Keine Periodic Tasks im Beat registriert (RetentionCleanup, LogRotation)
- CSVHeaderValidierung ist warnonly, kein Abbruch bei fehlenden Pflichtfeldern
- Keine Vorschau (SingleLetterPreview) vor dem KomplettRender
- CSVHeaderValidierung im **HauptJobFlow** warnonly (im PreviewPfad bereits strikt)
- Image enthält LibreOffice (~400 MB) — könnte in separates WorkerImage ausgelagert werden
- Tests decken Preview + Retention ab — HauptJobFlow (CeleryTask `run_mailmerge`, StatusPolling, XAccelRedirect) noch ohne automatisierte Tests
### Erledigt
- [x] SingleLetterPreview im JobErstellungsFlow
- [x] TestSuite mit pytestdjango (3 Layer)
- [x] Logout via POSTForm (Django 5)
- [x] RetentionCleanup als periodic task (`mailmerge.cleanup_expired_jobs`, täglich)
### Roadmap (kurzfristig)
- [ ] SingleLetterPreview im JobErstellungsFlow
- [ ] RetentionCleanup als periodic task
- [ ] CSVValidierung strict mit Pflichtfeldliste pro Template
- [ ] CSVValidierung strict mit Pflichtfeldliste pro Template (auch im HauptFlow)
- [ ] Tests für CeleryTask `run_mailmerge` (mit `CELERY_TASK_ALWAYS_EAGER`)
- [ ] Mandanten/Berechtigungsmodell (Abteilung X sieht nur eigene Templates)
- [ ] LDAPAuth über `djangoauthldap` (ADIntegration GB)
- [ ] LDAPAuth über `djangoauthldap`
- [ ] PostgresPasswort als Single Source of Truth (`*_FILE`Env)
### Roadmap (mittelfristig)
- [ ] AppArmorProfile pro Container