- 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
90 lines
3.6 KiB
TypeScript
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;
|
|
}
|