Eine veroeffentlichte Vorlagen-Version ist fuer immer unveraenderlich — daran haengt, dass ein archivierter Lauf in zehn Jahren noch genau die Schritte zeigt, mit denen er gestartet wurde. Die Regel steht deshalb zweimal: im Service, der die verstaendliche Meldung gibt, und in der Datenbank, die die Garantie gibt. Vier Trigger fangen Schreibzugriffe ab, die nicht durch den Service kommen — Migrationen, psql, der Seed und ab Phase 4 die Connectoren — und versperren den Rueckweg nach ENTWURF, der alles andere ausgehebelt haette. - Service: anlegen, Kopfdaten bearbeiten, Entwurf als tiefe Kopie der letzten veroeffentlichten Version, speichern, veroeffentlichen, verwerfen, duplizieren, archivieren - Hoechstens ein Entwurf je Vorlage, durchgesetzt vom partiellen Index; zwei gleichzeitige Klicks auf „Bearbeiten“ bekommen denselben Entwurf statt eines Fehlers - Optimistisches Sperren ueber geaendertAm: wer auf einem ueberholten Stand speichert, wird abgewiesen statt die Arbeit des anderen spurlos zu ersetzen - JSON-Export und -Import samt Format-Dokumentation; der Export ist nachsichtig, der Import streng - src/lib/validierung.ts als einzige Zod-Quelle mit deutschen Meldungen, per ESLint-Regel erzwungen Der unabhaengige Review fand drei Blocker, alle an der Mandantengrenze: der Suchfilter brachte ein zweites OR mit und ueberschrieb die Kundenbedingung, Historie und Aufbau einer Version filterten gar nicht. Zu queries/vorlagen.ts gab es keinen Test — jetzt gibt es tests/vorlagen-sicht.test.ts. 428 Tests gruen (vorher 336). Schliesst #009 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
147 lines
7.4 KiB
Markdown
147 lines
7.4 KiB
Markdown
# 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.
|
|
|
|
Der Aufbau einer Version wird **als Ganzes** gespeichert, nicht Schritt für Schritt: der Editor
|
|
hält den Baum im Speicher und sortiert per Drag & Drop, ein Teil-Update wäre dort ständig mit dem
|
|
Sortieren im Streit. Dasselbe Zod-Schema (`src/lib/vorlagen-format.ts`) beschreibt den Entwurf im
|
|
Editor, den JSON-Export und den Import — drei getrennte Schemata würden auseinanderlaufen, und der
|
|
Import wäre der Ort, an dem das erst auffiele. Das Format ist in
|
|
[`docs/vorlagen-format.md`](vorlagen-format.md) beschrieben.
|
|
|
|
## 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
|
|
|
|
## 5a. Was in der Datenbank steht und warum
|
|
|
|
Die Service-Schicht ist der richtige Ort für Regeln mit verständlichen Meldungen. Sie ist aber
|
|
nicht der einzige Weg, auf dem Daten entstehen: Migrationen, `psql`, Skripte und ab Phase 4 die
|
|
Connectoren schreiben an ihr vorbei. Was stillschweigend falsch werden könnte, steht deshalb
|
|
zusätzlich in der Datenbank:
|
|
|
|
| Regel | Form |
|
|
|---|---|
|
|
| Höchstens eine eigene Organisation | partieller Eindeutigkeitsindex |
|
|
| Höchstens ein Entwurf je Vorlage | partieller Eindeutigkeitsindex |
|
|
| Kundenkürzel, `tagCode`-Form, verschlüsseltes Bind-Passwort | CHECK auf die Form |
|
|
| Registriertes Gerät braucht Hostname oder Bezeichnung | CHECK, greift erst ab `registriertAm` |
|
|
| Mitarbeiter aus dem Verzeichnis **oder** Freitextname | CHECK |
|
|
| Status und Zeitstempel laufen nicht auseinander | vier CHECKs |
|
|
| `NICHT_ZUTREFFEND` und `FEHLGESCHLAGEN` brauchen eine Notiz | CHECK |
|
|
| Einstellung ist eine einzelne Zeile | CHECK auf die feste Kennung |
|
|
| **Mandantengrenze** | vier zusammengesetzte Fremdschlüssel |
|
|
|
|
Die Mandantengrenze verdient die Erklärung: `Lauf` verweist mit `("geraetId", "kundeId")` auf
|
|
`Geraet("id", "kundeId")`, analog für Person und Abteilung, und `Geraet` ebenso auf den Besitzer.
|
|
Damit ist ein Lauf, der ein Gerät eines fremden Kunden zieht, nicht mehr bloß ein Fehler im
|
|
Service, sondern strukturell unmöglich. `MATCH SIMPLE` sorgt dafür, dass ein Lauf ohne Gerät
|
|
weiterhin erlaubt ist.
|
|
|
|
Zwei Dinge sind bewusst **nicht** kaskadierend: ein `Lauf` nimmt seine Schrittergebnisse und
|
|
Kommentare nicht mit, und ein `Protokoll`-Eintrag verliert seinen Urheber nicht. Beides stünde
|
|
gegen den Zweck eines Archivs. Der `tagCode` ist zusätzlich in `TagCodeVergabe` festgehalten,
|
|
aus dem nie gelöscht wird — sonst würde ein Code nach dem Löschen eines Geräts wieder frei und
|
|
ein alter Aufkleber zeigte auf ein fremdes Objekt.
|
|
|
|
## 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.
|