feat(kalender): Hintergrundjobs für Abgleich und Aufräumen

Alle 15 Minuten abgleichen, nachts alte Termine und abgelaufene
WebAuthn-Herausforderungen entfernen. Ohne das Aufräumen bliebe der
Terminspiegel für immer stehen.

Der Riegel über globalThis ist nötig, weil Next im Entwicklungsbetrieb
Module mehrfach lädt — sonst liefen nach ein paar Änderungen ein Dutzend
Zeitpläne parallel. Im Demo-Modus starten gar keine Jobs: die
Screenshot-Instanz soll sich nicht selbst verändern, während sie
abfotografiert wird.

Protokolliert werden nur Zahlen, nie Kontonamen oder Termininhalte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-09-10 18:05:07 +02:00
co-authored by Claude Opus 5
parent 0c67b73a62
commit 42c98c8b29
2 changed files with 87 additions and 0 deletions
+72
View File
@@ -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<void> {
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<void> {
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)
})
})
}