diff --git a/server/alertManager.js b/server/alertManager.js index 6cf229daa..752f7c59b 100644 --- a/server/alertManager.js +++ b/server/alertManager.js @@ -1249,30 +1249,18 @@ This alert was generated by Pulse monitoring system. }; const validTimestamp = this.getValidTimestamp(alert); - - const payload = { - timestamp: new Date(validTimestamp).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: [{ + + // Detect webhook type based on URL + const url = channel.config.url; + const isDiscord = url.includes('discord.com/api/webhooks') || url.includes('discordapp.com/api/webhooks'); + const isSlack = url.includes('slack.com/') || url.includes('hooks.slack.com'); + + let payload; + + if (isDiscord) { + // Discord-specific format + payload = { + embeds: [{ title: `${severityEmoji[alert.rule.severity] || '📢'} ${alert.rule.name}`, description: alert.rule.description, color: alert.rule.severity === 'critical' ? 15158332 : // Red @@ -1314,33 +1302,109 @@ This alert was generated by Pulse monitoring system. text: 'Pulse Monitoring System' }, timestamp: new Date(validTimestamp).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 + }] + }; + } else if (isSlack) { + // Slack-specific format + payload = { + 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(validTimestamp / 1000) + }] + }; + } else { + // Generic webhook format with all fields (backward compatibility) + payload = { + timestamp: new Date(validTimestamp).toISOString(), + alert: { + id: alert.id, + rule: { + name: alert.rule.name, + description: alert.rule.description, + severity: alert.rule.severity, + metric: alert.rule.metric }, - { - title: 'Node', - value: alert.guest.node, - short: true + guest: { + name: alert.guest.name, + id: alert.guest.id, + type: alert.guest.type, + node: alert.guest.node, + status: alert.guest.status }, - { - title: 'Metric', - value: `${alert.rule.metric.toUpperCase()}: ${alert.value}% (threshold: ${alert.threshold}%)`, - short: false - } - ], - footer: 'Pulse Monitoring', - ts: Math.floor(validTimestamp / 1000) - }] - }; + value: alert.value, + threshold: alert.threshold, + emoji: severityEmoji[alert.rule.severity] || '📢' + }, + // Include both formats for generic webhooks + embeds: [{ + title: `${severityEmoji[alert.rule.severity] || '📢'} ${alert.rule.name}`, + description: alert.rule.description, + color: alert.rule.severity === 'critical' ? 15158332 : + alert.rule.severity === 'warning' ? 15844367 : + 3447003, + 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: 'Metric', + value: `${alert.rule.metric.toUpperCase()}: ${alert.value}% (threshold: ${alert.threshold}%)`, + inline: true + } + ], + footer: { + text: 'Pulse Monitoring System' + }, + timestamp: new Date(validTimestamp).toISOString() + }], + 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: 'Metric', + value: `${alert.rule.metric.toUpperCase()}: ${alert.value}% (threshold: ${alert.threshold}%)`, + short: false + } + ], + footer: 'Pulse Monitoring', + ts: Math.floor(validTimestamp / 1000) + }] + }; + } // Set appropriate headers const headers = { diff --git a/server/configApi.js b/server/configApi.js index 5a42b5df1..381bdbbda 100644 --- a/server/configApi.js +++ b/server/configApi.js @@ -64,6 +64,10 @@ class ConfigApi { down: { enabled: config.ALERT_DOWN_ENABLED !== 'false' } + }, + webhook: { + url: config.WEBHOOK_URL, + enabled: config.WEBHOOK_ENABLED === 'true' } } }; @@ -182,6 +186,17 @@ class ConfigApi { existingConfig.ALERT_DOWN_ENABLED = alerts.down.enabled ? 'true' : 'false'; } } + + // Webhook settings + if (config.advanced.webhook) { + const webhook = config.advanced.webhook; + if (webhook.url !== undefined) { + existingConfig.WEBHOOK_URL = webhook.url; + } + if (webhook.enabled !== undefined) { + existingConfig.WEBHOOK_ENABLED = webhook.enabled ? 'true' : 'false'; + } + } } } diff --git a/server/index.js b/server/index.js index 3256692e2..b31b88bc7 100644 --- a/server/index.js +++ b/server/index.js @@ -875,94 +875,155 @@ app.post('/api/test-webhook', async (req, res) => { // 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', + + // Detect webhook type based on URL + const isDiscord = url.includes('discord.com/api/webhooks') || url.includes('discordapp.com/api/webhooks'); + const isSlack = url.includes('slack.com/') || url.includes('hooks.slack.com'); + + let testPayload; + + if (isDiscord) { + // Discord-specific format + testPayload = { + embeds: [{ + title: '🧪 Webhook Test Alert', description: 'This is a test alert to verify webhook configuration', - severity: 'info', - metric: 'test' + 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() + }] + }; + } else if (isSlack) { + // Slack-specific format + testPayload = { + 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) + }] + }; + } else { + // Generic webhook format with all fields (backward compatibility) + 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: '🧪' }, - 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 + // Include both formats for generic webhooks + embeds: [{ + title: '🧪 Webhook Test Alert', + description: 'This is a test alert to verify webhook configuration', + color: 3447003, + fields: [ + { + name: 'VM/LXC', + value: 'Test-VM (qemu 999)', + inline: true + }, + { + name: 'Node', + value: 'test-node', + inline: true + }, + { + name: 'Status', + value: 'running', + inline: true + } + ], + footer: { + text: 'Pulse Monitoring System - Test Message' }, - { - 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) - }] - }; + timestamp: new Date().toISOString() + }], + text: '🧪 *Webhook Test Alert*', + attachments: [{ + color: 'good', + fields: [ + { + title: 'VM/LXC', + value: 'Test-VM (qemu 999)', + 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, { diff --git a/src/public/js/ui/settings.js b/src/public/js/ui/settings.js index 9bb647dbb..1f2fa202e 100644 --- a/src/public/js/ui/settings.js +++ b/src/public/js/ui/settings.js @@ -552,7 +552,7 @@ PulseApp.ui.settings = (() => { Webhook URL

Discord, Slack, Teams, or any webhook endpoint

@@ -561,7 +561,7 @@ PulseApp.ui.settings = (() => {