package config import ( "encoding/json" "os" "path/filepath" "strings" ) type Config struct { Listen string `json:"listen"` DiscoveryPort int `json:"discovery_port"` DiscoveryTimeoutMs int `json:"discovery_timeout_ms"` OfflineAfterMs int `json:"offline_after_ms"` AgentToken string `json:"agent_token"` Concurrency int `json:"concurrency"` DataDir string `json:"data_dir,omitempty"` DBPath string `json:"db_path,omitempty"` LogDir string `json:"log_dir,omitempty"` MediaRepoPath string `json:"media_repo_path,omitempty"` // explicit import-only source; not used for runtime rendering AlarmRetentionDays int `json:"alarm_retention_days"` // auto-delete alarms older than N days, 0=keep forever DeviceAliases map[string]string `json:"device_aliases,omitempty"` path string } func LoadConfig(path string) (*Config, error) { file, err := os.Open(path) if err != nil { return nil, err } defer file.Close() var cfg Config decoder := json.NewDecoder(file) if err := decoder.Decode(&cfg); err != nil { return nil, err } cfg.path = path return &cfg, nil } func (c *Config) Save() error { if c == nil || c.path == "" { return nil } body, err := json.MarshalIndent(c, "", " ") if err != nil { return err } return os.WriteFile(c.path, append(body, '\n'), 0o644) } func (c *Config) DataDirOrDefault() string { if c != nil && strings.TrimSpace(c.DataDir) != "" { return filepath.Clean(strings.TrimSpace(c.DataDir)) } return "data" } func (c *Config) DBPathOrDefault() string { if c != nil && strings.TrimSpace(c.DBPath) != "" { return filepath.Clean(strings.TrimSpace(c.DBPath)) } return filepath.Join(c.DataDirOrDefault(), "app.db") } func (c *Config) LogDirOrDefault() string { if c != nil && strings.TrimSpace(c.LogDir) != "" { return filepath.Clean(strings.TrimSpace(c.LogDir)) } return filepath.Join(c.DataDirOrDefault(), "logs") }