Files
Florian WildeandClaude Opus 5 d30bb6d235
Prüfung / pruefung (push) Waiting to run
Prüfung / e2e (push) Blocked by required conditions
feat(laeufe): Vier-Augen-Freigabe (#040)
Verlangt eine Vorlagen-Version eine Freigabe, laesst sich ein Lauf erst
abschliessen, wenn ein Zweiter ihn freigegeben hat. Das braucht zweierlei, und
beides zusammen: das neue Recht `freigeben` (ADMIN und REDAKTEUR, nicht
TECHNIKER) und dass man nicht an dem Lauf gearbeitet hat.

Als gearbeitet zaehlt: angelegt, einen Schritt gesetzt oder abgeschlossen. Eine
Zuweisung allein nicht — waere die Regel hier strenger, blockierte sie kleine
Teams ohne Gewinn.

Auch ein Administrator kann sich nicht selbst freigeben, und das ist der ganze
Sinn. Die Pruefung sitzt in `hatDaranGearbeitet` an einer Stelle, wie
`laufMussVeraenderbarSein`; die wichtigste Haelfte — der Ersteller — haelt
zusaetzlich ein CHECK in der Datenbank.

Zurueckweisen verlangt eine Begruendung, setzt den Lauf auf IN_ARBEIT und nimmt
eine erteilte Freigabe zurueck. Kein fuenfter LaufStatus: ein neuer Enum-Wert
haette jeden Codepfad betroffen, der heute vier Faelle kennt.

Sobald jemand abzuschliessen versucht und die Freigabe fehlt, bekommen die
Freigabeberechtigten eine Mail — ein anderer Empfaengerkreis als bei jeder
anderen Meldung, und ohne die, die gearbeitet haben. Die Aufforderung entsteht
ausserhalb der Transaktion: innerhalb wuerde sie mit dem Fehler zurueckgerollt,
und dann wartet ein Lauf auf etwas, von dem niemand weiss. Ein partieller
Unique-Index haelt sie bei mehreren Versuchen auf einmal.

Die Pflicht haengt an der Version, nicht an der Vorlage: eine veroeffentlichte
Version ist unveraenderlich, ein laufender Lauf behaelt also seine Regel. Beim
Bearbeiten und Duplizieren wandert sie mit — das war die unmittelbare Lehre aus
#039, wo die tiefe Kopie zwei Felder stillschweigend verworfen hatte.

1526 Vitest-Tests (vorher 1496), 115 E2E-Ablaeufe gruen. Klickprobe am gebauten
Stand mit elf Pruefungen; drei rote Punkte darin waren meine eigenen
Testfehler, einer davon lehrreich: ein reload() direkt nach einer Server Action
trifft den Router-Cache von Next und zeigt kurz den alten Stand.

Closes #40

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-24 01:14:04 +02:00

620 lines
48 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Changelog
Alle nennenswerten Änderungen an Laufzettel werden in dieser Datei dokumentiert.
Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.1.0/),
und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/).
## [Unveröffentlicht]
### Hinzugefügt
- **Vier-Augen-Freigabe.** Verlangt eine Vorlagen-Version sie, lässt sich ein Lauf erst abschließen,
wenn ein **Zweiter** ihn freigegeben hat — jemand mit dem neuen Recht `freigeben` (`ADMIN` und
`REDAKTEUR`, **nicht** `TECHNIKER`), der nicht an dem Lauf gearbeitet hat. Als gearbeitet zählt:
angelegt, einen Schritt gesetzt oder abgeschlossen; eine Zuweisung allein nicht.
**Auch ein Administrator kann sich nicht selbst freigeben** — das ist der ganze Sinn, und dieselbe
Regel hält ein CHECK in der Datenbank. Zurückweisen verlangt eine Begründung, setzt den Lauf
zurück auf *in Arbeit* und nimmt eine erteilte Freigabe zurück.
Sobald jemand abzuschließen versucht und die Freigabe fehlt, bekommen die Freigabeberechtigten
eine Mail — einmal, auch bei mehreren Versuchen. Das Häkchen steht im Editor beim Veröffentlichen
und wandert beim Bearbeiten in den neuen Entwurf mit (#040)
- **Bedingte Abschnitte und Schritte.** Ein Abschnitt oder ein einzelner Schritt erscheint nur,
wenn seine Bedingung zutrifft — abhängig von einer **Variable** des Laufs oder vom **Zustand
eines anderen Schritts**. Der echte Fall steht im Seed: „Mitarbeiter-Onboarding“ trägt beide
Konto-Abschnitte, fünf AD-Schritte und vier für Entra ID, und `Kontotyp` sagt beim Start, welcher
gilt. Bisher klickte ein Techniker die anderen neun bei jedem Onboarding weg.
**Lässt sich eine Bedingung nicht entscheiden, erscheint der Teil** — hat niemand den Kontotyp
angegeben, stehen beide Abschnitte da, genau wie vorher. Ein ausgeblendeter Pflichtschritt hält
den Abschluss nicht auf, ausgeblendet heißt nicht gelöscht (eine Notiz bleibt stehen), und die
Nummerierung zählt nur, was man sieht. Ausgewertet wird **laufend**: ein Abschnitt „Nacharbeit“,
der erst erscheint, wenn der Restore-Test fehlschlägt, kommt bei einer offenen zweiten Sitzung
sofort dazu. Es gibt bewusst **kein** „und“ und kein „oder“ (#039)
### Behoben
- **Der Import einer Vorlage verwarf die Abteilungseinschränkung der Abschnitte.** Der Export
schreibt sie seit #031, das Schema nimmt sie an — der Import setzte sie nicht. Eine Vorlage, die
jemand in eine zweite Installation brachte, verlor ihre Abteilungsabschnitte: sie galten dort für
**alle**, also mehr Schritte statt weniger. Niemand hätte es gemerkt (#039)
- **„Bearbeiten“ einer Vorlage verlor ebenfalls alle Abteilungseinschränkungen.** Die tiefe Kopie,
aus der der Entwurf entsteht, nahm sie nicht mit — und `Vorlage duplizieren` genauso wenig, weil
beide die Kopie einzeln ausgeschrieben hatten. Ursache war ein von Hand getippter Zwischentyp, der
weniger Felder nannte als die Abfrage liefert; er ist jetzt aus Prisma abgeleitet, beide Wege
teilen eine Stelle, und ein Test vergleicht Kopie und Original Feld für Feld (#039)
- Die Auswahlfelder der neuen Bedingung trugen ihren Namen über ein umhüllendes `label`, wodurch
der zugängliche Name den Text der gewählten Option mitschluckte — für einen Screenreader eine
Bezeichnung, die sich beim Umschalten ändert (#039)
## [0.2.0] – 2026-10-11
### Geändert
- **Wer sich anmeldet und in keiner Rollengruppe steht, bekommt nicht mehr automatisch `LESER`.**
Bisher hieß das: Lesezugriff auf **alle** Läufe des Hauses — auch für einen ausgeschiedenen
Techniker, solange niemand sein Konto deaktivierte. Jetzt versucht Laufzettel, ihn über seinen UPN
einer Person im Verzeichnis zuzuordnen: klappt das, bekommt er die Selbstauskunft; klappt es
nicht, wird die Anmeldung mit einer Begründung abgewiesen.
**Diese Änderung greift nur, wenn Rollengruppen konfiguriert sind** (`OIDC_GROUP_*`) — ohne sie
bleibt alles wie vorher, denn ohne abgebildete Gruppen kann die Anwendung nicht unterscheiden, ob
jemand Techniker oder Mitarbeiter ist. Wer das alte Verhalten für bestimmte Leute braucht, trägt
sie in die Leser-Gruppe ein. Der lokale Notfallzugang hängt nicht an den Gruppen (#035)
- **Die Abarbeitungsansicht aktualisiert sich jetzt sofort statt alle zehn Sekunden.** Hakt ein
Kollege einen Schritt ab, schreibt einen Kommentar, hängt ein Foto an oder ändert den Titel,
erscheint das beim anderen in weniger als einer halben Sekunde — gemessen 485 ms, vorher bis zu
zehn Sekunden. Dahinter stehen vier Datenbank-Trigger mit `NOTIFY` und ein Ereignisstrom; das
funktioniert auch mit mehreren Instanzen der Anwendung, ohne einen zweiten Dienst. Zwanzig offene
Laschen kosten jetzt nichts mehr, statt zwanzig Abfragen je zehn Sekunden.
**Das Polling bleibt als Rückfall** an derselben Adresse, mit derselben Rechteprüfung: wer kein
`EventSource` hat oder hinter einem Proxy sitzt, der Ströme puffert, bekommt den alten Weg. Neu in
der Umgebung: `SSE_HERZSCHLAG_MS` (25000) und `SSE_MAX_STROEME` (200) (#033)
- Der Zustand eines Laufs trägt jetzt auch die Zahl der **Anhänge**. Ohne sie wäre ein angehängtes
Foto für die zweite Sitzung unsichtbar geblieben — die Anhänge kamen in #032 dazu, der Zustand
kannte sie nicht (#033)
- **`docs/betrieb/sicherung.md` ist neu geschrieben.** Dort stand „Kein Upload-Verzeichnis, keine
erzeugten Dateien“ — mit den Anhängen ist das falsch, und eine Sicherung nach der alten Anleitung
hätte ab #032 still Daten verloren: in der Datenbank stehen nur die Metadaten. Jetzt sind es drei
Dinge statt zwei, mit `tar` für beide Betriebswege, dem `chown`-Fallstrick nach einer
Rücksicherung als `root`, einem Sicherungsskript, das Datenbank und Anhänge im **selben** Lauf
mitnimmt, und einer Gegenprobe über die Zahlen. Sicherung und Rückspielung sind einmal wirklich
gefahren (#032)
- Der Seed macht Petra Klein zur Vorgesetzten von Maria Huber — die Zuordnung aus dem Auftrag, die
sich vorher nirgends speichern ließ (#020)
- Der Healthcheck `/api/gesundheit` nennt im Fehlerfall keinen Grund mehr. Der Pfad ist ohne
Anmeldung erreichbar, und eine Meldung von Prisma oder `pg` nennt Host, Port, Benutzernamen und
Datenbanknamen. Nach außen steht jetzt nur noch `{"status":"fehler"}` mit Status 503; die
Begründung geht ins Protokoll des Prozesses (#025)
- Die Konvention „nie auf rohe Hexwerte“ nennt ihre sechs begründeten Ausnahmen beim Namen —
Druckansichten, QR-Code und `themeColor` entscheiden nicht nach unserem Farbschema. Die Regel war
strenger formuliert als gemeint (#026)
- `CLAUDE.md` und `README.md` nennen den LDAP-Testcontainer als Teil des Tors vor dem Commit. Ohne
ihn überspringt die Suite **22 Tests** — den ganzen Verzeichnis-Abgleich —, sagt das auf stderr,
und der Lauf sieht trotzdem grün aus (#026)
- Im README fehlte `format:pruefen` in den Qualitätsschranken, obwohl `CLAUDE.md` es seit #010
verlangt (#026)
### Behoben
- **Die Zusage „jede Seite und jeder Route Handler prüft sein Recht selbst“ wird jetzt geprüft.**
Sie steht seit #007 in den Konventionen und hielt bis hierher nur durch Nachsehen von Hand — das
gilt für den Tag, an dem es lief. `tests/zugang.test.ts` liest jetzt jede `page.tsx` und jeden
`route.ts`; die Ausnahmen (Auth, Healthcheck) stehen dort namentlich samt Begründung (#038)
- Das Lesemodell der Anhänge liegt jetzt in `queries/` und wendet seine Mandantensicht **selbst** an.
Vorher hing es daran, dass beide Aufrufer den Lauf vorher prüften — kein Schaden, aber die falsche
Form: der dritte Aufrufer ist der, der es vergisst (#037)
- `/meine-geraete` wird jetzt bei 390 px gemessen, mit einem echten `MITARBEITER`-Konto. Die Seite
ist mit „am Handy kein horizontales Scrollen“ abgenommen worden — von Hand. Dazu ein Test, dass
dort kein Link in die Verwaltung angeboten wird (#037)
- **Der Link in der Mail „Verzeichnis-Abgleich fehlgeschlagen“ zeigte ins Leere.** Gebaut wurde
`/verwaltung/verzeichnis`; `verwaltung` ist aber der Name des Rechts, die Seite liegt unter
`/admin/verzeichnis`. Das ist die eine Meldung, die an alle Administratoren geht, und zwar im
Moment eines Ausfalls — der Empfänger klickte und landete auf der Nicht-gefunden-Seite. Ein Test
prüft jetzt jeden Pfad einer Meldung gegen die vorhandenen Routen (#036)
- **Ein offener Ereignisstrom überlebte Abmelden, Deaktivieren und Sitzungswiderruf.** Das Recht
wurde genau einmal geprüft, beim Aufbau des Stroms. Ein Administrator deaktivierte ein Konto, und
eine offen gelassene Lasche bekam weiter live Status und Schrittzustände dieses einen Laufs, bis
jemand sie schloss. Der Herzschlag prüft die Sitzung jetzt alle 25 Sekunden nach; eine
Herabstufung während des Stroms engt ihn ein, statt ihn zu beenden (#036)
- **Der Export eines Entwurfs mit Baustein lieferte einen leeren Abschnitt.** Ein Baustein-Abschnitt
bekommt seine Schritte erst beim Veröffentlichen; wer eine noch unveröffentlichte Vorlage in eine
zweite Installation brachte, fand den geteilten Abschnitt dort unter dem Namen „wird ersetzt“ und
ohne einen einzigen Schritt. Beim Export werden Bausteine jetzt aufgelöst (#036)
- **`docs/betrieb/sicherung.md` nannte für den nativen Weg einen Anhänge-Pfad, den es nirgends
gibt** — als Rückfallwert in einem Skript, in dem ein `rm -rf` steht. Auf einer Installation ohne
`ANHAENGE_PFAD` in der Env-Datei wäre in ein Verzeichnis zurückgespielt worden, das die Anwendung
nie liest: „Rücksicherung gelaufen“, und danach fehlt jede Datei. Jetzt bricht der Befehl ab,
statt zu raten (#036)
- **`README.md` kannte die Rolle `MITARBEITER` nicht** und behauptete weiter, unbekannte Benutzer
würden beim ersten Login angelegt. Seit #035 wird jemand ohne Rollengruppe über seinen UPN
zugeordnet oder abgewiesen. Wer auf v0.2.0 hebt und nur das README liest, hätte genau diese Leute
ausgesperrt (#036)
- **Der Protokolleintrag „Anmeldung abgewiesen: UPN mehrdeutig“ nannte immer zwei Treffer**, auch bei
sieben. Wer das Protokoll liest, um den Fall aufzulösen, braucht die wirkliche Zahl (#036)
- **`anzahlSchritte` im Protokoll eines neuen Laufs zählte die Schritte der Version**, nicht die des
Laufs — bei einer Vorlage mit abteilungsgebundenen Abschnitten also zu viele (#036)
- Die Attributzuordnung einer Verzeichnisquelle wird jetzt **im Dienst** geprüft und nicht nur im
Formular. Ein Attributname mit Komma und `$filter` kam zuvor durch und schrieb einen zweiten
`$filter` in die Graph-Abfrage (#036)
- Der Graph-Seitenabruf folgt `@odata.nextLink` nur noch, wenn die Adresse auf denselben Host zeigt
wie `GRAPH_API_URL`. Jede Seite wird mit einem mandantenweit gültigen Token geholt (#036)
- **Die Ende-zu-Ende-Abläufe räumen auf, was sie anlegen** — zentral, vor und nach dem Lauf. In der
Entwicklungsdatenbank standen 105 Vorlagen aus früheren Durchgängen. Ein struktureller Test
schlägt an, wenn ein neuer Ablauf das Präfix nicht benutzt (#027)
- Vier Ende-zu-Ende-Abläufe waren seit #020, #030, #031 beziehungsweise #033 rot, ohne dass es
auffiel: `pnpm test:e2e` steht in keinem Tor. Alle vier behoben, die Lücke ist als #038 benannt
(#027)
- Der Verbindungstest einer Verzeichnisquelle unterschied „der Filter findet niemanden“ nicht mehr
von „gelesen, aber nichts zuordenbar“ — zwei verschiedene Ursachen, die den Administrator an
verschiedene Stellen schicken. Die Unterscheidung war in #028 eingebaut worden, aber durch eine
doppelte Bedingung seit dem ersten Tag unerreichbar (#029)
- **`staerksteRolle` konnte mehr Rechte liefern als übergeben.** `LESER` war Startwert und damit
zugleich Untergrenze: eine Rolle unterhalb davon kam nie heraus. Solange alle Rollen mindestens
`LESER` waren, fiel das nicht auf — mit der neuen Rolle `MITARBEITER` ergab
`staerksteRolle(["MITARBEITER"])` plötzlich `LESER`. Gefunden von einem Test, der seit #007
unverändert dasteht (#035)
- **nginx hätte ein 10-MB-Foto abgewiesen.** `docs/betrieb/reverse-proxy.md` empfahl
`client_max_body_size 8m`, während `ANHANG_MAX_MB` 10 MB erlaubt: ein Handyfoto am oberen Rand
wäre mit „413 Request Entity Too Large“ an der Wand gelandet, und die freundliche Meldung der
Anwendung wäre nie zum Zuge gekommen. Jetzt 12m, mit dem Hinweis, dass beide Werte zusammengehören
(#033)
### Hinzugefügt
- **SCIM 2.0: Personen können jetzt auch geschickt werden, statt geholt zu werden.** Bisher
verbindet sich Laufzettel nach Zeitplan mit einem Verzeichnis und liest. Das setzt zwei Dinge
voraus, die nicht überall gegeben sind: Lesezugriff auf das Verzeichnis, und ein Netzweg dorthin.
Mit der neuen Quellenart `SCIM` ruft stattdessen der Identitätsanbieter Laufzettel an — authentik,
Entra ID, Okta — und zwar in dem Moment, in dem sich bei ihm etwas ändert, nicht erst beim
nächsten Intervall.
Umgesetzt ist, was die Anbieter wirklich schicken: `Users` mit dem Filter `userName eq "…"`,
`POST`, `GET`, `PUT`, `PATCH`, `DELETE`, dazu `ServiceProviderConfig`, `Schemas` und
`ResourceTypes` — ohne die drei weigern sich manche Anbieter, überhaupt anzufangen. Übernommen
werden Anmeldename, Name, Mailadresse, Titel, Abteilung, der Vorgesetzte und ob das Konto aktiv
ist; keine Gruppen, keine Rollen, keine Passwörter.
**`DELETE` deaktiviert und löscht nicht.** Der Anbieter bekommt `204` und hält die Person damit
für entfernt — aus seiner Sicht richtig. In Laufzettel bleibt sie inaktiv stehen, weil Läufe und
Geräte an ihr hängen. Das steht in der Anleitung, weil es sonst wie ein Fehler wirkt.
**Eine SCIM-Quelle wird nicht abgeglichen**, und das ist keine Kosmetik: ein Abgleich, der dort
nichts liest und „Erfolg“ meldet, hätte über die Abschaltregel die ganze Belegschaft deaktiviert.
Verbindungstest und „Jetzt abgleichen“ gibt es für sie deshalb nicht; an ihrer Stelle steht die
letzte Zustellung.
Das **Token** wird unter Admin → Verzeichnis erzeugt, **einmal** angezeigt und nur als
argon2-Hash gespeichert — es gibt keinen zweiten Weg, es zu lesen. Es ist der einzige
Mandantenbeweis, den eine Zustellung mitbringt: ein Token sieht nur die Personen seiner eigenen
Quelle. Einrichtung in authentik und Entra ID steht in
[Kapitel 06](docs/anleitung/06-verzeichnis.md#scim-der-anbieter-schickt), der Betrieb in
[`docs/betrieb/scim.md`](docs/betrieb/scim.md) (#029)
- **Entra ID als Verzeichnisquelle, über Microsoft Graph.** Ein Haus ohne lokales Active Directory
hat kein LDAP, auf das sich zeigen ließe. Eine Verzeichnisquelle hat jetzt eine **Art** — `LDAP`
oder `GRAPH` —, und beide laufen durch **denselben** Abgleich: anlegen, aktualisieren,
deaktivieren, **nie löschen**. Nur das Holen der Einträge unterscheidet sich, sonst drifteten die
Regeln auseinander.
Gelesen wird mit dem Client-Credentials-Flow über `GET /v1.0/users`, seitenweise über
`@odata.nextLink`, der Vorgesetzte über `$expand=manager($select=id)`. Das Client-Secret wird
verschlüsselt gespeichert wie das LDAP-Bind-Passwort und erscheint in keiner Antwort und in keinem
Protokolleintrag.
**Zwei Platzhalter bleiben bei Graph bewusst leer:** `{{ou}}` (Entra ID hat keine OU — steht an der
Abteilung eine Ziel-OU, greift die) und `{{sam}}` ohne AD-Synchronisierung. Was in Entra ID
einzurichten ist, steht in `docs/betrieb/entra-id.md` — mit einer Prüfliste, denn gegen einen
echten Mandanten ist dieser Weg **nicht** gelaufen (#028)
- **Mitarbeiter-Selbstauskunft.** „Was ist eigentlich mit meinem Notebook passiert?“ musste bisher
ein Techniker nachsehen. Ein Mitarbeiter kann die Arbeiten an **seinen** Geräten jetzt selbst
ansehen, unter **Meine Geräte** — mit der neuen Rolle `MITARBEITER`, zugeordnet über den **UPN**
beim Anmelden. Ein mehrdeutiger UPN wird abgewiesen und protokolliert: ein Zufallstreffer
entschiede nach Sortierung darüber, wessen Geräte jemand sieht.
**Was er sieht, sind Titel, Zeitraum und Status — und sonst nichts.** Keine Schritte, keine
Notizen, keine Kommentare, keine Anhänge: in einem Lauf stehen Arbeitsnotizen über Menschen und
Befehle mit Platzhaltern für Zugangsdaten. Eine Selbstauskunft ist keine Akteneinsicht, und das
hält nicht die Seite fest, sondern das Lesemodell.
Kapitel 09 der Anleitung richtet sich erstmals **an Mitarbeiter** und nicht an die IT (#035)
- **Benachrichtigungen per Mail und Webhook.** Ein zugewiesener Lauf fiel bisher nur auf, wenn jemand
die Übersicht öffnete; ein überfälliger überhaupt nicht. Vier Anlässe gehen jetzt hinaus —
**zugewiesen**, **überfällig**, **abgeschlossen** und **Verzeichnisabgleich fehlgeschlagen** —, je
Person abschaltbar, und **niemand bekommt Post über die eigene Tat**. Mail über SMTP (Zugangsdaten
verschlüsselt wie das LDAP-Bind-Passwort, mit „Testmail schicken“), dazu optional ein Webhook mit
HMAC-SHA256-Signatur im Kopf `X-Laufzettel-Signatur`.
Zugestellt wird aus einer **Warteschlange in der Datenbank**, nicht im Anfrage-Pfad: ein hängender
Mailserver darf kein Abhaken blockieren. Fehlschläge werden nach 1, 5, 30 und 120 Minuten
wiederholt und bleiben danach als **aufgegeben** sichtbar — unter Verwaltung → Zustellungen, denn
eine stille Warteschlange ist schlimmer als keine.
**In keiner Nachricht steht ein Schrittinhalt**, und damit auch kein erzeugtes Passwort: geprüft
über alle Rumpftexte, die wirklich verschickt wurden (#034)
- **Fotos und Dateien an einem Schritt.** „Seriennummer fotografiert“, „Typenschild“, „Zustand bei
Übergabe“ — manches lässt sich nicht abhaken, es muss gezeigt werden. Ein Schritt nimmt jetzt
Bilder (JPEG, PNG, WebP, HEIC) und PDF auf, am Handy direkt aus der Kamera. Die Dateien liegen im
Dateisystem unter `ANHAENGE_PFAD`, nach Kunde und Lauf gegliedert; in der Datenbank stehen nur
Name, Typ, Größe und Prüfsumme. Jeder Abruf prüft Recht und Mandantengrenze — auch der des Bildes
selbst. Grenzen über `ANHANG_MAX_MB` (10) und `ANHAENGE_JE_LAUF` (20), und eine abgewiesene Datei
nennt den Grund. Kein SVG: das ist ein Dokument, das Skripte ausführt. Der Archiv-Schreibschutz
gilt auch hier, und die Druckansicht zeigt Bilder als Miniatur, ein PDF als Zeile (#032)
- **Abschnitte nur für bestimmte Abteilungen.** Beim Onboarding in der Buchhaltung gehören Schritte
dazu, die sonst niemand braucht. Bisher hieß die Wahl: eine Vorlage je Abteilung pflegen, oder
Schritte, die für die meisten „nicht zutreffend“ sind. Jeder Abschnitt kann jetzt sagen, für welche
Abteilungen er gilt; gespeichert werden **Namen**, damit ein Umbenennen die Vorlage nicht
beschädigt. Welche Abschnitte ein Lauf hat, wird **beim Start festgeschrieben** — ein späterer
Abteilungswechsel ändert ihn nicht, und ein ausgefilterter Pflichtschritt blockiert den Abschluss
nicht. Der Start-Dialog sagt vorher, was die gewählte Abteilung hinzunimmt (#031)
- **Wiederverwendbare Bausteine.** Ein Abschnitt, den mehrere Vorlagen teilen und der an einer
Stelle gepflegt wird — „Datensicherung prüfen“ steht in vier der sieben mitgelieferten Vorlagen,
wortgleich. Versioniert nach denselben Regeln wie eine Vorlage: höchstens ein Entwurf, eine
veröffentlichte Fassung wird nie verändert. Eingesetzt wird ein Baustein im Editor; beim
**Veröffentlichen** einer Vorlage setzt der Dienst seine Schritte als tiefe Kopie ein. Damit
ändert eine neue Baustein-Fassung **keine** veröffentlichte Vorlagenversion und **keinen**
laufenden Lauf — sie wirkt erst in der nächsten veröffentlichten Version. Auf der Seite eines
Bausteins steht, in welchen Vorlagen er vorkommt und welche Fassung dort eingesetzt ist (#030)
- **Das Protokoll ist ansehbar**, unter Verwaltung → Protokoll. Geschrieben wurde es seit dem
Datenmodell, ansehen konnte es niemand: wer wissen wollte, wer wann eine Rolle geändert hat,
brauchte `psql`. Jetzt mit Volltext über Aktion und Lauftitel, Filter nach Urheber und Zeitraum,
fünfzig Einträge je Seite. Ein Eintrag ohne Urheber wird als **Systemlauf** gezeigt und nicht als
Lücke — der Zeitplan des Verzeichnisabgleichs hat keinen Menschen hinter sich. Am Lauf selbst
stehen seine Einträge eingeklappt in der Abarbeitungsansicht; wer den Lauf sehen darf, sieht auch
dessen Geschichte (#023)
- **`Protokoll` trägt eine `kundeId`**, wie CLAUDE.md es für jede kundenbezogene Zeile verlangt. Ohne
sie ließ sich die Mandantensicht nicht anwenden, ohne je Eintrag die Entität nachzuschlagen. Leer
heißt global; bestehende Einträge nimmt die Migration mit. Ein Eintrag, der sich **keinem** Kunden
zuordnen lässt — etwa zu einem gelöschten Gerät —, wird einem auf Kunden beschränkten Benutzer
nicht gezeigt (#023)
- **Schnellstart je Gerätetyp pflegen**, unter Verwaltung → Schnellstart. Welche Vorlagen auf der
Geräteseite als Kachel erscheinen, stand bisher nur im Seed: wer eine eigene Vorlage dort haben
wollte, kam nicht heran, und wer eine mitgelieferte loswerden wollte, auch nicht. Zur Auswahl
stehen nur veröffentlichte, nicht archivierte Vorlagen; die Reihenfolge der Liste ist die
Reihenfolge der Kacheln. Sobald jemand hier gespeichert hat, legt der Seed keine eigenen Vorgaben
mehr nach — eine entfernte Kachel soll nicht beim nächsten Einrichtungslauf zurückkommen (#022)
- **Vorgesetzter auch für von Hand angelegte Personen.** Die Zuordnung lief allein über die Kennung
des Verzeichnisses, und die ist nur innerhalb einer Verzeichnisquelle eindeutig: wer eine Person
selbst eingetragen hatte, konnte ihr keinen Vorgesetzten geben, und `{{vorgesetzter_sam}}` blieb im
Onboarding-Befehl leer — genau dort, wo er gebraucht wird. Jetzt trägt jede Person eine gepflegte
Zuordnung, die sich auf ihrer Seite setzen und entfernen lässt; der Abgleich schreibt sie aus dem
Verzeichnis mit, und liefert das Verzeichnis keinen Vorgesetzten, bleibt eine von Hand gesetzte
Zuordnung stehen. Bestehende Zuordnungen nimmt die Migration mit (#020)
- Mitgelieferte Vorlagen tragen einen **stabilen Schlüssel** (`seedSchluessel`). Der Seed erkennt
seine sieben Vorlagen daran statt am Namen — wer eine umbenannte, bekam vorher beim nächsten Lauf
eine zweite, samt doppeltem Schnellstart auf der Geräteseite. Eine bestehende Installation bekommt
den Schlüssel beim ersten Lauf danach nachgetragen, ohne Dubletten. Eine über den Import
eingespielte Kopie bekommt **keinen** Schlüssel: sie soll nicht den Platz des Originals einnehmen
(#021)
- `pnpm pruefe:doku` vergleicht die Schranken-Befehle in `CLAUDE.md` und `README.md` und verlangt von
beiden den LDAP-Container. Genau diese Abweichung war unbemerkt entstanden (#026)
- Fünf Tests für den Healthcheck, darunter einer, der belegt, dass die Begründung ins Protokoll
geschrieben und nicht bloß verschluckt wird (#025)
## [0.1.0] – 2026-09-26
### Hinzugefügt
- Projektgerüst: Next 16 mit App Router und TypeScript strict, Tailwind 4, Vitest 5,
Playwright 1.63, ESLint flat config, Prettier, husky mit lint-staged und commitlint (#005)
- Gestaltung: Design-Token als CSS-Variablen, selbst gehostete Schriften, Wortmarke und
Bildmarke `[ ✓ ]`, Versionsanzeige unten links (#005)
- Betriebsstand mit Docker Compose: die Datenbank ist von außen nicht erreichbar — kein
veröffentlichter Port und ein Netz ohne Gateway; Migrationen laufen beim Start in einem
eigenen Einmal-Dienst, der Seed bleibt ein bewusster Handgriff (#005)
- Healthcheck `GET /api/gesundheit` und Sicherheitskopfzeilen samt CSP-Nonce je Anfrage (#005)
- shadcn/ui eingerichtet: sein Variablen-Vertrag wird aus den Laufzettel-Token gespeist, damit
kopierte Bauteile ohne Nacharbeit passen (#005)
- `pnpm pruefe:doku` hält Dokumentation und Code zusammen: Version im Changelog, Issue-Dateien
gegen ihre Übersicht, keine Verweise auf fehlende Dateien, keine fehlenden Bilder (#005)
- `pnpm docs:screenshots` erzeugt Screenshots reproduzierbar und prüft dabei gleich, dass am
Handy nicht horizontal gescrollt wird (#005)
- Issues, Labels und Meilensteine für alle vier Phasen im Gitea-Tracker (#001–#019)
- Vollständiges Datenmodell in deutscher Benennung: 20 Tabellen, 9 Aufzählungen, erste
Migration, idempotenter Seed für Betriebsmodus, eigene Organisation und Administrator (#006)
- Erzeugung der `tagCode`s für die QR-Aufkleber, mit Vergabebuch, aus dem nie gelöscht wird —
ein Code kommt auch nach dem Löschen eines Geräts nicht zurück (#006)
- Verschlüsselung von Geheimnissen mit AES-256-GCM aus `APP_SECRET`, für das LDAP-Bind-Passwort
und später die Zugangsdaten der Connectoren (#006)
- Anmeldung über einen generischen OIDC-Anbieter mit Discovery, PKCE, `state` und `nonce`;
unbekannte Benutzer entstehen beim ersten Login, die Rolle kommt bei jeder Anmeldung neu aus
den Gruppen (#007)
- Lokale Anmeldung mit E-Mail und Passwort als Notfallzugang, nur bei `AUTH_LOCAL_ENABLED=true`
(#007)
- Rechte in einer Tabelle mit vier Rollen; geprüft ausschließlich serverseitig, Rolle und
Aktiv-Status pro Anfrage aus der Datenbank (#007)
- Benutzerverwaltung für Administratoren: Rolle setzen, Konto abschalten, lokales Konto
anlegen (#007)
- Navigation zeigt nur, was die eigene Rolle darf (#007)
- Betriebsmodus **Intern** und **Dienstleistung**, umschaltbar ohne Neustart. Im Modus Intern
erscheint die eigene Organisation nirgends; der Wechsel nimmt alle Geräte, Personen,
Abteilungen und Läufe mit, weil jede Zeile ohnehin schon eine Kunden-Zuordnung trägt (#008)
- Bestätigungsdialog für den Moduswechsel, der die Folgen aus derselben Prüfung zeigt, die der
Wechsel danach durchsetzt (#008)
- Kundenverwaltung im Modus Dienstleistung: anlegen, bearbeiten, archivieren statt löschen (#008)
- Admin → Einstellungen: Betriebsmodus, Name der eigenen Organisation, globale Namensregeln,
Sperr-OU und die Anzeige der Adresse, die in den QR-Codes steckt (#008)
- Navigation und Beschriftungen folgen dem Betriebsmodus — ein Codepfad, nur ein- und
ausgeblendete Einträge (#008)
- Versionierte Vorlagen: „Bearbeiten“ erzeugt eine tiefe Kopie der zuletzt veröffentlichten
Version als Entwurf, je Vorlage gibt es höchstens einen. Veröffentlichen hält Zeitpunkt,
Urheber und Änderungsnotiz fest (#009)
- Vorlagen duplizieren und archivieren statt löschen (#009)
- 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)
- Mitarbeiterliste mit Suche über Anzeigename, Mail, UPN und sAMAccountName, Filter nach Abteilung;
standardmäßig nur aktive Personen, mit Umschalter auch deaktivierte (#011)
- Mitarbeiterseite mit Stammdaten, zugeordneten Geräten und Platz für die Laufhistorie; Personen
werden deaktiviert statt gelöscht (#011)
- Abteilungen unter Admin: Ziel-OU, Standardgruppen und Mail-Domain — daraus entstehen später die
Befehle, die ein Konto anlegen. Abteilungsnamen, die bei Personen vorkommen, aber nicht gepflegt
sind, werden eigens ausgewiesen (#011)
- Geräteliste mit Suche über Hostname, Bezeichnung, Seriennummer und Tag-Code, Filter nach Typ;
Geräte anlegen, bearbeiten, archivieren (#011)
- CSV-Import für Geräte: je Zeile ein Ergebnis, eine fehlerhafte Zeile bricht den Import nicht ab,
sondern wird mit Zeilennummer und Grund gemeldet. Der Zerleger kommt mit Semikolon, BOM, CRLF,
Anführungszeichen und Zeilenumbrüchen im Wert zurecht (#011)
- Die Seitenleiste führt Personen und Geräte unter einem Eintrag; ein Umschalter wechselt zwischen
beiden Listen (#011)
- Platzhalter werden aufgelöst: eingebaute Werte aus Gerät, Lauf, Mitarbeiter und Abteilung, dazu
die Variablen der Vorlage. Ein unbekannter Platzhalter bleibt sichtbar stehen, statt still zu
verschwinden (#012)
- `{{ou}}` einer Person aus dem Verzeichnis ist der Eltern-Container ihres DN — genau der Pfad, den
ein `New-ADUser -Path` braucht (#012)
- Namensregeln für `upn`, `sam`, `mail` und `hostname` mit Transliteration, `{v}`, `{nnn}` und
`{kundenkuerzel}`; im Modus Dienstleistung überlagern Kundenregeln die globalen Feld für Feld
(#012)
- „Alle Befehle als Skript kopieren“ je Abschnitt: die Befehle in Reihenfolge, je mit einer
Kommentarzeile darüber und dem Zeilenumbruch der jeweiligen Shell (#012)
- Läufe: anlegen — auch als `offen` ohne Start, das ist die Zuweisung —, abarbeiten, abschließen
und archivieren. Ein Lauf wird erst `in Arbeit`, wenn ihn jemand wirklich öffnet (#013)
- Abarbeitungsansicht: nummerierte Abschnitte, Fortschrittsbalken, je Schritt Zustand, Wert,
Notiz, Kommentare und „wer wann“; jeder Schritt speichert einzeln, damit ein verlorener Klick
nicht zwei Stunden Arbeit kostet (#013)
- Die Ansicht frischt sich alle zehn Sekunden auf und pausiert, solange der Tab im Hintergrund
liegt — zwei Techniker an einem Onboarding sehen die Arbeit des anderen (#013)
- Umschalter „Kompakt / Ausführlich“: beim ersten Mal sind die Hilfetexte der eigentliche Wert,
beim dritten Mal stehen sie im Weg (#013)
- Befehlsschritte zeigen den fertigen Befehl mit eingesetzten Werten und einem Kopier-Knopf; ein
unaufgelöster Platzhalter wird angezeigt, statt stillschweigend mitkopiert zu werden (#013)
- „Abschnitt trifft nicht zu“ setzt alle Schritte eines Abschnitts mit einer Begründung auf
`nicht zutreffend` — für die Konto-Abschnitte eines Onboardings ohne eigenes AD (#013)
- Kommentare am Lauf und am einzelnen Schritt (#013)
- Ein Lauf mit Freitextnamen lässt sich nachträglich mit einem Konto aus dem Verzeichnis
verknüpfen: beim Onboarding gibt es die Person dort noch nicht (#013)
- Start-Dialog mit Vorbelegung aus der Adresse, Laufliste mit Suche nach Titel, Ticket, Hostname
und Person sowie Filter nach Status und eigener Zuweisung (#013)
- Der Start-Dialog schlägt `upn`, `sam` und `mail` nach den Namensregeln vor, sobald die Vorlage
solche Variablen hat; ausgefüllte Felder bleiben stehen (#013)
- Abschnitte klappen zu, sobald in ihnen nichts mehr zu tun ist — wer einen wieder aufklappt,
behält ihn offen, auch über das Speichern hinweg (#013)
- QR-Aufkleber: Stapel vorgedruckter Tags, Etikettendruck als Einzeletikett (62 × 29 mm) und als
A4-Bogen (3 × 8 à 70 × 37 mm), je mit QR, lesbarem Code, Hostname und Besitzer (#014)
- `/t/<code>` als Einstieg vom Aufkleber: registriertes Gerät → Geräteseite, vorgedruckter Tag →
Registrierformular, unbekannter Code → Hinweis mit Weg zur Suche (#014)
- Geräteseite mit Kopf, „Zugewiesen“, Schnellstart aus den Gerätetyp-Vorgaben, „Vorlage zuweisen“
und Historie — am Handy bedienbar, weil sie dort benutzt wird (#014)
- „Scannen“ mit der BarcodeDetector-API des Browsers, wo es sie gibt, sonst mit WebAssembly; wo
keine Kamera läuft, steht ein Feld für den abgetippten Code (#014)
- Der Tag-Code in der Geräteliste und in der Tag-Liste führt dorthin, wo auch der Scan landet (#014)
- Übersicht: überfällige Läufe, die eigenen Zuweisungen, zuletzt Bearbeitetes und ein Schnellstart
aus den Vorlagen, aus denen im Haus am häufigsten Läufe entstehen (#015)
- Archiv mit Volltextsuche über Titel, Notizen, erfasste Werte und Kommentare in einem gemeinsamen
Lesemodell; die Trefferliste zeigt, **wo** der Begriff steht, und hebt ihn hervor (#015)
- Filter im Archiv nach Vorlage, Person, Abteilung, Gerät, Zeitraum und Bearbeiter — im Modus
Dienstleistung zusätzlich nach Kunde (#015)
- Druckansicht eines Laufs: hell, ohne Navigation, auf A4, mit allen Schritten, Werten, Notizen,
Namen und Zeitstempeln. Sie ersetzt im MVP den PDF-Export (#015)
- Verzeichnis-Abgleich über LDAP: Quelle anlegen, Verbindung testen, nach Zeitplan oder auf Knopfdruck
abgleichen. Wer im Verzeichnis verschwindet oder deaktiviert wird, wird hier inaktiv — gelöscht wird
nie, sonst verlöre die Historie ihre Namen (#016)
- „Verbindung testen“ zeigt die ersten Einträge **mit den gemappten Feldern** und schreibt nichts: erst
daran sieht ein Administrator, ob die Zuordnung stimmt (#016)
- Der Zeitplan läuft im selben Prozess wie die Anwendung, sieht jede Minute nach, welche Quelle fällig
ist, und verlässt sich auf eine Sperre in der Datenbank — zwei Instanzen kommen sich nicht in die
Quere (#016)
- Personen aus einer CSV-Datei einlesen: je Zeile ein Ergebnis, eine fehlerhafte Zeile bricht den
Import nicht ab. Eine leere Zelle löscht nichts, und wer schon aus dem Abgleich stammt, wird nicht
überschrieben (#016)
- Abteilungsvorschläge aus den Verzeichnis-Werten: ein Klick macht daraus eine gepflegte Abteilung
(#016)
- Die Attributzuordnung ist in der Oberfläche bearbeitbar: zehn Felder mit den AD-Vorgaben, damit ein
OpenLDAP nicht in der Datenbank nachgepflegt werden muss (#016)
- Abgleich-Log je Quelle: die letzten zehn Läufe mit gelesen, angelegt, übernommen, aktualisiert,
deaktiviert und übersprungen — und wer sie angestoßen hat, Mensch oder Zeitplan (#016)
- Wer schon von Hand angelegt wurde und dann im Verzeichnis auftaucht, wird **übernommen** statt
verdoppelt: dieselbe Zeile behält ihre Läufe und Geräte (#016)
- Ein hinterlegtes CA-Zertifikat ist als vorhanden erkennbar und lässt sich entfernen; der
Zertifikatstext selbst kommt nie zurück in die Oberfläche (#016)
- Die sieben Abläufe des Hauses liegen nach dem Seed als veröffentlichte Version 1 bereit: PC
Neuinstallation, Onboarding, Offboarding, Server-Inbetriebnahme, Firewall-Rollout, Patchday und
Restore-Test — 36 Abschnitte, 155 Schritte, 28 Variablen, jeder Schritt mit Hilfetext (#017)
- Die Vorlagen stehen als JSON im Austauschformat unter `prisma/seed-daten/vorlagen/` und werden
beim Seed gegen dasselbe Schema geprüft wie ein Import: ein Fehler fällt beim Einrichten auf und
nicht erst im ersten Lauf (#017)
- Beispielbestand zum Vorführen: drei Abteilungen mit Ziel-OU und Standardgruppen, fünf Personen,
`PC-001` und `NB-001` mit ihren Tag-Codes, fünf vorgedruckte Tags und die Schnellstart-Vorgaben
je Gerätetyp (#017)
- `SEED_MODE=DIENSTLEISTUNG` legt „Musterfirma GmbH" und „Bäckerei Reiter" an; der Beispielbestand
hängt dann an der Musterfirma, `LZ-7K3F9Q` führt also auf `MUS-PC-001` (#017)
- Namensregeln und die Sperr-OU kommen mit dem Seed: „Namen aus den Regeln vorschlagen" schlägt für
Maria Huber genau `m.huber@firma.example` und `mhuber` vor — dieselben Werte, die im
Beispielbestand stehen (#017)
- Anleitung für Technik und Verwaltung unter `docs/anleitung/`: neun Kapitel von den ersten Schritten
bis zur Fehlerbehebung, mit Bildern, die `pnpm docs:screenshots` reproduzierbar neu erzeugt (#018)
- `pnpm docs:pdf` macht daraus ein A4-PDF mit Deckblatt, Inhaltsverzeichnis und Seitenzahlen — hell
gedruckt, auch wenn die App dunkel ist (#018)
- Screenshots lassen sich markieren: Rahmen in der Akzentfarbe mit Nummer, für die Stellen, die ein
Kapitel gerade beschreibt (#018)
- `{{ou_gesperrt}}` wird aus Admin → Einstellungen vorbelegt. Das Feld war gespeichert und wurde von
keinem Platzhalter gelesen; `Move-ADObject -TargetPath "{{ou_gesperrt}}"` blieb leer, obwohl die
Einstellungsseite es versprach (#017)
- Die Ende-zu-Ende-Abläufe prüfen ihre Voraussetzungen, bevor der erste startet: läuft ein gebauter
Stand, steht der Seed-Bestand, und liest der Server **dieselbe** Datenbank wie der Testprozess.
Vorher fiel eine fehlende Voraussetzung erst mitten im Lauf auf — als Zeitablauf oder als „0
Treffer" (#018)
- Jeder Ablauf legt an, was er braucht, und räumt es weg. Keiner setzt mehr voraus, dass die
Datenbank schon Läufe, Entwürfe oder bestimmte Personen enthält, und keiner sucht seine Vorlage
über die Position in der Liste (#018)
### Behoben
- Die Umsortierung im Schnellstart wurde nicht gespeichert, wenn mehrere Zeilen desselben
Gerätetyps dieselbe `position` trugen — bei gleicher Position ist die Reihenfolge in Postgres nicht
festgelegt, und Lesemodell und Dienst gaben die Zeilen unterschiedlich zurück. Gefunden beim
Durchklicken des gebauten Stands, nicht von der Testsuite (#022)
- Der Compose-Stand las `DATABASE_URL` aus der `.env` — dort steht die Adresse für die Entwicklung
(`localhost` und ein offener Port), und im Container zeigt sie ins Leere. Wer den dokumentierten
Weg ging, bekam einen Stand, der nicht startet. Die Adresse baut sich jetzt immer aus den
`POSTGRES_*`-Werten zusammen (#019)
- Der dokumentierte Seed-Befehl `docker compose run --rm migrate pnpm db:seed` scheiterte auf einer
frischen Installation: der Einmal-Dienst bekam kein `APP_SECRET`, und ohne das lässt sich das
Bind-Passwort der Beispiel-Verzeichnisquelle nicht verschlüsseln. Abgebrochen wurde erst, nachdem
die Vorlagen schon angelegt waren (#019)
- Der Healthcheck des Webdienstes konnte nie bestehen: busybox löst `localhost` im Container zuerst
nach `::1` auf, Next lauscht aber nur auf IPv4. `docker compose ps` zeigte `web` dauerhaft als
`unhealthy`, obwohl die App einwandfrei antwortete — und jedes `--wait` lief in den Zeitablauf.
Geprüft wird jetzt über `127.0.0.1`, und ein Test hält die Adresse fest (#019)
- Die CI-Pipeline konnte in keinem der beiden Jobs grün werden: dem Prüf-Job fehlte das
LDAP-Testverzeichnis, das `pnpm test` dort verlangt, dem E2E-Job fehlte ein gestarteter Server, und
`shellcheck` wurde aufgerufen, ohne vorhanden zu sein. Die Zusage „Qualitätsschranken“ galt damit
nur lokal (#019)
- Der README nannte für die Entwicklungsdatenbank Port 5433; `docker-compose.dev.yml` nimmt 5434.
Wer dem README folgte, landete auf der Datenbank eines anderen Projekts — mit „0 Treffer“ als
einzigem Hinweis (#019)
### Sicherheit
- Der Compose-Stand bricht ab, wenn `APP_SECRET`, `POSTGRES_PASSWORD` oder `APP_URL` fehlen,
statt mit Standardwerten zu starten (#005)
- Die Mandantengrenze steht als zusammengesetzter Fremdschlüssel in der Datenbank: ein Lauf kann
strukturell kein Gerät, keine Person und keine Abteilung eines fremden Kunden wählen (#006)
- Ein Bind-Passwort ohne Versionspräfix wird von der Datenbank abgewiesen, ein versehentlich im
Klartext geschriebenes fällt damit sofort auf (#006)
- Ein Lauf nimmt seine Schrittergebnisse und Kommentare beim Löschen nicht mehr mit, und ein
Protokolleintrag kann seinen Urheber nicht mehr verlieren (#006)
- Das Rücksprungziel nach der Anmeldung wird aufgelöst und gegen die eigene Herkunft geprüft,
statt nur Präfixe zu vergleichen — `/<TAB>/fremd.example` wäre sonst eine offene Weiterleitung
gewesen (#007)
- Alle Umleitungen gehen über `APP_URL` statt über die Herkunft der Anfrage; hinter einem Reverse
Proxy zeigten sie sonst auf den internen Host (#007)
- Eine Anmeldung über den Anbieter verknüpft nur mit einem bestehenden Konto, wenn die Adresse
bestätigt ist (#007)
- Ohne eingerichtete `OIDC_GROUP_*`-Zuordnung bleibt die Rolle unangetastet, statt jeden auf
`LESER` herabzustufen (#007)
- Der letzte aktive Administrator kann sich weder herabstufen noch abschalten (#007)
- Beim Abschalten eines Kontos werden laufende Sitzungen sofort ungültig (#007)
- Im Modus Intern wird eine vom Client mitgeschickte Kunden-Zuordnung verworfen, nicht bloß
abgelehnt — es gibt in diesem Modus keine Kundenwahl (#008)
- Der Rückweg nach Intern wird abgelehnt, solange mehr als ein nicht archivierter Kunde
existiert (#008)
- Die eigene Organisation lässt sich nicht archivieren (#008)
- Eine veröffentlichte Vorlagen-Version ist unveränderlich: jeder Schreibversuch wird abgewiesen,
und ein bestehender Lauf zeigt auch nach neuen Veröffentlichungen genau die Schritte, mit denen
er gestartet wurde (#009)
- Die Unveränderlichkeit setzt zusätzlich die Datenbank durch, damit auch Migrationen, `psql` und
spätere Connectoren nicht daran vorbeischreiben können; der Rückweg nach Entwurf ist versperrt
(#009)
- Vorlagen eines fremden Kunden bleiben auch bei Suche und Filtern verborgen, und Historie wie
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)
- Die Stammdaten einer Person aus dem Verzeichnis lassen sich nicht von Hand überschreiben — der
nächste Abgleich würde die Änderung ohnehin zurückdrehen, und ein Wert, der zwischen zwei Läufen
hin- und herspringt, ist schlimmer als ein falscher. Deaktivieren bleibt möglich (#011)
- Ein Gerät kann nur einer Person desselben Kunden gehören; der Dienst sagt es verständlich, bevor
der zusammengesetzte Fremdschlüssel zuschlägt (#011)
- Ein Gerät, an dem Läufe hängen, lässt sich nicht löschen, nur archivieren (#011)
- Der Tag-Code entsteht beim Anlegen, steht sofort im Vergabebuch und lässt sich nie ändern — beides
in einer Transaktion, damit kein Code ohne Eintrag entsteht (#011)
- Werte werden für die jeweilige Shell maskiert, bevor sie in einen Befehl kommen: PowerShell
verdoppelt `"` und Backtick und entschärft `$`, Bash setzt einfache Anführungszeichen. Für Cmd
wird ein Wert mit `"` oder Zeilenumbruch **abgelehnt** statt unsicher maskiert — dort gibt es
keine Regel, die in jedem Fall stimmt (#012)
- `{{generiertes_passwort}}` entsteht bei jeder Anzeige neu, besteht aus 20 Zeichen ohne
verwechselbare Buchstaben und wird nirgends gespeichert; in der Druckansicht steht
`[Passwort – nicht gespeichert]` (#012)
- Ein eingesetzter Wert wird nicht doppelt maskiert: React maskiert Textknoten selbst, und ein Test
am gerenderten HTML hält fest, dass ein `<script>` im Namen wirklich als Text ankommt (#012)
- Der Archiv-Schreibschutz sitzt an genau einer Stelle und wird vor jeder Änderung an Kopfdaten,
Schrittergebnis und Kommentar gerufen; ein abgeschlossener Lauf bleibt änderbar, ein archivierter
nicht, und zurückholen darf nur ein Administrator (#013)
- Ein Abschluss nimmt sich selbst zurück, sobald eine spätere Änderung einen Pflichtschritt wieder
offen lässt — „abgeschlossen“ soll nicht lügen (#013)
- `nicht zutreffend` und `fehlgeschlagen` verlangen eine Begründung; das Feld erscheint, bevor
gespeichert wird, statt den Benutzer in eine Absage laufen zu lassen (#013)
- Der Zustands-Endpunkt für die Aktualisierung prüft das Leserecht selbst, liefert einen fremden
Lauf als „nicht gefunden“ und **keine Befehle** — sonst entstünde bei jedem Abruf ein neues
generiertes Passwort (#013)
- In einem Lauf steht **ein** generiertes Passwort, nicht je Vorkommen ein anderes: der Befehl, der
das Konto anlegt, und die Zeile, die das Passwort mitteilt, meinen dasselbe. Vorher hätte das
Konto A bekommen und der Benutzer B erfahren — und A wäre unwiederbringlich gewesen (#013)
- Die Aktualisierung lädt die Seite nicht von selbst neu, solange ein generiertes Passwort darauf
steht; sie fragt (#013)
- Eine Anfrage unter `/api/` ohne Anmeldung bekommt `401` statt einer Weiterleitung auf die
Anmeldeseite — ein `fetch` folgte ihr und hielt die HTML-Antwort für einen Erfolg (#013)
- Zuweisen lässt sich ein Lauf nur an jemanden, der ihn auch öffnen könnte; die Auswahl zeigt keine
Bearbeiter fremder Kunden mehr (#013)
- „Nicht zutreffend“ und „fehlgeschlagen“ lassen sich mit einem Klick zurücknehmen. Vorher sah es
danach aus, als wäre nichts gewählt, während in der Datenbank weiter der alte Zustand stand (#013)
- Im QR steht ausschließlich `{APP_URL}/t/{tagCode}` — kein Name, keine Seriennummer, kein
Besitzer. Ein Aufkleber hängt jahrelang sichtbar am Gerät und wird von jedem Telefon gelesen;
was darauf steht, ist veröffentlicht (#014)
- Der QR wird lokal erzeugt, nie über eine externe API: die bekäme mit jedem Aufruf den Bestand an
Tag-Codes geliefert (#014)
- Das WebAssembly des QR-Lesers kommt aus dem eigenen Haus statt aus einem CDN — die
Content-Security-Policy erlaubt nur `connect-src 'self'`, und die Anwendung soll ohne Internet
laufen (#014)
- Ein Tag, ein Etikett und eine Geräteseite eines fremden Kunden sind nicht erreichbar; „gibt es
nicht“ und „darfst du nicht“ sind dieselbe Antwort (#014)
- In der Druckansicht erscheint vom Mitarbeiter nur Name, UPN und Abteilung — das Lesemodell holt
gar nichts anderes erst. Ein Ausdruck liegt herum; was darauf steht, ist aus der Hand gegeben
(#015)
- Für den Ausdruck entsteht **kein** generiertes Passwort: wo in einem Vorlagentext
`{{generiertes_passwort}}` steht, erscheint `[Passwort – nicht gespeichert]`. Zwei Ausdrucke
desselben Laufs sind deshalb Zeichen für Zeichen gleich — der Test prüft genau das (#015)
- Die Archivsuche setzt jeden Wert als Parameter und maskiert `%`, `_` und `\` für `ILIKE`: die
Suche nach „50 %“ trifft nicht „5099“ (#015)
- Das Bind-Passwort einer Verzeichnisquelle wird mit AES-256-GCM verschlüsselt, erreicht keine
Antwort und kein Log: Fehlermeldungen der LDAP-Bibliothek werden gefiltert, bevor sie gespeichert
werden, und die Ausnahme trägt keine `cause` — sonst stünde die ganze Kette im Server-Log (#016)
- Gespeichert werden nur die zugeordneten Attribute: keine Passwörter, keine
Gruppenmitgliedschaften (#016)
- Die Hostnamen-Prüfung von TLS lässt sich nicht abschalten; ein Test hält fest, dass weder
`rejectUnauthorized` noch `checkServerIdentity` gesetzt werden (#016)
- Das Verzeichnis gibt es nur im Modus Intern — Reiter, Seite und Dienst prüfen das je selbst (#016)
- Der Abgleich deaktiviert niemanden, wenn sich **kein einziger** gelesener Eintrag zuordnen ließ:
ein falsch gesetztes Kennungsattribut hätte sonst die gesamte Belegschaft abgeschaltet und dabei
Erfolg gemeldet (#016)
- Übersprungene Einträge werden nicht deaktiviert: sie standen im Ergebnis, nur ohne brauchbare
Kennung — wer sie mit abschaltete, nähme einem anwesenden Menschen den Zugang (#016)
- Ergebniscodes des Verzeichnisses werden in Sätze übersetzt: falsches Passwort, fehlendes Leserecht
und ein unbekannter Basis-DN sahen vorher gleich aus (#016)
- Die Beispiel-Verzeichnisquelle des Seeds ist abgeschaltet, und ihr Bind-Passwort ist ein bei jedem
Lauf neu gewürfelter Zufallswert: ein fest verdrahtetes Beispielpasswort im Quelltext stünde am
Ende in einer echten Installation (#017)
- Titel und Hilfetexte eines Laufs bekommen **dasselbe** generierte Passwort wie der Befehl daneben.
Vorher erzeugte jede Auflösung ein eigenes: im Befehl stand A, im Hilfetext B — wer B in den
Passwortmanager legte, sperrte den neuen Mitarbeiter aus (#017)
- Der Seed vergibt keinen verbrannten Tag-Code neu: er fragt das Vergabebuch und nicht nur die
Geräteliste, wie es der Gerätedienst tut (#017)
- Der Verzeichnis-Ablauf legt seine eigene Quelle an, statt die erste vorhandene zu nehmen: er hätte
in einer echten Installation die Attributzuordnung der produktiven Quelle überschrieben (#018)
- Ein Nachlauf setzte die Namensregeln auf leer, statt den vorherigen Wert wiederherzustellen — er
löschte damit, was der Seed eingerichtet hatte (#018)