/** * GET /api/zabbix/wan-gap-analysis * Cross-references Datto RMM sites, Zabbix WAN hosts, and IT Glue WAN circuits * to surface monitoring gaps and event correlation. * * Returns: * - summary: counts of covered/gap/no-itg sites * - rmm_gaps: RMM sites with no Zabbix host * - itg_gaps: IT Glue WAN circuits with no matched Zabbix host (and have a static IP) * - multi_circuit_sites: companies with >1 IT Glue WAN circuit, showing Zabbix coverage per circuit * - current_problems: Zabbix hosts with active problems + matching recent RMM alerts * - last_synced: timestamps of the cache tables */ import { NextResponse } from 'next/server'; import { postgresClient } from '@/lib/services/postgres-client'; export async function GET() { try { // ── 1. RMM sites missing from Zabbix ───────────────────────────────────── const rmmGaps = await postgresClient.query(` SELECT s.uid AS rmm_site_uid, s.name AS rmm_site_name, s.autotask_company_name AS company_name, rsm.company_id, s.number_of_devices, s.number_of_online_devices FROM datto_rmm_sites s JOIN rmm_site_mappings rsm ON rsm.rmm_site_uid = s.uid LEFT JOIN zabbix_wan_hosts z ON z.rmm_site_uid = s.uid WHERE z.hostid IS NULL AND s.number_of_online_devices > 0 ORDER BY s.autotask_company_name, s.name `); // ── 2. IT Glue WAN circuits missing from Zabbix (have IPs, not decommissioned) ── const itgGaps = await postgresClient.query(` SELECT ic.id AS itg_asset_id, ic.org_name, ic.autotask_company_id, ic.provider, ic.link_type, ic.static_ips, ic.location_name, ic.location_city, ic.upload_mbps, ic.download_mbps FROM itg_wan_circuits ic WHERE ic.zabbix_hostid IS NULL AND ic.is_decommissioned = FALSE AND array_length(ic.static_ips, 1) > 0 ORDER BY ic.org_name, ic.provider `); // ── 3. Multi-circuit companies (>1 IT Glue WAN circuit) ────────────────── const multiCircuit = await postgresClient.query(` SELECT ic.org_name, ic.autotask_company_id, COUNT(*) AS total_circuits, COUNT(*) FILTER (WHERE ic.zabbix_hostid IS NOT NULL) AS monitored_circuits, COUNT(*) FILTER (WHERE ic.zabbix_hostid IS NULL AND NOT ic.is_decommissioned AND array_length(ic.static_ips,1) > 0) AS gap_circuits, json_agg(json_build_object( 'id', ic.id, 'provider', ic.provider, 'link_type', ic.link_type, 'static_ips', ic.static_ips, 'location_name', ic.location_name, 'zabbix_hostid', ic.zabbix_hostid, 'is_decommissioned', ic.is_decommissioned ) ORDER BY ic.provider) AS circuits FROM itg_wan_circuits ic WHERE NOT ic.is_decommissioned GROUP BY ic.org_name, ic.autotask_company_id HAVING COUNT(*) > 1 ORDER BY gap_circuits DESC, ic.org_name `); // ── 4. Current Zabbix problems + correlated RMM alerts ─────────────────── const problems = await postgresClient.query(` SELECT z.hostid, z.display_name, z.wan_ip, z.isp_name, z.autotask_company_name, z.autotask_company_id, z.rmm_site_uid, z.last_problem_at, z.last_problem_name, -- Count open RMM alerts for the same company in the last 24h ( SELECT COUNT(*) FROM datto_rmm_alerts a JOIN datto_rmm_sites ds ON ds.uid = a.site_uid WHERE ds.autotask_company_id = z.autotask_company_id AND a.resolved = FALSE AND a.timestamp > NOW() - INTERVAL '24 hours' ) AS open_rmm_alerts_24h, -- Most recent RMM network alert for this company ( SELECT a.alert_message_en FROM datto_rmm_alerts a JOIN datto_rmm_sites ds ON ds.uid = a.site_uid WHERE ds.autotask_company_id = z.autotask_company_id AND (a.alert_category ILIKE '%network%' OR a.alert_category ILIKE '%wan%' OR a.alert_type ILIKE '%ping%' OR a.alert_type ILIKE '%offline%') AND a.timestamp > NOW() - INTERVAL '24 hours' ORDER BY a.timestamp DESC LIMIT 1 ) AS latest_rmm_network_alert, -- Open ticket count for this company today ( SELECT COUNT(*) FROM tickets t WHERE t.company_id = z.autotask_company_id AND t.source = 8 AND t.create_date > NOW() - INTERVAL '24 hours' AND t.is_deleted IS NOT TRUE ) AS monitor_tickets_24h FROM zabbix_wan_hosts z WHERE z.last_problem_at > NOW() - INTERVAL '24 hours' OR z.status = 1 ORDER BY z.last_problem_at DESC NULLS LAST `); // ── 5. Summary counts ───────────────────────────────────────────────────── const summary = await postgresClient.query(` SELECT (SELECT COUNT(*) FROM datto_rmm_sites s JOIN rmm_site_mappings rsm ON rsm.rmm_site_uid = s.uid) AS total_rmm_sites, (SELECT COUNT(*) FROM zabbix_wan_hosts) AS total_zabbix_hosts, (SELECT COUNT(*) FROM zabbix_wan_hosts WHERE status = 0) AS zabbix_enabled, (SELECT COUNT(*) FROM datto_rmm_sites s JOIN rmm_site_mappings rsm ON rsm.rmm_site_uid = s.uid LEFT JOIN zabbix_wan_hosts z ON z.rmm_site_uid = s.uid WHERE z.hostid IS NULL AND s.number_of_online_devices > 0) AS rmm_sites_no_zabbix, (SELECT COUNT(*) FROM itg_wan_circuits WHERE NOT is_decommissioned) AS total_itg_circuits, (SELECT COUNT(*) FROM itg_wan_circuits WHERE zabbix_hostid IS NOT NULL AND NOT is_decommissioned) AS itg_circuits_monitored, (SELECT COUNT(*) FROM itg_wan_circuits WHERE zabbix_hostid IS NULL AND NOT is_decommissioned AND array_length(static_ips,1) > 0) AS itg_circuits_gap, (SELECT MAX(last_synced_at) FROM zabbix_wan_hosts) AS zabbix_last_synced, (SELECT MAX(last_synced_at) FROM itg_wan_circuits) AS itg_last_synced `); return NextResponse.json({ summary: summary.rows[0], rmm_gaps: rmmGaps.rows, itg_gaps: itgGaps.rows, multi_circuit: multiCircuit.rows, problems: problems.rows, }); } catch (err) { const msg = err instanceof Error ? err.message : String(err); return NextResponse.json({ error: msg }, { status: 500 }); } }