179 lines
9.6 KiB
Markdown
179 lines
9.6 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.
|
|
|
|
Die **eingebauten Platzhalter** stehen als Liste in [`src/lib/platzhalter.ts`](../src/lib/platzhalter.ts)
|
|
— Daten, kein Code. Der Editor zeigt sie als Hilfe, die Warnung vor unbekannten Platzhaltern
|
|
vergleicht dagegen, und die Auflösung in #012 baut darauf auf. Eine zweite Aufzählung an anderer
|
|
Stelle würde unweigerlich auseinanderlaufen: ein Platzhalter, der in der Hilfe steht, aber nicht
|
|
aufgelöst wird, bliebe im fertigen PowerShell-Befehl als `{{…}}` stehen.
|
|
|
|
Die Warnung ist ausdrücklich **keine Ablehnung**. Ein Tippfehler soll im Editor auffallen und nicht
|
|
erst dem Techniker im Lauf; es kann aber gute Gründe geben, einen Platzhalter stehen zu lassen —
|
|
etwa weil die Variable gleich noch kommt. Speichern und Veröffentlichen bleiben deshalb erlaubt.
|
|
|
|
## 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.
|
|
|
|
## 5b. Warum ein Screenshot nichts über Bedienbarkeit sagt
|
|
|
|
In dieser Umgebung hydriert `next dev` nicht: die Seiten werden serverseitig gerendert und sehen
|
|
vollständig richtig aus, aber React hängt sich nie an — kein Knopf, kein Formular, kein Sortieren
|
|
tut etwas. Nachgewiesen in #010, und zwar auch ohne `src/proxy.ts` und ohne `output: "standalone"`;
|
|
der HMR-WebSocket scheitert mit `ERR_INVALID_HTTP_RESPONSE`. Es liegt am Werkzeug, nicht an diesem
|
|
Code — im gebauten Stand tragen 1281 von 1296 Elementen React-Knoten, im Entwicklungsmodus zwei.
|
|
|
|
Zwei Folgen, beide verbindlich:
|
|
|
|
1. **Oberflächen werden gegen `pnpm build && pnpm start` abgenommen**, nicht gegen `pnpm dev` —
|
|
ebenso die Screenshots. Steht in `CLAUDE.md`.
|
|
2. **Was nur mit angehängtem React gelingt, gehört in einen Playwright-Test.** `pnpm test:e2e`
|
|
klickt den Editor durch: anlegen, tippen, sortieren mit dem Finger bei 390 px, speichern, neu
|
|
laden und nachsehen, ob es wirklich in der Datenbank steht.
|
|
|
|
Die Content-Security-Policy in `src/proxy.ts` ist deshalb zweigeteilt: im Entwicklungsmodus mit
|
|
`unsafe-eval` und WebSocket, im ausgelieferten Stand ohne beides. Die Entscheidung fällt an
|
|
`process.env.NODE_ENV`, und `tests/sicherheitsrichtlinie.test.ts` prüft sie am Proxy selbst — eine
|
|
Prüfung der Funktion mit selbst gesetztem Argument hätte genau den Fehler durchgelassen, vor dem
|
|
der Kommentar darüber warnt.
|
|
|
|
## 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.
|