package config import ( "encoding/json" "os" "sync" ) // Store persists web-UI editable runtime state (sources added/edited) to a // JSON file so edits survive restarts. Base YAML sources are merged underneath. type Store struct { mu sync.Mutex path string data RuntimeConfig } func NewStore(path string) *Store { return &Store{path: path} } // Load reads the runtime file (missing file = empty state). func (s *Store) Load() error { s.mu.Lock() defer s.mu.Unlock() data, err := os.ReadFile(s.path) if err != nil { if os.IsNotExist(err) { s.data = RuntimeConfig{} return nil } return err } return json.Unmarshal(data, &s.data) } // List returns the runtime sources (those edited via web UI). func (s *Store) List() []Source { s.mu.Lock() defer s.mu.Unlock() out := make([]Source, len(s.data.Sources)) copy(out, s.data.Sources) return out } // Upsert adds or replaces a runtime source and persists. func (s *Store) Upsert(src Source) error { s.mu.Lock() defer s.mu.Unlock() for i := range s.data.Sources { if s.data.Sources[i].Name == src.Name { s.data.Sources[i] = src return s.persistLocked() } } s.data.Sources = append(s.data.Sources, src) return s.persistLocked() } // Remove deletes a runtime source and persists. func (s *Store) Remove(name string) (bool, error) { s.mu.Lock() defer s.mu.Unlock() kept := s.data.Sources[:0] removed := false for _, src := range s.data.Sources { if src.Name == name { removed = true continue } kept = append(kept, src) } if !removed { return false, nil } s.data.Sources = kept return true, s.persistLocked() } func (s *Store) persistLocked() error { b, err := json.MarshalIndent(s.data, "", " ") if err != nil { return err } return os.WriteFile(s.path, b, 0644) }