wulf-pulse/app/api/zabbix/wan-gap-analysis/route.ts
lorentz b98c67482a feat: QuickBooks Online integration
- Add QBO OAuth2 client with token refresh (lib/services/qbo-client.ts)
- Add QBO sync service for invoices, payments, deposits, purchases, journal entries, reports (lib/services/qbo-sync-service.ts)
- Add QBO types (lib/types/qbo.ts)
- Add API routes: /api/qbo/auth, /api/qbo/sync, /api/qbo/disconnect
- Add /admin/qbo status and sync management page
- Add legal pages: /legal/eula, /legal/privacy (Intuit app assessment)
- Add QBO nav link under Admin
- Fix reports: remove invalid summarize_column_by, add accounting_method from Preferences API, add showrows=all&showcols=all
- Add CashFlow report type alongside P&L and BalanceSheet
- Add NoReportData check to skip empty report months
- Add intuit_tid capture in error messages
- Add redirect: follow for cluster routing
- Migration 051: qbo_tokens, qbo_invoices, qbo_payments, qbo_deposits, qbo_transactions, qbo_reports tables

Also includes earlier work:
- Ping flap suppression pipeline step
- Ticket digest reports with LLM analysis
- Zabbix WAN monitor and gap analysis
- Kiosk is_deleted filter fixes
- Datto RMM ping target enrichment
- Entity sync soft-delete detection
2026-03-17 07:39:55 -04:00

166 lines
6.8 KiB
TypeScript

/**
* 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 });
}
}