- 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
122 lines
4.6 KiB
TypeScript
122 lines
4.6 KiB
TypeScript
/**
|
|
* POST /api/zabbix/sync-hosts
|
|
* Pulls all hosts from the live Zabbix API and caches them in zabbix_wan_hosts.
|
|
* Also fetches current open problems and stamps last_problem_at on affected hosts.
|
|
*/
|
|
import { NextResponse } from 'next/server';
|
|
import { ZabbixClient } from '@/lib/services/zabbix-client';
|
|
import { postgresClient } from '@/lib/services/postgres-client';
|
|
|
|
export const maxDuration = 120;
|
|
|
|
export async function POST() {
|
|
if (!process.env.ZABBIX_API_URL || !process.env.ZABBIX_API_TOKEN) {
|
|
return NextResponse.json({ error: 'Zabbix not configured' }, { status: 500 });
|
|
}
|
|
|
|
const zabbix = new ZabbixClient({
|
|
apiUrl: process.env.ZABBIX_API_URL,
|
|
apiToken: process.env.ZABBIX_API_TOKEN,
|
|
});
|
|
|
|
try {
|
|
const hosts = await zabbix.getHosts();
|
|
|
|
// Fetch open problems to stamp last_problem_at
|
|
const problems = await zabbix.getOpenProblems(2);
|
|
const problemByHostId = new Map<string, { name: string; clock: string }>();
|
|
for (const p of problems) {
|
|
for (const h of (p.hosts ?? [])) {
|
|
if (!problemByHostId.has(h.hostid) || Number(p.clock) > Number(problemByHostId.get(h.hostid)!.clock)) {
|
|
problemByHostId.set(h.hostid, { name: p.name, clock: p.clock });
|
|
}
|
|
}
|
|
}
|
|
|
|
let upserted = 0;
|
|
let skipped = 0;
|
|
|
|
for (const host of hosts) {
|
|
// Only process ICMP/WAN hosts — they have an agent interface with an IP
|
|
const iface = (host.interfaces ?? []).find(i => Number(i.useip) === 1 && i.ip && i.ip !== '127.0.0.1');
|
|
if (!iface) { skipped++; continue; }
|
|
|
|
const getMacro = (name: string) =>
|
|
(host.macros ?? []).find(m => m.macro === name)?.value ?? null;
|
|
|
|
const getTag = (name: string) =>
|
|
(host.tags ?? []).find(t => t.tag === name)?.value ?? null;
|
|
|
|
const rmmSiteUid = getMacro('{$RMM_SITE_UID}');
|
|
const autotaskCompanyId = getMacro('{$AUTOTASK_COMPANY_ID}');
|
|
const autotaskCompanyName = getMacro('{$AUTOTASK_COMPANY_NAME}');
|
|
const ispName = getMacro('{$ISP_NAME}');
|
|
const asn = getMacro('{$ASN}');
|
|
|
|
const isMultiWan = getTag('multi-wan') === 'true';
|
|
const source = getTag('source') ?? 'datto-rmm';
|
|
|
|
const problem = problemByHostId.get(host.hostid);
|
|
|
|
await postgresClient.query(
|
|
`INSERT INTO zabbix_wan_hosts (
|
|
hostid, host_name, display_name, wan_ip, status,
|
|
rmm_site_uid, autotask_company_id, autotask_company_name,
|
|
isp_name, asn, is_multi_wan, source, tags,
|
|
last_problem_at, last_problem_name,
|
|
last_synced_at, updated_at
|
|
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,NOW(),NOW())
|
|
ON CONFLICT (hostid) DO UPDATE SET
|
|
host_name = EXCLUDED.host_name,
|
|
display_name = EXCLUDED.display_name,
|
|
wan_ip = EXCLUDED.wan_ip,
|
|
status = EXCLUDED.status,
|
|
rmm_site_uid = EXCLUDED.rmm_site_uid,
|
|
autotask_company_id = EXCLUDED.autotask_company_id,
|
|
autotask_company_name = EXCLUDED.autotask_company_name,
|
|
isp_name = EXCLUDED.isp_name,
|
|
asn = EXCLUDED.asn,
|
|
is_multi_wan = EXCLUDED.is_multi_wan,
|
|
source = EXCLUDED.source,
|
|
tags = EXCLUDED.tags,
|
|
last_problem_at = EXCLUDED.last_problem_at,
|
|
last_problem_name = EXCLUDED.last_problem_name,
|
|
last_synced_at = NOW(),
|
|
updated_at = NOW()`,
|
|
[
|
|
host.hostid,
|
|
host.host,
|
|
host.name ?? host.host,
|
|
iface.ip,
|
|
Number(host.status ?? 0),
|
|
rmmSiteUid,
|
|
autotaskCompanyId ? Number(autotaskCompanyId) : null,
|
|
autotaskCompanyName,
|
|
ispName,
|
|
asn,
|
|
isMultiWan,
|
|
source,
|
|
JSON.stringify(host.tags ?? []),
|
|
problem ? new Date(Number(problem.clock) * 1000).toISOString() : null,
|
|
problem?.name ?? null,
|
|
]
|
|
);
|
|
upserted++;
|
|
}
|
|
|
|
// Remove hosts no longer in Zabbix
|
|
const liveIds = hosts.map(h => h.hostid);
|
|
if (liveIds.length > 0) {
|
|
const deleted = await postgresClient.query(
|
|
'DELETE FROM zabbix_wan_hosts WHERE hostid <> ALL($1) RETURNING hostid',
|
|
[liveIds]
|
|
);
|
|
return NextResponse.json({ upserted, skipped, removed: deleted.rowCount });
|
|
}
|
|
|
|
return NextResponse.json({ upserted, skipped, removed: 0 });
|
|
} catch (err) {
|
|
const msg = err instanceof Error ? err.message : String(err);
|
|
return NextResponse.json({ error: msg }, { status: 500 });
|
|
}
|
|
}
|