2025-10-28 11:21:04 -04:00
|
|
|
import { NextRequest, NextResponse } from 'next/server';
|
2025-11-19 14:18:16 -05:00
|
|
|
import { Pool } from 'pg';
|
2025-10-28 11:21:04 -04:00
|
|
|
import { getAutotaskClient } from '@/lib/services/autotask-factory';
|
|
|
|
|
import { getDattoRMMClient } from '@/lib/services/datto-rmm-factory';
|
2025-11-19 14:18:16 -05:00
|
|
|
import { getAuvikClient } from '@/lib/services/auvik-factory';
|
|
|
|
|
import { getAddigyClient } from '@/lib/services/addigy-factory';
|
2025-10-28 11:21:04 -04:00
|
|
|
import { apiCache } from '@/lib/services/cache';
|
|
|
|
|
import { DattoRMMDevice } from '@/lib/types/datto-rmm';
|
2025-11-19 14:18:16 -05:00
|
|
|
import { AuvikDevice } from '@/lib/types/auvik';
|
|
|
|
|
import { AddigyDevice } from '@/lib/types/addigy';
|
2025-10-28 11:21:04 -04:00
|
|
|
import { ConfigurationItem } from '@/lib/types/autotask';
|
|
|
|
|
|
2025-11-19 14:18:16 -05:00
|
|
|
const pool = new Pool({
|
|
|
|
|
host: process.env.POSTGRES_HOST,
|
|
|
|
|
port: parseInt(process.env.POSTGRES_PORT || '5432'),
|
|
|
|
|
database: process.env.POSTGRES_DB,
|
|
|
|
|
user: process.env.POSTGRES_USER,
|
|
|
|
|
password: process.env.POSTGRES_PASSWORD,
|
|
|
|
|
});
|
|
|
|
|
|
2025-10-28 11:21:04 -04:00
|
|
|
interface DeviceComparison {
|
|
|
|
|
autotaskDevice?: ConfigurationItem;
|
|
|
|
|
rmmDevice?: DattoRMMDevice;
|
2025-11-19 14:18:16 -05:00
|
|
|
auvikDevice?: AuvikDevice;
|
|
|
|
|
addigyDevice?: AddigyDevice;
|
2025-10-28 11:21:04 -04:00
|
|
|
status: 'matched' | 'autotask-only' | 'rmm-only';
|
|
|
|
|
matchedBy?: string; // What field was used to match
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-19 14:18:16 -05:00
|
|
|
// Helper function to normalize MAC address for comparison
|
|
|
|
|
function normalizeMacAddress(mac: string): string {
|
|
|
|
|
return mac.replace(/[:-]/g, '').toLowerCase();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Helper function to match Addigy device to Autotask device
|
|
|
|
|
function matchAddigyDevice(
|
|
|
|
|
autotaskDevice: ConfigurationItem,
|
|
|
|
|
addigyDevices: AddigyDevice[]
|
|
|
|
|
): AddigyDevice | null {
|
|
|
|
|
// Priority 1: Serial number (primary matching method for Apple devices)
|
|
|
|
|
if (autotaskDevice.serialNumber) {
|
|
|
|
|
const autotaskSerial = autotaskDevice.serialNumber?.toLowerCase().trim();
|
|
|
|
|
console.log(`Trying to match Autotask device "${autotaskDevice.referenceTitle}" with serial: ${autotaskSerial}`);
|
|
|
|
|
console.log(`Checking against ${addigyDevices.length} Addigy devices`);
|
|
|
|
|
|
|
|
|
|
const match = addigyDevices.find((d) => {
|
|
|
|
|
const addigySerial = d['Serial Number']?.toLowerCase().trim();
|
|
|
|
|
if (addigySerial) {
|
|
|
|
|
console.log(` Comparing with Addigy device "${d['Device Name']}" serial: ${addigySerial}`);
|
|
|
|
|
}
|
|
|
|
|
return addigySerial === autotaskSerial;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (match) {
|
|
|
|
|
console.log(
|
|
|
|
|
`✓ Matched Addigy device by serial: ${match['Device Name']} (${match['Serial Number']}) -> ${autotaskDevice.referenceTitle} (${autotaskDevice.serialNumber})`
|
|
|
|
|
);
|
|
|
|
|
return match;
|
|
|
|
|
} else {
|
|
|
|
|
console.log(`✗ No Addigy serial match found for ${autotaskDevice.serialNumber}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Priority 2: Device name/hostname
|
|
|
|
|
const hostname =
|
|
|
|
|
autotaskDevice.rmmDeviceAuditHostname || autotaskDevice.referenceTitle;
|
|
|
|
|
if (hostname) {
|
|
|
|
|
const match = addigyDevices.find(
|
|
|
|
|
(d) =>
|
|
|
|
|
d['Device Name']?.toLowerCase().trim() === hostname.toLowerCase().trim()
|
|
|
|
|
);
|
|
|
|
|
if (match) {
|
|
|
|
|
console.log(
|
|
|
|
|
`Matched Addigy device by name: ${match['Device Name']} -> ${autotaskDevice.referenceTitle}`
|
|
|
|
|
);
|
|
|
|
|
return match;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Helper function to match Auvik device to Autotask device
|
|
|
|
|
function matchAuvikDevice(
|
|
|
|
|
autotaskDevice: ConfigurationItem,
|
|
|
|
|
auvikDevices: AuvikDevice[]
|
|
|
|
|
): AuvikDevice | null {
|
|
|
|
|
// Priority 1: Serial number
|
|
|
|
|
if (autotaskDevice.serialNumber) {
|
|
|
|
|
const match = auvikDevices.find(
|
|
|
|
|
(d) =>
|
|
|
|
|
d.serialNumber?.toLowerCase().trim() ===
|
|
|
|
|
autotaskDevice.serialNumber?.toLowerCase().trim()
|
|
|
|
|
);
|
|
|
|
|
if (match) {
|
|
|
|
|
console.log(
|
|
|
|
|
`Matched Auvik device by serial: ${match.deviceName} -> ${autotaskDevice.referenceTitle}`
|
|
|
|
|
);
|
|
|
|
|
return match;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Priority 2: Hostname
|
|
|
|
|
const hostname =
|
|
|
|
|
autotaskDevice.rmmDeviceAuditHostname || autotaskDevice.referenceTitle;
|
|
|
|
|
if (hostname) {
|
|
|
|
|
const match = auvikDevices.find(
|
|
|
|
|
(d) =>
|
|
|
|
|
d.deviceName?.toLowerCase().trim() === hostname.toLowerCase().trim()
|
|
|
|
|
);
|
|
|
|
|
if (match) {
|
|
|
|
|
console.log(
|
|
|
|
|
`Matched Auvik device by hostname: ${match.deviceName} -> ${autotaskDevice.referenceTitle}`
|
|
|
|
|
);
|
|
|
|
|
return match;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Priority 3: MAC address
|
|
|
|
|
const macAddress = autotaskDevice.rmmDeviceAuditMacAddress;
|
|
|
|
|
if (macAddress && macAddress.length > 0) {
|
|
|
|
|
const normalizedMac = normalizeMacAddress(macAddress);
|
|
|
|
|
const match = auvikDevices.find((d) =>
|
|
|
|
|
d.macAddresses?.some(
|
|
|
|
|
(mac) => normalizeMacAddress(mac) === normalizedMac
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
if (match) {
|
|
|
|
|
console.log(
|
|
|
|
|
`Matched Auvik device by MAC: ${match.deviceName} -> ${autotaskDevice.referenceTitle}`
|
|
|
|
|
);
|
|
|
|
|
return match;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-28 11:21:04 -04:00
|
|
|
export async function GET(request: NextRequest) {
|
|
|
|
|
try {
|
|
|
|
|
const searchParams = request.nextUrl.searchParams;
|
|
|
|
|
const companyId = searchParams.get('companyId');
|
|
|
|
|
const companyName = searchParams.get('companyName');
|
|
|
|
|
const activeFilter = searchParams.get('activeFilter') || 'active';
|
2025-11-19 14:18:16 -05:00
|
|
|
const skipCache = searchParams.get('skipCache') === 'true';
|
2025-10-28 11:21:04 -04:00
|
|
|
|
2025-11-19 14:18:16 -05:00
|
|
|
// Get mapping counts to include in cache key (so cache invalidates when mappings change)
|
|
|
|
|
let rmmMappingCount = 0;
|
|
|
|
|
let auvikMappingCount = 0;
|
|
|
|
|
let addigyMappingCount = 0;
|
|
|
|
|
if (companyId) {
|
|
|
|
|
const rmmMappingsResult = await pool.query(
|
|
|
|
|
'SELECT COUNT(*) as count FROM rmm_site_mappings WHERE company_id = $1',
|
|
|
|
|
[parseInt(companyId)]
|
|
|
|
|
);
|
|
|
|
|
rmmMappingCount = parseInt(rmmMappingsResult.rows[0]?.count || '0');
|
|
|
|
|
|
|
|
|
|
const auvikMappingsResult = await pool.query(
|
|
|
|
|
'SELECT COUNT(*) as count FROM auvik_tenant_mappings WHERE autotask_company_id = $1',
|
|
|
|
|
[parseInt(companyId)]
|
|
|
|
|
);
|
|
|
|
|
auvikMappingCount = parseInt(auvikMappingsResult.rows[0]?.count || '0');
|
|
|
|
|
|
|
|
|
|
const addigyMappingsResult = await pool.query(
|
|
|
|
|
'SELECT COUNT(*) as count FROM addigy_org_mappings WHERE autotask_company_id = $1',
|
|
|
|
|
[parseInt(companyId)]
|
|
|
|
|
);
|
|
|
|
|
addigyMappingCount = parseInt(addigyMappingsResult.rows[0]?.count || '0');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check cache first (unless skipCache is true)
|
|
|
|
|
const cacheKey = `rmm-devices:${companyId}:${activeFilter}:rmm-${rmmMappingCount}:auvik-${auvikMappingCount}:addigy-${addigyMappingCount}`;
|
|
|
|
|
if (!skipCache) {
|
|
|
|
|
const cached = apiCache.get(cacheKey);
|
|
|
|
|
if (cached) {
|
|
|
|
|
console.log(`Cache hit for ${cacheKey}`);
|
|
|
|
|
return NextResponse.json(cached);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
console.log(`Skipping cache for ${cacheKey}`);
|
2025-10-28 11:21:04 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!companyId) {
|
|
|
|
|
return NextResponse.json({
|
|
|
|
|
rmmDevices: [],
|
|
|
|
|
autotaskDevices: [],
|
|
|
|
|
comparison: [],
|
|
|
|
|
message: 'Please select a company to view devices'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get Autotask devices based on active filter
|
|
|
|
|
const autotaskClient = getAutotaskClient();
|
|
|
|
|
let autotaskDevices: ConfigurationItem[] = [];
|
|
|
|
|
|
|
|
|
|
if (activeFilter === 'all') {
|
|
|
|
|
// Get all devices regardless of status
|
|
|
|
|
const allItems = await autotaskClient.queryEntity<ConfigurationItem>('ConfigurationItems', {
|
|
|
|
|
filter: [{ op: 'eq', field: 'companyID', value: parseInt(companyId) }],
|
|
|
|
|
});
|
|
|
|
|
autotaskDevices = allItems;
|
|
|
|
|
} else if (activeFilter === 'inactive') {
|
|
|
|
|
// Get only inactive devices
|
|
|
|
|
const inactiveItems = await autotaskClient.queryEntity<ConfigurationItem>('ConfigurationItems', {
|
|
|
|
|
filter: [
|
|
|
|
|
{ op: 'eq', field: 'companyID', value: parseInt(companyId) },
|
|
|
|
|
{ op: 'eq', field: 'isActive', value: false }
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
autotaskDevices = inactiveItems;
|
|
|
|
|
} else {
|
|
|
|
|
// Default: get only active devices
|
|
|
|
|
autotaskDevices = await autotaskClient.getConfigurationItemsByCompany(parseInt(companyId));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get RMM devices
|
|
|
|
|
let rmmDevices: DattoRMMDevice[] = [];
|
|
|
|
|
try {
|
|
|
|
|
const rmmClient = getDattoRMMClient();
|
|
|
|
|
|
2025-11-19 14:18:16 -05:00
|
|
|
// First, check if we have site mappings for this company
|
|
|
|
|
if (companyId) {
|
|
|
|
|
const mappingsResult = await pool.query(
|
|
|
|
|
'SELECT rmm_site_uid FROM rmm_site_mappings WHERE company_id = $1',
|
|
|
|
|
[parseInt(companyId)]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (mappingsResult.rows.length > 0) {
|
|
|
|
|
// Use the new multi-site method if mappings exist
|
|
|
|
|
const siteUids = mappingsResult.rows.map(row => row.rmm_site_uid);
|
|
|
|
|
console.log(`Found ${siteUids.length} mapped RMM sites for company ${companyId}`);
|
|
|
|
|
rmmDevices = await rmmClient.getDevicesForSites(siteUids);
|
|
|
|
|
} else if (companyName) {
|
|
|
|
|
// Fall back to old method if no mappings exist
|
|
|
|
|
console.log(`No RMM site mappings found for company ${companyId}, using name-based matching`);
|
|
|
|
|
rmmDevices = await rmmClient.getDevicesByCompanyName(companyName);
|
|
|
|
|
}
|
|
|
|
|
} else if (companyName) {
|
2025-10-28 11:21:04 -04:00
|
|
|
// Try to get devices by company name (matching site name)
|
|
|
|
|
rmmDevices = await rmmClient.getDevicesByCompanyName(companyName);
|
|
|
|
|
} else {
|
2025-11-19 14:18:16 -05:00
|
|
|
// If no company info, get all devices and try to match
|
2025-10-28 11:21:04 -04:00
|
|
|
rmmDevices = await rmmClient.getAllDevices();
|
|
|
|
|
}
|
2025-11-19 14:18:16 -05:00
|
|
|
|
|
|
|
|
// Filter RMM devices based on activeFilter
|
|
|
|
|
if (activeFilter === 'active') {
|
|
|
|
|
// Only show non-deleted, non-suspended RMM devices when filtering for active
|
|
|
|
|
rmmDevices = rmmDevices.filter(device => !device.deleted && !device.suspended);
|
|
|
|
|
} else if (activeFilter === 'inactive') {
|
|
|
|
|
// Only show deleted or suspended RMM devices when filtering for inactive
|
|
|
|
|
rmmDevices = rmmDevices.filter(device => device.deleted || device.suspended);
|
|
|
|
|
}
|
|
|
|
|
// If 'all', show all RMM devices (no filtering)
|
|
|
|
|
|
|
|
|
|
// Deduplicate RMM devices by ID (in case the same device appears in multiple sites)
|
|
|
|
|
const uniqueRmmDevices = new Map<string, DattoRMMDevice>();
|
|
|
|
|
rmmDevices.forEach(device => {
|
|
|
|
|
const deviceId = String(device.id);
|
|
|
|
|
if (!uniqueRmmDevices.has(deviceId)) {
|
|
|
|
|
uniqueRmmDevices.set(deviceId, device);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
rmmDevices = Array.from(uniqueRmmDevices.values());
|
|
|
|
|
console.log(`After deduplication: ${rmmDevices.length} unique RMM devices`)
|
|
|
|
|
|
2025-10-28 11:21:04 -04:00
|
|
|
} catch (rmmError) {
|
|
|
|
|
console.error('Error fetching RMM devices:', rmmError);
|
|
|
|
|
// Continue with empty RMM devices array
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-19 14:18:16 -05:00
|
|
|
// Get Auvik devices
|
|
|
|
|
let auvikDevices: AuvikDevice[] = [];
|
|
|
|
|
try {
|
|
|
|
|
const auvikClient = getAuvikClient();
|
|
|
|
|
|
|
|
|
|
if (companyId) {
|
|
|
|
|
// Try to find tenant using company ID mapping first (most accurate)
|
|
|
|
|
const tenant = await auvikClient.findTenantByCompanyId(parseInt(companyId));
|
|
|
|
|
|
|
|
|
|
if (tenant) {
|
|
|
|
|
console.log(`Found Auvik tenant via mapping: ${tenant.domainPrefix} for company ID: ${companyId}`);
|
|
|
|
|
auvikDevices = await auvikClient.getDevicesByTenant(tenant.id);
|
|
|
|
|
} else if (companyName) {
|
|
|
|
|
// Fallback to name-based matching
|
|
|
|
|
console.log(`No mapping found, trying name match for: ${companyName}`);
|
|
|
|
|
const tenantByName = await auvikClient.findTenantByName(companyName);
|
|
|
|
|
if (tenantByName) {
|
|
|
|
|
console.log(`Found Auvik tenant by name: ${tenantByName.domainPrefix} for company: ${companyName}`);
|
|
|
|
|
auvikDevices = await auvikClient.getDevicesByTenant(tenantByName.id);
|
|
|
|
|
} else {
|
|
|
|
|
console.log(`No Auvik tenant found for company: ${companyName}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if (companyName) {
|
|
|
|
|
// If no company ID, try name matching
|
|
|
|
|
const tenant = await auvikClient.findTenantByName(companyName);
|
|
|
|
|
if (tenant) {
|
|
|
|
|
console.log(`Found Auvik tenant: ${tenant.domainPrefix} for company: ${companyName}`);
|
|
|
|
|
auvikDevices = await auvikClient.getDevicesByTenant(tenant.id);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// If no company info, get all devices
|
|
|
|
|
auvikDevices = await auvikClient.getAllDevices();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log(`Fetched ${auvikDevices.length} Auvik devices before filtering`);
|
|
|
|
|
|
|
|
|
|
// Filter Auvik devices to only show those with valid hostnames
|
|
|
|
|
// Exclude devices without deviceName or with names starting with "Device@"
|
|
|
|
|
auvikDevices = auvikDevices.filter(device => {
|
|
|
|
|
if (!device.deviceName) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if (device.deviceName.startsWith('Device@')) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.log(`Filtered to ${auvikDevices.length} Auvik devices with valid hostnames`);
|
|
|
|
|
} catch (auvikError) {
|
|
|
|
|
console.error('Error fetching Auvik devices:', auvikError);
|
|
|
|
|
// Continue with empty Auvik devices array
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get Addigy devices (Apple RMM)
|
|
|
|
|
let addigyDevices: AddigyDevice[] = [];
|
|
|
|
|
try {
|
|
|
|
|
const addigyClient = getAddigyClient();
|
|
|
|
|
|
|
|
|
|
if (companyId) {
|
|
|
|
|
// Try to find policy using company ID mapping first
|
|
|
|
|
const mappingsResult = await pool.query(
|
|
|
|
|
'SELECT addigy_org_id FROM addigy_org_mappings WHERE autotask_company_id = $1',
|
|
|
|
|
[parseInt(companyId)]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (mappingsResult.rows.length > 0) {
|
|
|
|
|
// Get all devices and filter by policy IDs in code
|
|
|
|
|
const policyIds = new Set(mappingsResult.rows.map(row => row.addigy_org_id));
|
|
|
|
|
console.log(`Found ${policyIds.size} mapped Addigy policies for company ${companyId}:`, Array.from(policyIds));
|
|
|
|
|
|
|
|
|
|
// Fetch all devices (without filter)
|
|
|
|
|
const allDevices = await addigyClient.getAllDevices();
|
|
|
|
|
console.log(`Fetched ${allDevices.length} total Addigy devices`);
|
|
|
|
|
|
|
|
|
|
// Filter devices by policy_id in code
|
|
|
|
|
addigyDevices = allDevices.filter(device => policyIds.has(device.policy_id));
|
|
|
|
|
console.log(`Filtered to ${addigyDevices.length} devices matching mapped policies`);
|
|
|
|
|
} else {
|
|
|
|
|
console.log(`No Addigy policy mappings found for company ${companyId}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log(`Final Addigy devices count: ${addigyDevices.length}`);
|
|
|
|
|
} catch (addigyError) {
|
|
|
|
|
console.error('Error fetching Addigy devices:', addigyError);
|
|
|
|
|
// Continue with empty Addigy devices array
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-28 11:21:04 -04:00
|
|
|
// Compare and match devices
|
|
|
|
|
const comparison: DeviceComparison[] = [];
|
|
|
|
|
const matchedAutotaskIds = new Set<number>();
|
|
|
|
|
const matchedRmmIds = new Set<string>();
|
|
|
|
|
|
|
|
|
|
// Try to match devices
|
|
|
|
|
for (const rmmDevice of rmmDevices) {
|
2025-11-19 14:18:16 -05:00
|
|
|
// Skip if this RMM device has already been matched
|
|
|
|
|
if (matchedRmmIds.has(String(rmmDevice.id))) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-28 11:21:04 -04:00
|
|
|
let matched = false;
|
|
|
|
|
|
|
|
|
|
// Try to match by RMM Device UID
|
|
|
|
|
if (rmmDevice.uid) {
|
|
|
|
|
const autotaskMatch = autotaskDevices.find(
|
|
|
|
|
at => at.rmmDeviceUID === rmmDevice.uid && !matchedAutotaskIds.has(at.id)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (autotaskMatch) {
|
|
|
|
|
comparison.push({
|
|
|
|
|
autotaskDevice: autotaskMatch,
|
|
|
|
|
rmmDevice: rmmDevice,
|
|
|
|
|
status: 'matched',
|
|
|
|
|
matchedBy: 'RMM UID'
|
|
|
|
|
});
|
|
|
|
|
matchedAutotaskIds.add(autotaskMatch.id);
|
2025-10-28 22:49:08 -04:00
|
|
|
matchedRmmIds.add(String(rmmDevice.id));
|
2025-10-28 11:21:04 -04:00
|
|
|
matched = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Try to match by serial number
|
|
|
|
|
if (!matched && rmmDevice.serialNumber) {
|
|
|
|
|
const autotaskMatch = autotaskDevices.find(
|
|
|
|
|
at => (at.serialNumber === rmmDevice.serialNumber ||
|
|
|
|
|
at.dattoSerialNumber === rmmDevice.serialNumber) &&
|
|
|
|
|
!matchedAutotaskIds.has(at.id)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (autotaskMatch) {
|
|
|
|
|
comparison.push({
|
|
|
|
|
autotaskDevice: autotaskMatch,
|
|
|
|
|
rmmDevice: rmmDevice,
|
|
|
|
|
status: 'matched',
|
|
|
|
|
matchedBy: 'Serial Number'
|
|
|
|
|
});
|
|
|
|
|
matchedAutotaskIds.add(autotaskMatch.id);
|
2025-10-28 22:49:08 -04:00
|
|
|
matchedRmmIds.add(String(rmmDevice.id));
|
2025-10-28 11:21:04 -04:00
|
|
|
matched = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Try to match by hostname
|
|
|
|
|
if (!matched && rmmDevice.hostname) {
|
|
|
|
|
const autotaskMatch = autotaskDevices.find(
|
|
|
|
|
at => (at.rmmDeviceAuditHostname?.toLowerCase() === rmmDevice.hostname.toLowerCase() ||
|
|
|
|
|
at.dattoHostname?.toLowerCase() === rmmDevice.hostname.toLowerCase() ||
|
|
|
|
|
at.referenceTitle?.toLowerCase().includes(rmmDevice.hostname.toLowerCase())) &&
|
|
|
|
|
!matchedAutotaskIds.has(at.id)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (autotaskMatch) {
|
|
|
|
|
comparison.push({
|
|
|
|
|
autotaskDevice: autotaskMatch,
|
|
|
|
|
rmmDevice: rmmDevice,
|
|
|
|
|
status: 'matched',
|
|
|
|
|
matchedBy: 'Hostname'
|
|
|
|
|
});
|
|
|
|
|
matchedAutotaskIds.add(autotaskMatch.id);
|
2025-10-28 22:49:08 -04:00
|
|
|
matchedRmmIds.add(String(rmmDevice.id));
|
2025-10-28 11:21:04 -04:00
|
|
|
matched = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Try to match by IP address
|
|
|
|
|
if (!matched && (rmmDevice.intIpAddress || rmmDevice.extIpAddress)) {
|
|
|
|
|
const autotaskMatch = autotaskDevices.find(
|
|
|
|
|
at => (at.rmmDeviceAuditIPAddress === rmmDevice.intIpAddress ||
|
|
|
|
|
at.rmmDeviceAuditIPAddress === rmmDevice.extIpAddress ||
|
|
|
|
|
at.dattoInternalIP === rmmDevice.intIpAddress ||
|
|
|
|
|
at.dattoRemoteIP === rmmDevice.extIpAddress) &&
|
|
|
|
|
!matchedAutotaskIds.has(at.id)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (autotaskMatch) {
|
|
|
|
|
comparison.push({
|
|
|
|
|
autotaskDevice: autotaskMatch,
|
|
|
|
|
rmmDevice: rmmDevice,
|
|
|
|
|
status: 'matched',
|
|
|
|
|
matchedBy: 'IP Address'
|
|
|
|
|
});
|
|
|
|
|
matchedAutotaskIds.add(autotaskMatch.id);
|
2025-10-28 22:49:08 -04:00
|
|
|
matchedRmmIds.add(String(rmmDevice.id));
|
2025-10-28 11:21:04 -04:00
|
|
|
matched = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If no match found, add as RMM-only
|
|
|
|
|
if (!matched) {
|
|
|
|
|
comparison.push({
|
|
|
|
|
rmmDevice: rmmDevice,
|
|
|
|
|
status: 'rmm-only'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-19 14:18:16 -05:00
|
|
|
// Add Autotask-only devices and match with Auvik and Addigy
|
2025-10-28 11:21:04 -04:00
|
|
|
for (const autotaskDevice of autotaskDevices) {
|
|
|
|
|
if (!matchedAutotaskIds.has(autotaskDevice.id)) {
|
2025-11-19 14:18:16 -05:00
|
|
|
// Try to match with Auvik device
|
|
|
|
|
const auvikMatch = matchAuvikDevice(autotaskDevice, auvikDevices);
|
|
|
|
|
// Try to match with Addigy device
|
|
|
|
|
const addigyMatch = matchAddigyDevice(autotaskDevice, addigyDevices);
|
|
|
|
|
|
2025-10-28 11:21:04 -04:00
|
|
|
comparison.push({
|
|
|
|
|
autotaskDevice: autotaskDevice,
|
2025-11-19 14:18:16 -05:00
|
|
|
auvikDevice: auvikMatch || undefined,
|
|
|
|
|
addigyDevice: addigyMatch || undefined,
|
2025-10-28 11:21:04 -04:00
|
|
|
status: 'autotask-only'
|
|
|
|
|
});
|
2025-11-19 14:18:16 -05:00
|
|
|
} else {
|
|
|
|
|
// For already matched devices, also try to match with Auvik and Addigy
|
|
|
|
|
const existingComparison = comparison.find(
|
|
|
|
|
(c) => c.autotaskDevice?.id === autotaskDevice.id
|
|
|
|
|
);
|
|
|
|
|
if (existingComparison) {
|
|
|
|
|
if (!existingComparison.auvikDevice) {
|
|
|
|
|
const auvikMatch = matchAuvikDevice(autotaskDevice, auvikDevices);
|
|
|
|
|
if (auvikMatch) {
|
|
|
|
|
existingComparison.auvikDevice = auvikMatch;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!existingComparison.addigyDevice) {
|
|
|
|
|
const addigyMatch = matchAddigyDevice(autotaskDevice, addigyDevices);
|
|
|
|
|
if (addigyMatch) {
|
|
|
|
|
existingComparison.addigyDevice = addigyMatch;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Track which Auvik and Addigy devices have been matched
|
|
|
|
|
const matchedAuvikIds = new Set<string>();
|
|
|
|
|
const matchedAddigyIds = new Set<string>();
|
|
|
|
|
|
|
|
|
|
comparison.forEach(item => {
|
|
|
|
|
if (item.auvikDevice?.id) {
|
|
|
|
|
matchedAuvikIds.add(item.auvikDevice.id);
|
|
|
|
|
}
|
|
|
|
|
if (item.addigyDevice?.agentid) {
|
|
|
|
|
matchedAddigyIds.add(item.addigyDevice.agentid);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Skip unmatched Auvik devices (NMS-only) - don't add them to comparison
|
|
|
|
|
// They will still be counted in stats but won't appear in the device list
|
|
|
|
|
for (const auvikDevice of auvikDevices) {
|
|
|
|
|
if (!matchedAuvikIds.has(auvikDevice.id)) {
|
|
|
|
|
// Mark as matched so it's counted but don't add to comparison
|
|
|
|
|
matchedAuvikIds.add(auvikDevice.id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add unmatched Addigy devices (ARMM-only)
|
|
|
|
|
for (const addigyDevice of addigyDevices) {
|
|
|
|
|
if (!matchedAddigyIds.has(addigyDevice.agentid)) {
|
|
|
|
|
comparison.push({
|
|
|
|
|
addigyDevice: addigyDevice,
|
|
|
|
|
status: 'rmm-only' // Using rmm-only status for non-PSA devices
|
|
|
|
|
});
|
2025-10-28 11:21:04 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sort comparison results
|
|
|
|
|
comparison.sort((a, b) => {
|
|
|
|
|
// Sort by status first (matched, then autotask-only, then rmm-only)
|
|
|
|
|
const statusOrder = { 'matched': 0, 'autotask-only': 1, 'rmm-only': 2 };
|
|
|
|
|
const statusDiff = statusOrder[a.status] - statusOrder[b.status];
|
|
|
|
|
if (statusDiff !== 0) return statusDiff;
|
|
|
|
|
|
2025-11-19 14:18:16 -05:00
|
|
|
// Then sort by device name (check all possible sources)
|
|
|
|
|
const aName = a.autotaskDevice?.referenceTitle ||
|
|
|
|
|
a.rmmDevice?.hostname ||
|
|
|
|
|
a.auvikDevice?.deviceName ||
|
|
|
|
|
a.addigyDevice?.['Device Name'] ||
|
|
|
|
|
'';
|
|
|
|
|
const bName = b.autotaskDevice?.referenceTitle ||
|
|
|
|
|
b.rmmDevice?.hostname ||
|
|
|
|
|
b.auvikDevice?.deviceName ||
|
|
|
|
|
b.addigyDevice?.['Device Name'] ||
|
|
|
|
|
'';
|
2025-10-28 11:21:04 -04:00
|
|
|
return aName.localeCompare(bName);
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-19 14:18:16 -05:00
|
|
|
// Fetch contacts for the company to avoid individual API calls
|
|
|
|
|
const contacts: Record<number, any> = {};
|
|
|
|
|
try {
|
|
|
|
|
const contactIds = new Set<number>();
|
|
|
|
|
autotaskDevices.forEach(device => {
|
|
|
|
|
if (device.contactID) {
|
|
|
|
|
contactIds.add(device.contactID);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (contactIds.size > 0) {
|
|
|
|
|
console.log(`Fetching ${contactIds.size} contacts for company ${companyId}`);
|
|
|
|
|
|
|
|
|
|
// Fetch all contacts for the company in one query
|
|
|
|
|
const companyContacts = await autotaskClient.queryEntity('Contacts', {
|
|
|
|
|
filter: [{ op: 'eq', field: 'companyID', value: parseInt(companyId) }],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Map contacts by ID
|
|
|
|
|
companyContacts.forEach((contact: any) => {
|
|
|
|
|
contacts[contact.id] = contact;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.log(`Fetched ${Object.keys(contacts).length} contacts`);
|
|
|
|
|
}
|
|
|
|
|
} catch (contactError) {
|
|
|
|
|
console.error('Error fetching contacts:', contactError);
|
|
|
|
|
// Continue without contacts
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-28 11:21:04 -04:00
|
|
|
const response = {
|
|
|
|
|
rmmDevices,
|
|
|
|
|
autotaskDevices,
|
2025-11-19 14:18:16 -05:00
|
|
|
auvikDevices,
|
|
|
|
|
addigyDevices,
|
2025-10-28 11:21:04 -04:00
|
|
|
comparison,
|
2025-11-19 14:18:16 -05:00
|
|
|
contacts, // Include contacts in response
|
2025-10-28 11:21:04 -04:00
|
|
|
stats: {
|
|
|
|
|
totalRmm: rmmDevices.length,
|
|
|
|
|
totalAutotask: autotaskDevices.length,
|
2025-11-19 14:18:16 -05:00
|
|
|
totalAuvik: auvikDevices.length,
|
|
|
|
|
totalAddigy: addigyDevices.length,
|
2025-10-28 11:21:04 -04:00
|
|
|
matched: comparison.filter(c => c.status === 'matched').length,
|
|
|
|
|
autotaskOnly: comparison.filter(c => c.status === 'autotask-only').length,
|
|
|
|
|
rmmOnly: comparison.filter(c => c.status === 'rmm-only').length,
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Cache for 2 minutes
|
|
|
|
|
apiCache.set(cacheKey, response, 120); // 120 seconds = 2 minutes
|
|
|
|
|
|
|
|
|
|
return NextResponse.json(response);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error in RMM devices endpoint:', error);
|
|
|
|
|
|
|
|
|
|
if (error instanceof Error) {
|
|
|
|
|
return NextResponse.json(
|
|
|
|
|
{
|
|
|
|
|
error: 'Failed to fetch devices',
|
|
|
|
|
message: error.message,
|
|
|
|
|
},
|
|
|
|
|
{ status: 500 }
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return NextResponse.json(
|
|
|
|
|
{ error: 'An unexpected error occurred' },
|
|
|
|
|
{ status: 500 }
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|