docs(issues): Phase 3 schneiden, und Verweise zeigen wieder richtig
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s

Vier Meilensteine fuer v0.3.0, 34 h geschaetzt: bedingte Schritte und
Abschnitte (M23), Vier-Augen-Freigabe (M24), PDF-Uebergabeprotokoll (M25),
weitere Schritt-Typen (M26). Die Reihenfolge ist begruendet: M23 zuerst, weil
es als einziges am Datenmodell und an der Abschlussregel arbeitet; M26 zuletzt,
weil UNTERSCHRIFT erst im PDF einen Zweck hat.

Jedes Issue nennt die Entscheidungen, die zu treffen sind, mit Empfehlung und
Begruendung. Bei M23 ist die entscheidende: eine Bedingung auf einer Variable
liesse sich beim Start festschreiben wie bei #031, eine auf einem
Schrittergebnis nicht — die Antwort entsteht erst waehrend der Arbeit.

Beim Nummernvergeben einen Fund: vier Codestellen, sechs Changelog-Zeilen und
der Doku-Pruefer nannten dreistellig die Tracker-Nummern 39 und 40 — gemeint
waren die Issue-Dateien 025 und 026. Solange es diese Dateien nicht gab, fiel es
niemandem auf; mit Phase 3 waere es eine falsche Spur geworden, und zwar in
Kommentaren, die eine Codestelle begruenden.

Behoben, und pruefe:doku prueft es ab jetzt: jeder dreistellige Verweis muss auf
eine Issue-Datei zeigen. Gegenprobe mit einer erfundenen Nummer schlaegt an.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-10-11 15:36:27 +02:00
co-authored by Claude Opus 5
parent 60aaf746cf
commit 2f247b3617
13 changed files with 373 additions and 17 deletions
+6 -6
View File
@@ -44,15 +44,15 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/).
- Der Healthcheck `/api/gesundheit` nennt im Fehlerfall keinen Grund mehr. Der Pfad ist ohne - Der Healthcheck `/api/gesundheit` nennt im Fehlerfall keinen Grund mehr. Der Pfad ist ohne
Anmeldung erreichbar, und eine Meldung von Prisma oder `pg` nennt Host, Port, Benutzernamen und Anmeldung erreichbar, und eine Meldung von Prisma oder `pg` nennt Host, Port, Benutzernamen und
Datenbanknamen. Nach außen steht jetzt nur noch `{"status":"fehler"}` mit Status 503; die Datenbanknamen. Nach außen steht jetzt nur noch `{"status":"fehler"}` mit Status 503; die
Begründung geht ins Protokoll des Prozesses (#039) Begründung geht ins Protokoll des Prozesses (#025)
- Die Konvention „nie auf rohe Hexwerte“ nennt ihre sechs begründeten Ausnahmen beim Namen — - Die Konvention „nie auf rohe Hexwerte“ nennt ihre sechs begründeten Ausnahmen beim Namen —
Druckansichten, QR-Code und `themeColor` entscheiden nicht nach unserem Farbschema. Die Regel war Druckansichten, QR-Code und `themeColor` entscheiden nicht nach unserem Farbschema. Die Regel war
strenger formuliert als gemeint (#040) strenger formuliert als gemeint (#026)
- `CLAUDE.md` und `README.md` nennen den LDAP-Testcontainer als Teil des Tors vor dem Commit. Ohne - `CLAUDE.md` und `README.md` nennen den LDAP-Testcontainer als Teil des Tors vor dem Commit. Ohne
ihn überspringt die Suite **22 Tests** — den ganzen Verzeichnis-Abgleich —, sagt das auf stderr, ihn überspringt die Suite **22 Tests** — den ganzen Verzeichnis-Abgleich —, sagt das auf stderr,
und der Lauf sieht trotzdem grün aus (#040) und der Lauf sieht trotzdem grün aus (#026)
- Im README fehlte `format:pruefen` in den Qualitätsschranken, obwohl `CLAUDE.md` es seit #010 - Im README fehlte `format:pruefen` in den Qualitätsschranken, obwohl `CLAUDE.md` es seit #010
verlangt (#040) verlangt (#026)
### Behoben ### Behoben
@@ -235,9 +235,9 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/).
eingespielte Kopie bekommt **keinen** Schlüssel: sie soll nicht den Platz des Originals einnehmen eingespielte Kopie bekommt **keinen** Schlüssel: sie soll nicht den Platz des Originals einnehmen
(#021) (#021)
- `pnpm pruefe:doku` vergleicht die Schranken-Befehle in `CLAUDE.md` und `README.md` und verlangt von - `pnpm pruefe:doku` vergleicht die Schranken-Befehle in `CLAUDE.md` und `README.md` und verlangt von
beiden den LDAP-Container. Genau diese Abweichung war unbemerkt entstanden (#040) beiden den LDAP-Container. Genau diese Abweichung war unbemerkt entstanden (#026)
- Fünf Tests für den Healthcheck, darunter einer, der belegt, dass die Begründung ins Protokoll - Fünf Tests für den Healthcheck, darunter einer, der belegt, dass die Begründung ins Protokoll
geschrieben und nicht bloß verschluckt wird (#039) geschrieben und nicht bloß verschluckt wird (#025)
## [0.1.0] – 2026-09-26 ## [0.1.0] – 2026-09-26
+1 -1
View File
@@ -257,7 +257,7 @@ geprüft. In CI bricht sie stattdessen ab, absichtlich.
| `mailpit` | **3 Tests** (nachgezählt: 36 statt 33) | der echte SMTP-Versand in `tests/benachrichtigungen.test.ts` — die vier Anlässe, die Passwort-Prüfung über alle Rumpftexte und „nichts zweimal“ | | `mailpit` | **3 Tests** (nachgezählt: 36 statt 33) | der echte SMTP-Versand in `tests/benachrichtigungen.test.ts` — die vier Anlässe, die Passwort-Prüfung über alle Rumpftexte und „nichts zweimal“ |
`ldap` fiel bei der Abnahme zu #019 auf, `mailpit` kam mit #034 dazu. Beide Zahlen sind **gemessen**, `ldap` fiel bei der Abnahme zu #019 auf, `mailpit` kam mit #034 dazu. Beide Zahlen sind **gemessen**,
nicht geschätzt — in #040 hätte beinahe eine geratene in dieser Datei gestanden. nicht geschätzt — in #026 hätte beinahe eine geratene in dieser Datei gestanden.
`format:pruefen` gehört dazu, seit der Review in #010 darauf gestoßen ist: `lint-staged` formatiert `format:pruefen` gehört dazu, seit der Review in #010 darauf gestoßen ist: `lint-staged` formatiert
beim Commit automatisch nach, und das Ergebnis wäre ein Commit mit Änderungen, die niemand gesehen beim Commit automatisch nach, und das Ergebnis wäre ein Commit mit Änderungen, die niemand gesehen
+9 -4
View File
@@ -11,10 +11,15 @@ Nicht im MVP umsetzen. Hier gesammelt, damit das Datenmodell nichts verbaut.
## Aufgaben ## Aufgaben
- [ ] PDF-Übergabeprotokoll mit Unterschrift Geschnitten am 2026-10-11, nach dem Tag von v0.2.0. Vier Meilensteine, 34 h geschätzt; die
- [ ] Bedingte Schritte und Abschnitte Reihenfolge ist nicht beliebig.
- [ ] Vier-Augen-Freigabe
- [ ] Weitere Schritt-Typen | | Issue | Warum an dieser Stelle |
|---|---|---|
| M23 | [Bedingte Schritte und Abschnitte](039-bedingte-schritte.md) | zuerst, weil es als einziges am Datenmodell und an der Abschlussregel arbeitet — und weil der echte Fall im Seed steht: „Mitarbeiter-Onboarding“ trägt beide Konto-Abschnitte |
| M24 | [Vier-Augen-Freigabe](040-vier-augen-freigabe.md) | danach, weil der Abschluss dann einmal angefasst ist |
| M25 | [PDF-Übergabeprotokoll](041-pdf-uebergabeprotokoll.md) | nach M24, damit das Blatt den Freigeber nennen kann |
| M26 | [Weitere Schritt-Typen](042-weitere-schritt-typen.md) | zuletzt, weil `UNTERSCHRIFT` erst im PDF einen Zweck hat |
## Vorleistung im MVP ## Vorleistung im MVP
+95
View File
@@ -0,0 +1,95 @@
# #039 M23 – Bedingte Schritte und Abschnitte
**Status:** offen
**Im Tracker:** –
**Meilenstein:** v0.3.0
**Labels:** phase-3, backend, frontend
**Geschätzt:** 12 h **Tatsächlich:** –
## Ziel
Eine Checkliste enthält heute **alles**, und was nicht zutrifft, wird von Hand auf
`NICHT_ZUTREFFEND` gesetzt. Der echte Fall steht im Seed: „Mitarbeiter-Onboarding“ trägt **beide**
Konto-Abschnitte — „Konto anlegen (Active Directory)“ mit 5 Schritten und „Konto anlegen (Entra
ID)“ mit 4 —, obwohl in jedem einzelnen Lauf nur einer gilt. Die Variable `konto_typ` sagt es
bereits; niemand wertet sie aus.
Neun Schritte, die ein Techniker bei jedem Onboarding wegklickt. Die Abschnitts-Aktion aus #013
macht es erträglich, aber sie ist ein Pflaster: der Fortschrittsbalken zählt die Schritte mit, und
im Archiv steht später eine Checkliste mit einem Abschnitt, den niemand je gebraucht hat.
Setzt [#031](031-abteilungsabschnitte.md) voraus: `geltendeAbschnitte` und `nurGeltende` sind die
Stelle, an der das sitzt.
## Zu entscheiden
- [ ] **Wann wird ausgewertet?** Das ist die Entscheidung, an der alles hängt.
Für Abteilungen (#031) wird beim **Start festgeschrieben** — die Abteilung des Laufs steht
dann fest, und ein archivierter Lauf muss genau die Schritte zeigen, die damals galten.
Für Bedingungen reicht das **nicht**: eine Bedingung auf einer Variable (`konto_typ`) ließe
sich beim Start auswerten, eine Bedingung auf einem **Schrittergebnis** („wenn Schritt 3 =
nein, dann Abschnitt B“) nicht — die Antwort entsteht erst während der Arbeit.
Empfehlung: **laufend auswerten, Ergebnis mitschreiben.** Die Auswertung läuft bei jeder
Änderung an einem Schrittergebnis; was dabei herauskommt, wird in den Lauf geschrieben (wie
`geltendeAbschnitte`). Damit gilt für das Archiv weiter „der Lauf weiß, was galt“, und der
Ereignisstrom aus #033 trägt die Änderung ohnehin schon zur zweiten Sitzung.
- [ ] **Woran darf eine Bedingung hängen?** Empfehlung: an einer **Variable** des Laufs und an
einem **Schrittergebnis** derselben Version. Nicht an Gerätetyp, Abteilung oder Rolle — für
Abteilungen gibt es #031, und je mehr Quellen, desto schwerer zu erklären, warum ein Schritt
fehlt.
- [ ] **Wie wird sie gespeichert?** Empfehlung: ein Json-Feld `bedingung` an `VorlagenAbschnitt`
und `VorlagenSchritt`, mit einem engen Schema — `{ art: "variable", schluessel, operator,
wert }` oder `{ art: "schritt", schrittId, zustand }`. Json, weil `konfig` es schon so macht
und ein neuer Operator dann keine Migration braucht.
**Eine Bedingung, kein Ausdrucksbaum.** Kein `und`/`oder`, keine Verschachtelung: das wäre
eine kleine Sprache, die niemand im Editor pflegen kann, und ein Abschnitt, der aus
unerklärlichen Gründen fehlt, ist schlimmer als einer zu viel.
- [ ] **Was passiert mit einem Schrittergebnis, das schon Daten trägt**, wenn die Bedingung
umschlägt? Empfehlung: die Zeile **bleibt** und wird nur ausgeblendet. Löschen hieße, eine
Notiz zu verlieren, die jemand geschrieben hat; und schlägt die Bedingung zurück, ist sie
wieder da.
## Aufgaben
- [ ] Migration: `bedingung Json?` an `VorlagenAbschnitt` und `VorlagenSchritt`, dazu ein CHECK, der
sicherstellt, dass eine Bedingung auf einen Schritt **derselben Version** zeigt (sonst zeigt
sie nach dem Veröffentlichen ins Leere)
- [ ] `src/lib/bedingungen.ts`, framework-frei: Schema, Auswertung, und die Liste der Schritte und
Abschnitte, die gelten. **Dieselbe Stelle für alle drei Schichten**, wie
`src/lib/geltende-abschnitte.ts`
- [ ] Auswertung bei jedem Schreiben an einem `Schrittergebnis`; das Ergebnis in den Lauf
- [ ] Abschlussregel: ein ausgeblendeter Pflichtschritt hält den Abschluss **nicht** auf (dieselbe
Regel wie bei #031, dieselbe Funktion)
- [ ] Fortschrittsbalken und Zählwerte rechnen nur mit dem, was gilt
- [ ] Editor: Bedingung an Abschnitt und Schritt setzen, mit Auswahl der Variable bzw. des Schritts
**aus derselben Version**; eine Vorschau, die sagt „gilt, wenn …“
- [ ] Warnung im Editor bei einer Bedingung auf einen Schritt, der **nach** dem bedingten steht —
sie wäre beim Abarbeiten noch offen, und der Abschnitt erschiene erst rückwirkend
- [ ] Abarbeitung: ausgeblendete Abschnitte verschwinden, ohne die Nummerierung zu verschieben
(`01 · Vorbereitung` kommt aus der Position und wird nie gespeichert)
- [ ] Druckansicht und Archiv zeigen, was galt
- [ ] Export/Import: die Bedingung wandert mit, und ein Verweis auf einen Schritt wird dabei auf die
**neue** Kennung umgeschrieben — sonst zeigt sie in der zweiten Installation ins Leere
- [ ] Seed: „Mitarbeiter-Onboarding“ bekommt die Bedingung auf `konto_typ`, die beiden
Konto-Abschnitte bleiben drin und schließen sich gegenseitig aus. Das ist die Gegenprobe am
echten Fall.
- [ ] Anleitung Kapitel 04 (Vorlagen): wann eine Bedingung das richtige Mittel ist und wann eine
zweite Vorlage
## Akzeptanzkriterien
- [ ] Test: ein Lauf mit `konto_typ = AD` hat die 5 AD-Schritte und **keinen** der 4 Entra-Schritte
- [ ] Test: dasselbe umgekehrt, und bei nicht gesetzter Variable gelten **beide** — eine Bedingung,
die nicht entschieden werden kann, blendet nichts aus
- [ ] Test: eine Bedingung auf einem Schrittergebnis schlägt um, **während** der Lauf läuft, und der
Abschnitt erscheint
- [ ] Test: ein ausgeblendeter Pflichtschritt hält den Abschluss nicht auf
- [ ] Test: ein Schrittergebnis mit Notiz bleibt stehen, wenn seine Bedingung umschlägt, und ist
danach wieder da
- [ ] Test: Export und Import einer Vorlage mit Bedingungen ergeben dieselbe Struktur, und der
Schritt-Verweis zeigt auf den richtigen Schritt der **neuen** Version
- [ ] Test: ein archivierter Lauf zeigt genau die Schritte, die bei seinem Abschluss galten
- [ ] Am Handy kein horizontales Scrollen
+68
View File
@@ -0,0 +1,68 @@
# #040 M24 – Vier-Augen-Freigabe
**Status:** offen
**Im Tracker:** –
**Meilenstein:** v0.3.0
**Labels:** phase-3, backend, frontend
**Geschätzt:** 8 h **Tatsächlich:** –
## Ziel
Manche Arbeiten soll nicht derselbe abschließen, der sie gemacht hat: ein Firewall-Rollout, ein
Restore-Test, das Löschen eines Kontos. Heute kann jeder `TECHNIKER` jeden Lauf abschließen und
archivieren.
Gebraucht wird: eine Vorlage kann sagen „dieser Lauf braucht eine Freigabe“, und dann schließt ihn
ein **Zweiter** ab — nachweisbar im Protokoll.
## Zu entscheiden
- [ ] **Freigabe am Lauf oder am Schritt?** Empfehlung: **am Lauf**, gesetzt von der Vorlage.
Am Schritt wäre feiner und verführt zu Checklisten, in denen an fünf Stellen jemand warten
muss; am Lauf ist es eine Aussage über die Arbeit als Ganzes und erklärt sich von selbst.
Schritt-Freigaben bleiben möglich, wenn sich der Bedarf zeigt — `VorlagenSchritt.konfig` ist
ein Json.
- [ ] **Wer darf freigeben?** Empfehlung: ein eigenes Recht `freigeben`, standardmäßig bei `ADMIN`
und `REDAKTEUR` — **nicht** bei `TECHNIKER`. Begründung: die Vier-Augen-Regel soll nicht
davon abhängen, dass zufällig ein zweiter Techniker greifbar ist; und wer sie umgehen will,
braucht dann ein zweites Konto mit mehr Rechten, was im Protokoll steht.
- [ ] **Darf der Bearbeiter sich selbst freigeben?** **Nein**, und das ist der ganze Sinn. Geprüft
wird gegen `Schrittergebnis.aktualisiertVonId` **und** `Lauf.erstelltVonId`: wer irgendetwas
daran getan hat, gibt nicht frei. Ein `ADMIN`, der allein im Haus ist, kommt damit nicht
weiter — das ist eine bewusste Härte, und die Anleitung sagt es.
- [ ] **Was, wenn die Freigabe verweigert wird?** Empfehlung: ein Zustand „zurückgewiesen“ mit
Pflichtnotiz. Der Lauf geht zurück auf `IN_ARBEIT`, die Notiz steht im Protokoll und an der
Abarbeitungsansicht. Kein eigener `LaufStatus` — das wäre eine Änderung an einem Enum, das
vier Jahre halten soll; es genügt ein Feld plus Protokolleintrag.
## Aufgaben
- [ ] Migration: `freigabePflicht Boolean` an `VorlagenVersion`, dazu `freigegebenVonId`,
`freigegebenAm` und `freigabeNotiz` am `Lauf`; CHECK, dass `freigegebenVonId` und
`freigegebenAm` zusammen gesetzt sind (wie bei `abgeschlossenAm`)
- [ ] CHECK: `freigegebenVonId` ist **nicht** `erstelltVonId` — die Regel gehört in die Datenbank,
nicht nur in den Dienst (CLAUDE.md)
- [ ] Recht `freigeben` in `RECHTE`, Rollenmatrix-Test erweitern
- [ ] Dienst: `laufFreigeben(laufId, benutzer, notiz?)` und `laufZurueckweisen(laufId, benutzer,
notiz)`; Abschluss verlangt die Freigabe, wenn die Version sie fordert
- [ ] Die Prüfung „wer hat daran gearbeitet“ an **einer** Stelle, wie `laufMussVeraenderbarSein`
- [ ] Benachrichtigung: ein neuer `Meldeanlass` `LAUF_FREIGABE_OFFEN` an die Rollen mit dem Recht —
sonst wartet ein Lauf, von dem niemand weiß
- [ ] Oberfläche: Hinweis am Lauf („wartet auf Freigabe durch einen Zweiten“), Knöpfe für Freigeben
und Zurückweisen, Notizfeld; die Übersicht bekommt einen Filter „wartet auf meine Freigabe“
- [ ] Editor: Häkchen „dieser Lauf braucht eine Freigabe“ an der Version
- [ ] Druckansicht und PDF nennen Freigeber und Zeitpunkt
- [ ] Anleitung: Kapitel 02 (Läufe abarbeiten) und 04 (Vorlagen), mit dem Hinweis, dass ein
Einzelkämpfer sich nicht selbst freigeben kann
## Akzeptanzkriterien
- [ ] Test: ein Lauf mit Freigabepflicht lässt sich nicht abschließen, solange keine Freigabe steht
- [ ] Test: der Bearbeiter kann **nicht** freigeben — auch nicht als `ADMIN`
- [ ] Test: die Datenbank weist es ebenfalls ab (CHECK), nicht nur der Dienst
- [ ] Test: eine Rolle ohne `freigeben` wird abgewiesen, über jede Server Action
- [ ] Test: Zurückweisen verlangt eine Notiz und setzt den Lauf auf `IN_ARBEIT`
- [ ] Test: die Meldung geht an die Rollen mit dem Recht und nicht an alle
- [ ] Test: ein Lauf **ohne** Freigabepflicht verhält sich unverändert — bestehende Läufe und
Vorlagen merken nichts
- [ ] Am Handy kein horizontales Scrollen
+68
View File
@@ -0,0 +1,68 @@
# #041 M25 – PDF-Übergabeprotokoll mit Unterschrift
**Status:** offen
**Im Tracker:** –
**Meilenstein:** v0.3.0
**Labels:** phase-3, backend, frontend
**Geschätzt:** 8 h **Tatsächlich:** –
## Ziel
Wer ein Notebook übergibt, braucht ein Blatt, das der Empfänger unterschreibt — und das danach
auffindbar ist. Heute gibt es die Druckansicht eines Laufs (`/drucken/lauf/[id]`, #015): sie ist
hell, vollständig und als Vorstufe genau dafür gebaut. Was fehlt, ist die Datei.
Ein Browser-Ausdruck genügt nicht: er hängt am Drucker-Dialog des Betrachters, trägt dessen
Kopfzeilen und lässt sich nicht an ein Ticket anhängen.
## Zu entscheiden
- [ ] **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.**
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
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.
**`{{generiertes_passwort}}` erscheint auch hier nicht** — die Druckansicht zeigt
`[Passwort – nicht gespeichert]`, und das PDF übernimmt das.
## Aufgaben
- [ ] `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,
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,
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
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:
**eine Meldung, kein stilles leeres PDF**
- [ ] Anleitung Kapitel 02 und 10 (Archiv)
## Akzeptanzkriterien
- [ ] 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
Obergrenze gilt)
- [ ] Ein Blatt ist ausgedruckt und gegengesehen worden — das ist eine Abnahme von Hand und steht
als solche im Devlog
+62
View File
@@ -0,0 +1,62 @@
# #042 M26 – Weitere Schritt-Typen
**Status:** offen
**Im Tracker:** –
**Meilenstein:** v0.3.0
**Labels:** phase-3, backend, frontend
**Geschätzt:** 6 h **Tatsächlich:** –
## Ziel
`SchrittTyp` hat heute sechs Werte: `HAKEN TEXT ZAHL AUSWAHL DATUM BEFEHL`. Das Enum und das
Json-Feld `konfig` sind seit #006 genau dafür gebaut, dass neue Typen ohne Migration der
bestehenden dazukommen.
Die Frage ist nicht, **wie** ein Typ dazukommt, sondern **welche** — und das Issue nennt sie
absichtlich noch nicht als beschlossen: ein Typ, der im Editor steht und in keiner Vorlage
auftaucht, ist ungenutzter und damit ungeprüfter Code.
## Zu entscheiden
- [ ] **Welche Typen?** Vorschlag, nach Nutzen geordnet, jeder mit dem Fall, der ihn verlangt:
| Typ | Der Fall | Ohne ihn |
|---|---|---|
| `DATEI` | „Foto des Typenschilds“ als **Pflicht** | ein Anhang ist seit #032 möglich, aber nie verlangt — ein Lauf schließt ohne ihn ab |
| `UNTERSCHRIFT` | Übergabe mit dem Finger auf dem Handy | das Blatt muss gedruckt und wieder eingescannt werden ([#041](041-pdf-uebergabeprotokoll.md)) |
| `MEHRFACHAUSWAHL` | „welche Lizenzen zugewiesen“ | heute ein Textfeld, also nicht auswertbar |
| `URL` | „Link zum Ticket“ | ein Textfeld ohne Prüfung und ohne Klick |
| `UEBERSCHRIFT` | eine Zwischenüberschrift ohne Zustand | ein `HAKEN`, den jemand abhaken muss, damit der Lauf fertig wird |
**Empfehlung: `DATEI` und `UNTERSCHRIFT`**, und die drei anderen nur, wenn eine Seed-Vorlage
sie wirklich benutzt. `UEBERSCHRIFT` ist der zweifelhafteste: ein Schritt ohne Zustand bricht
die Annahme „jeder Schritt hat ein `Schrittergebnis`“, und die steckt an vielen Stellen.
- [ ] **Bleibt `Schrittergebnis.wert` ein Json?** Ja — es ist seit #006 eines, genau dafür. Eine
Mehrfachauswahl ist ein Array, eine Unterschrift ein Verweis auf einen `Anhang`.
- [ ] **Wie verträgt sich `DATEI` mit dem Archiv-Schreibschutz?** Ein Pflicht-Anhang muss vor dem
Abschluss da sein; danach kommt keiner mehr weg (#032). Das ist bereits so und braucht nur
einen Test.
## Aufgaben
- [ ] Je gewählten Typ: Enum-Wert, Zod-Schema für `konfig` und `wert`, Darstellung im Editor,
Darstellung in der Abarbeitung, Darstellung im Druck und im PDF
- [ ] Die Abschlussregel kennt den neuen Typ: was zählt als „erledigt“? Bei `DATEI` ein Anhang, bei
`UNTERSCHRIFT` ein Bild, bei `MEHRFACHAUSWAHL` mindestens eine Wahl
- [ ] `UNTERSCHRIFT`: Zeichenfläche am Handy (Pointer-Ereignisse, nicht Touch-spezifisch), Ergebnis
als PNG über den Anhänge-Weg aus #032 — **keine neue Ablage**
- [ ] Export/Import und die Vorlagen-Austauschform um die neuen Typen erweitern, nachsichtig: eine
Datei mit einem unbekannten Typ wird abgewiesen **mit Nennung des Typs**, nicht stillschweigend
verändert
- [ ] Mindestens eine Seed-Vorlage benutzt jeden neuen Typ — sonst ist er ungeprüfter Code
- [ ] Anleitung Kapitel 04, mit dem Satz, wann ein Typ das richtige Mittel ist
## Akzeptanzkriterien
- [ ] Test je Typ: anlegen, abarbeiten, abschließen, drucken
- [ ] Test: ein `DATEI`-Pflichtschritt ohne Anhang hält den Abschluss auf
- [ ] Test: eine Unterschrift landet als `Anhang` und nicht als Json-Blob in `wert`
- [ ] Test: ein Lauf einer **älteren** Version verhält sich unverändert — die neuen Typen tauchen
dort nicht auf, und keine Migration hat Daten angefasst
- [ ] Test: eine Austauschdatei mit unbekanntem Typ wird abgewiesen und nennt den Typ
- [ ] Am Handy bedienbar, bei 390 px geprüft — besonders die Zeichenfläche
+5 -1
View File
@@ -43,4 +43,8 @@ Diese Dateien sind die Quelle; `pnpm issues:sync` spiegelt sie in den Gitea-Trac
| #036 | [Reviewbefunde aus der Durchsicht von Phase 2](036-reviewbefunde-phase-2.md) | erledigt | v0.2.0 | 4 h | 3 h 40 min | | #036 | [Reviewbefunde aus der Durchsicht von Phase 2](036-reviewbefunde-phase-2.md) | erledigt | v0.2.0 | 4 h | 3 h 40 min |
| #037 | [Zwei Nachzügler aus der Durchsicht von Phase 2](037-anhaenge-lesemodell.md) | erledigt | v0.2.0 | 2 h | 1 h 50 min | | #037 | [Zwei Nachzügler aus der Durchsicht von Phase 2](037-anhaenge-lesemodell.md) | erledigt | v0.2.0 | 2 h | 1 h 50 min |
| #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 | | #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 |
| | **Summe** | | | **238 h** | **73 h 55 min** | | #039 | [M23 – Bedingte Schritte und Abschnitte](039-bedingte-schritte.md) | offen | v0.3.0 | 12 h | – |
| #040 | [M24 – Vier-Augen-Freigabe](040-vier-augen-freigabe.md) | offen | v0.3.0 | 8 h | – |
| #041 | [M25 – PDF-Übergabeprotokoll mit Unterschrift](041-pdf-uebergabeprotokoll.md) | offen | v0.3.0 | 8 h | – |
| #042 | [M26 – Weitere Schritt-Typen](042-weitere-schritt-typen.md) | offen | v0.3.0 | 6 h | – |
| | **Summe** | | | **272 h** | **73 h 55 min** |
+55 -1
View File
@@ -141,7 +141,7 @@ function feld(text, name) {
// README fehlte `format:pruefen`, und beide nannten den LDAP-Container nicht, ohne // README fehlte `format:pruefen`, und beide nannten den LDAP-Container nicht, ohne
// den die Suite 22 Tests still ueberspringt. Beides fiel erst bei der Abnahme zu // den die Suite 22 Tests still ueberspringt. Beides fiel erst bei der Abnahme zu
// #019 auf -- ein gruener Lauf sieht genauso aus. Geprueft wird deshalb der // #019 auf -- ein gruener Lauf sieht genauso aus. Geprueft wird deshalb der
// Befehlsblock selbst, nicht seine Umgebung (#040). // Befehlsblock selbst, nicht seine Umgebung (#026).
{ {
/** Der erste eingezaeunte Block nach einer Ueberschrift. */ /** Der erste eingezaeunte Block nach einer Ueberschrift. */
const blockNach = (text, ueberschrift) => { const blockNach = (text, ueberschrift) => {
@@ -197,6 +197,60 @@ function feld(text, name) {
} }
} }
// ---- 7. Jeder (#NNN)-Verweis zeigt auf eine Issue-Datei --------------------
{
/*
* Verweise wie `(#032)` stehen in Kommentaren, im Changelog und in CLAUDE.md
* und sind die einzige Bruecke zwischen einer Codestelle und ihrer
* Begruendung. Zeigt einer ins Leere, ist die Begruendung verloren — und
* schlimmer: zeigt er auf die **falsche** Datei, fuehrt er jemanden in die
* Irre, der ihm vertraut.
*
* Genau das war passiert: vier Codestellen und sechs Changelog-Zeilen nannten
* dreistellig die **Tracker**-Nummern 39 und 40 — gemeint waren die
* Issue-Dateien 025 und 026. Solange es keine Dateien mit diesen Nummern gab,
* fiel es niemandem auf; mit Phase 3 waere es eine falsche Spur geworden.
* Verbindlich ist die **Dateinummer**; die Tracker-Nummer steht in der
* Issue-Datei unter „Im Tracker“.
*
* Diese Datei nennt deshalb selbst keine Nummer in der dreistelligen Form —
* sonst meldete die Pruefung ihren eigenen Kommentar.
*/
const verz = "docs/issues";
const nummern = new Set(
readdirSync(join(wurzel, verz))
.filter((n) => /^\d{3}-.+\.md$/.test(n))
.map((n) => n.slice(0, 3)),
);
const sammeln = (ordner) =>
readdirSync(ordner, { withFileTypes: true }).flatMap((eintrag) => {
if (["node_modules", ".git", ".next", "generated", "test-results"].includes(eintrag.name)) {
return [];
}
const pfad = join(ordner, eintrag.name);
if (eintrag.isDirectory()) return sammeln(pfad);
return /\.(ts|tsx|mjs|md|sql)$/.test(eintrag.name) ? [pfad] : [];
});
for (const datei of sammeln(wurzel)) {
const kurz = datei.replace(`${wurzel}/`, "");
// Der Devlog zitiert Tracker-Nummern aus Pull Requests; dort gilt die Regel nicht.
if (kurz === "docs/devlog.md") continue;
const text = readFileSync(datei, "utf8");
const gemeldet = new Set();
for (const treffer of text.matchAll(/#(\d{3})\b/g)) {
const nummer = treffer[1];
if (nummern.has(nummer) || gemeldet.has(nummer)) continue;
gemeldet.add(nummer);
melde(
`${kurz} verweist auf #${nummer}, aber ${verz}/${nummer}-*.md gibt es nicht. ` +
`Gemeint ist vielleicht eine Tracker-Nummer — verbindlich ist die Dateinummer.`,
);
}
}
}
// ---- Ergebnis -------------------------------------------------------------- // ---- Ergebnis --------------------------------------------------------------
if (befunde.length > 0) { if (befunde.length > 0) {
console.error(`\nDokumentation und Code laufen auseinander (${befunde.length}):\n`); console.error(`\nDokumentation und Code laufen auseinander (${befunde.length}):\n`);
+1 -1
View File
@@ -10,7 +10,7 @@ import { prisma } from "@/lib/db";
* Datenbanknamen. Nach aussen genuegt, **ob** es geht; warum nicht, steht im * Datenbanknamen. Nach aussen genuegt, **ob** es geht; warum nicht, steht im
* Protokoll des Prozesses (`docker compose logs web`, * Protokoll des Prozesses (`docker compose logs web`,
* `journalctl -u laufzettel`). Dasselbe Muster wie in * `journalctl -u laufzettel`). Dasselbe Muster wie in
* `src/server/actions/verzeichnis.ts` (#039). * `src/server/actions/verzeichnis.ts` (#025).
*/ */
export async function GET(): Promise<Response> { export async function GET(): Promise<Response> {
try { try {
+1 -1
View File
@@ -149,7 +149,7 @@ export async function tokenHolen(zugang: GraphZugang, jetzt = Date.now()): Promi
/* /*
* Die ausführliche Meldung ins Protokoll des Prozesses, eine kurze nach * Die ausführliche Meldung ins Protokoll des Prozesses, eine kurze nach
* draußen. Dieselbe Trennung wie beim LDAP-Bind-Fehler (#016) und beim * draußen. Dieselbe Trennung wie beim LDAP-Bind-Fehler (#016) und beim
* Healthcheck (#039): eine Meldung, die Mandant und Anwendungs-ID nennt, * Healthcheck (#025): eine Meldung, die Mandant und Anwendungs-ID nennt,
* gehört nicht in eine Oberfläche. * gehört nicht in eine Oberfläche.
*/ */
console.error( console.error(
+1 -1
View File
@@ -46,7 +46,7 @@ export function alsScimAntwort(fehler: unknown): Response {
/* /*
* Alles andere ist ein Fehler bei uns, nicht beim Anbieter. Die Meldung geht * Alles andere ist ein Fehler bei uns, nicht beim Anbieter. Die Meldung geht
* ins Protokoll des Prozesses und **nicht** in die Antwort: eine * ins Protokoll des Prozesses und **nicht** in die Antwort: eine
* Datenbankmeldung nennt Host, Port und Benutzernamen (#039). * Datenbankmeldung nennt Host, Port und Benutzernamen (#025).
*/ */
console.error( console.error(
"SCIM-Anfrage fehlgeschlagen:", "SCIM-Anfrage fehlgeschlagen:",
+1 -1
View File
@@ -1,7 +1,7 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
/** /**
* Der Healthcheck (#039). * Der Healthcheck (#025).
* *
* Dieser Pfad ist der einzige, der ohne Anmeldung **und** mit Datenbankzugriff * Dieser Pfad ist der einzige, der ohne Anmeldung **und** mit Datenbankzugriff
* antwortet. Geprueft wird deshalb nicht, dass er funktioniert — das tun der * antwortet. Geprueft wird deshalb nicht, dass er funktioniert — das tun der