import * as Sentry from "@sentry/nextjs"; import { Cron } from "croner"; import { env } from "../src/env"; import { logger } from "../src/lib/logger"; import { prisma } from "../src/lib/prisma"; function initWorkerSentry(): void { const dsn = process.env.SENTRY_DSN; if (!dsn || process.env.NODE_ENV !== "production") return; Sentry.init({ dsn, environment: process.env.NODE_ENV, release: process.env.APP_VERSION, tracesSampleRate: 0.05, }); logger.info("Sentry initialized for jobs worker", { module: "jobs" }); } function captureWorkerError(err: unknown, context: string): void { logger.error(context, { module: "jobs", err: err instanceof Error ? err.message : String(err), }); if (process.env.SENTRY_DSN) { Sentry.captureException(err); } } async function backupEmulatorJar(): Promise { if (!env.EMULATOR_JAR_PATH || !env.EMULATOR_BACKUP_DIR) return; const { copyFileSync, mkdirSync, readdirSync, unlinkSync, existsSync } = await import("node:fs"); const { resolve } = await import("node:path"); const timestamp = new Date().toISOString().slice(0, 19).replace(/[T:]/g, "-"); const backupFile = resolve( env.EMULATOR_BACKUP_DIR, `emulator-${timestamp}.jar`, ); if (!existsSync(env.EMULATOR_BACKUP_DIR)) { mkdirSync(env.EMULATOR_BACKUP_DIR, { recursive: true }); } try { copyFileSync(env.EMULATOR_JAR_PATH, backupFile); logger.info("Backed up emulator JAR", { module: "jobs", backupFile, }); const keep = env.EMULATOR_BACKUP_KEEP ?? 7; const files = readdirSync(env.EMULATOR_BACKUP_DIR) .filter((f) => f.startsWith("emulator-") && f.endsWith(".jar")) .sort() .reverse(); for (let i = keep; i < files.length; i++) { unlinkSync(resolve(env.EMULATOR_BACKUP_DIR, files[i])); logger.info("Rotated out old backup", { module: "jobs", file: files[i], }); } } catch (err) { captureWorkerError(err, "JAR backup failed"); } } async function cleanupOldLogs(): Promise { try { const cutoff = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); await prisma.websiteLoginLogs.deleteMany({ where: { createdAt: { lt: cutoff } }, }); logger.info("Cleaned up login logs older than 30 days", { module: "jobs" }); } catch (err) { captureWorkerError(err, "Log cleanup failed"); } } async function cleanupOldSessions(): Promise { try { const cutoff = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); await prisma.passwordReset.deleteMany({ where: { createdAt: { lt: cutoff } }, }); logger.info("Cleaned up expired password reset tokens", { module: "jobs", }); } catch (err) { captureWorkerError(err, "Session cleanup failed"); } } async function main() { initWorkerSentry(); logger.info("Worker started", { module: "jobs" }); if (env.EMULATOR_JAR_PATH && env.EMULATOR_BACKUP_DIR) { new Cron("0 3 * * *", () => { backupEmulatorJar().catch((e) => captureWorkerError(e, "Backup error")); }); logger.info("Scheduled: emulator JAR backup (daily 03:00)", { module: "jobs", }); } new Cron("0 4 * * *", () => { Promise.all([cleanupOldLogs(), cleanupOldSessions()]).catch((e) => captureWorkerError(e, "Cleanup error"), ); }); logger.info("Scheduled: old data cleanup (daily 04:00)", { module: "jobs" }); await Promise.all([ backupEmulatorJar(), cleanupOldLogs(), cleanupOldSessions(), ]); } main().catch((err) => { captureWorkerError(err, "Fatal"); process.exit(1); });