# Konventionen für Laufzettel Diese Datei ist verbindlich. Wer hier etwas ändert, ändert es auch in `docs/architektur.md`. ## Sprache **Deutsch, überall wo ein Mensch liest oder benennt** — auch Bezeichner. Funktions- und Variablennamen, Prisma-Modelle und -Felder, Enum-Werte, Fehlerklassen, Dateinamen, Kommentare, Testbeschreibungen, Commit-Betreffs und Dokumentation sind deutsch. **Umlaute werden ASCII-gefaltet**: `Geraet`, `geaendertAm`, `laeufe`, `veroeffentlichtAm`. **Gefaltet werden Bezeichner, nicht Fließtext.** Alles, was ein Mensch zu lesen bekommt, schreibt sich richtig: Oberflächentexte, Fehlermeldungen, Protokoll-Aktionen (`"Rolle geändert"`), Überschriften. „Kuerzel“ in einer Fehlermeldung ist ein Fehler, `kuerzel` als Feldname nicht. Englisch bleiben nur: - vom Framework vorgegebene Namen: `GET`, `POST`, `layout.tsx`, `route.ts`, `page.tsx`, `proxy.ts`, `dependencies`, `default` - Lehnwörter aus dem Fachjargon: `tagCode`, `Token`, `OIDC`, `LDAP`, `Hostname`, `Shell`, `Markdown`, `Nonce`, `Healthcheck` Kommentare erklären **warum**, nicht was, und tragen wo hilfreich die Issue-Nummer: `(#007)`. ## Namensabbildung zur Auftragsvorgabe Der Auftrag nennt englische Modelle. Verbindlich gilt: | Vorgabe | Modell | Vorgabe | Modell | |---|---|---|---| | User | `Benutzer` | Template | `Vorlage` | | Customer | `Kunde` | TemplateVersion | `VorlagenVersion` | | Setting | `Einstellung` | TemplateVariable | `VorlagenVariable` | | DirectorySource | `Verzeichnisquelle` | TemplateSection | `VorlagenAbschnitt` | | Employee | `Mitarbeiter` | TemplateStep | `VorlagenSchritt` | | Department | `Abteilung` | ChecklistRun | `Lauf` | | Device | `Geraet` | StepResult | `Schrittergebnis` | | DeviceTemplateDefault | `GeraetetypVorlage` | Comment | `Kommentar` | | — (ergänzt) | `AbgleichSperre` | AuditLog | `Protokoll` | | — (ergänzt) | `BenutzerKunde` | — (ergänzt) | `TagCodeVergabe` | | — (ergänzt) | `Zustellung` | | | Enums: `Rolle {ADMIN REDAKTEUR TECHNIKER LESER}` · `Betriebsmodus {INTERN DIENSTLEISTUNG}` · `LaufStatus {OFFEN IN_ARBEIT ABGESCHLOSSEN ARCHIVIERT}` · `SchrittZustand {OFFEN ERLEDIGT NICHT_ZUTREFFEND FEHLGESCHLAGEN}` · `SchrittTyp {HAKEN TEXT ZAHL AUSWAHL DATUM BEFEHL}` (später `AKTION`) · `VariablenTyp {TEXT ZAHL AUSWAHL DATUM}` · `GeraeteTyp {PC NOTEBOOK SERVER FIREWALL SWITCH DRUCKER RAUM SONSTIGES}` · `VersionStatus {ENTWURF VEROEFFENTLICHT}` · `Herkunft {ABGLEICH MANUELL}` · `Zustellweg {MAIL WEBHOOK}` · `Zustellzustand {OFFEN GESENDET FEHLGESCHLAGEN AUFGEGEBEN}` · `Meldeanlass {LAUF_ZUGEWIESEN LAUF_UEBERFAELLIG LAUF_ABGESCHLOSSEN ABGLEICH_FEHLGESCHLAGEN}`. `SEED_MODE` akzeptiert `DIENSTLEISTUNG` und `SERVICE` als Alias. ## Schichtung ``` src/server/services/* Regeln und Schreiboperationen. Werfen eigene …Fehler-Klassen. src/server/queries/* Lesemodelle. Wenden Rollen- und Mandantensicht selbst an. src/server/actions/* Dünne "use server"-Einstiege. Prüfen genau ein Recht, rufen den Service. src/lib/* Framework-freie Helfer. Kein Next, kein React, gut testbar. src/app/api/* Route Handler. Nur Zustandsstrom, QR-SVG, CSV, Anhaenge, Auth, Healthcheck. Minimal. src/components/* Darstellung. Keine Geschäftslogik. ``` **Keine Geschäftslogik in Komponenten oder Route Handlern.** Eine Server Action ist ein Dreizeiler: Recht prüfen, Zod-Schema anwenden, Service rufen. ## Feste Regeln, die in Tests festgehalten sind - **Versionierung:** Eine veröffentlichte `VorlagenVersion` wird nie verändert. „Bearbeiten“ erzeugt eine tiefe Kopie als Entwurf. Höchstens ein Entwurf je Vorlage. Läufe verweisen nur auf veröffentlichte Versionen. - **Archiv:** `laufMussVeraenderbarSein(lauf)` sitzt an genau einer Stelle und wird vor jeder Änderung an Kopfdaten, `Schrittergebnis` und `Kommentar` gerufen. Zurückholen nur `ADMIN`. - **Abschluss:** nur wenn alle Pflichtschritte `ERLEDIGT` oder `NICHT_ZUTREFFEND` sind. `NICHT_ZUTREFFEND` und `FEHLGESCHLAGEN` verlangen eine Notiz. - **Mandant:** Jede kundenbezogene Zeile trägt `kundeId`. `kundeErmitteln()` liefert im Modus `INTERN` immer die eigene Organisation und **verwirft** eine mitgeschickte `kundeId`. - **Rechte:** nur serverseitig, aus der `RECHTE`-Tabelle über `darf()` / `rechtErforderlich()`. Rolle und Aktiv-Status werden pro Anfrage **aus der Datenbank** gelesen, nicht aus dem Token. **Jede Seite prüft selbst** — die Hülle unter `src/app/(app)/layout.tsx` ist das zweite Netz, nicht das erste: Next rendert Layouts bei Navigation innerhalb desselben Layouts nicht neu. **Route Handler deckt die Hülle nicht ab**; alles unter `src/app/api/`, das mehr tut als Auth und Healthcheck, ruft selbst `rechtErforderlich()`. Der Proxy prüft das Cookie absichtlich nur auf Vorhandensein, nie inhaltlich. - **Server-Module gehören nicht ins Browser-Bündel.** Eine Client-Komponente importiert nichts aus `src/server/*`. Was beide Seiten brauchen (Beschriftungen, Formate), liegt in `src/lib` — `ROLLEN_TEXT` etwa in `src/lib/rollen-text.ts`, während `RECHTE` und `darf()` serverseitig bleiben. - **`tagCode`:** wird nie geändert und nie wiederverwendet, auch nicht nach Archivierung. - **Löschen:** Geräte, Abteilungen und Vorlagen werden archiviert. Mitarbeiter werden nur `aktiv = false`. Nichts wird gelöscht, woran ein Lauf hängt. Genau **eine** Ausnahme gibt es: ein Gerät ohne Läufe lässt sich endgültig löschen, weil der Vertipper von eben sonst für immer in der Liste stünde. Das verlangt `verwaltung` — der Tag-Code ist danach für immer verbrannt, und der gedruckte Aufkleber wertlos. - **`{{generiertes_passwort}}`:** wird je Anzeige neu erzeugt und **nie** gespeichert — nicht in `Schrittergebnis`, `Kommentar`, `Protokoll` oder der Druckansicht. - **Erweiterbarkeit:** Kein Codepfad nimmt an, dass ein Schritt nur von einem Menschen erledigt wird. `SchrittTyp` bekommt später `AKTION`, `konfig` und `wert` sind Json, und `Lauf.erstelltVonId` wie `Schrittergebnis.aktualisiertVonId` dürfen leer sein — ein Systemlauf ist darstellbar. - **Regeln stehen in der Datenbank, nicht nur im Service.** CHECK-Bedingungen, partielle Eindeutigkeitsindizes, zusammengesetzte Fremdschlüssel und Trigger halten fest, was auch dann gelten muss, wenn jemand an der Service-Schicht vorbei schreibt. `tests/datenbankregeln.test.ts` zählt sie **namentlich** auf — dort steht die jeweils aktuelle Liste, und genau deshalb steht hier keine Zahl mehr: sie lief dreimal der Wirklichkeit nach. Die Mandantengrenze ist einer davon: ein Lauf kann strukturell kein Gerät, keine Person und keine Abteilung eines fremden Kunden wählen. Der Service prüft dasselbe noch einmal, nur mit besserer Meldung. ## Werkzeuge und Versionen Node 24 (`.nvmrc`), pnpm 12 über corepack. Exakt gepinnt, weil dort etwas im Fluss ist: `next@16.3.6`, `react@19.3.0`, `prisma`/`@prisma/client`/`@prisma/adapter-pg` auf **`7.10.0`** (der `latest`-Tag von `prisma` zeigt auf einen 8.0-RC, `@prisma/client` nicht — das würde auseinanderlaufen), `vitest@5.0.1`, `@playwright/test@1.63.0`. Zwei Obergrenzen, die nicht angehoben werden dürfen: - **`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. Eine neuere Fassung des Plugins gibt es nicht. - **`typescript` bleibt auf `~6.0.3`.** `typescript-eslint@8` erlaubt nur `>=4.8.4 <6.1.0`. TypeScript 7 ist noch nicht unterstützt. Build-Skripte von Abhängigkeiten sind in `pnpm-workspace.yaml` unter `allowBuilds` freigegeben (pnpm 12 liest das `pnpm`-Feld in `package.json` nicht mehr). **`minimumReleaseAgeExclude` bleibt leer.** pnpm 12 nimmt Pakete erst nach einer Wartezeit an — ein Schutz gegen ein kompromittiertes Release, das Stunden später zurückgezogen wird. Trägt `pnpm add` dort von sich aus eine Ausnahme ein, ist das kein Formfehler, sondern die Meldung „diese Version ist frisch von heute“. Dann gehört **eine abgelagerte Version** gepinnt, nicht die Ausnahme committet. `nodemailer` steht deshalb exakt auf `10.0.10` und nicht auf der neuesten Fassung (#034). ## Next 16 `middleware.ts` heißt **`proxy.ts`** und exportiert `proxy()`, Node-Laufzeit. `cookies()`, `headers()`, `params` und `searchParams` sind **async**. Route Handler: `(anfrage, { params }: { params: Promise<{ id: string }> })`. Turbopack ist Standard. `next lint` ist entfernt, `pnpm lint` ruft `eslint` direkt. ## Prisma 7 Generator `prisma-client` mit Pflicht-`output` nach `src/generated/prisma`, `datasource` ohne `url`, Zugriff über `@prisma/adapter-pg`. **Importpfade, einmal richtig:** Modelltypen kommen aus `@/generated/prisma/client` (`import type { Benutzer } from "@/generated/prisma/client"`), Enum-**Werte** aus `@/generated/prisma/enums`. `@/generated/prisma/models` exportiert nur die Hilfstypen der Abfragen, nicht die Modelle selbst — ein `import` von dort schlägt fehl. Ein Adapter ist **Pflicht** — `new PrismaClient()` ohne `adapter` wirft. Seed steht in `prisma.config.ts` unter `migrations.seed`. Der Client wird über den Lazy-Proxy in `src/lib/db.ts` geholt, damit `next build` ohne `DATABASE_URL` läuft. ## Tests brauchen die Testdatenbank für sich allein `tests/helpers/db.ts` leert vor jedem Test alle Tabellen. Zwei gleichzeitige Läufe gegen dieselbe Datenbank räumen sich deshalb gegenseitig die Daten weg, und das sieht aus wie ein echter Fehler (typisch: `Foreign key constraint violated on Protokoll_benutzerId_fkey`). Wer parallel prüfen lässt — etwa einen Review-Agenten —, gibt ihm eine **eigene** Datenbank: ```bash createdb -h 127.0.0.1 -p 5434 -U laufzettel laufzettel_review TEST_DATABASE_URL=postgresql://laufzettel:laufzettel@localhost:5434/laufzettel_review pnpm test ``` ## Oberfläche prüfen **Immer gegen den gebauten Stand, nie gegen `pnpm dev`:** ```bash pnpm build && PORT=3300 APP_URL=http://127.0.0.1:3300 pnpm start ``` 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 dort mit `ERR_INVALID_HTTP_RESPONSE`. Es liegt also am Werkzeug, nicht an diesem Code — der gebaute Stand hydriert einwandfrei (1281 von 1296 Elementen tragen React-Knoten, im Entwicklungsmodus 2). Das ist heimtückisch: ein Screenshot beweist damit **nichts** über Bedienbarkeit. Wer eine Oberfläche abnimmt, klickt sie im gebauten Stand durch — hinzufügen, speichern, neu laden, und nachsehen, ob es wirklich in der Datenbank steht. `pnpm docs:screenshots` verlangt deshalb ebenfalls den gebauten Stand. ## Vor jedem Commit ```bash docker compose -f docker-compose.test.yml up -d --wait ldap mailpit pnpm lint && pnpm typecheck && pnpm format:pruefen && pnpm test && pnpm pruefe:doku ``` **Die beiden Container gehören in die erste Zeile.** Ohne sie überspringt die Suite Tests, sagt das auf stderr — und sieht trotzdem grün aus. Wer nur diesem Abschnitt folgte, hätte das Betroffene nie geprüft. In CI bricht sie stattdessen ab, absichtlich. | Container | Fehlt er, fehlen | Betroffen | |---|---|---| | `ldap` | **22 Tests** (nachgezählt: 81 statt 59) | der ganze Verzeichnis-Abgleich, `tests/abgleich.test.ts` und `tests/verzeichnis.test.ts` | | `mailpit` | **3 Tests** (nachgezählt: 36 statt 33) | der echte SMTP-Versand in `tests/benachrichtigungen.test.ts` — die vier Anlässe, die Passwort-Prüfung über alle Rumpftexte und „nichts zweimal“ | `ldap` fiel bei der Abnahme zu #019 auf, `mailpit` kam mit #034 dazu. Beide Zahlen sind **gemessen**, nicht geschätzt — in #040 hätte beinahe eine geratene in dieser Datei gestanden. `format:pruefen` gehört dazu, seit der Review in #010 darauf gestoßen ist: `lint-staged` formatiert beim Commit automatisch nach, und das Ergebnis wäre ein Commit mit Änderungen, die niemand gesehen hat. Lieber vorher sehen, was sich ändert. Der Pre-Commit-Hook erzwingt Lint und Format, der Commit-Msg-Hook Conventional Commits mit höchstens 72 Zeichen im Betreff. Ein Commit pro abgeschlossener, lauffähiger Änderung. Kein `wip` auf `main`. Fußzeile referenziert das Issue: `Closes #12`. ## Gestaltung Dunkel zuerst, ein einziger Akzent (Signal-Lime `#C6F135`) nur für Aktionen, Fortschritt, Fokus und aktive Zustände. Token stehen als CSS-Variablen in `src/app/globals.css` unter `@theme`; Komponenten greifen nur darauf zu, nie auf rohe Hexwerte. Schriften selbst gehostet über `@fontsource-variable`. Icons ausschließlich Lucide-Stroke-SVGs — **keine Emojis**. Keine Verläufe, keine Schatten; Karten mit 1-px-Linie. Buttons als Pillen, 40 px hoch, am Handy 44–46 px. Abschnitte mit zweistelliger Nummer (`01 · Vorbereitung`), die aus der Position kommt und nie gespeichert wird. Druckansichten und Etiketten immer hell. **Am Handy nie horizontales Scrollen** — bei 390 px geprüft. **„Nie auf rohe Hexwerte" meint die Oberfläche.** Es gibt genau sechs begründete Ausnahmen, und sie haben alle denselben Grund: dort entscheidet nicht unser Farbschema, sondern etwas außerhalb davon. | Stelle | Wert | Warum kein Token | |---|---|---| | `src/app/drucken/lauf/[id]/page.tsx`, `src/app/drucken/etiketten/page.tsx` | `#ffffff` | Druck ist immer hell, unabhängig vom Erscheinungsbild der Anwendung | | `src/lib/qr.ts` | `#000000ff` / `#ffffffff` | Ein QR-Code muss reines Schwarz auf reinem Weiß sein, sonst liest ihn kein Decoder | | `src/app/layout.tsx` (`themeColor`), `src/app/manifest.ts` (zweimal) | `#0B0D10` | `themeColor` und das Web-App-Manifest vertragen keine CSS-Variablen | Wer eine siebte hinzufügt, trägt sie hier ein — oder lässt es. > Der folgende Block ist **nicht von uns**: `next dev` schreibt ihn bei jedem Start selbst in diese > Datei und stellt ihn wieder her, wenn man ihn löscht. Er steht deshalb hier, auf Englisch, und > wird mitcommittet, damit der Arbeitsbaum sauber bleibt. # This is NOT the Next.js you know This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.