Das vollständige Datenmodell in deutscher Benennung: 20 Tabellen, 9 Aufzählungen, eine Migration, dazu die beiden framework-freien Helfer für die tagCodes der QR-Aufkleber und für verschlüsselte Geheimnisse. Die tragende Entscheidung ist, wo die Regeln stehen. Die Service-Schicht ist der richtige Ort für verständliche Meldungen, aber nicht der einzige Weg, auf dem Daten entstehen — Migrationen, psql, Skripte und ab Phase 4 die Connectoren schreiben an ihr vorbei. Was dabei stillschweigend falsch werden könnte, steht deshalb zusätzlich in der Datenbank: zwölf CHECK-Bedingungen, zwei partielle Eindeutigkeitsindizes und vier zusammengesetzte Fremdschlüssel. Die Mandantengrenze ist einer davon. Lauf verweist mit (geraetId, kundeId) auf Geraet(id, kundeId), analog für Person und Abteilung, und Geraet ebenso auf seinen Besitzer. Ein Lauf, der ein Gerät eines fremden Kunden zieht, ist damit nicht mehr bloß ein Fehler im Service, sondern strukturell unmöglich. MATCH SIMPLE lässt einen Lauf ohne Gerät weiterhin zu. Zwei Dinge kaskadieren bewusst nicht: ein Lauf nimmt seine Schrittergebnisse und Kommentare nicht mit, und ein Protokolleintrag verliert seinen Urheber nicht. Beides stünde gegen den Zweck eines Archivs. Aus demselben Grund gibt es TagCodeVergabe: ohne dieses Buch wäre ein tagCode nach dem Löschen eines Geräts wieder frei, und ein alter Aufkleber im Regal zeigte auf ein fremdes Objekt. Ein unabhängiger Review-Agent hat drei Lücken gefunden, die alle behoben sind: der im Auftrag geforderten Suchspalte Schrittergebnis.wert fehlte der Trigramm-Index; die im Lauf gewählte Abteilung war nur ein Wert in variablen statt ein Bezug, wodurch die Druckansicht eines archivierten Laufs nicht reproduzierbar gewesen wäre; und für die Kundenbindung eines Benutzers (#008) gab es keine Struktur, sie hätte also nur im Token stehen können — gegen die eigene Regel, Rechte pro Anfrage aus der Datenbank zu lesen. Das tagCode-Alphabet des Auftrags widerspricht sich: der wörtlich vorgegebene Zeichensatz enthält ein L, die Klammer daneben verbietet es. Wir folgen dem Wortlaut, weil die Klammer Verwechslungsgruppen benennt und keine Ausschlussliste ist — aus {1, I, L} sind 1 und I gestrichen, wodurch L eindeutig wird, und das Präfix LZ- enthält ohnehin in jedem Code ein L. Das Akzeptanzkriterium in #006 ist richtiggestellt worden statt umgekehrt. 106 Tests, davon 42 gegen die echten Datenbankregeln. Closes #6 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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 indocs/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:
postgreshat keinports:-Mapping, nurexposepostgreshängt ausschließlich im Netzintern, dasinternal: trueist — ein Netz ohne Gateway, also weder vom Host noch aus dem LAN erreichbar- nur
webhängt zusätzlich im Netzextern(Reverse Proxy, OIDC, LDAP) webbindet standardmäßig auf127.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
- Anleitung für Techniker und Admins — entsteht mit #018, inkl. Bildern und PDF
- Architektur und Entscheidungen
- Anbindung an authentik — Inbetriebnahme, Proxmox und Reverse Proxy folgen mit #019
- Konventionen — Sprache, Schichtung, feste Regeln
- Änderungen · Entwicklungstagebuch
Lizenz
Noch nicht festgelegt.