# 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`](docs/issues/README.md), Entscheidungen im > [Entwicklungstagebuch](docs/devlog.md). ## 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 ```bash 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:5434 pnpm db:migrate pnpm db:seed pnpm dev # http://localhost:3000 ``` `APP_SECRET` erzeugen: ```bash openssl rand -base64 48 ``` ### Qualitätsschranken ```bash docker compose -f docker-compose.test.yml up -d --wait ldap mailpit # sonst fehlen 25 Tests pnpm lint && pnpm typecheck && pnpm format:pruefen && pnpm test && pnpm pruefe:doku ``` Die beiden Container sind keine Zierde: ohne `ldap` überspringt die Suite den ganzen Verzeichnis-Abgleich (22 Tests), ohne `mailpit` den echten Mailversand (3 Tests) — sie sagt das auf stderr, aber der Lauf sieht grün aus. In CI bricht sie stattdessen ab. Ein Pre-Commit-Hook erzwingt Lint und Format, ein Commit-Msg-Hook Conventional Commits. Ende-zu-Ende-Tests: `pnpm test:e2e` (verlangt einen gebauten, laufenden Stand — siehe `docs/anleitung/`). ## Betrieb mit Docker Compose ```bash 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: ```bash docker compose run --rm migrate pnpm db:seed ``` ### Was der Seed anlegt | | Inhalt | |---|---| | Grundbestand | Betriebsmodus, eigene Organisation, Administrator aus `SEED_ADMIN_EMAIL` und `SEED_ADMIN_PASSWORD` | | Vorlagen | die sieben Abläufe des Hauses als **veröffentlichte Version 1** — PC Neuinstallation, Onboarding, Offboarding, Server-Inbetriebnahme, Firewall-Rollout, Patchday, Restore-Test | | Beispielbestand | 3 Abteilungen, 5 Personen, `PC-001` und `NB-001` mit Tag-Code, 5 vorgedruckte Tags, Schnellstart-Vorgaben je Gerätetyp | | Verzeichnis | eine **deaktivierte** Beispielquelle zum Abschauen — Adresse und Bind-Passwort ersetzen, dann einschalten | Mit `SEED_MODE=DIENSTLEISTUNG` (Alias `SEED_MODE=SERVICE`) kommen zwei Demo-Kunden dazu, „Musterfirma GmbH" (`MUS`) und „Bäckerei Reiter" (`REI`), mit je einer Abteilung, einer Person und einem Gerät nach der Hostname-Regel (`MUS-PC-001`). **Der Seed richtet ein, er räumt nicht auf.** Was schon da ist, bleibt: eine bearbeitete Vorlage, eine geänderte Ziel-OU oder ein umgeschalteter Betriebsmodus überstehen jeden weiteren Lauf. Zweimal hintereinander aufgerufen ändert der zweite Lauf nichts. Die sieben Vorlagen liegen als JSON im [Austauschformat](docs/vorlagen-format.md) unter `prisma/seed-daten/vorlagen/`. Wer eine anpassen will, ändert die Datei — oder besser: die Vorlage in der Oberfläche, denn eine veröffentlichte Version wird nie verändert, und der Seed fasst eine vorhandene Vorlage nicht mehr an. ### 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: ```bash 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 `5434` öffnet — umstellbar über `DEV_DB_PORT`. 5432 ist oft lokal belegt und 5433 von einem anderen Projekt, daher dieser Port. 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: ```bash docker compose exec -T postgres pg_dump -U laufzettel laufzettel | gzip > sicherung-$(date +%F).sql.gz ``` Zurückspielen: ```bash gunzip -c sicherung-2026-09-24.sql.gz | docker compose exec -T postgres psql -U laufzettel laufzettel ``` Gesichert werden müssen **zwei** Dinge: die Datenbank (das Volume heißt `laufzettel_datenbank`; die App selbst hält keinen veränderlichen Zustand auf der Platte) **und die Geheimnisse aus der `.env`**. Ohne das alte `APP_SECRET` sind nach dem Zurückspielen alle gespeicherten LDAP-Bind-Passwörter unlesbar — und das merkt man erst beim nächsten Abgleich. Einzelheiten in [`docs/betrieb/sicherung.md`](docs/betrieb/sicherung.md). ## 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`](docs/betrieb/authentik.md) Schritt für Schritt beschrieben. ## Verzeichnis-Abgleich (nur Modus `INTERN`) Unter **Admin → Verzeichnis** wird eine LDAP-Quelle angelegt: Adresse (`ldaps://` empfohlen, `ldap+starttls://` und `ldap://` gehen auch), ein Dienstkonto mit **Leserecht**, der Basis-DN und optional ein eigener Filter. Ein Dienstkonto genügt — Laufzettel schreibt nie ins Verzeichnis. - **„Verbindung testen“** liest die ersten Einträge und zeigt sie **mit den gemappten Feldern**, ohne etwas zu speichern. Erst daran sieht man, ob die Zuordnung stimmt. - **Die Attributzuordnung** ist einklappbar im Formular und trägt die AD-Vorgaben; ein OpenLDAP bekommt hier etwa `entryUUID` als Kennung. - **Der Zeitplan** läuft im selben Prozess wie die Anwendung und sieht jede Minute nach, welche Quelle fällig ist; das Intervall steht je Quelle. `ZEITPLAN_AKTIV=false` schaltet ihn ab — für Einmal-Container, die nur Migrationen fahren. - **Gelöscht wird nie.** Wer im Verzeichnis verschwindet oder deaktiviert wird, wird hier inaktiv. Lässt sich **kein einziger** Eintrag zuordnen, deaktiviert der Abgleich niemanden und meldet stattdessen einen Fehler — ein falsch gesetztes Kennungsattribut soll nicht die halbe Firma abschalten. - **Das Bind-Passwort** wird verschlüsselt gespeichert und nie wieder angezeigt; ein leeres Feld beim Bearbeiten heißt „unverändert“. Ohne Verzeichnis geht es auch: Personen lassen sich von Hand anlegen oder aus einer CSV-Datei einlesen. ## Umgebungsvariablen Jede Variable steht mit Erklärung in [`.env.example`](.env.example) — hier die Übersicht. **Pflicht** sind `APP_URL`, `APP_SECRET`, `POSTGRES_PASSWORD` und `DATABASE_URL`; fehlt eine davon, bricht der Compose-Stand ab, statt mit Standardwerten zu starten. | Variable | Bedeutung | |---|---| | `APP_URL` | Die **öffentliche** Adresse, unter der die App erreichbar ist. Steht sie hinter einem Reverse Proxy, ist es dessen Adresse — nicht die interne. Aus ihr entstehen die QR-Adressen auf den Aufklebern und die OIDC-Rücksprungadresse. | | `APP_SECRET` | Schlüssel für Sitzungs-Cookies und die Verschlüsselung des LDAP-Bind-Passworts. Mindestens 32 Zeichen, erzeugen mit `openssl rand -base64 48`. **Ändert man ihn, sind alle Sitzungen ungültig und gespeicherte Bind-Passwörter unlesbar.** | | `DATABASE_URL` | Verbindung zur PostgreSQL-Datenbank. | | `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB` | Zugangsdaten des Datenbank-Containers im Compose-Stand. | | `AUTH_LOCAL_ENABLED` | `true` schaltet die lokale Anmeldung mit E-Mail und Passwort frei — Notfallzugang und Entwicklung. Auf `false` verschwindet das Formular **und** der Endpunkt lehnt ab. | | `OIDC_ISSUER`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET` | Der Identitätsanbieter. Discovery über `/.well-known/openid-configuration`. | | `OIDC_NAME` | Beschriftung des Anmeldeknopfs, etwa „Firmenanmeldung“. | | `OIDC_SCOPE` | Angefragte Scopes; muss einen enthalten, der `groups` liefert. | | `OIDC_GROUP_ADMIN`, `OIDC_GROUP_EDITOR`, `OIDC_GROUP_TECHNICIAN`, `OIDC_GROUP_VIEWER` | Welche Gruppe welche Rolle bekommt. Die Rolle wird bei **jeder** Anmeldung neu gesetzt. | | `OIDC_CUSTOMER_GROUP_PREFIX` | Nur im Modus `DIENSTLEISTUNG`: Gruppen mit diesem Präfix weisen einen Benutzer einem Kunden zu. | | `BIND_ADDR`, `APP_PORT` | Wohin der Compose-Stand die App bindet. Standard `127.0.0.1:3000` — davor gehört ein Reverse Proxy. | | `APP_VERSION`, `GIT_SHA` | Erscheinen unten links in der Oberfläche. Im Compose-Stand Build-Argumente. | | `SEED_ADMIN_EMAIL`, `SEED_ADMIN_PASSWORD` | Der Administrator, den `pnpm db:seed` anlegt. Das Passwort wird nur als argon2-Hash gespeichert. Fehlt eines, überspringt der Seed ihn. | | `SEED_MODE` | `INTERN` (Standard) oder `DIENSTLEISTUNG` — der Alias `SERVICE` tut dasselbe. Betrifft nur den **ersten** Seed-Lauf; danach steht der Modus in der Datenbank. | | `SEED_ORG_NAME`, `SEED_ORG_KUERZEL` | Name und Kürzel der eigenen Organisation. Das Kürzel steht auf Etiketten und in der Hostname-Regel. | | `ZEITPLAN_AKTIV` | `false` schaltet den Verzeichnis-Abgleich nach Zeitplan ab. | | `TEST_DATABASE_URL` | Datenbank für die Tests. Ohne sie überspringen sich alle Datenbanktests. | ## Dokumentation - [Anleitung für Technik und Verwaltung](docs/anleitung/) — acht Kapitel mit Bildern. `pnpm docs:pdf` macht daraus ein PDF; jedem Release liegt es bei - [Architektur und Entscheidungen](docs/architektur.md) - [Austauschformat für Vorlagen](docs/vorlagen-format.md) — wie eine Vorlage als JSON aussieht - [Betrieb](docs/betrieb/) — [Inbetriebnahme mit Compose](docs/betrieb/inbetriebnahme.md), [nativ im Proxmox-LXC](docs/betrieb/proxmox.md), [Reverse Proxy](docs/betrieb/reverse-proxy.md), [Anbindung an authentik](docs/betrieb/authentik.md), [Sichern und Zurückspielen](docs/betrieb/sicherung.md) - [Konventionen](CLAUDE.md) — Sprache, Schichtung, feste Regeln - [Änderungen](CHANGELOG.md) · [Entwicklungstagebuch](docs/devlog.md) ## Lizenz Noch nicht festgelegt.