fix: Reviewbefunde aus der Durchsicht von Phase 2 (#036, #027)
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s

Acht Meilensteine lagen ohne fremden Blick auf main. Ein Review mit eigener
Datenbank, Gegenproben und vollem Suitenlauf hat sie gegen die
Akzeptanzkriterien und gegen CLAUDE.md geprueft.

Die Kernregeln halten: fuenf von fuenf Gegenproben gegen Versionierung,
Mandantengrenze, Abschlussregel und Selbstauskunft wurden rot. Alle Befunde
lagen am Rand — und die drei wertvollsten waren Gegenproben, die gruen blieben.

Schwer:

- Der Link in der Mail "Verzeichnis-Abgleich fehlgeschlagen" war ein 404
  (/verwaltung/verzeichnis statt /admin/verzeichnis). Die eine Meldung, die an
  alle Administratoren geht, im Moment eines Ausfalls. Ein Test prueft jetzt
  jeden Meldungspfad gegen die vorhandenen Routen.
- Der Export eines Entwurfs mit Baustein lieferte einen leeren Abschnitt unter
  dem Platzhalter "wird ersetzt" — der Zweck des Formats ist, eine Vorlage in
  eine zweite Installation zu bringen. Bausteine werden jetzt aufgeloest.

Mittel:

- Der Anhang-Handler hatte keinen Test; die ganze Rechtepruefung liess sich
  durch einen hartcodierten ADMIN ersetzen, Suite gruen. Es ist die einzige
  Stelle, die rohe Dateibytes ausliefert. tests/anhang-route.test.ts faehrt ihn
  jetzt echt, acht Pruefungen.
- /api/scim in der Proxy-Allowlist stand in keinem Test. OEFFENTLICH ist
  exportiert, der Test vergleicht beide Richtungen: eine Liste, die nur in einer
  Richtung geprueft wird, laeuft dem Code hinterher.
- Ein offener Ereignisstrom ueberlebte Abmelden, Deaktivieren und
  Sitzungswiderruf. Der Herzschlag prueft die Sitzung jetzt nach. Dass es kein
  Test zeigte, lag an einer Attrappe `sitzung: {}` — eine Attrappe ohne die Form
  der Wirklichkeit prueft etwas anderes als die Wirklichkeit.
- README kannte MITARBEITER nicht und behauptete, unbekannte Benutzer wuerden
  angelegt. Wer auf v0.2.0 hebt und nur das README liest, haette genau diese
  Leute ausgesperrt.
- sicherung.md nannte einen Anhaenge-Pfad, den es nirgends gibt — als
  Rueckfallwert in einem Skript mit rm -rf. Jetzt bricht es ab statt zu raten.
- Attributnamen wurden nur in der Server Action geprueft, waehrend der
  Graph-Leser sich im Kommentar darauf berief.

Dazu #027: die Ende-zu-Ende-Ablaeufe raeumen jetzt zentral auf, vor und nach
dem Lauf, und ein struktureller Test bewacht es. Beim ersten echten Lauf seit
dem 26. September waren sieben Ablaeufe rot, vier Ursachen, keine ein
Produktfehler — unbemerkt, weil pnpm test:e2e in keinem Tor steht. Das ist jetzt
#038 und keine Fussnote.

Ein Befund des Reviews war falsch (der Zustands-Handler habe keinen Test; er hat
acht direkte Aufrufe). Nachgeprueft, bevor etwas geaendert wurde.

1454 Vitest-Tests (vorher 1432), 113 E2E-Ablaeufe gruen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-10-11 01:32:09 +02:00
co-authored by Claude Opus 5
parent fcd67018b8
commit 80d97467da
37 changed files with 1752 additions and 59 deletions
+22 -5
View File
@@ -17,7 +17,7 @@ Drei Dinge, und nur diese drei:
|---|---|---|
| **1** | **Die Datenbank.** Läufe, Vorlagen, Schrittergebnisse, Kommentare, Geräte, Personen, Tag-Codes, Protokoll. | `pg_dump`, siehe unten |
| **2** | **Die Geheimnisse.** `APP_SECRET` und das Datenbank-Passwort. | Compose: die `.env` · Nativ: `/etc/laufzettel/laufzettel.env` |
| **3** | **Die Anhänge.** Die Dateien und Fotos an den Schritten. | Volume `laufzettel_anhaenge` · Nativ: `ANHAENGE_PFAD` |
| **3** | **Die Anhänge.** Die Dateien und Fotos an den Schritten. | Volume `laufzettel_anhaenge` · Nativ: `ANHAENGE_PFAD`, von `install.sh` auf `/var/lib/laufzettel/anhaenge` gesetzt |
> **Die Anhänge sind seit #032 dabei, und sie sind die Falle.** In der Datenbank stehen nur ihre
> **Metadaten** — Name, Typ, Größe, Prüfsumme. Die Dateien selbst liegen im Dateisystem. Eine
@@ -157,11 +157,23 @@ sudo -u postgres pg_dump -Fc laufzettel > /var/backups/laufzettel-$(date +%F).du
Und die Anhänge dazu — gewöhnliche Dateien, also `tar`:
```bash
# Der Pfad steht in ANHAENGE_PFAD; ohne Angabe liegt er unter /opt/laufzettel/daten/anhaenge.
ANHAENGE=$(sudo sh -c '. /etc/laufzettel/laufzettel.env; echo "${ANHAENGE_PFAD:-/opt/laufzettel/daten/anhaenge}"')
# Der Pfad steht in ANHAENGE_PFAD; `deploy/install.sh` setzt ihn auf
# /var/lib/laufzettel/anhaenge. **Kein Rückfallwert** – siehe den Kasten darunter.
ANHAENGE=$(sudo sh -c '. /etc/laufzettel/laufzettel.env; echo "${ANHAENGE_PFAD:?ANHAENGE_PFAD fehlt in /etc/laufzettel/laufzettel.env}"')
sudo tar czf /var/backups/laufzettel-anhaenge-$(date +%F).tar.gz -C "$ANHAENGE" .
```
> **Warum hier kein `${ANHAENGE_PFAD:-…}` steht.** Es stand einmal eines, und es war falsch:
> `/opt/laufzettel/daten/anhaenge` passt zu keiner Installation. `deploy/install.sh` setzt
> `/var/lib/laufzettel/anhaenge`, und ohne die Variable nimmt die Anwendung `./daten/anhaenge`
> **relativ zu ihrem Arbeitsverzeichnis** `/opt/laufzettel/.next/standalone` — also noch etwas
> anderes.
>
> Ein geratener Pfad ist beim Sichern eine leere Datei und beim Zurückspielen ein Schaden: dort
> steht ein `rm -rf` davor. Fehlt die Variable, soll der Befehl **abbrechen** und nicht raten.
> Deshalb `:?` und nicht `:-`. Gefunden bei der Durchsicht von Phase 2, nachdem derselbe Fehler in
> dieser Datei schon einmal Daten gekostet hätte (#032).
Und das Geheimnis, einmalig und in den Passwortmanager:
```bash
@@ -187,7 +199,10 @@ sudo -u postgres pg_restore -d laufzettel --no-owner /var/backups/laufzettel-202
# 4. Die Anhänge zurück – **vorher leeren**, sonst bleiben Dateien liegen, auf
# die keine Zeile mehr zeigt
ANHAENGE=$(sudo sh -c '. /etc/laufzettel/laufzettel.env; echo "${ANHAENGE_PFAD:-/opt/laufzettel/daten/anhaenge}"')
# Ohne ANHAENGE_PFAD bricht das hier ab – und zwar **vor** dem rm -rf. Ein
# geratener Pfad hiesse: das echte Verzeichnis bleibt leer, die Anwendung meldet
# „Die Datei zu … fehlt im Dateisystem“, und niemand weiss, warum.
ANHAENGE=$(sudo sh -c '. /etc/laufzettel/laufzettel.env; echo "${ANHAENGE_PFAD:?ANHAENGE_PFAD fehlt in /etc/laufzettel/laufzettel.env}"')
sudo rm -rf "$ANHAENGE"
sudo install -d -o laufzettel -g laufzettel -m 0750 "$ANHAENGE"
sudo tar xzf /var/backups/laufzettel-anhaenge-2026-09-24.tar.gz -C "$ANHAENGE"
@@ -247,7 +262,9 @@ runuser -u postgres -- pg_dump -Fc laufzettel > "/var/backups/laufzettel-$tag.du
# shellcheck source=/dev/null
. /etc/laufzettel/laufzettel.env
anhaenge="${ANHAENGE_PFAD:-/opt/laufzettel/daten/anhaenge}"
# Kein Rueckfallwert: ein geratener Pfad ergibt eine leere Sicherung, und die
# faellt erst beim Zuruckspielen auf.
anhaenge="${ANHAENGE_PFAD:?ANHAENGE_PFAD fehlt in /etc/laufzettel/laufzettel.env}"
if [ -d "$anhaenge" ]; then
tar czf "/var/backups/laufzettel-anhaenge-$tag.tar.gz" -C "$anhaenge" .
fi
+82 -2
View File
@@ -36,12 +36,14 @@ als Unterpunkte mit Aufgabe und Dauer.
| #035 | M22 – Mitarbeiter-Selbstauskunft | 2 h 50 min |
| #028 | M15 – Quellenarten und Microsoft-Graph-Quelle | 4 h 10 min |
| #029 | M16 – SCIM-Endpunkt als Alternative zum Abgleich | 4 h 05 min |
| | **Summe** | **64 h 40 min** |
| #036 | Reviewbefunde aus der Durchsicht von Phase 2 | 3 h 40 min |
| #027 | Die Editor-Abläufe lassen ihre Vorlagen stehen | 2 h 10 min |
| | **Summe** | **70 h 30 min** |
| Meilenstein | Tatsächlich | Status |
|---|---|---|
| v0.1.0 | 35 h 20 min | veröffentlicht |
| v0.2.0 | 29 h 20 min | in Arbeit |
| v0.2.0 | 35 h 10 min | in Arbeit |
---
@@ -1531,3 +1533,81 @@ stehen in `docs/betrieb/scim.md`.
fremden Blick auf `main`, und diese Sitzung hat wieder zwei Dinge gezeigt, die eine grüne Suite
nicht sieht: einen Schalter, der eine Änderung verschluckte, und einen Zweig, der nie erreicht
wurde. Beide Befunde kamen aus Gegenproben, nicht aus Tests.
---
## 2026-10-11 · Sitzung 23 · 21:50–01:30
**Issues:** #036 (Reviewbefunde Phase 2), #027 (E2E-Reste)
**Version:** 0.2.0 (unveröffentlicht)
### Erledigt
- **Der unabhängige Review ist gelaufen** — acht Meilensteine (M15–M22) lagen ohne fremden Blick auf
`main`, und das stand seit Sitzung 21 als „überfällig“ im Devlog. Ein Agent mit eigener Datenbank,
der den Code nicht geschrieben hat, gegen die Akzeptanzkriterien und gegen `CLAUDE.md`.
- **Dreizehn Befunde, zwölf behoben**, zwei als #037 und #038 benannt. Alle lagen am Rand: zwei
Route Handler, ein Export-Pfad, Betriebsdokumentation, Kommentare. **Die Kernregeln halten** —
fünf von fünf Gegenproben gegen Versionierung, Mandantengrenze, Abschlussregel und
Selbstauskunft wurden rot.
- **#027 abgeschlossen**: die 105 Altreste sind weg, geräumt wird jetzt zentral vor und nach dem
Lauf, und ein struktureller Test bewacht es.
- 1454 Vitest-Tests (vorher 1432), 113 E2E-Abläufe grün.
### Entscheidungen
- **Eine Liste, die nur in einer Richtung geprüft wird, läuft dem Code hinterher.** `/api/scim` kam
mit #029 in die Proxy-Allowlist und stand in keinem Test; die Zeile ließ sich entfernen, ohne
dass etwas rot wurde. `OEFFENTLICH` ist jetzt exportiert, und der Test vergleicht **beide**
Richtungen: wer die Allowlist erweitert, muss es aufschreiben. Dasselbe Muster wie die Zahlen in
`CLAUDE.md`, die dreimal der Wirklichkeit nachliefen.
- **Ein Rückspielskript rät nicht.** In `sicherung.md` stand `${ANHAENGE_PFAD:-/opt/…}` — ein
Rückfallwert, der zu keiner Installation passte, in einem Skript mit `rm -rf`. Jetzt `:?`: fehlt
die Variable, bricht es ab. Ein geratener Pfad ist beim Sichern eine leere Datei und beim
Zurückspielen ein Schaden.
- **Die Zugweite eines Sortiertests wird gemessen, nicht gesetzt.** Eine feste Pixelzahl ist eine
Zusage über das Layout, die niemand pflegt — und sie war falsch, seit #030 und #031 jeden
Abschnitt höher gemacht haben.
- **Protokoll-Waisen werden nicht erraten.** Eine Zeile, deren Gegenstand fort ist, über den Namen
im Json zu greifen wäre naheliegend und falsch: derselbe Name steht in echter Historie einer noch
bestehenden Zeile. Dass eine Protokollzeile ihren Gegenstand überlebt, ist der Grund, warum
`entitaetId` kein Fremdschlüssel ist.
### Probleme
- **Die drei wertvollsten Befunde waren Gegenproben, die grün blieben.** Das ist inzwischen das
wiederkehrende Muster dieses Projekts: nicht der falsche Code fällt auf, sondern der Code, der
*aussieht*, als prüfte er etwas.
- Der **Link in der Abgleich-Meldung** war ein 404 (`/verwaltung/verzeichnis` statt
`/admin/verzeichnis`) — die eine Meldung, die an alle Administratoren geht, im Moment eines
Ausfalls. Der bestehende Test prüfte, dass der **übergebene** Link im Rumpf steht. Das ist
richtig und sagt nichts darüber, ob der Pfad existiert.
- Der **Anhang-Handler** hatte keinen einzigen Test. Die ganze Rechteprüfung ließ sich durch einen
hartcodierten ADMIN ersetzen, Suite grün. Das ist die einzige Stelle, die rohe Dateibytes
ausliefert. Bei SCIM war das Muster schon da — hier wurde es nicht angewandt.
- **`/api/scim`** in der Allowlist, siehe oben.
- **Ein offener Ereignisstrom überlebte das Deaktivieren eines Kontos.** Gefunden durch Lesen, nicht
durch einen Test. Dabei fiel auf, warum: die Attrappe der Sitzung war `{}` — ohne `benutzerId`
ließe sich eine Nachprüfung nicht einmal zeigen. **Eine Attrappe, die nicht die Form der
Wirklichkeit hat, prüft etwas anderes als die Wirklichkeit.** Dieselbe Lehre wie beim
Graph-Doppelgänger in #028, nur von der anderen Seite.
- **Die E2E-Suite war seit Monaten rot**, vier Ursachen, keine ein Produktfehler: ein Test, der die
Felder des Zustands aufzählt und `anzahlAnhaenge` nicht kannte (#033); ein Selektor, der seit #020
zweimal traf; eine feste Zugweite von 300 px bei 532 px Abstand (#030/#031); und
Entwicklungsdaten, die vom Seed abwichen. Niemand hat es gemerkt, weil `pnpm test:e2e` in keinem
Tor steht — das ist jetzt #038 und keine Fußnote.
- **Ein Befund des Reviews war falsch:** der Zustands-Handler habe keinen Vitest-Test. Er hat acht
direkte Aufrufe. Nachgeprüft, bevor etwas geändert wurde — ein Agentenbericht ist eine Behauptung,
keine Feststellung.
- **Ein `DELETE` von 105 Zeilen wurde abgelehnt**, und das war am Ende richtig: der im Issue
vorgesehene Weg erledigte es von selbst, weil das Aufräumen über das Präfix beim ersten Lauf auch
die Altlast mitnimmt. Eine Sicherung der Entwicklungsdatenbank lag vorher.
### Nächster Schritt
- Offen bleiben **#024** (Proxmox-Einzeiler auf echtem PVE-Host), **#037** (zwei Nachzügler) und
**#038** (E2E in ein Tor).
- Danach ist v0.2.0 reif für einen Tag. Vorher gehört #038 entschieden: ein Release, dessen
E2E-Suite nur gelegentlich läuft, ist ein Release mit ungeprüften Zusagen — und genau das hat
diese Sitzung gezeigt.
+63 -8
View File
@@ -1,10 +1,10 @@
# #027 Die Editor-Abläufe lassen ihre Vorlagen stehen
**Status:** offen
**Status:** erledigt
**Im Tracker:** [#46](https://git.wilde-server.de/florian.wilde/Laufzettel/issues/46)
**Meilenstein:** v0.2.0
**Labels:** phase-2, test
**Geschätzt:** 1 h **Tatsächlich:** –
**Geschätzt:** 1 h **Tatsächlich:** 2 h 10 min
## Ziel
@@ -22,14 +22,69 @@ Vorlagenliste in der Oberfläche, Screenshots, und jede Abfrage von Hand. Bei ei
## Aufgaben
- [ ] Die Abläufe unter `tests/e2e/`, die Vorlagen anlegen, räumen sie in einem Nachlauf wieder weg —
- [x] Die Abläufe unter `tests/e2e/`, die Vorlagen anlegen, räumen sie in einem Nachlauf wieder weg —
über ihr Präfix, nicht über gemerkte Kennungen (ein abgebrochener Lauf merkt sich nichts)
- [ ] Dasselbe für Geräte, Personen und Läufe prüfen, die die Abläufe anlegen
- [ ] Einmalig aufräumen: die vorhandenen Reste aus der Entwicklungsdatenbank entfernen
- [ ] Prüfen, ob ein Nachlauf das überhaupt leisten kann — ein Ablauf, der mitten im Lauf scheitert,
- [x] Dasselbe für Geräte, Personen und Läufe prüfen, die die Abläufe anlegen
- [x] Einmalig aufräumen: die vorhandenen Reste aus der Entwicklungsdatenbank entfernen
- [x] Prüfen, ob ein Nachlauf das überhaupt leisten kann — ein Ablauf, der mitten im Lauf scheitert,
darf den Nachlauf nicht überspringen
## Akzeptanzkriterien
- [ ] Nach `pnpm test:e2e` steht in `Vorlage` keine Zeile mit `ZZ E2E`
- [ ] Ein abgebrochener Lauf lässt ebenfalls nichts stehen
- [x] Nach `pnpm test:e2e` steht in `Vorlage` keine Zeile mit `ZZ E2E`
- [x] Ein abgebrochener Lauf lässt ebenfalls nichts stehen
## Beim Umsetzen dazugekommen
- **Die Beschreibung oben war zur Hälfte überholt.** „Bei jedem Lauf kommen drei dazu“ stimmte beim
Schreiben, aber nicht mehr: alle 105 Reste stammen aus einem Fenster von sechzehn Stunden am
25./26. September, und seither ist keine dazugekommen. `tests/e2e/vorlagen-editor.spec.ts` räumt
seit [#018](018-e2e-anleitung-pdf.md) über das Präfix auf, und das hält. Nachgewiesen an den
Zeitstempeln der Zeilen, nicht vermutet.
- **Dafür gab es eine neue Lücke, an anderer Stelle:** `anhaenge.spec.ts` (#032) und
`live.spec.ts` (#033) legen `ZZ E2E`-Läufe an und hatten **keinen** Nachlauf. In der
Entwicklungsdatenbank stand von ihnen nichts — nicht weil sie aufräumten, sondern weil die
E2E-Suite dort seit dem 26. September nicht mehr gelaufen war.
- **Geräumt wird jetzt zentral und an einer Stelle**: `tests/e2e/aufraeumen.ts`. Ein `afterAll` je
Datei ist die falsche Form für diese Zusage — beim nächsten Meilenstein fehlt wieder einer.
- **Vor *und* nach dem Lauf.** Ein `globalTeardown` läuft bei Testfehlern und bei Zeitabläufen, aber
nicht bei `Strg+C`. Der Vorlauf fängt genau das: ein Vorlauf lässt sich nicht überspringen. Er
**sagt es laut** („Rückstand aus einem früheren Lauf: …“), denn still wegzuräumen hieße, die
Ursache zu verdecken.
- **Das Präfix ist `ZZ` und nicht `ZZ E2E`.** Die Abläufe benutzen es in fünf Schreibweisen
(`ZZ E2E Editor …`, `ZZ-E2E-<zeit>-PC-01` — ein Hostname darf kein Leerzeichen haben —,
`ZZPruef…`, `ZZQuelle…`, `ZZ Abteilung …`). Alle fünf sind sinnvoll, alle fünf beginnen mit `ZZ`.
Darauf zu greifen deckt sie ab, ohne sechzehn Dateien umzubenennen.
- **Ein struktureller Test bewacht das** (`tests/betriebsstand.test.ts`): er liest die Abläufe und
schlägt an, wenn einer Zeilen anlegt, ohne das Präfix zu benutzen — und prüft, dass Vor- und
Nachlauf registriert sind. Er fand beim ersten Durchgang sofort zwei Dateien, die ich übersehen
hätte.
- **Protokoll-Waisen werden nicht erraten.** Eine Zeile, deren Gegenstand schon fort ist, bleibt
stehen; sie über den Namen im Json zu greifen wäre naheliegend und falsch, weil derselbe Name in
echter Historie einer noch bestehenden Zeile steht. Dass eine Protokollzeile ihren Gegenstand
überlebt, ist der Grund, warum `entitaetId` kein Fremdschlüssel ist.
## Was der erste echte Lauf zutage brachte
Der E2E-Durchgang war die ganze Zeit **rot**, und niemand hat es gemerkt — `pnpm test:e2e` steht
nicht im Tor vor dem Commit (Reviewbefund zu [#029](029-scim-endpunkt.md)). Sieben Fehlschläge, vier
Ursachen, keine davon ein Produktfehler, alle vier behoben:
- `laeufe.spec.ts` zählte die Felder des Zustands auf und kannte `anzahlAnhaenge` nicht — das kam mit
#033 dazu. Die Aufzählung ist Absicht und bleibt: der Zustand ist die Antwort, die am häufigsten
hinausgeht, und ein Feld, das sich einschleicht, wandert ungesehen mit.
- `stammdaten.spec.ts` suchte „kommt aus dem Verzeichnis“ — seit #020 steht derselbe Satz auch am
Vorgesetzten-Feld, also zweimal auf der Seite. Playwright wies das als „strict mode violation“ ab.
- `vorlagen-editor.spec.ts` zog am Handy feste 300 px. Der Abstand zwischen den Anfassern ist
**532 px**, seit mit #030 und #031 Bausteinverweis und Abteilungsfilter in jeden Abschnitt kamen —
gemessen, nicht geschätzt. Dazu lag der erste Anfasser bei `y = -115`, also außerhalb des
Sichtfensters: der Touch-Start traf nichts. Jetzt wird erst gescrollt und die Zugweite aus dem
Layout genommen. Eine feste Pixelzahl in einem Sortiertest ist eine Zusage über das Layout, die
niemand pflegt.
- `onboarding.spec.ts` prüft den `New-ADUser`-Befehl zeichengenau und scheiterte an
`DC=firma,DC=de` statt `DC=firma,DC=local`: die **Entwicklungsdaten** wichen vom Seed ab, und der
Seed setzt bestehende Abteilungen absichtlich nicht zurück. Der Vorlauf prüft das jetzt und sagt,
was zu tun ist — statt einer Meldung, die nach einem Fehler im Befehlsbau aussieht.
**Daraus folgt ein eigenes Issue:** `pnpm test:e2e` gehört ins Tor oder mindestens in eine
nachvollziehbare Regel, sonst wiederholt sich das. Siehe [#038](038-e2e-im-tor.md).
+136
View File
@@ -0,0 +1,136 @@
# #036 Reviewbefunde aus der Durchsicht von Phase 2
**Status:** erledigt
**Im Tracker:** –
**Meilenstein:** v0.2.0
**Labels:** phase-2, backend, test, docs
**Geschätzt:** 4 h **Tatsächlich:** 3 h 40 min
## Ziel
Acht Meilensteine von Phase 2 (M15–M22) lagen ohne fremden Blick auf `main`. Ein Review-Agent, der
den Code nicht geschrieben hat, hat sie gegen die Akzeptanzkriterien und gegen `CLAUDE.md` geprüft —
mit eigener Datenbank, Gegenproben und einem vollen Suitenlauf.
**Ergebnis:** Die Kernregeln halten. Fünf von fünf Gegenproben gegen Versionierung,
Mandantengrenze, Abschlussregel und Selbstauskunft wurden rot. Alle Befunde lagen am Rand — in zwei
Route Handlern, einem Export-Pfad, in Betriebsdokumentation und in Kommentaren.
**Die drei wertvollsten Befunde waren Gegenproben, die grün blieben.** Das ist das wiederkehrende
Muster dieses Projekts: nicht der falsche Code fällt auf, sondern der Code, der *aussieht*, als
prüfte er etwas.
## Behoben
### Schwer
- [x] **Der Link in der Mail „Verzeichnis-Abgleich fehlgeschlagen“ war ein 404.** Gebaut wurde
`/verwaltung/verzeichnis` — `verwaltung` ist der Name des **Rechts**, die Seite liegt unter
`/admin/verzeichnis`. Das ist die eine Meldung, die an **alle** Administratoren geht, und
zwar im Moment eines Ausfalls: der Empfänger klickt und landet auf der
Nicht-gefunden-Seite. Die Pfade stehen jetzt als `MELDUNGS_PFADE` an einer Stelle, und ein
Test prüft **jeden gegen die Routen unter `src/app/`** — ein Pfad, den es nicht gibt, fällt
beim Testlauf auf und nicht im Postfach.
- [x] **Der Export eines Entwurfs mit Baustein lieferte einen leeren Abschnitt**, unter dem
Editor-Platzhalter „wird ersetzt“, und der Import nahm das stillschweigend an. Ein Redakteur
bringt eine noch unveröffentlichte Vorlage in eine zweite Installation — genau der Zweck des
Formats —, und der geteilte Abschnitt kommt dort ohne einen einzigen Schritt an. Baustein-
Abschnitte werden beim Export jetzt **aufgelöst**, wie beim Veröffentlichen; der Verweis
wandert weiterhin nicht mit.
### Mittel
- [x] **Der Route Handler, der Anhänge ausliefert, hatte keinen einzigen Test.** Die ganze
Rechteprüfung ließ sich durch einen hartcodierten ADMIN ersetzen, ohne dass ein Test rot
wurde. Das ist die **einzige** Stelle der Anwendung, die rohe Dateibytes ausliefert. Neu:
`tests/anhang-route.test.ts` fährt den Handler echt, acht Prüfungen — darunter das
Akzeptanzkriterium von #032 („nicht abrufbar **über seine Adresse**“), das bis hierher nur
auf Dienstebene geprüft war. Die Gegenprobe ist jetzt dreifach rot.
- [x] **`/api/scim` in der Proxy-Allowlist stand in keinem Test.** Die Zeile ließ sich entfernen,
ohne dass etwas rot wurde — und ohne sie leitet der Proxy jede Zustellung auf `/anmelden` um:
der Anbieter meldet „Provisioning failed“, in Laufzettel steht nichts, das Provisioning hört
still auf zu arbeiten. `OEFFENTLICH` ist jetzt exportiert, und ein Test prüft **in beide
Richtungen**: wer die Liste erweitert, muss es im Test aufschreiben. Eine Aufzählung, die nur
in einer Richtung geprüft wird, läuft dem Code hinterher.
- [x] **Ein offener Ereignisstrom überlebte Abmelden, Deaktivieren und Sitzungswiderruf.** Das
Recht wurde genau einmal geprüft, beim Aufbau, und Kennung samt Rolle für die Lebensdauer des
Stroms festgehalten. Ein Administrator deaktiviert ein Konto — und eine offen gelassene
Lasche bekommt weiter live Status, Schrittzustände und Zählwerte, bis jemand sie schließt.
`docs/architektur.md` verspricht „ein deaktivierter Benutzer wird auch mit gültigem Cookie
abgewiesen“; für einen Strom galt das nicht. Der Herzschlag prüft jetzt nach
(`sitzungNochGueltig`), eine Herabstufung engt den Strom ein statt ihn zu beenden.
- [x] **`README.md` kannte die Rolle `MITARBEITER` nicht**, und ein Satz daneben war seit #035
falsch: „Unbekannte Benutzer werden beim ersten Login angelegt.“ Wer auf v0.2.0 hebt, liest
das README, nimmt an, für Leute ohne Rollengruppe bleibe alles wie bisher — und sperrt danach
genau diese Leute aus. #035 hatte den Satz in `docs/betrieb/authentik.md` richtiggestellt und
das README stehengelassen. Dazu: der Stand stand weiter auf „in Entwicklung auf v0.1.0“.
- [x] **`docs/betrieb/sicherung.md` nannte einen Anhänge-Pfad, den es nirgends gibt**
(`/opt/laufzettel/daten/anhaenge`) — als Rückfallwert in einem Skript, in dem ein `rm -rf`
steht. `deploy/install.sh` setzt `/var/lib/laufzettel/anhaenge`, und ohne die Variable nimmt
die Anwendung `./daten/anhaenge` relativ zu ihrem Arbeitsverzeichnis. Der Rückfall passte zu
**keinem** der drei. Jetzt `${ANHAENGE_PFAD:?…}`: fehlt die Variable, **bricht** der Befehl
ab, statt zu raten — vor dem `rm -rf`. Dasselbe Dokument hat denselben Fehler in #032 schon
einmal gehabt.
- [x] **Attributnamen wurden nur in der Server Action geprüft**, während sich
`src/server/graph/leser.ts` im Kommentar darauf berief („feste Feldnamen aus
`ATTRIBUT_MUSTER`, also ist hier nichts zu maskieren“). Nachgewiesen: ein Name mit Komma und
`$filter` kam durch den Dienst und schrieb einen **zweiten `$filter`** in die Graph-Adresse.
Hereinkommen konnte er nur mit `verwaltung` oder per Skript, Seed und `psql`, die
Sicherheitswirkung ist also klein — der Befund ist der **Ort** der Regel. Jetzt
`zuordnungPruefen` im Dienst (wirft) und Verwerfen in `zuordnungLesen` (damit eine kaputte
Zeile nicht die Quellenliste sprengt).
### Leicht
- [x] **Drei Kommentare versprachen mehr als der Code hält.** Der Kommentar am Konstanten-Block der
Wartezeiten sagte „nach dem vierten Fehlschlag wird aufgegeben“, während es der fünfte ist —
und das ist der Kommentar, den man zuerst liest. Die Migration der Benachrichtigungen
behauptete „der Dienst prüft dasselbe noch einmal, nur mit besserer Meldung“; tut er nicht,
und er soll es auch nicht (zwei Instanzen hinter einem Lastverteiler fragen beide und
bekommen beide „nein“ — nur der partielle Index hält). **Die Migration selbst bleibt
unverändert:** eine angewandte Migration wird nicht umgeschrieben, so wie eine
veröffentlichte `VorlagenVersion` nicht. Die Richtigstellung steht bei `einreihen`, wo sie
gelesen wird.
- [x] **Der Protokolleintrag „UPN mehrdeutig“ nannte immer „2“**, auch bei sieben Treffern:
`personZuUpn` hatte `take: 2`, und dieselbe Zahl ging in den Eintrag. Wer das Protokoll
liest, liest es, **um den Fall aufzulösen** — er muss wissen, wie viele Zeilen zu bereinigen
sind. `take` entfernt, Test mit vier Treffern.
- [x] **`anzahlSchritte` im Protokoll eines neuen Laufs zählte die Version, nicht den Lauf.** Bei
einer Vorlage mit abteilungsgebundenen Abschnitten (#031) behauptete der Eintrag mehr
Schritte, als der Lauf hat. Das Protokoll ist seit #023 ansehbar.
- [x] **Die Phase-2-Seiten fehlten in der 390-px-Prüfung.** Ergänzt: Bausteinliste, Schnellstart,
Protokoll, Zustellungen — gerade die Tabellen, also die Stellen, an denen horizontales
Scrollen entsteht. Jedes Issue berichtet eine Klickprobe von Hand; eine Probe von Hand ist
kein Regressionstest.
- [x] **Der Graph-Seitenabruf folgte `@odata.nextLink` ohne Host-Prüfung**, mit einem mandantenweit
gültigen Token im Kopf. Graph über TLS zu trauen ist vertretbar — aber `GRAPH_API_URL` ist
absichtlich einstellbar (nationale Clouds, Test-Doppelgänger), und ein Tippfehler darin
schickte das Token woandershin. `naechsteSeite` vergleicht jetzt den Ursprung.
## Nicht behoben, bewusst
- **`anhaengeDesLaufs` liegt in `services/` und wendet keine Mandantensicht an.** Beide Aufrufer
prüfen den Lauf vorher, und die Kommentare sagen das auch — aber es steht gegen die Regel aus
`CLAUDE.md` („`queries/*` wenden ihre Sicht selbst an, damit kein Aufrufer sie vergessen kann“).
Zwei Aufrufer in zwei Schichten gibt es schon; der dritte ist der, der es vergisst. Eigenes
Issue: [#037](037-anhaenge-lesemodell.md).
- **`/meine-geraete` fehlt weiter in der 390-px-Prüfung.** Die Seite verlangt `selbstauskunft`, und
das hat nur `MITARBEITER`; der E2E-Ablauf meldet sich als Admin an und würde die
Verweigerungsseite messen. Dafür braucht es ein eigenes Konto samt Personenbindung — siehe
[#037](037-anhaenge-lesemodell.md).
- **Das Graph-Token wird einmal je Abgleich geholt**, vor der Seitenschleife. Bei einem Mandanten,
dessen Abgleich länger als die Token-Lebensdauer läuft, hilft der Sicherheitsabstand nicht. Das
gehört auf die offene Prüfliste in `docs/betrieb/entra-id.md` und ist ohne echten Mandanten nicht
zu beurteilen.
## Was der Review *nicht* gefunden hat
Benannt, weil es den Befunden ihr Gewicht gibt: keine `kundeId` oder `quelleId` aus der Eingabe
statt aus Sitzung bzw. Token; kein Lesemodell, das seine Sicht ganz vergisst; keine
Client-Komponente mit einem Wert-Import aus `src/server/*`; kein Geheimnis in einer Sicht, Antwort
oder einem Protokolleintrag; kein gespeichertes `{{generiertes_passwort}}`; kein Codepfad, der
annimmt, dass nur ein Mensch einen Schritt erledigt.
Ein Befund des Reviews war **falsch**: der Zustands-Handler habe keinen Vitest-Test. Er hat acht
direkte Aufrufe in `tests/lauf-ereignisse.test.ts`, darunter „weist ohne Anmeldung ab“ und „liefert
für einen fremden Kunden keinen Strom“. Nachgeprüft, bevor etwas geändert wurde.
+53
View File
@@ -0,0 +1,53 @@
# #037 Zwei Nachzügler aus der Durchsicht von Phase 2
**Status:** offen
**Im Tracker:** –
**Meilenstein:** v0.3.0
**Labels:** phase-2, backend, test
**Geschätzt:** 2 h **Tatsächlich:** –
## Ziel
Zwei Befunde aus [#036](036-reviewbefunde-phase-2.md), die mehr sind als eine Korrektur: beide
verlangen einen Umbau, und beide sind heute kein Schaden, sondern eine offene Flanke.
## 1. `anhaengeDesLaufs` ist ein Lesemodell am falschen Ort
`anhaengeDesLaufs(laufId)` liegt in `src/server/services/anhaenge.ts`, nimmt nur eine Lauf-Kennung
und wendet **weder Rollen- noch Mandantensicht** an. Beide Aufrufer
(`src/app/(app)/laeufe/[id]/page.tsx` und `src/server/queries/druck.ts`) prüfen den Lauf vorher, und
die Kommentare sagen das auch — es ist also heute kein Fehler.
Es steht aber gegen die Regel aus `CLAUDE.md` und `docs/architektur.md` §2: Lesemodelle liegen in
`queries/*` und **wenden ihre Sicht selbst an, damit kein Aufrufer sie vergessen kann**. Zwei
Aufrufer in zwei Schichten gibt es schon; der dritte ist der, der es vergisst — und er wird nicht
auffallen, weil die zwei bestehenden korrekt sind.
- [ ] Nach `src/server/queries/anhaenge.ts` ziehen, mit `HandelnderBenutzer` als Parameter
- [ ] Mandanten- und Rollensicht dort anwenden, nicht beim Aufrufer
- [ ] Test: ein Benutzer, der auf andere Kunden beschränkt ist, bekommt eine leere Liste — **ohne**
dass der Aufrufer vorher geprüft hat
- [ ] Gegenprobe: die Sicht entfernen, der Test muss rot werden
## 2. `/meine-geraete` fehlt in der 390-px-Prüfung
Die Seite ist das Ergebnis von #035 und ausdrücklich mit „am Handy kein horizontales Scrollen“
abgenommen — von Hand. In `tests/e2e/mobil.spec.ts` steht sie nicht, und sie kann dort nicht
einfach ergänzt werden: sie verlangt das Recht `selbstauskunft`, und das hat nur `MITARBEITER`. Der
Ablauf meldet sich als Admin an und würde die Verweigerungsseite messen — die scrollt garantiert
nicht.
- [ ] Ein `MITARBEITER`-Konto für die Abläufe: lokales Passwort, verknüpft mit einer
`Mitarbeiter`-Zeile (der CHECK `Benutzer_mitarbeiter_nur_bei_rolle` verlangt die Verknüpfung)
- [ ] Im Seed oder im E2E-Vorlauf? **Im Vorlauf**, denn ein Konto mit bekanntem Passwort gehört
nicht in einen Seed, der auch produktiv läuft
- [ ] `/meine-geraete` in die Seitenliste von `mobil.spec.ts`
- [ ] Dabei prüfen, was ein `MITARBEITER` auf den **anderen** Seiten sieht: die Rollenmatrix ist in
Vitest vollständig, aber kein Ablauf klickt sie durch
## Akzeptanzkriterien
- [ ] `anhaengeDesLaufs` liegt in `queries/`, und ein Test belegt die Sicht ohne Vorprüfung des
Aufrufers
- [ ] `/meine-geraete` wird bei 390 px gemessen, mit einem echten `MITARBEITER`
- [ ] Beide Gegenproben werden rot
+54
View File
@@ -0,0 +1,54 @@
# #038 Die E2E-Abläufe laufen in keinem Tor und waren monatelang rot
**Status:** offen
**Im Tracker:** –
**Meilenstein:** v0.3.0
**Labels:** phase-2, test, infra
**Geschätzt:** 3 h **Tatsächlich:** –
## Ziel
Beim Abschluss von [#027](027-e2e-reste-vorlagen.md) lief `pnpm test:e2e` erstmals seit dem
26. September wieder vollständig. **Sieben Abläufe waren rot**, und niemand hatte es gemerkt.
Keiner der sieben war ein Produktfehler — und genau das ist das Problem. Vier Ursachen:
| Ablauf | Ursache | Seit |
|---|---|---|
| `laeufe.spec.ts` | zählt die Felder des Zustands auf, `anzahlAnhaenge` fehlte | #033 |
| `stammdaten.spec.ts` | „kommt aus dem Verzeichnis“ steht seit #020 zweimal auf der Seite | #020 |
| `vorlagen-editor.spec.ts` | zieht am Handy feste 300 px, der Abstand ist 532 px | #030/#031 |
| `onboarding.spec.ts` | die **Entwicklungsdaten** wichen vom Seed ab | unbekannt |
Alle vier sind behoben. Das Muster bleibt: **die E2E-Schicht trägt Zusagen, die das Commit-Tor nicht
fährt** — Rückfall auf Polling, Herzschlag, 390 px, die Auslieferung eines Anhangs über seine
Adresse. Steht eine Zusage nur in Playwright, und läuft Playwright nur gelegentlich, dann ist sie
eine Notiz und keine Prüfung.
## Zu entscheiden
- [ ] **`pnpm test:e2e` ins Tor vor dem Commit?** Dagegen: es verlangt einen **gebauten,
laufenden** Stand (`pnpm build && pnpm start`), dauert über vier Minuten und braucht eine
Datenbank mit Seed-Bestand. Vier Minuten vor jedem Commit ist ein Tor, das umgangen wird.
- [ ] **Oder eine kleinere, verbindliche Regel:** E2E läuft vor jedem Merge nach `main` — das wären
acht Läufe in Phase 2 gewesen und hätte jeden der vier Befunde am Tag seiner Entstehung
gezeigt. Das ist die Empfehlung.
- [ ] **Oder im CI** (`.gitea/workflows/ci.yml`). Dort ist kein Runner registriert; das zu ändern
ist eine eigene Entscheidung und gehört nicht in dieses Issue.
## Aufgaben
- [ ] Die gewählte Regel in `CLAUDE.md` aufschreiben, mit **gemessener** Dauer und der Begründung,
warum sie nicht im Commit-Tor steht
- [ ] Die Voraussetzungen, die `tests/e2e/global-setup.ts` prüft, in derselben Reihenfolge
dokumentieren — der Vorlauf ist inzwischen die ehrlichste Beschreibung dessen, was E2E braucht
- [ ] Prüfen, welche Zusagen **ausschließlich** in E2E stehen, und für jede entscheiden: gehört sie
zusätzlich in einen Vitest-Test, der den Handler direkt fährt? (Das Muster steht in
`tests/scim.test.ts`, `tests/anhang-route.test.ts` und `tests/lauf-ereignisse.test.ts`.)
- [ ] Mindestens die Rechteprüfung der beiden Phase-2-Handler ist damit schon abgedeckt — bleiben
Rückfall auf Polling, Herzschlag am echten Browser und 390 px
## Akzeptanzkriterien
- [ ] In `CLAUDE.md` steht, wann E2E läuft, und die Angabe ist gemessen, nicht geschätzt
- [ ] Eine Liste der Zusagen, die nur in E2E stehen — und zu jeder eine Entscheidung
+5 -2
View File
@@ -31,7 +31,7 @@ Diese Dateien sind die Quelle; `pnpm issues:sync` spiegelt sie in den Gitea-Trac
| #024 | [Proxmox-Einzeiler auf einem echten PVE-Host gegenprüfen](024-proxmox-echtlauf.md) | offen | v0.2.0 | 2 h | – |
| #025 | [Healthcheck gibt die rohe Datenbankfehlermeldung heraus](025-gesundheit-fehlermeldung.md) | erledigt | v0.2.0 | 1 h | 25 min |
| #026 | [Zwei Konventionen präzisieren, die strenger klingen als sie gemeint sind](026-konventionen-praezisieren.md) | erledigt | v0.2.0 | 1 h | 20 min |
| #027 | [Die Editor-Abläufe lassen ihre Vorlagen stehen](027-e2e-reste-vorlagen.md) | offen | v0.2.0 | 1 h | – |
| #027 | [Die Editor-Abläufe lassen ihre Vorlagen stehen](027-e2e-reste-vorlagen.md) | erledigt | v0.2.0 | 1 h | 2 h 10 min |
| #028 | [M15 – Quellenarten und Microsoft-Graph-Quelle](028-graph-quelle.md) | erledigt | v0.2.0 | 10 h | 4 h 10 min |
| #029 | [M16 – SCIM-Endpunkt als Alternative zum Abgleich](029-scim-endpunkt.md) | erledigt | v0.2.0 | 12 h | 4 h 05 min |
| #030 | [M17 – Wiederverwendbare Bausteine](030-bausteine.md) | erledigt | v0.2.0 | 12 h | 2 h 25 min |
@@ -40,4 +40,7 @@ Diese Dateien sind die Quelle; `pnpm issues:sync` spiegelt sie in den Gitea-Trac
| #033 | [M20 – Live-Aktualisierung per SSE statt Polling](033-sse-live.md) | erledigt | v0.2.0 | 6 h | 1 h 40 min |
| #034 | [M21 – Benachrichtigungen per Mail und Webhook](034-benachrichtigungen.md) | erledigt | v0.2.0 | 10 h | 3 h 30 min |
| #035 | [M22 – Mitarbeiter-Selbstauskunft](035-selbstauskunft.md) | erledigt | v0.2.0 | 8 h | 2 h 50 min |
| | **Summe** | | | **229 h** | **64 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) | offen | v0.3.0 | 2 h | – |
| #038 | [Die E2E-Abläufe laufen in keinem Tor und waren monatelang rot](038-e2e-im-tor.md) | offen | v0.3.0 | 3 h | – |
| | **Summe** | | | **238 h** | **70 h 30 min** |