Files
Laufzettel/CHANGELOG.md
T
Florian WildeandClaude Opus 5 2d3325ef20
Prüfung / pruefung (push) Canceled after 0s
Prüfung / pruefung (pull_request) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
Prüfung / e2e (pull_request) Canceled after 0s
feat(meldungen): Benachrichtigungen per Mail und Webhook (#54)
Ein zugewiesener Lauf fiel bisher nur auf, wenn jemand die Uebersicht
oeffnete; ein ueberfaelliger ueberhaupt nicht. Vier Anlaesse gehen jetzt
hinaus — zugewiesen, ueberfaellig, abgeschlossen und Verzeichnisabgleich
fehlgeschlagen —, je Person abschaltbar. Niemand bekommt Post ueber die
eigene Tat: das ist der haeufigste Grund, Benachrichtigungen ganz
abzuschalten, und deshalb faellt der Handelnde immer aus der
Empfaengerliste.

Eingereiht wird im Anfrage-Pfad, gesendet wird es nicht. Ein haengender
Mailserver darf kein Abhaken blockieren, also schreibt der Dienst eine
Zeile in `Zustellung` und ein eigener Zeitplan schickt sie. Betreff und
Rumpf stehen fertig darin: eine Meldung sagt, was *damals* war.

In keiner Nachricht steht ein Schrittinhalt, und damit auch kein
erzeugtes Passwort. Darueber wacht src/lib/meldungen.ts — die Funktionen
dort nehmen nur Kopfdaten, der Typ laesst nichts anderes zu. Geprueft
wird es zusaetzlich ueber alle Rumpftexte, die wirklich verschickt
wurden, so wie #012 es fuer die Druckansicht tat.

Zwei Dinge haetten ohne Datenbank nicht gehalten. Die Einmaligkeit der
Ueberfaelligkeitsmeldung liegt in einem partiellen eindeutigen Index:
der Zeitplan sieht jede Minute nach, und ohne ihn bekaeme der
Zustaendige fuer einen Lauf, der ueber das Wochenende steht, 2880
Mails — jede fuer sich korrekt, kein Test rot. Und die Sperre der
Warteschlange liegt in derselben Tabelle wie die des Abgleichs, denn
hinter einem Lastverteiler laufen zwei Instanzen.

Eine stille Warteschlange ist schlimmer als keine: Verwaltung →
Zustellungen zeigt mit Grund, was hinausging und was nicht.

Geprueft gegen einen echten SMTP-Faenger (mailpit) statt gegen eine
abgefangene Funktion — und `mailpit` gehoert damit ins Tor vor dem
Commit, sonst ueberspringt die Suite 3 Tests und sieht gruen aus.

Closes #54

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-05 17:22:20 +02:00

36 KiB
Raw Blame History

Changelog

Alle nennenswerten Änderungen an Laufzettel werden in dieser Datei dokumentiert.

Das Format basiert auf Keep a Changelog, und dieses Projekt folgt Semantic Versioning.

[Unveröffentlicht]

Geändert

  • 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

  • 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

  • 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 tagCodes 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)