Der Proxmox-Einzeiler ist auf einem echten PVE-Host nie gelaufen — das ist eine Gegenpruefung und kein fehlendes Stueck: shellcheck-geprueft, im --dry-run gefahren, und der Installationsteil laeuft vollstaendig in einem debian:13-Container. Was fehlt, ist pct create auf einem Host, den diese Umgebung nicht hat. Die Pruefliste wartet unveraendert. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1767 lines
106 KiB
Markdown
1767 lines
106 KiB
Markdown
# DEVLOG – Laufzettel
|
||
|
||
Entwicklungstagebuch mit Zeiterfassung. Eine Sitzung ist ein Abschnitt; Sub-Agenten-Läufe stehen
|
||
als Unterpunkte mit Aufgabe und Dauer.
|
||
|
||
## Zeitübersicht
|
||
|
||
| Issue | Titel | Tatsächlich |
|
||
|---|---|---|
|
||
| #005 | M0 – Gerüst, Prozess, Design-Token | 1 h 25 min |
|
||
| #006 | M1 – Datenmodell | 1 h 10 min |
|
||
| #007 | M2 – Auth, Rollen, Benutzer | 1 h 35 min |
|
||
| #008 | M3 – Betriebsmodus und Kunden | 1 h 20 min |
|
||
| #009 | M4 – Vorlagen-Versionierung im Service | 1 h 20 min |
|
||
| #010 | M5 – Vorlagen-Editor | 2 h 10 min |
|
||
| #011 | M6 – Mitarbeiter, Abteilungen, Geräte | 2 h 15 min |
|
||
| #012 | M7 – Platzhalter, Namensregeln, Escaping | 2 h 30 min |
|
||
| #013 | M8 – Läufe: Service und Abarbeitung | 3 h 05 min |
|
||
| #014 | M9 – QR-Tags, Geräteseite, Etiketten, Scanner | 3 h 35 min |
|
||
| #015 | M10 – Dashboard und Archiv | 2 h 20 min |
|
||
| #016 | M11 – Verzeichnis und LDAP-Abgleich | 3 h 25 min |
|
||
| #017 | M12 – Seed der sieben Vorlagen | 1 h 30 min |
|
||
| #018 | M13 – E2E-Tests, Anleitung, PDF | 2 h 15 min |
|
||
| #019 | M14 – Betriebswege und Release v0.1.0 | 5 h 25 min |
|
||
| #025 | Healthcheck gibt die rohe Datenbankfehlermeldung heraus | 25 min |
|
||
| #026 | Zwei Konventionen präzisieren | 20 min |
|
||
| #020 | Vorgesetzter auch für von Hand angelegte Personen | 1 h 20 min |
|
||
| #021 | Stabiler Schlüssel für Seed-Vorlagen | 55 min |
|
||
| #022 | Schnellstart je Gerätetyp pflegen | 1 h 10 min |
|
||
| #023 | Protokoll ansehen | 1 h 50 min |
|
||
| #030 | M17 – Wiederverwendbare Bausteine | 2 h 25 min |
|
||
| #031 | M18 – Abteilungsspezifische Zusatzabschnitte | 1 h 35 min |
|
||
| #032 | M19 – Anhänge und Fotos an Schritten | 3 h 05 min |
|
||
| #033 | M20 – Live-Aktualisierung per SSE statt Polling | 1 h 40 min |
|
||
| #034 | M21 – Benachrichtigungen per Mail und Webhook | 3 h 30 min |
|
||
| #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 |
|
||
| #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 |
|
||
| #038 | Die E2E-Abläufe laufen in keinem Tor | 1 h 35 min |
|
||
| #037 | Zwei Nachzügler aus der Durchsicht von Phase 2 | 1 h 50 min |
|
||
| | **Summe** | **73 h 55 min** |
|
||
|
||
| Meilenstein | Tatsächlich | Status |
|
||
|---|---|---|
|
||
| v0.1.0 | 35 h 20 min | veröffentlicht |
|
||
| v0.2.0 | 38 h 35 min | in Arbeit |
|
||
|
||
---
|
||
|
||
## 2026-09-24 · Sitzung 1 · 22:05–23:30
|
||
|
||
**Issue:** #005 (M0)
|
||
**Version:** 0.1.0
|
||
|
||
### Erledigt
|
||
|
||
- Umgebung geprüft: Node v24.21.0 nur über nvm (nicht im `PATH` nicht-interaktiver Shells),
|
||
Docker 29.1.3, Chromium-Systembibliotheken vorhanden, kein `pandoc`
|
||
- Gitea aufgesetzt: 11 Labels, 5 Meilensteine (`v0.1.0`–`v0.5.0`), 19 Issues (#1–#19), Nummern
|
||
passen zu den Dateinamen unter `docs/issues/`
|
||
- Projektgerüst angelegt und alle vier Schranken grün: `lint`, `typecheck`, `test`, `build`
|
||
- Betriebsstand gehärtet und der Nachweis in `tests/betriebsstand.test.ts` festgeschrieben
|
||
- Abnahme von #005 wirklich gefahren, nicht behauptet: `docker compose up -d --build` startet den
|
||
Stack, `migrate` läuft und beendet sich, `web` wartet korrekt darauf, `/api/gesundheit` meldet
|
||
`ok` — und `5432` ist vom Host nicht erreichbar, aus `web` schon
|
||
- `scripts/pruefe-doku.mjs` gegen vier eingebaute Fehler geprüft: es findet abweichende Zeiten,
|
||
fehlende Einträge, Verweise auf fehlende Dateien und fehlende Bilder
|
||
|
||
### Entscheidungen
|
||
|
||
- **Deutsch im Code, auch für Bezeichner.** Der Auftragstext verlangte Englisch; Florian hat sich
|
||
für Einheitlichkeit mit ProxLedger entschieden. Verbindliche Namensabbildung in `CLAUDE.md`.
|
||
- **Kein Auth.js.** v5 steht seit Jahren auf Beta (`5.0.0-beta.32`), authjs.dev führt das Projekt
|
||
im Wartungsmodus („now part of Better Auth“), die Maintainer nennen „no immediate plans for v5“,
|
||
und `@auth/prisma-adapter` deklariert Prisma nur bis 6 — wir fahren Prisma 7. Stattdessen
|
||
`openid-client` + `jose` wie in ProxLedger. Dieselbe Überlegung wie dort, nur ein Jahr später
|
||
noch deutlicher.
|
||
- **`prisma` exakt auf 7.10.0 gepinnt.** Der `latest`-Tag von `prisma` zeigt auf `8.0.0-rc.17`,
|
||
der von `@prisma/client` auf `7.10.0`. Ein unbedachtes `pnpm add prisma` hätte einen
|
||
Versions-Konflikt erzeugt.
|
||
- **`eslint` bleibt auf 9.x.** Mit eslint 10 bricht das Regelwerk beim Laden ab:
|
||
`eslint-config-next` bringt `eslint-plugin-react@7.37.5` mit, das `eslint` nur bis `^9.7`
|
||
unterstützt, und eine neuere Fassung des Plugins gibt es nicht (letzte Veröffentlichung
|
||
April 2025). Empirisch festgestellt, nicht geraten.
|
||
- **`typescript` bleibt auf `~6.0.3`.** `typescript-eslint@8.70.1` erlaubt nur
|
||
`>=4.8.4 <6.1.0`; TypeScript 7.0.2 ist zwar `latest`, aber noch nicht unterstützt.
|
||
- **`@node-rs/argon2` statt `argon2`.** Liefert vorgebaute musl- und gnu-Binaries, braucht auf
|
||
`node:24-alpine` also keine C++-Toolchain in der Bau-Stufe.
|
||
- **`barcode-detector` statt `jsQR`.** `jsqr` ist seit April 2021 unveröffentlicht. Der Polyfill
|
||
bietet dieselbe API wie das native `BarcodeDetector`, also ein Codepfad statt zwei.
|
||
- **Seed läuft nicht automatisch.** Die Migrationen schon, der Seed bleibt ein dokumentierter
|
||
Handgriff — so steht es in den Abnahmekriterien.
|
||
- **shadcn/ui eingerichtet, aber der Button folgt der Vorgabe, nicht dem Preset.** Der Vertrag
|
||
von shadcn (`--background`, `--primary`, `--border`, …) wird in `globals.css` aus den
|
||
Laufzettel-Token gespeist; jedes später kopierte Bauteil sieht damit sofort richtig aus und
|
||
folgt auch dem hellen Modus. Der Scannen-Knopf nutzt den shadcn-Button, überschreibt aber Form
|
||
und Höhe (Pille, 44 px am Handy statt 32 px).
|
||
- **PDF der Anleitung über Playwright.** `pandoc` und `wkhtmltopdf` fehlen und sind ohne root
|
||
nicht installierbar; Chromium ist für die E2E-Tests ohnehin da.
|
||
|
||
### Probleme
|
||
|
||
- **Projektverzeichnis gehörte `root`.** `~/Projekte/Laufzettel` war root-eigen, die Sitzung läuft
|
||
als `florianwilde`, und `sudo` verlangt hier ein Passwort. Bis Florian den `chown` gemacht hat,
|
||
wurde das Gerüst im Scratchpad vorgebaut und danach hineinverschoben — die Gitea-Arbeit brauchte
|
||
das Verzeichnis nicht und lief parallel.
|
||
- **pnpm 12 hat die Einstellung für Build-Skripte umbenannt.** `onlyBuiltDependencies` wird noch
|
||
von `pnpm config list` angezeigt, aber nicht mehr angewandt; wirksam ist `allowBuilds` als
|
||
Zuordnung in `pnpm-workspace.yaml`. Das `pnpm`-Feld in `package.json` liest pnpm 12 gar nicht
|
||
mehr. Gefunden über `pnpm approve-builds --all`, das die richtige Form selbst schreibt.
|
||
- **Context7-MCP ist nicht autorisiert**, der OAuth-Ablauf geht in dieser Sitzung nicht. Alle
|
||
Versionen wurden stattdessen direkt gegen das npm-Registry und die offiziellen Docs geprüft.
|
||
- **pnpm 12 prüft die Lieferkette und lehnt taufrische Pakete ab.** `vite@8.3.1` (8,7 h alt) und
|
||
`lucide-react@1.48.0` (15,3 h alt) fielen durch. Beide auf die Vorgängerfassung gesetzt, die
|
||
Richtlinie bleibt an. Bei der Gelegenheit alle direkten Abhängigkeiten auf ihr Alter geprüft.
|
||
- **`shadcn init` lässt sich nicht skripten.** Es fragt auch mit `--yes` nach einem Preset und
|
||
bleibt hinter einer Pipe stumm hängen. `shadcn add <bauteil> --yes --overwrite < /dev/null`
|
||
geht dagegen. Zusätzlich nötig: Pfad `"cn": ["./src/lib/utils"]` in `tsconfig.json` und das
|
||
Paket `radix-ui`, beides schreibt `init` nicht.
|
||
- **Port 3000 ist von ProxLedger belegt.** Die Abnahme lief auf 3100 und 3200.
|
||
- **Kein Gitea-Actions-Runner registriert** (0 Runner). `.gitea/workflows/ci.yml` wird trotzdem
|
||
angelegt und ist nutzbar, sobald ein Runner da ist; verbindlich sind bis dahin der
|
||
Pre-Commit-Hook und die Durchläufe vor jedem Commit.
|
||
|
||
### Offen
|
||
|
||
- Der `pct create`-Teil des Proxmox-Skripts (#019) lässt sich hier nicht ausführen, weil kein
|
||
PVE-Host erreichbar ist. Geplant: `shellcheck` plus `--dry-run`, und `deploy/install.sh`
|
||
vollständig in einem `debian:13`-Container geprüft. Der Lauf auf echtem Proxmox braucht eine
|
||
Gegenprüfung durch Florian, dafür kommt eine Prüfliste.
|
||
- Der OpenLDAP-Testcontainer wandert von #005 nach #016, wo er tatsächlich benutzt wird — ein
|
||
Dienst, den noch kein Test anfasst, wäre eine Attrappe.
|
||
|
||
### Nächster Schritt
|
||
|
||
- #006 Datenmodell: das vollständige Schema in deutscher Benennung, `tagCode`-Generator,
|
||
Krypto-Helfer, erste Migration, idempotenter Seed-Rumpf.
|
||
|
||
---
|
||
|
||
## 2026-09-25 · Sitzung 2 · 00:05–01:15
|
||
|
||
**Issue:** #006 (M1)
|
||
**Version:** 0.1.0
|
||
|
||
### Erledigt
|
||
|
||
- Vollständiges Schema in deutscher Benennung: 20 Tabellen, 9 Aufzählungen, eine Migration
|
||
- `src/lib/tagcode.ts` und `src/lib/geheimnisse.ts` samt Tests, parallel von einem Sub-Agenten
|
||
gebaut, während ich das Schema schrieb (Dauer dort: rund 10 min)
|
||
- Idempotenter Seed für Betriebsmodus, eigene Organisation und Administrator — zweimal
|
||
hintereinander gefahren, keine Zeile geändert
|
||
- Unabhängiger Review-Agent gegen die Akzeptanzkriterien; alle Blocker und alle neun „sollte
|
||
behoben werden"-Punkte abgearbeitet
|
||
- **106 Tests** grün (vorher 11), davon 42 gegen die echten Datenbankregeln
|
||
|
||
### Entscheidungen
|
||
|
||
- **Regeln gehören zusätzlich in die Datenbank.** Zwölf CHECK-Bedingungen, zwei partielle
|
||
Eindeutigkeitsindizes und vier zusammengesetzte Fremdschlüssel. Begründung: die Service-Schicht
|
||
ist nicht der einzige Weg, auf dem Daten entstehen — Migrationen, `psql`, Skripte und ab
|
||
Phase 4 die Connectoren schreiben an ihr vorbei.
|
||
- **Die Mandantengrenze wird strukturell erzwungen.** `Lauf` verweist mit
|
||
`("geraetId", "kundeId")` auf `Geraet("id", "kundeId")`, analog für Person und Abteilung.
|
||
`docs/architektur.md` nennt die Grenze eine Sicherheitsgrenze — dann soll sie nicht allein an
|
||
`kundeErmitteln()` hängen. `MATCH SIMPLE` lässt einen Lauf ohne Gerät weiter zu.
|
||
- **Kein Cascade dort, wo das Archiv hängt.** Ein `DELETE` auf einen Lauf hätte Schrittergebnisse
|
||
und Kommentare stillschweigend mitgenommen, ein Protokolleintrag hätte seinen Urheber verlieren
|
||
können. Beides jetzt `Restrict`.
|
||
- **`TagCodeVergabe` als Vergabebuch.** Die Eindeutigkeit allein aus `Geraet.tagCode` zu ziehen
|
||
reicht nicht: verschwindet die Zeile, wäre der Code wieder frei und ein alter Aufkleber im
|
||
Regal zeigte auf ein fremdes Objekt.
|
||
- **Widerspruch im Auftrag beim tagCode-Alphabet.** Das wörtliche Alphabet enthält ein `L`,
|
||
die Klammer daneben verbietet es. Aufgelöst zugunsten des wörtlichen Alphabets: die Klammer
|
||
benennt Verwechslungsgruppen, keine Ausschlussliste — aus `{1, I, L}` sind `1` und `I` weg,
|
||
dadurch ist `L` eindeutig. Das Akzeptanzkriterium in #006 ist richtiggestellt worden, statt
|
||
einen grünen Test darüber zu setzen.
|
||
- **Ein Systemlauf muss darstellbar sein.** `Lauf.erstelltVonId` ist optional, wie schon
|
||
`Schrittergebnis.aktualisiertVonId`. Wiederkehrende Läufe und der Start per Webhook (Phase 4)
|
||
haben keinen Menschen dahinter.
|
||
|
||
### Probleme
|
||
|
||
- **`prisma migrate reset --force` zeigte den Bestätigungsdialog trotzdem** und ließ einen
|
||
Schema-Engine-Prozess zurück, der die Advisory-Sperre hielt; der nächste `migrate dev` lief
|
||
daraufhin in einen Timeout. Verlässlicher: `DROP SCHEMA public CASCADE` per psql, dann
|
||
`migrate dev --create-only < /dev/null`.
|
||
- **Port 5433 ist von ProxLedger belegt.** Die Entwicklungsdatenbank liegt jetzt auf 5434,
|
||
umstellbar über `DEV_DB_PORT`.
|
||
- **Eine Regex-Ersetzung hat `quelle.length` auch aus der Längenprüfung gerissen** und den
|
||
Krypto-Helfer unübersetzbar gemacht. Die Testsuite hat es sofort gezeigt — ein Argument mehr
|
||
dafür, nach jeder Sammeländerung wirklich alle Schranken laufen zu lassen.
|
||
|
||
### Nächster Schritt
|
||
|
||
- #007 Auth, Rollen, Benutzer: `openid-client` mit Discovery, `jose`-Sitzungscookie, lokaler
|
||
Login mit argon2, `RECHTE`-Tabelle, Login-Seite mit Rücksprung auf die ursprüngliche URL.
|
||
|
||
---
|
||
|
||
## 2026-09-25 · Sitzung 3 · 01:55–03:35
|
||
|
||
**Issue:** #007 (M2)
|
||
**Version:** 0.1.0
|
||
|
||
### Erledigt
|
||
|
||
- OIDC mit `openid-client` 6.8.8: Discovery, PKCE, `state`, `nonce`; API vorher gegen die
|
||
Typdefinitionen im Paket geprüft statt aus dem Gedächtnis geschrieben
|
||
- `jose`-Sitzungscookie, lokaler Login mit argon2, Rechtetabelle, JIT-Provisionierung
|
||
- Anmeldeseite, geschützter Bereich, Benutzerverwaltung, rollenabhängige Navigation
|
||
- Sub-Agent hat `rollen.ts` und `sitzung.ts` samt 85 Tests parallel gebaut (rund 6 min)
|
||
- Unabhängiger Review-Agent: ein Blocker, acht weitere Befunde, alle abgearbeitet
|
||
- **274 Tests** grün (vorher 106)
|
||
|
||
### Entscheidungen
|
||
|
||
- **Rücksprungziel wird aufgelöst, nicht mit Präfixen verglichen.** Die Präfix-Prüfung war eine
|
||
offene Weiterleitung: `/<TAB>/fremd.example` besteht `startsWith("/")`, ist kein `//` und kein
|
||
`/\` — Browser entfernen den Tabulator aber beim Verarbeiten der URL, übrig bleibt
|
||
`//fremd.example`. Das Auflösen gegen eine Wegwerf-Basis macht genau das, was der Browser tut,
|
||
und der Vergleich der Herkunft erschlägt alle Formen auf einmal.
|
||
- **Alle Umleitungen über `APP_URL`.** `nextUrl.origin` lieferte im Test `localhost` statt
|
||
`127.0.0.1`; hinter einem Reverse Proxy wäre es der interne Host. Neues Modul
|
||
`src/lib/adressen.ts`, die Herkunft der Anfrage ist nur noch Notnagel.
|
||
- **Ohne `OIDC_GROUP_*` bleibt die Rolle, wie sie ist.** Sonst wäre jede OIDC-Anmeldung eine
|
||
stille Herabstufung auf `LESER` — bei einem einzigen Administrator ohne Rückweg.
|
||
- **Verknüpfung über die Adresse nur bei `email_verified`.** Sonst wäre ein Anbieter mit frei
|
||
wählbaren Adressen der Weg, den lokalen Notfall-Administrator zu übernehmen.
|
||
- **Der letzte aktive Administrator ist geschützt**, gegen Herabstufung wie gegen Abschalten.
|
||
- **Kein `overflow-x: hidden` auf `body`.** Es sieht aus wie eine Lösung, verdeckt aber nur: zu
|
||
breiter Inhalt wird abgeschnitten statt erreichbar, und die Prüfung im Screenshot-Skript kann
|
||
ihn nicht mehr finden. Layouts müssen bei 390 px von sich aus passen.
|
||
- **`ROLLEN_TEXT` liegt in `src/lib`,** nicht in der Server-Schicht: eine Client-Komponente hätte
|
||
sonst die komplette Rechtetabelle ins Browser-Bündel gezogen.
|
||
|
||
### Probleme
|
||
|
||
- **Der Platzhalter-Hash gegen den Zeitkanal war erfunden** und hätte sofort geworfen statt Zeit
|
||
zu kosten. Die Zusage wäre damit schlicht falsch gewesen. Jetzt ein echter, lazy erzeugter
|
||
argon2-Hash; nachgemessen rund 1 ms Unterschied über je 60 Anfragen.
|
||
- **Beim Verschieben der Übersicht nach `(app)/` blieb `<Huelle>` in der Seite stehen.** Die Hülle
|
||
wurde doppelt gerendert — zwei Kopfzeilen, das innere Menü ohne Benutzer. Es war im Screenshot
|
||
zu sehen und ist mir trotzdem entgangen; gefunden hat es der Review-Agent. Ein Bild anzusehen
|
||
ersetzt nicht, gezielt nach dem zu suchen, was sich gerade geändert hat.
|
||
- **Prisma-Modelltypen kommen aus `@/generated/prisma/client`,** nicht aus `.../models` — dort
|
||
liegen nur die Hilfstypen der Abfragen. Steht jetzt in `CLAUDE.md`.
|
||
- **`networkidle` als Wartebedingung in Playwright** läuft bei einer Anwendung mit offenen
|
||
Verbindungen in den Timeout. `load` plus geladene Schriften ist deterministisch.
|
||
|
||
### Nächster Schritt
|
||
|
||
- #008 Betriebsmodus und Kunden: `betriebsmodus()`, `kundeErmitteln()`, Moduswechsel in beiden
|
||
Richtungen, Kundenverwaltung, Admin-Einstellungen.
|
||
|
||
---
|
||
|
||
## 2026-09-25 · Sitzung 4 · 05:35–07:00
|
||
|
||
**Issue:** #008 (M3)
|
||
**Version:** 0.1.0
|
||
|
||
### Erledigt
|
||
|
||
- `src/server/mandant.ts` als die eine Stelle für die Mandantenregel
|
||
- Moduswechsel in beiden Richtungen, Kundenverwaltung (Sub-Agent, 25 Tests), Admin-Einstellungen
|
||
- Wechsel per Playwright **durch die Oberfläche** gefahren, nicht nur im Service: Navigation
|
||
wechselt sofort, `/kunden` wird erreichbar
|
||
- Unabhängiger Review: drei Blocker, sechs weitere Befunde, alle abgearbeitet
|
||
- **336 Tests** grün (vorher 274)
|
||
|
||
### Entscheidungen
|
||
|
||
- **Die Sichtbarkeit gehört in `kundeErmitteln`,** nicht in eine zweite Funktion daneben. Sonst
|
||
müsste jeder künftige Service daran denken — und #009 ff. übernehmen diese Signatur.
|
||
- **Lesemodelle filtern selbst.** Sie verlangen den Benutzer, statt sich auf den Aufrufer zu
|
||
verlassen. `undefined` heißt „keine Einschränkung“, `[]` heißt „sieht nichts“; die Umsetzung
|
||
steht an einer Stelle, und beide Fälle haben einen Test.
|
||
- **`pg_advisory_xact_lock` statt Serializable.** Der Wechsel ist selten und kurz; ein Schloss auf
|
||
Transaktionsebene ist verständlicher als eine Isolationsstufe, die man beim Lesen übersieht, und
|
||
wird auch bei einem Abbruch freigegeben.
|
||
- **Die Prüfung läuft zweimal** — einmal für den Dialog, einmal innerhalb der Transaktion. Beide
|
||
Male derselbe Code, damit es keine nachgebaute zweite Wahrheit gibt.
|
||
- **ASCII-Faltung gilt für Bezeichner, nicht für Fließtext.** Steht jetzt ausdrücklich in
|
||
`CLAUDE.md`; Protokoll-Aktionen sind entsprechend nachgezogen.
|
||
|
||
### Probleme
|
||
|
||
- **Ich habe in einem Kommentar etwas behauptet, das nicht stimmte** („Alles in einer
|
||
Transaktion"), während der Protokolleintrag dahinter stand. Ein Kommentar, der eine Zusage
|
||
macht, gehört genauso geprüft wie der Code darunter.
|
||
- **`sichtbareKundenIds` hatte keinen einzigen Aufrufer.** Eine Funktion zu schreiben, die eine
|
||
Regel durchsetzt, und sie dann nirgends zu rufen, sieht im Review aus wie Absicht und ist es
|
||
nicht. Der Volltextlauf über `src` hätte das in zehn Sekunden gezeigt.
|
||
- **Der Wettlauf war reproduzierbar**, nicht theoretisch: zwei gleichzeitige Wechsel liefen beide
|
||
durch.
|
||
|
||
### Nächster Schritt
|
||
|
||
- #009 Vorlagen-Versionierung: Veröffentlichen, tiefe Kopie als Entwurf, höchstens ein Entwurf,
|
||
Unveränderlichkeit, JSON-Export und -Import.
|
||
|
||
---
|
||
|
||
## 2026-09-25 · Sitzung 5 · 07:00–08:20
|
||
|
||
**Issue:** #009 (M4)
|
||
**Version:** 0.1.0
|
||
|
||
### Erledigt
|
||
|
||
- `src/server/services/vorlagen.ts`: Anlegen, Kopfdaten bearbeiten, Entwurf holen (tiefe Kopie),
|
||
Speichern, Veröffentlichen, Verwerfen, Duplizieren, Archivieren
|
||
- `src/server/services/vorlagen-austausch.ts` mit `docs/vorlagen-format.md`: ein Zod-Schema für
|
||
Editor, Export und Import — eine Vorlage zieht als Datei zwischen zwei Installationen um
|
||
- `src/lib/validierung.ts` als einzige Quelle für Zod samt deutscher Meldungen, mit ESLint-Regel
|
||
- Migration `vorlagen_unveraenderlich`: vier Trigger, die die Unveränderlichkeit in der Datenbank
|
||
durchsetzen — nicht nur im Service
|
||
- Unabhängiger Review: drei Blocker (alle an der Mandantengrenze), acht weitere Befunde, alle
|
||
abgearbeitet
|
||
- **428 Tests** grün (vorher 336), davon neu: Mandantensicht, Datenbankregeln, Gleichzeitigkeit,
|
||
Austausch mit unsauberen Daten
|
||
|
||
### Entscheidungen
|
||
|
||
- **Die Regel steht zweimal: im Service und in der Datenbank.** Der Service gibt die verständliche
|
||
Meldung, die Datenbank die Garantie. Alles, was nicht durch den Service geht — Migrationen,
|
||
`psql`, der Seed, ab Phase 4 die Connectoren —, läuft sonst an der einen Regel vorbei, die das
|
||
Archiv überhaupt erst etwas wert macht. Die doppelte Haltung kostet einen Trigger und spart die
|
||
Annahme, dass jeder künftige Schreibweg daran denkt.
|
||
- **Ein Entwurf je Vorlage kommt vom partiellen Index**, nicht von einer Prüfung davor. Zwei
|
||
gleichzeitige Klicks auf „Bearbeiten“ laufen damit unvermeidlich in `P2002` — und genau dort
|
||
wiederholt der Service und gibt beiden denselben Entwurf zurück, statt einen Fehler zu zeigen,
|
||
den niemand versteht.
|
||
- **Optimistisches Sperren über `geaendertAm`,** nicht über eine Zählerspalte. Der Zeitstempel ist
|
||
ohnehin da, und `updateMany` mit Bedingung erledigt den Vergleich in derselben Anweisung.
|
||
- **Unbekannte `konfig`-Schlüssel überleben, unbekannte Werte bekannter Schlüssel nicht.** Ein
|
||
neues Feld aus einer späteren Fassung darf einen Umzug nicht verhindern; eine unbekannte `shell`
|
||
aber entscheidet darüber, wie ein Wert maskiert wird — sie stillschweigend zu verwerfen hieße,
|
||
einen bash-Befehl nach PowerShell-Regeln zu maskieren, und das kann auf einem Server Daten
|
||
kosten. Also laut abweisen, mit Pfad und erlaubten Werten.
|
||
- **Der Export ist nachsichtig, der Import streng.** Herausgeben muss man auch, was nicht ins
|
||
Schema passt, sonst verliert die Datei Inhalt. Annehmen darf man es nicht.
|
||
- **`tests/datenbankregeln.test.ts` als Wächter.** Prisma erzeugt für jede künftige Migration
|
||
Löschbefehle gegen die handgeschriebenen Objekte. Der Test zählt sie namentlich auf, damit ein
|
||
übersehener `DROP` die Suite rot macht statt still die Mandantengrenze zu entfernen.
|
||
|
||
### Probleme
|
||
|
||
- **Die drei Blocker lagen alle an derselben Stelle: den Lesemodellen.** Der Suchfilter brachte
|
||
einen zweiten `OR`-Schlüssel mit und überschrieb die Kundenbedingung — ein Buchstabe im Suchfeld
|
||
genügte, um einem gebundenen Techniker alle Vorlagen aller Kunden zu zeigen. Historie und Aufbau
|
||
einer Version filterten gar nicht. Zu `queries/vorlagen.ts` gab es keinen Test; die Sicht in #008
|
||
abzusichern und in #009 nur den Service zu testen war der Fehler.
|
||
- **`Prisma.validator`-Objekte mit zwei gleichen Schlüsseln sind kein Typfehler.** Ein
|
||
zusammengebautes `where` mit `OR` aus zwei Quellen fällt weder ESLint noch `tsc` auf. Deshalb
|
||
liegt die Mandantenbedingung jetzt in `AND`, wo ein zweiter Filter sie nicht erreichen kann.
|
||
- **Prisma wollte sieben handgeschriebene Objekte löschen.** Die Migration musste von Hand
|
||
geschrieben werden; zusätzlich fehlten `@default(now())` bei den `@updatedAt`-Feldern, weshalb
|
||
Prisma bei jedem Lauf `DROP DEFAULT` vorschlug.
|
||
- **Zwei gleichzeitige Testläufe gegen `laufzettel_test`** ergaben 62 falsche Fehlschläge. Der
|
||
Review-Agent und ich haben uns die Daten weggetruncated; im Auftrag an den Agenten hatte ich die
|
||
eigene Datenbank nicht verlangt.
|
||
|
||
### Nächster Schritt
|
||
|
||
- #010 Vorlagen-Editor: Liste mit Suche und Filtern, Abschnitte und Schritte per Drag & Drop,
|
||
Vorschau, Veröffentlichen, Versionshistorie, Warnung bei unbekannten Platzhaltern.
|
||
|
||
---
|
||
|
||
## 2026-09-25 · Sitzung 6 · 08:20–10:30
|
||
|
||
**Issue:** #010 (M5)
|
||
**Version:** 0.1.0
|
||
|
||
### Erledigt
|
||
|
||
- Vorlagenliste mit Suche und Filtern, Vorlagenseite mit Versionsgeschichte, Kopfdaten, Duplizieren,
|
||
Archivieren, JSON-Ex- und -Import
|
||
- Vorlagen-Editor (Sub-Agent): Abschnitte, Schritte, Variablen — anlegen, bearbeiten, löschen,
|
||
sortieren mit Finger, Maus, Tastatur und Knöpfen; typabhängige Felder, Markdown-Hilfetexte,
|
||
Platzhalter-Warnung, Vorschau mit Beispielwerten
|
||
- `src/lib/platzhalter.ts` als einzige Quelle der eingebauten Platzhalter — #012 baut darauf auf
|
||
- Erste Playwright-Tests: der Editor wird durch die Oberfläche gefahren, einschließlich Sortieren
|
||
mit dem Finger bei 390 px
|
||
- Unabhängiger Review: zwei Blocker, sechs weitere Befunde, alle abgearbeitet
|
||
- **496 Tests** grün (vorher 428), dazu 5 E2E-Tests
|
||
|
||
### Entscheidungen
|
||
|
||
- **Oberflächen werden gegen den gebauten Stand abgenommen, nie gegen `pnpm dev`.** Der
|
||
Entwicklungsmodus hydriert in dieser Umgebung nicht; ein Screenshot sieht deshalb richtig aus,
|
||
während kein Knopf etwas tut. Das ändert die Arbeitsweise für alle folgenden Meilensteine und
|
||
steht in `CLAUDE.md` und `docs/architektur.md`.
|
||
- **Was nur mit angehängtem React gelingt, gehört in einen Playwright-Test.** Nicht erst in #018:
|
||
ohne ihn wäre „Sortieren funktioniert am Handy“ eine Behauptung geblieben.
|
||
- **Die Nummer eines Abschnitts kommt aus der Position und wird nie gespeichert.** Die
|
||
Formatdokumentation sagte bisher das Gegenteil; beim ersten Umsortieren wäre eine gespeicherte
|
||
Nummer falsch gewesen. Entfernt wird eine vorangestellte Nummer nur beim Trenner `·` und nur,
|
||
wenn dahinter keine Ziffer steht — „24 · 7 Bereitschaft“ bleibt.
|
||
- **Ein Knopf, der auf eine fehlende Seite zeigt, wird nicht verlinkt.** Galt für die Navigation
|
||
schon, jetzt auch für „Scannen“.
|
||
- **`format:pruefen` ist ein Tor.** Sonst formatiert `lint-staged` beim Commit nach, und im Commit
|
||
stehen Änderungen, die niemand gesehen hat.
|
||
|
||
### Probleme
|
||
|
||
- **Mein eigener Prüfaufbau hat mich getäuscht.** Ich habe Seiten im Browser angesehen, für gut
|
||
befunden und weitergemacht — in einem Entwicklungsmodus, in dem nichts anklickbar war. Aufgefallen
|
||
ist es erst, als ich zum ersten Mal wirklich geklickt habe. Zwei Stunden Suche nach der Ursache,
|
||
am Ende lag es am Werkzeug und nicht am Code; die Erkenntnis war die Zeit wert.
|
||
- **Ein Kommentar hat wieder eine Zusage gemacht, die kein Test hielt** („ein Test hält das fest“,
|
||
über der CSP). Dritter Meilenstein in Folge, in dem der Review genau diese Sorte Fehler findet.
|
||
Ein Kommentar, der eine Garantie behauptet, braucht den Test daneben — oder den Satz nicht.
|
||
- **Der Sub-Agent hat im Browser Dinge geprüft, die dort gar nicht prüfbar waren.** Mein Auftrag
|
||
hat ihn dorthin geschickt; die Lücke war meine.
|
||
- **Der Editor war bei einer einzigen Vorlage 16 986 px hoch.** Ich habe es im Screenshot gesehen,
|
||
nicht der Review.
|
||
|
||
### Nächster Schritt
|
||
|
||
- #011 Mitarbeiter, Abteilungen und Geräte: Listen mit Suche, Mitarbeiterseite mit Historie,
|
||
Abteilungen mit Ziel-OU und Standardgruppen, Geräte mit CSV-Import.
|
||
|
||
---
|
||
|
||
## 2026-09-25 · Sitzung 7 · 10:30–12:55
|
||
|
||
**Issue:** #011 (M6)
|
||
**Version:** 0.1.0
|
||
|
||
### Erledigt
|
||
|
||
- Mitarbeiter: Dienst mit Sperre für abgeglichene Stammdaten, Liste mit Suche über vier Kennungen,
|
||
Personenseite mit Geräten, Vorgesetztem und Platz für die Laufhistorie
|
||
- Abteilungen unter Admin: Ziel-OU, Standardgruppen, Mail-Domain, dazu die Liste der
|
||
Abteilungsnamen, die bei Personen vorkommen, aber nicht gepflegt sind
|
||
- Geräte samt CSV-Import (Sub-Agent): eigener Zerleger in `src/lib/csv.ts`, Tag-Code-Vergabe in
|
||
einer Transaktion, Archivieren statt Löschen
|
||
- Neues Recht `mitarbeiterBearbeiten`; die Seitenleiste führt Personen und Geräte unter einem
|
||
Eintrag, im Modus Dienstleistung anders beschriftet
|
||
- Unabhängiger Review: zwei Blocker, sieben weitere Befunde, alle abgearbeitet
|
||
- **610 Tests** grün (vorher 601 vor dem Review), dazu 18 E2E-Abläufe
|
||
|
||
### Entscheidungen
|
||
|
||
- **Ein CSV-Wert reicht nicht über die Zeile hinaus.** Das Format erlaubt es, und `csvErzeugen`
|
||
schreibt es auch so — beim **Lesen** einer fremden Datei ist es trotzdem die schlechtere Annahme:
|
||
ein Umbruch in einem Hostnamen kommt nicht vor, ein vergessenes schließendes Anführungszeichen
|
||
ständig. Mit der Grenze endet der Schaden an der Zeile, in der er beginnt; ohne sie verschluckt
|
||
eine kaputte Zelle alle Folgezeilen, und der Import meldet dafür Erfolg.
|
||
- **Eine abgeglichene Person ist vollständig gesperrt**, nicht nur teilweise. Alle Stammdaten
|
||
kommen aus dem Verzeichnis, auch der Titel. Übrig bleibt genau ein Weg: aktivieren und
|
||
deaktivieren — und der muss bleiben, weil das Offboarding nicht auf den nächsten Abgleich wartet.
|
||
- **Löschen eines Geräts verlangt `verwaltung`.** Archivieren ist der Normalfall. Löschen ist nicht
|
||
rücknehmbar und verbrennt den Tag-Code für immer — der gedruckte Aufkleber ist danach wertlos.
|
||
- **Die Lesemodelle liefern den Kundennamen immer, die Seite entscheidet über die Anzeige.**
|
||
Vorher entschied das eine Lesemodell nach `istEigeneOrganisation` und das andere gar nicht.
|
||
- **Die Mitarbeitersuche ist ein eigenes Bauteil**, bevor sie zum zweiten Mal gebraucht wird: #013
|
||
und #014 brauchen sie wieder, und dreimal abgeschrieben vergisst man den Umschalter „auch
|
||
deaktivierte“ zweimal.
|
||
|
||
### Probleme
|
||
|
||
- **Der schwerste Fehler war wieder einer, der Erfolg meldet.** Der CSV-Import hat Zeilen
|
||
verschluckt und „3 von 3 angelegt“ gesagt. Gefunden hat ihn der Review mit einer Datei, wie sie
|
||
aus einem halb ausgefüllten Excel kommt — meine eigenen Tests prüften nur saubere Dateien und die
|
||
eine Ausnahme, die ohnehin geworfen wurde.
|
||
- **Zwei Funktionen im selben Modul widersprachen sich**: `abteilungsListe` schlüsselte nach Kunde
|
||
und Name, `ungepflegteAbteilungen` nach Namen allein. Nebeneinander geschrieben, nicht
|
||
nebeneinander gelesen.
|
||
- **Vierter Meilenstein in Folge mit einem Kommentar, der eine Zusage macht, die der Code nicht
|
||
hält.** Diesmal versprach er zwei verschiedene Meldungen, gerendert wurde eine.
|
||
|
||
### Nächster Schritt
|
||
|
||
- #012 Platzhalter, Namensregeln und Escaping: die reine Service-Schicht, aus der die fertigen
|
||
PowerShell-Befehle entstehen — mit dichter Testabdeckung, denn hier darf nichts schiefgehen.
|
||
|
||
---
|
||
|
||
## 2026-09-25 · Sitzung 8 · 12:55–15:35
|
||
|
||
**Issue:** #012 (M7)
|
||
**Version:** 0.1.0
|
||
|
||
### Erledigt
|
||
|
||
- `src/lib/shell.ts` (Sub-Agent): Maskierung je Shell, Skript-Kopie mit Kommentarzeilen
|
||
- `src/lib/namensregeln.ts` (Sub-Agent): Transliteration, `{v}`, `{nnn}`, `{kundenkuerzel}`,
|
||
Grenzen für `sam` und `hostname`
|
||
- `src/lib/passwort.ts`, `src/lib/platzhalter-werte.ts`, `src/server/services/platzhalter.ts` und
|
||
`src/server/services/befehle.ts`: das generierte Passwort, das Ersetzen und die fertigen Befehle
|
||
- Unabhängiger Review: **drei Blocker**, sieben weitere Befunde, alle abgearbeitet
|
||
- **810 Tests** grün (vorher 797 mit einem stillen Übersprung)
|
||
|
||
### Entscheidungen
|
||
|
||
- **PowerShell wird wirklich gegen PowerShell geprüft.** Auf dieser Maschine gibt es kein `pwsh`,
|
||
also nimmt der Test das Container-Abbild — und in CI bricht er ab, statt sich zu überspringen.
|
||
Der schwerste Befund des Meilensteins wäre ohne das nie aufgefallen.
|
||
- **Typografische Anführungszeichen werden auf ihre ASCII-Form gebracht und dann maskiert.** Als
|
||
Text durchreichen lassen sie sich nicht: PowerShell liest sie ohnehin als Begrenzer. Lieber ein
|
||
Zeichen, das anders aussieht als gedacht, als ein Befehl, der etwas anderes tut als er liest.
|
||
- **Ein Platzhalter außerhalb von Anführungszeichen wird abgewiesen**, nicht gefüllt. Die
|
||
Maskierung kann daraus kein Argument machen, und ein halb maskierter Befehl zum Kopieren wäre die
|
||
gefährlichste aller Antworten.
|
||
- **Gesperrt sind fünf Variablennamen, nicht alle eingebauten.** `techniker`, `datum`, `kunde`,
|
||
`kundenkuerzel` und `generiertes_passwort` machen eine Aussage über wer, wann und für wen. `upn`,
|
||
`sam` und `mail` bleiben frei — beim Onboarding gibt es die Person im Verzeichnis noch nicht.
|
||
- **Ein `sam` hält drei Stellen frei.** Ein einmal notierter Anmeldename muss auch morgen aus der
|
||
Regel herausfallen, wenn ein zweiter Träger desselben Namens dazukommt.
|
||
|
||
### Probleme
|
||
|
||
- **Ich bin in genau die Falle getappt, die ich in #010 repariert habe:** `$'` als Rückverweis in
|
||
`String.replaceAll`. Gefunden hat es mein eigener Test gegen echtes Bash — Lesen hätte es nicht
|
||
gefunden.
|
||
- **Meine erste HTML-Maskierung war doppelt**, weil ich vergessen hatte, dass React Textknoten
|
||
selbst maskiert. Aufgefallen beim Nachdenken über den Test, nicht beim Schreiben des Codes.
|
||
- **Der Review hat sich PowerShell in einem Container geholt.** Das ist der Unterschied zwischen
|
||
„nach bestem Wissen richtig“ und „gemessen“ — und der Unterschied war hier eine
|
||
Codeausführung auf dem Domänencontroller.
|
||
- **Vierter Meilenstein in Folge mit einem Kommentar, der ins Leere zeigt:** ein Testkommentar
|
||
verwies auf einen Beweis in einer anderen Datei, den es dort nicht gab.
|
||
|
||
### Nächster Schritt
|
||
|
||
- #013 Läufe: Service und Abarbeitung — Lauf anlegen, abarbeiten, abschließen, archivieren, mit
|
||
der Abarbeitungsansicht am Handy.
|
||
|
||
---
|
||
|
||
## 2026-09-25 · Sitzung 9 · 15:40–18:45
|
||
|
||
**Issue:** #013 (M8)
|
||
**Version:** 0.1.0
|
||
|
||
### Erledigt
|
||
|
||
- `src/server/services/{laeufe,schrittergebnisse,kommentare}.ts` (Sub-Agent): anlegen, öffnen,
|
||
abschließen, archivieren, zurückholen, Schrittergebnisse, Abschnitts-Aktion, Kommentare
|
||
- `src/server/queries/laeufe.ts`, die Abarbeitungsansicht, der Start-Dialog, die Laufliste und der
|
||
Zustands-Endpunkt für die Aktualisierung alle zehn Sekunden
|
||
- Namensvorschläge aus den Regeln im Start-Dialog und „Mit AD-Konto verknüpfen“ am Lauf
|
||
- Unabhängiger Review: **zwei Blocker**, zwölf Befunde, acht Anmerkungen — alle abgearbeitet
|
||
- **879 Tests** (vorher 810) und **37 Ende-zu-Ende-Tests** grün
|
||
|
||
### Entscheidungen
|
||
|
||
- **Ein Lauf, ein Passwort je Seitenaufbau.** Vorher entstand je *Vorkommen* ein neues: ein
|
||
`New-ADUser` mit `-AccountPassword` und einer Zeile, die das Passwort mitteilt, trug zwei
|
||
verschiedene. Das Konto bekäme A, der Benutzer erführe B, und weil nichts gespeichert wird, wäre
|
||
A für immer weg. „Bei jeder Anzeige neu“ heißt jetzt: je Aufbau der Seite neu, innerhalb der
|
||
Seite überall dasselbe.
|
||
- **Abgeschlossen ist nicht archiviert.** „Abgeschlossen“ ist ein Arbeitszustand; Nachträge
|
||
passieren danach, und zwar oft. Würde schon der Abschluss sperren, bräuchte jede Kleinigkeit
|
||
einen Administrator — und Läufe würden schlicht nicht mehr abgeschlossen.
|
||
- **Das Polling trägt keine Befehle, und es lädt auch nicht von selbst neu**, solange ein Passwort
|
||
auf der Seite steht. Die erste Hälfte der Zusage war leicht, die zweite fiel erst beim
|
||
Nachdenken über `router.refresh()` auf: ein Neuaufbau tauscht das Passwort genauso aus.
|
||
- **`/api/` bekommt ohne Anmeldung 401, keine Weiterleitung.** Ein `fetch` folgt einer
|
||
Weiterleitung von sich aus und hält die Anmeldeseite für eine gültige Antwort; die
|
||
Aktualisierung hätte stundenlang weitergefragt.
|
||
- **Abschnitte klappen zu, wenn sie fertig sind — der Zustand liegt im Browser.** Aus den Daten
|
||
gerechnet klappte ein von Hand geöffneter Abschnitt beim nächsten Speichern wieder zu.
|
||
|
||
### Probleme
|
||
|
||
- **React baut das Formular erst nach dem erneuten Rendern zusammen.** Mein erster Versuch, den
|
||
Zustand am abschickenden Knopf mitzugeben, setzte im selben Klick die Vorauswahl um — und damit
|
||
den Wert des Knopfes. Angekommen ist wahlweise „offen“ oder gar kein Feld. Gefunden nicht durch
|
||
Lesen, sondern durch Nachstellen im gebauten Stand mit einer Ausgabe der `FormData`.
|
||
- **Der Review hat einen Blocker gefunden, den mein eigener Test festgeschrieben hatte:** „zwei
|
||
Vorkommen ergeben zwei Passwörter“ stand als Erwartung samt Begründung im Test. Die Begründung
|
||
war falsch, der Test hat sie zementiert.
|
||
- **Ein `h2` in einem `button` ist keine Überschrift mehr.** In einem Knopf sind alle Nachfahren
|
||
nur noch Beschriftung; der Abschnittskopf verlor damit seine Gliederung — aufgefallen, weil ein
|
||
Playwright-Test die Überschrift nicht mehr fand.
|
||
- **Ein `details` und React geraten vor der Hydrierung aneinander:** der Browser klappt auf, React
|
||
klappt gleich darauf zu. Jetzt ein Knopf mit `aria-expanded`, der vor der Hydrierung schlicht
|
||
nichts tut — wie jeder andere Knopf auf dieser Seite.
|
||
- **Fünfter Meilenstein in Folge mit Kommentaren, die mehr versprechen als der Code hält:** der
|
||
Titelvorschlag behauptete, der Start-Dialog zeige ihn; `startAuswahl` behauptete, der Dialog
|
||
nenne seine Grenze von 500 Einträgen. Beides stimmte nicht.
|
||
|
||
### Nächster Schritt
|
||
|
||
- #014 QR-Tags, Geräteseite, Etiketten und Scanner — der Einstieg, für den die ganze Anwendung
|
||
gebaut ist: Code scannen, Geräteseite öffnen, loslegen.
|
||
|
||
---
|
||
|
||
## 2026-09-25 · Sitzung 10 · 18:50–21:30
|
||
|
||
**Issue:** #014 (M9)
|
||
**Version:** 0.1.0
|
||
|
||
### Erledigt
|
||
|
||
- **Zwei Sub-Agenten parallel**, auf getrennten Dateien: QR, Etiketten und Tag-Stapel der eine,
|
||
Geräteseite und `/t/<code>` der andere. Der Scanner kam von Hand dazu.
|
||
- `/t/<tagCode>` als Weiche, Registrierformular, Geräteseite mit Zuweisungen, Schnellstart,
|
||
„Vorlage zuweisen“ und Historie
|
||
- Etikettendruck in zwei Formaten, QR ausschließlich mit `{APP_URL}/t/{tagCode}`
|
||
- Scanner mit der BarcodeDetector-API des Browsers, WebAssembly als Rückfall, Eingabefeld als
|
||
Ersatzweg
|
||
- Unabhängiger Review: **drei Blocker**, elf Befunde, mehrere Anmerkungen — alle abgearbeitet
|
||
- **930 Tests** (vorher 879) und **54 Ende-zu-Ende-Tests** grün
|
||
|
||
### Entscheidungen
|
||
|
||
- **Parallel arbeiten heißt Dateien trennen, nicht Aufgaben.** Beide Agenten bekamen eine Liste
|
||
der Dateien, die ihnen gehören, und je eine eigene Testdatenbank. Es gab keinen einzigen
|
||
Schreibkonflikt — die Fehler saßen stattdessen an den **Nähten**, und zwar alle drei.
|
||
- **Das WebAssembly des QR-Lesers wird kopiert, nicht eingecheckt.** `zxing-wasm` lädt es sonst
|
||
aus einem CDN: die Content-Security-Policy erlaubt `connect-src 'self'`, und eine selbst
|
||
gehostete Anwendung soll ohne Internet laufen. 1,1 MB gehören nicht ins Repository, also legt
|
||
`scripts/wasm-bereitstellen.mjs` sie vor jedem Bau bereit.
|
||
- **`'wasm-unsafe-eval'` statt `unsafe-eval`.** Die schmale Erlaubnis, WebAssembly zu übersetzen —
|
||
ohne sie läuft die Kamera und erkennt nie etwas. Der Test prüft jetzt auf die **Marke**, nicht
|
||
auf das Wort: `wasm-unsafe-eval` enthält „unsafe-eval“ und ist doch etwas anderes.
|
||
- **Eine Route ohne Aufrufer wird entfernt, nicht dokumentiert.** `/api/qr/[tagCode]` war sauber
|
||
gebaut, hatte aber keinen Nutzer, keinen Test und eine abweichende Groß-/Kleinschreibung. Die
|
||
Geräteseite setzt das SVG jetzt direkt ein.
|
||
|
||
### Probleme
|
||
|
||
- **Drei Defekte, die nur der gebaute Stand zeigt.** „Starten“ setzte `IN_ARBEIT` und sprang
|
||
nicht (der Knopf verschwindet beim Neuaufbau, bevor sein Effekt läuft); „Vorlage zuweisen“ bot
|
||
Vorlagen an, die der Dienst danach ablehnte; die Geräteliste verlinkte nichts. Kein einziger
|
||
davon wäre durch Lesen aufgefallen.
|
||
- **Der Scanner benutzte nie die native API.** `barcode-detector/ponyfill` gibt **immer** die
|
||
eigene Klasse zurück — der Kommentar behauptete das Gegenteil, und die Auftragszeile „native
|
||
BarcodeDetector-API mit Polyfill-Fallback“ war damit unerfüllt. Ein Blick in `node_modules`
|
||
hätte genügt; ich habe der Paketbeschreibung geglaubt.
|
||
- **Ein Unterpfad in `APP_URL` fiel aus dem QR heraus.** `new URL("/t/…", "https://firma.de/app")`
|
||
verschluckt `/app`. Für eine Umleitung fällt das nie auf, weil der Reverse Proxy den Pfad wieder
|
||
anhängt — auf einem geklebten Aufkleber ist es das Ende. Der Test dazu hieß „nimmt APP_URL mit
|
||
Pfad ernst“ und prüfte nur einen Schrägstrich.
|
||
- **Sechster Meilenstein in Folge mit Zusagen ohne Deckung:** archivierte Geräte im Druck, „1 von
|
||
2 fehlt“ bei doppelten Codes, eine Vorbelegung, die nicht kam, ein Kommentarblock über der
|
||
falschen Funktion.
|
||
|
||
### Nächster Schritt
|
||
|
||
- #015 Dashboard und Archiv — die Übersicht für den Alltag und die Volltextsuche über alles, was
|
||
abgearbeitet wurde.
|
||
|
||
---
|
||
|
||
## 2026-09-25 · Sitzung 11 · 21:35–23:00
|
||
|
||
**Issue:** #015 (M10)
|
||
**Version:** 0.1.0
|
||
|
||
### Erledigt
|
||
|
||
- **Drei Teile parallel**: Archiv (Sub-Agent), Druckansicht (Sub-Agent), Übersicht (selbst)
|
||
- Archiv mit Volltextsuche über Titel, Notizen, Werte und Kommentare, Fundstellen im Treffer,
|
||
sechs Filtern und Mandantensicht in jeder Abfrage
|
||
- Druckansicht eines Laufs: hell, ohne Navigation, A4 — ersetzt im MVP den PDF-Export
|
||
- Übersicht mit Überfälligem, eigenen Zuweisungen, zuletzt Bearbeitetem und Schnellstart
|
||
- Unabhängiger Review: **zwei Blocker**, fünf Befunde, neun Anmerkungen — alle abgearbeitet
|
||
- **980 Tests** (vorher 930) und **67 Ende-zu-Ende-Tests** grün
|
||
|
||
### Entscheidungen
|
||
|
||
- **`ABGESCHLOSSEN` gehört ins Archiv.** „Abgeschlossen ist nicht archiviert" betrifft den
|
||
Schreibschutz, nicht das Suchen: wer die Seriennummer von letzter Woche sucht, weiß nicht, ob
|
||
schon jemand archiviert hat.
|
||
- **Eine SQL-Anweisung für drei Tabellen.** `Schrittergebnis.wert` ist ein Json — nur
|
||
`("wert")::text` trifft den Trigramm-Index, und über Prisma lässt sich eine Json-Spalte nicht als
|
||
Text durchsuchen. Vor allem aber gehört die **Fundstelle** zum Treffer: ein Treffer ohne
|
||
sichtbaren Grund ist für den Suchenden wertlos.
|
||
- **Die Druckansicht ist ein eigenes Lesemodell**, kein Schalter an `laufZumAbarbeiten`. Dort
|
||
entsteht beim Füllen der Befehle ein Passwort; auf einem Blatt, das jemand abheftet, darf keines
|
||
stehen.
|
||
- **Was „überfällig" heißt, steht jetzt in `src/lib/fristen.ts`** — eine Regel, zwei Benutzer
|
||
(Lesemodell und Anzeige). Zwei Fassungen derselben Regel laufen auseinander.
|
||
|
||
### Probleme
|
||
|
||
- **Beide Blocker steckten in meinem eigenen Teil.** „Überfällig" verglich mit dem Augenblick
|
||
statt mit dem Tagesbeginn — ein heute fälliger Lauf stand ab zwei Uhr morgens in Warnfarbe. Und
|
||
der Schnellstart zählte Läufe fremder Kunden mit: eine Vorlage ohne Kundenbezug gilt für alle,
|
||
ihr Zähler nicht.
|
||
- **Mein Test ließ genau die Grenze aus, an der der Fehler saß.** Er kannte „gestern" und
|
||
„morgen", aber nicht „heute". Seitdem steht die Regel in einer reinen Funktion mit eigenen
|
||
Grenztests.
|
||
- **Ein E2E-Test, der nicht scheitern konnte:** „trägt kein Passwort" lief gegen eine Vorlage ohne
|
||
Passwortschritt. Er hätte auch bestanden, wenn die Druckansicht Passwörter erzeugte.
|
||
- **Der Trigramm-Index auf `Lauf.titel` war strukturell unerreichbar**, weil der Titeltreffer aus
|
||
der sechsmal gelesenen und deshalb materialisierten CTE las. Der Review hat das mit 40 000
|
||
Läufen gemessen, nicht vermutet — das ist der Unterschied zwischen „sollte passen" und „passt".
|
||
- **Siebter Meilenstein in Folge mit einer Zusage ohne Deckung:** „kein
|
||
`{{generiertes_passwort}}` im Druck" galt nur für Vorlagentexte.
|
||
|
||
### Nächster Schritt
|
||
|
||
- #016 Verzeichnis und LDAP-Abgleich — der größte verbleibende Brocken: `ldapts`, Attribut-Mapping,
|
||
Zeitplan, verschlüsseltes Bind-Passwort und ein Integrationstest gegen einen OpenLDAP-Container.
|
||
|
||
---
|
||
|
||
## 2026-09-26 · Sitzung 12 · 23:05–02:30
|
||
|
||
**Issue:** #016 (M11)
|
||
**Version:** 0.1.0
|
||
|
||
### Erledigt
|
||
|
||
- **Vier Teile parallel**: LDAP-Schicht und Abgleich (Sub-Agent), Oberfläche Admin → Verzeichnis
|
||
(Sub-Agent), CSV-Import und Abteilungsvorschläge (Sub-Agent), Zeitplan und Server Actions (selbst)
|
||
- `ldapts` mit `ldaps://`, `ldap+starttls://` und `ldap://`, eigenem CA-Zertifikat, TLS ab 1.2 und
|
||
`searchPaginated` in 1000er-Seiten — Eintrag für Eintrag verarbeitet, nicht erst gesammelt
|
||
- `objectGUID` als Puffer gelesen und gemischt-endian zur GUID umsortiert; ein Puffer, der keine
|
||
GUID ist, wird nur dann als Text genommen, wenn dabei lesbarer Text herauskommt
|
||
- Abgleich mit Anlegen, Aktualisieren, Übernehmen und Deaktivieren, Vorgesetzten-Auflösung in einem
|
||
zweiten Durchgang und Ergebnis in `Verzeichnisquelle` **und** `Protokoll`
|
||
- Zeitplan mit `node-cron` im selben Prozess, gestartet aus `src/instrumentation.ts`, Sperre in der
|
||
Datenbank; „Jetzt abgleichen“ nimmt denselben Dienst
|
||
- Admin → Verzeichnis mit Quelle anlegen und bearbeiten, Verbindungstest samt gemappter Felder,
|
||
bearbeitbarer Attributzuordnung, CA-Zertifikat und Abgleich-Log
|
||
- Personen aus CSV einlesen, Abteilungsvorschläge aus den `department`-Werten
|
||
- Unabhängiger Review: **ein Blocker**, acht Befunde, sieben Anmerkungen — alle abgearbeitet
|
||
- **1098 Tests** (vorher 980) und **71 Ende-zu-Ende-Tests** (vorher 67) grün, Screenshots in
|
||
`docs/screenshots/m11/`
|
||
|
||
### Entscheidungen
|
||
|
||
- **Der Zeitplan nimmt keine eigene Sperre.** Der Dienst sperrt selbst, in der Datenbank. Eine
|
||
zweite Sperre im Zeitplan trüge einen anderen Schlüssel, liefe an der ersten vorbei — und ein von
|
||
Hand angestoßener Abgleich sähe für den Zeitplan wie ein unerwarteter Fehler aus.
|
||
- **Übersprungene Einträge sind ein Hinweis, kein Fehler.** `letzterAbgleichFehler` soll heißen,
|
||
dass etwas schiefging, und nicht „ein Konto war krumm“. Der Hinweis steht in der Erfolgsmeldung
|
||
und im Log; der Lauf gilt als gelungen.
|
||
- **Von Hand angelegte Personen werden übernommen, nicht verdoppelt.** Beim Onboarding entsteht die
|
||
Person hier und das AD-Konto danach. Gesucht wird über `upn`, dann `sam`, ohne Rücksicht auf
|
||
Groß- und Kleinschreibung — und nur unter Zeilen ohne Quelle: wer schon einer anderen Quelle
|
||
gehört, wandert nicht zwischen zwei Verzeichnissen hin und her.
|
||
- **Attributzuordnung und Intervallgrenzen liegen in `src/lib/verzeichnis.ts`.** Das Formular
|
||
braucht dieselbe Liste wie der Dienst, und eine Client-Komponente importiert nichts aus
|
||
`src/server/*`.
|
||
|
||
### Probleme
|
||
|
||
- **Der Blocker hätte die ganze Firma gekostet.** Steht in der Attributzuordnung ein
|
||
Kennungsattribut, das es am Server nicht gibt, wird **jeder** Eintrag übersprungen: das Lesen
|
||
klappt, niemand wird wiedergesehen, und die Abschaltung trifft die gesamte Belegschaft — mit
|
||
Status `ERFOLG`. Das Abgleich-Log hat den Fall danach selbst dokumentiert: der Lauf um 01:20 zeigt
|
||
„4 gelesen, 0 angelegt, 3 deaktiviert, 4 übersprungen“, die Läufe danach nie wieder.
|
||
- **Der Code versprach mehr als die Oberfläche hielt.** Ein Kommentar sagte „wer ein OpenLDAP
|
||
anbindet, ändert einzelne Zeilen“ — über die Oberfläche ging genau das nicht. Achter Meilenstein
|
||
in Folge mit einer Zusage ohne Deckung; diesmal war es die Zusage einer Bedienbarkeit.
|
||
- **Ein hinterlegtes CA-Zertifikat war unsichtbar und unlöschbar.** Das Feld stand beim Bearbeiten
|
||
leer da, der Hinweis darunter behauptete „ohne Eintrag gelten die Systemwurzeln“ — und ein
|
||
leeres Feld hieß „unverändert“. Das Zertifikat entschied also unsichtbar weiter mit.
|
||
- **Ein Test, der sich selbst aus dem Weg ging:** die Zusicherung im Nebenläufigkeitstest der
|
||
Vorlagen stand in einem `if`. Gewann das Speichern das Rennen, lief der Test durch, ohne etwas zu
|
||
belegen. Jetzt werden beide Ausgänge geprüft.
|
||
- **Eine Stunde gegen einen alten Server geprüft.** Der gebaute Stand lief, sah richtig aus und
|
||
zeigte trotzdem keine der neuen Funktionen — auf Port 3300 hing noch ein `next start` aus einer
|
||
früheren Sitzung, und der neue Start scheiterte still an `EADDRINUSE`. Seitdem: erst
|
||
nachsehen, wer auf dem Port sitzt, dann glauben, was die Seite zeigt.
|
||
- **Das Screenshot-Skript hat bei jedem Lauf zwei PNG-Dateien angefasst.** `kachelnErzeugen`
|
||
schrieb `public/icon-*.png` immer neu; `git status` zeigte Änderungen, die niemand gemacht hatte.
|
||
Jetzt wird verglichen und nur bei echtem Unterschied geschrieben.
|
||
|
||
### Nächster Schritt
|
||
|
||
- #017 Seed der sieben Vorlagen — alle sieben wortgetreu als veröffentlichte Version 1, dazu die
|
||
Beispieldaten und der Modus `DIENSTLEISTUNG` mit „Musterfirma GmbH“ und „Bäckerei Reiter“.
|
||
|
||
---
|
||
|
||
## 2026-09-26 · Sitzung 13 · 02:35–04:05
|
||
|
||
**Issue:** #017 (M12)
|
||
**Version:** 0.1.0
|
||
|
||
### Erledigt
|
||
|
||
- **Drei Teile parallel**: Vorlagen 1 und 4, Vorlagen 2 und 3, Vorlagen 5 bis 7 (je ein Sub-Agent);
|
||
die Seed-Infrastruktur selbst
|
||
- Die sieben Vorlagen als JSON im Austauschformat: 36 Abschnitte, 155 Schritte, 28 Variablen,
|
||
jeder Schritt mit Hilfetext, als veröffentlichte Version 1 im Seed
|
||
- Beispielbestand (3 Abteilungen, 5 Personen, `PC-001`, `NB-001`, 5 vorgedruckte Tags,
|
||
Schnellstart je Gerätetyp), deaktivierte Beispiel-Verzeichnisquelle, Namensregeln und Sperr-OU
|
||
- `SEED_MODE=DIENSTLEISTUNG` legt „Musterfirma GmbH" und „Bäckerei Reiter" an — der
|
||
Beispielbestand hängt dann an der Musterfirma, `LZ-7K3F9Q` führt auf `MUS-PC-001`
|
||
- Unabhängiger Review: **zwei Blocker**, neun Befunde, sieben Anmerkungen — alle abgearbeitet
|
||
- Zwei Zusagen der Oberfläche eingelöst, die niemand deckte: `{{ou_gesperrt}}` kommt jetzt wirklich
|
||
aus den Einstellungen, und der Hilfetext neben einem Befehl zeigt kein zweites Passwort mehr
|
||
- **1131 Tests** (vorher 1098) grün, im gebauten Stand durchgeklickt: Lauf aus der
|
||
Onboarding-Vorlage, `-Path "OU=Buchhaltung,OU=Benutzer,DC=firma,DC=local"` und
|
||
`"G-Buchhaltung, G-Alle"` kommen aus der Abteilung, kein roher Platzhalter bleibt stehen
|
||
|
||
### Entscheidungen
|
||
|
||
- **Die Vorlagen sind Daten, kein Code.** Als JSON im Austauschformat prüft sie dasselbe Schema wie
|
||
Import, Export und Editor: eine Seed-Vorlage ist genau das, was ein Redakteur einspielen könnte.
|
||
Wer eine ändern will, ändert eine Datei und braucht keinen Übersetzer.
|
||
- **Der Seed geht denselben Weg wie ein Redakteur**: erst Entwurf, dann veröffentlichen. Nicht aus
|
||
Stilgefühl — die Trigger aus #010 verbieten jedes Schreiben an einer veröffentlichten Version.
|
||
- **Der Seed richtet ein, er räumt nicht auf.** Kein `upsert` mit gefülltem `update`: eine
|
||
bearbeitete Vorlage, eine geänderte Ziel-OU und ein umgeschalteter Betriebsmodus überstehen jeden
|
||
weiteren Lauf.
|
||
- **Tag-Codes fest statt gezogen.** Ein Seed, der jedes Mal fünf neue Codes zieht, ist nicht
|
||
idempotent — beim dritten Lauf lägen fünfzehn Aufkleber im Stapel.
|
||
|
||
### Probleme
|
||
|
||
- **Die Datenbank hat den Seed korrigiert.** Der erste Entwurf legte die Version gleich als
|
||
`VEROEFFENTLICHT` an und füllte sie danach — die Trigger aus #010 haben das abgewiesen. Genau
|
||
dafür stehen sie dort.
|
||
- **Zwei verschiedene Passwörter auf einer Seite** (Blocker): die Hilfetexte trugen
|
||
`{{generiertes_passwort}}`, und das Lesemodell löste Hilfetexte ohne das Passwort des
|
||
Seitenaufbaus auf. Im Befehl stand A, im Hilfetext daneben B. Die Warnung davor stand seit #012
|
||
wörtlich in der Kopfdokumentation des Moduls, das beides erzeugt.
|
||
- **Ein verbrannter Tag-Code kam zurück** (Blocker): der Seed fragte nur nach dem Gerät, nicht nach
|
||
dem Vergabebuch — und der Kommentar derselben Funktion beschrieb genau den Fehler, den sie machte.
|
||
- **„Stattdessen" hieß „zusätzlich".** Im Modus DIENSTLEISTUNG hing der Beispielbestand weiter an
|
||
der eigenen Organisation; das Abnahmekriterium `LZ-7K3F9Q` → `MUS-PC-001` wäre in keinem Modus
|
||
erfüllt gewesen. Ein Wort im Auftrag, drei Dateien Arbeit.
|
||
- **Mein eigener Test konnte nicht scheitern** — zum zweiten Mal in zwei Meilensteinen. Die Suche
|
||
nach Klartextpasswörtern lief gegen `JSON.stringify`, wo vor jedem Anführungszeichen ein
|
||
Backslash steht; ein eingebautes `passwort = "Sommer2026!"` blieb unbemerkt. Gefunden durch die
|
||
Gegenprobe, die seitdem zum Schreiben eines solchen Tests gehört: einmal absichtlich verletzen.
|
||
- **Eine Wegwerfdatei im Wurzelverzeichnis** ließ `pnpm format:pruefen` scheitern, während der
|
||
Review lief. Sie gehört in das Verzeichnis für Zwischenstände, nicht neben den Quelltext.
|
||
- **Zum zweiten Mal gegen einen veralteten Server geprüft.** `pkill -f "next-server"` erwischte den
|
||
eigenen Shell-Prozess statt den Server; der lief mit dem Stand von neunzehn Minuten zuvor weiter.
|
||
Das sah aus wie ein Codefehler — `ChunkLoadError`, leere Editor-Seite, fünf rote Abläufe. Seitdem:
|
||
`ps -eo lstart` gegen `.next/BUILD_ID` halten, bevor man der Seite glaubt.
|
||
- **Der E2E-Lauf schrieb in eine andere Datenbank, als der Server las.** Die Abläufe legen ihre
|
||
Fixtures über `prisma` aus `.env` an, der Server bekommt seine Adresse über die Umgebung. Zeigen
|
||
beide woandershin, heißt die Meldung „0 Treffer" und sagt nichts über die Ursache.
|
||
- **Der Seed hat die E2E-Suite rot gemacht.** Die Abläufe wählten ihre Vorlage über `index: 1` —
|
||
eine Annahme über die Sortierung der Vorlagenliste, die genau so lange hielt, wie es nur eine
|
||
Vorlage gab. Jetzt wird sie am Namen gesucht.
|
||
|
||
### Nächster Schritt
|
||
|
||
- Zwei Folge-Issues aus dem Review: [#020](issues/020-vorgesetzter-manuell.md) (Vorgesetzter
|
||
für von Hand angelegte Personen) und
|
||
[#021](issues/021-stabiler-schluessel-vorlagen.md) (stabiler Schlüssel für Seed-Vorlagen)
|
||
- #018 E2E-Tests, Anleitung und PDF — die Hauptabläufe in beiden Betriebsmodi, die bebilderte
|
||
Anleitung für Techniker und Admins und der PDF-Export über Chromium.
|
||
|
||
---
|
||
|
||
## 2026-09-26 · Sitzung 14 · 05:30–10:50
|
||
|
||
**Issue:** #018 (M13)
|
||
**Version:** 0.1.0
|
||
|
||
### Erledigt
|
||
|
||
- **Vier Teile parallel**: vorhandene Abläufe robust machen, vier neue Abläufe, die Anleitung, der
|
||
PDF-Export. Das Fundament — gemeinsamer Helfer, Vorabprüfung, Betriebsmodus — kam vorher von mir
|
||
- **Anleitung** unter `docs/anleitung/`: neun Kapitel, rund 11.600 Wörter, getrennt für Technik und
|
||
Verwaltung, mit 35 Bildern und einem Fehlerbehebungskapitel aus 18 echten Meldungen
|
||
- **`pnpm docs:pdf`**: 72 Seiten A4, Deckblatt, Inhaltsverzeichnis, Seitenzahlen, hell gedruckt,
|
||
ohne neue Abhängigkeit (`react-markdown` über `react-dom/server`)
|
||
- **Markierungen** in den Screenshots: Rahmen in der Akzentfarbe mit Nummer, für die Stellen, die
|
||
ein Kapitel gerade beschreibt
|
||
- **Vier neue Abläufe**: Hauptablauf von vorn bis hinten, Onboarding mit Zwischenablage-Vergleich,
|
||
Rollen, und 390 px auf achtzehn Seiten
|
||
- **101 Ende-zu-Ende-Abläufe** grün (vorher 67), Gesamtsuite unverändert grün
|
||
|
||
### Entscheidungen
|
||
|
||
- **Die Voraussetzungen eines Laufs werden geprüft, nicht behauptet.** `tests/e2e/global-setup.ts`
|
||
fragt vor dem ersten Ablauf: läuft ein gebauter Stand, steht der Seed-Bestand, und liest der
|
||
Server **dieselbe** Datenbank wie der Testprozess? Jede Meldung nennt die Ursache und was zu tun
|
||
ist. Die dritte Frage hat in #017 einen halben Abend gekostet.
|
||
- **Das PDF kommt nicht ins Repo.** Es entsteht aus Kapiteln und Bildern, die beide versioniert
|
||
sind, und würde bei jedem Lauf 8,7 MB neu anhängen. Der Auftrag sagt „liegt jedem Release bei“ —
|
||
genau so wird es gemacht.
|
||
- **Die `beispiel:*`-Skripte bleiben.** Sie sollten laut ihrem Kopfkommentar mit #017 verschwinden;
|
||
dort wurden zwei Dinge verwechselt. Der Seed richtet eine **Installation** ein, die Skripte
|
||
stellen her, was **Screenshots und Abläufe** brauchen: einen offenen Entwurf, einen halb
|
||
abgearbeiteten Lauf, eine Person aus dem Verzeichnis.
|
||
|
||
### Probleme
|
||
|
||
- **Das Projekt „handy" prüfte 412 px statt 390.** `devices["Pixel 7"]` ist 412 breit; CLAUDE.md und
|
||
das Abnahmekriterium nennen 390. Jeder „trägt am Handy ohne horizontales Scrollen"-Test lief damit
|
||
gegen eine Breite, die niemand verlangt hatte — zweiundzwanzig Pixel, die eine ausbrechende Seite
|
||
tadellos aussehen lassen. Nach der Korrektur blieb die Suite grün; die Zusage hielt also, sie war
|
||
nur nie geprüft.
|
||
- **Eine Auskunft, die niemand je gesehen hat.** Ein angemeldeter Benutzer ohne Recht wurde auf
|
||
`/anmelden?fehler=keinrecht` geschickt — und die Anmeldeseite schickt einen Angemeldeten sofort
|
||
weiter auf die Übersicht. Die Meldung „Für diesen Bereich fehlt dir die Berechtigung" war
|
||
unerreichbar, der Kommentar im Code versprach sie trotzdem. Jetzt gibt es `/kein-zugriff`.
|
||
- **Zwei Tests, die Schaden anrichteten.** Der Verzeichnis-Ablauf nahm die erste vorhandene Quelle
|
||
und schrieb ihre Attributzuordnung um — in einer echten Installation die produktive. Und ein
|
||
Nachlauf setzte die Namensregeln auf leer, statt den vorherigen Wert zurückzustellen: er löschte,
|
||
was der Seed eingerichtet hatte. Beides stammt aus meinen eigenen Meilensteinen.
|
||
- **Vier stille Annahmen**: Vorlage über die Position in der Liste, „die Datenbank enthält schon
|
||
Läufe", `test.skip` bei fehlenden Daten, und Fixtures im Kopfkommentar statt geprüft. Alle vier
|
||
fielen erst auf, als zum ersten Mal gegen eine **frisch eingerichtete** Installation geprüft
|
||
wurde.
|
||
- **Sechs Abläufe kannten die Kundenebene nicht.** Im Modus DIENSTLEISTUNG hat jedes Anlege-Formular
|
||
ein Feld mehr, und es ist Pflicht: `kundeErmitteln` wirft sonst. Schlimmer als das Feld ist die
|
||
Regel dahinter — ein Lauf kann strukturell kein Gerät und keine Abteilung eines fremden Kunden
|
||
wählen, dafür sorgen zusammengesetzte Fremdschlüssel. „Irgendein Kunde" ist dort nicht ungenau,
|
||
sondern rot. Beim Onboarding war es sogar eine Reihenfolge: die Kundenwahl stand hinter dem
|
||
Namensvorschlag, und ohne Kunden können die Namensregeln gar nicht greifen.
|
||
- **Parallele Arbeit an einem Verzeichnis geht schief.** Eine Wegwerf-Probe für den PDF-Bau hat die
|
||
`README.md` der Anleitung überschrieben, während sie geschrieben wurde. Wiederhergestellt, aber
|
||
der Ablageort für Proben gehört in das Verzeichnis für Zwischenstände, nicht neben die Arbeit.
|
||
|
||
### Nächster Schritt
|
||
|
||
- #019 Betriebswege und Release `v0.1.0` — `deploy/install.sh`, der Proxmox-One-Liner, die
|
||
Betriebsdokumentation und der Tag mit Release-Notes.
|
||
|
||
---
|
||
|
||
## 2026-09-26 · Sitzung 15 · 10:50–16:15
|
||
|
||
**Issue:** #019 (M14)
|
||
**Version:** 0.1.0
|
||
|
||
### Erledigt
|
||
|
||
- **`deploy/install.sh`** (1058 Zeilen): neun Schritte von den Voraussetzungen bis zum gestarteten
|
||
Dienst, PostgreSQL 17 nur am Unix-Socket (`listen_addresses = ''`), Dienstnutzer ohne Login,
|
||
Geheimnisse im Zielsystem erzeugt, systemd-Unit im Skript, `--test` als Selbstprüfung
|
||
- **`deploy/proxmox/laufzettel.sh`** (963 Zeilen): der Einzeiler im Stil der Helper-Scripts, Dialog
|
||
Standard gegen Erweitert, Trockenlauf, Erkennung eines vorhandenen Containers, `--aktualisieren`
|
||
- **`docs/betrieb/`**: Inbetriebnahme, Proxmox, Reverse Proxy, Sicherung und eine Übersicht — dazu
|
||
eine Prüfliste für den Teil, den hier niemand ausführen konnte
|
||
- **Der gehärtete Compose-Stand einmal wirklich gefahren**, nicht nur gelesen: Migrationen laufen von
|
||
selbst, der Seed bleibt ein Handgriff, `5432` ist vom Host dicht und aus `web` erreichbar
|
||
- **Abnahme gegen alle 61 Kriterien** durch einen Agenten, der den Code nicht geschrieben hat:
|
||
57 erfüllt, drei Befunde noch vor dem Tag behoben, drei Folge-Issues (#024–#026)
|
||
- **Release `v0.1.0`**: Changelog-Abschnitt, Tag, Gitea-Release mit dem Anleitungs-PDF
|
||
|
||
### Entscheidungen
|
||
|
||
- **Die Umgebungsdatei wird nie überschrieben, auch nicht beim Aktualisieren.** Sonst würde ein
|
||
Aktualisierungslauf das `APP_SECRET` neu erzeugen: alle Sitzungen enden, und die verschlüsselten
|
||
LDAP-Bind-Passwörter wären unlesbar. Wer Schalter mitgibt, die deshalb nicht mehr wirken, bekommt
|
||
eine Warnung statt stillen Gehorsams.
|
||
- **Das Erstpasswort reist über eine Leihgabe, nicht über die Ausgabe.** Der Proxmox-Weg kann die
|
||
Ausgabe von `install.sh` hinter `pct exec` nicht mitlesen. Sie mitzuschneiden und zu durchsuchen
|
||
hätte das Passwort durch Pipes und Protokolldateien geschleift und von Formulierungen abgehangen.
|
||
Stattdessen legt `install.sh` es auf Wunsch in eine Datei (0600, root), der Aufrufer liest sie
|
||
einmal und löscht sie sofort — gelesen oder nicht.
|
||
- **shellcheck läuft in CI im Container**, nicht aus dem Paketverzeichnis des Runners. So prüft die
|
||
Pipeline genau die Fassung, die hier lokal geprüft wurde, und der Runner muss nichts mitbringen.
|
||
- **Kriterium 4 bleibt offen statt „im Grunde erfüllt“.** Der `pct`-Teil ist nie gelaufen; hier stand
|
||
kein Proxmox-Host. Abgehakt wird er, wenn ihn ein Mensch einmal gefahren hat — Prüfliste und
|
||
Folge-Issue #024 stehen dafür bereit.
|
||
|
||
### Probleme
|
||
|
||
- **Der dokumentierte Weg startete nicht.** Der Compose-Stand las `DATABASE_URL` aus der `.env` —
|
||
dort steht die Entwicklungsadresse mit `localhost`, die im Container ins Leere zeigt. Und der
|
||
Einmal-Dienst für die Migration bekam kein `APP_SECRET`, weshalb der dokumentierte Seed-Befehl
|
||
mitten im Lauf abbrach, nachdem die Vorlagen schon angelegt waren. Beides fiel erst auf, als der
|
||
Stand zum ersten Mal wirklich gefahren wurde — die Suite war die ganze Zeit grün. **Ein Fehler im
|
||
dokumentierten Weg eines Dritten ist für eine Testsuite unsichtbar.**
|
||
- **Ein Healthcheck, der nie bestehen konnte.** busybox löst `localhost` im Container zuerst nach
|
||
`::1` auf, Next lauscht nur auf IPv4. `docker compose ps` zeigte `web` dauerhaft als `unhealthy`,
|
||
während die App einwandfrei antwortete. Jedes `docker compose up --wait` wäre in den Zeitablauf
|
||
gelaufen. Wieder eine Zusage ohne Deckung — jetzt hält ein Test die Adresse fest.
|
||
- **Die CI-Pipeline konnte in keinem Job grün werden.** Dem Prüf-Job fehlte das LDAP-Testverzeichnis,
|
||
das `pnpm test` dort ausdrücklich verlangt; dem E2E-Job fehlte ein gestarteter Server, weshalb die
|
||
Vorabprüfung aus #018 sofort abgebrochen hätte; und `shellcheck` wurde aufgerufen, ohne installiert
|
||
zu sein. „Qualitätsschranken“ galt damit nur lokal. Gefunden hat das die Abnahme, nicht die Arbeit
|
||
daran.
|
||
- **Ich habe „shellcheck ohne Befund“ gemeldet, ohne es zu sein.** Der Satz kam aus einem `echo`, das
|
||
unabhängig vom Ergebnis lief, während shellcheck vier SC1111-Warnungen ausgab. Das ist derselbe
|
||
Fehler, den dieser Devlog seit #010 als teuersten beschreibt, nur diesmal in meiner eigenen
|
||
Berichterstattung. Richtiggestellt, dann behoben — und seither wird jede Behauptung über ein
|
||
Prüfergebnis am Exit-Status gemessen.
|
||
- **Zwei Skripte, eine Naht.** `laufzettel.sh` übergibt seine Werte als Umgebungsvariablen, weil
|
||
`pct exec` keine Befehlszeile durchreicht; `install.sh` las zunächst nur Schalter. Der Dienst wäre
|
||
auf `127.0.0.1` gebunden geblieben, während der Abschlussbildschirm `http://<ip>:3000` versprach.
|
||
Nahtstellen zwischen zwei Skripten prüft keine Suite — nur das Lesen beider Seiten.
|
||
- **Der README nannte Port 5433, die Entwicklungsdatenbank hört auf 5434.** Wer dem README folgte,
|
||
landete auf der Datenbank eines anderen Projekts. Der Hinweis darauf heißt „0 Treffer“ — genau die
|
||
Meldung, die in #016 und #018 schon je einen halben Abend gekostet hat.
|
||
|
||
### Nächster Schritt
|
||
|
||
- Phase 1 ist abgeschlossen und veröffentlicht. Offen für `v0.2.0`: #020–#026, darunter der echte
|
||
Proxmox-Lauf (#024), der die letzte offene Zeile der Abnahme schließt.
|
||
|
||
---
|
||
|
||
## 2026-09-26 · Sitzung 16 · 16:00–16:45
|
||
|
||
**Issues:** #025, #026 (beide aus der Abnahme zu #019)
|
||
**Version:** 0.2.0 (unveröffentlicht)
|
||
|
||
### Erledigt
|
||
|
||
- **`/api/gesundheit` verrät nichts mehr.** Der Pfad ist ohne Anmeldung erreichbar; eine Meldung von
|
||
Prisma oder `pg` nennt Host, Port, Benutzernamen und Datenbanknamen. Nach außen bleibt
|
||
`{"status":"fehler"}` mit 503, die Begründung geht ins Protokoll — dasselbe Muster, das
|
||
`src/server/actions/verzeichnis.ts` schon für unbekannte Ausnahmen benutzt
|
||
- **Fünf Tests** dafür, einer davon für die andere Hälfte der Zusage: dass die Begründung wirklich
|
||
**geschrieben** und nicht bloß verschluckt wird
|
||
- **Die Hexwert-Regel nennt ihre Ausnahmen.** Sechs Stellen, als Tabelle mit Begründung je Zeile
|
||
- **Der LDAP-Container steht jetzt im Tor**, in `CLAUDE.md` und im README — mit der gemessenen Zahl
|
||
- **`pnpm pruefe:doku` vergleicht die beiden Schranken-Blöcke** und verlangt von beiden den Container
|
||
|
||
### Entscheidungen
|
||
|
||
- **Die Regel wurde ergänzt, nicht der Code geändert.** Alle sechs Hexwerte haben denselben Grund:
|
||
dort entscheidet nicht unser Farbschema, sondern etwas außerhalb davon — Papier, ein QR-Decoder,
|
||
das Web-App-Manifest. Sie in eine Konstante zu ziehen hätte eine Abstraktion über drei
|
||
unverwandte Dinge gelegt und die Begründung versteckt. Die Tabelle in `CLAUDE.md` zeigt sie.
|
||
- **Der Hinweis auf den Container reicht nicht, er muss geprüft werden.** Zwei Dateien beschreiben
|
||
dasselbe Tor und waren auseinandergelaufen. Ein Satz in einer Konvention hält nur, solange ihn
|
||
jemand liest; die sechste Prüfung in `pruefe:doku` hält ihn, auch wenn niemand hinsieht.
|
||
|
||
### Probleme
|
||
|
||
- **Ich hätte „rund zwanzig Tests" in eine Konvention geschrieben.** Die Zahl stammte aus dem
|
||
Abnahmebericht (dort: 18), nicht aus einer Messung. Nachgezählt sind es **22** — mit Container 81,
|
||
ohne 59. Eine ungefähre Zahl in einer verbindlichen Datei ist derselbe Fehlertyp, den dieses
|
||
Projekt vierzehn Meilensteine lang gesammelt hat, nur kleiner.
|
||
- **Der Review hat eine Ausnahme übersehen.** Er nannte vier rohe Hexwerte, es sind sechs:
|
||
`src/lib/qr.ts` fehlte. Das Issue #026 hat die falsche Zahl von ihm übernommen; beim Abhaken wäre
|
||
ein richtiger Haken über falschem Text entstanden. Issue-Text korrigiert, dann abgehakt.
|
||
- **`docs/architektur.md` trägt die Gestaltungsregel nicht**, obwohl `CLAUDE.md` verlangt, dass
|
||
Änderungen dort mitgehen und das Issue es behauptete. Geprüft statt geglaubt.
|
||
|
||
### Nächster Schritt
|
||
|
||
- Phase 2 (#002) oder die vier verbliebenen v0.2.0-Issues: #020 Vorgesetzter, #021 stabiler
|
||
Schlüssel, #022 Schnellstart pflegen, #023 Protokoll-Ansicht. #024 wartet auf einen PVE-Host.
|
||
|
||
---
|
||
|
||
## Phase 1 abgeschlossen: was aus dem Plan wurde
|
||
|
||
**35 h 20 min** über vierzehn Meilensteine, geschätzt waren **148 h**. Der Faktor ist kein Verdienst,
|
||
sondern eine Eigenschaft der Arbeitsweise: parallele Sub-Agenten an Stellen, wo die Schnittstelle
|
||
vorher feststand, und ein Mensch, der nur an den Nahtstellen eingreift. Die Schätzung war für
|
||
Menschenarbeit gedacht und bleibt als Maßstab stehen, weil sie den **Umfang** beschreibt.
|
||
|
||
### Was entstanden ist
|
||
|
||
| | |
|
||
|---|---|
|
||
| Quelltext | rund 30.000 Zeilen unter `src/` (ohne erzeugten Prisma-Client) |
|
||
| Datenmodell | 20 Tabellen, 9 Aufzählungen, 2 Migrationen |
|
||
| Tests | 1131 Vitest-Tests, 101 Ende-zu-Ende-Abläufe (`INTERN`), 97 (`DIENSTLEISTUNG`) |
|
||
| Vorlagen | 7 Abläufe mit 155 Schritten, als veröffentlichte Version 1 im Seed |
|
||
| Dokumentation | Anleitung in neun Kapiteln (72 Seiten PDF), Architektur, Betrieb, 14 Issue-Dateien |
|
||
|
||
### Wo die Zeit hinging
|
||
|
||
Der teuerste Meilenstein war **#019 Betriebswege** (5 h 25) — und zwar nicht wegen der beiden
|
||
Skripte, sondern weil dort zum ersten Mal der **dokumentierte Weg eines Dritten** gefahren wurde:
|
||
Compose-Stand hochziehen, Seed nach Anleitung, Installation in einem leeren Debian. Vier Fehler lagen
|
||
genau dort, wo keine Suite hinsieht, und die Suite war die ganze Zeit grün.
|
||
|
||
Dahinter **#014 QR-Tags** (3 h 35), **#016 Verzeichnis** (3 h 25) und **#013 Läufe** (3 h 05) — alle
|
||
drei nicht wegen des Umfangs, sondern wegen der Randfälle: ein Scanner, der die native Schnittstelle
|
||
nie benutzte; ein Abgleich, der bei einem Tippfehler die ganze Firma abgeschaltet hätte; ein Passwort,
|
||
das zweimal verschieden erzeugt wurde. Keiner dieser Fehler fiel beim Bauen auf. Alle drei fand der
|
||
unabhängige Review.
|
||
|
||
### Was sich als richtig erwiesen hat
|
||
|
||
- **Ein Review-Agent je Meilenstein, der den Code nicht geschrieben hat.** In **jedem** der vierzehn
|
||
fand er etwas, in neun davon etwas Ernstes. Die teuersten Funde waren nie Tippfehler, sondern
|
||
Zusagen ohne Deckung: ein Kommentar, eine Oberflächenmeldung oder ein Testname, der etwas behauptet,
|
||
das der Code nicht tut. Das ist der häufigste Fehlertyp dieses Projekts — vor Logikfehlern, weit
|
||
vor Tippfehlern.
|
||
- **Regeln in der Datenbank, nicht nur im Dienst.** Zwölf CHECK-Bedingungen, zwei partielle
|
||
Eindeutigkeitsindizes, vier zusammengesetzte Fremdschlüssel und die Trigger auf veröffentlichten
|
||
Versionen haben mehrfach Fehler abgefangen, die durch die Service-Schicht gekommen wären — zuletzt
|
||
in #017, als der Seed eine veröffentlichte Version füllen wollte.
|
||
- **Gegen den gebauten Stand prüfen.** `next dev` hydriert in dieser Umgebung nicht; ein Screenshot
|
||
beweist nichts über Bedienbarkeit. Die Regel steht seit #010 in `CLAUDE.md` und hat sich in jedem
|
||
Oberflächen-Meilenstein bewährt.
|
||
|
||
### Was ich anders machen würde
|
||
|
||
- **Zweimal gegen einen veralteten Server geprüft** (#016, #018), beide Male eine halbe Stunde. Die
|
||
Konsequenz steht jetzt in der Arbeitsweise: erst `ps -eo lstart` gegen `.next/BUILD_ID` halten,
|
||
dann der Seite glauben.
|
||
- **Einmal in die falsche Datenbank geschrieben** (#017): Testprozess und Server zeigten
|
||
woandershin, und die Meldung hieß „0 Treffer". Seit #018 prüft das ein `global-setup` vorab.
|
||
- **Zwei eigene Tests konnten nicht scheitern** (#017, #018). Seitdem gehört die Gegenprobe zum
|
||
Schreiben eines Tests: einmal absichtlich verletzen und sehen, ob er rot wird.
|
||
- **Parallele Agenten im selben Verzeichnis** haben sich einmal gegenseitig eine Datei überschrieben
|
||
(#018). Die Trennung muss an Dateien verlaufen, nicht an Themen.
|
||
|
||
### Was bewusst offen bleibt
|
||
|
||
Sechs Folge-Issues für `v0.2.0`: Vorgesetzter für von Hand angelegte Personen (#020), stabiler
|
||
Schlüssel für Seed-Vorlagen (#021), Schnellstart je Gerätetyp pflegen (#022), Protokollansicht
|
||
(#023). Dazu die Punkte, die der Abnahme-Review aufwirft. Nichts davon blockiert den Betrieb; alles
|
||
davon steht geschrieben, statt verschwiegen zu werden.
|
||
|
||
---
|
||
|
||
## 2026-10-04 · Sitzung 17 · der ganze Tag
|
||
|
||
**Issues:** #021, #020, #022, #023, #046/#047, Phase-2-Plan, #030 (M17), #031 (M18), #032 (M19)
|
||
**Version:** 0.2.0 (unveröffentlicht)
|
||
|
||
Eine lange Sitzung mit acht zusammengeführten Änderungen. Der Devlog-Eintrag kommt am Ende des Tages
|
||
und nicht nach jeder einzelnen — das ist der Grund, warum er alles auf einmal trägt.
|
||
|
||
### Erledigt
|
||
|
||
- **Der Rückstand für `v0.2.0` ist leer.** Stabiler Seed-Schlüssel (#021), Vorgesetzter für von Hand
|
||
angelegte Personen (#020), Schnellstart je Gerätetyp in der Oberfläche pflegen (#022), und das
|
||
Protokoll ist ansehbar geworden (#023) — geschrieben wurde es seit dem Datenmodell, ansehen konnte
|
||
es niemand.
|
||
- **Phase 2 in acht Meilensteine geschnitten** (#028–#035), mit drei Entscheidungen, die vor dem
|
||
ersten Codepfad fallen mussten.
|
||
- **M17 Bausteine** (#030): ein Abschnitt, den mehrere Vorlagen teilen, versioniert nach denselben
|
||
Regeln wie eine Vorlage. Eingesetzt wird er als tiefe Kopie beim **Veröffentlichen** — damit ändert
|
||
eine neue Baustein-Fassung keine veröffentlichte Vorlagenversion und keinen laufenden Lauf.
|
||
- **M18 Abteilungsabschnitte** (#031): ein Abschnitt kann sagen, für welche Abteilungen er gilt.
|
||
Welche Abschnitte ein Lauf hat, wird beim Start festgeschrieben.
|
||
- **M19 Anhänge** (#032): Fotos und Dateien an einem Schritt, am Handy direkt aus der Kamera. Dateien
|
||
im Dateisystem, Metadaten in der Datenbank, jeder Abruf prüft Recht und Mandant.
|
||
- **Sicherung und Rücksicherung der Anhänge wirklich gefahren** — Abzug gezogen, Datenbank verworfen,
|
||
Verzeichnis gelöscht, zurückgespielt, und die ausgelieferten Bytes gegen die gespeicherte
|
||
`pruefsumme` gehalten.
|
||
- 1246 Vitest-Tests (von 1130 am Morgen), alle Schranken grün.
|
||
|
||
### Entscheidungen
|
||
|
||
- **Ein Baustein wird beim Veröffentlichen aufgelöst, nicht beim Lesen.** Eine Auflösung zur Laufzeit
|
||
wäre weniger Code gewesen und hätte die Zusage „eine veröffentlichte Version wird nie verändert“
|
||
gebrochen — über eine Hintertür, die niemand so nennt.
|
||
- **`Lauf.geltendeAbschnitte` ist leer heißt „alle“.** Damit verhalten sich Läufe von vor #031
|
||
unverändert, ohne Migration von Daten.
|
||
- **Anhänge ins Dateisystem, nicht als `bytea`.** Ein `pg_dump` mit Fotos ist nach einem Jahr kein
|
||
Abzug mehr, den jemand einspielt. Der Preis ist die zweite Hälfte der Sicherung, und der steht
|
||
jetzt in `docs/betrieb/sicherung.md`.
|
||
- **Hochgeladen wird über eine Server Action, nicht über einen zweiten Endpunkt.** Das Issue hatte
|
||
einen Route Handler vorgesehen; der wäre eine zweite Tür mit eigener Rechteprüfung. Der Route
|
||
Handler bleibt für das Ausliefern, und er ruft `rechtErforderlich` selbst — die Hülle unter `(app)`
|
||
deckt Route Handler nicht ab.
|
||
- **Die Regel „geltende Abschnitte“ liegt in `src/lib`**, nicht in einem Lesemodell. Drei Schichten
|
||
brauchen sie; lebte sie in einem Lesemodell, importierte ein Dienst daraus.
|
||
|
||
### Probleme
|
||
|
||
- **Ein Fehler, den neunzehn grüne Tests nicht sehen konnten.** Der Schritt hat zwei Felder `datei`
|
||
in einem Formular — „Foto“ und „Datei“ —, abgeschickt wird immer beides, und `FormData.get` nahm
|
||
das erste, also das leere. Wer „Datei“ wählte, bekam „Keine Datei gewählt“, während „Foto“ ging.
|
||
Kein Dienst-Test schickt zwei Felder; gefunden hat es erst die Klickprobe am gebauten Stand. Die
|
||
Lehre ist dieselbe wie in #019: **Fehler im dokumentierten Weg eines Dritten sind für eine grüne
|
||
Suite unsichtbar.** Seitdem gibt es `tests/e2e/anhaenge.spec.ts`, der bewusst über den zweiten
|
||
Knopf hochlädt, und die Gegenprobe wurde gefahren: mit `get` statt `getAll` wird er rot.
|
||
- **`docs/betrieb/sicherung.md` enthielt einen falschen Satz**, nicht bloß eine Lücke: „Kein
|
||
Upload-Verzeichnis, keine erzeugten Dateien.“ Mit den Anhängen hätte eine Sicherung nach dieser
|
||
Anleitung still Daten verloren. Die Datei ist neu geschrieben.
|
||
- **Zweimal habe ich dem Produkt angelastet, was mein Prüfskript falsch machte** — ein zu weiter
|
||
`role="status"`-Selektor und eine Prüfung direkt nach `reload()`, als `networkidle` zu früh
|
||
zurückkam. Die Datenbank hat beide Male entschieden, und sie hat dem Produkt recht gegeben.
|
||
- **Einmal die Ursache falsch benannt.** Ich hielt eine `positionenSauber`-Prüfung für die Behebung
|
||
des Schnellstart-Fehlers; die Gegenprobe zeigte, dass die Tests ohne sie grün bleiben. Die
|
||
Behebung ist der deterministische Stichentscheid beim Sortieren.
|
||
- **Ein Numerusfehler in einem eigenen Hinweistext** („Ein Abschnitt gilt … und *bleiben* weg“),
|
||
gefunden beim Durchklicken. Alles, was ein Mensch liest, schreibt sich richtig.
|
||
- **`tests/datenbankregeln.test.ts` wurde rot** — genau wie gewollt: er zählt die handgeschriebenen
|
||
Datenbankobjekte namentlich auf, und die zwei neuen CHECK-Bedingungen und zwei zusammengesetzten
|
||
Fremdschlüssel standen nicht drin.
|
||
- **`/daten/` fehlte in `.gitignore`.** Ohne das läge das erste hochgeladene Foto im nächsten Commit.
|
||
|
||
### Nächster Schritt
|
||
|
||
- **M20 (#033)** Live-Aktualisierung per SSE statt Polling, dann **M21 (#034)** Benachrichtigungen,
|
||
**M22 (#035)** Selbstauskunft. **M15 (#028)** Graph-Quelle und **M16 (#029)** SCIM sind die beiden
|
||
großen verbliebenen.
|
||
- Weiter offen und nicht von hier aus machbar: **#024** Proxmox-Einzeiler auf einem echten PVE-Host.
|
||
- **#027** Die Editor-Abläufe lassen ihre Vorlagen in der Entwicklungsdatenbank stehen.
|
||
|
||
---
|
||
|
||
## 2026-10-05 · Sitzung 18 · 09:00–10:40
|
||
|
||
**Issue:** #033 (M20)
|
||
**Version:** 0.2.0 (unveröffentlicht)
|
||
|
||
### Erledigt
|
||
|
||
- **Die Live-Aktualisierung läuft über einen Ereignisstrom.** Vier Trigger schicken
|
||
`NOTIFY lauf_geaendert, '<laufId>'`, eine LISTEN-Verbindung je Prozess hört zu, der Route Handler
|
||
schiebt weiter. Gemessen: der Haken ist nach **485 ms** beim Zuschauer (Handy: 487 ms), vorher
|
||
bis zu zehn Sekunden.
|
||
- **Das Polling bleibt als Rückfall** — an derselben Adresse, unterschieden nur am `Accept`-Kopf.
|
||
- 1266 Vitest-Tests (vorher 1247), dazu `tests/e2e/live.spec.ts` mit zwei Browser-Kontexten.
|
||
- Der Herzschlag ist **echt über 70 Sekunden** gefahren: `readyState` blieb `OPEN`, ein einziges
|
||
Ereignis, kein Neuaufbau.
|
||
|
||
### Entscheidungen
|
||
|
||
- **Das Ereignis kommt aus der Datenbank, nicht aus dem Dienst.** Ein Ereignis, das der Service
|
||
verschickt, entsteht nur auf dem Weg durch den Service — ein Trigger sieht auch den Seed, eine
|
||
Migration und die Reparatur per `psql`. Und `NOTIFY` erreicht jede Instanz, ohne einen zweiten
|
||
Dienst wie Redis.
|
||
- **Ein Handler für beide Wege.** Damit ist „das Polling bleibt als Rückfall, nicht als zweite
|
||
Wahrheit“ keine Absichtserklärung, sondern Bauart: eine Rechteprüfung, ein Lesemodell, eine
|
||
Nutzlast.
|
||
- **Die Obergrenze braucht keinen Sonderfall.** Ist sie erreicht, antwortet der Handler gewöhnlich
|
||
mit JSON; `EventSource` meldet darauf einen Fehler und versucht es nicht wieder, und der Client
|
||
fällt von sich aus zurück. Kein eigener Fehlercode.
|
||
- **Keine Verbindung aus dem Prisma-Pool.** Ein Pool gibt Verbindungen zurück und verteilt sie neu;
|
||
ein `LISTEN` darauf gilt danach für eine fremde Anfrage oder für niemanden.
|
||
- **Bei versteckter Lasche wird der Strom geschlossen**, nicht bloß ignoriert — sonst belegte jede
|
||
Hintergrundlasche genau die Verbindung, die die Obergrenze schützen soll.
|
||
|
||
### Probleme
|
||
|
||
- **Das Issue hätte einen Rückschritt gebaut.** Es nannte Trigger auf `Schrittergebnis` und `Lauf`.
|
||
`laufZustand` trägt aber auch die Zahl der Kommentare, und seit #032 gibt es Anhänge: der Strom
|
||
sieht **nur**, was gemeldet wird, während das Polling den ganzen Stand verglich. Mit den zwei
|
||
Triggern aus dem Issue wäre der Kommentar des Kollegen nie angekommen — und die Seite hätte
|
||
ausgesehen wie immer. Das ist derselbe Fehlertyp wie in #032: eine Zusage, die niemand prüft.
|
||
Jetzt melden vier Tabellen, `laufZustand` trägt `anzahlAnhaenge`, und beide Tests zählen namentlich
|
||
auf, was dazugehört.
|
||
- **Zwei Fehler in `reverse-proxy.md`, beide von vorgestern.** `client_max_body_size 8m` bei
|
||
`ANHANG_MAX_MB=10`: ein 10-MB-Foto wäre an nginx mit 413 gescheitert, und die eigene Meldung
|
||
(„Erlaubt sind höchstens 10,0 MB“) wäre nie erschienen. Dazu ein Kommentar, der noch das alte
|
||
Polling beschrieb. **Gefunden beim Schreiben der Doku, nicht von einem Test** — Dateien, die ein
|
||
Dritter liest, prüft hier niemand.
|
||
- **`## 5e` war in `docs/architektur.md` schon belegt.** Statt alles umzunummerieren und Verweise zu
|
||
riskieren, hängt der neue Text als `###` unter 5d.
|
||
- **Zwei eigene Testfehler, keine Produktfehler:** `schritte` zählte die „erledigt“-Knöpfe der
|
||
ganzen Seite (34) statt die des ersten Abschnitts (5), und ein `const` nach einem `?.`-Guard
|
||
verlor in der Closure seine Verengung. Beide Male war die Meldung eindeutig.
|
||
|
||
### Nächster Schritt
|
||
|
||
- **M21 (#034)** Benachrichtigungen per Mail und Webhook — baut direkt auf den Ereignissen auf, die
|
||
jetzt stehen. Danach **M22 (#035)** Selbstauskunft.
|
||
- **M15 (#028)** Graph-Quelle und **M16 (#029)** SCIM sind die beiden großen verbliebenen.
|
||
- Weiter offen und nicht von hier aus machbar: **#024** Proxmox-Einzeiler auf einem echten PVE-Host.
|
||
|
||
---
|
||
|
||
## 2026-10-05 · Sitzung 19 · 10:45–14:15
|
||
|
||
**Issue:** #034 (M21)
|
||
**Version:** 0.2.0 (unveröffentlicht)
|
||
|
||
### Erledigt
|
||
|
||
- **Vier Anlässe gehen hinaus**, per Mail über SMTP und optional per Webhook mit
|
||
HMAC-SHA256-Signatur. Je Person abschaltbar, und niemand bekommt Post über die eigene Tat.
|
||
- **Eine Warteschlange in der Datenbank**, abgearbeitet von einem eigenen Zeitplan im selben
|
||
Prozess. Fehlschläge nach 1, 5, 30 und 120 Minuten wiederholt, danach sichtbar aufgegeben.
|
||
- **Verwaltung → Zustellungen** zeigt, was hinausging und was nicht, mit Grund.
|
||
- 1302 Vitest-Tests (vorher 1266), davon 36 neue — **gegen einen echten SMTP-Fänger** (mailpit),
|
||
nicht gegen eine abgefangene Funktion.
|
||
- Im gebauten Stand gefahren: SMTP eingetragen, Testmail angekommen, ein überfälliger Lauf angelegt,
|
||
und der Zeitplan hat die Meldung **von selbst** verschickt.
|
||
|
||
### Entscheidungen
|
||
|
||
- **Eingereiht im Anfrage-Pfad, gesendet vom Zeitplan.** Ein hängender Mailserver darf kein Abhaken
|
||
blockieren. Das Einreihen passiert nach der Transaktion und mit abgefangenem Fehler.
|
||
- **Betreff und Rumpf stehen fertig in der Zeile.** Eine Meldung sagt, was *damals* war; würde der
|
||
Text erst beim Senden gebaut, stünde in einer Mail von heute der Stand von jetzt.
|
||
- **Die Texte liegen in `src/lib/meldungen.ts` und nehmen nur Kopfdaten.** Der Typ lässt keinen
|
||
Schrittinhalt zu — das ist die Absicht, und es ist die einzige Stelle, die man dafür ändern müsste.
|
||
- **Die Sperre wurde aus dem Abgleich herausgezogen** nach `services/sperre.ts`. Zwei Fassungen
|
||
derselben Mechanik wären die schlechteste aller Lösungen.
|
||
- **Die Mandantengrenze gilt auch für Empfänger.** Ohne diese Prüfung wäre die Benachrichtigung der
|
||
eine Weg nach draußen, den `sichtbareKundenIds` nicht abdeckt.
|
||
|
||
### Probleme
|
||
|
||
- **pnpm wollte eine Sicherheitsregel umgehen, und ich hätte es fast committet.** `pnpm add
|
||
nodemailer` trug von sich aus `minimumReleaseAgeExclude: nodemailer@10.0.15` in
|
||
`pnpm-workspace.yaml` ein — die Version war **vom selben Tag**. Das ist kein Formfehler, sondern
|
||
genau die Warnung, vor der die Regel schützt. Gepinnt ist jetzt `10.0.10` vom 14. September, die
|
||
Ausnahme ist weg, und die Konvention steht in `CLAUDE.md`. Aufgefallen beim Durchsehen des Diffs,
|
||
nicht durch einen Test — eine Zeile in einer Datei, die man leicht übersieht.
|
||
- **Drei Tests waren stillschweigend übersprungen.** `it.runIf(mailpitDa)` wird beim **Einlesen** der
|
||
Datei ausgewertet; mein Flag stand in `beforeAll` und kam zu spät. Der erste Lauf meldete
|
||
„3 skipped“, war grün — und hatte den echten SMTP-Dialog nie geprüft. Genau das Muster, das
|
||
`ldap` in #019 schon einmal gekostet hat. `mailpit` steht jetzt im Tor vor dem Commit, und
|
||
`pruefe:doku` erzwingt es; gegengeprobt.
|
||
- **Die Wartezeiten waren um eins verschoben.** Nach dem ersten Fehlschlag wartete meine erste
|
||
Fassung fünf Minuten statt einer. Der Test hat es sofort gezeigt — und die Umbenennung des
|
||
Parameters auf `versucheGesamt` macht die Semantik jetzt lesbar.
|
||
- **Zwei Knöpfe „Speichern“ auf einer Seite**, beim Durchklicken gefunden. Der neue heißt jetzt
|
||
„Benachrichtigungen speichern“.
|
||
- **Drei eigene Testfehler**, keine Produktfehler: die `Verzeichnisquelle` hat `url`, nicht
|
||
`host`/`port`; `Lauf` mit Status `ABGESCHLOSSEN` braucht `abgeschlossenAm` (die Datenbank verlangt
|
||
es); und ein fester Testzeitpunkt lag **vor** der echten Einreihung, sodass der Durchgang die
|
||
Zeile gar nicht erst anfasste.
|
||
|
||
### Nächster Schritt
|
||
|
||
- **M22 (#035)** Mitarbeiter-Selbstauskunft — dort kommt auch die Profilseite, auf der jeder seine
|
||
eigenen Benachrichtigungen abschalten kann.
|
||
- **M15 (#028)** Graph-Quelle und **M16 (#029)** SCIM sind die beiden großen verbliebenen.
|
||
- Weiter offen und nicht von hier aus machbar: **#024** Proxmox-Einzeiler auf einem echten PVE-Host.
|
||
|
||
---
|
||
|
||
## 2026-10-06 · Sitzung 20 · 06:00–08:50
|
||
|
||
**Issue:** #035 (M22)
|
||
**Version:** 0.2.0 (unveröffentlicht)
|
||
|
||
### Erledigt
|
||
|
||
- **Die fünfte Rolle `MITARBEITER`** mit genau einem Recht: `selbstauskunft`. Zugeordnet beim
|
||
Anmelden über den UPN, festgehalten in `Benutzer.mitarbeiterId`.
|
||
- **`/meine-geraete`**: meine Geräte, je Gerät die Arbeiten chronologisch, dazu meine Stammdaten aus
|
||
dem Verzeichnis. Titel, Zeitraum, Status — nichts weiter.
|
||
- **Drei PRs von gestern sind auf `main`** (#59, #60, #61); die beiden Folgezweige mussten nach dem
|
||
ersten Squash rebased werden.
|
||
- 1341 Vitest-Tests (vorher 1302), davon 25 neue. Im gebauten Stand mit einem echten
|
||
MITARBEITER-Konto durchgeklickt.
|
||
|
||
### Entscheidungen
|
||
|
||
- **`selbstauskunft` ist ein Ersatz für `lesen`, kein Zusatz** — deshalb hat auch ein ADMIN es
|
||
nicht. Ein zweiter Weg zu denselben Daten wäre eine zweite Stelle, an der jemand die
|
||
Einschränkung auf die eigene Person vergisst.
|
||
- **Was zurückkommt, entscheidet das Lesemodell und nicht die Seite.** Eine Seite, die filtert, ist
|
||
eine Seite, die es vergessen kann.
|
||
- **Kein zusammengesetzter Fremdschlüssel**, obwohl die Aufgabe ihn verlangte: ein `Benutzer` trägt
|
||
keine `kundeId`. Stattdessen ein CHECK, dass Rolle und Zuordnung zusammengehören, und ein
|
||
eindeutiger Index, dass eine Person höchstens ein Konto hat.
|
||
- **Ein mehrdeutiger UPN wird abgewiesen**, nicht geraten. Zwei Kunden können dieselbe Person
|
||
führen; ein `findFirst` entschiede nach Sortierung darüber, wessen Geräte jemand sieht.
|
||
- **Die Weiche greift nur mit konfigurierten Rollengruppen.** Ein Update, das jeden aussperrt, dem
|
||
die Gruppen fehlen, wäre ein kaputtes Update.
|
||
- **Nicht `/kein-zugriff` bei gescheiterter Anmeldung**, wie das Issue vorsah: diese Seite verlangt
|
||
eine Sitzung, die es dann nicht gibt — das wäre eine Schleife. Die Begründung steht auf der
|
||
Anmeldeseite, mit zwei eigenen Codes.
|
||
|
||
### Probleme
|
||
|
||
- **Eine Gegenprobe blieb grün, und das war der eigentliche Befund.** Der Test „sieht keine Notizen“
|
||
prüft die Antwort, und die ist durch `laufBauen` sicher. Als ich im `select` versuchsweise
|
||
`ergebnisse: { notiz }` ergänzte, blieb er deshalb grün — während der Kopfkommentar behauptet, das
|
||
`select` hole es „gar nicht erst“. **Eine Zusage ohne eigenen Test ist keine Zusage**, auch wenn
|
||
ein Nachbartest sie zufällig mitträgt. Jetzt liest ein Test die Datei selbst.
|
||
- **Ein Test von #007 fand eine echte Lücke.** `staerksteRolle` benutzte `LESER` als Startwert und
|
||
damit als Untergrenze; mit einer Rolle darunter lieferte sie **mehr** Rechte als übergeben. Drei
|
||
Jahre Projektzeit hätte das niemand gemerkt, weil es keine Rolle unter `LESER` gab.
|
||
- **Zwei Bedienfehler beim Durchklicken**, beide dieselbe Ursache: das Standardziel nach der
|
||
Anmeldung ist `/`, und die Übersicht verlangt `lesen`. Ein Mitarbeiter sah als Erstes „Dafür fehlt
|
||
dir die Berechtigung“ — und der Knopf dort führte zurück auf `/`, also im Kreis. Ohne Klickprobe
|
||
wäre das bis zum ersten echten Nutzer unentdeckt geblieben: **alle Tests waren grün.**
|
||
- **Die Navigation hätte sechs tote Links angeboten.** `recht` stand nur am Admin-Bereich, weil
|
||
bisher jede Rolle `lesen` durfte.
|
||
- **Ein falscher Satz in `docs/betrieb/authentik.md`:** „Ist der Benutzer in keiner der vier
|
||
Gruppen, bekommt er `LESER`.“ Richtig war er bis zu dieser Sitzung.
|
||
- **Drei eigene Testfehler**, keine Produktfehler: Tag-Codes müssen dem Alphabet ohne `0`, `1`, `I`
|
||
und `O` folgen; die Kundenbindungstests gaben nur eine Kundengruppe mit und hingen damit an der
|
||
neuen Weiche; und ein Einfüge-Skript meldete „ok“, ohne geschrieben zu haben — seitdem prüfe ich
|
||
nach dem Einfügen mit `grep`.
|
||
|
||
### Nächster Schritt
|
||
|
||
- **Phase 2 ist bis auf die beiden großen Verzeichnis-Themen fertig:** **M15 (#028)**
|
||
Microsoft-Graph-Quelle und **M16 (#029)** SCIM-Endpunkt.
|
||
- Vor beiden wäre ein unabhängiger Review über die sechs Meilensteine dieser Phase angebracht —
|
||
M17 bis M22 liegen ohne fremden Blick auf `main`.
|
||
- Weiter offen und nicht von hier aus machbar: **#024** Proxmox-Einzeiler auf einem echten PVE-Host.
|
||
|
||
---
|
||
|
||
## 2026-10-06 · Sitzung 21 · 09:00–13:10
|
||
|
||
**Issue:** #028 (M15)
|
||
**Version:** 0.2.0 (unveröffentlicht)
|
||
|
||
### Erledigt
|
||
|
||
- **Eine Verzeichnisquelle hat eine Art**: `LDAP`, `GRAPH` oder `SCIM`. Bestehende Zeilen sind
|
||
`LDAP` — das war vor #028 die einzige.
|
||
- **Entra ID über Microsoft Graph**: Client-Credentials-Flow mit Token-Zwischenspeicher, Seitenabruf
|
||
über `@odata.nextLink`, Vorgesetzter über `$expand`.
|
||
- **Derselbe Abgleich für beide Arten.** Die Weiche sitzt in
|
||
`src/server/verzeichnis/quellenleser.ts` und ist die einzige Stelle, die die Art kennt.
|
||
- 1373 Vitest-Tests (vorher 1342), davon 31 neue gegen einen HTTP-Doppelgänger mit aufgezeichneten
|
||
Graph-Antworten.
|
||
- Im gebauten Stand eine Graph-Quelle wirklich angelegt: Art gespeichert, Geheimnis verschlüsselt,
|
||
nicht im Quelltext der Seite.
|
||
|
||
### Entscheidungen
|
||
|
||
- **Ein Enum auf der Quelle, keine Tabelle je Art.** Drei Arten rechtfertigen keine Vererbung; was
|
||
eine Art braucht, hält ein CHECK fest — in **beide** Richtungen, sonst wäre eine Graph-Quelle mit
|
||
`basisDn` möglich.
|
||
- **Die Art ist unveränderlich.** An ihr hängt, was `externeId` bedeutet; nach einem Wechsel würde
|
||
kein Mitarbeiter wiedererkannt. Steht so nicht im Issue und ist die wichtigste Zusage dieses
|
||
Meilensteins.
|
||
- **`{{ou}}` bleibt bei Graph leer.** Etwas hineinzudeuten wäre ein Wert, den kein PowerShell-Befehl
|
||
brauchen kann — und er sähe richtig aus.
|
||
- **`SCIM` steht im Enum, wird aber abgewiesen.** Ein Wert, der stillschweigend nichts tut, hätte
|
||
hier die ganze Belegschaft deaktiviert.
|
||
|
||
### Probleme
|
||
|
||
- **Der teuerste Fund: „deaktiviert“ als „übersprungen“ zu melden hätte Leute aktiv gelassen.**
|
||
„Übersprungen“ heißt im Abgleich „nicht zuordenbar“, und solche Einträge werden seit #016
|
||
absichtlich nicht abgeschaltet. Ein gesperrtes Entra-Konto wäre damit genau umgekehrt behandelt
|
||
worden als ein gesperrtes AD-Konto. Jetzt filtert die Abfrage serverseitig, als Gegenstück zum
|
||
LDAP-Standardfilter.
|
||
**Gefunden hat es der Test — aber nur, weil der Doppelgänger den Filter wirklich auswertet.** Hätte
|
||
er den Filter bloß empfangen und ignoriert, wäre der Test grün gewesen und der Fehler im Betrieb
|
||
aufgefallen. Ein Doppelgänger, der nur antwortet, prüft die Anfrage nicht.
|
||
- **`URLSearchParams` kodiert `$` zu `%24`** und die Klammern in `manager($select=id)`. Graph nimmt
|
||
das teils an und antwortet teils mit „Invalid expand clause“. Jetzt von Hand zusammengesetzt.
|
||
- **Ein Import-Zyklus drohte**: die Weiche braucht `zugangAus`, und der Dienst braucht die Weiche.
|
||
Beides aufgelöst durch ein eigenes Modul. In ESM läuft so ein Zyklus oft — aber nicht zuverlässig.
|
||
- **Zwei eigene Testfehler**: die Token-Grenze liegt bei +3540 s und nicht „ungefähr eine Stunde“,
|
||
und die Klickprobe zählte die Felder der Bearbeiten-Formulare mit.
|
||
|
||
### Was offen bleibt, und das ist benannt
|
||
|
||
Gegen einen **echten Entra-Mandanten** ist nichts davon gelaufen. Die Form der Antworten im
|
||
Doppelgänger stammt aus der Dokumentation, nicht aus einem Mitschnitt — stimmt sie nicht, stimmen
|
||
Test und Wirklichkeit nicht. Die Prüfliste steht am Ende von `docs/betrieb/entra-id.md`, acht
|
||
Punkte, darunter ein Mandant mit mehr als 999 Benutzern. Dieselbe Art offener Punkt wie #024.
|
||
|
||
### Nächster Schritt
|
||
|
||
- **M16 (#029)** SCIM-Endpunkt ist der letzte Meilenstein von Phase 2. Die Weiche dafür steht.
|
||
- Vor #029 wäre ein unabhängiger Review angebracht: **sieben Meilensteine** dieser Phase liegen ohne
|
||
fremden Blick auf `main`, und diese Sitzung hat wieder einen Fehler gezeigt, den die Suite nur
|
||
deshalb gefunden hat, weil der Doppelgänger streng genug war.
|
||
|
||
|
||
---
|
||
|
||
## 2026-10-10 · Sitzung 22 · 17:40–21:45
|
||
|
||
**Issue:** #029 (M16)
|
||
**Version:** 0.2.0 (unveröffentlicht)
|
||
|
||
### Erledigt
|
||
|
||
- **SCIM 2.0 als dritte Quellenart.** Neun Endpunkte unter `/api/scim/v2`, jeder prüft sein Token
|
||
selbst — die Hülle unter `(app)` liegt dort nicht darüber, und der Proxy lässt `/api/scim`
|
||
absichtlich ohne Cookie durch.
|
||
- **Ein Token je Quelle**, argon2-gehasht, einmal angezeigt. Zwei CHECKs halten das in der
|
||
Datenbank: `…_scimtoken_gehasht` (`LIKE '$argon2%'`) und `…_scimtoken_mit_zeitpunkt`.
|
||
- **Oberfläche**: Art SCIM im Anlege-Formular, Token erzeugen und widerrufen, letzte Zustellung,
|
||
Endpunktadresse an der Quelle. Verbindungstest, Zeitplan, Abgleich-Log und Attributzuordnung
|
||
fallen dort weg — es gibt sie nicht, und ein Knopf, der nur eine Erklärung auswirft, ist kein
|
||
Knopf.
|
||
- 1432 Vitest-Tests (vorher 1373), davon 59 neue. Die Anfrageformen von authentik und Entra ID
|
||
stehen als benannte Konstanten oben in `tests/scim.test.ts`.
|
||
- **Klickprobe am gebauten Stand, zwanzig Prüfungen**, von „Art SCIM wählbar“ über eine echte
|
||
Zustellung mit genau dem Token aus der Oberfläche bis „390 px ohne horizontales Scrollen“. Die
|
||
Probe hat ihre Daten danach selbst entfernt — die Lehre aus #027.
|
||
- Anleitung Kapitel 06 um den SCIM-Abschnitt erweitert (Einrichtung in authentik und Entra ID,
|
||
Fehlerbilder), `docs/betrieb/scim.md` neu.
|
||
|
||
### Entscheidungen
|
||
|
||
- **Umgesetzt ist nicht der Standard, sondern was die Anbieter schicken.** Komplexe Filter,
|
||
Sortierung, `bulk`, `/Groups` — nichts davon. `ServiceProviderConfig` **meldet** das als
|
||
`supported: false`, und die Verneinungen sind der wichtigere Teil der Antwort: ein Anbieter, der
|
||
`bulk` für unterstützt hält, schickt Anfragen, die niemand beantwortet.
|
||
- **Ein unverstandener Filter ergibt `400`, nicht „alle Personen“.** Die wichtigste
|
||
Einzelentscheidung hier. Alles zu liefern wäre die gefährlichere Antwort: der Anbieter schlösse
|
||
daraus, die gesuchte Person gäbe es schon, und legte sie **nie** an.
|
||
- **`404` und nicht `403`** für eine Person einer fremden Quelle. Ob es sie gibt, ist für ein
|
||
fremdes Token keine Auskunft. Ebenso dieselbe `401` für „kein Token“ und „falsches Token“.
|
||
- **`DELETE` deaktiviert.** Der Anbieter bekommt `204` und hält die Person für entfernt — aus seiner
|
||
Sicht richtig. In der Anleitung steht das ausdrücklich, sonst wirkt die inaktive Zeile wie ein
|
||
Fehler.
|
||
- **SCIM-Quellen bleiben aus dem Zeitplan heraus**, an zwei Stellen gehalten. Ein Abgleich, der dort
|
||
nichts liest und `ERFOLG` meldet, hätte die ganze Belegschaft deaktiviert.
|
||
|
||
### Probleme
|
||
|
||
- **Zwei von acht Gegenproben blieben grün — und das waren die wertvollen.** Sechs schlugen sofort
|
||
an. Die beiden stillen:
|
||
- **`PATCH remove` wirkte gar nicht.** Kein Testloch, ein Fehler: `personAendern` hatte einen
|
||
Schalter `nurGesetzte`, der bei `PATCH` jedes `null` als „nicht mitgeschickt“ las. Nimmt jemand
|
||
beim Anbieter die Abteilung aus dem Profil, stand sie in Laufzettel weiter — sichtbar, gepflegt
|
||
aussehend, falsch. Der Schalter war nie nötig: `patchAnwenden` setzt auf dem vorhandenen Stand
|
||
auf und liefert immer einen vollständigen Zielzustand. Der Unterschied zwischen `PUT` und
|
||
`PATCH` liegt **vor** der Funktion, nicht in ihr.
|
||
- **Entra ID schickt `active` als `"True"`/`"False"`.** Richtig umgesetzt, aber nur halb geprüft:
|
||
beim Deaktivieren kommt mit einem reinen `value === true` **zufällig** das Richtige heraus. Nur
|
||
das Aktivieren wäre kaputt gewesen — ein wieder freigeschaltetes Konto, das in Laufzettel für
|
||
immer inaktiv bleibt. Ein Fehler, der nur in einer Richtung sichtbar ist, ist der teuerste.
|
||
- **`manager` wäre fast untergegangen.** Es stand im Issue, und umgesetzt war zunächst nur
|
||
`department`. Dazugekommen: drei Lesearten (`{value}`, `{$ref}`, nackte Zeichenkette), die
|
||
Auflösung, und `manager` im `Schemas`-Endpunkt — das Letzte ist das Entscheidende, denn ein
|
||
Anbieter schickt nur, was dort steht. Ohne den Eintrag bliebe der Vorgesetzte dauerhaft leer,
|
||
**ohne Fehlermeldung auf einer der beiden Seiten.**
|
||
- **`manager` war ein Weg über die Quellengrenze.** Eine Kennung aus einer fremden Quelle hätte über
|
||
die Vorgesetzten-Anzeige einen fremden Namen in die Oberfläche gebracht. Die Auflösung sucht nur
|
||
in der eigenen Quelle; die Gegenprobe ohne diese Einschränkung ist rot.
|
||
- **Ein Fehler aus #028 im Nachbarcode**: in `verbindungTestenAktion` stand zweimal dieselbe
|
||
Bedingung, der zweite Zweig war unerreichbar. Genau dieser Zweig war dort als Reviewbefund
|
||
eingebaut worden — „gelesen, aber nichts zuordenbar“ sollte anders klingen als „der Filter findet
|
||
niemanden“, weil die Ursachen den Administrator an verschiedene Stellen schicken. Wirkungslos
|
||
seit dem Tag, an dem er geschrieben wurde. Das ist dasselbe Muster wie die stillen Gegenproben:
|
||
**Code, der aussieht, als prüfte er etwas.**
|
||
- **Ein eigener Probenfehler**: die Klickprobe suchte „Klick Probe“ in der Mitarbeiterliste. Ohne
|
||
`displayName` baut Laufzettel „Nachname, Vorname“ — wie der Abgleich. Kein Produktfehler, aber
|
||
ein Beleg dafür, dass ein roter Befund erst gelesen und dann bewertet gehört.
|
||
- Ein Test aus #028 war **überholt**, nicht falsch: er verlangte, dass die Meldung der Weiche „#029“
|
||
nennt.
|
||
|
||
### Was offen bleibt, und das ist benannt
|
||
|
||
Gegen **echte Mandanten** ist nichts davon gelaufen. Die Anfrageformen stammen aus der
|
||
Dokumentation von authentik und Entra ID, nicht aus einem Mitschnitt — dieselbe Grenze wie bei #028,
|
||
und sie steht im Kopfkommentar von `tests/scim.test.ts`. Die Prüfbefehle für den ersten echten Lauf
|
||
stehen in `docs/betrieb/scim.md`.
|
||
|
||
### Nächster Schritt
|
||
|
||
- **Phase 2 ist damit vollständig.** Acht Meilensteine (M15–M22) liegen danach auf `main`.
|
||
- Offen bleiben **#024** (Proxmox-Einzeiler auf echtem PVE-Host) und **#027** (die Editor-Abläufe
|
||
lassen ihre Vorlagen in der Dev-Datenbank stehen).
|
||
- **Ein unabhängiger Review ist jetzt überfällig.** Acht Meilensteine dieser Phase liegen ohne
|
||
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.
|
||
|
||
|
||
---
|
||
|
||
## 2026-10-11 · Sitzung 24 · 02:00–06:10
|
||
|
||
**Issues:** #038 (E2E in ein Tor), #037 (zwei Nachzügler)
|
||
**Version:** 0.2.0 (unveröffentlicht)
|
||
|
||
### Erledigt
|
||
|
||
- **#038 entschieden und festgeschrieben.** E2E läuft **vor jedem Merge nach `main`**, nicht vor
|
||
jedem Commit. Die Regel steht in `CLAUDE.md` mit gemessenen Zahlen, `docs/architektur.md` §5b
|
||
trägt sie mit.
|
||
- **Die Liste der Zusagen, die nur in E2E hängen** — neun Stück, alle brauchen einen echten
|
||
Browser, keine lässt sich nach Vitest holen. Steht als Tabelle in `CLAUDE.md`.
|
||
- **Was sich holen ließ, ist geholt:** `tests/zugang.test.ts` prüft jetzt strukturell, dass jede
|
||
`page.tsx` und jeder `route.ts` sein Recht selbst prüft.
|
||
- **#037 beide Teile**: `anhaengeDesLaufs` liegt in `queries/` und engt selbst ein; `/meine-geraete`
|
||
wird bei 390 px gemessen, mit einem echten `MITARBEITER`-Konto aus dem E2E-Vorlauf.
|
||
- 1457 Vitest-Tests (vorher 1454), 115 E2E-Abläufe (vorher 113).
|
||
|
||
### Entscheidungen
|
||
|
||
- **Neun Minuten vor jedem Commit ist ein Tor, das umgangen wird.** Gemessen: Bau 14 s, Start 7 s,
|
||
Abläufe 256 s — zu den vier Minuten des bestehenden Tores. Vor jedem Merge hätte in Phase 2 acht
|
||
Läufe bedeutet und jeden der vier Befunde aus #027 am Tag seiner Entstehung gezeigt. Das ist die
|
||
Rechnung, die die Regel trägt.
|
||
- **Die Antwort auf #038 ist nicht „verlagern“.** Jeder der 16 Abläufe wurde gegen die Frage
|
||
geprüft, ob es ein Vitest-Gegenstück gibt. Was dort hängt, braucht einen Browser: Layout bei
|
||
390 px, `BarcodeDetector`, Touch, zwei Kontexte, Zwischenablage, Print-CSS, gerendertes DOM, und
|
||
die Hydrierung selbst.
|
||
- **Das MITARBEITER-Konto liegt im E2E-Vorlauf, nicht im Seed.** Ein Konto mit bekanntem Passwort
|
||
gehört nicht in einen Seed, der auch produktiv läuft. Die Person wird gewählt (erste aktive mit
|
||
Gerät), nicht gesetzt — eine feste Kennung wäre eine Annahme über den Seed.
|
||
|
||
### Probleme
|
||
|
||
- **Der eigentliche Fund steckte in #038 und war keine Testlücke, sondern eine Konvention ohne
|
||
Prüfer.** „Jede Seite prüft selbst“ steht seit #007 in `CLAUDE.md`; geprüft hat es bis hierher
|
||
niemand außer einem Menschen, der nachsieht. Der Review von Phase 2 hat alle 27 Seiten
|
||
durchgesehen — ein Ergebnis, das für genau einen Tag gilt. Jetzt liest ein Test sie, und beide
|
||
Gegenproben (eine Seite, ein Handler ohne Prüfung) werden rot.
|
||
- **Ein gemeinsamer Helfer in einer Testsuite ist geteilter Zustand.** Für das zweite Konto brauchte
|
||
`anmelden` eine Variante, die nicht auf `/` wartet. Ich habe das Warten dabei für **alle**
|
||
gelockert — und damit zwei unverwandte Abläufe unzuverlässig gemacht, darunter einen in
|
||
`laeufe.spec.ts`, der mit dem Thema nichts zu tun hat. **Sichtbar nur im vollen Durchgang**, nicht
|
||
beim Einzellauf: einzeln waren beide grün. Behoben, indem `anmelden` wieder streng wartet und
|
||
`anmeldenMit` das Ziel als Parameter nimmt; dazu beide auf `load`, denn ein `waitForURL` endet an
|
||
der Adresse, nicht am geladenen Dokument.
|
||
Dass genau dieser Fehler in derselben Sitzung passiert ist, in der die Regel „E2E vor jedem Merge“
|
||
entstand, ist die beste Begründung für sie: ohne vollen Durchgang hätte ich zwei rote Abläufe
|
||
gemergt.
|
||
|
||
### Nächster Schritt
|
||
|
||
- Offen bleibt nur noch **#024** (Proxmox-Einzeiler auf einem echten PVE-Host) — und das verlangt
|
||
einen Host, den es hier nicht gibt.
|
||
- **v0.2.0 ist damit reif für einen Tag**: Changelog-Abschnitt, Devlog-Phasenzusammenfassung, Tag,
|
||
Release-Notiz. Die Bedingung aus Sitzung 23 („vorher gehört #038 entschieden“) ist erfüllt.
|
||
|
||
|
||
---
|
||
|
||
## Phase 2 abgeschlossen: was aus dem Plan wurde
|
||
|
||
**38 h 35 min**, geschätzt waren **90 h** für die zwölf Issues dieser Version. Acht davon waren
|
||
Meilensteine (M15–M22), die übrigen entstanden **während** der Arbeit — und zwei der nützlichsten
|
||
(#036, #038) stammen gar nicht aus einem Plan, sondern aus der Durchsicht.
|
||
|
||
### Was dazugekommen ist
|
||
|
||
| | v0.1.0 | v0.2.0 |
|
||
|---|---|---|
|
||
| Quelltext unter `src/` | rund 30.000 Zeilen | **40.781 Zeilen** (ohne erzeugten Prisma-Client) |
|
||
| Datenmodell | 20 Tabellen, 9 Aufzählungen, 2 Migrationen | **25 Tabellen, 13 Aufzählungen, 14 Migrationen** |
|
||
| Datenbankregeln namentlich | — | **56** (CHECKs, partielle Indizes, Trigger, zusammengesetzte Fremdschlüssel) |
|
||
| Vitest | 1131 Tests | **1457 Tests** in 61 Dateien |
|
||
| Ende-zu-Ende | 101 Abläufe | **115 Abläufe** in 16 Dateien |
|
||
| Verzeichnisquellen | LDAP | **LDAP, Microsoft Graph, SCIM** |
|
||
| Dokumentation | 9 Kapitel, 14 Issue-Dateien | 9 Kapitel, 8 Betriebsseiten, **38 Issue-Dateien** |
|
||
|
||
Der Seed ist unverändert bei 7 Vorlagen mit 155 Schritten — nachgezählt, nicht übernommen.
|
||
|
||
### Die acht Meilensteine
|
||
|
||
| | | Zeit |
|
||
|---|---|---|
|
||
| M15 | Quellenarten und Microsoft-Graph-Quelle (#028) | 4 h 10 |
|
||
| M16 | SCIM-Endpunkt als Alternative zum Abgleich (#029) | 4 h 05 |
|
||
| M17 | Wiederverwendbare Bausteine (#030) | 2 h 25 |
|
||
| M18 | Abteilungsspezifische Zusatzabschnitte (#031) | 1 h 35 |
|
||
| M19 | Anhänge und Fotos an Schritten (#032) | 3 h 05 |
|
||
| M20 | Live-Aktualisierung per SSE statt Polling (#033) | 1 h 40 |
|
||
| M21 | Benachrichtigungen per Mail und Webhook (#034) | 3 h 30 |
|
||
| M22 | Mitarbeiter-Selbstauskunft (#035) | 2 h 50 |
|
||
|
||
### Was diese Phase gelehrt hat
|
||
|
||
**Die teuersten Fehler waren dreimal derselbe: eine Zusage ohne Prüfer.** Nicht falscher Code —
|
||
Code, der *aussieht*, als prüfte er etwas.
|
||
|
||
- Ein Kommentar sagte, ein `select` hole Notizen „gar nicht erst“, während ein anderer Codepfad den
|
||
Test rettete (#035).
|
||
- `docs/betrieb/sicherung.md` sagte „kein Upload-Verzeichnis“, nachdem die Anhänge dazugekommen
|
||
waren — eine Sicherung nach der alten Anleitung hätte still Daten verloren (#032). Dasselbe
|
||
Dokument hatte in #036 wieder einen Satz, der zu keiner Installation passte.
|
||
- „Jede Seite prüft selbst“ steht seit #007 in `CLAUDE.md` und hielt bis #038 **nur durch Nachsehen
|
||
von Hand**. Der Review hat alle 27 Seiten durchgesehen — ein Ergebnis, das für genau einen Tag
|
||
gilt.
|
||
|
||
**Die Gegenprobe ist das Werkzeug, das sie findet.** Regel brechen, Test muss rot werden. In dieser
|
||
Phase blieben fünf Gegenproben grün, und jede davon war ein Befund: `PATCH remove` wirkte gar nicht,
|
||
Entra IDs `"True"` wäre beim *Aktivieren* kaputt gewesen, der Anhang-Handler hatte keinen Test,
|
||
`/api/scim` stand in keiner Allowlist-Prüfung, und der Link in der Abgleich-Meldung war ein 404.
|
||
|
||
**Ein Fehler, der nur in einer Richtung sichtbar ist, ist der teuerste.** Entra ID schickt `active`
|
||
als `"True"`/`"False"`. Beim Deaktivieren kommt mit einem reinen `value === true` zufällig das
|
||
Richtige heraus — nur das Aktivieren wäre kaputt gewesen: ein wieder freigeschaltetes Konto, das für
|
||
immer inaktiv bleibt.
|
||
|
||
**Eine Attrappe ohne die Form der Wirklichkeit prüft etwas anderes als die Wirklichkeit.** Zweimal
|
||
gelernt, von beiden Seiten: der Graph-Doppelgänger fand einen echten Fehler, **weil** er den Filter
|
||
wirklich auswertete (#028); die Sitzungs-Attrappe `{}` verdeckte einen, weil sie keine `benutzerId`
|
||
hatte (#036).
|
||
|
||
**Ein gemeinsamer Helfer in einer Testsuite ist geteilter Zustand.** Das Anmelde-Warten für einen
|
||
neuen Fall aufzuweichen machte zwei unverwandte Abläufe unzuverlässig — sichtbar nur im vollen
|
||
Durchgang (#037).
|
||
|
||
### Wo die Zeit hinging
|
||
|
||
Die beiden Verzeichnis-Meilensteine (#028, #029) waren mit 8 h 15 zusammen die teuersten, und zwar
|
||
**nicht** wegen der Protokolle: beide liefen gegen Doppelgänger, und in beiden Fällen entschied die
|
||
Strenge der Attrappe, ob ein Fehler gefunden wurde. Die eigentliche Arbeit war, die Weiche an genau
|
||
einer Stelle zu halten und beide Richtungen eines CHECKs aufzuschreiben — was eine Art braucht, und
|
||
was sie **nicht haben darf**.
|
||
|
||
### Was offen bleibt
|
||
|
||
**#024** — der Proxmox-Einzeiler ist auf einem echten PVE-Host nie gelaufen. Das steht seit v0.1.0
|
||
so da und wandert mit dem Tag auf v0.3.0: es ist eine **Gegenprüfung** und kein fehlendes Stück. Das
|
||
Skript ist `shellcheck`-geprüft und im `--dry-run` gefahren, der Installationsteil läuft vollständig
|
||
in einem `debian:13`-Container. Was fehlt, ist `pct create` auf einem echten Host — und der verlangt
|
||
einen Host, den diese Umgebung nicht hat. Die Prüfliste liegt bereit.
|
||
|
||
Zwei Grenzen gehören dazu genannt, beide in den Issues festgehalten: **gegen einen echten
|
||
Entra-Mandanten** und **gegen echte SCIM-Anbieter** ist nichts gelaufen. Die Formen in den Tests
|
||
stammen aus der Dokumentation, nicht aus Mitschnitten. Stimmt eine Form nicht, stimmen Test und
|
||
Wirklichkeit nicht — die Prüflisten stehen in `docs/betrieb/entra-id.md` und
|
||
`docs/betrieb/scim.md`.
|