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

laufzettel

Checklisten für wiederkehrende IT-Arbeiten. Versionierte Vorlagen, daraus gestartete Läufe, im Team abgearbeitet, abgeschlossen und unveränderlich archiviert — mit Historie je Gerät und je Mitarbeiter. Einstieg über QR-Aufkleber: Code scannen, Geräteseite öffnen, loslegen.

Stand: in Entwicklung auf v0.1.0 (MVP). Der Fortschritt steht in docs/issues/README.md, Entscheidungen im Entwicklungstagebuch.

Zwei Betriebsmodi

Modus Für wen Kunden-Ebene Verzeichnis
INTERN (Standard) eine Firma, eigene Mitarbeiter unsichtbar eigenes AD per LDAP
DIENSTLEISTUNG IT-Dienstleister mit mehreren Kunden sichtbar manuell oder per CSV je Kunde

Der Modus liegt in der Datenbank und ist vom Admin ohne Neustart umschaltbar. Jede kundenbezogene Zeile trägt immer eine Kunden-Zuordnung, im Modus INTERN die eigene Organisation — deshalb ist der Wechsel eine Datenänderung und keine Migration.

Voraussetzungen

  • Node 24 (.nvmrc) und pnpm 12 über corepack, oder nur Docker mit Compose-Plugin
  • PostgreSQL 17 (im Compose-Stand enthalten)

Entwicklung

corepack enable pnpm
pnpm install
cp .env.example .env          # APP_SECRET und Passwörter ausfüllen
pnpm db:up                    # PostgreSQL auf 127.0.0.1:5433
pnpm db:migrate
pnpm db:seed
pnpm dev                      # http://localhost:3000

APP_SECRET erzeugen:

openssl rand -base64 48

Qualitätsschranken

pnpm lint && pnpm typecheck && pnpm test && pnpm pruefe:doku

Ein Pre-Commit-Hook erzwingt Lint und Format, ein Commit-Msg-Hook Conventional Commits. Ende-zu-Ende-Tests: pnpm test:e2e.

Betrieb mit Docker Compose

cp .env.example .env          # APP_URL, APP_SECRET, POSTGRES_PASSWORD sind Pflicht
docker compose up -d --build

Die Migrationen laufen beim Start automatisch in einem eigenen Einmal-Dienst. Der Seed läuft absichtlich nicht automatisch — ihn startest du einmal von Hand:

docker compose run --rm migrate pnpm db:seed

Die Datenbank ist von außen nicht erreichbar

Das ist bewusst so gebaut und in tests/betriebsstand.test.ts festgehalten:

  • postgres hat kein ports:-Mapping, nur expose
  • postgres hängt ausschließlich im Netz intern, das internal: true ist — ein Netz ohne Gateway, also weder vom Host noch aus dem LAN erreichbar
  • nur web hängt zusätzlich im Netz extern (Reverse Proxy, OIDC, LDAP)
  • web bindet standardmäßig auf 127.0.0.1:3000; davor gehört ein Reverse Proxy

Prüfen:

nc -z localhost 5432 && echo "FEHLER: offen" || echo "dicht"
docker compose exec web nc -z postgres 5432 && echo "von innen erreichbar"

Für die Entwicklung gibt es docker-compose.dev.yml, das PostgreSQL auf 5433 öffnet. Diese Datei ist nicht für den Betrieb gedacht.

Reverse Proxy

Die App läuft hinter einem Reverse Proxy und hat keine fest verdrahteten Adressen. Maßgeblich ist APP_URL — sie steckt in den QR-Codes und in der OIDC-Rückruf-Adresse. Der Proxy muss X-Forwarded-Proto und X-Forwarded-Host setzen.

Sicherung

Der geschlossene Port ändert nichts an der Sicherung, sie läuft über den Dienstnamen:

docker compose exec -T postgres pg_dump -U laufzettel laufzettel | gzip > sicherung-$(date +%F).sql.gz

Zurückspielen:

gunzip -c sicherung-2026-09-24.sql.gz | docker compose exec -T postgres psql -U laufzettel laufzettel

Gesichert werden muss nur die Datenbank — die App hält keinen veränderlichen Zustand auf der Platte. Das Volume heißt laufzettel_datenbank.

Anmeldung

Zwei Wege, beide gleichzeitig möglich:

  • OIDC (Ziel: authentik) für den normalen Betrieb
  • lokal mit E-Mail und Passwort, nur wenn AUTH_LOCAL_ENABLED=true — für die Entwicklung und als Notfallzugang, wenn der Identitätsanbieter ausfällt

Die Rolle kommt bei jedem OIDC-Login aus dem groups-Claim und wird über OIDC_GROUP_ADMIN, OIDC_GROUP_EDITOR, OIDC_GROUP_TECHNICIAN und OIDC_GROUP_VIEWER zugeordnet. Unbekannte Benutzer werden beim ersten Login angelegt.

Rolle Darf
ADMIN alles, inkl. Benutzer, Verzeichnis, Abteilungen, Archiv-Verwaltung
REDAKTEUR Vorlagen anlegen, bearbeiten, veröffentlichen; alles was TECHNIKER darf
TECHNIKER Läufe starten, abarbeiten, abschließen, archivieren; Geräte pflegen
LESER nur lesen

Die Anbindung an authentik ist in docs/betrieb/authentik.md Schritt für Schritt beschrieben.

Umgebungsvariablen

Alle Variablen mit Erklärung stehen in .env.example. Pflicht sind APP_URL, APP_SECRET, POSTGRES_PASSWORD und DATABASE_URL; der Compose-Stand bricht ab, statt mit Standardwerten zu starten.

Dokumentation

Lizenz

Noch nicht festgelegt.

S
Description
No description provided
Readme
27 MiB
2026-10-02 22:17:05 +00:00
Languages
TypeScript 96.1%
Shell 2.4%
PLpgSQL 0.8%
JavaScript 0.5%
CSS 0.2%