// import required so Node type-strips this .cts (it has no other import). import type { Db } from '../types/db'; const cron = require('node-cron'); const { log } = require('../utils/logger.cts'); const { getSetting, setSetting } = require('../db/database.cts'); const { applyScheduledRetention, createBackup } = require('./backupService.cts'); interface ScheduleSettings { enabled: boolean; frequency: string; time: string; retention_count: number; } let task: any = null; let nextRunAt: string | null = null; let running = false; function parseBool(value: unknown): boolean { return value === true || value === 'true'; } function validateScheduleSettings(input: any = {}): ScheduleSettings { const enabled = parseBool(input.enabled); const frequency = input.frequency || 'daily'; const time = input.time || '02:00'; const retentionCount = parseInt(input.retention_count ?? '2', 10); if (!['daily', 'weekly'].includes(frequency)) { const err = new Error('frequency must be daily or weekly') as any; err.status = 400; throw err; } if (!/^\d{2}:\d{2}$/.test(time)) { const err = new Error('time must use HH:MM format') as any; err.status = 400; throw err; } const [hour, minute] = time.split(':').map(Number); if (hour < 0 || hour > 23 || minute < 0 || minute > 59) { const err = new Error('time must be a valid 24-hour time') as any; err.status = 400; throw err; } if (!Number.isInteger(retentionCount) || retentionCount < 1 || retentionCount > 365) { const err = new Error('retention_count must be between 1 and 365') as any; err.status = 400; throw err; } return { enabled, frequency, time, retention_count: retentionCount }; } function readSettings(): ScheduleSettings { return validateScheduleSettings({ enabled: getSetting('backup_schedule_enabled') === 'true', frequency: getSetting('backup_schedule_frequency') || 'daily', time: getSetting('backup_schedule_time') || '02:00', retention_count: getSetting('backup_schedule_retention_count') || '2', }); } function getLastRunAt(): string | null { return getSetting('backup_schedule_last_run_at') || null; } function getLastError(): string | null { return getSetting('backup_schedule_last_error') || null; } function computeNextRun( settings: ScheduleSettings = readSettings(), from: Date = new Date(), ): string | null { if (!settings.enabled) return null; const [hour, minute] = settings.time.split(':').map(Number); const next = new Date(from); next.setHours(hour, minute, 0, 0); if (settings.frequency === 'weekly') { while (next <= from || next.getDay() !== 0) next.setDate(next.getDate() + 1); return next.toISOString(); } if (next <= from) next.setDate(next.getDate() + 1); return next.toISOString(); } function cronExpression(settings: ScheduleSettings): string { const [hour, minute] = settings.time.split(':').map(Number); return settings.frequency === 'weekly' ? `${minute} ${hour} * * 0` : `${minute} ${hour} * * *`; } async function runScheduledBackupNow(): Promise { if (running) { const err = new Error('Scheduled backup is already running') as any; err.status = 409; throw err; } running = true; try { const settings = readSettings(); const backup = await createBackup('scheduled-backup'); applyScheduledRetention(settings.retention_count); setSetting('backup_schedule_last_run_at', new Date().toISOString()); setSetting('backup_schedule_last_error', ''); nextRunAt = computeNextRun(settings); return backup; } catch (err: any) { setSetting('backup_schedule_last_error', err.message || 'Scheduled backup failed'); throw err; } finally { running = false; } } function stop(): void { if (task) task.stop(); task = null; } function reloadSchedule() { stop(); const settings = readSettings(); nextRunAt = computeNextRun(settings); if (!settings.enabled) return getScheduleStatus(); task = cron.schedule(cronExpression(settings), () => { runScheduledBackupNow().catch((err: any) => { log.error('[backupScheduler] Scheduled backup failed:', err.message); }); }); return getScheduleStatus(); } function saveSettings(input: any) { const settings = validateScheduleSettings(input); setSetting('backup_schedule_enabled', settings.enabled ? 'true' : 'false'); setSetting('backup_schedule_frequency', settings.frequency); setSetting('backup_schedule_time', settings.time); setSetting('backup_schedule_retention_count', String(settings.retention_count)); return reloadSchedule(); } function getScheduleStatus() { const settings = readSettings(); return { ok: !getLastError(), enabled: settings.enabled, running, frequency: settings.frequency, time: settings.time, retention_count: settings.retention_count, last_run_at: getLastRunAt(), next_run_at: nextRunAt ?? computeNextRun(settings), last_error: getLastError(), }; } function start() { return reloadSchedule(); } module.exports = { computeNextRun, getScheduleStatus, reloadSchedule, runScheduledBackupNow, saveSettings, start, validateScheduleSettings, };