diff --git a/README.md b/README.md index a4da0ea..e44cbbb 100644 --- a/README.md +++ b/README.md @@ -396,7 +396,7 @@ The UI detects your deployment type and reminds you which command sequence to ru Quick start - most settings are in the web UI: - **Settings → Nodes**: Add/remove Proxmox instances -- **Settings → System**: Polling intervals, timeouts, update settings +- **Settings → System**: Monitor cadence (PVE polling), timeouts, update settings - **Settings → Security**: Authentication and API tokens - **Alerts**: Thresholds and notifications diff --git a/cmd/pulse/main.go b/cmd/pulse/main.go index 59aef35..81f3333 100644 --- a/cmd/pulse/main.go +++ b/cmd/pulse/main.go @@ -252,22 +252,14 @@ func runServer() { configWatcher.ReloadConfig() } - // Reload system.json - persistence := config.NewConfigPersistence(cfg.DataPath) - if persistence != nil { - if _, err := persistence.LoadSystemSettings(); err == nil { - // Note: Polling interval is now hardcoded to 10s, no longer configurable - // Could reload other system.json settings here - log.Info().Msg("Reloaded system configuration") + if reloadFunc != nil { + if err := reloadFunc(); err != nil { + log.Error().Err(err).Msg("Failed to reload monitor after SIGHUP") } else { - log.Error().Err(err).Msg("Failed to reload system.json") + log.Info().Msg("Runtime configuration reloaded") } } - // Could reload other configs here (alerts.json, webhooks.json, etc.) - - log.Info().Msg("Configuration reload complete") - case <-sigChan: log.Info().Msg("Shutting down server...") goto shutdown diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index f0c0e8b..12761ac 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -107,7 +107,8 @@ PROXY_AUTH_LOGOUT_URL=/logout # URL for SSO logout **Contents:** ```json { - "pbsPollingInterval": 60, // Seconds between PBS refreshes (PVE polling fixed at 10s) + "pvePollingInterval": 10, // Seconds between PVE refreshes (10s default, 10–3600 allowed) + "pbsPollingInterval": 60, // Seconds between PBS refreshes "pmgPollingInterval": 60, // Seconds between PMG refreshes (mail analytics and health) "connectionTimeout": 60, // Seconds before node connection timeout "autoUpdateEnabled": false, // Systemd timer toggle for automatic updates @@ -142,6 +143,7 @@ PROXY_AUTH_LOGOUT_URL=/logout # URL for SSO logout - Can be safely backed up without exposing secrets - Missing file results in defaults being used - Changes take effect immediately (no restart required) +- `pvePollingInterval` controls how often Pulse polls all Proxmox VE nodes. Configure it in **Settings → System → General → Monitoring cadence** (10–3600 seconds). - API tokens are no longer managed in system.json (moved to .env in v4.3.9+) - **Adaptive polling controls** (`adaptivePollingEnabled`, `adaptivePolling*Interval`) map directly to the Scheduler Health API and adjust queue/backoff behaviour in real time. - **Runtime logging controls** (`logLevel`, `logFormat`, `logFile`, `logMaxSize`, `logMaxAge`, `logCompress`) can be tuned from the UI or system.json; updates are applied immediately so you can raise verbosity, switch to structured JSON, or stream logs to disk without restarting Pulse. diff --git a/frontend-modern/src/components/Settings/Settings.tsx b/frontend-modern/src/components/Settings/Settings.tsx index c5960b6..7693117 100644 --- a/frontend-modern/src/components/Settings/Settings.tsx +++ b/frontend-modern/src/components/Settings/Settings.tsx @@ -406,6 +406,17 @@ const BACKUP_INTERVAL_OPTIONS = [ const BACKUP_INTERVAL_MAX_MINUTES = 7 * 24 * 60; // 7 days +const PVE_POLLING_MIN_SECONDS = 10; +const PVE_POLLING_MAX_SECONDS = 3600; +const PVE_POLLING_PRESETS = [ + { label: '10 seconds (default)', value: 10 }, + { label: '15 seconds', value: 15 }, + { label: '30 seconds', value: 30 }, + { label: '60 seconds', value: 60 }, + { label: '2 minutes', value: 120 }, + { label: '5 minutes', value: 300 }, +]; + // Node with UI-specific fields type NodeConfigWithStatus = NodeConfig & { hasPassword?: boolean; @@ -610,7 +621,11 @@ const Settings: Component = (props) => { const pmgNodes = createMemo(() => nodes().filter((n) => n.type === 'pmg')); // System settings - // PBS polling interval removed - fixed at 10 seconds + const [pvePollingInterval, setPVEPollingInterval] = createSignal(PVE_POLLING_MIN_SECONDS); + const [pvePollingSelection, setPVEPollingSelection] = createSignal( + PVE_POLLING_MIN_SECONDS, + ); + const [pvePollingCustomSeconds, setPVEPollingCustomSeconds] = createSignal(30); const [allowedOrigins, setAllowedOrigins] = createSignal('*'); const [discoveryEnabled, setDiscoveryEnabled] = createSignal(false); const [discoverySubnet, setDiscoverySubnet] = createSignal('auto'); @@ -627,6 +642,8 @@ const Settings: Component = (props) => { Boolean( envOverrides().temperatureMonitoringEnabled || envOverrides()['ENABLE_TEMPERATURE_MONITORING'], ); + const pvePollingEnvLocked = () => + Boolean(envOverrides().pvePollingInterval || envOverrides().PVE_POLLING_INTERVAL); let discoverySubnetInputRef: HTMLInputElement | undefined; const parseSubnetList = (value: string) => { @@ -1912,7 +1929,22 @@ const Settings: Component = (props) => { // Load system settings try { const systemSettings = await SettingsAPI.getSystemSettings(); - // PBS polling interval is now fixed at 10 seconds + const rawPVESecs = + typeof systemSettings.pvePollingInterval === 'number' + ? Math.round(systemSettings.pvePollingInterval) + : PVE_POLLING_MIN_SECONDS; + const clampedPVESecs = Math.min( + PVE_POLLING_MAX_SECONDS, + Math.max(PVE_POLLING_MIN_SECONDS, rawPVESecs), + ); + setPVEPollingInterval(clampedPVESecs); + const presetMatch = PVE_POLLING_PRESETS.find((opt) => opt.value === clampedPVESecs); + if (presetMatch) { + setPVEPollingSelection(presetMatch.value); + } else { + setPVEPollingSelection('custom'); + setPVEPollingCustomSeconds(clampedPVESecs); + } setAllowedOrigins(systemSettings.allowedOrigins || '*'); // Connection timeout is backend-only // Load discovery settings (default to false when unset) @@ -2027,7 +2059,7 @@ const Settings: Component = (props) => { ) { // Save system settings using typed API await SettingsAPI.updateSystemSettings({ - // PBS polling interval is now fixed at 10 seconds + pvePollingInterval: pvePollingInterval(), allowedOrigins: allowedOrigins(), // Connection timeout is backend-only // Discovery settings are saved immediately on toggle @@ -3523,6 +3555,133 @@ const Settings: Component = (props) => { + +
+
+
+ +
+ +
+
+
+
+

+ Shorter intervals provide near-real-time updates at the cost of higher API + and CPU usage on each node. Set a longer interval to reduce load on busy + clusters. +

+

+ Current cadence: {pvePollingInterval()} seconds ( + {pvePollingInterval() >= 60 + ? `${(pvePollingInterval() / 60).toFixed( + pvePollingInterval() % 60 === 0 ? 0 : 1, + )} minute${pvePollingInterval() / 60 === 1 ? '' : 's'}` + : 'under a minute'} + ). +

+
+
+
+ + {(option) => ( + + )} + + +
+ +
+ + { + if (pvePollingEnvLocked()) return; + const parsed = Math.floor(Number(e.currentTarget.value)); + if (Number.isNaN(parsed)) { + return; + } + const clamped = Math.min( + PVE_POLLING_MAX_SECONDS, + Math.max(PVE_POLLING_MIN_SECONDS, parsed), + ); + setPVEPollingCustomSeconds(clamped); + setPVEPollingInterval(clamped); + setHasUnsavedChanges(true); + }} + /> +

+ Applies to all PVE clusters and standalone nodes. +

+
+
+ +
+ + + + + + Managed via environment variable PVE_POLLING_INTERVAL. +
+
+
+
+
diff --git a/frontend-modern/src/types/config.ts b/frontend-modern/src/types/config.ts index 343c068..fa20d19 100644 --- a/frontend-modern/src/types/config.ts +++ b/frontend-modern/src/types/config.ts @@ -27,7 +27,7 @@ export interface AuthConfig { * These are application behavior settings */ export interface SystemConfig { - // Note: PVE polling is hardcoded to 10s (Proxmox cluster/resources updates every 10s) + pvePollingInterval?: number; // PVE polling interval in seconds pbsPollingInterval?: number; // PBS polling interval in seconds connectionTimeout?: number; // Seconds before timeout (default: 10) autoUpdateEnabled: boolean; // Enable auto-updates diff --git a/internal/api/config_handlers.go b/internal/api/config_handlers.go index 7464359..b4a0291 100644 --- a/internal/api/config_handlers.go +++ b/internal/api/config_handlers.go @@ -2876,6 +2876,7 @@ func (h *ConfigHandlers) HandleGetSystemSettings(w http.ResponseWriter, r *http. // Get current values from running config settings := *persistedSettings + settings.PVEPollingInterval = int(h.config.PVEPollingInterval.Seconds()) settings.PBSPollingInterval = int(h.config.PBSPollingInterval.Seconds()) settings.BackupPollingInterval = int(h.config.BackupPollingInterval.Seconds()) settings.BackendPort = h.config.BackendPort diff --git a/internal/api/router.go b/internal/api/router.go index 0df7b5c..1c86266 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -977,7 +977,7 @@ func (r *Router) setupRoutes() { })) // System settings and API token management - r.systemSettingsHandler = NewSystemSettingsHandler(r.config, r.persistence, r.wsHub, r.monitor, r.reloadSystemSettings) + r.systemSettingsHandler = NewSystemSettingsHandler(r.config, r.persistence, r.wsHub, r.monitor, r.reloadSystemSettings, r.reloadFunc) r.mux.HandleFunc("/api/system/settings", RequireAdmin(r.config, RequireScope(config.ScopeSettingsRead, r.systemSettingsHandler.HandleGetSystemSettings))) r.mux.HandleFunc("/api/system/settings/update", RequireAdmin(r.config, RequireScope(config.ScopeSettingsWrite, r.systemSettingsHandler.HandleUpdateSystemSettings))) r.mux.HandleFunc("/api/system/ssh-config", r.handleSSHConfig) diff --git a/internal/api/system_settings.go b/internal/api/system_settings.go index c87d827..219826b 100644 --- a/internal/api/system_settings.go +++ b/internal/api/system_settings.go @@ -28,6 +28,7 @@ type SystemSettingsHandler struct { persistence *config.ConfigPersistence wsHub *websocket.Hub reloadSystemSettingsFunc func() // Function to reload cached system settings + reloadMonitorFunc func() error monitor interface { GetDiscoveryService() *discovery.Service StartDiscoveryService(ctx context.Context, wsHub *websocket.Hub, subnet string) @@ -48,13 +49,14 @@ func NewSystemSettingsHandler(cfg *config.Config, persistence *config.ConfigPers DisableTemperatureMonitoring() GetNotificationManager() *notifications.NotificationManager HasSocketTemperatureProxy() bool -}, reloadSystemSettingsFunc func()) *SystemSettingsHandler { +}, reloadSystemSettingsFunc func(), reloadMonitorFunc func() error) *SystemSettingsHandler { return &SystemSettingsHandler{ config: cfg, persistence: persistence, wsHub: wsHub, monitor: monitor, reloadSystemSettingsFunc: reloadSystemSettingsFunc, + reloadMonitorFunc: reloadMonitorFunc, } } @@ -118,8 +120,21 @@ func discoveryConfigMap(raw map[string]interface{}) (map[string]interface{}, boo // validateSystemSettings validates settings before applying them func validateSystemSettings(settings *config.SystemSettings, rawRequest map[string]interface{}) error { - // Note: PVE polling is hardcoded to 10s since Proxmox cluster/resources endpoint only updates every 10s - // Legacy polling interval fields are ignored if provided + if val, ok := rawRequest["pvePollingInterval"]; ok { + if interval, ok := val.(float64); ok { + if interval <= 0 { + return fmt.Errorf("PVE polling interval must be positive (minimum 10 seconds)") + } + if interval < 10 { + return fmt.Errorf("PVE polling interval must be at least 10 seconds") + } + if interval > 3600 { + return fmt.Errorf("PVE polling interval cannot exceed 3600 seconds (1 hour)") + } + } else { + return fmt.Errorf("PVE polling interval must be a number") + } + } if val, ok := rawRequest["pbsPollingInterval"]; ok { if interval, ok := val.(float64); ok { @@ -382,6 +397,7 @@ func (h *SystemSettingsHandler) HandleGetSystemSettings(w http.ResponseWriter, r Msg("Loaded system settings for API response") // Always expose effective backup polling configuration + settings.PVEPollingInterval = int(h.config.PVEPollingInterval.Seconds()) settings.BackupPollingInterval = int(h.config.BackupPollingInterval.Seconds()) enabled := h.config.EnableBackupPolling settings.BackupPollingEnabled = &enabled @@ -485,9 +501,12 @@ func (h *SystemSettingsHandler) HandleUpdateSystemSettings(w http.ResponseWriter discoveryConfigUpdated := false prevTempEnabled := h.config.TemperatureMonitoringEnabled tempToggleRequested := false + pveIntervalChanged := false // Only update fields that were provided in the request - // Note: PVE polling is hardcoded to 10s, legacy polling fields are ignored + if _, ok := rawRequest["pvePollingInterval"]; ok { + settings.PVEPollingInterval = updates.PVEPollingInterval + } if _, ok := rawRequest["pbsPollingInterval"]; ok { settings.PBSPollingInterval = updates.PBSPollingInterval } @@ -608,7 +627,16 @@ func (h *SystemSettingsHandler) HandleUpdateSystemSettings(w http.ResponseWriter } // Update the config - // Note: PVE polling is hardcoded to 10s + if _, ok := rawRequest["pvePollingInterval"]; ok && settings.PVEPollingInterval > 0 { + newInterval := time.Duration(settings.PVEPollingInterval) * time.Second + if newInterval < 10*time.Second { + newInterval = 10 * time.Second + } + if h.config.PVEPollingInterval != newInterval { + pveIntervalChanged = true + } + h.config.PVEPollingInterval = newInterval + } if settings.AllowedOrigins != "" { h.config.AllowedOrigins = settings.AllowedOrigins } @@ -718,6 +746,14 @@ func (h *SystemSettingsHandler) HandleUpdateSystemSettings(w http.ResponseWriter h.reloadSystemSettingsFunc() } + if pveIntervalChanged && h.reloadMonitorFunc != nil { + if err := h.reloadMonitorFunc(); err != nil { + log.Error().Err(err).Msg("Failed to reload monitor after PVE polling interval change") + http.Error(w, "Configuration saved but failed to reload monitor", http.StatusInternalServerError) + return + } + } + log.Info().Msg("System settings updated") // Broadcast theme change to all connected clients if theme was updated diff --git a/internal/api/system_settings_temperature_test.go b/internal/api/system_settings_temperature_test.go index 50363d5..ca311fe 100644 --- a/internal/api/system_settings_temperature_test.go +++ b/internal/api/system_settings_temperature_test.go @@ -4,7 +4,9 @@ import ( "bytes" "net/http" "net/http/httptest" + "strings" "testing" + "time" "github.com/rcourtman/pulse-go-rewrite/internal/config" ) @@ -14,7 +16,7 @@ func TestHandleUpdateSystemSettingsRejectsTempsWithoutTransport(t *testing.T) { t.Setenv("PULSE_DOCKER", "true") cfg := &config.Config{DataPath: tempDir, ConfigPath: tempDir, PVEInstances: []config.PVEInstance{{Name: "pve-a"}}} persistence := config.NewConfigPersistence(tempDir) - handler := NewSystemSettingsHandler(cfg, persistence, nil, nil, nil) + handler := NewSystemSettingsHandler(cfg, persistence, nil, nil, nil, nil) req := httptest.NewRequest(http.MethodPost, "/api/system/settings/update", bytes.NewBufferString(`{"temperatureMonitoringEnabled":true}`)) rec := httptest.NewRecorder() @@ -25,3 +27,53 @@ func TestHandleUpdateSystemSettingsRejectsTempsWithoutTransport(t *testing.T) { t.Fatalf("expected status 400, got %d", rec.Code) } } + +func TestHandleUpdateSystemSettingsRejectsInvalidPVEPollingInterval(t *testing.T) { + tempDir := t.TempDir() + cfg := &config.Config{ + DataPath: tempDir, + ConfigPath: tempDir, + PVEPollingInterval: 10 * time.Second, + } + persistence := config.NewConfigPersistence(tempDir) + handler := NewSystemSettingsHandler(cfg, persistence, nil, nil, nil, nil) + + req := httptest.NewRequest(http.MethodPost, "/api/system/settings/update", strings.NewReader(`{"pvePollingInterval":5}`)) + rec := httptest.NewRecorder() + + handler.HandleUpdateSystemSettings(rec, req) + + if rec.Code != http.StatusBadRequest { + t.Fatalf("expected status 400, got %d", rec.Code) + } +} + +func TestHandleUpdateSystemSettingsUpdatesPVEPollingInterval(t *testing.T) { + tempDir := t.TempDir() + cfg := &config.Config{ + DataPath: tempDir, + ConfigPath: tempDir, + PVEPollingInterval: 10 * time.Second, + } + persistence := config.NewConfigPersistence(tempDir) + reloaded := false + handler := NewSystemSettingsHandler(cfg, persistence, nil, nil, nil, func() error { + reloaded = true + return nil + }) + + req := httptest.NewRequest(http.MethodPost, "/api/system/settings/update", strings.NewReader(`{"pvePollingInterval":30}`)) + rec := httptest.NewRecorder() + + handler.HandleUpdateSystemSettings(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("expected status 200, got %d", rec.Code) + } + if cfg.PVEPollingInterval != 30*time.Second { + t.Fatalf("expected interval to be 30s, got %v", cfg.PVEPollingInterval) + } + if !reloaded { + t.Fatal("expected reload to be triggered") + } +} diff --git a/internal/config/config.go b/internal/config/config.go index ff499c3..8cdbfd0 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -83,7 +83,7 @@ type Config struct { PMGInstances []PMGInstance // Monitoring settings - // Note: PVE polling is hardcoded to 10s since Proxmox cluster/resources endpoint only updates every 10s + PVEPollingInterval time.Duration `envconfig:"PVE_POLLING_INTERVAL"` // PVE polling interval (10s default) PBSPollingInterval time.Duration `envconfig:"PBS_POLLING_INTERVAL"` // PBS polling interval (60s default) PMGPollingInterval time.Duration `envconfig:"PMG_POLLING_INTERVAL"` // PMG polling interval (60s default) ConcurrentPolling bool `envconfig:"CONCURRENT_POLLING" default:"true"` @@ -529,6 +529,7 @@ func Load() (*Config, error) { BackupPollingCycles: 10, BackupPollingInterval: 0, EnableBackupPolling: true, + PVEPollingInterval: 10 * time.Second, WebhookBatchDelay: 10 * time.Second, AdaptivePollingEnabled: false, AdaptivePollingBaseInterval: 10 * time.Second, @@ -578,7 +579,10 @@ func Load() (*Config, error) { // Load system configuration if systemSettings, err := persistence.LoadSystemSettings(); err == nil && systemSettings != nil { - // Load PBS polling interval if configured + // Load polling intervals if configured + if systemSettings.PVEPollingInterval > 0 { + cfg.PVEPollingInterval = time.Duration(systemSettings.PVEPollingInterval) * time.Second + } if systemSettings.PBSPollingInterval > 0 { cfg.PBSPollingInterval = time.Duration(systemSettings.PBSPollingInterval) * time.Second } @@ -675,8 +679,13 @@ func Load() (*Config, error) { log.Warn().Err(err).Msg("Failed to load API tokens from persistence") } - // Ensure PBS polling interval has default if not set - // Note: PVE polling is hardcoded to 10s in monitor.go + // Ensure polling intervals have sane defaults if not set + if cfg.PVEPollingInterval <= 0 { + cfg.PVEPollingInterval = 10 * time.Second + } + if cfg.PVEPollingInterval > time.Hour { + cfg.PVEPollingInterval = time.Hour + } if cfg.PBSPollingInterval == 0 { cfg.PBSPollingInterval = 60 * time.Second } @@ -723,6 +732,28 @@ func Load() (*Config, error) { } } + if intervalStr := utils.GetenvTrim("PVE_POLLING_INTERVAL"); intervalStr != "" { + if dur, err := time.ParseDuration(intervalStr); err == nil { + if dur < 10*time.Second { + log.Warn().Dur("interval", dur).Msg("Ignoring PVE_POLLING_INTERVAL below 10s from environment") + } else { + cfg.PVEPollingInterval = dur + cfg.EnvOverrides["PVE_POLLING_INTERVAL"] = true + log.Info().Dur("interval", dur).Msg("Overriding PVE polling interval from environment") + } + } else if seconds, err := strconv.Atoi(intervalStr); err == nil { + if seconds < 10 { + log.Warn().Int("seconds", seconds).Msg("Ignoring PVE_POLLING_INTERVAL below 10s from environment") + } else { + cfg.PVEPollingInterval = time.Duration(seconds) * time.Second + cfg.EnvOverrides["PVE_POLLING_INTERVAL"] = true + log.Info().Int("seconds", seconds).Msg("Overriding PVE polling interval (seconds) from environment") + } + } else { + log.Warn().Str("value", intervalStr).Msg("Invalid PVE_POLLING_INTERVAL value, expected duration or seconds") + } + } + if enabledStr := utils.GetenvTrim("ENABLE_TEMPERATURE_MONITORING"); enabledStr != "" { if enabled, err := strconv.ParseBool(enabledStr); err == nil { cfg.TemperatureMonitoringEnabled = enabled @@ -1295,7 +1326,7 @@ func SaveConfig(cfg *Config) error { // Save system configuration adaptiveEnabled := cfg.AdaptivePollingEnabled systemSettings := SystemSettings{ - // Note: PVE polling is hardcoded to 10s + PVEPollingInterval: int(cfg.PVEPollingInterval / time.Second), UpdateChannel: cfg.UpdateChannel, AutoUpdateEnabled: cfg.AutoUpdateEnabled, AutoUpdateCheckInterval: int(cfg.AutoUpdateCheckInterval.Hours()), @@ -1349,7 +1380,12 @@ func (c *Config) Validate() error { } // Validate monitoring settings - // Note: PVE polling is hardcoded to 10s + if c.PVEPollingInterval < 10*time.Second { + return fmt.Errorf("PVE polling interval must be at least 10 seconds") + } + if c.PVEPollingInterval > time.Hour { + return fmt.Errorf("PVE polling interval cannot exceed 1 hour") + } if c.ConnectionTimeout < time.Second { return fmt.Errorf("connection timeout must be at least 1 second") } diff --git a/internal/config/persistence.go b/internal/config/persistence.go index 1080803..d120048 100644 --- a/internal/config/persistence.go +++ b/internal/config/persistence.go @@ -803,33 +803,33 @@ type NodesConfig struct { // SystemSettings represents system configuration settings type SystemSettings struct { - // Note: PVE polling is hardcoded to 10s since Proxmox cluster/resources endpoint only updates every 10s - PBSPollingInterval int `json:"pbsPollingInterval"` // PBS polling interval in seconds - PMGPollingInterval int `json:"pmgPollingInterval"` // PMG polling interval in seconds - BackupPollingInterval int `json:"backupPollingInterval,omitempty"` - BackupPollingEnabled *bool `json:"backupPollingEnabled,omitempty"` - AdaptivePollingEnabled *bool `json:"adaptivePollingEnabled,omitempty"` - AdaptivePollingBaseInterval int `json:"adaptivePollingBaseInterval,omitempty"` - AdaptivePollingMinInterval int `json:"adaptivePollingMinInterval,omitempty"` - AdaptivePollingMaxInterval int `json:"adaptivePollingMaxInterval,omitempty"` - BackendPort int `json:"backendPort,omitempty"` - FrontendPort int `json:"frontendPort,omitempty"` - AllowedOrigins string `json:"allowedOrigins,omitempty"` - ConnectionTimeout int `json:"connectionTimeout,omitempty"` - UpdateChannel string `json:"updateChannel,omitempty"` - AutoUpdateEnabled bool `json:"autoUpdateEnabled"` // Removed omitempty so false is saved - AutoUpdateCheckInterval int `json:"autoUpdateCheckInterval,omitempty"` - AutoUpdateTime string `json:"autoUpdateTime,omitempty"` - LogLevel string `json:"logLevel,omitempty"` - DiscoveryEnabled bool `json:"discoveryEnabled"` - DiscoverySubnet string `json:"discoverySubnet,omitempty"` - DiscoveryConfig DiscoveryConfig `json:"discoveryConfig"` - Theme string `json:"theme,omitempty"` // User theme preference: "light", "dark", or empty for system default + PVEPollingInterval int `json:"pvePollingInterval"` // PVE polling interval in seconds + PBSPollingInterval int `json:"pbsPollingInterval"` // PBS polling interval in seconds + PMGPollingInterval int `json:"pmgPollingInterval"` // PMG polling interval in seconds + BackupPollingInterval int `json:"backupPollingInterval,omitempty"` + BackupPollingEnabled *bool `json:"backupPollingEnabled,omitempty"` + AdaptivePollingEnabled *bool `json:"adaptivePollingEnabled,omitempty"` + AdaptivePollingBaseInterval int `json:"adaptivePollingBaseInterval,omitempty"` + AdaptivePollingMinInterval int `json:"adaptivePollingMinInterval,omitempty"` + AdaptivePollingMaxInterval int `json:"adaptivePollingMaxInterval,omitempty"` + BackendPort int `json:"backendPort,omitempty"` + FrontendPort int `json:"frontendPort,omitempty"` + AllowedOrigins string `json:"allowedOrigins,omitempty"` + ConnectionTimeout int `json:"connectionTimeout,omitempty"` + UpdateChannel string `json:"updateChannel,omitempty"` + AutoUpdateEnabled bool `json:"autoUpdateEnabled"` // Removed omitempty so false is saved + AutoUpdateCheckInterval int `json:"autoUpdateCheckInterval,omitempty"` + AutoUpdateTime string `json:"autoUpdateTime,omitempty"` + LogLevel string `json:"logLevel,omitempty"` + DiscoveryEnabled bool `json:"discoveryEnabled"` + DiscoverySubnet string `json:"discoverySubnet,omitempty"` + DiscoveryConfig DiscoveryConfig `json:"discoveryConfig"` + Theme string `json:"theme,omitempty"` // User theme preference: "light", "dark", or empty for system default AllowEmbedding bool `json:"allowEmbedding"` // Allow iframe embedding AllowedEmbedOrigins string `json:"allowedEmbedOrigins,omitempty"` // Comma-separated list of allowed origins for embedding TemperatureMonitoringEnabled bool `json:"temperatureMonitoringEnabled"` - DNSCacheTimeout int `json:"dnsCacheTimeout,omitempty"` // DNS cache timeout in seconds (0 = default 5 minutes) - SSHPort int `json:"sshPort,omitempty"` // Default SSH port for temperature monitoring (0 = use 22) + DNSCacheTimeout int `json:"dnsCacheTimeout,omitempty"` // DNS cache timeout in seconds (0 = default 5 minutes) + SSHPort int `json:"sshPort,omitempty"` // Default SSH port for temperature monitoring (0 = use 22) WebhookAllowedPrivateCIDRs string `json:"webhookAllowedPrivateCIDRs,omitempty"` // Comma-separated list of private CIDR ranges allowed for webhooks (e.g., "192.168.1.0/24,10.0.0.0/8") // APIToken removed - now handled via .env file only } @@ -838,6 +838,7 @@ type SystemSettings struct { func DefaultSystemSettings() *SystemSettings { defaultDiscovery := DefaultDiscoveryConfig() return &SystemSettings{ + PVEPollingInterval: 10, PBSPollingInterval: 60, PMGPollingInterval: 60, AutoUpdateEnabled: false, @@ -1361,9 +1362,8 @@ func (c *ConfigPersistence) updateEnvFile(envFile string, settings SystemSetting for scanner.Scan() { line := scanner.Text() - // Skip POLLING_INTERVAL lines - deprecated + // Skip legacy POLLING_INTERVAL lines - replaced by system.json PVE_POLLING_INTERVAL if strings.HasPrefix(line, "POLLING_INTERVAL=") { - // Skip this line, polling interval is now hardcoded continue } else if strings.HasPrefix(line, "UPDATE_CHANNEL=") && settings.UpdateChannel != "" { lines = append(lines, fmt.Sprintf("UPDATE_CHANNEL=%s", settings.UpdateChannel)) @@ -1381,7 +1381,7 @@ func (c *ConfigPersistence) updateEnvFile(envFile string, settings SystemSetting return err } - // Note: POLLING_INTERVAL is deprecated and no longer written + // Note: legacy POLLING_INTERVAL is deprecated and no longer written // Build the new content content := strings.Join(lines, "\n") diff --git a/internal/monitoring/monitor.go b/internal/monitoring/monitor.go index a1525ce..fef26ed 100644 --- a/internal/monitoring/monitor.go +++ b/internal/monitoring/monitor.go @@ -4019,10 +4019,28 @@ func (m *Monitor) getExecutor() PollExecutor { return exec } +func (m *Monitor) effectivePVEPollingInterval() time.Duration { + const minInterval = 10 * time.Second + const maxInterval = time.Hour + + interval := minInterval + if m != nil && m.config != nil && m.config.PVEPollingInterval > 0 { + interval = m.config.PVEPollingInterval + } + if interval < minInterval { + interval = minInterval + } + if interval > maxInterval { + interval = maxInterval + } + return interval +} + // Start begins the monitoring loop func (m *Monitor) Start(ctx context.Context, wsHub *websocket.Hub) { + pollingInterval := m.effectivePVEPollingInterval() log.Info(). - Dur("pollingInterval", 10*time.Second). + Dur("pollingInterval", pollingInterval). Msg("Starting monitoring loop") m.mu.Lock() @@ -4113,9 +4131,7 @@ func (m *Monitor) Start(ctx context.Context, wsHub *websocket.Hub) { wsHub.BroadcastAlert(alert) }) - // Create separate tickers for polling and broadcasting - // Hardcoded to 10 seconds since Proxmox updates cluster/resources every 10 seconds - const pollingInterval = 10 * time.Second + // Create separate tickers for polling and broadcasting using the configured cadence workerCount := len(m.pveClients) + len(m.pbsClients) + len(m.pmgClients) m.startTaskWorkers(ctx, workerCount) diff --git a/internal/monitoring/reload.go b/internal/monitoring/reload.go index 097db3f..59815c4 100644 --- a/internal/monitoring/reload.go +++ b/internal/monitoring/reload.go @@ -90,9 +90,7 @@ func (rm *ReloadableMonitor) doReload() error { return err } - // Note: Polling interval is now hardcoded to 10s, no special handling needed - - // For other changes, do a full reload + // Polling interval changes and other settings require a full reload log.Info().Msg("Performing full monitor reload") // Cancel current monitor @@ -123,8 +121,6 @@ func (rm *ReloadableMonitor) doReload() error { return nil } -// Note: Polling interval change detection removed - now hardcoded to 10s - // GetMonitor returns the current monitor instance func (rm *ReloadableMonitor) GetMonitor() *Monitor { rm.mu.RLock() @@ -160,5 +156,3 @@ func (rm *ReloadableMonitor) Stop() { rm.monitor.Stop() } } - -// Note: UpdatePollingInterval removed - polling interval is now hardcoded to 10s