diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 0b2e511..7126b49 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -47,6 +47,9 @@ jobs: - name: Code-Stil prüfen run: pnpm lint + - name: Format prüfen + run: pnpm format:pruefen + - name: Typen prüfen run: pnpm typecheck diff --git a/CHANGELOG.md b/CHANGELOG.md index 28849a8..8dd4a8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,19 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/). - JSON-Export und -Import für Vorlagen, Format dokumentiert in `docs/vorlagen-format.md` (#009) - Zwei Redakteure am selben Entwurf treten sich nicht mehr auf die Füße: wer auf einem überholten Stand speichert, wird abgewiesen statt die Arbeit des anderen spurlos zu überschreiben (#009) +- Vorlagenliste mit Suche, Filter nach Kategorie und Schlagwort und dem Kennzeichen + „Entwurf vorhanden“; eine Vorlage lässt sich anlegen, duplizieren, archivieren und als JSON + ein- und ausspielen (#010) +- Vorlagen-Editor: Abschnitte, Schritte und Variablen anlegen, bearbeiten, löschen und sortieren + — mit dem Finger, der Maus, der Tastatur oder zwei Knöpfen an jeder Zeile (#010) +- Typabhängige Felder je Schritt: Auswahloptionen, Zahlenbereich mit Einheit, mehrzeiliger Text, + Shell und Befehlstext; Hilfetexte als Markdown, dazu ein Doku-Link (#010) +- Warnung im Editor, wenn ein Platzhalter zu keiner Variablen und zu keinem eingebauten Wert + gehört — sie blockiert nichts, sie zeigt den Tippfehler, bevor der Techniker ihn im Lauf findet + (#010) +- Vorschau der Vorlage mit Beispielwerten, so wie ein Lauf sie zeigen wird (#010) +- Versionsgeschichte je Vorlage: wer wann mit welcher Notiz veröffentlicht hat und wie viele Läufe + an einer Version hängen (#010) ### Sicherheit @@ -94,3 +107,14 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/). Aufbau einer Version geben nichts heraus, was der Benutzer nicht sehen darf (#009) - Der Doku-Link eines Schritts nimmt nur `http` und `https` an; über eine importierte Datei wäre ein `javascript:`-Link sonst fremder Code im Browser des Technikers (#009) +- Hilfetexte werden als Markdown ohne rohes HTML gerendert — ein Skript im Hilfetext einer + eingespielten Vorlage bleibt sichtbarer Text (#010) +- Der Doku-Link eines Schritts wird auch in der Vorschau nur dann verlinkt, wenn er `http` oder + `https` ist — dort hat ihn das Schema noch nicht gesehen (#010) +- Die Content-Security-Policy lockert `unsafe-eval` und WebSocket-Verbindungen **nur** im + Entwicklungsmodus; ein Test prüft die Entscheidung im Proxy selbst und nicht bloß die Funktion + darunter (#010) +- Eine abgelaufene Anmeldung beendet den Vorgang mit einer Meldung, statt die Seite auszuhängen — + im Editor wäre sonst jede ungespeicherte Arbeit verloren gewesen (#010) +- Eine archivierte Kundenzuordnung bleibt beim Speichern der Kopfdaten erhalten, statt sich + stillschweigend in „für alle Kunden“ zu verwandeln (#010) diff --git a/CLAUDE.md b/CLAUDE.md index 35ac254..d6cd838 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -149,12 +149,37 @@ 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 test && pnpm pruefe:doku +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`. @@ -166,5 +191,20 @@ und aktive Zustände. Token stehen als CSS-Variablen in `src/app/globals.css` un 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`). Druckansichten und Etiketten immer hell. +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. + + + +# 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. + + diff --git a/docs/architektur.md b/docs/architektur.md index 8b06075..288a40e 100644 --- a/docs/architektur.md +++ b/docs/architektur.md @@ -75,6 +75,16 @@ Editor, den JSON-Export und den Import — drei getrennte Schemata würden ausei 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 @@ -118,6 +128,28 @@ gegen den Zweck eines Archivs. Der `tagCode` ist zusätzlich in `TagCodeVergabe` 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: diff --git a/docs/devlog.md b/docs/devlog.md index b6db790..707f005 100644 --- a/docs/devlog.md +++ b/docs/devlog.md @@ -12,11 +12,12 @@ als Unterpunkte mit Aufgabe und Dauer. | #007 | M2 – Auth, Rollen, Benutzer | 1 h 35 min | | #008 | M3 – Betriebsmodus und Kunden | 1 h 20 min | | #009 | M4 – Vorlagen-Versionierung im Service | 1 h 20 min | -| | **Summe** | **6 h 50 min** | +| #010 | M5 – Vorlagen-Editor | 2 h 10 min | +| | **Summe** | **9 h** | | Meilenstein | Tatsächlich | Status | |---|---|---| -| v0.1.0 | 6 h 50 min | in Arbeit | +| v0.1.0 | 9 h | in Arbeit | --- @@ -339,3 +340,59 @@ als Unterpunkte mit Aufgabe und Dauer. - #010 Vorlagen-Editor: Liste mit Suche und Filtern, Abschnitte und Schritte per Drag & Drop, Vorschau, Veröffentlichen, Versionshistorie, Warnung bei unbekannten Platzhaltern. + +--- + +## 2026-09-25 · Sitzung 6 · 08:20–10:30 + +**Issue:** #010 (M5) +**Version:** 0.1.0 + +### Erledigt + +- Vorlagenliste mit Suche und Filtern, Vorlagenseite mit Versionsgeschichte, Kopfdaten, Duplizieren, + Archivieren, JSON-Ex- und -Import +- Vorlagen-Editor (Sub-Agent): Abschnitte, Schritte, Variablen — anlegen, bearbeiten, löschen, + sortieren mit Finger, Maus, Tastatur und Knöpfen; typabhängige Felder, Markdown-Hilfetexte, + Platzhalter-Warnung, Vorschau mit Beispielwerten +- `src/lib/platzhalter.ts` als einzige Quelle der eingebauten Platzhalter — #012 baut darauf auf +- Erste Playwright-Tests: der Editor wird durch die Oberfläche gefahren, einschließlich Sortieren + mit dem Finger bei 390 px +- Unabhängiger Review: zwei Blocker, sechs weitere Befunde, alle abgearbeitet +- **496 Tests** grün (vorher 428), dazu 5 E2E-Tests + +### Entscheidungen + +- **Oberflächen werden gegen den gebauten Stand abgenommen, nie gegen `pnpm dev`.** Der + Entwicklungsmodus hydriert in dieser Umgebung nicht; ein Screenshot sieht deshalb richtig aus, + während kein Knopf etwas tut. Das ändert die Arbeitsweise für alle folgenden Meilensteine und + steht in `CLAUDE.md` und `docs/architektur.md`. +- **Was nur mit angehängtem React gelingt, gehört in einen Playwright-Test.** Nicht erst in #018: + ohne ihn wäre „Sortieren funktioniert am Handy“ eine Behauptung geblieben. +- **Die Nummer eines Abschnitts kommt aus der Position und wird nie gespeichert.** Die + Formatdokumentation sagte bisher das Gegenteil; beim ersten Umsortieren wäre eine gespeicherte + Nummer falsch gewesen. Entfernt wird eine vorangestellte Nummer nur beim Trenner `·` und nur, + wenn dahinter keine Ziffer steht — „24 · 7 Bereitschaft“ bleibt. +- **Ein Knopf, der auf eine fehlende Seite zeigt, wird nicht verlinkt.** Galt für die Navigation + schon, jetzt auch für „Scannen“. +- **`format:pruefen` ist ein Tor.** Sonst formatiert `lint-staged` beim Commit nach, und im Commit + stehen Änderungen, die niemand gesehen hat. + +### Probleme + +- **Mein eigener Prüfaufbau hat mich getäuscht.** Ich habe Seiten im Browser angesehen, für gut + befunden und weitergemacht — in einem Entwicklungsmodus, in dem nichts anklickbar war. Aufgefallen + ist es erst, als ich zum ersten Mal wirklich geklickt habe. Zwei Stunden Suche nach der Ursache, + am Ende lag es am Werkzeug und nicht am Code; die Erkenntnis war die Zeit wert. +- **Ein Kommentar hat wieder eine Zusage gemacht, die kein Test hielt** („ein Test hält das fest“, + über der CSP). Dritter Meilenstein in Folge, in dem der Review genau diese Sorte Fehler findet. + Ein Kommentar, der eine Garantie behauptet, braucht den Test daneben — oder den Satz nicht. +- **Der Sub-Agent hat im Browser Dinge geprüft, die dort gar nicht prüfbar waren.** Mein Auftrag + hat ihn dorthin geschickt; die Lücke war meine. +- **Der Editor war bei einer einzigen Vorlage 16 986 px hoch.** Ich habe es im Screenshot gesehen, + nicht der Review. + +### Nächster Schritt + +- #011 Mitarbeiter, Abteilungen und Geräte: Listen mit Suche, Mitarbeiterseite mit Historie, + Abteilungen mit Ziel-OU und Standardgruppen, Geräte mit CSV-Import. diff --git a/docs/issues/010-vorlagen-editor.md b/docs/issues/010-vorlagen-editor.md index aebce8b..f57c33c 100644 --- a/docs/issues/010-vorlagen-editor.md +++ b/docs/issues/010-vorlagen-editor.md @@ -1,9 +1,9 @@ # #010 M5 – Vorlagen-Editor -**Status:** offen +**Status:** erledigt **Meilenstein:** v0.1.0 **Labels:** phase-1, frontend -**Geschätzt:** 10 h **Tatsächlich:** – +**Geschätzt:** 10 h **Tatsächlich:** 2 h 10 min ## Ziel @@ -11,21 +11,68 @@ Der Editor, in dem ein Redakteur eine Vorlage baut, ohne den Kopf über Versione ## Aufgaben -- [ ] Vorlagenliste mit Suche, Filter nach Kategorie und Tag, Kennzeichnung „Entwurf vorhanden“ -- [ ] Editor: Abschnitte und Schritte anlegen, bearbeiten, löschen; Abschnitte mit zweistelliger Nummer (`01 · Vorbereitung`) -- [ ] Sortieren per Drag & Drop mit `@dnd-kit`, auch am Handy mit dem Finger bedienbar -- [ ] Je Schritt: Titel, Typ (`HAKEN`, `TEXT`, `ZAHL`, `AUSWAHL`, `DATUM`, `BEFEHL`), Pflicht (roter Stern), Hilfetext als Markdown, Doku-Link -- [ ] Typabhängige `konfig`-Formulare: Auswahloptionen, Min/Max/Einheit, bei `BEFEHL` die Shell (`powershell`/`bash`/`cmd`), `multiline` und der Befehlstext -- [ ] Variablen definieren: `key`, Label, Typ, Optionen, Pflicht, Position -- [ ] Vorschau der Vorlage wie im Lauf, mit Beispielwerten -- [ ] Veröffentlichen mit Änderungsnotiz, Versionshistorie ansehen, Vorlage duplizieren, JSON-Export und -Import -- [ ] Warnung im Editor bei unbekannten Platzhaltern in Titel, Hilfetext und Befehlstext -- [ ] Screenshots Liste, Editor, Historie in `docs/screenshots/m5/` +- [x] Vorlagenliste mit Suche, Filter nach Kategorie und Tag, Kennzeichnung „Entwurf vorhanden“ +- [x] Editor: Abschnitte und Schritte anlegen, bearbeiten, löschen; Abschnitte mit zweistelliger Nummer (`01 · Vorbereitung`) +- [x] Sortieren per Drag & Drop mit `@dnd-kit`, auch am Handy mit dem Finger bedienbar +- [x] Je Schritt: Titel, Typ (`HAKEN`, `TEXT`, `ZAHL`, `AUSWAHL`, `DATUM`, `BEFEHL`), Pflicht (roter Stern), Hilfetext als Markdown, Doku-Link +- [x] Typabhängige `konfig`-Formulare: Auswahloptionen, Min/Max/Einheit, bei `TEXT` mehrzeilig, bei `BEFEHL` die Shell (`powershell`/`bash`/`cmd`) und der Befehlstext +- [x] Variablen definieren: `key`, Label, Typ, Optionen, Pflicht, Position +- [x] Vorschau der Vorlage wie im Lauf, mit Beispielwerten +- [x] Veröffentlichen mit Änderungsnotiz, Versionshistorie ansehen, Vorlage duplizieren, JSON-Export und -Import +- [x] Warnung im Editor bei unbekannten Platzhaltern in Titel, Hilfetext und Befehlstext +- [x] Screenshots Liste, Editor, Historie in `docs/screenshots/m5/` ## Akzeptanzkriterien -- [ ] Eine Vorlage lässt sich vollständig ohne Umweg über die Datenbank anlegen, sortieren, veröffentlichen -- [ ] Eine veröffentlichte Version ist im Editor nur lesend; „Bearbeiten“ erzeugt sichtbar einen Entwurf -- [ ] Unbekannte Platzhalter werden als Warnung angezeigt, blockieren aber das Speichern nicht -- [ ] Sortieren funktioniert am Handy bei 390 px Breite ohne horizontales Scrollen -- [ ] Der Markdown-Hilfetext rendert ohne rohes HTML +- [x] Eine Vorlage lässt sich vollständig ohne Umweg über die Datenbank anlegen, sortieren, veröffentlichen +- [x] Eine veröffentlichte Version ist im Editor nur lesend; „Bearbeiten“ erzeugt sichtbar einen Entwurf +- [x] Unbekannte Platzhalter werden als Warnung angezeigt, blockieren aber das Speichern nicht +- [x] Sortieren funktioniert am Handy bei 390 px Breite ohne horizontales Scrollen +- [x] Der Markdown-Hilfetext rendert ohne rohes HTML + +## Beim Bauen aufgefallen + +- **`next dev` hydriert in dieser Umgebung überhaupt nicht.** Die Seiten werden serverseitig + gerendert und sehen vollständig richtig aus, aber React hängt sich nie an — kein Knopf tut etwas. + Gemessen: im Entwicklungsmodus tragen 2 von 1302 Elementen React-Knoten, im gebauten Stand 1281 + von 1296. Es liegt nicht an diesem Code: auch ohne `src/proxy.ts` und ohne `output: "standalone"` + bleibt es dabei, und der HMR-WebSocket scheitert mit `ERR_INVALID_HTTP_RESPONSE`. Das ist der + heimtückischste Befund des Meilensteins, denn **ein Screenshot beweist damit nichts über + Bedienbarkeit** — und genau darauf haben sich die Meilensteine #005 bis #009 verlassen. Seitdem + steht in `CLAUDE.md`: Oberflächen werden gegen `pnpm build && pnpm start` abgenommen, und was nur + mit angehängtem React gelingt, gehört in einen Playwright-Test. +- **Die CSP hatte im Entwicklungsmodus kein `unsafe-eval`.** React braucht es dort, die Konsole + meldete es auf jeder Seite. Behoben — es war nicht die Ursache des Hydrierungsproblems, aber ein + echter Fehler. Der ausgelieferte Stand bleibt streng, und der Test dazu prüft jetzt den Proxy + selbst statt nur die Funktion darunter: der Review hatte zu Recht beanstandet, dass der + Kommentar eine Zusage machte, die kein Test hielt. +- **Eine abgelaufene Anmeldung hätte im Editor eine Stunde Arbeit vernichtet.** `rechtErforderlich` + wirft, der Wurf lief durch die Server Action bis in Nexts Fehlergrenze, und die hängt die Seite + aus — samt allem, was noch nicht gespeichert war. Nach zwölf Stunden läuft die Sitzung von selbst + ab; es hätte genau die Person getroffen, die lange an einer Vorlage sitzt. Jetzt wird der Fehler + gefangen und als Meldung gezeigt, der Aufbau lässt sich bei **jedem** fehlgeschlagenen Speichern + als JSON herauskopieren, und es gibt eine deutsche Fehlergrenze. +- **Eine archivierte Kundenzuordnung verschwand beim Speichern der Kopfdaten.** Der archivierte + Kunde fehlte in der Auswahlliste, der Browser wählte die erste Option („für alle Kunden“), und + ein Klick auf „Kopfdaten speichern“ machte aus einer kundenspezifischen Vorlage eine für alle + sichtbare. Genau das, was der Kommentar über der Unterscheidung „Feld fehlt“ gegen „Feld leer“ + zu verhindern versprach — er deckte den Fall „Feld da, aber unvollständig“ nicht ab. +- **Zwei stille Textverfälscher.** `titelOhneNummer` machte aus „24 · 7 Bereitschaft“ ein + „7 Bereitschaft“, und `mitBeispielwerten` wertete `$&` im **Ersatztext** als Rückverweis — in + #012 wäre daraus ein verfälschter PowerShell-Befehl geworden. +- **Der Editor war 16 986 px hoch**, weil jeder Schritt dauerhaft aufgeklappt war. Mit den sieben + Vorlagen aus #017 (rund 150 Schritte) wäre er unbenutzbar gewesen. Jetzt klappen Schritte, + Variablen und Abschnitte zu: dieselbe Vorlage misst 3 482 px am Handy. +- **`abschnitte.2.schritte.0.titel` ist eine richtige und nutzlose Fehlermeldung.** Daraus wird + jetzt „Abschnitt 03, Schritt 1: …“. Ein leeres Pflichtfeld weist weiterhin den ganzen Aufbau ab — + aber man findet die Stelle. +- **Der „Scannen“-Knopf zeigte auf eine Seite, die es nicht gibt.** Next holt Verweise beim + Darüberfahren vorab, jede Seite erzeugte also im Hintergrund eine 404. Bis #014 ist er gedämpft + und nicht verlinkt — dieselbe Regel, die für die Navigation schon galt. +- **Eine veröffentlichte Vorlage lässt sich nicht mehr löschen**, auch nicht über einen + Kaskadenlöschvorgang: der Trigger aus #009 verweigert es. Beim Aufräumen der Testdaten selbst + erlebt. Das ist die Zusage, an der das Archiv hängt — die E2E-Tests archivieren deshalb, statt + zu löschen, und brauchen ab #018 eine Wegwerf-Datenbank. +- **`prettier --check` war rot**, ohne dass ein Tor es bemerkt hätte: `lint-staged` formatiert beim + Commit automatisch nach, und das Ergebnis wäre ein Commit mit Änderungen, die niemand gesehen + hat. `pnpm format:pruefen` gehört jetzt zu den Toren und zur CI. diff --git a/docs/issues/README.md b/docs/issues/README.md index 8ed8400..5053d10 100644 --- a/docs/issues/README.md +++ b/docs/issues/README.md @@ -14,7 +14,7 @@ Diese Dateien sind die Quelle; `pnpm issues:sync` spiegelt sie in den Gitea-Trac | #007 | [M2 – Auth, Rollen, Benutzer](007-auth-rollen-benutzer.md) | erledigt | v0.1.0 | 8 h | 1 h 35 min | | #008 | [M3 – Betriebsmodus und Kunden](008-betriebsmodus-kunden.md) | erledigt | v0.1.0 | 6 h | 1 h 20 min | | #009 | [M4 – Vorlagen-Versionierung im Service](009-vorlagen-versionierung.md) | erledigt | v0.1.0 | 6 h | 1 h 20 min | -| #010 | [M5 – Vorlagen-Editor](010-vorlagen-editor.md) | offen | v0.1.0 | 10 h | – | +| #010 | [M5 – Vorlagen-Editor](010-vorlagen-editor.md) | erledigt | v0.1.0 | 10 h | 2 h 10 min | | #011 | [M6 – Mitarbeiter, Abteilungen, Geräte](011-mitarbeiter-abteilungen-geraete.md) | offen | v0.1.0 | 9 h | – | | #012 | [M7 – Platzhalter, Namensregeln, Escaping](012-platzhalter-namensregeln.md) | offen | v0.1.0 | 8 h | – | | #013 | [M8 – Läufe: Service und Abarbeitung](013-laeufe-abarbeitung.md) | offen | v0.1.0 | 14 h | – | @@ -24,4 +24,4 @@ Diese Dateien sind die Quelle; `pnpm issues:sync` spiegelt sie in den Gitea-Trac | #017 | [M12 – Seed der sieben Vorlagen](017-seed-sieben-vorlagen.md) | offen | v0.1.0 | 8 h | – | | #018 | [M13 – E2E-Tests, Anleitung, PDF](018-e2e-anleitung-pdf.md) | offen | v0.1.0 | 14 h | – | | #019 | [M14 – Betriebswege und Release v0.1.0](019-betrieb-release.md) | offen | v0.1.0 | 10 h | – | -| | **Summe** | | | **136 h** | **6 h 50 min** | +| | **Summe** | | | **136 h** | **9 h** | diff --git a/docs/screenshots/m5/admin-benutzer-desktop.png b/docs/screenshots/m5/admin-benutzer-desktop.png new file mode 100644 index 0000000..58743c6 Binary files /dev/null and b/docs/screenshots/m5/admin-benutzer-desktop.png differ diff --git a/docs/screenshots/m5/admin-benutzer-handy.png b/docs/screenshots/m5/admin-benutzer-handy.png new file mode 100644 index 0000000..4f6758f Binary files /dev/null and b/docs/screenshots/m5/admin-benutzer-handy.png differ diff --git a/docs/screenshots/m5/admin-einstellungen-desktop.png b/docs/screenshots/m5/admin-einstellungen-desktop.png new file mode 100644 index 0000000..73d467a Binary files /dev/null and b/docs/screenshots/m5/admin-einstellungen-desktop.png differ diff --git a/docs/screenshots/m5/admin-einstellungen-handy.png b/docs/screenshots/m5/admin-einstellungen-handy.png new file mode 100644 index 0000000..f47021a Binary files /dev/null and b/docs/screenshots/m5/admin-einstellungen-handy.png differ diff --git a/docs/screenshots/m5/admin-moduswechsel-desktop.png b/docs/screenshots/m5/admin-moduswechsel-desktop.png new file mode 100644 index 0000000..08d8178 Binary files /dev/null and b/docs/screenshots/m5/admin-moduswechsel-desktop.png differ diff --git a/docs/screenshots/m5/admin-moduswechsel-handy.png b/docs/screenshots/m5/admin-moduswechsel-handy.png new file mode 100644 index 0000000..bb6b4a2 Binary files /dev/null and b/docs/screenshots/m5/admin-moduswechsel-handy.png differ diff --git a/docs/screenshots/m5/anmelden-desktop.png b/docs/screenshots/m5/anmelden-desktop.png new file mode 100644 index 0000000..a009bfb Binary files /dev/null and b/docs/screenshots/m5/anmelden-desktop.png differ diff --git a/docs/screenshots/m5/anmelden-handy.png b/docs/screenshots/m5/anmelden-handy.png new file mode 100644 index 0000000..3dd1a9f Binary files /dev/null and b/docs/screenshots/m5/anmelden-handy.png differ diff --git a/docs/screenshots/m5/uebersicht-desktop.png b/docs/screenshots/m5/uebersicht-desktop.png new file mode 100644 index 0000000..c58a5c6 Binary files /dev/null and b/docs/screenshots/m5/uebersicht-desktop.png differ diff --git a/docs/screenshots/m5/uebersicht-handy.png b/docs/screenshots/m5/uebersicht-handy.png new file mode 100644 index 0000000..8e4b1db Binary files /dev/null and b/docs/screenshots/m5/uebersicht-handy.png differ diff --git a/docs/screenshots/m5/vorlagen-editor-desktop.png b/docs/screenshots/m5/vorlagen-editor-desktop.png new file mode 100644 index 0000000..87d4522 Binary files /dev/null and b/docs/screenshots/m5/vorlagen-editor-desktop.png differ diff --git a/docs/screenshots/m5/vorlagen-editor-handy.png b/docs/screenshots/m5/vorlagen-editor-handy.png new file mode 100644 index 0000000..d18dca8 Binary files /dev/null and b/docs/screenshots/m5/vorlagen-editor-handy.png differ diff --git a/docs/screenshots/m5/vorlagen-liste-desktop.png b/docs/screenshots/m5/vorlagen-liste-desktop.png new file mode 100644 index 0000000..3c26b8d Binary files /dev/null and b/docs/screenshots/m5/vorlagen-liste-desktop.png differ diff --git a/docs/screenshots/m5/vorlagen-liste-handy.png b/docs/screenshots/m5/vorlagen-liste-handy.png new file mode 100644 index 0000000..adff965 Binary files /dev/null and b/docs/screenshots/m5/vorlagen-liste-handy.png differ diff --git a/docs/screenshots/m5/vorlagen-seite-desktop.png b/docs/screenshots/m5/vorlagen-seite-desktop.png new file mode 100644 index 0000000..d324bf6 Binary files /dev/null and b/docs/screenshots/m5/vorlagen-seite-desktop.png differ diff --git a/docs/screenshots/m5/vorlagen-seite-handy.png b/docs/screenshots/m5/vorlagen-seite-handy.png new file mode 100644 index 0000000..7b144eb Binary files /dev/null and b/docs/screenshots/m5/vorlagen-seite-handy.png differ diff --git a/docs/vorlagen-format.md b/docs/vorlagen-format.md index 4d794dc..85e77d5 100644 --- a/docs/vorlagen-format.md +++ b/docs/vorlagen-format.md @@ -56,7 +56,7 @@ Platzhaltern. **JSON kennt keine Kommentare** — die Zeilen mit `//` müssen vo // Die Reihenfolge dieser Liste ist die Reihenfolge in der Ansicht. "abschnitte": [ { - "titel": "01 · Vorbereitung", + "titel": "Vorbereitung", "schritte": [ { "titel": "Gerät auspacken", "pflicht": true }, { @@ -75,7 +75,7 @@ Platzhaltern. **JSON kennt keine Kommentare** — die Zeilen mit `//` müssen vo ] }, { - "titel": "02 · Konto", + "titel": "Konto", "schritte": [ { "titel": "Benutzerkonto anlegen", @@ -135,9 +135,17 @@ dass es jemand merkt. | Feld | Pflicht | Bedeutung | | ---------- | -------- | ----------------------------------------------------------------------------- | -| `titel` | Pflicht | Überschrift, nicht leer. Üblich mit zweistelliger Nummer: `01 · Vorbereitung`. | +| `titel` | Pflicht | Überschrift, nicht leer und **ohne** Nummer — siehe unten. | | `schritte` | optional | Liste von Schritten in Abarbeitungsreihenfolge; Standard `[]`. | +Abschnitte werden überall mit einer zweistelligen Nummer angezeigt (`01 · Vorbereitung`). Diese +Nummer wird **aus der Position abgeleitet und nie gespeichert**. Stünde sie im Titel, wäre sie beim +ersten Umsortieren falsch — und niemand ginge hinterher durch und zöge sie nach. Trägt ein Titel +dennoch eine vorangestellte Nummer mit `·` als Trenner (ältere Dateien taten das), wird sie beim +Öffnen im Editor entfernt, damit nicht `01 · 01 · Vorbereitung` dasteht; mit dem nächsten Speichern +gilt der bereinigte Titel. Andere Trenner bleiben unangetastet: „2. Etage prüfen“ ist ein Titel und +keine Nummerierung. + ### Schritt | Feld | Pflicht | Bedeutung | diff --git a/package.json b/package.json index 23fef70..8de6350 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "node": ">=24" }, "scripts": { + "beispiel:vorlage": "tsx scripts/beispiel-vorlage.ts", "build": "next build", "db:down": "docker compose -f docker-compose.dev.yml down", "db:generate": "prisma generate", @@ -20,6 +21,7 @@ "docs:pdf": "tsx scripts/anleitung-pdf.ts", "docs:screenshots": "tsx scripts/screenshots.ts", "format": "prettier --write .", + "format:pruefen": "prettier --check .", "issues:sync": "tsx scripts/gitea-issues.ts", "lint": "eslint", "postinstall": "prisma generate", diff --git a/scripts/beispiel-vorlage.ts b/scripts/beispiel-vorlage.ts new file mode 100644 index 0000000..ee5e285 --- /dev/null +++ b/scripts/beispiel-vorlage.ts @@ -0,0 +1,160 @@ +#!/usr/bin/env tsx +/** + * Eine Beispielvorlage fuer die Screenshots des Editors (#010). + * + * Der Editor laesst sich nur mit Inhalt zeigen, und die sieben echten Vorlagen + * kommen erst mit #017. Bis dahin legt dieses Skript eine Vorlage mit + * veroeffentlichter Version 1 **und** einem offenen Entwurf an — also genau die + * Lage, die die Oberflaeche unterscheiden muss. + * + * Es geht durch die echten Dienste aus `src/server/services/vorlagen.ts`, nicht + * an ihnen vorbei: waeren die Regeln anders als hier angenommen, schlaegt es + * fehl, statt eine Lage herzustellen, die es in der Anwendung nie gibt. + * + * Idempotent: ein zweiter Lauf legt nichts Neues an. + * + * pnpm beispiel:vorlage + * + * Mit #017 wird das Skript geloescht. + */ +import "dotenv/config"; +import { prisma } from "../src/lib/db.js"; +import type { AbschnittEingabe, StrukturEingabe, VariableEingabe } from "../src/lib/vorlagen-format.js"; +import { + entwurfHolenOderErzeugen, + entwurfSpeichern, + versionVeroeffentlichen, + vorlageAnlegen, +} from "../src/server/services/vorlagen.js"; + +const NAME = "PC-Neuinstallation (Beispiel)"; + +const VARIABLEN: VariableEingabe[] = [ + { + schluessel: "os_version", + beschriftung: "Betriebssystem", + typ: "AUSWAHL", + optionen: ["Windows 11 Pro 24H2", "Windows 11 Enterprise 24H2"], + pflicht: true, + }, + { + schluessel: "join_typ", + beschriftung: "Einbindung", + typ: "AUSWAHL", + optionen: ["Active Directory", "Entra ID"], + pflicht: true, + }, + { schluessel: "arbeitsspeicher", beschriftung: "Arbeitsspeicher in GB", typ: "ZAHL" }, +]; + +const ABSCHNITTE: AbschnittEingabe[] = [ + { + titel: "Vorbereitung", + schritte: [ + { titel: "Ticket und Endbenutzer im Lauf erfasst", pflicht: true }, + { + titel: "Datensicherung geprüft", + hilfetext: + "Auch nach Daten **außerhalb** des Profils fragen: Desktop, `D:\\`, lokale Outlook-Archive.\n\n" + + "- OneDrive-Status prüfen\n- lokale Archive suchen", + pflicht: true, + }, + { + titel: "Gerätedaten notiert", + typ: "TEXT", + hilfetext: "Seriennummer steht auf der Unterseite.", + konfig: { mehrzeilig: false }, + dokuUrl: "https://intern.example/geraetedaten", + }, + ], + }, + { + titel: "Installation", + schritte: [ + { + titel: "{{os_version}} installiert", + pflicht: true, + hilfetext: "Vom vorbereiteten Stick starten, keine Datenträgerverschlüsselung aktivieren.", + }, + { + titel: "Rechner umbenannt", + typ: "BEFEHL", + pflicht: true, + konfig: { + shell: "powershell", + befehl: 'Rename-Computer -NewName "{{hostname}}" -Restart', + }, + }, + { + titel: "Arbeitsspeicher geprüft", + typ: "ZAHL", + konfig: { min: 8, max: 128, einheit: "GB" }, + }, + { titel: "Übergabe am {{datum}} bestätigt", typ: "DATUM", pflicht: true }, + ], + }, +]; + +const VEROEFFENTLICHT: StrukturEingabe = { variablen: VARIABLEN, abschnitte: ABSCHNITTE }; + +/** Der Entwurf zeigt, wie eine Weiterentwicklung aussieht: ein Abschnitt mehr. */ +const ENTWURF: StrukturEingabe = { + variablen: VARIABLEN, + abschnitte: [ + ...ABSCHNITTE, + { + titel: "Abschluss", + schritte: [ + { + titel: "Konto angelegt", + typ: "BEFEHL", + pflicht: true, + konfig: { + shell: "powershell", + befehl: + 'New-ADUser -Name "{{mitarbeiter}}" -UserPrincipalName "{{upn}}" ' + + '-SamAccountName "{{sam}}" -Path "{{ou}}" ' + + '-AccountPassword (ConvertTo-SecureString "{{generiertes_passwort}}" -AsPlainText -Force) -Enabled $true', + }, + }, + { + titel: "Endgerät übergeben", + typ: "AUSWAHL", + konfig: { optionen: ["persönlich", "per Post", "über Kollegen"] }, + }, + ], + }, + ], +}; + +const admin = await prisma.benutzer.findFirst({ + where: { rolle: "ADMIN" }, + orderBy: { erstelltAm: "asc" }, + select: { id: true }, +}); +if (!admin) { + throw new Error("Es gibt keinen Administrator. Erst pnpm db:seed laufen lassen."); +} + +const vorhanden = await prisma.vorlage.findFirst({ where: { name: NAME }, select: { id: true } }); +if (vorhanden) { + console.log(`„${NAME}“ gibt es schon – nichts zu tun.`); +} else { + const { vorlage, version } = await vorlageAnlegen( + { + name: NAME, + beschreibung: "Neuaufsetzen eines Arbeitsplatzrechners, von der Sicherung bis zur Übergabe.", + kategorie: "Client", + schlagworte: ["windows", "arbeitsplatz"], + }, + admin.id, + ); + await entwurfSpeichern(version.id, VEROEFFENTLICHT); + await versionVeroeffentlichen(version.id, "Erste Fassung nach der Abstimmung im Team.", admin.id); + + const entwurf = await entwurfHolenOderErzeugen(vorlage.id, admin.id); + await entwurfSpeichern(entwurf.id, ENTWURF); + console.log(`„${NAME}“ angelegt: v1 veröffentlicht, v2 als Entwurf.`); +} + +await prisma.$disconnect(); diff --git a/scripts/screenshots.ts b/scripts/screenshots.ts index 4097a22..be65711 100644 --- a/scripts/screenshots.ts +++ b/scripts/screenshots.ts @@ -10,8 +10,13 @@ * pnpm docs:screenshots --ziel m0 nach docs/screenshots/m0/ * pnpm docs:screenshots --nur uebersicht nur diese Seite * - * Voraussetzung: die App laeuft (pnpm start oder pnpm dev). Adresse ueber - * E2E_APP_URL, Standard http://127.0.0.1:3000. + * Voraussetzung: die App laeuft als **gebauter Stand** (pnpm build && pnpm start). + * Adresse ueber E2E_APP_URL, Standard http://127.0.0.1:3000. + * + * Ausdruecklich nicht gegen `pnpm dev`: dort haengt das Entwickler-Overlay im + * Bild, und in dieser Umgebung hydriert der Entwicklungsmodus gar nicht — ein + * Bild sieht dann richtig aus, zeigt aber eine Seite, auf der kein Knopf etwas + * tut. Siehe CLAUDE.md, Abschnitt „Oberflaeche pruefen“. */ import { mkdir } from "node:fs/promises"; import { join } from "node:path"; @@ -64,6 +69,35 @@ const SEITEN: readonly Seite[] = [ }, { name: "admin-benutzer", pfad: "/admin/benutzer" }, { name: "kunden", pfad: "/kunden", nurModus: "DIENSTLEISTUNG" }, + { name: "vorlagen-liste", pfad: "/vorlagen" }, + /* + * Vorlagenseite und Editor haengen an Kennungen, die je Datenbank anders sind. + * Statt sie zu erraten, geht das Skript den Weg, den auch ein Redakteur geht: + * von der Liste aus klicken. Bricht der Weg, bricht der Screenshot — und das + * ist die richtige Antwort, denn dann ist die Oberflaeche kaputt. + */ + { + name: "vorlagen-seite", + pfad: "/vorlagen", + vorbereiten: async (tab) => { + await tab + .getByRole("link", { name: /PC-Neuinstallation/ }) + .first() + .click(); + await tab.waitForURL(/\/vorlagen\/[^/]+$/); + }, + }, + { + name: "vorlagen-editor", + pfad: "/vorlagen", + vorbereiten: async (tab) => { + await tab + .getByRole("link", { name: /Entwurf öffnen/ }) + .first() + .click(); + await tab.waitForURL(/\/vorlagen\/version\/[^/]+$/); + }, + }, ]; function argument(name: string): string | undefined { diff --git a/src/app/(app)/vorlagen/[id]/page.tsx b/src/app/(app)/vorlagen/[id]/page.tsx new file mode 100644 index 0000000..aaf57fa --- /dev/null +++ b/src/app/(app)/vorlagen/[id]/page.tsx @@ -0,0 +1,208 @@ +import Link from "next/link"; +import { notFound } from "next/navigation"; +import { ArrowLeft, Eye } from "lucide-react"; +import { VorlageKopfdaten } from "@/components/vorlage-kopfdaten"; +import { VorlageBearbeitenKnopf } from "@/components/vorlage-bearbeiten-knopf"; +import { + EntwurfVerwerfen, + VersionExportieren, + VorlageArchivieren, + VorlageDuplizieren, +} from "@/components/vorlage-aktionen"; +import { VersionStatus } from "@/generated/prisma/enums"; +import { darf } from "@/server/auth/rollen"; +import { sitzungErforderlich } from "@/server/auth/sitzungspflicht"; +import { kundenEbeneSichtbar } from "@/server/mandant"; +import { kundeHolen, kundenListe } from "@/server/queries/kunden"; +import { versionsHistorie, vorlageHolen, type VersionsZeile } from "@/server/queries/vorlagen"; + +/** + * Eine Vorlage mit ihrer Versionsgeschichte (#010). + * + * Die Seite beantwortet drei Fragen auf einen Blick: was ist gerade gueltig, wird + * gerade daran gearbeitet, und was ist wann veroeffentlicht worden. Der Aufbau + * selbst steht im Editor — eine veroeffentlichte Version zeigt er nur lesend. + */ + +const DATUM = new Intl.DateTimeFormat("de-DE", { + day: "2-digit", + month: "2-digit", + year: "numeric", + hour: "2-digit", + minute: "2-digit", +}); + +function HistorienZeile({ zeile, darfBearbeiten }: { zeile: VersionsZeile; darfBearbeiten: boolean }) { + const istEntwurf = zeile.status === VersionStatus.ENTWURF; + return ( +
  • +
    +
    + v{zeile.version} + {istEntwurf ? ( + Entwurf + ) : ( + + veröffentlicht {zeile.veroeffentlichtAm ? DATUM.format(zeile.veroeffentlichtAm) : "–"} + {zeile.veroeffentlichtVon ? ` von ${zeile.veroeffentlichtVon}` : ""} + + )} + + {zeile.anzahlLaeufe} {zeile.anzahlLaeufe === 1 ? "Lauf" : "Läufe"} + +
    + {zeile.aenderungsnotiz ? ( +

    {zeile.aenderungsnotiz}

    + ) : null} +
    + +
    + + {istEntwurf ? ( + "Im Editor öffnen" + ) : ( + <> +
    +
  • + ); +} + +/** + * Die Kunden fuer das Kopfdaten-Formular — einschliesslich des zugeordneten, + * auch wenn er archiviert ist. + * + * Sonst faellt genau dieser Eintrag aus der Liste, `defaultValue` trifft nichts, + * der Browser waehlt die erste Option („für alle Kunden“), und ein Klick auf + * „Kopfdaten speichern“ loest die Zuordnung — aus einer Vorlage fuer einen + * Kunden wird eine fuer alle. Ein archivierter Kunde ist nicht geloescht; seine + * Vorlagen bleiben ihm zugeordnet. + */ +async function kundenAuswahl( + zugeordnet: string | null, + benutzer: Parameters[0], +): Promise { + const aktive = (await kundenListe(benutzer)).map(({ id, name }) => ({ id, name })); + if (!zugeordnet || aktive.some((kunde) => kunde.id === zugeordnet)) return aktive; + + const archivierter = await kundeHolen(zugeordnet, benutzer); + /* + * Sieht der Benutzer den zugeordneten Kunden ueberhaupt nicht, laesst sich + * keine richtige Auswahl anbieten. Dann gibt es gar keine: eine leere Liste + * bedeutet „kein Feld im Formular“, und ein fehlendes Feld laesst die + * Zuordnung unangetastet (siehe actions/vorlagen.ts). Lieber nicht aenderbar + * als stillschweigend geloest. + */ + if (!archivierter) return []; + return [{ id: archivierter.id, name: `${archivierter.name} (archiviert)` }, ...aktive]; +} + +export default async function VorlagenSeite({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + const { benutzer } = await sitzungErforderlich("lesen", `/vorlagen/${id}`); + const vorlage = await vorlageHolen(id, benutzer); + // Nicht vorhanden und nicht sichtbar sind absichtlich dieselbe Antwort – die + // Unterscheidung waere schon eine Auskunft ueber fremde Kunden (#009). + if (!vorlage) notFound(); + + const darfBearbeiten = darf(benutzer.rolle, "vorlagenBearbeiten"); + const [historie, kundenEbene] = await Promise.all([ + versionsHistorie(vorlage.id, benutzer), + kundenEbeneSichtbar(), + ]); + const kunden = kundenEbene && darfBearbeiten ? await kundenAuswahl(vorlage.kundeId, benutzer) : []; + const entwurf = historie.find((zeile) => zeile.status === VersionStatus.ENTWURF); + + return ( + <> + +