- Add MorningSummaryService with Zabbix aggregation and adaptive card builder - Add webhook delivery system with Teams incoming webhooks - Add admin UI at /admin/morning-summary for webhook/config management - Add API routes: /send, /test, /webhooks, /webhooks/[id], /config, /history - Register morning-summary cron job in SyncScheduler (Mon-Fri 6:30 AM) - Add outages_only filter (Unavailable triggers only) - Fix host resolution: use getTriggerEnabledHosts to exclude disabled hosts - Fix resolved events: event.get value:1 scoped to window with r_eventid filter - Remove emojis from fact rows and section headers in card - Remove Open Zabbix button (duplicate of View Problems) - Add migrations: morning_summary_config + morning_summaries tables - Add outages_only column to morning_summary_config
142 lines
4.6 KiB
TypeScript
142 lines
4.6 KiB
TypeScript
/**
|
|
* PUT /api/zabbix/hosts/[hostid] — full update of an existing Zabbix host
|
|
*
|
|
* If companyId is provided (or changed), rebuilds host groups / macros / tags
|
|
* via buildHostParams (same as creation flow). Otherwise applies fields directly.
|
|
*/
|
|
|
|
import { NextRequest, NextResponse } from 'next/server';
|
|
import { ZabbixClient } from '@/lib/services/zabbix-client';
|
|
import { postgresClient } from '@/lib/services/postgres-client';
|
|
import { ZabbixHostMacro, ZabbixHostTag } from '@/lib/types/zabbix';
|
|
import {
|
|
sanitizeHostname,
|
|
lookupIsp,
|
|
buildHostParams,
|
|
discoverIcmpTemplate,
|
|
} from '@/lib/services/zabbix-wan-utils';
|
|
|
|
interface UpdateBody {
|
|
name: string;
|
|
ip: string;
|
|
description?: string;
|
|
companyId?: number | null;
|
|
tags?: ZabbixHostTag[];
|
|
macros?: ZabbixHostMacro[];
|
|
rebuildFromClient?: boolean; // if true, fully re-run buildHostParams
|
|
}
|
|
|
|
export async function PUT(
|
|
request: NextRequest,
|
|
{ params }: { params: Promise<{ hostid: string }> }
|
|
) {
|
|
try {
|
|
const { hostid } = await params;
|
|
const body: UpdateBody = await request.json();
|
|
const { name, ip, description, companyId, tags, macros, rebuildFromClient } = body;
|
|
|
|
if (!name || !ip) {
|
|
return NextResponse.json({ error: 'name and ip are required' }, { status: 400 });
|
|
}
|
|
|
|
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
|
|
if (!ipv4Regex.test(ip)) {
|
|
return NextResponse.json({ error: 'Invalid IPv4 address' }, { status: 400 });
|
|
}
|
|
|
|
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!,
|
|
});
|
|
|
|
if (rebuildFromClient) {
|
|
// Full rebuild: re-resolve ISP, rebuild groups/macros/tags from scratch
|
|
let companyName: string | undefined;
|
|
if (companyId) {
|
|
const res = await postgresClient.query<{ company_name: string }>(
|
|
'SELECT company_name FROM companies WHERE id = $1 LIMIT 1',
|
|
[companyId]
|
|
);
|
|
companyName = res.rows[0]?.company_name;
|
|
}
|
|
|
|
const ispInfo = await lookupIsp(ip);
|
|
const globalGroupId = await zabbix.ensureHostGroup('Datto RMM Sites');
|
|
const icmpTemplateId = await discoverIcmpTemplate(zabbix);
|
|
|
|
const hostParams = await buildHostParams({
|
|
siteName: name,
|
|
wanIp: ip,
|
|
companyId: companyId ?? undefined,
|
|
companyName,
|
|
ispInfo,
|
|
source: 'manual',
|
|
icmpTemplateId,
|
|
globalGroupId,
|
|
zabbix,
|
|
});
|
|
|
|
await zabbix['rpc']('host.update', {
|
|
hostid,
|
|
host: sanitizeHostname(name),
|
|
name,
|
|
description: description ?? hostParams.description,
|
|
groups: hostParams.groups,
|
|
templates: hostParams.templates,
|
|
macros: hostParams.macros,
|
|
tags: hostParams.tags,
|
|
});
|
|
|
|
// Update IP interface separately
|
|
const existingHost = await zabbix['rpc']<Array<{ interfaces: Array<{ interfaceid: string; main: number }> }>>('host.get', {
|
|
output: ['hostid'],
|
|
hostids: [hostid],
|
|
selectInterfaces: ['interfaceid', 'main', 'type'],
|
|
});
|
|
const mainIface = existingHost[0]?.interfaces?.find((i: any) => i.main === 1 || i.main === '1');
|
|
if (mainIface) {
|
|
await zabbix['rpc']('hostinterface.update', {
|
|
interfaceid: mainIface.interfaceid,
|
|
ip,
|
|
useip: 1,
|
|
dns: '',
|
|
});
|
|
}
|
|
} else {
|
|
// Direct update — apply exactly what was sent
|
|
await zabbix['rpc']('host.update', {
|
|
hostid,
|
|
host: sanitizeHostname(name),
|
|
name,
|
|
...(description !== undefined ? { description } : {}),
|
|
...(tags !== undefined ? { tags } : {}),
|
|
...(macros !== undefined ? { macros } : {}),
|
|
});
|
|
|
|
// Update IP interface
|
|
const existingHost = await zabbix['rpc']<Array<{ interfaces: Array<{ interfaceid: string; main: number }> }>>('host.get', {
|
|
output: ['hostid'],
|
|
hostids: [hostid],
|
|
selectInterfaces: ['interfaceid', 'main', 'type'],
|
|
});
|
|
const mainIface = existingHost[0]?.interfaces?.find((i: any) => i.main === 1 || i.main === '1');
|
|
if (mainIface) {
|
|
await zabbix['rpc']('hostinterface.update', {
|
|
interfaceid: mainIface.interfaceid,
|
|
ip,
|
|
useip: 1,
|
|
dns: '',
|
|
});
|
|
}
|
|
}
|
|
|
|
return NextResponse.json({ updated: true, hostid });
|
|
} catch (error) {
|
|
console.error('[PUT /api/zabbix/hosts/[hostid]]', error);
|
|
return NextResponse.json({ error: String(error) }, { status: 500 });
|
|
}
|
|
}
|