Die Stammdaten, an denen die Historie haengt. Personen werden deaktiviert, Geraete und Abteilungen archiviert — geloescht wird nichts, woran ein Lauf haengen kann. - Mitarbeiterliste mit Suche ueber Anzeigename, Mail, UPN und sAMAccountName, Filter nach Abteilung, standardmaessig nur aktive; die Suche ist ein eigenes Bauteil, weil #013 und #014 sie wieder brauchen - Personenseite mit Stammdaten, Geraeten, Vorgesetztem; was aus dem Verzeichnis kommt, ist gesperrt — der naechste Abgleich wuerde es ohnehin zurueckdrehen - Abteilungen unter Admin: Ziel-OU, Standardgruppen, Mail-Domain, dazu die Namen, die bei Personen vorkommen, aber nicht gepflegt sind - Geraete mit Suche ueber Hostname, Bezeichnung, Seriennummer und Tag-Code; CSV-Import mit einem Ergebnis je Zeile - Neues Recht mitarbeiterBearbeiten; die Seitenleiste fuehrt Personen und Geraete unter einem Eintrag, im Modus Dienstleistung anders beschriftet Der unabhaengige Review fand zwei Blocker. Der schwerere meldete Erfolg: ein nicht geschlossenes Anfuehrungszeichen verschluckte im CSV-Import alle Folgezeilen bis zum naechsten `"`, und der Import sagte „3 von 3 Zeilen angelegt". Ein Wert reicht jetzt nicht mehr ueber die Zeile hinaus — fuer Hostnamen und Seriennummern ist ein Umbruch praktisch immer ein vergessenes Anfuehrungszeichen, und so endet der Schaden an seiner Zeile. Der zweite: ungepflegte Abteilungen wurden ueber alle Kunden hinweg gruppiert. Dazu sieben weitere Befunde, alle behoben — darunter eine Besitzerauswahl, die Personen fremder Kunden anbot, und ein Techniker, der ein Geraet endgueltig loeschen konnte. 610 Tests gruen, 18 E2E-Ablaeufe gegen den gebauten Stand. Schliesst #011 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
214 lines
12 KiB
Markdown
214 lines
12 KiB
Markdown
# 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` |
|
||
|
||
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}`.
|
||
|
||
`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 Polling, QR-SVG, CSV, 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.** Zwölf CHECK-Bedingungen, zwei
|
||
partielle Eindeutigkeitsindizes und vier zusammengesetzte Fremdschlüssel halten fest, was auch
|
||
dann gelten muss, wenn jemand an der Service-Schicht vorbei schreibt. 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).
|
||
|
||
## 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
|
||
pnpm lint && pnpm typecheck && pnpm format:pruefen && pnpm test && pnpm pruefe:doku
|
||
```
|
||
|
||
`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.
|
||
|
||
> 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.
|
||
|
||
<!-- BEGIN:nextjs-agent-rules -->
|
||
|
||
# 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.
|
||
|
||
<!-- END:nextjs-agent-rules -->
|