From b77df2f2f1fecb3f404fb5528941c0ad35500a46 Mon Sep 17 00:00:00 2001 From: Pulse Monitor Date: Tue, 12 Aug 2025 14:03:13 +0000 Subject: [PATCH] feat: add custom webhook payload templates and fix Telegram chat_id handling - Add custom JSON payload template support for generic webhooks - Users can now define custom webhook formats with Go template syntax - Fix Telegram webhook issue where chat_id in URL caused 400 errors - Automatically strip chat_id from URL and place in JSON body for Telegram - Add comprehensive webhook documentation with examples - Update API documentation with webhook endpoints Addresses #305 --- README.md | 11 + RELEASE_NOTES_v4.3.0.md | 42 --- docs/API.md | 53 ++++ docs/WEBHOOKS.md | 262 ++++++++++++++++++ .../src/components/Alerts/WebhookConfig.tsx | 44 ++- .../src/components/Alerts/WebhookConfig.tsx | 44 ++- internal/api/notifications.go | 33 ++- internal/notifications/notifications.go | 107 ++++++- internal/notifications/webhook_enhanced.go | 25 +- internal/notifications/webhook_templates.go | 2 +- 10 files changed, 542 insertions(+), 81 deletions(-) delete mode 100644 RELEASE_NOTES_v4.3.0.md create mode 100644 docs/WEBHOOKS.md diff --git a/README.md b/README.md index 44f0f6be8..a1d3da6ac 100644 --- a/README.md +++ b/README.md @@ -243,6 +243,17 @@ docker logs pulse journalctl -u pulse -f ``` +## Documentation + +- [Configuration Guide](docs/CONFIGURATION.md) - Complete setup and configuration +- [API Reference](docs/API.md) - REST API endpoints and examples +- [Webhook Guide](docs/WEBHOOKS.md) - Setting up webhooks and custom payloads +- [Reverse Proxy Setup](docs/REVERSE_PROXY.md) - nginx, Caddy, Apache, Traefik configs +- [PBS Agent](docs/PBS-AGENT.md) - Monitoring isolated PBS servers +- [Security](docs/SECURITY.md) - Security features and best practices +- [FAQ](docs/FAQ.md) - Common questions and troubleshooting +- [Migration Guide](docs/MIGRATION_V3_TO_V4.md) - Upgrading from v3 to v4 + ## Security - Credentials stored encrypted (AES-256-GCM) diff --git a/RELEASE_NOTES_v4.3.0.md b/RELEASE_NOTES_v4.3.0.md deleted file mode 100644 index dbe57830b..000000000 --- a/RELEASE_NOTES_v4.3.0.md +++ /dev/null @@ -1,42 +0,0 @@ -# Pulse v4.3.0 Release Notes - -## ⚠️ BREAKING CHANGES - -### Frontend Now Embedded in Binary -Starting with v4.3.0, the frontend is embedded directly in the Pulse binary. This is a significant architectural change that simplifies deployment but has some implications: - -**What Changed:** -- Frontend files are now compiled into the Go binary at build time -- No separate `frontend-modern/` directory needed -- Single binary deployment - truly portable -- Smaller release tarballs - -**Migration Notes:** -- **For manual installations**: Simply replace your binary. The old `frontend-modern/` directory can be deleted -- **For Docker users**: No changes needed, containers will work as normal -- **For developers**: Frontend changes now require recompiling the Go binary (use `scripts/rebuild-frontend.sh`) -- **Auto-updates**: Will work seamlessly from v4.2.1 to v4.3.0+ - -**Benefits:** -- Eliminates redirect loop issues (#304) -- No more path resolution problems -- Simpler installation and deployment -- Consistent behavior across all installation methods -- Reduced deployment size - -## Features -- Embedded frontend for simplified single-binary deployment -- Improved installation experience - -## Bug Fixes -- Fixed redirect loops in various installation scenarios (#304) -- Fixed path resolution issues with community scripts vs manual installs - -## Technical Details -The frontend is embedded using Go's `embed` package. During the build process: -1. Frontend is built with npm -2. Built files are copied to `internal/api/frontend-modern/` -3. Go compiler embeds these files into the binary -4. Runtime serves embedded files directly from memory - -This change makes Pulse a true single-binary application, eliminating an entire class of deployment issues. \ No newline at end of file diff --git a/docs/API.md b/docs/API.md index 010756f7b..46bc6cd47 100644 --- a/docs/API.md +++ b/docs/API.md @@ -177,6 +177,59 @@ POST /api/notifications/email/test # Send test email GET /api/notifications/email-providers # List email providers ``` +### Webhook Configuration +Manage webhook notification endpoints. + +```bash +GET /api/notifications/webhooks # List all webhooks +POST /api/notifications/webhooks # Create new webhook +PUT /api/notifications/webhooks/ # Update webhook +DELETE /api/notifications/webhooks/ # Delete webhook +POST /api/notifications/webhooks/test # Test webhook +GET /api/notifications/webhook-templates # Get service templates +``` + +#### Create Webhook Example +```bash +curl -X POST http://localhost:7655/api/notifications/webhooks \ + -H "Content-Type: application/json" \ + -H "X-API-Token: your-token" \ + -d '{ + "name": "Discord Alert", + "url": "https://discord.com/api/webhooks/xxx/yyy", + "method": "POST", + "service": "discord", + "enabled": true + }' +``` + +#### Custom Payload Template Example +```bash +curl -X POST http://localhost:7655/api/notifications/webhooks \ + -H "Content-Type: application/json" \ + -H "X-API-Token: your-token" \ + -d '{ + "name": "Custom Webhook", + "url": "https://my-service.com/webhook", + "method": "POST", + "service": "generic", + "enabled": true, + "template": "{\"alert\": \"{{.Level}}: {{.Message}}\", \"value\": {{.Value}}}" + }' +``` + +#### Test Webhook +```bash +curl -X POST http://localhost:7655/api/notifications/webhooks/test \ + -H "Content-Type: application/json" \ + -H "X-API-Token: your-token" \ + -d '{ + "name": "Test", + "url": "https://example.com/webhook", + "service": "generic" + }' +``` + ### Alert Thresholds Manage alert thresholds and overrides. diff --git a/docs/WEBHOOKS.md b/docs/WEBHOOKS.md new file mode 100644 index 000000000..73f658866 --- /dev/null +++ b/docs/WEBHOOKS.md @@ -0,0 +1,262 @@ +# Webhook Configuration Guide + +Pulse supports sending alert notifications to various webhook services including Discord, Slack, Microsoft Teams, Telegram, PagerDuty, and any custom webhook endpoint. + +## Quick Start + +1. Navigate to **Settings** → **Alerts** → **Webhooks** +2. Click **Add Webhook** +3. Select your service type or choose "Generic" for custom webhooks +4. Enter the webhook URL and configure settings +5. Test the webhook to ensure it's working +6. Save your configuration + +## Supported Services + +### Discord +``` +URL Format: https://discord.com/api/webhooks/{webhook_id}/{webhook_token} +``` +1. In Discord, go to Server Settings → Integrations → Webhooks +2. Create a new webhook and copy the URL +3. Paste the URL in Pulse + +### Telegram +``` +URL Format: https://api.telegram.org/bot{bot_token}/sendMessage?chat_id={chat_id} +``` +1. Create a bot with @BotFather on Telegram +2. Get your bot token +3. Get your chat ID by messaging the bot and visiting: `https://api.telegram.org/bot/getUpdates` +4. Use the URL format above (Pulse will handle chat_id correctly) + +### Slack +``` +URL Format: https://hooks.slack.com/services/{webhook_path} +``` +1. In Slack, go to Apps → Incoming Webhooks +2. Add to Slack and choose a channel +3. Copy the webhook URL + +### Microsoft Teams +``` +URL Format: https://{tenant}.webhook.office.com/webhookb2/{webhook_path} +``` +1. In Teams channel, click ... → Connectors +2. Configure Incoming Webhook +3. Copy the URL + +### PagerDuty +``` +URL: https://events.pagerduty.com/v2/enqueue +``` +1. In PagerDuty, go to Configuration → Services +2. Add an integration → Events API V2 +3. Copy the Integration Key +4. Add the key as a header: `routing_key: YOUR_KEY` + +## Custom Payload Templates + +For generic webhooks, you can define custom JSON payloads using Go template syntax. + +### Available Variables + +| Variable | Description | Example Value | +|----------|-------------|---------------| +| `{{.ID}}` | Alert ID | "alert-123" | +| `{{.Level}}` | Alert level | "warning", "critical" | +| `{{.Type}}` | Resource type | "cpu", "memory", "disk" | +| `{{.ResourceName}}` | Name of the resource | "Web Server VM" | +| `{{.ResourceID}}` | Resource identifier | "vm-100" | +| `{{.Node}}` | Proxmox node name | "pve-node-01" | +| `{{.Instance}}` | Proxmox instance URL | "https://192.168.1.100:8006" | +| `{{.Message}}` | Alert message | "CPU usage exceeded 90%" | +| `{{.Value}}` | Current metric value | 95.5 | +| `{{.Threshold}}` | Alert threshold | 90.0 | +| `{{.Duration}}` | How long alert has been active | "5m" | +| `{{.Timestamp}}` | Current timestamp | "2024-01-15T10:30:00Z" | +| `{{.StartTime}}` | When alert started | "2024-01-15T10:25:00Z" | + +### Template Functions + +| Function | Description | Example | +|----------|-------------|---------| +| `{{.Level \| title}}` | Capitalize first letter | "Warning" | +| `{{.Level \| upper}}` | Uppercase | "WARNING" | +| `{{.Level \| lower}}` | Lowercase | "warning" | +| `{{printf "%.1f" .Value}}` | Format numbers | "95.5" | + +### Example Templates + +#### Simple JSON +```json +{ + "text": "Alert: {{.Level}} - {{.Message}}", + "resource": "{{.ResourceName}}", + "value": {{.Value}}, + "threshold": {{.Threshold}} +} +``` + +#### Formatted Alert +```json +{ + "alert": { + "level": "{{.Level | upper}}", + "message": "{{.Message}}", + "details": { + "resource": "{{.ResourceName}}", + "node": "{{.Node}}", + "current_value": "{{printf "%.1f" .Value}}%", + "threshold": "{{printf "%.0f" .Threshold}}%", + "duration": "{{.Duration}}" + } + }, + "timestamp": "{{.Timestamp}}" +} +``` + +#### Slack-Compatible Custom Format +```json +{ + "text": "Pulse Alert", + "attachments": [{ + "color": "{{if eq .Level "critical"}}danger{{else}}warning{{end}}", + "title": "{{.Level | title}} Alert: {{.ResourceName}}", + "text": "{{.Message}}", + "fields": [ + {"title": "Value", "value": "{{printf "%.1f" .Value}}%", "short": true}, + {"title": "Threshold", "value": "{{printf "%.0f" .Threshold}}%", "short": true}, + {"title": "Node", "value": "{{.Node}}", "short": true}, + {"title": "Duration", "value": "{{.Duration}}", "short": true} + ], + "footer": "Pulse Monitoring", + "ts": {{.Timestamp}} + }] +} +``` + +#### Home Assistant +```json +{ + "title": "Pulse Alert: {{.Level | title}}", + "message": "{{.Message}}", + "data": { + "entity_id": "sensor.{{.Node | lower}}_{{.Type}}", + "state": {{.Value}}, + "attributes": { + "resource": "{{.ResourceName}}", + "threshold": {{.Threshold}}, + "duration": "{{.Duration}}" + } + } +} +``` + +#### n8n / Node-RED +```json +{ + "workflow": "pulse_alert", + "data": { + "alert_id": "{{.ID}}", + "level": "{{.Level}}", + "resource": "{{.ResourceName}}", + "node": "{{.Node}}", + "metric": { + "type": "{{.Type}}", + "value": {{.Value}}, + "threshold": {{.Threshold}} + }, + "message": "{{.Message}}", + "timestamp": "{{.Timestamp}}" + } +} +``` + +## Testing Webhooks + +1. After configuring a webhook, click the **Test** button +2. Pulse will send a test alert to verify the webhook is working +3. Check the receiving service to confirm the message arrived +4. If the test fails, verify: + - The URL is correct and accessible + - Any required authentication tokens are included + - The payload format matches what the service expects + +## Troubleshooting + +### Webhook Returns 400 Bad Request +- Check if the payload format is correct for your service +- For Telegram, ensure chat_id is in the URL (Pulse handles it automatically) +- Verify all required fields are present in custom templates + +### Webhook Returns 401/403 +- Check authentication tokens/keys +- Verify the webhook URL hasn't expired +- Ensure IP restrictions allow Pulse server + +### No Notifications Received +- Verify the webhook is enabled +- Check alert thresholds are configured correctly +- Ensure notification cooldown period has passed +- Test the webhook manually using the Test button + +## API Reference + +### Create Webhook +```bash +POST /api/notifications/webhooks +Content-Type: application/json + +{ + "name": "My Webhook", + "url": "https://example.com/webhook", + "method": "POST", + "service": "generic", + "enabled": true, + "template": "{\"alert\": \"{{.Message}}\"}" +} +``` + +### Test Webhook +```bash +POST /api/notifications/webhooks/test +Content-Type: application/json + +{ + "name": "Test", + "url": "https://example.com/webhook", + "service": "generic", + "template": "{\"test\": true}" +} +``` + +### Update Webhook +```bash +PUT /api/notifications/webhooks/{id} +Content-Type: application/json + +{ + "name": "Updated Webhook", + "url": "https://example.com/new-webhook", + "enabled": false +} +``` + +### Delete Webhook +```bash +DELETE /api/notifications/webhooks/{id} +``` + +### List Webhooks +```bash +GET /api/notifications/webhooks +``` + +## Security Considerations + +- **Never expose webhook URLs publicly** - they often contain authentication tokens +- **Use HTTPS URLs** when possible to encrypt data in transit +- **Rotate webhook URLs periodically** if they contain embedded tokens +- **Test webhooks carefully** to avoid sending test data to production channels +- **Limit webhook permissions** in the receiving service where possible \ No newline at end of file diff --git a/frontend-modern/src/components/Alerts/WebhookConfig.tsx b/frontend-modern/src/components/Alerts/WebhookConfig.tsx index e90678d90..bfc3f277a 100644 --- a/frontend-modern/src/components/Alerts/WebhookConfig.tsx +++ b/frontend-modern/src/components/Alerts/WebhookConfig.tsx @@ -23,13 +23,14 @@ interface WebhookConfigProps { export function WebhookConfig(props: WebhookConfigProps) { const [adding, setAdding] = createSignal(false); const [editingId, setEditingId] = createSignal(null); - const [formData, setFormData] = createSignal & { service: string }>({ + const [formData, setFormData] = createSignal & { service: string; payloadTemplate?: string }>({ name: '', url: '', method: 'POST', service: 'generic', headers: { 'Content-Type': 'application/json' }, - enabled: true + enabled: true, + payloadTemplate: '' }); const [templates, setTemplates] = createSignal([]); const [showServiceDropdown, setShowServiceDropdown] = createSignal(false); @@ -60,7 +61,8 @@ export function WebhookConfig(props: WebhookConfigProps) { method: data.method, headers: data.headers, enabled: data.enabled, - service: data.service + service: data.service, + template: data.payloadTemplate }; props.onAdd(newWebhook); // Reset form but keep adding state true @@ -70,7 +72,8 @@ export function WebhookConfig(props: WebhookConfigProps) { method: 'POST', service: 'generic', headers: { 'Content-Type': 'application/json' }, - enabled: true + enabled: true, + payloadTemplate: '' }); } }; @@ -84,7 +87,8 @@ export function WebhookConfig(props: WebhookConfigProps) { method: 'POST', service: 'generic', headers: { 'Content-Type': 'application/json' }, - enabled: true + enabled: true, + payloadTemplate: '' }); }; @@ -92,7 +96,8 @@ export function WebhookConfig(props: WebhookConfigProps) { setEditingId(webhook.id!); setFormData({ ...webhook, - service: webhook.service || 'generic' + service: webhook.service || 'generic', + payloadTemplate: webhook.template || '' }); setAdding(true); }; @@ -284,6 +289,33 @@ export function WebhookConfig(props: WebhookConfigProps) { /> + {/* Custom Payload Template - only show for generic service */} + +
+ +