import schedule from "node-schedule"; import { and, eq } from "drizzle-orm"; import { db } from "../db/client.js"; import { integrations } from "../db/schema.js"; import { loadIntegrationConfig } from "../integrations/loadIntegration.js"; import { createProxmoxAdapter, type ProxmoxBackupTask } from "../integrations/proxmox/adapter.js"; import { notifyProxmoxBackupFailure } from "./notify.js"; import { getSettings, getInternalFlag, setInternalFlag } from "./settingsStore.js"; const LAST_RUN_FLAG = "proxmoxBackupCheckLastRunDate"; async function checkProxmoxBackups(): Promise { const rows = await db .select({ id: integrations.id, name: integrations.name }) .from(integrations) .where(and(eq(integrations.type, "proxmox"), eq(integrations.enabled, true))); const failures: { integrationName: string; node: string; guestId: string | null; status: string }[] = []; for (const row of rows) { try { const loaded = await loadIntegrationConfig(row.id); if (!loaded) continue; const adapter = createProxmoxAdapter(loaded.config as any); const tasks = await adapter.listRecentBackupTasks(); // Each node runs its own backup schedule independently, so check the // most recent run per node rather than only the single most recent // task overall — otherwise a failing node could be masked by a // healthier node's more recent run. const latestByNode = new Map(); for (const t of tasks) { const existing = latestByNode.get(t.node); if (!existing || t.startTime > existing.startTime) latestByNode.set(t.node, t); } for (const [node, task] of latestByNode) { if (!task.ok && task.status !== "running") { failures.push({ integrationName: row.name, node, guestId: task.guestId, status: task.status }); } } } catch (err) { console.error(`[proxmoxBackup] check failed for integration ${row.id}:`, err); } } await notifyProxmoxBackupFailure(failures); } async function checkProxmoxBackupsOnce(): Promise { const today = new Date().toDateString(); const lastRun = await getInternalFlag(LAST_RUN_FLAG); if (lastRun === today) return; await setInternalFlag(LAST_RUN_FLAG, today); await checkProxmoxBackups(); } function cronFromTime(time: string): string { const [h, m] = time.split(":").map(Number); return `${Number.isFinite(m) ? m : 0} ${Number.isFinite(h) ? h : 8} * * *`; } let currentJob: schedule.Job | null = null; /** (Re)schedules the daily Proxmox backup-failure check per the current notification settings. Call again after settings change. */ export async function scheduleProxmoxBackupCheck(): Promise { if (currentJob) { currentJob.cancel(); currentJob = null; } const { notifications } = await getSettings(); currentJob = schedule.scheduleJob({ rule: cronFromTime(notifications.secretCheckTime), tz: notifications.timezone }, () => { setInternalFlag(LAST_RUN_FLAG, "").catch(() => {}); getSettings().then(({ notifications: n }) => { if (n.proxmoxBackupCheck) checkProxmoxBackups().catch((err) => console.error("[proxmoxBackup] check failed:", err)); }); }); console.log(`Proxmox backup check scheduled at ${notifications.secretCheckTime} (${notifications.timezone})`); } /** Runs once at startup (skipped if already run today), then arms the daily schedule. */ export async function initProxmoxBackupScheduler(): Promise { await checkProxmoxBackupsOnce(); await scheduleProxmoxBackupCheck(); }