package setup import ( "encoding/json" "fmt" "log" "os" "path/filepath" "sync" "gitea.dooplex.hu/admin/felhom-controller/internal/config" ) // NeedsSetup checks whether the controller should enter setup mode. // Setup is needed when no customer ID has been configured (empty string) // or when a debug-triggered setup marker file exists. func NeedsSetup(cfg *config.Config) bool { if cfg.Customer.ID == "" { return true } if _, err := os.Stat(filepath.Join(cfg.Paths.DataDir, ".needs-setup")); err == nil { return true } return false } // ClearSetupMarker removes the debug-triggered setup marker file. func ClearSetupMarker(dataDir string) { os.Remove(filepath.Join(dataDir, ".needs-setup")) } // SetupState persists wizard progress to survive browser crashes. type SetupState struct { mu sync.Mutex `json:"-"` path string `json:"-"` Step string `json:"step"` // "welcome", "scan", "hub-restore", "restore-exec", "fresh-hub", "fresh-manual", "done" Mode string `json:"mode"` // "restore" or "fresh" FormData map[string]string `json:"form_data"` // partially filled form fields SelectedBackup *SelectedBackup `json:"selected_backup,omitempty"` } // SelectedBackup tracks which backup the user chose. type SelectedBackup struct { Source string `json:"source"` // "local" or "hub" DrivePath string `json:"drive_path"` // for local CustomerID string `json:"customer_id"` Timestamp string `json:"timestamp"` } // LoadState loads or creates setup state from the data directory. func LoadState(dataDir string) *SetupState { path := filepath.Join(dataDir, "setup-state.json") s := &SetupState{path: path, Step: "welcome"} data, err := os.ReadFile(path) if err != nil { return s // fresh state } if err := json.Unmarshal(data, s); err != nil { return &SetupState{path: path, Step: "welcome"} } s.path = path return s } // Save persists the setup state atomically. func (s *SetupState) Save() error { s.mu.Lock() defer s.mu.Unlock() if s.FormData == nil { s.FormData = make(map[string]string) } data, err := json.MarshalIndent(s, "", " ") if err != nil { return fmt.Errorf("marshaling setup state: %w", err) } if err := os.MkdirAll(filepath.Dir(s.path), 0755); err != nil { return err } tmp := s.path + ".tmp" if err := os.WriteFile(tmp, data, 0600); err != nil { os.Remove(tmp) return err } if err := os.Rename(tmp, s.path); err != nil { os.Remove(tmp) return err } return nil } // SetStep updates the current step and saves. func (s *SetupState) SetStep(step string) { s.mu.Lock() s.Step = step s.mu.Unlock() if err := s.Save(); err != nil { log.Printf("[WARN] Failed to save setup step %q: %v", step, err) } } // SetFormField saves a form field for state persistence. func (s *SetupState) SetFormField(key, value string) { s.mu.Lock() if s.FormData == nil { s.FormData = make(map[string]string) } s.FormData[key] = value s.mu.Unlock() } // GetFormField retrieves a saved form field. func (s *SetupState) GetFormField(key string) string { s.mu.Lock() defer s.mu.Unlock() if s.FormData == nil { return "" } return s.FormData[key] } // Remove deletes the setup state file. func (s *SetupState) Remove() { os.Remove(s.path) } // DefaultHubURL is the default Hub URL. const DefaultHubURL = "https://hub.felhom.eu" // LogSetupMode logs the setup mode startup message. func LogSetupMode(domain string, ips []string, setupListen string, logger *log.Logger) { logger.Printf("[INFO] Controller in setup mode — waiting for configuration via web UI") if domain != "" { logger.Printf("[INFO] Setup wizard available at: https://felhom.%s", domain) } for _, ip := range ips { logger.Printf("[INFO] Setup wizard available at: http://%s%s", ip, setupListen) } }