diff --git a/server/alertManager.js b/server/alertManager.js index 08829afb2..e4a5dba1f 100644 --- a/server/alertManager.js +++ b/server/alertManager.js @@ -2,6 +2,7 @@ const EventEmitter = require('events'); const fs = require('fs').promises; const path = require('path'); const nodemailer = require('nodemailer'); +const axios = require('axios'); const customThresholdManager = require('./customThresholds'); class AlertManager extends EventEmitter { @@ -1134,15 +1135,135 @@ This alert was generated by Pulse monitoring system. } /** - * Send webhook notification (placeholder for future implementation) + * Send webhook notification */ async sendWebhookNotification(channel, alert) { if (!channel.config.url) { throw new Error('Webhook URL not configured'); } - - // Placeholder for webhook implementation - console.log(`[WEBHOOK] Would send to: ${channel.config.url}`); + + const severityEmoji = { + 'info': '๐', + 'warning': 'โ ๏ธ', + 'critical': '๐จ' + }; + + const payload = { + timestamp: new Date(alert.timestamp).toISOString(), + alert: { + id: alert.id, + rule: { + name: alert.rule.name, + description: alert.rule.description, + severity: alert.rule.severity, + metric: alert.rule.metric + }, + guest: { + name: alert.guest.name, + id: alert.guest.id, + type: alert.guest.type, + node: alert.guest.node, + status: alert.guest.status + }, + value: alert.value, + threshold: alert.threshold, + emoji: severityEmoji[alert.rule.severity] || '๐ข' + }, + // Discord/Slack compatible format + embeds: [{ + title: `${severityEmoji[alert.rule.severity] || '๐ข'} ${alert.rule.name}`, + description: alert.rule.description, + color: alert.rule.severity === 'critical' ? 15158332 : // Red + alert.rule.severity === 'warning' ? 15844367 : // Orange + 3447003, // Blue + fields: [ + { + name: 'VM/LXC', + value: `${alert.guest.name} (${alert.guest.type} ${alert.guest.id})`, + inline: true + }, + { + name: 'Node', + value: alert.guest.node, + inline: true + }, + { + name: 'Status', + value: alert.guest.status, + inline: true + }, + { + name: 'Metric', + value: alert.rule.metric.toUpperCase(), + inline: true + }, + { + name: 'Current Value', + value: `${alert.value}%`, + inline: true + }, + { + name: 'Threshold', + value: `${alert.threshold}%`, + inline: true + } + ], + footer: { + text: 'Pulse Monitoring System' + }, + timestamp: new Date(alert.timestamp).toISOString() + }], + // Slack compatible format + text: `${severityEmoji[alert.rule.severity] || '๐ข'} *${alert.rule.name}*`, + attachments: [{ + color: alert.rule.severity === 'critical' ? 'danger' : + alert.rule.severity === 'warning' ? 'warning' : 'good', + fields: [ + { + title: 'VM/LXC', + value: `${alert.guest.name} (${alert.guest.type} ${alert.guest.id})`, + short: true + }, + { + title: 'Node', + value: alert.guest.node, + short: true + }, + { + title: 'Metric', + value: `${alert.rule.metric.toUpperCase()}: ${alert.value}% (threshold: ${alert.threshold}%)`, + short: false + } + ], + footer: 'Pulse Monitoring', + ts: Math.floor(alert.timestamp / 1000) + }] + }; + + // Set appropriate headers + const headers = { + 'Content-Type': 'application/json', + 'User-Agent': 'Pulse-Monitoring/1.0', + ...channel.config.headers + }; + + try { + const response = await axios.post(channel.config.url, payload, { + headers, + timeout: 10000, // 10 second timeout + maxRedirects: 3 + }); + + console.log(`[WEBHOOK] Alert sent to: ${channel.config.url} (${response.status})`); + } catch (error) { + if (error.response) { + throw new Error(`Webhook failed: ${error.response.status} ${error.response.statusText}`); + } else if (error.request) { + throw new Error(`Webhook failed: No response from ${channel.config.url}`); + } else { + throw new Error(`Webhook failed: ${error.message}`); + } + } } destroy() { diff --git a/server/index.js b/server/index.js index 39b5e4b50..3d6d62f81 100644 --- a/server/index.js +++ b/server/index.js @@ -861,6 +861,145 @@ Pulse Monitoring System } }); +// Test webhook endpoint +app.post('/api/test-webhook', async (req, res) => { + try { + const { url, enabled } = req.body; + + if (!url) { + return res.status(400).json({ + success: false, + error: 'Webhook URL is required for testing' + }); + } + + // Create test webhook payload + const axios = require('axios'); + const testPayload = { + timestamp: new Date().toISOString(), + alert: { + id: 'test-alert-' + Date.now(), + rule: { + name: 'Webhook Test Alert', + description: 'This is a test alert to verify webhook configuration', + severity: 'info', + metric: 'test' + }, + guest: { + name: 'Test-VM', + id: '999', + type: 'qemu', + node: 'test-node', + status: 'running' + }, + value: 75, + threshold: 80, + emoji: '๐งช' + }, + // Discord/Slack compatible format + embeds: [{ + title: '๐งช Webhook Test Alert', + description: 'This is a test alert to verify webhook configuration', + color: 3447003, // Blue + fields: [ + { + name: 'VM/LXC', + value: 'Test-VM (qemu 999)', + inline: true + }, + { + name: 'Node', + value: 'test-node', + inline: true + }, + { + name: 'Status', + value: 'running', + inline: true + }, + { + name: 'Metric', + value: 'TEST', + inline: true + }, + { + name: 'Current Value', + value: '75%', + inline: true + }, + { + name: 'Threshold', + value: '80%', + inline: true + } + ], + footer: { + text: 'Pulse Monitoring System - Test Message' + }, + timestamp: new Date().toISOString() + }], + // Slack compatible format + text: '๐งช *Webhook Test Alert*', + attachments: [{ + color: 'good', + fields: [ + { + title: 'VM/LXC', + value: 'Test-VM (qemu 999)', + short: true + }, + { + title: 'Node', + value: 'test-node', + short: true + }, + { + title: 'Status', + value: 'Webhook configuration test successful!', + short: false + } + ], + footer: 'Pulse Monitoring - Test', + ts: Math.floor(Date.now() / 1000) + }] + }; + + // Send test webhook + const response = await axios.post(url, testPayload, { + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'Pulse-Monitoring/1.0' + }, + timeout: 10000, // 10 second timeout + maxRedirects: 3 + }); + + console.log(`[WEBHOOK TEST] Test webhook sent successfully to: ${url} (${response.status})`); + res.json({ + success: true, + message: 'Test webhook sent successfully!', + status: response.status + }); + + } catch (error) { + console.error('[WEBHOOK TEST] Failed to send test webhook:', error); + + let errorMessage = 'Failed to send test webhook'; + if (error.response) { + errorMessage = `Webhook failed: ${error.response.status} ${error.response.statusText}`; + } else if (error.request) { + errorMessage = `Webhook failed: No response from ${url}`; + } else { + errorMessage = `Webhook failed: ${error.message}`; + } + + res.status(400).json({ + success: false, + error: errorMessage + }); + } +}); + // Global error handler for unhandled API errors app.use((err, req, res, next) => { console.error('Unhandled API error:', err); diff --git a/src/public/js/ui/settings.js b/src/public/js/ui/settings.js index 8fe1c51be..17dd1fb14 100644 --- a/src/public/js/ui/settings.js +++ b/src/public/js/ui/settings.js @@ -186,8 +186,9 @@ PulseApp.ui.settings = (() => { } else if (activeTab === 'alerts') { // Load threshold configurations when alerts tab is opened loadThresholdConfigurations(); - // Setup email test button + // Setup email and webhook test buttons setupEmailTestButton(); + setupWebhookTestButton(); } } @@ -540,6 +541,55 @@ PulseApp.ui.settings = (() => { + +
Send alert notifications to Discord, Slack, Teams, or any webhook-compatible service.
+ +Discord, Slack, Teams, or any webhook endpoint
+
+ Popular services:
+ โข Discord: Server Settings โ Integrations โ Webhooks
+ โข Slack: Apps โ Incoming Webhooks
+ โข Teams: Channel โ Connectors โ Incoming Webhook
+