diff --git a/internal/alerts/alerts.go b/internal/alerts/alerts.go index a317ad0d2..ae5a39ebe 100644 --- a/internal/alerts/alerts.go +++ b/internal/alerts/alerts.go @@ -530,6 +530,8 @@ type Manager struct { // When a host agent is running on a Proxmox node, we prefer the host agent // alerts and suppress the node alerts to avoid duplicate monitoring. hostAgentHostnames map[string]struct{} // Normalized hostnames (lowercase) + // License checking for Pro-only alert features + hasProFeature func(feature string) bool } type ackRecord struct { @@ -715,6 +717,14 @@ func NewManager() *Manager { return m } +// SetLicenseChecker sets the function used to check Pro license features. +// This enables gating Pro-only alert features like update alerts. +func (m *Manager) SetLicenseChecker(checker func(feature string) bool) { + m.mu.Lock() + defer m.mu.Unlock() + m.hasProFeature = checker +} + // addRecentlyResolvedUnlocked records a resolved alert assuming the caller does not hold m.mu. func (m *Manager) addRecentlyResolvedUnlocked(alertID string, resolved *ResolvedAlert) { m.resolvedMutex.Lock() @@ -4352,8 +4362,20 @@ func (m *Manager) checkDockerContainerImageUpdate(host models.DockerHost, contai // Check if update detection is enabled m.mu.RLock() delayHours := m.config.DockerDefaults.UpdateAlertDelayHours + hasProFeature := m.hasProFeature m.mu.RUnlock() + // Update alerts are a Pro-only feature + // Free users still see update badges in the UI, but alerts require Pro + if hasProFeature != nil && !hasProFeature("update_alerts") { + // Not licensed for update alerts - clear any existing alert and tracking + m.clearAlert(alertID) + m.mu.Lock() + delete(m.dockerUpdateFirstSeen, resourceID) + m.mu.Unlock() + return + } + // Negative value means disabled if delayHours < 0 { m.clearAlert(alertID) diff --git a/internal/api/router.go b/internal/api/router.go index 47053f3fa..91f81a143 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -1183,6 +1183,16 @@ func (r *Router) setupRoutes() { r.aiSettingsHandler.SetMetadataProvider(metadataProvider) // Wire license checker for Pro feature gating (AI Patrol, Alert Analysis, Auto-Fix) r.aiSettingsHandler.SetLicenseChecker(r.licenseHandlers.Service()) + // Wire license checker for alert manager Pro features (Update Alerts) + if r.monitor != nil { + alertMgr := r.monitor.GetAlertManager() + if alertMgr != nil { + licSvc := r.licenseHandlers.Service() + alertMgr.SetLicenseChecker(func(feature string) bool { + return licSvc.HasFeature(feature) + }) + } + } r.mux.HandleFunc("/api/settings/ai", RequireAdmin(r.config, RequireScope(config.ScopeSettingsRead, r.aiSettingsHandler.HandleGetAISettings))) r.mux.HandleFunc("/api/settings/ai/update", RequireAdmin(r.config, RequireScope(config.ScopeSettingsWrite, r.aiSettingsHandler.HandleUpdateAISettings))) r.mux.HandleFunc("/api/ai/test", RequireAdmin(r.config, RequireScope(config.ScopeSettingsWrite, r.aiSettingsHandler.HandleTestAIConnection))) diff --git a/internal/license/features.go b/internal/license/features.go index 6c7b36e0f..edfc35b9f 100644 --- a/internal/license/features.go +++ b/internal/license/features.go @@ -4,12 +4,15 @@ package license // Feature constants represent gated features in Pulse Pro. // These are embedded in license JWTs and checked at runtime. const ( - // Pro tier features + // Pro tier features - AI FeatureAIPatrol = "ai_patrol" // Background AI health monitoring FeatureAIAlerts = "ai_alerts" // AI analysis when alerts fire FeatureAIAutoFix = "ai_autofix" // Automatic remediation FeatureKubernetesAI = "kubernetes_ai" // AI analysis of K8s (NOT basic monitoring) + // Pro tier features - Monitoring + FeatureUpdateAlerts = "update_alerts" // Alerts for pending container/package updates + // MSP tier features (FUTURE - not in v1 launch) FeatureMultiUser = "multi_user" // RBAC - NOT IMPLEMENTED YET FeatureWhiteLabel = "white_label" // Custom branding - NOT IMPLEMENTED YET @@ -39,24 +42,28 @@ var TierFeatures = map[Tier][]string{ FeatureAIAlerts, FeatureAIAutoFix, FeatureKubernetesAI, + FeatureUpdateAlerts, }, TierProAnnual: { FeatureAIPatrol, FeatureAIAlerts, FeatureAIAutoFix, FeatureKubernetesAI, + FeatureUpdateAlerts, }, TierLifetime: { FeatureAIPatrol, FeatureAIAlerts, FeatureAIAutoFix, FeatureKubernetesAI, + FeatureUpdateAlerts, }, TierMSP: { FeatureAIPatrol, FeatureAIAlerts, FeatureAIAutoFix, FeatureKubernetesAI, + FeatureUpdateAlerts, FeatureUnlimited, // Note: FeatureMultiUser, FeatureWhiteLabel, FeatureMultiTenant // are on the roadmap but NOT included until implemented @@ -66,6 +73,7 @@ var TierFeatures = map[Tier][]string{ FeatureAIAlerts, FeatureAIAutoFix, FeatureKubernetesAI, + FeatureUpdateAlerts, FeatureUnlimited, FeatureMultiUser, FeatureWhiteLabel, @@ -118,6 +126,8 @@ func GetFeatureDisplayName(feature string) string { return "AI Auto-Fix" case FeatureKubernetesAI: return "Kubernetes AI Analysis" + case FeatureUpdateAlerts: + return "Update Alerts (Container/Package Updates)" case FeatureMultiUser: return "Multi-User / RBAC" case FeatureWhiteLabel: