# Architektur und Entscheidungen Ergänzt [`CLAUDE.md`](../CLAUDE.md), das die verbindlichen Konventionen enthält. Hier steht das Warum. ## 1. Zwei Betriebsmodi, ein Codepfad `Einstellung.modus` ist `INTERN` oder `DIENSTLEISTUNG` und liegt in der Datenbank, nicht in der Umgebung — der Admin schaltet ohne Neustart um. Der Kern der Entscheidung: **jede kundenbezogene Zeile trägt immer eine `kundeId`**, auch im Modus `INTERN`. Dort ist es die eine Organisation mit `isteigeneOrganisation = true`, die in keiner Liste, keinem Filter, keinem Formular und auf keinem Etikett erscheint. Dadurch ist der Wechsel nach `DIENSTLEISTUNG` eine Datenänderung (die eigene Organisation wird zum ersten Kunden) und keine Migration. Es gibt daher **nicht zwei Codepfade** für Geräte oder Läufe, sondern ein- und ausgeblendete Felder und Filter. Zwei Funktionen tragen das: - `betriebsmodus()` — gecachter Zugriff, den alle Listen und Formulare fragen - `kundeErmitteln(eingabe)` — liefert im Modus `INTERN` immer die eigene Organisation und **verwirft** eine mitgeschickte `kundeId`. Das ist eine Sicherheitsgrenze, kein Komfort. ## 2. Schichtung ``` Server Component ──► queries/* ──► prisma Server Action ──► services/* ──► prisma └─ rechtErforderlich(), Zod, Regeln, Protokoll ``` `services/*` hält die Regeln und wirft eigene `…Fehler`-Klassen. `queries/*` hält Lesemodelle und wendet Rollen- und Mandantensicht selbst an, damit kein Aufrufer sie vergessen kann. `actions/*` sind Dreizeiler: Recht prüfen, Schema anwenden, Service rufen. Der Grund für die Trennung von Action und Service: die Regeln sollen **ohne Next.js testbar** sein. Vitest ruft den Service direkt. ## 3. Anmeldung: kein Auth.js Der Auftrag nannte Auth.js. Beim Prüfen der Lage am 2026-09-24: - `next-auth` v5 steht auf `5.0.0-beta.32`, seit Jahren ohne GA - authjs.dev führt das Projekt im Wartungsmodus („The Auth.js project is now part of Better Auth"), die Maintainer schreiben „no immediate plans for v5" - `@auth/prisma-adapter` deklariert `@prisma/client` nur bis `>=6` — wir fahren Prisma 7 Entschieden wurde deshalb der Eigenbau mit stabilen Bausteinen, wie in ProxLedger: - **`openid-client` 6.x** für generisches OIDC mit Discovery (Ziel: authentik) - **`jose`** für ein signiertes Sitzungs-Cookie (HS256 aus `APP_SECRET`) - **`@node-rs/argon2`** für den lokalen Notfallzugang Rolle und Aktiv-Status werden **pro Anfrage aus der Datenbank** gelesen, nicht aus dem Token. Eine Rollenänderung greift damit sofort, und ein deaktivierter Benutzer wird auch mit gültigem Cookie abgewiesen. ## 4. Versionierung und Archiv Zwei Regeln tragen die Nachvollziehbarkeit: 1. **Eine veröffentlichte `VorlagenVersion` wird nie verändert.** „Bearbeiten" erzeugt eine tiefe Kopie als Entwurf. Ein Lauf auf Version 1 zeigt für immer die Schritte aus Version 1, auch wenn längst Version 5 veröffentlicht ist. 2. **Ein archivierter Lauf ist schreibgeschützt.** Die Sperre sitzt in genau einer Funktion, `laufMussVeraenderbarSein(lauf)`, die vor jeder Änderung an Kopfdaten, `Schrittergebnis` und `Kommentar` gerufen wird. Nur `ADMIN` holt einen Lauf zurück, mit `Protokoll`-Eintrag. Beides ist durch Tests abgedeckt, nicht durch Disziplin. ## 5. Der Weg zu Phase 4 (Aktionen) Die in den Vorlagen vorbereiteten PowerShell-Befehle sind bewusst die **Vorstufe ausführbarer Schritte**. Damit der Schritt dorthin keine Migration braucht: - `VorlagenSchritt.typ` ist ein Enum, dem `AKTION` hinzugefügt wird; `konfig` (Json) trägt dann Connector und Parameter - `Schrittergebnis.wert` ist ein Json, das ein Aktionsergebnis mit Log, IDs und Zeitstempeln aufnimmt - kein Codepfad nimmt an, dass ein Schritt nur von einem Menschen erledigt wird - das Krypto-Modul für das LDAP-Bind-Passwort ist schon der Platz für Connector-Geheimnisse ## 6. Betrieb: zwei Wege, eine Regel Die Datenbank ist auf beiden Wegen von außen nicht erreichbar: | Weg | Wie | |---|---| | Docker Compose | `postgres` ohne `ports:`, nur im Netz `intern` mit `internal: true` (kein Gateway) | | Nativ im LXC | PostgreSQL mit `listen_addresses = ''` — nur Unix-Socket | Festgehalten in `tests/betriebsstand.test.ts`, damit ein `ports:`-Mapping nicht versehentlich zurückkommt. ## 7. Werkzeug-Obergrenzen Zwei Abhängigkeiten dürfen **nicht** angehoben werden, beides empirisch festgestellt: - **`eslint` bleibt auf 9.x.** `eslint-config-next` bringt `eslint-plugin-react@7.37.5` mit, das `eslint` nur bis `^9.7` unterstützt. Mit eslint 10 bricht das Regelwerk beim Laden ab (`contextOrFilename.getFilename is not a function`). Eine neuere Fassung des Plugins gibt es nicht — die letzte stammt von April 2025. - **`typescript` bleibt auf `~6.0.3`.** `typescript-eslint@8` erlaubt nur `>=4.8.4 <6.1.0`. TypeScript 7.0.2 ist zwar `latest`, aber noch nicht unterstützt. Dazu ein Pinning: **`prisma` und `@prisma/client` exakt auf `7.10.0`**. Der `latest`-Tag von `prisma` zeigt auf einen 8.0-RC, der von `@prisma/client` nicht — ein unbedachtes `pnpm add prisma` würde beide auseinanderlaufen lassen.