feat(seed): sieben Vorlagen, Beispielbestand und Demo-Kunden
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

Die sieben Ablaeufe des Hauses liegen nach dem Seed als veroeffentlichte
Version 1 bereit: 36 Abschnitte, 155 Schritte, 28 Variablen, jeder
Schritt mit Hilfetext. Sie stehen als JSON im Austauschformat unter
prisma/seed-daten/vorlagen/ und werden beim Einrichten gegen dasselbe
Schema geprueft wie ein Import.

Der Seed richtet ein, er raeumt nicht auf: jede Zeile hat einen
sprechenden Schluessel, und was es schon gibt, bleibt unberuehrt. Erst
Entwurf, dann veroeffentlichen - die Trigger aus #010 lassen keinen
anderen Weg zu. Im Modus DIENSTLEISTUNG haengt der Beispielbestand an
der Musterfirma, LZ-7K3F9Q fuehrt also auf MUS-PC-001.

Dazu zwei Zusagen eingeloest, die niemand deckte: {{ou_gesperrt}} kommt
jetzt wirklich aus den Einstellungen, und Titel und Hilfetext eines
Laufs bekommen dasselbe generierte Passwort wie der Befehl daneben -
vorher stand im Befehl A und im Hilfetext B.

Closes #17

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-09-26 05:26:04 +02:00
co-authored by Claude Opus 5
parent ae290345b7
commit 5c81a62cfd
33 changed files with 3400 additions and 119 deletions
+73 -81
View File
@@ -1,19 +1,37 @@
import "dotenv/config";
import { hash } from "@node-rs/argon2";
import { PrismaPg } from "@prisma/adapter-pg";
import { PrismaClient } from "../src/generated/prisma/client.js";
import { Betriebsmodus, Rolle } from "../src/generated/prisma/enums.js";
import { Betriebsmodus } from "../src/generated/prisma/enums.js";
import { demoKundenSaeen } from "./seed-daten/dienstleistung.js";
import {
adminAnlegen,
eigeneOrganisationAnlegen,
einstellungAnlegen,
modusLesen,
} from "./seed-daten/grundbestand.js";
import { beispielQuelleSaeen, stammdatenSaeen } from "./seed-daten/stammdaten.js";
import { typVorgabenSaeen, vorlagenSaeen } from "./seed-daten/vorlagen.js";
/**
* Grundbestand, ohne den die Anwendung nicht bedienbar waere: der Betriebsmodus,
* die eigene Organisation und ein Administrator.
* Der Seed: was eine frische Installation braucht, um bedienbar zu sein (#017).
*
* Der Seed ist idempotent – zweimal hintereinander aufgerufen aendert der zweite
* Lauf nichts. Er laeuft im Compose-Stand bewusst NICHT automatisch mit, sondern
* ist ein dokumentierter Handgriff (siehe README).
*
* Die sieben Vorlagen, die Beispiel-Mitarbeiter und die Demo-Kunden kommen mit
* #017 dazu.
* Angelegt werden: Betriebsmodus samt Namensregeln, die eigene Organisation, ein
* Administrator, die sieben Vorlagen als veroeffentlichte Version 1 und ein
* Beispielbestand aus Abteilungen, Personen, Geraeten und Tags. Im Modus INTERN
* haengt der an der eigenen Organisation und bekommt eine **deaktivierte**
* Beispiel-Verzeichnisquelle dazu; im Modus DIENSTLEISTUNG haengt er an den
* Demo-Kunden — „erzeugt stattdessen“, so steht es im Auftrag.
*
* Die einzelnen Schritte liegen unter `prisma/seed-daten/`, damit Tests sie
* einzeln aufrufen koennen: diese Datei fuehrt beim Import den ganzen Seed aus.
*
* Was schon da ist, bleibt, wie es ist. Der Seed richtet ein; er raeumt nicht auf
* und stellt nichts zurueck. Wer eine Vorlage bearbeitet oder eine Ziel-OU
* geaendert hat, bekommt sie beim naechsten Lauf nicht ueberschrieben.
*/
const url = process.env["DATABASE_URL"];
@@ -23,84 +41,58 @@ if (!url) {
const prisma = new PrismaClient({ adapter: new PrismaPg({ connectionString: url }) });
/** Akzeptiert den im Auftrag dokumentierten Wert SERVICE als Alias. */
function modusLesen(): Betriebsmodus {
const roh = (process.env["SEED_MODE"] ?? "INTERN").trim().toUpperCase();
if (roh === "DIENSTLEISTUNG" || roh === "SERVICE") return Betriebsmodus.DIENSTLEISTUNG;
if (roh === "INTERN") return Betriebsmodus.INTERN;
throw new Error(`SEED_MODE kennt nur INTERN und DIENSTLEISTUNG (Alias SERVICE), nicht "${roh}".`);
}
async function einstellungAnlegen(modus: Betriebsmodus): Promise<void> {
// Nur anlegen, nicht ueberschreiben: ein spaeterer Seed-Lauf darf einen im
// Betrieb umgeschalteten Modus nicht zuruecksetzen.
const vorhanden = await prisma.einstellung.findUnique({ where: { id: "global" } });
if (vorhanden) {
console.log(` Einstellung: vorhanden, Modus ${vorhanden.modus} bleibt`);
return;
}
await prisma.einstellung.create({ data: { id: "global", modus } });
console.log(` Einstellung: angelegt, Modus ${modus}`);
}
async function eigeneOrganisationAnlegen(): Promise<string> {
const name = process.env["SEED_ORG_NAME"]?.trim() || "Eigene Organisation";
const kuerzel = (process.env["SEED_ORG_KUERZEL"]?.trim() || "EIGEN").toUpperCase();
if (!/^[A-Z0-9]{2,6}$/.test(kuerzel)) {
throw new Error(`SEED_ORG_KUERZEL muss 2 bis 6 Grossbuchstaben oder Ziffern sein, nicht "${kuerzel}".`);
}
// Bewusst nicht aktualisieren: wer Name oder Kuerzel im Betrieb geaendert hat,
// soll das nicht beim naechsten Seed-Lauf zurueckbekommen. Geaenderte Werte in
// der Umgebung bleiben deshalb wirkungslos, sobald die Zeile existiert.
const vorhanden = await prisma.kunde.findFirst({ where: { istEigeneOrganisation: true } });
if (vorhanden) {
console.log(` Eigene Organisation: vorhanden (${vorhanden.name})`);
return vorhanden.id;
}
const kunde = await prisma.kunde.create({
data: { name, kuerzel, istEigeneOrganisation: true },
});
console.log(` Eigene Organisation: angelegt (${kunde.name}, ${kunde.kuerzel})`);
return kunde.id;
}
async function adminAnlegen(): Promise<void> {
const email = process.env["SEED_ADMIN_EMAIL"]?.trim().toLowerCase();
const passwort = process.env["SEED_ADMIN_PASSWORD"];
if (!email || !passwort) {
console.log(" Administrator: uebersprungen (SEED_ADMIN_EMAIL oder SEED_ADMIN_PASSWORD fehlt)");
return;
}
// Gesucht wird ueber die Adresse: eine geaenderte SEED_ADMIN_EMAIL legt also
// einen zweiten Administrator an, statt den ersten umzubenennen. Das ist
// gewollt – umbenennen waere eine Aenderung an einem Konto, das laengst benutzt
// sein kann.
const vorhanden = await prisma.benutzer.findUnique({ where: { email } });
if (vorhanden) {
console.log(` Administrator: vorhanden (${email})`);
return;
}
// Das Passwort wird nur als argon2-Hash gespeichert, nie im Klartext.
await prisma.benutzer.create({
data: {
email,
name: "Administrator",
rolle: Rolle.ADMIN,
passwortHash: await hash(passwort),
aktiv: true,
},
});
console.log(` Administrator: angelegt (${email})`);
}
async function main(): Promise<void> {
const modus = modusLesen();
console.log("Seed laeuft:");
await einstellungAnlegen(modus);
await eigeneOrganisationAnlegen();
await adminAnlegen();
await einstellungAnlegen(prisma, modus);
const eigeneId = await eigeneOrganisationAnlegen(prisma);
await adminAnlegen(prisma);
const vorlagen = await vorlagenSaeen(prisma);
const neueVorlagen = vorlagen.filter((eintrag) => eintrag.neu).length;
console.log(` Vorlagen: ${vorlagen.length} vorhanden, davon ${neueVorlagen} neu angelegt`);
const typVorgaben = await typVorgabenSaeen(prisma, vorlagen);
console.log(` Schnellstart je Geraetetyp: ${typVorgaben} neu`);
/*
* Der Beispielbestand haengt an dem, der ihn im jeweiligen Modus braucht:
*
* - INTERN: an der eigenen Organisation. Sie ist das Haus.
* - DIENSTLEISTUNG: an der Musterfirma. Der Auftrag verlangt, dass `LZ-7K3F9Q`
* dort auf `MUS-PC-001` fuehrt — „erzeugt **stattdessen**“. Beides zugleich
* ginge ohnehin nicht: ein Tag-Code gehoert genau einem Geraet.
*/
if (modus === Betriebsmodus.INTERN) {
const bestand = await stammdatenSaeen(prisma, eigeneId);
console.log(
` Beispielbestand: ${bestand.abteilungen} Abteilungen, ${bestand.personen} Personen, ` +
`${bestand.geraete} Geraete, ${bestand.offeneTags} freie Tags (jeweils neu)`,
);
// Das Verzeichnis gibt es nur im Modus INTERN (#008) – im Modus
// DIENSTLEISTUNG waere die Beispielquelle eine Zeile, die niemand sieht.
const quelle = await beispielQuelleSaeen(prisma, eigeneId);
console.log(
quelle
? " Verzeichnisquelle: Beispiel angelegt (deaktiviert, Adresse und Passwort ersetzen)"
: " Verzeichnisquelle: vorhanden",
);
} else {
const kunden = await demoKundenSaeen(prisma);
for (const kunde of kunden) {
const bestand = kunde.bestand;
const zahlen = bestand
? `${bestand.abteilungen} Abteilungen, ${bestand.personen} Personen, ` +
`${bestand.geraete} Geraete, ${bestand.offeneTags} freie Tags (jeweils neu)`
: "kein Bestand";
console.log(
` Kunde ${kunde.kuerzel}: ${kunde.neu ? "angelegt" : "vorhanden"} ` +
`(${kunde.kunde}, ${kunde.hostname}) – ${zahlen}`,
);
}
}
console.log("Seed fertig.");
}