From c0190c7dd11a6c46ed18f71b69ec4d848b65c171 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 2 Feb 2026 23:02:34 -0500 Subject: [PATCH] feat: add tenants/sites sync for accurate dashboard stats - Created auvik_tenants and rmm_sites tables - Added /api/sync/tenants-sites endpoint to sync all tenants from Auvik and all sites from Datto RMM for accurate mapped/unmapped counts --- app/api/sync/tenants-sites/route.ts | 88 +++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 app/api/sync/tenants-sites/route.ts diff --git a/app/api/sync/tenants-sites/route.ts b/app/api/sync/tenants-sites/route.ts new file mode 100644 index 0000000..99e13c0 --- /dev/null +++ b/app/api/sync/tenants-sites/route.ts @@ -0,0 +1,88 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { postgresClient } from '@/lib/services/postgres-client'; +import { getAuvikClient } from '@/lib/services/auvik-factory'; +import { getDattoRMMClient } from '@/lib/services/datto-rmm-factory'; + +export async function POST(request: NextRequest) { + try { + const results = { + auvik: { added: 0, updated: 0, errors: [] as string[] }, + rmm: { added: 0, updated: 0, errors: [] as string[] }, + }; + + // Sync Auvik tenants + try { + const auvikClient = getAuvikClient(); + const tenants = await auvikClient.getTenants(); + + for (const tenant of tenants) { + try { + const result = await postgresClient.query( + `INSERT INTO auvik_tenants (tenant_id, tenant_name, domain_prefix, last_sync_at) + VALUES ($1, $2, $3, CURRENT_TIMESTAMP) + ON CONFLICT (tenant_id) DO UPDATE SET + tenant_name = EXCLUDED.tenant_name, + domain_prefix = EXCLUDED.domain_prefix, + last_sync_at = CURRENT_TIMESTAMP, + updated_at = CURRENT_TIMESTAMP + RETURNING (xmax = 0) as is_new`, + [tenant.id, tenant.domainPrefix, tenant.domainPrefix] + ); + + if (result.rows[0]?.is_new) { + results.auvik.added++; + } else { + results.auvik.updated++; + } + } catch (error) { + results.auvik.errors.push(`Tenant ${tenant.id}: ${error instanceof Error ? error.message : String(error)}`); + } + } + } catch (error) { + results.auvik.errors.push(`Failed to fetch tenants: ${error instanceof Error ? error.message : String(error)}`); + } + + // Sync RMM sites + try { + const rmmClient = getDattoRMMClient(); + const sites = await rmmClient.getSites(); + + for (const site of sites) { + try { + const result = await postgresClient.query( + `INSERT INTO rmm_sites (site_uid, site_name, last_sync_at) + VALUES ($1, $2, CURRENT_TIMESTAMP) + ON CONFLICT (site_uid) DO UPDATE SET + site_name = EXCLUDED.site_name, + last_sync_at = CURRENT_TIMESTAMP, + updated_at = CURRENT_TIMESTAMP + RETURNING (xmax = 0) as is_new`, + [site.uid, site.name] + ); + + if (result.rows[0]?.is_new) { + results.rmm.added++; + } else { + results.rmm.updated++; + } + } catch (error) { + results.rmm.errors.push(`Site ${site.uid}: ${error instanceof Error ? error.message : String(error)}`); + } + } + } catch (error) { + results.rmm.errors.push(`Failed to fetch sites: ${error instanceof Error ? error.message : String(error)}`); + } + + return NextResponse.json({ + success: true, + message: `Synced ${results.auvik.added + results.auvik.updated} Auvik tenants and ${results.rmm.added + results.rmm.updated} RMM sites`, + results, + }); + } catch (error) { + console.error('Error syncing tenants and sites:', error); + return NextResponse.json( + { error: 'Failed to sync tenants and sites' }, + { status: 500 } + ); + } +}