wulf-pulse/app/api/zabbix/sync-hosts/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

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