Files
Laufzettel/docs/betrieb/pdf.md
T
Florian WildeandClaude Opus 5 f3950847f3
Prüfung / pruefung (push) Waiting to run
Prüfung / e2e (push) Blocked by required conditions
feat(laeufe): Uebergabeprotokoll als PDF (#041)
Neben "Drucken" steht am Lauf "Als PDF", im Archiv an jedem Treffer "Protokoll
als PDF" — dasselbe Blatt als fertige Datei, zum Verschicken oder Abheften.
Unten darauf jetzt zwei Unterschriftsfelder mit Zeile fuer Ort, Datum und Namen;
wo eine Vier-Augen-Freigabe erteilt wurde, steht sie mit im Kopf.

Erzeugt wird es, indem der Server seine eigene Druckansicht ueber 127.0.0.1
laedt und druckt — mit dem Sitzungscookie des Anfragenden. Damit gibt es genau
eine Vorlage fuer Papier und Datei, und fuer das PDF gilt dieselbe Rechte- und
Mandantenpruefung wie fuer die Seite. Ein zweites Layout waere billiger und
wuerde frueher oder spaeter abweichen, ohne dass es jemandem auffaellt.

Ein eigener Browser-Kontext je PDF, kein geteilter: er traegt die fremde
Sitzung, und ein geteilter gaebe sie an das naechste PDF weiter. Gespeichert
wird nichts — ein abgelegtes PDF waere eine zweite Wahrheit neben dem Lauf.
Ein erzeugtes Passwort steht auch hier nie darauf, geprueft am extrahierten
Text des fertigen PDF und nicht an den Bytes: der Inhalt ist komprimiert, dort
nichts zu finden bewiese nichts.

Chromium bleibt optional, und die gemessene Zahl ist der Grund: das
Laufzeitbild waechst von 370 MB auf 1,52 GB, Alpines chromium kostet allein
1,13 GB. Er kommt nur mit `--build-arg MIT_PDF=1` beziehungsweise
`deploy/install.sh --mit-pdf` mit. Fehlt er, erscheint der Knopf gar nicht
erst — ein Knopf, der zuverlaessig in eine Fehlermeldung fuehrt, ist schlimmer
als keiner. Ein direkter Aufruf bekommt 503 mit einer Meldung, die sagt, was
fehlt und wo es steht; ein leeres PDF gibt es in keinem Fall.

Meine Schaetzung lag um das Dreieinhalbfache daneben: "+328 MB" war das
.apk-Paket, nicht das Bild, und stand schon im Dockerfile-Kommentar, bevor ich
gebaut hatte. Jetzt steht die gemessene Zahl an drei Stellen.

1545 Vitest-Tests (vorher 1526): 15 in tests/pdf.test.ts, die den Handler echt
fahren, 3 in tests/pdf-grenze.test.ts fuer die Obergrenze mit gestelltem
Browser, einer in tests/archiv.test.ts. Dazu drei E2E-Ablaeufe; volle Suite
gruen mit 118 bestanden. Ein Blatt wurde wirklich erzeugt und angesehen.

Sechs Gegenproben, alle angeschlagen — darunter ein echtes Passwort auf dem
Blatt und die Freigabe des Zaehlers aus dem finally genommen, der Fall, in dem
ein Fehlschlag die PDF-Erzeugung bis zum Neustart leise stilllegt.

Closes #41

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-25 11:14:25 +01:00

168 lines
7.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Das Übergabeprotokoll als PDF
Jeder Lauf lässt sich als PDF herunterladen — ein Blatt mit Kopfdaten, allen Schritten samt
Zuständen und Notizen, den Kommentaren und zwei Unterschriftsfeldern. Gedacht ist es für die
Übergabe: ein Gerät wechselt den Besitzer, beide unterschreiben, das Blatt geht in die Ablage oder
zum Kunden.
**Das ist eine Zusatzausstattung, und sie kostet.** Ohne sie fehlt nichts Wesentliches: die
Druckansicht zeigt dasselbe Blatt, und jeder Browser kann daraus „in eine Datei drucken“. Das PDF
vom Server nimmt diesen Schritt nur ab — für den, der zwanzig Protokolle am Stück braucht, oder für
den, der sie aus dem Archiv heraus einzeln verschickt.
## Was es braucht
**Chromium auf dem Server.** Nichts weiter — keine Schriften aus dem Netz, kein Dienst, kein
zusätzlicher Port.
Der Grund: das PDF entsteht, indem der Server seine **eigene** Druckansicht lädt, so wie ein
Browser es täte, und sie druckt. Dadurch gibt es genau eine Vorlage für Papier und PDF. Ein zweites
Layout im Code wäre billiger zu betreiben und würde früher oder später von der Druckansicht
abweichen, ohne dass es jemandem auffällt — bis ein Kunde zwei verschiedene Blätter nebeneinander
legt.
Chromium ist der Preis dafür, und er ist hoch:
| Laufzeitbild | Größe |
|---|---|
| ohne Chromium | **370 MB** |
| mit Chromium | **1,52 GB** |
Gemessen am 2026-10-25. Alpines `chromium` kostet allein 1,13 GB — mehr als das Dreifache der
gesamten Anwendung. Deshalb ist er nicht im Standardbild, und deshalb ist diese Seite kein Anhang,
sondern eine Entscheidung.
## Mit Docker Compose
Chromium kommt über ein Bau-Argument ins Bild:
```bash
docker compose build --build-arg MIT_PDF=1 web
docker compose up -d
```
Damit das beim nächsten `docker compose build` nicht verlorengeht, gehört es in die
`docker-compose.yml` beziehungsweise in eine Override-Datei:
```yaml
services:
web:
build:
args:
MIT_PDF: "1"
```
Mehr ist nicht zu tun: das Paket liegt dann unter `/usr/bin/chromium-browser`, und dort sucht die
Anwendung von sich aus.
## Nativ, im LXC oder auf einem Debian
`deploy/install.sh` fragt beim ersten Lauf, ob Chromium mit soll, und merkt die Antwort in
`/etc/laufzettel/laufzettel.env`. Nachträglich:
```bash
apt-get install -y chromium
systemctl restart laufzettel
```
Auf Debian 13 liegt es danach unter `/usr/bin/chromium`. Der Neustart ist nötig, weil die Anwendung
den Pfad beim ersten PDF sucht und sich das Ergebnis für die Laufzeit des Prozesses merkt.
## Wenn Chromium fehlt
**Dann passiert nichts Stilles.** Der Knopf „Als PDF“ erscheint am Lauf und im Archiv gar nicht
erst — die Anwendung sieht bei jedem Seitenaufbau nach, ob ein Chromium da ist. Wer den Endpunkt
trotzdem aufruft, etwa aus einem alten Tab oder mit einem Lesezeichen, bekommt `503` und diesen
Text:
> Auf diesem Server ist kein Chromium installiert, und ohne ihn entsteht kein PDF. Die Druckansicht
> funktioniert weiterhin: dort im Browser „in eine Datei drucken“. Wie Chromium dazukommt, steht in
> docs/betrieb/pdf.md.
Ein leeres oder halbes PDF gibt es in keinem Fall. Das war die Vorgabe für dieses Verhalten: ein
Blatt, das aussieht wie ein Protokoll und keines ist, wäre schlimmer als eine klare Absage.
## Die drei Einstellungen
Alle drei sind Vorgaben, die man kennen sollte und selten braucht.
| Variable | Vorgabe | Wofür |
|---|---|---|
| `CHROMIUM_PFAD` | leer | Wo Chromium liegt, wenn er woanders liegt |
| `PDF_MAX_GLEICHZEITIG` | `2` | Wie viele PDFs gleichzeitig entstehen dürfen |
| `PDF_LEERLAUF_MS` | `60000` | Nach wie viel Leerlauf der Browser wieder schließt |
**`CHROMIUM_PFAD` leer lassen heißt „suche selbst“** — der Reihe nach
`/usr/bin/chromium-browser`, `/usr/bin/chromium`, `/usr/bin/google-chrome`. Ein gesetzter Pfad, der
ins Leere zeigt, ist dagegen ein **Fehler** und kein Rückfall auf die Suche: wer hier etwas
einträgt, hat eine Absicht, und ein Tippfehler soll auffallen und nicht dazu führen, dass
stillschweigend ein anderer Browser läuft als der gemeinte.
**`PDF_MAX_GLEICHZEITIG` schützt den Arbeitsspeicher.** Jedes PDF hält kurz einen eigenen
Browser-Kontext offen — eigen, und nicht geteilt, weil er das Sitzungscookie des Anfragenden trägt.
Ist die Grenze erreicht, bekommt der nächste die Bitte, es gleich noch einmal zu versuchen, statt
einer Maschine, die unter Last steht.
**`PDF_LEERLAUF_MS`** hält den Chromium nach dem letzten PDF noch eine Minute offen, weil ihn zu
starten rund zwei Sekunden kostet und Protokolle meist in Serie entstehen. Danach schließt er: offen
belegt er rund 100 MB, und auf einem 2-GB-LXC ist das ein Achtel des Arbeitsspeichers für etwas, das
niemand gerade braucht.
## Was auf dem Blatt steht — und was nicht
Es steht dasselbe darauf wie in der Druckansicht, denn es **ist** die Druckansicht. Insbesondere:
- **Kein generiertes Passwort.** Wo ein Befehl `{{generiertes_passwort}}` enthält, steht auf dem
Blatt `[Passwort – nicht gespeichert]`. Das Passwort entsteht bei jeder Anzeige neu und wird
nirgends gespeichert; ein Ausdruck dagegen liegt herum. Geprüft wird das am extrahierten Text des
fertigen PDF, nicht an der Vorlage.
- **Keine Anhänge**, nur ihre Anzahl. Ein Foto gehört nicht auf ein Übergabeprotokoll, und ein PDF
mit zwanzig Bildern wäre nicht mehr zu verschicken.
- **Befehle unmaskiert.** Das Blatt hält fest, was zu tun war; Maskierzeichen wären auf Papier
Rauschen. Wer einen Befehl ausführen will, nimmt die Laufseite — dort ist er für die gewählte
Shell maskiert.
## Fehlersuche
**Der Knopf fehlt, obwohl Chromium installiert ist.** Die Anwendung hat ihn nicht gefunden. Nachsehen,
wo er liegt, und `CHROMIUM_PFAD` setzen:
```bash
command -v chromium chromium-browser google-chrome
```
Danach den Dienst neu starten. Im Compose-Stand prüft man im Container selbst, nicht auf dem Host:
```bash
docker compose exec web command -v chromium-browser
```
**`503` mit „Die Druckansicht antwortete mit 307“** (oder 302, oder 401). Das Sitzungscookie kam
nicht mit oder gilt nicht mehr — Chromium wurde auf die Anmeldeseite geschickt. Neu anmelden und es
noch einmal versuchen. Bleibt es dabei, steht vermutlich ein Reverse Proxy im Weg, der `Cookie`
nicht durchlässt.
**`503` mit „Es entstehen gerade … PDFs gleichzeitig“.** Die Grenze greift. Entweder warten oder
`PDF_MAX_GLEICHZEITIG` anheben — und dabei im Blick behalten, dass jeder gleichzeitige Vorgang
Arbeitsspeicher kostet.
**`500` mit „Einzelheiten stehen im Protokoll des Dienstes“.** Hier geht die Meldung absichtlich
nicht hinaus, weil eine rohe Ausnahme Pfade und Zugangsdaten nennen kann. Nachsehen:
```bash
docker compose logs -f web | grep PDF # Compose
journalctl -u laufzettel -f | grep PDF # nativ
```
**Leere Kästen statt Buchstaben im PDF.** Dem Chromium fehlen Systemschriften. Im Bild ist
`font-noto` deshalb mit dabei; nativ gehört es nachinstalliert
(`apt-get install -y fonts-noto-core`). Die Schriften der Oberfläche selbst liefert die Anwendung
mit, aber für Zeichen, die darin nicht vorkommen, braucht Chromium einen Rückfall.
## Wo sonst noch etwas steht
- [`../../.env.example`](../../.env.example) — die drei Variablen mit Kommentar
- [`inbetriebnahme.md`](inbetriebnahme.md) — der Compose-Weg im Ganzen
- [`proxmox.md`](proxmox.md) — der native Weg und `deploy/install.sh`
- [`../anleitung/02-laeufe-abarbeiten.md`](../anleitung/02-laeufe-abarbeiten.md) — wie man das Protokoll als Anwender holt