wulf-pulse/lib/services/rmm-device-resolver.ts
lorentz ea3471d38d feat: Veeam RPO analysis, comparison, ticket analysis + company teams table
- Add Veeam RPO analysis page (/veeam-analysis) and comparison page (/veeam-comparison)
- Add API routes: /api/veeam/rpo-analyze, rpo-comparison, rpo-offline-log, ticket-analysis
- Add veeam-rpo-service.ts enhancements (RPO logic, offline detection, comparison)
- Add veeam-analysis-state.ts and rmm-device-resolver.ts services
- Add migrations 065-068: company_teams, veeam_rpo_offline_log, rpo_comparison_tables, veeam_ticket_analysis
- Add backup-status page updates and nav links for new Veeam pages
- Add scripts: deactivate-cis-for-inactive-companies, workstation category updates
- Add docs: mimecast-api-guide, veeam-backup-alerting-recommendation, workstation-backup-overview, ticket-analyzer-prompt
- Minor: webhook-service, entity-sync, entity-mapper, sync-helpers, sync.ts, middleware.ts updates
2026-04-29 09:16:46 -04:00

90 lines
3.6 KiB
TypeScript

/**
* 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<Map<string, RmmDeviceInfo>> {
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<string, RmmDeviceInfo>();
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;
}