# 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 | | | **Summe** | **4 h 10 min** | | Meilenstein | Tatsächlich | Status | |---|---|---| | v0.1.0 | 4 h 10 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 --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: `//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 `` 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.