diff --git a/src/instrumentation.ts b/src/instrumentation.ts new file mode 100644 index 0000000..91c9d1b --- /dev/null +++ b/src/instrumentation.ts @@ -0,0 +1,15 @@ +/** + * Wird von Next einmal beim Hochfahren des Servers ausgeführt. + * + * Nur in der Node-Laufzeit: im Edge-Umfeld gibt es weder Prisma noch + * node-cron. Der Demo-Modus startet bewusst keine Jobs — die + * Screenshot-Instanz soll sich nicht selbst verändern, während sie + * abfotografiert wird. + */ +export async function register(): Promise { + if (process.env.NEXT_RUNTIME !== 'nodejs') return + if (process.env.DEMO_MODUS === 'true') return + + const { jobsStarten } = await import('@/server/jobs') + jobsStarten() +} diff --git a/src/server/jobs.ts b/src/server/jobs.ts new file mode 100644 index 0000000..a7df519 --- /dev/null +++ b/src/server/jobs.ts @@ -0,0 +1,72 @@ +import 'server-only' +import cron from 'node-cron' +import { db } from '@/server/db' +import { abgleichFenster, kontoAbgleichen } from '@/server/kalender/abgleich' +import { abgelaufeneChallengesEntfernen } from '@/server/auth/passkey' +import { jetzt } from '@/server/zeitgeber' + +/** + * Hintergrundjobs. + * + * Werden einmal je Prozess gestartet. Im Entwicklungsbetrieb lädt Next Module + * mehrfach neu — deshalb der Riegel über globalThis, sonst liefen nach ein + * paar Änderungen ein Dutzend Zeitpläne parallel. + */ + +const globalerBereich = globalThis as unknown as { planerJobs?: boolean } + +/** Alle 15 Minuten abgleichen; öfter bringt nichts und kostet nur Aufrufe. */ +const ABGLEICH_PLAN = '*/15 * * * *' +/** Einmal nachts aufräumen. */ +const AUFRAEUM_PLAN = '17 3 * * *' + +async function alleKontenAbgleichen(): Promise { + const zeitpunkt = jetzt() + const konten = await db.kalenderkonto.findMany({ + where: { aktiv: true }, + select: { id: true }, + }) + + let ok = 0 + let gescheitert = 0 + for (const konto of konten) { + const bericht = await kontoAbgleichen(konto.id, zeitpunkt) + if (bericht.ok) ok++ + else gescheitert++ + } + + if (gescheitert > 0) { + // Nur Zahlen, keine Namen und keine Termininhalte. + console.warn( + `Kalenderabgleich: ${ok} Konto/Konten erfolgreich, ${gescheitert} fehlgeschlagen.`, + ) + } +} + +/** + * Entfernt Termine, die aus dem Abgleichfenster herausgewandert sind. + * Ohne das bliebe der Spiegel für immer stehen. + */ +async function alteTermineEntfernen(): Promise { + const fenster = abgleichFenster(jetzt()) + await db.termin.deleteMany({ where: { ende: { lt: fenster.von } } }) +} + +export function jobsStarten(): void { + if (globalerBereich.planerJobs) return + globalerBereich.planerJobs = true + + cron.schedule(ABGLEICH_PLAN, () => { + void alleKontenAbgleichen().catch((fehler: unknown) => { + console.error('Kalenderabgleich abgebrochen:', fehler) + }) + }) + + cron.schedule(AUFRAEUM_PLAN, () => { + void Promise.all([alteTermineEntfernen(), abgelaufeneChallengesEntfernen(jetzt())]) + .then(() => undefined) + .catch((fehler: unknown) => { + console.error('Aufräumen abgebrochen:', fehler) + }) + }) +}