From 2391329d286e85fc7d324a802fdaf8b7a7bae075 Mon Sep 17 00:00:00 2001 From: Pulse Monitor Date: Thu, 4 Sep 2025 12:07:23 +0000 Subject: [PATCH] fix: provide full Pulse URL in Gotify/ntfy webhook notifications (addresses #415) - Added PULSE_PUBLIC_URL config option to specify the full URL to access Pulse - Updated notification manager to use publicURL when constructing webhook payloads - Modified prepareWebhookData to construct full URLs instead of just paths - Fixed test webhooks to use configured publicURL instead of hardcoded values - Gotify and ntfy notifications now include clickable links that work properly --- internal/config/config.go | 5 ++++ internal/monitoring/monitor.go | 2 +- internal/notifications/notifications.go | 32 +++++++++++++++++++--- internal/notifications/webhook_enhanced.go | 8 +++++- 4 files changed, 41 insertions(+), 6 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 0f4a6dc74..ff9daba37 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -58,6 +58,7 @@ type Config struct { FrontendPort int `envconfig:"FRONTEND_PORT" default:"7655"` ConfigPath string `envconfig:"CONFIG_PATH" default:"/etc/pulse"` DataPath string `envconfig:"DATA_PATH" default:"/var/lib/pulse"` + PublicURL string `envconfig:"PULSE_PUBLIC_URL" default:""` // Full URL to access Pulse (e.g., http://192.168.1.100:7655) // Proxmox VE connections PVEInstances []PVEInstance @@ -480,6 +481,10 @@ func Load() (*Config, error) { cfg.EnvOverrides["allowedOrigins"] = true log.Info().Str("origins", allowedOrigins).Msg("Allowed origins overridden by ALLOWED_ORIGINS env var") } + if publicURL := os.Getenv("PULSE_PUBLIC_URL"); publicURL != "" { + cfg.PublicURL = publicURL + log.Info().Str("url", publicURL).Msg("Public URL configured from PULSE_PUBLIC_URL env var") + } // Set log level switch cfg.LogLevel { diff --git a/internal/monitoring/monitor.go b/internal/monitoring/monitor.go index f42cab79e..aec7e67b6 100644 --- a/internal/monitoring/monitor.go +++ b/internal/monitoring/monitor.go @@ -157,7 +157,7 @@ func New(cfg *config.Config) (*Monitor, error) { rateTracker: NewRateTracker(), metricsHistory: NewMetricsHistory(1000, 24*time.Hour), // Keep up to 1000 points or 24 hours alertManager: alerts.NewManager(), - notificationMgr: notifications.NewNotificationManager(), + notificationMgr: notifications.NewNotificationManager(cfg.PublicURL), configPersist: config.NewConfigPersistence(cfg.DataPath), discoveryService: nil, // Will be initialized in Start() authFailures: make(map[string]int), diff --git a/internal/notifications/notifications.go b/internal/notifications/notifications.go index fe115897e..b00be15e7 100644 --- a/internal/notifications/notifications.go +++ b/internal/notifications/notifications.go @@ -47,6 +47,7 @@ type NotificationManager struct { pendingAlerts []*alerts.Alert groupTimer *time.Timer groupByNode bool + publicURL string // Full URL to access Pulse groupByGuest bool webhookHistory []WebhookDelivery // Keep last 100 webhook deliveries for debugging } @@ -92,7 +93,7 @@ type WebhookConfig struct { } // NewNotificationManager creates a new notification manager -func NewNotificationManager() *NotificationManager { +func NewNotificationManager(publicURL string) *NotificationManager { return &NotificationManager{ enabled: true, cooldown: 5 * time.Minute, @@ -103,6 +104,7 @@ func NewNotificationManager() *NotificationManager { groupByNode: true, groupByGuest: false, webhookHistory: make([]WebhookDelivery, 0, 100), // Pre-allocate for 100 entries + publicURL: publicURL, } } @@ -820,6 +822,19 @@ func (n *NotificationManager) sendWebhook(webhook WebhookConfig, alert *alerts.A func (n *NotificationManager) prepareWebhookData(alert *alerts.Alert, customFields map[string]interface{}) WebhookPayloadData { duration := time.Since(alert.StartTime) + // Construct full Pulse URL if publicURL is configured + instance := alert.Instance + if n.publicURL != "" && alert.Instance != "" { + // If alert.Instance is just the instance name (e.g., "pve-10.0.1.21") + // construct the full URL to view this instance in Pulse + if !strings.HasPrefix(alert.Instance, "http://") && !strings.HasPrefix(alert.Instance, "https://") { + // Remove trailing slash from publicURL if present + publicURL := strings.TrimRight(n.publicURL, "/") + // Construct the full URL with the instance path + instance = fmt.Sprintf("%s/%s", publicURL, alert.Instance) + } + } + return WebhookPayloadData{ ID: alert.ID, Level: string(alert.Level), @@ -827,7 +842,7 @@ func (n *NotificationManager) prepareWebhookData(alert *alerts.Alert, customFiel ResourceName: alert.ResourceName, ResourceID: alert.ResourceID, Node: alert.Node, - Instance: alert.Instance, + Instance: instance, Message: alert.Message, Value: alert.Value, Threshold: alert.Threshold, @@ -1129,6 +1144,12 @@ func (n *NotificationManager) SendTestNotification(method string) error { // SendTestWebhook sends a test notification to a specific webhook func (n *NotificationManager) SendTestWebhook(webhook WebhookConfig) error { // Create a test alert for webhook testing with realistic values + // Use the configured publicURL if available, otherwise use a placeholder + instanceURL := n.publicURL + if instanceURL == "" { + instanceURL = "http://your-pulse-instance:7655" + } + testAlert := &alerts.Alert{ ID: "test-webhook-" + webhook.ID, Type: "cpu", @@ -1136,7 +1157,7 @@ func (n *NotificationManager) SendTestWebhook(webhook WebhookConfig) error { ResourceID: "webhook-test", ResourceName: "Test Alert", Node: "test-node", - Instance: "http://your-pulse-instance:7655", // Placeholder URL for test webhooks + Instance: instanceURL, // Use the actual Pulse URL Message: fmt.Sprintf("This is a test alert from Pulse to verify your %s webhook is working correctly", webhook.Name), Value: 85.5, Threshold: 80.0, @@ -1158,7 +1179,10 @@ func (n *NotificationManager) SendTestWebhook(webhook WebhookConfig) error { func (n *NotificationManager) SendTestNotificationWithConfig(method string, config *EmailConfig, nodeInfo *TestNodeInfo) error { // Use actual node info if provided, otherwise use defaults nodeName := "test-node" - instanceURL := "https://proxmox.local:8006" + instanceURL := n.publicURL + if instanceURL == "" { + instanceURL = "https://proxmox.local:8006" + } if nodeInfo != nil { if nodeInfo.NodeName != "" { nodeName = nodeInfo.NodeName diff --git a/internal/notifications/webhook_enhanced.go b/internal/notifications/webhook_enhanced.go index 7fbd61b0a..355e97516 100644 --- a/internal/notifications/webhook_enhanced.go +++ b/internal/notifications/webhook_enhanced.go @@ -405,6 +405,12 @@ func formatWebhookDuration(d time.Duration) string { // TestEnhancedWebhook tests a webhook with a specific payload func (n *NotificationManager) TestEnhancedWebhook(webhook EnhancedWebhookConfig) (int, string, error) { + // Use the configured publicURL if available, otherwise use a placeholder + instanceURL := n.publicURL + if instanceURL == "" { + instanceURL = "https://192.168.1.100:8006" + } + // Create test alert testAlert := &alerts.Alert{ ID: "test-" + time.Now().Format("20060102-150405"), @@ -413,7 +419,7 @@ func (n *NotificationManager) TestEnhancedWebhook(webhook EnhancedWebhookConfig) ResourceID: "100", ResourceName: "Test VM", Node: "pve-node-01", - Instance: "https://192.168.1.100:8006", + Instance: instanceURL, Message: "Test webhook notification from Pulse Monitoring", Value: 85.5, Threshold: 80.0,