TaskTemplate had zero seed data - a fresh deployment showed a completely empty recurring-task list with no hint of what to even put there. Seeds 12 YEARLY templates, one per month, covering the standard German-beekeeping annual cycle (Frühjahrsdurchsicht, Schwarmkontrolle, Einfütterung, Varroa-Restentmilbung/Oxalsäure, Wintersitz-Kontrolle, etc.) so a new beekeeper gets a ready-made starting point instead of a blank page. Runs once (guarded by a taskTemplate.count() check, same idempotency shape as the existing admin-user seed) and only ever adds rows - admins can edit or deactivate any of them afterward. Co-Authored-By: Claude Sonnet 5 <[email protected]>
245 lines
7.9 KiB
TypeScript
245 lines
7.9 KiB
TypeScript
import 'dotenv/config';
|
|
import * as bcrypt from 'bcryptjs';
|
|
import * as crypto from 'node:crypto';
|
|
import { PrismaPg } from '@prisma/adapter-pg';
|
|
import { Prisma, PrismaClient } from '../generated/prisma';
|
|
|
|
const BCRYPT_SALT_ROUNDS = 12;
|
|
const DEFAULT_ADMIN_EMAIL = '[email protected]';
|
|
|
|
// No fixed fallback password: a fixed literal shipped in source would be a
|
|
// well-known, guessable credential for any deployment that forgets to set
|
|
// SEED_ADMIN_PASSWORD. A random one is generated instead and printed once
|
|
// below, same as an operator-supplied one would be.
|
|
function generateRandomPassword(): string {
|
|
return crypto.randomBytes(18).toString('base64url');
|
|
}
|
|
|
|
// Only printed the moment the account is actually created (first deploy on
|
|
// an empty database), never on later restarts: by then the real admin may
|
|
// have changed the password via /profile, so repeating it would just be
|
|
// stale and would leak credentials into logs indefinitely for no reason.
|
|
function printLoginBanner(email: string, password: string): void {
|
|
const url = process.env.FRONTEND_ORIGIN ?? 'http://localhost:3000';
|
|
const line = '='.repeat(60);
|
|
console.log(line);
|
|
console.log('Seed: Admin-Zugang eingerichtet.');
|
|
console.log(` URL: ${url}`);
|
|
console.log(` Benutzer: ${email}`);
|
|
console.log(` Passwort: ${password}`);
|
|
console.log(line);
|
|
}
|
|
|
|
// Duplicated from src/task-templates/recurrence.ts rather than imported:
|
|
// tsconfig.seed.json scopes this build to rootDir "./prisma" only (its own
|
|
// isolated compile, separate from the main Nest build), so a cross-rootDir
|
|
// import would fail to compile. Only the YEARLY case is needed here.
|
|
function nextYearlyOccurrenceUtc(
|
|
month: number,
|
|
dayOfMonth: number,
|
|
from: Date,
|
|
): Date {
|
|
const year = from.getUTCFullYear();
|
|
const clampedDay = (y: number): number =>
|
|
Math.min(dayOfMonth, new Date(Date.UTC(y, month, 0)).getUTCDate());
|
|
const today = new Date(
|
|
Date.UTC(from.getUTCFullYear(), from.getUTCMonth(), from.getUTCDate()),
|
|
);
|
|
const candidate = new Date(Date.UTC(year, month - 1, clampedDay(year)));
|
|
if (candidate.getTime() >= today.getTime()) {
|
|
return candidate;
|
|
}
|
|
return new Date(Date.UTC(year + 1, month - 1, clampedDay(year + 1)));
|
|
}
|
|
|
|
interface DefaultTaskTemplate {
|
|
title: string;
|
|
description: string;
|
|
priority: 'LOW' | 'NORMAL' | 'HIGH' | 'URGENT';
|
|
month: number;
|
|
dayOfMonth: number;
|
|
}
|
|
|
|
// The standard German-beekeeping annual cycle, one flagship task per month -
|
|
// gives a new deployment a ready-made starter calendar instead of a
|
|
// completely empty task-template list. Admins can edit/deactivate any of
|
|
// these afterward; this only ever runs once (see the count() guard below).
|
|
const DEFAULT_TASK_TEMPLATES: DefaultTaskTemplate[] = [
|
|
{
|
|
title: 'Wintersitz kontrollieren',
|
|
description:
|
|
'Ruhige Kontrolle von außen: Flugloch frei, Gewicht per Anheben schätzen, auf Mäuse-/Spechtschäden achten. Beute möglichst nicht öffnen.',
|
|
priority: 'NORMAL',
|
|
month: 1,
|
|
dayOfMonth: 15,
|
|
},
|
|
{
|
|
title: 'Gemülldiagnose',
|
|
description:
|
|
'Windel auswerten: natürlicher Varroa-Totenfall, Bienensitz und Lage der Wintertraube anhand des Gemülls einschätzen.',
|
|
priority: 'NORMAL',
|
|
month: 2,
|
|
dayOfMonth: 15,
|
|
},
|
|
{
|
|
title: 'Frühjahrsdurchsicht',
|
|
description:
|
|
'Erste Innendurchsicht bei ca. 15°C und ruhigem Wetter: Brutnest, Futtervorrat, Volksstärke und Weiselrichtigkeit prüfen.',
|
|
priority: 'HIGH',
|
|
month: 3,
|
|
dayOfMonth: 15,
|
|
},
|
|
{
|
|
title: 'Volk erweitern',
|
|
description:
|
|
'Zargen/Rähmchen nach Volksstärke erweitern, Baurahmen einhängen, Platz für die Frühtracht schaffen.',
|
|
priority: 'NORMAL',
|
|
month: 4,
|
|
dayOfMonth: 1,
|
|
},
|
|
{
|
|
title: 'Schwarmkontrolle beginnen',
|
|
description:
|
|
'Wöchentlich auf Weiselzellen kontrollieren, bis zum Ende der Schwarmzeit fortführen. Maßnahmen bei Bedarf: Ableger bilden, Zellen brechen.',
|
|
priority: 'HIGH',
|
|
month: 5,
|
|
dayOfMonth: 1,
|
|
},
|
|
{
|
|
title: 'Honigernte vorbereiten',
|
|
description:
|
|
'Reifegrad der Frühtracht prüfen (Spritzprobe), Schleuderplan und Gerätereinigung vorbereiten.',
|
|
priority: 'NORMAL',
|
|
month: 6,
|
|
dayOfMonth: 15,
|
|
},
|
|
{
|
|
title: 'Varroa-Kontrolle nach der Ernte',
|
|
description:
|
|
'Milbenbefall messen (z. B. Puderzuckermethode oder natürlicher Totenfall) und Behandlung planen, sobald die Honigräume abgeräumt sind.',
|
|
priority: 'HIGH',
|
|
month: 7,
|
|
dayOfMonth: 15,
|
|
},
|
|
{
|
|
title: 'Einfütterung beginnen',
|
|
description:
|
|
'Wintervorrat auffüttern (Zielgewicht je nach Beutensystem), Futteraufnahme nach ein bis zwei Wochen kontrollieren.',
|
|
priority: 'HIGH',
|
|
month: 8,
|
|
dayOfMonth: 1,
|
|
},
|
|
{
|
|
title: 'Varroa-Restentmilbung / Winterfestigkeit prüfen',
|
|
description:
|
|
'Zweite Varroabehandlung durchführen, Volksstärke und Weiselrichtigkeit für den Winter beurteilen.',
|
|
priority: 'HIGH',
|
|
month: 9,
|
|
dayOfMonth: 15,
|
|
},
|
|
{
|
|
title: 'Fütterung abschließen, Mäusegitter anbringen',
|
|
description:
|
|
'Letzte Futtergabe, Fluglochkeile bzw. Mäusegitter montieren, Bienenstand winterfest machen.',
|
|
priority: 'NORMAL',
|
|
month: 10,
|
|
dayOfMonth: 1,
|
|
},
|
|
{
|
|
title: 'Winterbehandlung (Oxalsäure)',
|
|
description:
|
|
'Brutfreiheit abwarten und Träufel- oder Sprühbehandlung gegen Varroa durchführen.',
|
|
priority: 'HIGH',
|
|
month: 11,
|
|
dayOfMonth: 15,
|
|
},
|
|
{
|
|
title: 'Jahresrückblick und Gerätewartung',
|
|
description:
|
|
'Saison auswerten, Zargen und Rähmchen reinigen/reparieren, Material für die kommende Saison planen.',
|
|
priority: 'LOW',
|
|
month: 12,
|
|
dayOfMonth: 15,
|
|
},
|
|
];
|
|
|
|
async function seedDefaultTaskTemplates(prisma: PrismaClient): Promise<void> {
|
|
const existingCount = await prisma.taskTemplate.count();
|
|
if (existingCount > 0) {
|
|
return;
|
|
}
|
|
const now = new Date();
|
|
await prisma.taskTemplate.createMany({
|
|
data: DEFAULT_TASK_TEMPLATES.map((template) => ({
|
|
title: template.title,
|
|
description: template.description,
|
|
priority: template.priority,
|
|
recurrenceType: 'YEARLY',
|
|
recurrenceMonth: template.month,
|
|
recurrenceDayOfMonth: template.dayOfMonth,
|
|
nextDueDate: nextYearlyOccurrenceUtc(
|
|
template.month,
|
|
template.dayOfMonth,
|
|
now,
|
|
),
|
|
})),
|
|
});
|
|
console.log(
|
|
`Seed: ${DEFAULT_TASK_TEMPLATES.length} Standard-Aufgabenvorlagen angelegt.`,
|
|
);
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
const prisma = new PrismaClient({
|
|
adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }),
|
|
});
|
|
|
|
await seedDefaultTaskTemplates(prisma);
|
|
|
|
const email = process.env.SEED_ADMIN_EMAIL ?? DEFAULT_ADMIN_EMAIL;
|
|
const password = process.env.SEED_ADMIN_PASSWORD ?? generateRandomPassword();
|
|
const existing = await prisma.user.findUnique({ where: { email } });
|
|
if (existing) {
|
|
console.log(`Seed: admin user ${email} already exists, skipping.`);
|
|
await prisma.$disconnect();
|
|
return;
|
|
}
|
|
|
|
const passwordHash = await bcrypt.hash(password, BCRYPT_SALT_ROUNDS);
|
|
const adminRole = await prisma.role.findUniqueOrThrow({
|
|
where: { key: 'ADMIN' },
|
|
});
|
|
try {
|
|
await prisma.user.create({
|
|
data: {
|
|
email,
|
|
passwordHash,
|
|
name: 'Administrator',
|
|
roleId: adminRole.id,
|
|
},
|
|
});
|
|
printLoginBanner(email, password);
|
|
} catch (error: unknown) {
|
|
// P2002 = unique constraint violation on email: another replica's
|
|
// startup won this race between the existence check above and this
|
|
// create() call. The user now exists either way, so this is a no-op,
|
|
// not a failure - container startup must not crash on it.
|
|
if (
|
|
error instanceof Prisma.PrismaClientKnownRequestError &&
|
|
error.code === 'P2002'
|
|
) {
|
|
console.log(
|
|
`Seed: admin user ${email} was created concurrently, skipping.`,
|
|
);
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
await prisma.$disconnect();
|
|
}
|
|
|
|
main().catch((error: unknown) => {
|
|
console.error(error);
|
|
process.exit(1);
|
|
});
|