#038: Die E2E-Ablaeufe laufen ab jetzt vor jedem Merge nach main, nicht vor jedem Commit. Gemessen: Bau 14 s, Start 7 s, Ablaeufe 256 s — zu den vier Minuten des bestehenden Tores. Neun Minuten vor jedem Commit ist ein Tor, das umgangen wird. Vor jedem Merge waeren es in Phase 2 acht Laeufe gewesen, und jeder der vier Befunde aus #027 waere am Tag seiner Entstehung aufgefallen. Geprueft wurde jeder der 16 Ablaeufe gegen die Frage, ob es ein Vitest-Gegenstueck gibt. Neun Zusagen haengen ausschliesslich dort, und keine laesst sich holen: alle brauchen einen echten Browser. Die Liste steht in CLAUDE.md. Der eigentliche Fund dabei war keine Testluecke, sondern eine Konvention ohne Pruefer: "jede Seite prueft selbst" steht seit #007 in CLAUDE.md, und geprueft hat es bis hierher nur ein Mensch, der nachsieht. Der Review von Phase 2 hat alle 27 Seiten durchgesehen — ein Ergebnis, das fuer genau einen Tag gilt. tests/zugang.test.ts liest sie jetzt, samt jedem Route Handler; die Ausnahmen (Auth, Healthcheck) stehen namentlich mit Begruendung, und ein Test prueft auch die Gegenrichtung. #037: Das Lesemodell der Anhaenge liegt in queries/ und engt selbst ein — vorher hing das daran, dass beide Aufrufer den Lauf vorher pruefen. /meine-geraete wird bei 390 px gemessen, mit einem echten MITARBEITER-Konto aus dem E2E-Vorlauf, dazu ein Test, dass dort kein Link in die Verwaltung angeboten wird. Dabei selbst einen Fehler gemacht, der die neue Regel begruendet: fuer das zweite Konto brauchte `anmelden` eine Variante, die nicht auf / wartet, und ich habe das Warten fuer alle gelockert. Das machte zwei unverwandte Ablaeufe unzuverlaessig — sichtbar nur im vollen Durchgang, einzeln waren beide gruen. Ein gemeinsamer Helfer in einer Testsuite ist geteilter Zustand. Ohne vollen Durchgang haette ich zwei rote Ablaeufe gemergt. 1457 Vitest-Tests (vorher 1454), 115 E2E-Ablaeufe (vorher 113). Offen bleibt nur noch #024, und das verlangt einen echten Proxmox-Host. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
579 lines
45 KiB
Markdown
579 lines
45 KiB
Markdown
# 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]
|
||
|
||
### 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 (#039)
|
||
- 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 (#040)
|
||
- `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 (#040)
|
||
- Im README fehlte `format:pruefen` in den Qualitätsschranken, obwohl `CLAUDE.md` es seit #010
|
||
verlangt (#040)
|
||
|
||
### 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 (#040)
|
||
- Fünf Tests für den Healthcheck, darunter einer, der belegt, dass die Begründung ins Protokoll
|
||
geschrieben und nicht bloß verschluckt wird (#039)
|
||
|
||
## [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)
|