Pulse/internal/api/settings.go
Pulse Monitor 8e0aa39643 Fix alert system: clearing and frontend reactivity
- Fixed alert clearing logic to work even when alerts are acknowledged
- Added immediate WebSocket state broadcast after alert resolution
- Fixed frontend activeAlerts store updates to maintain SolidJS reactivity
- Added logging for alert resolution events

The alert system now properly:
- Creates alerts when thresholds are exceeded
- Clears alerts automatically when values drop below clear threshold
- Updates frontend in real-time without requiring page refresh
2025-07-29 14:53:41 +00:00

169 lines
No EOL
4.5 KiB
Go

package api
import (
"encoding/json"
"net/http"
"os"
"path/filepath"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v3"
)
// SettingsResponse represents the current settings and capabilities
type SettingsResponse struct {
Current *config.Settings `json:"current"`
Defaults *config.Settings `json:"defaults"`
Capabilities Capabilities `json:"capabilities"`
}
// Capabilities represents what can be configured
type Capabilities struct {
CanRestart bool `json:"canRestart"`
CanValidatePorts bool `json:"canValidatePorts"`
RequiresRestart bool `json:"requiresRestart"`
}
// SettingsUpdate represents a settings update request
type SettingsUpdate struct {
Settings *config.Settings `json:"settings"`
RestartNow bool `json:"restartNow"`
ValidateOnly bool `json:"validateOnly"`
}
// getSettings returns current settings and configuration capabilities
func getSettings(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Load current settings
loader := config.NewConfigLoader()
current, err := loader.LoadConfig()
if err != nil {
log.Error().Err(err).Msg("Failed to load current settings")
http.Error(w, "Failed to load settings", http.StatusInternalServerError)
return
}
response := SettingsResponse{
Current: current,
Defaults: config.DefaultSettings(),
Capabilities: Capabilities{
CanRestart: true,
CanValidatePorts: true,
RequiresRestart: true,
},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// updateSettings updates the configuration
func updateSettings(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var update SettingsUpdate
if err := json.NewDecoder(r.Body).Decode(&update); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
// Validate settings
if err := update.Settings.Validate(); err != nil {
log.Error().Err(err).Msg("Settings validation failed")
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Check port availability
if !update.ValidateOnly {
if !config.IsPortAvailable(update.Settings.Server.Backend.Host, update.Settings.Server.Backend.Port) {
http.Error(w, "Backend port is not available", http.StatusConflict)
return
}
if !config.IsPortAvailable(update.Settings.Server.Frontend.Host, update.Settings.Server.Frontend.Port) {
http.Error(w, "Frontend port is not available", http.StatusConflict)
return
}
}
// If validate only, return success
if update.ValidateOnly {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"valid": true,
"message": "Configuration is valid",
})
return
}
// Save configuration to file
if err := saveSettings(update.Settings); err != nil {
log.Error().Err(err).Msg("Failed to save settings")
http.Error(w, "Failed to save settings", http.StatusInternalServerError)
return
}
// Prepare response
response := map[string]interface{}{
"success": true,
"message": "Settings saved successfully",
"requiresRestart": true,
}
// Handle restart if requested
if update.RestartNow {
// Schedule a restart after response is sent
go func() {
log.Info().Msg("Restarting Pulse due to configuration change")
// The systemd service will automatically restart us
os.Exit(0)
}()
response["restarting"] = true
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// saveSettings persists settings to the configuration file
func saveSettings(settings *config.Settings) error {
// Determine config path - prefer /etc/pulse if writable
configPath := "./pulse.yml"
if _, err := os.Stat("/etc/pulse"); err == nil {
// Check if we can write to /etc/pulse
testFile := "/etc/pulse/.write-test"
if f, err := os.Create(testFile); err == nil {
f.Close()
os.Remove(testFile)
configPath = "/etc/pulse/pulse.yml"
}
}
// Marshal to YAML
data, err := yaml.Marshal(settings)
if err != nil {
return err
}
// Create directory if needed
dir := filepath.Dir(configPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
// Write file
if err := os.WriteFile(configPath, data, 0644); err != nil {
return err
}
log.Info().Str("path", configPath).Msg("Configuration saved")
return nil
}