From 3af29f4b098cf57fd81bbbe2156a2b2148347a3a Mon Sep 17 00:00:00 2001 From: Pulse Monitor Date: Tue, 19 Aug 2025 09:04:54 +0000 Subject: [PATCH] feat: add UI warnings for environment variable overrides - Track which settings are overridden by env vars in backend - Expose env override information in system settings API - Show clear warnings in UI when settings are controlled by env vars - Disable input fields when overridden by environment variables - Add helpful instructions for users to remove env vars if needed This improves UX by making it clear why UI changes don't take effect when environment variables are set. Follows container best practices where env vars have highest precedence, while clearly communicating this behavior to users. Addresses user confusion when UI settings don't work due to env var overrides. --- .../src/components/Settings/Settings.tsx | 47 +++++++++++++++---- internal/api/system_settings.go | 11 ++++- internal/config/config.go | 9 ++++ 3 files changed, 56 insertions(+), 11 deletions(-) diff --git a/frontend-modern/src/components/Settings/Settings.tsx b/frontend-modern/src/components/Settings/Settings.tsx index 770484851..3051a4788 100644 --- a/frontend-modern/src/components/Settings/Settings.tsx +++ b/frontend-modern/src/components/Settings/Settings.tsx @@ -97,6 +97,7 @@ const Settings: Component = () => { // System settings // PBS polling interval removed - fixed at 10 seconds const [allowedOrigins, setAllowedOrigins] = createSignal('*'); + const [envOverrides, setEnvOverrides] = createSignal>({}); // Connection timeout removed - backend-only setting // Update settings @@ -350,6 +351,10 @@ const Settings: Component = () => { if (systemSettings.updateChannel) { setUpdateChannel(systemSettings.updateChannel as 'stable' | 'rc'); } + // Track environment variable overrides + if (systemSettings.envOverrides) { + setEnvOverrides(systemSettings.envOverrides); + } } else { // Fallback to old endpoint await SettingsAPI.getSettings(); @@ -1153,16 +1158,38 @@ const Settings: Component = () => {

For reverse proxy setups (* = allow all, empty = same-origin only)

- { - setAllowedOrigins(e.currentTarget.value); - setHasUnsavedChanges(true); - }} - placeholder="* or https://example.com" - class="w-full px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800" - /> +
+ { + if (!envOverrides().allowedOrigins) { + setAllowedOrigins(e.currentTarget.value); + setHasUnsavedChanges(true); + } + }} + disabled={envOverrides().allowedOrigins} + placeholder="* or https://example.com" + class={`w-full px-3 py-1.5 text-sm border rounded-lg ${ + envOverrides().allowedOrigins + ? 'border-amber-300 dark:border-amber-600 bg-amber-50 dark:bg-amber-900/20 cursor-not-allowed opacity-75' + : 'border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800' + }`} + /> + {envOverrides().allowedOrigins && ( +
+
+ + + + Overridden by ALLOWED_ORIGINS environment variable +
+
+ Remove the env var and restart to enable UI configuration +
+
+ )} +
diff --git a/internal/api/system_settings.go b/internal/api/system_settings.go index 013271b46..9419b7512 100644 --- a/internal/api/system_settings.go +++ b/internal/api/system_settings.go @@ -38,8 +38,17 @@ func (h *SystemSettingsHandler) HandleGetSystemSettings(w http.ResponseWriter, r } } + // Include env override information + response := struct { + *config.SystemSettings + EnvOverrides map[string]bool `json:"envOverrides,omitempty"` + }{ + SystemSettings: settings, + EnvOverrides: h.config.EnvOverrides, + } + w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(settings) + json.NewEncoder(w).Encode(response) } // HandleUpdateSystemSettings updates the system settings diff --git a/internal/config/config.go b/internal/config/config.go index e14d65e58..3627437b7 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -107,6 +107,9 @@ type Config struct { // Deprecated - for backward compatibility Port int `envconfig:"PORT"` // Maps to BackendPort Debug bool `envconfig:"DEBUG" default:"false"` + + // Track which settings are overridden by environment variables + EnvOverrides map[string]bool `json:"-"` } // PVEInstance represents a Proxmox VE connection @@ -207,6 +210,7 @@ func Load() (*Config, error) { PVEPollingInterval: 10 * time.Second, // Deprecated - not used PBSPollingInterval: 60 * time.Second, // Default PBS polling (slower) DiscoverySubnet: "auto", + EnvOverrides: make(map[string]bool), } // Initialize persistence @@ -389,23 +393,28 @@ func Load() (*Config, error) { // NOTE: Environment variables always take precedence over UI/system.json settings if discoverySubnet := os.Getenv("DISCOVERY_SUBNET"); discoverySubnet != "" { cfg.DiscoverySubnet = discoverySubnet + cfg.EnvOverrides["discoverySubnet"] = true log.Info().Str("subnet", discoverySubnet).Msg("Discovery subnet overridden by DISCOVERY_SUBNET env var") } if logLevel := os.Getenv("LOG_LEVEL"); logLevel != "" { cfg.LogLevel = logLevel + cfg.EnvOverrides["logLevel"] = true log.Info().Str("level", logLevel).Msg("Log level overridden by LOG_LEVEL env var") } if connectionTimeout := os.Getenv("CONNECTION_TIMEOUT"); connectionTimeout != "" { if d, err := time.ParseDuration(connectionTimeout + "s"); err == nil { cfg.ConnectionTimeout = d + cfg.EnvOverrides["connectionTimeout"] = true log.Info().Dur("timeout", d).Msg("Connection timeout overridden by CONNECTION_TIMEOUT env var") } else if d, err := time.ParseDuration(connectionTimeout); err == nil { cfg.ConnectionTimeout = d + cfg.EnvOverrides["connectionTimeout"] = true log.Info().Dur("timeout", d).Msg("Connection timeout overridden by CONNECTION_TIMEOUT env var") } } if allowedOrigins := os.Getenv("ALLOWED_ORIGINS"); allowedOrigins != "" { cfg.AllowedOrigins = allowedOrigins + cfg.EnvOverrides["allowedOrigins"] = true log.Info().Str("origins", allowedOrigins).Msg("Allowed origins overridden by ALLOWED_ORIGINS env var") }