Files
serienbrief_django/README.md
T

637 lines
24 KiB
Markdown
Raw Normal View History

2026-05-21 10:36:16 +02:00
# Serienbrief DOCXMailmerge mit Django, Celery & LibreOffice
WebAnwendung zur Erstellung personalisierter Serienbriefe aus DOCXVorlagen und CSVEmpfängerlisten. Ergebnis ist ein zusammengeführtes PDF mit einem Brief pro Empfänger:in.
Zielumgebung: internes LAN, hinter zentralem ReverseProxy (TLSTerminierung extern). Produktionsnah konzipiert: Hardening, ResourceLimits, Healthchecks, BackupJob.
---
## Inhaltsverzeichnis
1. [Funktionsumfang](#funktionsumfang)
2. [Architekturübersicht](#architekturübersicht)
3. [TechStack](#techstack)
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)
---
## Funktionsumfang
- DOCXVorlagen mit Jinjaartigen Platzhaltern (`{{ feldname }}`) via **docxtpl**
- CSVUpload als Empfängerliste (UTF8, KommaTrenner)
- Asynchrone Verarbeitung ü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**
- Geschütztes Ausliefern generierter PDFs via **XAccelRedirect** (Nginx)
- AuditLog pro Job (StatusWechsel, Fehler, abgearbeitete Zeilen)
- Tägliches Backup (pg_dump + MediaTar, 14 Tage Retention)
---
## Architekturübersicht
```
┌──────────────────────────────────────────────┐
│ Externer LANProxy (Nginx, TLSTerminierung)│
│ https://serienbrief.lan │
└────────────────────┬─────────────────────────┘
│ HTTP (intern)
┌───────────────────────────────────────────────────────────┐
│ ComposeStack »serienbrief« │
│ │
│ ┌───────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ nginx │──▶│ web (Django) │──▶│ db (Postgres 16) │ │
│ │ AppProxy │ │ Gunicorn │ │ │ │
│ └─────┬─────┘ └──────┬───────┘ └──────────────────┘ │
│ │ XAccel │ enqueue │
│ │ static/media ▼ │
│ │ ┌──────────────┐ ┌──────────────────┐ │
│ └────────▶│ redis (Broker)│◀─│ worker (Celery) │ │
│ └──────────────┘ │ LibreOffice+pypdf│ │
│ └──────────────────┘ │
│ ┌──────────────┐ ┌──────────────────┐ │
│ │ beat (Cron) │ │ backup (pg_dump) │ │
│ └──────────────┘ └──────────────────┘ │
└───────────────────────────────────────────────────────────┘
```
Zwei BridgeNetze trennen Verantwortlichkeiten:
- **frontend** nur `nginx``web`. Einziges Netz mit PortBind nach außen.
- **backend** `web`, `worker`, `beat`, `db`, `redis`, `backup`. Kein direktes Internet/LANExposing.
---
## TechStack
| Schicht | Komponente | Version |
|---|---|---|
| HostOS | Ubuntu Server LTS | 22.04 / 24.04 |
| Container Runtime | Docker Engine | 27.x+ |
| Orchestrierung | Docker Compose v2 | — |
| AppFramework | Django | 5.1.4 |
| WSGI (Prod) | Gunicorn | — |
| Task Queue | Celery | 5.4 |
| Broker / Result Backend | Redis | 7alpine |
| Datenbank | PostgreSQL | 16alpine |
| DOCXTemplating | docxtpl | 0.19 |
| DOCX→PDF | LibreOffice (headless) | bundled in Image |
| PDFMerge | pypdf | 5.1 |
| AppProxy | Nginx | 1.27alpine |
| AuthHardening | djangoaxes | aktuell |
| UIInteraktion | djangohtmx | aktuell |
| DBAdapter | psycopg | 3.2.3 (binary) |
| Python | CPython | 3.12 (slimbookworm) |
---
## Komponenten im Detail
### `nginx` Appinterner ReverseProxy
- Terminiert **kein** TLS (übernimmt der externe Proxy)
- Liefert Static und MediaDateien direkt aus den Volumes aus
- Setzt `XAccelRedirect` für Authgeschützte PDFs um: Django prüft die Berechtigung, Nginx streamt die Datei ohne Python im Pfad
- Hört auf `${APP_BIND_IP:-127.0.0.1}:${APP_BIND_PORT:-8080}` (Default Loopback)
- Healthcheck gegen `/healthz`
### `web` Django + Gunicorn
- Rolle `web` (per `ROLE`Env unterschieden, gleiches Image wie worker/beat)
- Stateless, readonly RootFS, `/tmp` als tmpfs
- SettingsSplit: `base.py`, `dev.py`, `production.py`
- Entrypoint wartet auf DBReadiness, führt `migrate` und (in Prod) `collectstatic` aus
- Healthcheck via `/healthz`
### `worker` Celery
- Verarbeitet `process_mailmerge_job`Tasks
- LibreOfficeUserProfil im tmpfs (`/tmp/lo_profile_<task_id>`) → keine Leaks zwischen Jobs
- `--concurrency=2`, `--max-tasks-per-child=50` (RAMLeakSchutz bei LibreOffice)
### `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)*
### `db` PostgreSQL 16
- Eigenes Volume `postgres_data`
- Passwort via DockerSecret (`/run/secrets/postgres_password`)
- InitSkripte unter `./postgres/init` (z.B. zusätzliche Extensions)
- `shm_size: 256mb` für komplexere Queries
### `redis` Broker & Result Backend
- Passwortgeschützt (`REDIS_PASSWORD` aus `.env`)
- AOF persistence + RDB snapshot (`save 900 1`)
- `maxmemory 256mb` mit `allkeyslru`
### `backup` nightly pg_dump + media tar
- CustomLoop mit `sleep 86400` (kein systemd nötig)
- pg_dump im CustomFormat (`-Fc`) → `pg_restore`kompatibel
- MediaTar aus `media_files`Volume (readonly Mount)
- Retention 14 Tage (konfigurierbar)
- Schreibt nach `./backups` auf dem Host
---
## Verzeichnisstruktur
```
serienbrief/
├── app/ # DjangoProjekt
│ ├── Dockerfile # MultiStage: builder → runtimebase → runtime / dev
│ ├── entrypoint.sh # wait_for_db → migrate → collectstatic → exec CMD
│ ├── manage.py
│ ├── requirements.txt
│ ├── requirements-dev.txt # debugpy, ipython, ruff, pytest, debug-toolbar
│ ├── config/ # Project package
│ │ ├── celery.py
│ │ ├── urls.py
│ │ ├── wsgi.py
│ │ └── settings/
│ │ ├── base.py
│ │ ├── dev.py
│ │ └── production.py
│ ├── mailmerge/ # Hauptapp
│ │ ├── models.py # LetterTemplate, MailMergeJob, JobLogEntry
│ │ ├── views.py
│ │ ├── admin.py
│ │ ├── forms.py
│ │ ├── urls.py
│ │ ├── tasks.py # Celery tasks
│ │ ├── services/
│ │ │ ├── docx_renderer.py # docxtpl + LibreOffice
│ │ │ └── pdf_merge.py # pypdf
│ │ └── management/commands/
│ │ └── wait_for_db.py
│ └── templates/
│ ├── base.html
│ ├── registration/login.html
│ └── mailmerge/…
├── nginx/
│ ├── nginx.conf
│ └── conf.d/serienbrief.conf
├── postgres/init/ # SQL init scripts
├── secrets/
│ └── postgres_password.txt # 0600, NICHT in Git
├── backups/ # nightly dumps + media tars
├── docker-compose.yml # Produktion
├── docker-compose.override.yml # Dev (automatisch gemerged)
├── .env # NICHT in Git
├── .env.example
├── .devcontainer/ # VS Code Dev Container
├── .vscode/ # launch / tasks / settings / extensions
└── README.md
```
---
## 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:**
- 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
---
## Datenmodell
```text
LetterTemplate
├── id, name (unique), description
├── docx_file (FileField → media/templates/)
├── created_by (FK User), created_at, updated_at
└── is_active
MailMergeJob
├── id, template (FK LetterTemplate)
├── csv_file (FileField → media/jobs/<id>/input.csv)
├── output_pdf (FileField → media/jobs/<id>/output.pdf)
├── status: queued | running | done | failed
├── total_rows, processed_rows
├── created_by, created_at, started_at, finished_at
└── error_message
JobLogEntry
├── id, job (FK MailMergeJob, related_name="log_entries")
├── level: info | warning | error
├── message, created_at
```
---
## Sicherheitskonzept
Im Sinne der SpaceVorgabe »SecuritybyDefault«:
### ContainerHardening
- `no-new-privileges: true` bei allen Services
- `read_only: true` für `web`, `worker`, `beat` (writable nur via tmpfs `/tmp`)
- NonrootUser `app` (UID/GID 1000, anpassbar per BuildArgs)
- Minimales Image (`python:3.12-slim-bookworm`), MultiStageBuild trennt Build und LaufzeitDependencies
- RessourcenLimits (memory + cpus) auf allen Services
- `tmpfs`Größen explizit limitiert (`size=256M`, etc.)
### Netzwerk
- Zwei isolierte Bridges (`frontend`, `backend`)
- Nur `nginx` bindet nach außen, default auf **Loopback**
- DB und Redis ausschließlich auf `backend`
- Externer Proxy übernimmt TLS, HSTS, ggf. IPAllowlist
### Secrets
- PostgresPasswort via DockerSecret (`./secrets/postgres_password.txt`, Mode 0600)
- RedisPasswort + DjangoSecretKey in `.env` (nicht in Git)
- `.env` und `secrets/` sind in `.gitignore` ausgenommen
### AnwendungsSicherheit
- djangoaxes: Lockout nach n Fehlversuchen
- CSRF aktiv, Trusted Origins explizit gepflegt
- `SECURE_PROXY_SSL_HEADER` gesetzt, damit Django HTTPS hinter dem externen Proxy korrekt erkennt
- `USE_X_FORWARDED_HOST=True`
- XAccelRedirect statt direktem FileDownload → Nginx liefert nur, was Django freigibt
- SessionCookies `HttpOnly`, `SameSite=Lax`, `Secure` (in Prod)
### DefenseinDepth (Empfehlungen)
- AppArmorProfile pro Container (z.B. via `--security-opt apparmor=...`) aktuell nicht aktiv
- seccompProfile (Default reicht, custom optional)
- ImageScanning (`trivy`, `grype`) in CI integrieren
- Renovate/Dependabot für BaseImage und PythonDeps
---
## Netzwerk & Ports
| Port (Host) | Service | Sichtbarkeit | Zweck |
|---|---|---|---|
| 8080 | nginx | `${APP_BIND_IP}` | HTTP für externen Proxy |
| 8000 | web | nur Dev | Django runserver |
| 5678 | web (Dev) | nur Dev | debugpy |
| 5432 | db | nur Dev | Postgres (Tools wie pgAdmin) |
| 6379 | redis | nur Dev | RedisCLI |
In Produktion bleiben **nur** Port 8080 (intern an externen Proxy) erreichbar.
---
## Volumes & Persistenz
| Volume | Inhalt | Backup? |
|---|---|---|
| `postgres_data` | PostgresDaten | pg_dump nightly |
| `redis_data` | AOF + RDB | nein (BrokerState, regenerierbar) |
| `static_files` | Generierte StaticFiles | nein (regenerierbar via `collectstatic`) |
| `media_files` | Hochgeladene Templates, CSVs, generierte PDFs | tar nightly |
| `nginx_cache` | Nginx Cache | nein |
| `nginx_run` | Nginx PID/Socket | nein |
BindMount im Dev: `./app:/app` für LiveReload.
---
## Konfiguration
`.env` (aus `.env.example` abgeleitet) — Auszug:
```env
# App
APP_VERSION=1.0.0
APP_BIND_IP=127.0.0.1
APP_BIND_PORT=8080
APP_UID=1000
APP_GID=1000
# Django
DJANGO_SECRET_KEY=<openssl rand -base64 64>
DJANGO_DEBUG=False
DJANGO_ALLOWED_HOSTS=serienbrief.lan,127.0.0.1,localhost
CSRF_TRUSTED_ORIGINS=https://serienbrief.lan
SECURE_PROXY_SSL_HEADER=HTTP_X_FORWARDED_PROTO,https
USE_X_FORWARDED_HOST=True
# Postgres
POSTGRES_DB=serienbrief
POSTGRES_USER=serienbrief
DATABASE_URL=postgres://serienbrief:<HEX-PWD>@db:5432/serienbrief
# Redis
REDIS_PASSWORD=<openssl rand -hex 32>
CELERY_BROKER_URL=redis://:<REDIS_PASSWORD>@redis:6379/0
CELERY_RESULT_BACKEND=redis://:<REDIS_PASSWORD>@redis:6379/1
# Business
JOB_RETENTION_DAYS=30
```
**Wichtig:**
- `secrets/postgres_password.txt` muss exakt denselben Wert enthalten wie in `DATABASE_URL`
- Datei darf **kein** abschließendes Newline enthalten — Generierung: `openssl rand -hex 32 | tr -d '\n' > secrets/postgres_password.txt && chmod 600 secrets/postgres_password.txt`
- Hex statt Base64, weil Base64`=` URLParsing in `DATABASE_URL` bricht
---
## Inbetriebnahme
### Voraussetzungen
- Ubuntu 22.04/24.04 LTS, aktueller PatchStand
- Docker Engine 27.x+ und Compose v2 (aptPakete `docker-ce` + `docker-compose-plugin`, nicht Snap)
- HostUser in `docker`Gruppe (`sudo usermod -aG docker $USER`, dann ReLogin)
### Erststart
```bash
# 1. Repo holen / entpacken in z.B. ~/projekte/serienbrief
cd ~/projekte/serienbrief
# 2. Konfiguration aus Template ableiten
cp .env.example .env
$EDITOR .env # Werte ausfüllen, siehe oben
# 3. PostgresPasswort als Datei
openssl rand -hex 32 | tr -d '\n' > secrets/postgres_password.txt
chmod 600 secrets/postgres_password.txt
# → diesen Wert auch in DATABASE_URL eintragen
# 4. Image bauen
docker compose build
# 5. Hochfahren
docker compose up -d
# 6. Status & Logs
docker compose ps -a
docker compose logs -f web
# 7. Superuser anlegen
docker compose exec web python manage.py createsuperuser
```
2026-05-21 17:54:38 +02:00
### Externen Reverse-Proxy konfigurieren
Der Stack liefert intern HTTP auf `${APP_BIND_IP}:${APP_BIND_PORT}` aus (Default `127.0.0.1:8080`). Der externe Proxy übernimmt TLS-Terminierung und die nach außen sichtbare Domain.
#### Voraussetzungen für externen Zugriff
Wenn der Proxy auf einem **anderen Host** läuft (typisch bei Nginx Proxy Manager als eigener Container/VM), muss `APP_BIND_IP` von `127.0.0.1` auf eine LAN-erreichbare Adresse umgestellt werden:
```bash
# LAN-IP des Docker-Hosts ermitteln
ip -4 -br a | grep -v lo
# In .env eintragen (Beispiel)
sed -i 's/^APP_BIND_IP=.*/APP_BIND_IP=192.168.10.42/' .env
docker compose up -d
```
Alternativ `0.0.0.0` für alle Interfaces — die explizite IP ist sicherheitstechnisch sauberer.
Firewall absichern, damit nur der Proxy-Host auf 8080 zugreifen darf:
```bash
sudo ufw allow from <proxy-host-ip> to any port 8080 proto tcp comment 'reverse proxy'
sudo ufw deny 8080
```
#### Django-Seite anpassen
Die externe Domain muss in `.env` eingetragen sein, sonst lehnt Django die Requests ab (400 Bad Request bzw. 403 CSRF):
```env
DJANGO_ALLOWED_HOSTS=serienbrief.example.lan,localhost,127.0.0.1
CSRF_TRUSTED_ORIGINS=https://serienbrief.example.lan,http://localhost:8080,http://127.0.0.1:8080
```
Nach Änderung `docker compose restart web`.
#### Variante A: Nginx Proxy Manager (NPM)
Im NPM-Webinterface unter **Proxy Hosts → Add Proxy Host**:
| Feld | Wert |
|---|---|
| Domain Names | `serienbrief.example.lan` |
| Scheme | `http` |
| Forward Hostname / IP | LAN-IP des Docker-Hosts (z.B. `192.168.10.42`) |
| Forward Port | `8080` |
| Cache Assets | aus |
| Block Common Exploits | an |
| Websockets Support | an (HTMX nutzt nur AJAX, schadet aber nicht) |
Im Tab **Custom Locations** oder **Advanced** zusätzlich:
```nginx
client_max_body_size 50M;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
```
NPM setzt `Host`, `X-Forwarded-For` und Standard-Header bereits selbst. **Pflicht** ist nur `X-Forwarded-Proto $scheme`, weil Djangos `SECURE_PROXY_SSL_HEADER` darauf basiert.
TLS-Zertifikat (Let's Encrypt für interne Domains via DNS-Challenge, oder eigenes CA-Cert) im Tab **SSL** zuweisen.
#### Variante B: Generisches Nginx / OpenResty
2026-05-21 10:36:16 +02:00
```nginx
server {
listen 443 ssl http2;
2026-05-21 17:54:38 +02:00
server_name serienbrief.example.lan;
2026-05-21 10:36:16 +02:00
ssl_certificate /etc/ssl/lan/serienbrief.crt;
ssl_certificate_key /etc/ssl/lan/serienbrief.key;
2026-05-21 17:54:38 +02:00
client_max_body_size 50M; # für CSV/DOCX-Uploads
2026-05-21 10:36:16 +02:00
location / {
proxy_pass http://<docker-host-ip>:8080;
2026-05-21 17:54:38 +02:00
proxy_http_version 1.1;
2026-05-21 10:36:16 +02:00
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
2026-05-21 17:54:38 +02:00
proxy_set_header X-Forwarded-Host $host;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
2026-05-21 10:36:16 +02:00
proxy_redirect off;
}
}
```
2026-05-21 17:54:38 +02:00
#### Diagnose bei 502 Bad Gateway
Vom Proxy-Host aus testen:
```bash
# TCP-Connect — "succeeded" erwartet
nc -zv <docker-host-ip> 8080
# HTTP-Request — 200/302 erwartet
curl -v -H "Host: serienbrief.example.lan" http://<docker-host-ip>:8080/
```
Typische Ursachen:
- `Connection refused``APP_BIND_IP` steht noch auf `127.0.0.1`
- `Timeout` → Firewall blockt
- 502 ohne Connect-Fehler → falsche Upstream-IP im Proxy-Eintrag
2026-05-21 10:36:16 +02:00
---
## Entwicklung in VS Code
- `.devcontainer/devcontainer.json` referenziert den ComposeStack mit DevOverride
- `.vscode/launch.json` enthält Configs:
- **Django: runserver (attach)** → debugpy auf 5678
- **Django: pytest** → führt Tests im Container aus
- `.vscode/tasks.json` für `makemigrations`, `migrate`, `shell`, `collectstatic`
- `.vscode/extensions.json` empfiehlt: Python, Pylance, Django, Docker, Even Better TOML, Ruff
### Workflow
```bash
# Stack hochfahren (Override greift automatisch)
docker compose up -d
# Code‑Änderungen werden via BindMount sofort sichtbar
# runserver reloaded automatisch
# Debugging: F5 in VS Code → attach an debugpy:5678
```
### Wichtige Unterschiede Dev ↔ Prod
| Aspekt | Dev | Prod |
|---|---|---|
| WSGI | `runserver` | Gunicorn |
| Code | BindMount `./app:/app` | im Image |
| Healthcheck | deaktiviert | aktiv (`/healthz`) |
| `read_only` | aus | an |
| `DJANGO_DEBUG` | True | False |
| `collectstatic` | auto im Entrypoint | im Entrypoint |
| `target` | `dev` (mit debugpy, ipython, pytest) | `runtime` |
---
## Betrieb & Wartung
### Häufige Operationen
```bash
# Logs eines einzelnen Services
docker compose logs -f worker
# In den Container schauen
docker compose exec web bash
docker compose exec db psql -U serienbrief
# Migration nach Modelländerung
docker compose exec web python manage.py makemigrations
docker compose exec web python manage.py migrate
# Statics neu generieren
docker compose exec web python manage.py collectstatic --noinput
# CeleryQueue Status
docker compose exec worker celery -A config inspect active
docker compose exec worker celery -A config inspect reserved
```
### Updates
```bash
# Code aktualisieren
git pull
# Image neu bauen
docker compose build --pull
# Mit ZeroDowntime nicht möglich (singlehost) → kurzer Restart
docker compose up -d
# DBMigrationen
docker compose exec web python manage.py migrate
```
### Backup & Restore
```bash
# Manuelles Backup erzwingen
docker compose restart backup
docker compose logs --tail=50 backup
# Restore (Beispiel)
docker compose exec -T db pg_restore -U serienbrief -d serienbrief --clean < backups/db_20260521_030000.dump
```
### Monitoring (Empfehlung, nicht enthalten)
- `docker stats` für adhoc ContainerRessourcen
- HealthcheckStatus: `docker compose ps`
- ContainerLogs via zentralem syslog/journald (Docker `json-file` rotiert 5×10 MB)
- Optional: Prometheus + cAdvisor + node_exporter, Loki für Logs
---
## Datenschutz & Compliance
- **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)
- **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
---
## Bekannte Einschränkungen & Roadmap
### 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
- Image enthält LibreOffice (~400 MB) — könnte in separates WorkerImage ausgelagert werden
### Roadmap (kurzfristig)
- [ ] SingleLetterPreview im JobErstellungsFlow
- [ ] RetentionCleanup als periodic task
- [ ] CSVValidierung strict mit Pflichtfeldliste pro Template
- [ ] Mandanten/Berechtigungsmodell (Abteilung X sieht nur eigene Templates)
- [ ] LDAPAuth über `djangoauthldap` (ADIntegration GB)
### Roadmap (mittelfristig)
- [ ] AppArmorProfile pro Container
- [ ] ImageScanning in CI (Trivy)
- [ ] Separate WorkerImage ohne WebDependencies
- [ ] AESVerschlüsselung der hochgeladenen CSVs at rest
- [ ] Optional: Konvertierung über Gotenberg statt LibreOfficeimImage (cleaneres Separation of Concerns)
---
## Lizenz & Verantwortlichkeit
Internes Projekt. Verantwortlich für Konzept & Betrieb: **Datenschutzkoordination / ITSystemadministration**.
Keine externe Lizenz festgelegt — Verteilung ausschließlich innerbetrieblich.