Files
planer/prisma/demo-seed.ts
T
Florian WildeandClaude Opus 5 3c7cdbbbf3 chore: Demo-Daten, Bildaufnahme und Platz in der Kopfleiste
Die Demo-Instanz redet mit niemandem mehr: Solange DEMO_MODUS gilt,
starten die Jobs fuer Zammad, Telegram und den Spiegel gar nicht erst.
Sie ist mit erfundenen Daten gefuellt und steht nur fuer Bilder — ein
Aufruf nach draussen waere bestenfalls sinnlos.

Die Kopfleiste ist breiter als die Inhaltsspalte. Mit dem elften Eintrag
passte sie dort nicht mehr, und "Einstellungen" verschwand
stillschweigend am rechten Rand. Aufgefallen ist das erst im
aufgenommenen Bild.

Der Kartentext beschrieb noch den Weg von vor dem Kopplungscode.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 04:51:37 +02:00

504 lines
14 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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' },
},
]
/**
* Erfundene Aufgaben. Die Fälligkeit steht als Versatz in Tagen zum festen
* Demo-Zeitpunkt, damit die Bilder immer dieselbe Mischung aus überfällig,
* heute und später zeigen.
*/
const AUFGABEN = [
// Überfällig
{
titel: 'Angebot Heizungstausch nachfassen',
bereich: 'Gewerbe',
versatz: -3,
prioritaet: 2,
aufwand: 30,
},
{
titel: 'Rechnung 2026-114 korrigieren',
bereich: 'Gewerbe',
versatz: -1,
prioritaet: 1,
aufwand: 20,
},
// Heute und morgen
{
titel: 'Wochenbericht an die Geschäftsführung',
bereich: 'Firma',
versatz: 0,
prioritaet: 2,
aufwand: 45,
},
{
titel: 'Rückruf Frau Sonnenberg',
bereich: 'Firma',
versatz: 0,
prioritaet: 1,
aufwand: 15,
},
{
titel: 'Zahnarzttermin bestätigen',
bereich: 'Privat',
versatz: 1,
prioritaet: 3,
aufwand: 10,
},
// Später
{
titel: 'Backup-Strategie für den Server überdenken',
bereich: 'Homelab',
versatz: 9,
prioritaet: 4,
aufwand: 120,
},
{
titel: 'Reifen wechseln lassen',
bereich: 'Privat',
versatz: 14,
prioritaet: 3,
aufwand: 60,
},
// Ohne Termin
{
titel: 'Ablage im Keller sortieren',
bereich: 'Privat',
versatz: null,
prioritaet: 5,
aufwand: 180,
},
// Erledigt
{
titel: 'Steuerunterlagen zusammenstellen',
bereich: 'Gewerbe',
versatz: -5,
prioritaet: 3,
aufwand: 90,
status: 'ERLEDIGT' as const,
},
]
/** Liegt noch in der Inbox – ohne Bereich. */
const INBOX = [
{ titel: 'Sicherungskasten beschriften' },
{ titel: 'Angebot Netzwerkschrank einholen' },
]
const IDEEN = [
{ titel: 'Newsletter für Bestandskunden' },
{ titel: 'Werkstatt-Podcast aufnehmen' },
{ titel: 'Regale im Lager neu beschriften' },
]
const PROJEKT_AUFGABEN = [
{
titel: 'Anforderungen sammeln',
typ: 'ARBEITSPAKET' as const,
status: 'ERLEDIGT' as const,
},
{
titel: 'Entwurf abstimmen',
typ: 'ARBEITSPAKET' as const,
status: 'ERLEDIGT' as const,
},
{ titel: 'Texte schreiben', typ: 'ARBEITSPAKET' as const, status: 'OFFEN' as const },
{ titel: 'Fotos beauftragen', typ: 'ARBEITSPAKET' as const, status: 'WARTET' as const },
{ titel: 'Seite ist online', typ: 'MEILENSTEIN' as const, status: 'OFFEN' as const },
]
/** Fester Bezugspunkt, passend zu src/server/zeitgeber.ts. */
const DEMO_JETZT = new Date('2026-03-17T08:20:00.000Z')
function tageVersetzt(tage: number): Date {
return new Date(DEMO_JETZT.getTime() + tage * 24 * 60 * 60 * 1000)
}
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)),
},
})
// --- Aufgaben, Ideen und ein Projekt -------------------------------------
await db.aufgabe.deleteMany({})
await db.projekt.deleteMany({})
const bereichsKennung = new Map(
(await db.bereich.findMany({ select: { id: true, name: true } })).map((b) => [
b.name,
b.id,
]),
)
for (const eintrag of AUFGABEN) {
await db.aufgabe.create({
data: {
titel: eintrag.titel,
bereichId: bereichsKennung.get(eintrag.bereich) ?? null,
prioritaet: eintrag.prioritaet,
aufwandMinuten: eintrag.aufwand,
faelligAm: eintrag.versatz === null ? null : tageVersetzt(eintrag.versatz),
status: eintrag.status ?? 'OFFEN',
erledigtAm: eintrag.status === 'ERLEDIGT' ? tageVersetzt(-4) : null,
},
})
}
for (const eintrag of INBOX) {
await db.aufgabe.create({ data: { titel: eintrag.titel } })
}
for (const eintrag of IDEEN) {
await db.aufgabe.create({ data: { titel: eintrag.titel, typ: 'IDEE' } })
}
const projekt = await db.projekt.create({
data: {
name: 'Website neu aufsetzen',
beschreibung:
'Der alte Auftritt ist von 2019 und auf dem Handy kaum lesbar. Fertig ist es, wenn die neue Seite online ist und das Kontaktformular ankommt.',
bereichId: bereichsKennung.get('Gewerbe') ?? null,
zielAm: tageVersetzt(45),
},
})
for (const eintrag of PROJEKT_AUFGABEN) {
await db.aufgabe.create({
data: {
titel: eintrag.titel,
typ: eintrag.typ,
status: eintrag.status,
projektId: projekt.id,
bereichId: bereichsKennung.get('Gewerbe') ?? null,
aufwandMinuten: eintrag.typ === 'MEILENSTEIN' ? null : 120,
erledigtAm: eintrag.status === 'ERLEDIGT' ? tageVersetzt(-7) : null,
},
})
}
// --- Kalenderkonto und erfundene Termine ---------------------------------
// Die Tokens sind absichtlich Unsinn: die Screenshot-Instanz spricht mit
// keinem Anbieter, sie soll die Seite nur nicht leer zeigen.
await db.kalenderkonto.deleteMany({})
const kalenderkonto = await db.kalenderkonto.create({
data: {
benutzerId: benutzer.id,
anbieter: 'GRAPH',
anzeigename: 'Firma Outlook',
extKontoId: 'martina.beispiel@firma.invalid',
letzterAbgleich: new Date(DEMO_JETZT.getTime() - 6 * 60_000),
},
})
const TERMINE = [
{ betreff: 'Wochenbesprechung', vonStunde: 8, bisStunde: 9 },
{ betreff: 'Kundengespräch Sonnenberg', vonStunde: 10, bisStunde: 11 },
{ betreff: 'Mittagspause', vonStunde: 11, bisStunde: 12, frei: true },
{ betreff: 'Abnahme Baustelle', vonStunde: 13, bisStunde: 15 },
]
for (const eintrag of TERMINE) {
await db.termin.create({
data: {
kalenderkontoId: kalenderkonto.id,
extId: `demo-${eintrag.betreff.toLowerCase().replace(/[^a-z]+/g, '-')}`,
betreff: eintrag.betreff,
beginn: new Date(Date.UTC(2026, 2, 17, eintrag.vonStunde - 1, 0)),
ende: new Date(Date.UTC(2026, 2, 17, eintrag.bisStunde - 1, 0)),
zeigtAls: eintrag.frei ? 'FREI' : 'GEBUCHT',
},
})
}
await db.termin.create({
data: {
kalenderkontoId: kalenderkonto.id,
extId: 'demo-brueckentag',
betreff: 'Brückentag',
beginn: new Date(Date.UTC(2026, 2, 20, 0, 0)),
ende: new Date(Date.UTC(2026, 2, 21, 0, 0)),
ganztaegig: true,
zeigtAls: 'ABWESEND',
},
})
// --- Zeitblöcke: eingeplante Aufgaben im Kalender -------------------------
await db.zeitblock.deleteMany({})
const einzuplanen = await db.aufgabe.findMany({
where: {
titel: {
in: [
'Wochenbericht an die Geschäftsführung',
'Angebot Heizungstausch nachfassen',
],
},
},
select: { id: true, titel: true, aufwandMinuten: true },
})
let blockStunde = 15
for (const aufgabe of einzuplanen) {
const beginn = new Date(Date.UTC(2026, 2, 17, blockStunde - 1, 0))
await db.zeitblock.create({
data: {
aufgabeId: aufgabe.id,
kalenderkontoId: kalenderkonto.id,
beginn,
ende: new Date(beginn.getTime() + (aufgabe.aufwandMinuten ?? 60) * 60_000),
extId: `demo-block-${aufgabe.id}`,
hash: 'demo',
},
})
await db.aufgabe.update({ where: { id: aufgabe.id }, data: { status: 'GEPLANT' } })
blockStunde += 2
}
// Eine Aufgabe, die von außen kam – zeigt die Herkunft in der Liste.
await db.aufgabe.create({
data: {
titel: 'Störung Klimagerät Serverraum prüfen',
bereichId: bereichsKennung.get('Firma') ?? null,
prioritaet: 2,
aufwandMinuten: 60,
faelligAm: tageVersetzt(2),
quelle: 'ZAMMAD',
quelleRef: 'demo-38421',
quelleUrl: 'https://zammad.example.invalid/ticket/38421',
},
})
// --- Benachrichtigungen ---------------------------------------------------
// Für die Bilder eingeschaltet. Eine leere Karte zeigt nichts von dem,
// was das Kapitel erklärt.
await db.benachrichtigungseinstellung.upsert({
where: { benutzerId: benutzer.id },
update: {},
create: {
benutzerId: benutzer.id,
anlaesse: 'tagesueberblick,faelligkeit,zeitblock',
perPush: true,
perMail: true,
perTelegram: true,
ueberblickUm: 7 * 60,
ruheVon: 20 * 60,
ruheBis: 7 * 60,
ruheAmWochenende: false,
zeitblockVorlaufMinuten: 10,
},
})
// Kein Push-Gerät in den Demo-Daten: Seine Schlüssel liegen
// verschlüsselt, und der Versand entfernt jedes Gerät, dessen Schlüssel er
// nicht lesen kann. Ein erfundener Eintrag verschwände beim ersten Lauf
// wieder — die Bilder zeigen deshalb den Zustand vor der ersten Anmeldung.
// --- Integrationen --------------------------------------------------------
await db.integrationszustand.deleteMany({})
await db.integrationszustand.create({
data: {
quelle: 'TELEGRAM',
merker: '4711',
letzterLauf: new Date(Date.UTC(2026, 2, 17, 8, 18)),
letzterErfolg: new Date(Date.UTC(2026, 2, 17, 8, 18)),
},
})
await db.integrationszustand.create({
data: {
quelle: 'SPIEGEL',
letzterLauf: new Date(Date.UTC(2026, 2, 17, 8, 15)),
letzterErfolg: new Date(Date.UTC(2026, 2, 17, 8, 15)),
},
})
await db.telegramChat.deleteMany({})
await db.telegramChat.create({
data: {
// Erfunden. Der Bot ist ohnehin abgeschaltet, solange DEMO_MODUS gilt.
chatId: BigInt(147258369),
benutzerId: benutzer.id,
bezeichnung: 'Martina',
erstelltAm: new Date(Date.UTC(2026, 1, 3, 8, 25)),
zuletztBenutzt: new Date(Date.UTC(2026, 2, 17, 6, 40)),
},
})
// Dienstliche Belegung, wie sie aus der Firma hereinkaeme: ohne Titel.
await db.spiegelbelegung.deleteMany({})
for (const [tag, stunde, dauer] of [
[17, 9, 2],
[18, 13, 3],
[19, 8, 1],
] as const) {
await db.spiegelbelegung.create({
data: {
quelleRef: `demo-spiegel-${tag}-${stunde}`,
beginn: new Date(Date.UTC(2026, 2, tag, stunde - 1, 0)),
ende: new Date(Date.UTC(2026, 2, tag, stunde - 1 + dauer, 0)),
},
})
}
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)
})