// Daily-ish scheduler for the audit archive run. Disabled by default // (ARCHIVE_ENABLED=false) โ€” production turns it on once volumes warrant. // The operator UI can force a run via POST /audit/archive/run regardless // of this flag. import { Injectable, Logger, type OnApplicationBootstrap, type OnModuleDestroy, } from '@nestjs/common' import { ConfigService } from '@nestjs/config' import { ArchiveService } from './archive.service.js' // One hour. Inside each tick we check if the current UTC hour matches the // configured run-hour; only one of 24 ticks per day actually invokes the // archive. Cheap and simple โ€” no real cron lib needed. const TICK_MS = 60 * 60 * 1000 const DEFAULT_RUN_HOUR_UTC = 3 // 03:00 UTC daily @Injectable() export class ArchiveWorker implements OnApplicationBootstrap, OnModuleDestroy { private readonly logger = new Logger(ArchiveWorker.name) private readonly enabled: boolean private readonly runHour: number private timer: NodeJS.Timeout | null = null private lastRunDay: string | null = null constructor( private readonly archive: ArchiveService, config: ConfigService, ) { this.enabled = config.get('ARCHIVE_ENABLED') === 'true' this.runHour = Number(config.get('ARCHIVE_RUN_HOUR_UTC') ?? DEFAULT_RUN_HOUR_UTC) } onApplicationBootstrap(): void { if (!this.enabled) { this.logger.log( 'ARCHIVE_ENABLED=false โ€” archive scheduler dormant. Operator UI can force runs.', ) return } this.logger.log(`Archive scheduler active ยท runs daily at ${this.runHour}:00 UTC`) // Fire once on startup in case we missed a window; the day-guard prevents // double-runs within the same UTC day. void this.tick() this.timer = setInterval(() => void this.tick(), TICK_MS) } onModuleDestroy(): void { if (this.timer) clearInterval(this.timer) } private async tick(): Promise { const now = new Date() if (now.getUTCHours() !== this.runHour) return const today = now.toISOString().slice(0, 10) // YYYY-MM-DD UTC if (this.lastRunDay === today) return // already ran today this.lastRunDay = today this.logger.log(`Archive tick fired for ${today}`) try { const res = await this.archive.runOnce() if (res.ok && res.eventCount) { this.logger.log(`Archive complete: ${res.eventCount} events (${res.startSeq}-${res.endSeq})`) } else if (res.ok) { this.logger.log(`Archive complete: ${res.reason ?? 'no-op'}`) } else { this.logger.error(`Archive failed: ${res.reason}`) } } catch (err) { this.logger.error( `Archive tick crashed: ${err instanceof Error ? err.message : String(err)}`, ) } } }