chore: Screenshot-Kette und Demo-Seed
npm run screenshots startet eine Wegwerf-Instanz in einem eigenen Compose-Projekt mit eigener Datenbank, spielt ausschließlich erfundene Daten ein, fährt die Seiten mit Playwright ab und räumt am Ende alles wieder weg. Je Seite entstehen vier Bilder: hell und dunkel, Schreibtisch und Handy. Die Uhr steht fest, damit sich zwischen zwei Läufen nur das ändert, was sich wirklich geändert hat. Der Demo-Seed verweigert den Dienst ohne DEMO_MODUS=true — er würde sonst erfundene Daten in eine echte Datenbank schreiben. Test- und Werkzeugordner sind jetzt aus dem Docker-Kontext ausgenommen. Ein Typfehler in scripts/screenshots.ts hatte den Bau des Abbilds blockiert, obwohl die Datei dort nichts zu suchen hat. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e3390c88f4
commit
a88273117c
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* Daten für die Screenshot-Instanz.
|
||||
*
|
||||
* Alles hier ist frei erfunden. Diese Datei darf niemals gegen eine
|
||||
* Datenbank mit echten Daten laufen — deshalb bricht sie ab, wenn
|
||||
* DEMO_MODUS nicht ausdrücklich auf true steht.
|
||||
*
|
||||
* Aufruf: über npm run screenshots, nicht von Hand.
|
||||
*/
|
||||
import 'dotenv/config'
|
||||
import { PrismaPg } from '@prisma/adapter-pg'
|
||||
import bcrypt from 'bcryptjs'
|
||||
import { PrismaClient } from '../src/generated/prisma/client.js'
|
||||
|
||||
if (process.env.DEMO_MODUS !== 'true') {
|
||||
console.error(
|
||||
'Der Demo-Seed läuft nur mit DEMO_MODUS=true. Das ist Absicht: er würde ' +
|
||||
'sonst erfundene Daten in eine echte Datenbank schreiben.',
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const verbindung = process.env.DATABASE_URL
|
||||
if (!verbindung) {
|
||||
console.error('DATABASE_URL ist nicht gesetzt.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const db = new PrismaClient({ adapter: new PrismaPg({ connectionString: verbindung }) })
|
||||
|
||||
export const DEMO_EMAIL = 'martina.beispiel@planer.invalid'
|
||||
export const DEMO_PASSWORT = 'demo-instanz-nur-fuer-bilder'
|
||||
|
||||
const BEREICHE = [
|
||||
{
|
||||
name: 'Firma',
|
||||
farbe: '#2563eb',
|
||||
reihenfolge: 1,
|
||||
arbeitsfensterVon: 450,
|
||||
arbeitsfensterBis: 990,
|
||||
kapazitaetMo: 360,
|
||||
kapazitaetDi: 360,
|
||||
kapazitaetMi: 360,
|
||||
kapazitaetDo: 360,
|
||||
kapazitaetFr: 240,
|
||||
kapazitaetSa: 0,
|
||||
kapazitaetSo: 0,
|
||||
},
|
||||
{
|
||||
name: 'Gewerbe',
|
||||
farbe: '#7c3aed',
|
||||
reihenfolge: 2,
|
||||
arbeitsfensterVon: 1020,
|
||||
arbeitsfensterBis: 1260,
|
||||
kapazitaetMo: 90,
|
||||
kapazitaetDi: 90,
|
||||
kapazitaetMi: 90,
|
||||
kapazitaetDo: 90,
|
||||
kapazitaetFr: 60,
|
||||
kapazitaetSa: 180,
|
||||
kapazitaetSo: 0,
|
||||
},
|
||||
{
|
||||
name: 'Privat',
|
||||
farbe: '#16a34a',
|
||||
reihenfolge: 3,
|
||||
arbeitsfensterVon: 1020,
|
||||
arbeitsfensterBis: 1320,
|
||||
kapazitaetMo: 60,
|
||||
kapazitaetDi: 60,
|
||||
kapazitaetMi: 60,
|
||||
kapazitaetDo: 60,
|
||||
kapazitaetFr: 120,
|
||||
kapazitaetSa: 240,
|
||||
kapazitaetSo: 240,
|
||||
},
|
||||
{
|
||||
name: 'Homelab',
|
||||
farbe: '#ea580c',
|
||||
reihenfolge: 4,
|
||||
arbeitsfensterVon: 1140,
|
||||
arbeitsfensterBis: 1380,
|
||||
kapazitaetMo: 0,
|
||||
kapazitaetDi: 60,
|
||||
kapazitaetMi: 0,
|
||||
kapazitaetDo: 60,
|
||||
kapazitaetFr: 0,
|
||||
kapazitaetSa: 120,
|
||||
kapazitaetSo: 60,
|
||||
},
|
||||
]
|
||||
|
||||
/** Erfundene Protokolleinträge, damit die Protokollseite nicht leer wirkt. */
|
||||
const PROTOKOLL = [
|
||||
{ aktion: 'bereich.angelegt', objektTyp: 'Bereich', details: { name: 'Homelab' } },
|
||||
{
|
||||
aktion: 'bereich.geaendert',
|
||||
objektTyp: 'Bereich',
|
||||
details: { vorher: 'Nebenerwerb', nachher: 'Gewerbe' },
|
||||
},
|
||||
{
|
||||
aktion: 'passkey.angelegt',
|
||||
objektTyp: 'Passkey',
|
||||
details: { bezeichnung: 'Diensthandy' },
|
||||
},
|
||||
]
|
||||
|
||||
async function main(): Promise<void> {
|
||||
console.log('Demo-Daten einspielen.')
|
||||
|
||||
for (const bereich of BEREICHE) {
|
||||
await db.bereich.upsert({
|
||||
where: { name: bereich.name },
|
||||
update: bereich,
|
||||
create: bereich,
|
||||
})
|
||||
}
|
||||
|
||||
const benutzer = await db.benutzer.upsert({
|
||||
where: { email: DEMO_EMAIL },
|
||||
update: { rolle: 'ADMIN', name: 'Martina Beispiel' },
|
||||
create: {
|
||||
email: DEMO_EMAIL,
|
||||
name: 'Martina Beispiel',
|
||||
rolle: 'ADMIN',
|
||||
passwortHash: await bcrypt.hash(DEMO_PASSWORT, 10),
|
||||
},
|
||||
})
|
||||
|
||||
await db.auditEintrag.deleteMany({})
|
||||
// Feste Zeitpunkte, damit die Bilder zwischen zwei Läufen gleich bleiben.
|
||||
let versatz = 0
|
||||
for (const eintrag of PROTOKOLL) {
|
||||
versatz += 37
|
||||
await db.auditEintrag.create({
|
||||
data: {
|
||||
zeitpunkt: new Date(Date.UTC(2026, 2, 17, 9, versatz % 60)),
|
||||
benutzerId: benutzer.id,
|
||||
benutzerEmail: benutzer.email,
|
||||
aktion: eintrag.aktion,
|
||||
objektTyp: eintrag.objektTyp,
|
||||
details: eintrag.details,
|
||||
instanz: 'daheim',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
await db.passkey.deleteMany({ where: { benutzerId: benutzer.id } })
|
||||
await db.passkey.create({
|
||||
data: {
|
||||
benutzerId: benutzer.id,
|
||||
credentialId: 'demo-passkey-diensthandy',
|
||||
publicKey: Buffer.from([0]),
|
||||
bezeichnung: 'Diensthandy',
|
||||
gesichert: true,
|
||||
erstelltAm: new Date(Date.UTC(2026, 1, 3, 8, 15)),
|
||||
zuletztBenutzt: new Date(Date.UTC(2026, 2, 17, 7, 42)),
|
||||
},
|
||||
})
|
||||
|
||||
console.log(`Demo-Daten stehen bereit. Anmeldung: ${DEMO_EMAIL}`)
|
||||
}
|
||||
|
||||
main()
|
||||
.then(async () => {
|
||||
await db.$disconnect()
|
||||
})
|
||||
.catch(async (fehler: unknown) => {
|
||||
console.error(fehler)
|
||||
await db.$disconnect()
|
||||
process.exit(1)
|
||||
})
|
||||
Reference in New Issue
Block a user