package config import ( "fmt" "os" "strings" "time" ) // Config is the runtime configuration, populated from the environment. // Everything here is deployment shape; the things that get tuned mid-challenge // (start date, targets) live in the data file instead, so changing a target // does not mean a redeploy. type Config struct { Addr string BaseURL string // public origin, e.g. https://fit.scottyah.com DataPath string // JSON file on the mounted volume TZ *time.Location } // Load reads configuration from the environment, applying defaults. It returns // an error only for settings that have no safe default. func Load() (*Config, error) { c := &Config{ Addr: env("ADDR", ":8080"), BaseURL: strings.TrimRight(env("BASE_URL", "http://localhost:8080"), "/"), DataPath: env("DATA_PATH", "data/fit.json"), } // The whole app is date arithmetic, so a wrong timezone silently logs // entries against the wrong day. Fail loudly rather than fall back to UTC. tzName := env("TZ_NAME", "America/Los_Angeles") loc, err := time.LoadLocation(tzName) if err != nil { return nil, fmt.Errorf("TZ_NAME %q: %w", tzName, err) } c.TZ = loc return c, nil } func env(key, def string) string { if v := os.Getenv(key); v != "" { return v } return def }