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 { alteNachweiseEntfernen, benachrichtigungenVerschicken, } from '@/server/benachrichtigung/versand' import { zammadAbgleichen } from '@/server/integration/zammad' import { alteKopplungenEntfernen, telegramAbholen } from '@/server/integration/telegram' import { spiegelSenden } from '@/server/integration/spiegel' import { umgebung } from '@/server/umgebung' 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 * * *' /** * Alle zehn Minuten nachsehen, ob etwas zu melden ist. * * Der Takt passt zu den Toleranzfenstern in src/lib/benachrichtigung.ts: Ein * Anlass, der in einem 15-Minuten-Fenster liegt, wird sicher getroffen, aber * dank Wiederholungssperre nur einmal gemeldet. */ const MELDUNGS_PLAN = '*/10 * * * *' /** * Integrationen laufen im eigenen Takt. * * Telegram oefter als der Rest: Wer dem Bot etwas schickt, erwartet die * Aufgabe gleich in der Liste, nicht in einer Viertelstunde. Zammad und der * Spiegel duerfen traeger sein — dort aendert sich nichts im Minutentakt. */ const TELEGRAM_PLAN = '*/2 * * * *' const ZAMMAD_PLAN = '*/15 * * * *' const SPIEGEL_PLAN = '*/15 * * * *' /** * Wer im Protokoll steht, wenn niemand geklickt hat. * * Ein Job hat keinen Benutzer. Statt den Eintrag wegzulassen, traegt er * sichtbar den Dienst ein — sonst sieht es aus, als sei nichts passiert. */ const DIENST = { id: null, email: 'dienst@intern' } 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(MELDUNGS_PLAN, () => { void benachrichtigungenVerschicken() .then((bericht) => { // Nur Zahlen, nie Empfänger oder Inhalte. if (bericht.gesendet > 0) { console.warn(`Benachrichtigungen: ${bericht.gesendet} verschickt.`) } }) .catch((fehler: unknown) => { console.error('Benachrichtigungen abgebrochen:', fehler) }) }) const einst = umgebung() if (einst.zammadAktiv) { cron.schedule(ZAMMAD_PLAN, () => { void zammadAbgleichen(DIENST) .then((bericht) => { if (bericht.angelegt > 0 || bericht.geschlossen > 0) { console.warn( `Zammad: ${bericht.angelegt} neu, ${bericht.geschlossen} geschlossen.`, ) } }) .catch((fehler: unknown) => { console.error('Zammad-Abgleich abgebrochen:', fehler) }) }) } if (einst.telegramAktiv) { cron.schedule(TELEGRAM_PLAN, () => { void telegramAbholen(DIENST) .then((bericht) => { if (bericht.verworfen > 0) { // Nie die Chatkennung: Sie gehoert zu einer Person. console.warn(`Telegram: ${bericht.verworfen} Nachricht(en) verworfen.`) } }) .catch((fehler: unknown) => { console.error('Telegram-Abholen abgebrochen:', fehler) }) }) } if (einst.spiegelSendenAktiv) { cron.schedule(SPIEGEL_PLAN, () => { void spiegelSenden().catch((fehler: unknown) => { console.error('Spiegel-Versand abgebrochen:', fehler) }) }) } cron.schedule(AUFRAEUM_PLAN, () => { void Promise.all([ alteTermineEntfernen(), abgelaufeneChallengesEntfernen(jetzt()), alteNachweiseEntfernen(jetzt()), alteKopplungenEntfernen(jetzt()), ]) .then(() => undefined) .catch((fehler: unknown) => { console.error('Aufräumen abgebrochen:', fehler) }) }) }