feat: sync UI settings with .env file

- UI settings now automatically update .env file when changed
- .env file and system.json stay in sync
- Settings hierarchy preserved: env vars > .env > UI
- .env file is auto-generated/updated when UI saves settings
- Improved user experience - changes in UI reflect in .env and vice versa
This commit is contained in:
Pulse Monitor
2025-08-09 06:56:37 +00:00
parent d29c4da7e9
commit af84e27e80
2 changed files with 124 additions and 1 deletions
@@ -922,6 +922,21 @@ const Settings: Component = () => {
<div>
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4">System Configuration</h3>
{/* Environment Variable Info */}
<div class="mb-4 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-700 rounded-lg">
<div class="flex items-start gap-2">
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div class="text-sm text-blue-800 dark:text-blue-200">
<p class="font-medium mb-1">Configuration Sync:</p>
<p> Changes made here update both the database and .env file</p>
<p> If .env file exists at /etc/pulse/.env, it stays in sync with UI</p>
<p> System environment variables override both (if set)</p>
</div>
</div>
</div>
<div class="space-y-4">
{/* Performance Settings */}
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4">
+109 -1
View File
@@ -2,8 +2,10 @@ package config
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
@@ -376,11 +378,12 @@ func (c *ConfigPersistence) LoadNodesConfig() (*NodesConfig, error) {
return &config, nil
}
// SaveSystemSettings saves system settings to file
// SaveSystemSettings saves system settings to both system.json and .env file
func (c *ConfigPersistence) SaveSystemSettings(settings SystemSettings) error {
c.mu.Lock()
defer c.mu.Unlock()
// Save to system.json for backwards compatibility
data, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return err
@@ -394,6 +397,13 @@ func (c *ConfigPersistence) SaveSystemSettings(settings SystemSettings) error {
return err
}
// Also update .env file if it exists
envFile := filepath.Join(c.configDir, ".env")
if err := c.updateEnvFile(envFile, settings); err != nil {
log.Warn().Err(err).Msg("Failed to update .env file")
// Don't fail the operation if .env update fails
}
log.Info().Str("file", c.systemFile).Msg("System settings saved")
return nil
}
@@ -423,6 +433,104 @@ func (c *ConfigPersistence) LoadSystemSettings() (*SystemSettings, error) {
return &settings, nil
}
// updateEnvFile updates or creates .env file with system settings
func (c *ConfigPersistence) updateEnvFile(envFile string, settings SystemSettings) error {
// Read existing .env file if it exists
envVars := make(map[string]string)
if data, err := os.ReadFile(envFile); err == nil {
lines := strings.Split(string(data), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) == 2 {
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
envVars[key] = value
}
}
}
// Update with new values from settings
if settings.PollingInterval > 0 {
envVars["POLLING_INTERVAL"] = strconv.Itoa(settings.PollingInterval)
}
if settings.ConnectionTimeout > 0 {
envVars["CONNECTION_TIMEOUT"] = strconv.Itoa(settings.ConnectionTimeout)
}
if settings.UpdateChannel != "" {
envVars["UPDATE_CHANNEL"] = settings.UpdateChannel
}
envVars["AUTO_UPDATE_ENABLED"] = strconv.FormatBool(settings.AutoUpdateEnabled)
if settings.AutoUpdateCheckInterval > 0 {
envVars["AUTO_UPDATE_CHECK_INTERVAL"] = strconv.Itoa(settings.AutoUpdateCheckInterval)
}
if settings.AutoUpdateTime != "" {
envVars["AUTO_UPDATE_TIME"] = settings.AutoUpdateTime
}
if settings.AllowedOrigins != "" {
envVars["ALLOWED_ORIGINS"] = settings.AllowedOrigins
}
// Build the new .env content
var content strings.Builder
content.WriteString("# Pulse Configuration File - Automatically updated by UI\n")
content.WriteString("# Changes made in the UI will be reflected here\n\n")
// Write server settings
if port, ok := envVars["FRONTEND_PORT"]; ok {
content.WriteString(fmt.Sprintf("FRONTEND_PORT=%s\n", port))
}
if port, ok := envVars["BACKEND_PORT"]; ok {
content.WriteString(fmt.Sprintf("BACKEND_PORT=%s\n", port))
}
content.WriteString("\n")
// Write monitoring settings
content.WriteString("# Monitoring\n")
if interval, ok := envVars["POLLING_INTERVAL"]; ok {
content.WriteString(fmt.Sprintf("POLLING_INTERVAL=%s\n", interval))
}
if timeout, ok := envVars["CONNECTION_TIMEOUT"]; ok {
content.WriteString(fmt.Sprintf("CONNECTION_TIMEOUT=%s\n", timeout))
}
content.WriteString("\n")
// Write update settings
content.WriteString("# Updates\n")
if channel, ok := envVars["UPDATE_CHANNEL"]; ok {
content.WriteString(fmt.Sprintf("UPDATE_CHANNEL=%s\n", channel))
}
if enabled, ok := envVars["AUTO_UPDATE_ENABLED"]; ok {
content.WriteString(fmt.Sprintf("AUTO_UPDATE_ENABLED=%s\n", enabled))
}
if interval, ok := envVars["AUTO_UPDATE_CHECK_INTERVAL"]; ok {
content.WriteString(fmt.Sprintf("AUTO_UPDATE_CHECK_INTERVAL=%s\n", interval))
}
if time, ok := envVars["AUTO_UPDATE_TIME"]; ok {
content.WriteString(fmt.Sprintf("AUTO_UPDATE_TIME=%s\n", time))
}
content.WriteString("\n")
// Write CORS settings
if origins, ok := envVars["ALLOWED_ORIGINS"]; ok && origins != "" {
content.WriteString("# CORS\n")
content.WriteString(fmt.Sprintf("ALLOWED_ORIGINS=%s\n", origins))
content.WriteString("\n")
}
// Write logging settings if present
if level, ok := envVars["LOG_LEVEL"]; ok {
content.WriteString("# Logging\n")
content.WriteString(fmt.Sprintf("LOG_LEVEL=%s\n", level))
}
// Write the file
return os.WriteFile(envFile, []byte(content.String()), 0644)
}
// Helper function
func ptrFloat64(v float64) *float64 {
return &v