wulf-pulse/app/api/rmm-devices/route.ts

630 lines
23 KiB
TypeScript
Raw Normal View History

import { NextRequest, NextResponse } from 'next/server';
import { Pool } from 'pg';
import { getAutotaskClient } from '@/lib/services/autotask-factory';
import { getDattoRMMClient } from '@/lib/services/datto-rmm-factory';
import { getAuvikClient } from '@/lib/services/auvik-factory';
import { getAddigyClient } from '@/lib/services/addigy-factory';
import { apiCache } from '@/lib/services/cache';
import { DattoRMMDevice } from '@/lib/types/datto-rmm';
import { AuvikDevice } from '@/lib/types/auvik';
import { AddigyDevice } from '@/lib/types/addigy';
import { ConfigurationItem } from '@/lib/types/autotask';
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,
});
interface DeviceComparison {
autotaskDevice?: ConfigurationItem;
rmmDevice?: DattoRMMDevice;
auvikDevice?: AuvikDevice;
addigyDevice?: AddigyDevice;
status: 'matched' | 'autotask-only' | 'rmm-only';
matchedBy?: string; // What field was used to match
}
// 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;
}
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';
const skipCache = searchParams.get('skipCache') === 'true';
// 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}`);
}
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();
// 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) {
// Try to get devices by company name (matching site name)
rmmDevices = await rmmClient.getDevicesByCompanyName(companyName);
} else {
// If no company info, get all devices and try to match
rmmDevices = await rmmClient.getAllDevices();
}
// 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`)
} catch (rmmError) {
console.error('Error fetching RMM devices:', rmmError);
// Continue with empty RMM devices array
}
// 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
}
// 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) {
// Skip if this RMM device has already been matched
if (matchedRmmIds.has(String(rmmDevice.id))) {
continue;
}
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);
Add Addigy API integration and Docker deployment with Redis caching - Implemented complete Addigy API v2 client with authentication via x-api-key - Added device and policy endpoints with automatic org ID resolution - Created field mapping from snake_case to Title Case for UI compatibility - Handles nested 'facts' response structure from Addigy devices API - Added comprehensive API documentation in ADDIGY_API_GUIDE.md - Multi-stage Dockerfile with optimized production build - Custom ports: App on 3100, Redis on 6380 (avoids conflicts) - Docker Compose orchestration with health checks - Standalone Next.js output for smaller container images - Non-root user execution for security - Implemented Redis caching layer for API responses - 5-minute TTL with graceful fallback if Redis unavailable - Cache key structure: service:entity:filter1:filter2 - Applied to Addigy devices endpoint with cache hit/miss logging - Fixed TypeScript strict mode errors for production builds - Added null safety checks with optional chaining throughout API routes - Wrapped useSearchParams in Suspense boundary for Next.js 15+ compatibility - Fixed type assertions for dynamic API responses - Corrected Set<string> type mismatches in device comparison logic - Created DOCKER_README.md with complete deployment guide - Updated ADDIGY_API_GUIDE.md with real-world API patterns - Documented response structures, field mappings, and troubleshooting - Next.js 16.0.0 with Turbopack - Redis 7 with AOF persistence - Podman/Docker compatible - TypeScript strict mode compliant
2025-10-28 22:49:08 -04:00
matchedRmmIds.add(String(rmmDevice.id));
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);
Add Addigy API integration and Docker deployment with Redis caching - Implemented complete Addigy API v2 client with authentication via x-api-key - Added device and policy endpoints with automatic org ID resolution - Created field mapping from snake_case to Title Case for UI compatibility - Handles nested 'facts' response structure from Addigy devices API - Added comprehensive API documentation in ADDIGY_API_GUIDE.md - Multi-stage Dockerfile with optimized production build - Custom ports: App on 3100, Redis on 6380 (avoids conflicts) - Docker Compose orchestration with health checks - Standalone Next.js output for smaller container images - Non-root user execution for security - Implemented Redis caching layer for API responses - 5-minute TTL with graceful fallback if Redis unavailable - Cache key structure: service:entity:filter1:filter2 - Applied to Addigy devices endpoint with cache hit/miss logging - Fixed TypeScript strict mode errors for production builds - Added null safety checks with optional chaining throughout API routes - Wrapped useSearchParams in Suspense boundary for Next.js 15+ compatibility - Fixed type assertions for dynamic API responses - Corrected Set<string> type mismatches in device comparison logic - Created DOCKER_README.md with complete deployment guide - Updated ADDIGY_API_GUIDE.md with real-world API patterns - Documented response structures, field mappings, and troubleshooting - Next.js 16.0.0 with Turbopack - Redis 7 with AOF persistence - Podman/Docker compatible - TypeScript strict mode compliant
2025-10-28 22:49:08 -04:00
matchedRmmIds.add(String(rmmDevice.id));
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);
Add Addigy API integration and Docker deployment with Redis caching - Implemented complete Addigy API v2 client with authentication via x-api-key - Added device and policy endpoints with automatic org ID resolution - Created field mapping from snake_case to Title Case for UI compatibility - Handles nested 'facts' response structure from Addigy devices API - Added comprehensive API documentation in ADDIGY_API_GUIDE.md - Multi-stage Dockerfile with optimized production build - Custom ports: App on 3100, Redis on 6380 (avoids conflicts) - Docker Compose orchestration with health checks - Standalone Next.js output for smaller container images - Non-root user execution for security - Implemented Redis caching layer for API responses - 5-minute TTL with graceful fallback if Redis unavailable - Cache key structure: service:entity:filter1:filter2 - Applied to Addigy devices endpoint with cache hit/miss logging - Fixed TypeScript strict mode errors for production builds - Added null safety checks with optional chaining throughout API routes - Wrapped useSearchParams in Suspense boundary for Next.js 15+ compatibility - Fixed type assertions for dynamic API responses - Corrected Set<string> type mismatches in device comparison logic - Created DOCKER_README.md with complete deployment guide - Updated ADDIGY_API_GUIDE.md with real-world API patterns - Documented response structures, field mappings, and troubleshooting - Next.js 16.0.0 with Turbopack - Redis 7 with AOF persistence - Podman/Docker compatible - TypeScript strict mode compliant
2025-10-28 22:49:08 -04:00
matchedRmmIds.add(String(rmmDevice.id));
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);
Add Addigy API integration and Docker deployment with Redis caching - Implemented complete Addigy API v2 client with authentication via x-api-key - Added device and policy endpoints with automatic org ID resolution - Created field mapping from snake_case to Title Case for UI compatibility - Handles nested 'facts' response structure from Addigy devices API - Added comprehensive API documentation in ADDIGY_API_GUIDE.md - Multi-stage Dockerfile with optimized production build - Custom ports: App on 3100, Redis on 6380 (avoids conflicts) - Docker Compose orchestration with health checks - Standalone Next.js output for smaller container images - Non-root user execution for security - Implemented Redis caching layer for API responses - 5-minute TTL with graceful fallback if Redis unavailable - Cache key structure: service:entity:filter1:filter2 - Applied to Addigy devices endpoint with cache hit/miss logging - Fixed TypeScript strict mode errors for production builds - Added null safety checks with optional chaining throughout API routes - Wrapped useSearchParams in Suspense boundary for Next.js 15+ compatibility - Fixed type assertions for dynamic API responses - Corrected Set<string> type mismatches in device comparison logic - Created DOCKER_README.md with complete deployment guide - Updated ADDIGY_API_GUIDE.md with real-world API patterns - Documented response structures, field mappings, and troubleshooting - Next.js 16.0.0 with Turbopack - Redis 7 with AOF persistence - Podman/Docker compatible - TypeScript strict mode compliant
2025-10-28 22:49:08 -04:00
matchedRmmIds.add(String(rmmDevice.id));
matched = true;
}
}
// If no match found, add as RMM-only
if (!matched) {
comparison.push({
rmmDevice: rmmDevice,
status: 'rmm-only'
});
}
}
// Add Autotask-only devices and match with Auvik and Addigy
for (const autotaskDevice of autotaskDevices) {
if (!matchedAutotaskIds.has(autotaskDevice.id)) {
// Try to match with Auvik device
const auvikMatch = matchAuvikDevice(autotaskDevice, auvikDevices);
// Try to match with Addigy device
const addigyMatch = matchAddigyDevice(autotaskDevice, addigyDevices);
comparison.push({
autotaskDevice: autotaskDevice,
auvikDevice: auvikMatch || undefined,
addigyDevice: addigyMatch || undefined,
status: 'autotask-only'
});
} 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
});
}
}
// 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;
// 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'] ||
'';
return aName.localeCompare(bName);
});
// 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
}
const response = {
rmmDevices,
autotaskDevices,
auvikDevices,
addigyDevices,
comparison,
contacts, // Include contacts in response
stats: {
totalRmm: rmmDevices.length,
totalAutotask: autotaskDevices.length,
totalAuvik: auvikDevices.length,
totalAddigy: addigyDevices.length,
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 }
);
}
}