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

7.4 KiB
Raw Blame History

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:

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:

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:

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:

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:

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:

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