Some checks failed
secrets-guard / encrypted (push) Successful in 24s
shell-lint / shellcheck (push) Successful in 26s
Deploy frontend to LAN dev server / build (push) Successful in 2m11s
Build + push frontend image (Forgejo registry) / build-push (push) Successful in 2m20s
Build + push backend image (Forgejo registry) / build-push (push) Successful in 2m28s
Deploy backend to LAN dev server / build (push) Successful in 2m45s
PR build (required check) / changes (pull_request) Successful in 16s
secrets-guard / encrypted (pull_request) Successful in 16s
shell-lint / shellcheck (pull_request) Successful in 15s
PR build (required check) / build-frontend (pull_request) Has been skipped
PR build (required check) / validate-observability (pull_request) Has been skipped
Deploy frontend to LAN dev server / deploy (push) Successful in 39s
PR build (required check) / build-backend (pull_request) Successful in 1m21s
PR build (required check) / gate (pull_request) Successful in 3s
Deploy backend to LAN dev server / deploy (push) Failing after 2m31s
127 lines
4.9 KiB
Go
127 lines
4.9 KiB
Go
// Package config reads every environment variable the SSR frontend consumes.
|
|
//
|
|
// The names, defaults and required/optional split mirror the Python service
|
|
// exactly (app.py, api_client.py, content.py, paths.py, and the Dockerfile /
|
|
// infra/docker-compose.yml PORT wiring). Do not invent new variable names —
|
|
// the deploy path (compose + /etc/thermograph.env) sets these and only these.
|
|
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Config is the fully-resolved runtime configuration.
|
|
type Config struct {
|
|
// APIBaseInternal is THERMOGRAPH_API_BASE_INTERNAL — the backend's URL on
|
|
// the compose-internal network (e.g. http://backend:8137). REQUIRED: the
|
|
// Python client raised RuntimeError at import when unset ("a missing
|
|
// backend URL should break the boot, not silently 500 on the first
|
|
// request") — Load returns an error and main exits, same philosophy.
|
|
APIBaseInternal string
|
|
|
|
// APIVersion is THERMOGRAPH_API_VERSION (default "v2") — the single pin
|
|
// point for the backend content-API version this service speaks. Bump only
|
|
// in lockstep with a verified backend /api/version check (see
|
|
// frontend CLAUDE.md, "API-version pinning contract").
|
|
APIVersion string
|
|
|
|
// Base is THERMOGRAPH_BASE normalized the way content.py normalized it:
|
|
// strip "/" from both ends, then "/"+rest, or "" when the variable is set
|
|
// to "/" (the deployed clean-root topology — the Dockerfile sets "/").
|
|
// Default when unset: "/thermograph" (LAN dev under a sub-path).
|
|
Base string
|
|
|
|
// AssetBase is THERMOGRAPH_API_BASE_PUBLIC with any trailing "/" removed,
|
|
// falling back to Base when empty — the browser-facing base for static
|
|
// asset / SPA-shell URLs in templates. Empty var = today's same-origin
|
|
// default, where those references stay relative (ASSET_BASE == BASE).
|
|
AssetBase string
|
|
|
|
// SSRCacheTTL is THERMOGRAPH_SSR_CACHE_TTL in seconds (default 600) — the
|
|
// content-API response-cache TTL in the backend client. The Python parsed
|
|
// it with float(env or 600): an empty string means the default, a present
|
|
// but unparsable value crashed the boot — Load mirrors both.
|
|
SSRCacheTTL time.Duration
|
|
|
|
// GoogleVerify / BingVerify are THERMOGRAPH_GOOGLE_VERIFY /
|
|
// THERMOGRAPH_BING_VERIFY, whitespace-trimmed; empty = no verification
|
|
// <meta> tag emitted.
|
|
GoogleVerify string
|
|
BingVerify string
|
|
|
|
// Port is PORT (default "8080"). The Python process itself never read it —
|
|
// the Dockerfile's `uvicorn --port ${PORT}` did — but it is the one knob
|
|
// infra uses to move the listen port, so the Go binary reads the same name.
|
|
Port string
|
|
|
|
// StaticDir / ContentDir are where static assets and the structured SSR
|
|
// copy (glossary.yaml / pages.yaml) live. The Python resolved these from
|
|
// its own source location (paths.py); a compiled binary has no source
|
|
// directory, so these default to "static" and "content" relative to the
|
|
// working directory — run from frontend/ locally, /app in the image (the
|
|
// Dockerfile must COPY static/ and content/ there and keep WORKDIR /app).
|
|
StaticDir string
|
|
ContentDir string
|
|
}
|
|
|
|
// Load resolves the configuration from the process environment.
|
|
func Load() (Config, error) {
|
|
return load(os.LookupEnv)
|
|
}
|
|
|
|
// load is Load with an injectable environment, for tests.
|
|
func load(getenv func(string) (string, bool)) (Config, error) {
|
|
get := func(name, dflt string) string {
|
|
if v, ok := getenv(name); ok {
|
|
return v
|
|
}
|
|
return dflt
|
|
}
|
|
|
|
var cfg Config
|
|
|
|
cfg.APIBaseInternal, _ = getenv("THERMOGRAPH_API_BASE_INTERNAL")
|
|
if cfg.APIBaseInternal == "" {
|
|
return cfg, fmt.Errorf("THERMOGRAPH_API_BASE_INTERNAL must be set (e.g. http://backend:8137)")
|
|
}
|
|
|
|
cfg.APIVersion = get("THERMOGRAPH_API_VERSION", "v2")
|
|
|
|
// content.py / api_client.py: os.environ.get("THERMOGRAPH_BASE",
|
|
// "/thermograph").strip("/"), then "/"+base if base else "".
|
|
base := strings.Trim(get("THERMOGRAPH_BASE", "/thermograph"), "/")
|
|
if base != "" {
|
|
cfg.Base = "/" + base
|
|
}
|
|
|
|
// content.py: os.environ.get("THERMOGRAPH_API_BASE_PUBLIC", "").rstrip("/") or BASE.
|
|
cfg.AssetBase = strings.TrimRight(get("THERMOGRAPH_API_BASE_PUBLIC", ""), "/")
|
|
if cfg.AssetBase == "" {
|
|
cfg.AssetBase = cfg.Base
|
|
}
|
|
|
|
// api_client.py: float(os.environ.get("THERMOGRAPH_SSR_CACHE_TTL", "600") or 600).
|
|
ttlRaw := get("THERMOGRAPH_SSR_CACHE_TTL", "600")
|
|
if ttlRaw == "" {
|
|
ttlRaw = "600"
|
|
}
|
|
ttlSecs, err := strconv.ParseFloat(ttlRaw, 64)
|
|
if err != nil {
|
|
// The Python raised ValueError at import for a garbage value — fail
|
|
// loud here too rather than silently running with a wrong TTL.
|
|
return cfg, fmt.Errorf("THERMOGRAPH_SSR_CACHE_TTL: %w", err)
|
|
}
|
|
cfg.SSRCacheTTL = time.Duration(ttlSecs * float64(time.Second))
|
|
|
|
cfg.GoogleVerify = strings.TrimSpace(get("THERMOGRAPH_GOOGLE_VERIFY", ""))
|
|
cfg.BingVerify = strings.TrimSpace(get("THERMOGRAPH_BING_VERIFY", ""))
|
|
|
|
cfg.Port = get("PORT", "8080")
|
|
cfg.StaticDir = "static"
|
|
cfg.ContentDir = "content"
|
|
return cfg, nil
|
|
}
|