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.
This commit is contained in:
Pulse Monitor 2025-08-19 09:04:54 +00:00
parent 362ace960d
commit 3af29f4b09
3 changed files with 56 additions and 11 deletions

View file

@ -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<Record<string, boolean>>({});
// 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 = () => {
<div>
<label class="text-sm font-medium text-gray-900 dark:text-gray-100">CORS Allowed Origins</label>
<p class="text-xs text-gray-600 dark:text-gray-400 mb-2">For reverse proxy setups (* = allow all, empty = same-origin only)</p>
<input
type="text"
value={allowedOrigins()}
onChange={(e) => {
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"
/>
<div class="relative">
<input
type="text"
value={allowedOrigins()}
onChange={(e) => {
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 && (
<div class="mt-2 p-2 bg-amber-100 dark:bg-amber-900/30 border border-amber-300 dark:border-amber-700 rounded text-xs text-amber-800 dark:text-amber-200">
<div class="flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<span>Overridden by ALLOWED_ORIGINS environment variable</span>
</div>
<div class="mt-1 text-amber-700 dark:text-amber-300">
Remove the env var and restart to enable UI configuration
</div>
</div>
)}
</div>
</div>
<div class="mt-3 p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg">

View file

@ -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

View file

@ -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")
}