wulf-pulse/app/api/zabbix/sync-events/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.3 KiB
TypeScript

/**
* POST /api/zabbix/sync-events
* Polls Zabbix for WAN problem events (past N days) and caches them in zabbix_events.
* Joins against local zabbix_wan_hosts to enrich with company/site context.
* Body: { days?: number } — defaults to 30
*/
import { NextRequest, NextResponse } from 'next/server';
import { ZabbixClient } from '@/lib/services/zabbix-client';
import { postgresClient } from '@/lib/services/postgres-client';
export const maxDuration = 300;
export async function POST(request: NextRequest) {
if (!process.env.ZABBIX_API_URL || !process.env.ZABBIX_API_TOKEN) {
return NextResponse.json({ error: 'Zabbix not configured' }, { status: 500 });
}
const body = await request.json().catch(() => ({}));
const days: number = Number(body.days ?? 30);
const zabbix = new ZabbixClient({
apiUrl: process.env.ZABBIX_API_URL,
apiToken: process.env.ZABBIX_API_TOKEN,
});
try {
// Load local WAN host cache for enrichment (hostid → context)
const hostRows = await postgresClient.query<{
hostid: string;
display_name: string;
wan_ip: string;
autotask_company_id: number | null;
autotask_company_name: string | null;
rmm_site_uid: string | null;
isp_name: string | null;
}>('SELECT hostid, display_name, wan_ip, autotask_company_id, autotask_company_name, rmm_site_uid, isp_name FROM zabbix_wan_hosts');
const hostMap = new Map(hostRows.rows.map(r => [r.hostid, r]));
if (hostMap.size === 0) {
return NextResponse.json({ error: 'No Zabbix hosts cached — run Sync Hosts first' }, { status: 400 });
}
const from = new Date(Date.now() - days * 86400 * 1000);
const to = new Date();
// Pull WAN-host-only events by scoping to our known hostids
const hostIds = [...hostMap.keys()];
const events = await zabbix.getEvents({ hostIds, from, to, limit: 10000 });
let upserted = 0;
let noHost = 0;
for (const ev of events) {
const hostid = ev.hosts?.[0]?.hostid ?? null;
const host = hostid ? hostMap.get(hostid) ?? null : null;
if (!host) { noHost++; continue; }
const clockTs = new Date(Number(ev.clock) * 1000);
const rClockTs = ev.r_clock && ev.r_clock !== '0'
? new Date(Number(ev.r_clock) * 1000)
: null;
const duration = rClockTs
? Math.round((rClockTs.getTime() - clockTs.getTime()) / 1000)
: null;
await postgresClient.query(
`INSERT INTO zabbix_events (
eventid, objectid, name, severity, clock,
r_eventid, r_clock, duration_seconds,
hostid, host_name, wan_ip,
autotask_company_id, autotask_company_name, rmm_site_uid, isp_name,
last_synced_at
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,NOW())
ON CONFLICT (eventid) DO UPDATE SET
r_eventid = EXCLUDED.r_eventid,
r_clock = EXCLUDED.r_clock,
duration_seconds = EXCLUDED.duration_seconds,
host_name = EXCLUDED.host_name,
wan_ip = EXCLUDED.wan_ip,
autotask_company_id = EXCLUDED.autotask_company_id,
autotask_company_name = EXCLUDED.autotask_company_name,
rmm_site_uid = EXCLUDED.rmm_site_uid,
isp_name = EXCLUDED.isp_name,
last_synced_at = NOW()`,
[
ev.eventid,
ev.objectid,
ev.name,
Number(ev.severity),
clockTs,
ev.r_eventid && ev.r_eventid !== '0' ? ev.r_eventid : null,
rClockTs,
duration,
hostid,
host.display_name,
host.wan_ip,
host.autotask_company_id,
host.autotask_company_name,
host.rmm_site_uid,
host.isp_name,
]
);
upserted++;
}
const stats = await postgresClient.query<{ total: string; open: string; oldest: string; newest: string }>(`
SELECT COUNT(*) AS total,
COUNT(*) FILTER (WHERE r_eventid IS NULL) AS open,
MIN(clock) AS oldest,
MAX(clock) AS newest
FROM zabbix_events
`);
return NextResponse.json({ upserted, no_host: noHost, days, ...stats.rows[0] });
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return NextResponse.json({ error: msg }, { status: 500 });
}
}