mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-22 11:13:26 +00:00
fix: webhook configuration persistence and Discord 400 errors
- Add webhook configuration to structured config response in configApi - Update settings UI to properly display webhook enabled state - Fix Discord webhook 400 errors by sending platform-specific payloads - Detect webhook type (Discord/Slack) and send appropriate format - Preserve webhook settings when saving configuration - Update both test webhook and alert webhook sending logic Fixes #119 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+113
-49
@@ -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 = {
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+146
-85
@@ -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, {
|
||||
|
||||
@@ -552,7 +552,7 @@ PulseApp.ui.settings = (() => {
|
||||
Webhook URL
|
||||
</label>
|
||||
<input type="url" name="WEBHOOK_URL"
|
||||
value="${config.WEBHOOK_URL || ''}"
|
||||
value="${(config.advanced && config.advanced.webhook && config.advanced.webhook.url) || config.WEBHOOK_URL || ''}"
|
||||
placeholder="https://discord.com/api/webhooks/..."
|
||||
class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Discord, Slack, Teams, or any webhook endpoint</p>
|
||||
@@ -561,7 +561,7 @@ PulseApp.ui.settings = (() => {
|
||||
|
||||
<div class="flex items-center space-x-4 mb-4">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" name="WEBHOOK_ENABLED" ${config.WEBHOOK_ENABLED === 'true' ? 'checked' : ''}
|
||||
<input type="checkbox" name="WEBHOOK_ENABLED" ${(config.advanced && config.advanced.webhook && config.advanced.webhook.enabled) || config.WEBHOOK_ENABLED === 'true' ? 'checked' : ''}
|
||||
class="mr-2 h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">Enable Webhook Notifications</span>
|
||||
</label>
|
||||
|
||||
Reference in New Issue
Block a user