cleanup von abgeschlossenen jobs
This commit is contained in:
@@ -14,16 +14,18 @@ Zielumgebung: internes LAN, hinter zentralem Reverse‑Proxy (TLS‑Terminierung
|
||||
4. [Komponenten im Detail](#komponenten-im-detail)
|
||||
5. [Verzeichnisstruktur](#verzeichnisstruktur)
|
||||
6. [Datenfluss eines Serienbrief‑Jobs](#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. [Single‑Letter‑Preview](#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 Reverse‑Proxy (TLS‑Terminierung
|
||||
|
||||
- DOCX‑Vorlagen mit Jinja‑artigen Platzhaltern (`{{ feldname }}`) via **docxtpl**
|
||||
- CSV‑Upload als Empfängerliste (UTF‑8, Komma‑Trenner)
|
||||
- Asynchrone Verarbeitung über Celery — UI bleibt responsiv, Status‑Polling per HTMX
|
||||
- **Single‑Letter‑Preview** vor dem Komplett‑Render: rendert nur die erste CSV‑Zeile synchron als PDF und liefert es inline aus — kein Job, keine Persistenz, kein Queue‑Roundtrip
|
||||
- Asynchrone Verarbeitung des Komplett‑Jobs über Celery — UI bleibt responsiv, Status‑Polling per HTMX
|
||||
- DOCX → PDF Konversion mit headless **LibreOffice**
|
||||
- Zusammenführung aller Einzel‑PDFs via **pypdf**
|
||||
- Authentifizierung über Djangos Auth‑System, Brute‑Force‑Schutz mit **django‑axes**
|
||||
- Logout per POST‑Formular (Django 5 Pflicht)
|
||||
- Geschütztes Ausliefern generierter PDFs via **X‑Accel‑Redirect** (Nginx)
|
||||
- Audit‑Log pro Job (Status‑Wechsel, Fehler, abgearbeitete Zeilen)
|
||||
- Tägliches Backup (pg_dump + Media‑Tar, 14 Tage Retention)
|
||||
- **Test‑Suite** mit pytest‑django (Service‑Unit, View‑Layer, Integration mit echtem LibreOffice) — siehe [TEST.md](TEST.md)
|
||||
|
||||
---
|
||||
|
||||
@@ -96,6 +101,7 @@ Zwei Bridge‑Netze trennen Verantwortlichkeiten:
|
||||
| UI‑Interaktion | django‑htmx | aktuell |
|
||||
| DB‑Adapter | psycopg | 3.2.3 (binary) |
|
||||
| Python | CPython | 3.12 (slim‑bookworm) |
|
||||
| Tests | pytest, pytest‑django | 8.3 / 4.9 |
|
||||
|
||||
---
|
||||
|
||||
@@ -114,6 +120,7 @@ Zwei Bridge‑Netze trennen Verantwortlichkeiten:
|
||||
- Settings‑Split: `base.py`, `dev.py`, `production.py`
|
||||
- Entrypoint wartet auf DB‑Readiness, führt `migrate` und (in Prod) `collectstatic` aus
|
||||
- Healthcheck via `/healthz`
|
||||
- **Synchron**er Preview‑Endpoint (`/jobs/preview/`) rendert über LibreOffice direkt im `web`‑Container — kein Celery‑Roundtrip
|
||||
|
||||
### `worker` – Celery
|
||||
- Verarbeitet `process_mailmerge_job`‑Tasks
|
||||
@@ -122,8 +129,9 @@ Zwei Bridge‑Netze trennen Verantwortlichkeiten:
|
||||
|
||||
### `beat` – Celery Scheduler
|
||||
- Nutzt `DatabaseScheduler` von django‑celery‑beat
|
||||
- Geplant für: Retention‑Cleanup (alte Jobs/PDFs), Audit‑Log‑Rotation
|
||||
- *Hinweis: Konkrete Periodic Tasks aktuell noch nicht registriert (siehe Roadmap)*
|
||||
- Periodic Tasks werden über Django‑Migrationen registriert (reproduzierbar, single source of truth)
|
||||
- Aktiv:
|
||||
- **Retention‑Cleanup** (`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 Serienbrief‑Jobs
|
||||
|
||||
1. **Upload Vorlage:** `LetterTemplate` mit DOCX‑Datei wird angelegt (Admin oder UI)
|
||||
2. **Job‑Erstellung:** 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. **Job‑Erstellung:** 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, Ergebnis‑PDF 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 (UTF‑8, 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. **UI‑Polling:** HTMX fragt `/jobs/<id>/status/` alle ~2 s ab → Fortschrittsbalken
|
||||
6. **Download:** Nutzer klickt Download → View prüft Auth → Antwort enthält `X‑Accel‑Redirect: /protected/…` → Nginx streamt das PDF
|
||||
7. **UI‑Polling:** HTMX fragt `/jobs/<id>/status/` alle ~2 s ab → Fortschrittsbalken
|
||||
8. **Download:** Nutzer klickt Download → View prüft Auth → Antwort enthält `X‑Accel‑Redirect: /protected/…` → Nginx streamt das PDF
|
||||
|
||||
---
|
||||
|
||||
## Single‑Letter‑Preview
|
||||
|
||||
Vor dem Komplett‑Render kann eine Vorschau auf Basis der ersten CSV‑Zeile erstellt werden. Spart Zeit bei der Template‑Entwicklung und verhindert, dass fehlerhafte Templates 200 PDFs ungenutzt produzieren.
|
||||
|
||||
### Design‑Entscheidungen
|
||||
|
||||
| Aspekt | Wahl | Begründung |
|
||||
|---|---|---|
|
||||
| Ausführung | **synchron im `web`‑Container** | Renderzeit < 5 s, kein Celery‑Roundtrip nötig, kein State |
|
||||
| Persistenz | **keine** — kein `MailMergeJob`, keine Datei auf `media_files` | Preview ist ephemer, hinterlässt keine DSGVO‑relevanten Artefakte |
|
||||
| Render‑Pfad | tmpfs `/tmp` (size‑limitiert, kein Disk‑I/O) | LibreOffice‑User‑Profil und Zwischen‑DOCX werden mit Request‑Ende verworfen |
|
||||
| Validierung | strikt: fehlende Spalten → `PreviewError` | Im Komplett‑Job aktuell warn‑only — Preview‑Pfad gibt klare Fehlermeldung ins UI |
|
||||
| Auth | `@login_required`, POST‑only | gleiche Schutzklasse wie Komplett‑Job |
|
||||
| Response | `Content‑Type: application/pdf`, `Content‑Disposition: inline` | iframe im Form zeigt das PDF direkt |
|
||||
|
||||
### Response‑Header
|
||||
|
||||
Die Preview‑View setzt Diagnose‑Header, die das Front‑End auswertet:
|
||||
|
||||
- `X-Preview-Placeholders` — Komma‑Liste der im Template gefundenen Platzhalter
|
||||
- `X-Preview-Extra-Columns` — Komma‑Liste der CSV‑Spalten, die das Template nicht verwendet (UI zeigt Hinweis)
|
||||
|
||||
### Fehlerfälle
|
||||
|
||||
| Auslöser | Verhalten |
|
||||
|---|---|
|
||||
| Form ungültig (fehlende Datei, falsche Extension) | Formular wird mit Django‑Form‑Errors neu gerendert |
|
||||
| `PreviewError` (z.B. Spalte fehlt) | Formular mit `preview_error`‑Message neu gerendert |
|
||||
| LibreOffice‑Crash / Timeout | Formular mit generischer Fehlermeldung; Stacktrace im Server‑Log |
|
||||
|
||||
---
|
||||
|
||||
@@ -518,6 +568,8 @@ docker compose up -d
|
||||
# Debugging: F5 in VS Code → attach an debugpy:5678
|
||||
```
|
||||
|
||||
**Hinweis zum Compose‑Aufruf:** Niemals `docker compose -f docker-compose.yml up -d` verwenden — das ignoriert die Override‑Datei und führt zu Netzwerk‑Race‑Conditions 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 Test‑Suite liegt unter `app/mailmerge/tests/` und ist in drei Schichten organisiert:
|
||||
|
||||
| Layer | Datei | Was wird geprüft | LibreOffice |
|
||||
|---|---|---|---|
|
||||
| Service‑Unit | `test_preview_service.py` | CSV‑Parsing, Header‑Validierung, Result‑Aufbau, Fehlerpfade | gemockt |
|
||||
| View | `test_preview_view.py` | HTTP‑Layer: Auth, Method‑Restriction, Form‑Validation, Response‑Header, Fehler‑Rendering | gemockt |
|
||||
| Integration | `test_preview_integration.py` | End‑to‑End mit echtem `soffice --headless` | **real** (Marker `integration`) |
|
||||
|
||||
**Pytest‑Konfiguration** (`app/pyproject.toml`):
|
||||
- `--reuse-db` für schnelle Re‑Runs
|
||||
- Default‑Selektor `-m 'not integration'` — Integration‑Tests 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 Re‑Runs
|
||||
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 CI‑Integration: 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 Cloud‑Transfer
|
||||
- **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 — Cleanup‑Task noch zu implementieren (siehe Roadmap)
|
||||
- **Aufbewahrung:** Jobs + CSVs + Output‑PDFs werden nach `JOB_RETENTION_DAYS` (default 30) automatisch gelöscht. Der Retention‑Cleanup läuft als Celery‑Beat‑Periodic‑Task einmal täglich. Manuelle Ausführung/Dry‑Run: `docker compose exec web python manage.py cleanup_jobs --dry-run`
|
||||
- **Audit:** Jeder Job hat ein `JobLogEntry`‑Protokoll (wer, wann, was, Fehler)
|
||||
- **Backup‑Verschlüsselung:** Bind‑Mount `./backups` liegt aktuell unverschlüsselt — für produktiven Einsatz LUKS/Filesystem‑Encryption am Host **dringend empfohlen**
|
||||
- **DSFA‑Relevanz:** Bei Verarbeitung besonderer Datenkategorien oder umfangreicher Profilbildung ist eine Datenschutz‑Folgenabschä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
|
||||
### Tech‑Debt
|
||||
- Postgres‑Passwort doppelt gehalten (`DATABASE_URL` + Secret‑Datei) → refactor zu Single Source via Entrypoint, der `DATABASE_URL` aus `*_FILE`‑Env zusammenbaut
|
||||
- Keine LDAP/AD‑Anbindung — aktuell lokale Django‑User
|
||||
- Keine Periodic Tasks im Beat registriert (Retention‑Cleanup, Log‑Rotation)
|
||||
- CSV‑Header‑Validierung ist warn‑only, kein Abbruch bei fehlenden Pflichtfeldern
|
||||
- Keine Vorschau (Single‑Letter‑Preview) vor dem Komplett‑Render
|
||||
- CSV‑Header‑Validierung im **Haupt‑Job‑Flow** warn‑only (im Preview‑Pfad bereits strikt)
|
||||
- Image enthält LibreOffice (~400 MB) — könnte in separates Worker‑Image ausgelagert werden
|
||||
- Tests decken Preview + Retention ab — Haupt‑Job‑Flow (Celery‑Task `run_mailmerge`, Status‑Polling, X‑Accel‑Redirect) noch ohne automatisierte Tests
|
||||
|
||||
### Erledigt
|
||||
- [x] Single‑Letter‑Preview im Job‑Erstellungs‑Flow
|
||||
- [x] Test‑Suite mit pytest‑django (3 Layer)
|
||||
- [x] Logout via POST‑Form (Django 5)
|
||||
- [x] Retention‑Cleanup als periodic task (`mailmerge.cleanup_expired_jobs`, täglich)
|
||||
|
||||
### Roadmap (kurzfristig)
|
||||
- [ ] Single‑Letter‑Preview im Job‑Erstellungs‑Flow
|
||||
- [ ] Retention‑Cleanup als periodic task
|
||||
- [ ] CSV‑Validierung strict mit Pflichtfeldliste pro Template
|
||||
- [ ] CSV‑Validierung strict mit Pflichtfeldliste pro Template (auch im Haupt‑Flow)
|
||||
- [ ] Tests für Celery‑Task `run_mailmerge` (mit `CELERY_TASK_ALWAYS_EAGER`)
|
||||
- [ ] Mandanten‑/Berechtigungsmodell (Abteilung X sieht nur eigene Templates)
|
||||
- [ ] LDAP‑Auth über `django‑auth‑ldap` (AD‑Integration GB)
|
||||
- [ ] LDAP‑Auth über `django‑auth‑ldap`
|
||||
- [ ] Postgres‑Passwort als Single Source of Truth (`*_FILE`‑Env)
|
||||
|
||||
### Roadmap (mittelfristig)
|
||||
- [ ] AppArmor‑Profile pro Container
|
||||
|
||||
Reference in New Issue
Block a user