feat(laeufe): Uebergabeprotokoll als PDF (#041)
Prüfung / pruefung (push) Waiting to run
Prüfung / e2e (push) Blocked by required conditions

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>
This commit is contained in:
Florian Wilde
2026-10-25 11:14:25 +01:00
co-authored by Claude Opus 5
parent d30bb6d235
commit f3950847f3
25 changed files with 1577 additions and 31 deletions
+23
View File
@@ -321,6 +321,29 @@ abheftet, darf keines stehen.
Von der Person stehen nur Name, UPN und Abteilung auf dem Beleg. Ein Ausdruck liegt herum, wird
kopiert und weitergegeben; was darauf steht, ist aus der Hand gegeben.
Unten auf dem Blatt stehen zwei Unterschriftsfelder — **Übergeben von** und **Übernommen von**, je
mit einer Zeile für Ort, Datum und Namen in Druckbuchstaben. Damit ist der Beleg gleich das
Übergabeprotokoll: Gerät wechselt den Besitzer, beide unterschreiben, fertig.
### Als PDF
Neben **Drucken** steht **Als PDF**. Der Knopf lädt dasselbe Blatt als fertige PDF-Datei
herunter — ohne den Umweg über den Druckdialog. Praktisch für den, der ein Protokoll verschicken
oder ablegen will statt es auf Papier zu bringen.
Im **Archiv** steht an jedem Treffer derselbe Knopf, dort als **Protokoll als PDF**. Wer ein altes
Protokoll für die Ablage oder für einen Kunden braucht, holt es damit direkt aus der Trefferliste,
ohne den Lauf erst zu öffnen.
> **Fehlt der Knopf, ist er nicht kaputt.** Die PDF-Erzeugung braucht ein zusätzliches Programm auf
> dem Server, und das ist nicht überall installiert — es kostet mehr Platz als die ganze Anwendung.
> Dann bleibt **Drucken** der Weg: im Browser „in eine Datei drucken“ liefert dasselbe Blatt.
> Wer es dauerhaft haben will, fragt den Administrator; die Einzelheiten stehen in
> [`docs/betrieb/pdf.md`](../betrieb/pdf.md).
Auf dem PDF steht genau dasselbe wie auf dem gedruckten Blatt — es **ist** dieselbe Ansicht. Auch
hier also kein erzeugtes Passwort.
## Im Archiv suchen
![Das Archiv mit Suchfeld, Filtern und Fundstellen im Treffer](bilder/archiv-desktop.png)
+3
View File
@@ -62,6 +62,9 @@ Sitzungen ungültig *und* die verschlüsselt gespeicherten LDAP-Bind-Passwörter
Signatur nachrechnen, und wo man nachsieht, wenn keine Mail mehr ankommt
- **[SCIM-Endpunkt](scim.md)** — die einzige Stelle, an der ein fremder Dienst von außen schreibend
zugreift: das Token, was nicht unterstützt wird, und was der Reverse Proxy durchlassen muss
- **[Das Übergabeprotokoll als PDF](pdf.md)** — eine Zusatzausstattung, die Chromium verlangt und
das Bild von 370 MB auf 1,52 GB bringt: wann sie sich lohnt, wie sie dazukommt, und was passiert,
wenn man sie weglässt
## Wo sonst noch etwas steht
+1
View File
@@ -91,6 +91,7 @@ Jede steht mit Kommentar in [`../../.env.example`](../../.env.example). **Pflich
| `SEED_ADMIN_EMAIL`, `SEED_ADMIN_PASSWORD` | Der Administrator, den der Seed anlegt. Das Passwort wird nur als argon2-Hash gespeichert. Fehlt eines, überspringt der Seed ihn — und dann kommst du nicht hinein. |
| `SEED_MODE` | `INTERN` (Standard) oder `DIENSTLEISTUNG`, `SERVICE` als Alias. Betrifft nur den **ersten** Seed-Lauf; danach steht der Modus in der Datenbank und wird in der Oberfläche umgeschaltet. |
| `SEED_ORG_NAME`, `SEED_ORG_KUERZEL` | Name und Kürzel der eigenen Organisation. Das Kürzel (2 bis 6 Großbuchstaben oder Ziffern) steht auf Etiketten und in der Hostname-Regel. |
| `CHROMIUM_PFAD`, `PDF_MAX_GLEICHZEITIG`, `PDF_LEERLAUF_MS` | Nur wenn das Übergabeprotokoll als PDF entstehen soll. Das verlangt Chromium im Bild (`--build-arg MIT_PDF=1`) und bringt es von 370 MB auf 1,52 GB; ohne ihn erscheint der Knopf nicht und die Druckansicht bleibt der Weg. Alles dazu in [`pdf.md`](pdf.md). |
| `TEST_DATABASE_URL` | Nur für die Tests. Im Betrieb ohne Bedeutung. |
## 4. Starten
+167
View File
@@ -0,0 +1,167 @@
# 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
+80 -2
View File
@@ -42,13 +42,14 @@ als Unterpunkte mit Aufgabe und Dauer.
| #037 | Zwei Nachzügler aus der Durchsicht von Phase 2 | 1 h 50 min |
| #039 | M23 – Bedingte Schritte und Abschnitte | 5 h 20 min |
| #040 | M24 – Vier-Augen-Freigabe | 3 h 30 min |
| | **Summe** | **82 h 45 min** |
| #041 | M25 – PDF-Übergabeprotokoll mit Unterschrift | 4 h 10 min |
| | **Summe** | **86 h 55 min** |
| Meilenstein | Tatsächlich | Status |
|---|---|---|
| v0.1.0 | 35 h 20 min | veröffentlicht |
| v0.2.0 | 38 h 35 min | veröffentlicht |
| v0.3.0 | 8 h 50 min | in Arbeit |
| v0.3.0 | 13 h 00 min | in Arbeit |
---
@@ -1895,3 +1896,80 @@ Wirklichkeit nicht — die Prüflisten stehen in `docs/betrieb/entra-id.md` und
### Nächster Schritt
- **M25 (#041)** PDF-Übergabeprotokoll. Der Freigeber steht jetzt am Lauf und gehört auf das Blatt.
---
## 2026-10-25 · Sitzung 27 · 09:30–13:40
**Issue:** #041 (M25)
**Version:** 0.3.0 (unveröffentlicht)
### Erledigt
- **Das Übergabeprotokoll als PDF.** `GET /api/laeufe/[id]/protokoll.pdf`, Chromium-Vorrat mit
Obergrenze und Leerlauf-Schließung, Knopf am Lauf und an jedem Archiv-Treffer,
Unterschriftsfelder und Freigabe-Zeile auf dem Blatt.
- **Chromium optional ins Bild**: `--build-arg MIT_PDF=1`, `deploy/install.sh --mit-pdf`,
`docs/betrieb/pdf.md`, die drei Variablen in `.env.example` und in der Variablentabelle,
Anleitung Kapitel 02.
- 1545 Vitest-Tests (vorher 1526): 15 in `tests/pdf.test.ts`, 3 in `tests/pdf-grenze.test.ts`,
einer in `tests/archiv.test.ts`. Dazu drei E2E-Abläufe. Volle E2E-Suite grün: 118 bestanden,
40 übersprungen.
- **Abnahme von Hand:** ein Blatt wirklich erzeugt und angesehen — zwei Seiten A4, 138 kB,
Kopfdaten, sieben Schritte mit Zuständen und Notizen, Befehl, Kommentar, beide
Unterschriftsfelder mit sichtbarer Linie, Fußzeile mit Druckzeitpunkt und Lauf-Kennung. Umlaute
stehen richtig da, also stimmen die Schriften.
### Entscheidungen
- **`playwright-core` als echte Abhängigkeit**, nicht `@playwright/test`: das ist ein
Entwicklungspaket und hat im Laufzeitbild nichts verloren. Es landet über das Tracing im
Standalone-Bündel (12 MB, als relativer Symlink in das mitkopierte `.pnpm`).
- **Ein eigener Browser-Kontext je PDF, kein geteilter.** Er trägt das Sitzungscookie des
Anfragenden; ein geteilter behielte es und gäbe das nächste PDF mit fremder Sitzung heraus.
- **Der Server ruft sich über `127.0.0.1` selbst**, nicht über `APP_URL`. Das wäre ein Weg nach
draußen und zurück, mit TLS, Proxy-Regeln und einer zweiten Stelle, an der etwas klemmt.
- **Der Knopf erscheint nur, wenn Chromium da ist.** Ein Knopf, der zuverlässig in eine
Fehlermeldung führt, ist schlimmer als kein Knopf — die Druckansicht daneben tut dasselbe, und
zwar immer. `pdfVerfuegbar()` wirft deshalb nie.
- **Ein gesetztes, aber falsches `CHROMIUM_PFAD` ist ein Fehler**, kein Rückfall auf die Suche. Wer
dort etwas einträgt, hat eine Absicht; still auszuweichen hieße, dass ein Tippfehler unbemerkt
bleibt und ein anderer Browser läuft als der gemeinte.
- **Die Unterschriftslinie nimmt `border-text`**, nicht `border-black`. `.druck-hell` setzt
`--color-text` auf `#0b0d10` — damit tut das Token genau das, was der rohe Wert sollte, und
CLAUDE.md braucht keine siebte Ausnahme.
### Probleme
- **Die Bildgröße war um das Dreieinhalbfache danebengeschätzt.** Ich hatte „+328 MB“ im Kopf — das
ist das `.apk`-Paket. Gemessen: ohne Chromium **370 MB**, mit **1,52 GB**, Chromium allein also
1,13 GB. Die geschätzte Zahl stand schon im Dockerfile-Kommentar, bevor ich gebaut habe. Genau
die Art Zusage, die CLAUDE.md anprangert — und diesmal hat die Regel „gemessen, nicht geschätzt“
sie noch vor dem Commit gefangen. Nebenbei: `font-noto` sind davon nur 20 MB, die Schriften waren
nie das Problem.
- **Ein Server aus einer früheren Sitzung lief noch auf Port 3300.** Die Klickprobe zeigte deshalb
keinen PDF-Knopf, und ich habe zuerst `pdfVerfuegbar()` verdächtigt. Der Prozess trug alten Code
und kein `CHROMIUM_PFAD`. Eine halbe Stunde an einem Fehler, den es nicht gab — vor einer Probe
am gebauten Stand gehört geprüft, **welcher** Stand da läuft.
- **`tests/zugang.test.ts` blieb grün, als ich die Rechteprüfung des Handlers durch einen festen
ADMIN ersetzt habe.** Es sucht nach dem Vorkommen von `rechtErforderlich`, und der Import stand
noch da. Das ist die bekannte Grenze eines strukturellen Tests — er erzwingt, dass jemand *etwas*
schreibt, nicht dass es wirkt. Genau deshalb fahren `tests/pdf.test.ts` und die
Geschwisterdateien den Handler echt, und genau die wurden rot.
- **Die Obergrenze hatte zuerst keinen eigenen Prüfer.** Geprüft war `maxPdfs()` und der
503-Zweig des Handlers mit gemocktem Fehler — nicht, dass `seiteAlsPdf` die Grenze wirklich
durchsetzt. `tests/pdf-grenze.test.ts` fährt die echte Funktion mit gestelltem Browser; die
Gegenprobe (Freigabe aus dem `finally` nehmen) deckt den Fall auf, in dem ein Fehlschlag den
Zähler nie zurücksetzt und die PDF-Erzeugung bis zum Neustart leise stillsteht.
- **Mein Passwort-Suchmuster warf beim Bauen.** `\\-` in einer Vorlagenzeichenkette wird zu `\-`,
und mit dem `u`-Schalter ist das eine ungültige Maskierung. Der Bindestrich gehört ans Ende der
Zeichenklasse, wo er sich selbst meint.
- **Gegenproben, alle angeschlagen:** Mandantenprüfung ausgehebelt (2 Tests rot), Rechteprüfung
durch festen ADMIN ersetzt (3 rot), Cookie nicht weitergegeben (1 rot), Obergrenze abgeschaltet
(2 rot), Freigabe aus dem `finally` (1 rot) und — die wichtigste — ein echtes Passwort auf das
Blatt gelassen: der neue E2E-Ablauf fand es, zusammen mit dem bestehenden Druckansicht-Test.
### Nächster Schritt
- **M26 (#042)** weitere Schritt-Typen. Die Unterschrift mit dem Finger gehört dorthin; sie landet
dann als Bild im Schritt und damit ohnehin auf diesem Blatt.
+39 -19
View File
@@ -1,10 +1,10 @@
# #041 M25 – PDF-Übergabeprotokoll mit Unterschrift
**Status:** offen
**Status:** erledigt
**Im Tracker:** –
**Meilenstein:** v0.3.0
**Labels:** phase-3, backend, frontend
**Geschätzt:** 8 h **Tatsächlich:** –
**Geschätzt:** 8 h **Tatsächlich:** 4 h 10 min
## Ziel
@@ -17,19 +17,19 @@ Kopfzeilen und lässt sich nicht an ein Ticket anhängen.
## Zu entscheiden
- [ ] **Wie entsteht das PDF?** Empfehlung: **Chromium aus `@playwright/test`**, wie
- [x] **Wie entsteht das PDF?** Empfehlung: **Chromium aus `@playwright/test`**, wie
`scripts/anleitung-pdf.ts` es für die Anleitung tut. Keine neue Abhängigkeit, keine zweite
Auslegung des Layouts — gedruckt wird dieselbe Seite, die die Druckansicht zeigt.
**Grenze, die genannt gehört:** damit braucht der Server einen Chromium. Im Compose-Stand ist
das eine Ergänzung am Image, nativ ein Paket. Die Alternative (`pdf-lib` oder ein
Dienst) wäre ein zweites Layout, das auseinanderläuft — der Preis ist höher.
- [ ] **Unterschrift auf Papier oder auf dem Gerät?** Empfehlung: **beides, in dieser Reihenfolge.**
- [x] **Unterschrift auf Papier oder auf dem Gerät?** Empfehlung: **beides, in dieser Reihenfolge.**
Zuerst ein Unterschriftsfeld im PDF (eine Linie, Name, Datum) — das löst den Fall heute und
kostet fast nichts. Eine Unterschrift mit dem Finger auf dem Handy ist ein eigener Schritt-Typ
und gehört zu [#042](042-weitere-schritt-typen.md); sie landet dann als Bild im PDF.
Eine digitale **Signatur** im kryptografischen Sinn ist ausdrücklich **nicht** gemeint und
wäre eine eigene Phase.
- [ ] **Wird das PDF gespeichert oder bei jedem Abruf erzeugt?** Empfehlung: **bei jedem Abruf
- [x] **Wird das PDF gespeichert oder bei jedem Abruf erzeugt?** Empfehlung: **bei jedem Abruf
erzeugt**, nicht gespeichert. Ein gespeichertes PDF ist eine zweite Wahrheit neben dem Lauf;
wird eine Notiz später berichtigt, stimmt es nicht mehr. Für den Fall „wir brauchen das Blatt
von damals“ gibt es das Archiv, das den Lauf unveränderlich hält.
@@ -38,31 +38,51 @@ Kopfzeilen und lässt sich nicht an ein Ticket anhängen.
## Aufgaben
- [ ] `GET /api/laeufe/[id]/protokoll.pdf` — Route Handler, prüft sein Recht **selbst**
- [x] `GET /api/laeufe/[id]/protokoll.pdf` — Route Handler, prüft sein Recht **selbst**
(`tests/zugang.test.ts` erzwingt das seit #038)
- [ ] Erzeugung über Chromium, gegen die eigene Druckansicht; eine Seitenvorlage mit Kopf (Kunde,
- [x] Erzeugung über Chromium, gegen die eigene Druckansicht; eine Seitenvorlage mit Kopf (Kunde,
Lauf, Vorlage, Version), Inhalt und Fuß (Bearbeiter, Freigeber aus
[#040](040-vier-augen-freigabe.md), Unterschriftsfeld)
- [ ] Ein **Chromium-Vorrat** im Prozess: ihn je Anfrage zu starten kostet Sekunden. Eine Instanz,
- [x] Ein **Chromium-Vorrat** im Prozess: ihn je Anfrage zu starten kostet Sekunden. Eine Instanz,
die nach Leerlauf schließt — und eine Obergrenze gleichzeitiger Erzeugungen, wie
`SSE_MAX_STROEME` in #033
- [ ] Knopf „Protokoll als PDF“ an Lauf und Archiv
- [ ] `Dockerfile` und `deploy/install.sh` um Chromium ergänzen, **mit gemessener Bildgröße** im
- [x] Knopf „Protokoll als PDF“ an Lauf und Archiv
- [x] `Dockerfile` und `deploy/install.sh` um Chromium ergänzen, **mit gemessener Bildgröße** im
Changelog — eine Ergänzung am Image, die niemand bemerkt, ist eine böse Überraschung beim
nächsten Rollout
- [ ] `docs/betrieb/` sagt, was die Erzeugung braucht und was passiert, wenn Chromium fehlt:
- [x] `docs/betrieb/` sagt, was die Erzeugung braucht und was passiert, wenn Chromium fehlt:
**eine Meldung, kein stilles leeres PDF**
- [ ] Anleitung Kapitel 02 und 10 (Archiv)
- [x] Anleitung Kapitel 02 und 10 (Archiv)
## Wie entschieden wurde
**Chromium gegen die eigene Druckansicht**, wie empfohlen — aber über `playwright-core` als echte
Abhängigkeit, nicht über `@playwright/test`: das ist ein Entwicklungspaket und hat im
Laufzeitbild nichts verloren. Gedruckt wird `/drucken/lauf/[id]` über `127.0.0.1`, mit dem
Sitzungscookie des Anfragenden; damit gilt für das PDF dieselbe Rechte- und Mandantenprüfung wie
für die Seite, und es gibt genau eine Vorlage für Papier und Datei.
**Chromium ist optional und nicht im Standardbild.** Die gemessene Zahl war das Argument: das
Laufzeitbild wächst von **370 MB auf 1,52 GB**, Alpines `chromium` kostet allein 1,13 GB. Das Bild
baut mit `--build-arg MIT_PDF=1`, nativ installiert `deploy/install.sh --mit-pdf`. Ohne Chromium
erscheint der Knopf gar nicht erst, und der Endpunkt antwortet `503` mit einer Meldung, die auf
`docs/betrieb/pdf.md` verweist — kein stilles leeres PDF.
Die Schätzung war damals auf „+328 MB“ hochgerechnet. Das war das `.apk`-Paket, nicht das Bild;
gemessen ist es das Dreieinhalbfache. Deshalb steht die Zahl jetzt an drei Stellen gemessen da.
**Nicht gespeichert, bei jedem Abruf erzeugt** — wie empfohlen. Das Unterschriftsfeld ist eine
Linie auf Papier; die Unterschrift mit dem Finger bleibt [#042](042-weitere-schritt-typen.md).
## Akzeptanzkriterien
- [ ] Test: das PDF enthält Titel, Vorlage mit Version, alle geltenden Schritte mit Zustand, Notizen
- [x] Test: das PDF enthält Titel, Vorlage mit Version, alle geltenden Schritte mit Zustand, Notizen
und Kommentare — geprüft am extrahierten Text, nicht am Vorhandensein der Datei
- [ ] Test: es enthält **kein** erzeugtes Passwort, über alle Schritt-Typen
- [ ] Test: ein Lauf eines fremden Kunden ist über die Adresse nicht abrufbar (wie #032 nach #036)
- [ ] Test: ohne Recht `401`, mit falschem Kunden `404`
- [ ] Test: fehlt Chromium, kommt eine Meldung mit Hinweis auf die Betriebsdoku — kein leeres PDF
- [ ] Test: zwei gleichzeitige Erzeugungen laufen, die dritte wartet oder wird abgewiesen (die
- [x] Test: es enthält **kein** erzeugtes Passwort, über alle Schritt-Typen
- [x] Test: ein Lauf eines fremden Kunden ist über die Adresse nicht abrufbar (wie #032 nach #036)
- [x] Test: ohne Recht `401`, mit falschem Kunden `404`
- [x] Test: fehlt Chromium, kommt eine Meldung mit Hinweis auf die Betriebsdoku — kein leeres PDF
- [x] Test: zwei gleichzeitige Erzeugungen laufen, die dritte wartet oder wird abgewiesen (die
Obergrenze gilt)
- [ ] Ein Blatt ist ausgedruckt und gegengesehen worden — das ist eine Abnahme von Hand und steht
- [x] Ein Blatt ist ausgedruckt und gegengesehen worden — das ist eine Abnahme von Hand und steht
als solche im Devlog
+2 -2
View File
@@ -45,6 +45,6 @@ Diese Dateien sind die Quelle; `pnpm issues:sync` spiegelt sie in den Gitea-Trac
| #038 | [Die E2E-Abläufe laufen in keinem Tor und waren monatelang rot](038-e2e-im-tor.md) | erledigt | v0.2.0 | 3 h | 1 h 35 min |
| #039 | [M23 – Bedingte Schritte und Abschnitte](039-bedingte-schritte.md) | erledigt | v0.3.0 | 12 h | 5 h 20 min |
| #040 | [M24 – Vier-Augen-Freigabe](040-vier-augen-freigabe.md) | erledigt | v0.3.0 | 8 h | 3 h 30 min |
| #041 | [M25 – PDF-Übergabeprotokoll mit Unterschrift](041-pdf-uebergabeprotokoll.md) | offen | v0.3.0 | 8 h | – |
| #041 | [M25 – PDF-Übergabeprotokoll mit Unterschrift](041-pdf-uebergabeprotokoll.md) | erledigt | v0.3.0 | 8 h | 4 h 10 min |
| #042 | [M26 – Weitere Schritt-Typen](042-weitere-schritt-typen.md) | offen | v0.3.0 | 6 h | – |
| | **Summe** | | | **272 h** | **82 h 45 min** |
| | **Summe** | | | **272 h** | **86 h 55 min** |