import { importSpielplan } from '../utils/spielplan-import.js' import { importLeagueTables } from '../utils/spielklassen-tables-import.js' import { publishImportedSpielplan } from '../utils/spielplan-publish.js' import { info as loggerInfo, error as loggerError } from '../utils/logger.js' import { cleanupPasswordResetLogs } from '../utils/password-reset-log.js' const TIME_ZONE = 'Europe/Berlin' const RUN_HOUR = 7 const RUN_MINUTE = 0 const MAX_TIMEOUT = 2_147_483_647 let timer = null let running = false function getTimeParts(date) { const parts = new Intl.DateTimeFormat('en-CA', { timeZone: TIME_ZONE, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).formatToParts(date) return Object.fromEntries(parts.map((part) => [part.type, part.value])) } function getTimeZoneOffset(date) { const parts = getTimeParts(date) const zonedAsUtc = Date.UTC( Number(parts.year), Number(parts.month) - 1, Number(parts.day), Number(parts.hour), Number(parts.minute), Number(parts.second) ) return zonedAsUtc - date.getTime() } function zonedDateToUtc(year, month, day, hour, minute) { const utcGuess = new Date(Date.UTC(year, month - 1, day, hour, minute, 0)) const offset = getTimeZoneOffset(utcGuess) return new Date(utcGuess.getTime() - offset) } function nextRunAt(now = new Date()) { const parts = getTimeParts(now) let year = Number(parts.year) let month = Number(parts.month) let day = Number(parts.day) let candidate = zonedDateToUtc(year, month, day, RUN_HOUR, RUN_MINUTE) if (candidate <= now) { const nextDay = zonedDateToUtc(year, month, day + 1, 12, 0) const nextParts = getTimeParts(nextDay) year = Number(nextParts.year) month = Number(nextParts.month) day = Number(nextParts.day) candidate = zonedDateToUtc(year, month, day, RUN_HOUR, RUN_MINUTE) } return candidate } async function runDailyJobs(reason, skipSpielplanImport = false) { if (running) return running = true try { try { const cleanup = await cleanupPasswordResetLogs() loggerInfo(`[password-reset-log] ${reason}: Bereinigung abgeschlossen`, cleanup) } catch (error) { loggerError('[password-reset-log] Bereinigung fehlgeschlagen:', { error }) } if (skipSpielplanImport) { return } const spielplan = await importSpielplan() loggerInfo(`[spielplan-import] ${reason}: ${spielplan.matchCount} Spiele importiert`, { range: `${spielplan.source.season.dateStart} - ${spielplan.source.season.dateEnd}` }) const published = await publishImportedSpielplan({ inputPath: spielplan.jsonFile }) loggerInfo(`[spielplan-import] ${reason}: Spielplan publiziert`, { season: published.seasonSlug, internalPath: published.internalSeasonPath }) try { const tables = await importLeagueTables() loggerInfo(`[spielplan-import] ${reason}: ${tables.importedCount}/${tables.teamCount} Tabellen importiert`, { season: tables.seasonSlug, outputFile: tables.outputFile, errors: tables.errorCount }) } catch (error) { loggerError('[spielplan-import] Tabellen-Import fehlgeschlagen:', { error }) } } catch (error) { loggerError('[spielplan-import] Import fehlgeschlagen:', { error }) } finally { running = false } } function scheduleNext(skipSpielplanImport = false) { const runAt = nextRunAt() const delay = Math.min(Math.max(runAt.getTime() - Date.now(), 1_000), MAX_TIMEOUT) timer = setTimeout(async () => { await runDailyJobs('taeglicher Lauf', skipSpielplanImport) scheduleNext(skipSpielplanImport) }, delay) timer.unref?.() loggerInfo('[spielplan-import] Naechster Lauf', { runAt: runAt.toISOString(), tz: TIME_ZONE, time: `${String(RUN_HOUR).padStart(2, '0')}:${String(RUN_MINUTE).padStart(2, '0')}` }) } export default defineNitroPlugin((nitroApp) => { const skipSpielplanImport = process.env.SPIELPLAN_IMPORT_DISABLED === 'true' if (skipSpielplanImport) { loggerInfo('[spielplan-import] Import deaktiviert; Passwort-Reset-Log-Bereinigung bleibt aktiv') } scheduleNext(skipSpielplanImport) if (process.env.SPIELPLAN_IMPORT_RUN_ON_START === 'true') { runDailyJobs('Startlauf', skipSpielplanImport) } nitroApp.hooks.hookOnce('close', () => { if (timer) clearTimeout(timer) }) })