- 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.3 KiB
TypeScript
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 });
|
|
}
|
|
}
|