/** * RMM Device Resolver * Matches Veeam backup agent jobs to their Datto RMM device via: * veeam_organizations.company_id → datto_rmm_sites.autotask_company_id (org match) * veeam_backup_agents.name ≈ datto_rmm_devices.hostname (hostname match) * * Scoped to Desktop/Laptop device categories only — server jobs are handled separately. * Reusable by any service that needs to correlate Veeam jobs with RMM device state. */ import postgresClient from './postgres-client'; export interface RmmDeviceInfo { hostname: string; site_name: string; device_type_category: string; last_seen: Date | null; online: boolean; } /** * Bulk-resolve Veeam backup agent jobs to their matching Datto RMM devices. * Returns a Map keyed by job instance_uid. Jobs with no RMM match are absent. */ export async function resolveRmmDevicesForJobs( jobInstanceUids: string[] ): Promise> { if (jobInstanceUids.length === 0) return new Map(); // datto_rmm_sites.autotask_company_id is not reliably populated from the API, // so join via companies.company_name = datto_rmm_sites.name instead. // Datto sites are often named "Company - Location" while company_name is "Company", // so match exact OR site name starts with "company - ". // DISTINCT ON (job) prevents fanout when a company has multiple matching sites, // preferring the device with the most recent last_seen. const res = await postgresClient.query(` SELECT DISTINCT ON (j.instance_uid) j.instance_uid AS job_instance_uid, rmm.hostname, rs.name AS site_name, rmm.device_type_category, rmm.last_seen, rmm.online FROM veeam_backup_agent_jobs j JOIN veeam_backup_agents ba ON ba.instance_uid = j.backup_agent_uid JOIN veeam_organizations vo ON vo.instance_uid = j.organization_uid JOIN companies c ON c.id = vo.company_id JOIN datto_rmm_sites rs ON LOWER(rs.name) = LOWER(c.company_name) OR LOWER(rs.name) LIKE LOWER(c.company_name) || ' - %' JOIN datto_rmm_devices rmm ON rmm.site_id = rs.id AND LOWER(rmm.hostname) = LOWER(ba.name) AND rmm.device_type_category IN ('Desktop', 'Laptop') AND rmm.deleted = false WHERE j.instance_uid = ANY($1) ORDER BY j.instance_uid, rmm.last_seen DESC NULLS LAST `, [jobInstanceUids]); const map = new Map(); for (const row of res.rows) { map.set(row.job_instance_uid, { hostname: row.hostname, site_name: row.site_name, device_type_category: row.device_type_category, last_seen: row.last_seen ? new Date(row.last_seen) : null, online: row.online, }); } return map; } /** * Returns true when the device should suppress RPO alerting. * A device is considered suppressed if it has been offline longer than one full * backup interval — meaning it was already offline when the backup was due to run. * No last_seen → not suppressed (device exists in RMM but has never reported; alert normally). */ export function isDeviceOfflineSuppressed(info: RmmDeviceInfo, intervalHours: number): boolean { if (!info.last_seen) return false; const hoursOffline = (Date.now() - info.last_seen.getTime()) / 3_600_000; return hoursOffline > intervalHours; } /** * Returns hours since the device was last seen by RMM, or null if never seen. */ export function hoursOffline(info: RmmDeviceInfo): number | null { if (!info.last_seen) return null; return (Date.now() - info.last_seen.getTime()) / 3_600_000; }