OIDC mit Discovery, PKCE, state und nonce; lokaler Login mit argon2 als Notfallzugang; Rechte in einer Tabelle, Prüfung ausschließlich serverseitig, Rolle und Aktiv-Status pro Anfrage aus der Datenbank. Ein unabhängiger Review fand eine offene Weiterleitung: die Präfix-Prüfung des Rücksprungziels ließ /<TAB>/fremd.example durch, woraus der Browser //fremd.example macht. Das Ziel wird jetzt aufgelöst und die Herkunft verglichen. Dazu acht weitere Befunde, alle behoben. 274 Tests. Closes #7 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
226 lines
13 KiB
Markdown
226 lines
13 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 |
|
||
| | **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 <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.
|