import cron from 'node-cron'; import { checkAllApps, pruneOldStatuses } from '$lib/server/services/healthcheckService.js'; import { broadcastNotification } from '$lib/server/services/notificationService.js'; import { pruneOldLogs } from '$lib/server/services/auditLogService.js'; import * as appService from '$lib/server/services/appService.js'; import { AppStatusValue, NotificationEvent } from '$lib/utils/constants.js'; let scheduledTask: cron.ScheduledTask | null = null; let cleanupTask: cron.ScheduledTask | null = null; let auditPruneTask: cron.ScheduledTask | null = null; // Track previous status per app to detect transitions const previousStatuses = new Map(); /** * Check if a status transition warrants a notification. */ function getStatusChangeEvent( previousStatus: string | undefined, newStatus: string ): string | null { if (!previousStatus) { return null; // First check — no transition } if (previousStatus === newStatus) { return null; // No change } if (newStatus === AppStatusValue.OFFLINE && previousStatus !== AppStatusValue.OFFLINE) { return NotificationEvent.APP_OFFLINE; } if (newStatus === AppStatusValue.ONLINE && previousStatus !== AppStatusValue.ONLINE) { return NotificationEvent.APP_ONLINE; } if (newStatus === AppStatusValue.DEGRADED && previousStatus !== AppStatusValue.DEGRADED) { return NotificationEvent.APP_DEGRADED; } return null; } /** * Start the healthcheck scheduler. * Runs checkAllApps on a cron schedule (default: every 60 seconds). * Also starts an hourly cleanup job to prune old status records. * Triggers notifications when app status changes. */ export function startScheduler(cronExpression: string = '* * * * *'): void { if (scheduledTask) { return; } scheduledTask = cron.schedule(cronExpression, async () => { try { const results = await checkAllApps(); // Check for status transitions and send notifications for (const result of results) { const prevStatus = previousStatuses.get(result.appId); const event = getStatusChangeEvent(prevStatus, result.status); if (event) { // Fire-and-forget notification appService .findById(result.appId) .then((app) => { const statusLabel = result.status.charAt(0).toUpperCase() + result.status.slice(1); const message = `${app.name} is now ${statusLabel} (was ${prevStatus ?? 'unknown'})`; return broadcastNotification(result.appId, event, message); }) .catch(() => { // Swallow notification errors }); } previousStatuses.set(result.appId, result.status); } } catch { // Swallow errors to prevent scheduler crash } }); // Cleanup job: run every hour at minute 0 cleanupTask = cron.schedule('0 * * * *', async () => { try { await pruneOldStatuses(); } catch { // Swallow errors to prevent scheduler crash } }); // Audit log pruning: run daily at midnight auditPruneTask = cron.schedule('0 0 * * *', async () => { try { await pruneOldLogs(90); // Default 90 day retention } catch { // Swallow errors to prevent scheduler crash } }); // Run an initial check shortly after startup setTimeout(() => { checkAllApps().catch(() => { // Swallow initial check errors }); }, 5000); } /** * Stop the healthcheck scheduler and cleanup job. */ export function stopScheduler(): void { if (scheduledTask) { scheduledTask.stop(); scheduledTask = null; } if (cleanupTask) { cleanupTask.stop(); cleanupTask = null; } if (auditPruneTask) { auditPruneTask.stop(); auditPruneTask = null; } }