- 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
166 lines
6.8 KiB
TypeScript
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 });
|
|
}
|
|
}
|