Files
Laufzettel/CHANGELOG.md
T
Florian WildeandClaude Opus 5 8e67591912
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(mandant): Betriebsmodus, Kundenverwaltung und Einstellungen
Laufzettel läuft jetzt in zwei Betriebsmodi, aber nicht in zwei Codepfaden. Jede
kundenbezogene Zeile trägt ohnehin schon eine Kunden-Zuordnung; im Modus INTERN
ist das die eine Organisation, die in der Oberfläche nirgends erscheint. Der
Wechsel ist deshalb eine Datenänderung und keine Migration — dieselbe Zeile
bleibt, nichts hängt um.

src/server/mandant.ts ist die eine Stelle, an der die Mandantenregel steht:
kundeErmitteln verwirft im Modus INTERN eine mitgeschickte kundeId, verlangt im
Modus DIENSTLEISTUNG eine Wahl und prüft, ob der Benutzer den Kunden überhaupt
sehen darf. Die Sichtbarkeit steckte zunächst in einer zweiten Funktion daneben,
an die jeder künftige Aufrufer selbst hätte denken müssen; da #009 diese Signatur
übernimmt, ist sie jetzt eingebaut.

Der unabhängige Review fand drei Blocker, alle behoben.

Die Lesemodelle wandten die Mandantensicht nicht an. kundenListe gab jedem alle
Kunden zurück, und sichtbareKundenIds hatte in der Produktion keinen einzigen
Aufrufer — ein gebundener Techniker hätte Namen, Notizen und Bestandszahlen
fremder Kunden gesehen. Die Funktionen verlangen jetzt den Benutzer und filtern
selbst, wie CLAUDE.md es verlangt.

Mein Kommentar "Alles in einer Transaktion" war falsch: der Protokolleintrag
stand dahinter. Schlug er fehl, war der Modus gewechselt und der Vorgang stand
nirgends. Und Prüfung und Vollzug lagen auseinander, sodass zwei gleichzeitige
Wechsel beide durchliefen und ein zwischendurch angelegter Kunde zu Modus INTERN
bei zwei Kunden führte. Beides reproduziert, beides geschlossen: alles in einer
Transaktion, mit pg_advisory_xact_lock als erster Anweisung.

Dazu sechs kleinere Befunde: ASCII-Faltung in Texten, die Menschen lesen (die
Regel gilt für Bezeichner, nicht für Fließtext — steht jetzt so in CLAUDE.md);
eine Erfolgsmeldung, die nach dem Wechsel unsichtbar blieb; eine Zusage im
Dialog, die der Code nicht durchsetzt; fehlende Fehlerbehandlung beim
Archivieren; ein Handy-Screenshot, der das Navigationsmenü statt des Dialogs
zeigte.

Der Wechsel wurde nicht nur im Service geprüft, sondern per Playwright durch die
Oberfläche gefahren: anmelden, Dialog aufklappen, bestätigen, und danach steht
die Navigation auf Kunden.

336 Tests.

Closes #8

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-25 07:01:46 +02:00

5.1 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]

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)

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)