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>
225 lines
12 KiB
Markdown
225 lines
12 KiB
Markdown
# Austauschformat für Vorlagen
|
|
|
|
Eine Vorlage lässt sich als JSON-Datei exportieren und wieder einspielen. Diese Datei beschreibt
|
|
das Format — für jemanden, der eine Vorlage von Hand schreiben oder ein Skript dafür bauen will.
|
|
|
|
Die Prüfung liegt in [`src/lib/vorlagen-format.ts`](../src/lib/vorlagen-format.ts), Export und
|
|
Import in [`src/server/services/vorlagen-austausch.ts`](../src/server/services/vorlagen-austausch.ts).
|
|
Beide benutzen dasselbe Schema: was hier steht, gilt für den Export, den Import und den Editor.
|
|
|
|
## Wozu das Format da ist
|
|
|
|
Eine exportierte Vorlage soll sich in eine **andere** Laufzettel-Installation einspielen lassen.
|
|
Deshalb enthält das Dokument nur das, was eine Vorlage inhaltlich ausmacht: Kopfdaten, Variablen,
|
|
Abschnitte, Schritte. Alles, was nur in der Quellinstallation eine Bedeutung hat, bleibt draußen.
|
|
|
|
**Nicht im Dokument enthalten:**
|
|
|
|
- **Kennungen** (`id`, `versionId`, `abschnittId`) — sie zeigen in der Zielinstallation ins Leere
|
|
- **Zeitstempel** (`erstelltAm`, `geaendertAm`, `veroeffentlichtAm`) und der veröffentlichende
|
|
Benutzer
|
|
- **Kundenbezug** (`kundeId`) — eine eingespielte Vorlage gilt zunächst für alle Kunden; wer sie
|
|
einem Kunden zuordnen will, tut das danach in der Oberfläche
|
|
- **`position`** — die Reihenfolge steckt in der Reihenfolge der Listen. Zwei Wahrheiten über
|
|
dieselbe Reihenfolge laufen früher oder später auseinander.
|
|
- **Läufe**, Schrittergebnisse, Kommentare und das Protokoll — das ist Betriebsgeschehen, keine
|
|
Vorlage
|
|
- der Status der Version und das Archiv-Kennzeichen
|
|
|
|
## Beispiel
|
|
|
|
Das folgende Dokument ist vollständig: zwei Abschnitte, zwei Variablen und ein `BEFEHL`-Schritt mit
|
|
Platzhaltern. **JSON kennt keine Kommentare** — die Zeilen mit `//` müssen vor dem Import raus.
|
|
|
|
```jsonc
|
|
{
|
|
// Fassung des Austauschformats, derzeit immer 1.
|
|
"format": 1,
|
|
"name": "Onboarding Notebook",
|
|
"beschreibung": "Neue Person, neues Gerät.",
|
|
"kategorie": "Onboarding",
|
|
"schlagworte": ["notebook", "eintritt"],
|
|
// Nur zur Information: welche Version der Quelle exportiert wurde. Beim
|
|
// Import wird der Wert gelesen und nicht übernommen.
|
|
"version": 3,
|
|
"struktur": {
|
|
// Wird beim Start eines Laufs abgefragt und füllt die Platzhalter.
|
|
"variablen": [
|
|
{ "schluessel": "vorname", "beschriftung": "Vorname", "typ": "TEXT", "pflicht": true },
|
|
{
|
|
"schluessel": "abteilung",
|
|
"beschriftung": "Abteilung",
|
|
"typ": "AUSWAHL",
|
|
"optionen": ["IT", "Vertrieb", "Buchhaltung"]
|
|
}
|
|
],
|
|
// Die Reihenfolge dieser Liste ist die Reihenfolge in der Ansicht.
|
|
"abschnitte": [
|
|
{
|
|
"titel": "01 · Vorbereitung",
|
|
"schritte": [
|
|
{ "titel": "Gerät auspacken", "pflicht": true },
|
|
{
|
|
"titel": "Seriennummer erfassen",
|
|
"hilfetext": "Aufkleber auf der Unterseite. **Nicht** die Artikelnummer.",
|
|
"typ": "TEXT",
|
|
"pflicht": true,
|
|
"konfig": { "mehrzeilig": false },
|
|
"dokuUrl": "https://intern.example/seriennummern"
|
|
},
|
|
{
|
|
"titel": "Arbeitsspeicher prüfen",
|
|
"typ": "ZAHL",
|
|
"konfig": { "min": 8, "max": 64, "einheit": "GB" }
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"titel": "02 · Konto",
|
|
"schritte": [
|
|
{
|
|
"titel": "Benutzerkonto anlegen",
|
|
"typ": "BEFEHL",
|
|
"pflicht": true,
|
|
// Die Platzhalter werden erst beim Abarbeiten ersetzt; hier stehen
|
|
// sie wörtlich.
|
|
"konfig": {
|
|
"shell": "powershell",
|
|
"befehl": "New-ADUser -Name \"{{vorname}}\" -Department \"{{abteilung}}\""
|
|
}
|
|
},
|
|
{ "titel": "Eintrittsdatum hinterlegen", "typ": "DATUM" }
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
## Felder
|
|
|
|
### Kopf
|
|
|
|
| Feld | Pflicht | Bedeutung |
|
|
| -------------- | -------- | ------------------------------------------------------------------------------- |
|
|
| `format` | Pflicht | Fassung des Austauschformats. Derzeit `1`; ein anderer Wert wird abgewiesen. |
|
|
| `name` | Pflicht | Name der Vorlage, nicht leer. Namen sind **nicht** eindeutig. |
|
|
| `beschreibung` | optional | Ein bis zwei Sätze, was die Vorlage abdeckt. |
|
|
| `kategorie` | optional | Freitext, gruppiert die Vorlagenliste (etwa `Onboarding`). |
|
|
| `schlagworte` | optional | Liste nicht leerer Texte; Standard `[]`. |
|
|
| `version` | optional | Versionsnummer der exportierten Quelle. Reine Information, siehe unten. |
|
|
| `struktur` | Pflicht | Objekt mit `variablen` und `abschnitte`. |
|
|
|
|
### `struktur`
|
|
|
|
| Feld | Pflicht | Bedeutung |
|
|
| ------------ | -------- | ---------------------------------------------------------- |
|
|
| `variablen` | optional | Liste von Variablen in Abfragereihenfolge; Standard `[]`. |
|
|
| `abschnitte` | optional | Liste von Abschnitten in Anzeigereihenfolge; Standard `[]`. |
|
|
|
|
### Variable
|
|
|
|
| Feld | Pflicht | Bedeutung |
|
|
| -------------- | -------- | ------------------------------------------------------------------------- |
|
|
| `schluessel` | Pflicht | Der Name im Platzhalter `{{schluessel}}`. Regeln siehe unten. |
|
|
| `beschriftung` | Pflicht | Was im Startformular über dem Feld steht, nicht leer. |
|
|
| `typ` | optional | `TEXT`, `ZAHL`, `AUSWAHL` oder `DATUM`; Standard `TEXT`. |
|
|
| `optionen` | optional | Nur bei `AUSWAHL`: die Auswahlwerte in Anzeigereihenfolge; Standard `[]`. |
|
|
| `pflicht` | optional | `true` verlangt eine Eingabe beim Start; Standard `false`. |
|
|
|
|
Ein Schlüssel darf in einem Dokument nur **einmal** vorkommen. Ein zweiter Eintrag mit demselben
|
|
Schlüssel ist ein Fehler und kein stilles Überschreiben — sonst verschwände eine Variable, ohne
|
|
dass es jemand merkt.
|
|
|
|
### Abschnitt
|
|
|
|
| Feld | Pflicht | Bedeutung |
|
|
| ---------- | -------- | ----------------------------------------------------------------------------- |
|
|
| `titel` | Pflicht | Überschrift, nicht leer. Üblich mit zweistelliger Nummer: `01 · Vorbereitung`. |
|
|
| `schritte` | optional | Liste von Schritten in Abarbeitungsreihenfolge; Standard `[]`. |
|
|
|
|
### Schritt
|
|
|
|
| Feld | Pflicht | Bedeutung |
|
|
| ----------- | -------- | -------------------------------------------------------------------------------- |
|
|
| `titel` | Pflicht | Was zu tun ist, nicht leer. Darf Platzhalter enthalten. |
|
|
| `hilfetext` | optional | Markdown, in der Abarbeitungsansicht ausklappbar. **Kein rohes HTML.** |
|
|
| `typ` | optional | Einer der Schritt-Typen unten; Standard `HAKEN`. |
|
|
| `pflicht` | optional | Pflichtschritte müssen vor dem Abschluss erledigt oder ausdrücklich als nicht zutreffend markiert sein; Standard `false`. |
|
|
| `konfig` | optional | Typabhängige Einzelheiten; Standard `{}`. Siehe unten. |
|
|
| `dokuUrl` | optional | Link auf eine Anleitung. Muss eine gültige Adresse sein. |
|
|
|
|
## Schritt-Typen und ihre `konfig`
|
|
|
|
| Typ | Was der Techniker tut | Schlüssel in `konfig` |
|
|
| --------- | ------------------------------ | -------------------------------------------------------------- |
|
|
| `HAKEN` | abhaken | keine |
|
|
| `TEXT` | Text eintragen | `mehrzeilig` (`true`/`false`) |
|
|
| `ZAHL` | Zahl eintragen | `min`, `max` (Zahlen), `einheit` (Text, höchstens 40 Zeichen) |
|
|
| `AUSWAHL` | aus einer Liste wählen | `optionen` (Liste nicht leerer Texte, in Anzeigereihenfolge) |
|
|
| `DATUM` | Datum wählen | keine |
|
|
| `BEFEHL` | Befehl kopieren und ausführen | `shell` (`powershell`, `bash` oder `cmd`), `befehl` (Text) |
|
|
|
|
`min` und `max` sind Hinweise: ein Wert außerhalb wird markiert, nicht blockiert.
|
|
|
|
`konfig` wird **nachsichtig** geprüft. Unbekannte Schlüssel bleiben erhalten — eine Datei aus einer
|
|
späteren Fassung verliert hier nichts, auch wenn diese Fassung die Schlüssel nicht kennt. Was die
|
|
Anwendung kennt, wird geprüft: `shell` etwa akzeptiert nur die drei genannten Werte.
|
|
|
|
Ein Schlüssel, der nicht zum Typ passt, ist kein Fehler; er wird schlicht nicht beachtet. Wer den
|
|
Typ eines Schritts ändert, räumt die `konfig` also besser selbst auf.
|
|
|
|
## Platzhalter und Variablenschlüssel
|
|
|
|
Ein Variablenschlüssel muss der Form `^[a-z][a-z0-9_]{0,39}$` entsprechen: Beginn mit einem
|
|
Kleinbuchstaben, danach Kleinbuchstaben, Ziffern und Unterstrich, höchstens 40 Zeichen.
|
|
`vorname`, `ad_gruppe` und `raum2` sind in Ordnung; `Vorname`, `mit-strich`, `2te_wahl` und ein
|
|
leerer Schlüssel nicht.
|
|
|
|
Die Strenge hat einen Grund: der Schlüssel steht als `{{schluessel}}` in Titeln, Hilfetexten und
|
|
Befehlen. Ein Platzhalter wird genau mit diesem Muster wiedererkannt. Wäre `Vor-Name` erlaubt,
|
|
müsste der Suchausdruck Groß- und Kleinschreibung, Striche und Leerzeichen auseinanderhalten — und
|
|
`{{Vor-Name}}` in einem PowerShell-Befehl wäre nicht mehr von dessen eigener Syntax zu trennen.
|
|
Deshalb: ein Zeichensatz, eine Regel, keine Ausnahmen.
|
|
|
|
Geschweifte Klammern dürfen Leerzeichen enthalten (`{{ vorname }}` wird erkannt). Neben den
|
|
Variablen der Vorlage gibt es eingebaute Platzhalter — `{{generiertes_passwort}}` etwa —, die nicht
|
|
im Austauschdokument stehen und auch nirgends gespeichert werden.
|
|
|
|
## Was beim Import passiert
|
|
|
|
1. Das Dokument wird gegen das Schema geprüft. Schlägt das fehl, wird **nichts** geschrieben.
|
|
2. Es entsteht eine **neue Vorlage** — der Import schreibt nie in eine bestehende hinein.
|
|
3. Die Vorlage bekommt genau eine Version: `version = 1`, Status **`ENTWURF`**. Eine eingespielte
|
|
Vorlage wird bewusst nicht sofort veröffentlicht; der Redakteur sieht sie sich erst an,
|
|
ergänzt vielleicht etwas und veröffentlicht dann. Erst danach lässt sich ein Lauf daran starten.
|
|
4. `position` wird aus der Reihenfolge der Listen abgeleitet (0, 1, 2 …).
|
|
5. Das mitgeschickte `version`-Feld wird nicht übernommen. Eine Vorlage, die in der Quelle auf
|
|
Version 7 stand, fängt hier wieder bei 1 an — die Versionsgeschichte der Quelle wandert nicht mit.
|
|
6. Der Vorlagenname kommt so an, wie er im Dokument steht. Gibt es ihn schon, ist das kein Fehler
|
|
und es wird nichts angehängt: Vorlagennamen sind nicht eindeutig.
|
|
7. Alles geschieht in einer Transaktion. Ein Fehler auf halbem Weg hinterlässt keine halbe Vorlage.
|
|
8. Ein Protokolleintrag mit der Aktion `Vorlage importiert` hält fest, wer die Vorlage eingespielt hat.
|
|
|
|
Wird das Dokument abgewiesen, nennt die Meldung die **erste** Beanstandung samt Pfad, etwa:
|
|
|
|
```
|
|
struktur.abschnitte.1.schritte.2.titel: Ein Schritt braucht einen Titel.
|
|
```
|
|
|
|
## Wie sich das Format ändern darf
|
|
|
|
Das Feld `format` ist die Fassung des Formats, nicht die der Anwendung. Es gilt:
|
|
|
|
- **Neue optionale Felder** und neue `konfig`-Schlüssel erhöhen `format` **nicht**. Alte Dateien
|
|
bleiben gültig, und weil `konfig` nachsichtig geprüft wird, überleben neue Schlüssel auch einen
|
|
Rundlauf durch eine ältere Fassung.
|
|
- **Neue Schritt- und Variablentypen** erhöhen `format` ebenfalls nicht. Eine Fassung, die den Typ
|
|
nicht kennt, weist die Datei allerdings ab, weil der Typ eine geprüfte Aufzählung ist.
|
|
- Erst eine Änderung, die alte Dateien **unbrauchbar** macht — ein umbenanntes Pflichtfeld, eine
|
|
andere Verschachtelung —, erhöht `format` auf `2`.
|
|
- Ein unbekannter `format`-Wert wird mit einer klaren Meldung abgewiesen, nie geraten. Ein Import,
|
|
der die Hälfte versteht und die andere Hälfte still verwirft, wäre schlimmer als ein Fehlschlag.
|
|
|
|
Die Zahl steht als `FORMAT_VERSION` in `src/lib/vorlagen-format.ts` an genau einer Stelle.
|