Zwei Saetze in CLAUDE.md beschrieben den Code nicht genau. "Nie auf rohe Hexwerte" hat sechs begruendete Ausnahmen, nicht null: Papier, ein QR-Decoder und das Web-App-Manifest entscheiden nicht nach unserem Farbschema. Sie stehen jetzt als Tabelle mit Begruendung je Zeile. Die Regel wurde ergaenzt, nicht der Code geaendert -- eine Konstante ueber drei unverwandte Dinge haette die Begruendung versteckt. Das Tor vor jedem Commit nennt den LDAP-Container. Ohne ihn ueberspringt die Suite 22 Tests -- nachgezaehlt, nicht geschaetzt: 81 mit Container, 59 ohne. Sie sagt es auf stderr, der Lauf sieht trotzdem gruen aus. Im README fehlte zusaetzlich format:pruefen, das CLAUDE.md seit #010 verlangt. Damit das haelt, auch wenn niemand hinsieht: pruefe:doku vergleicht die beiden Schranken-Bloecke und verlangt von beiden den Container. Gegenprobe gefahren. Der Review hatte vier Hexwerte genannt und src/lib/qr.ts uebersehen, und das Issue behauptete die Regel stehe auch in docs/architektur.md -- tut sie nicht. Beides im Issue-Text richtiggestellt, statt einen Haken ueber falschen Text zu setzen. Closes #40
1092 lines
65 KiB
Markdown
1092 lines
65 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 |
|
||
| | **Summe** | **36 h 05 min** |
|
||
|
||
| Meilenstein | Tatsächlich | Status |
|
||
|---|---|---|
|
||
| v0.1.0 | 35 h 20 min | veröffentlicht |
|
||
| v0.2.0 | 45 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.
|