diff --git a/cmd/telesrv-admin/web/src/pages/ServerSettingsPage.tsx b/cmd/telesrv-admin/web/src/pages/ServerSettingsPage.tsx index 84237a88..1a67c500 100644 --- a/cmd/telesrv-admin/web/src/pages/ServerSettingsPage.tsx +++ b/cmd/telesrv-admin/web/src/pages/ServerSettingsPage.tsx @@ -415,9 +415,9 @@ function EnvSection() { setError(""); try { const g = await api.serverEnv(); - setGroups(g); + setGroups(g ?? []); const next: Record = {}; - for (const group of g) { + for (const group of g ?? []) { for (const field of group.fields) { next[field.key] = field.value; } diff --git a/internal/procctl/procctl.go b/internal/procctl/procctl.go index 60fd3c23..5c034ed2 100644 --- a/internal/procctl/procctl.go +++ b/internal/procctl/procctl.go @@ -517,23 +517,33 @@ type EnvGroup struct { Fields []EnvField `json:"fields"` } -// ReadEnvGroups parses .env.example into the same panel-visible groups -// server-panel.py's parse_env_template() does (identical header/format -// rules -- see that function's docstring), then fills in each field's -// current effective value from .env. +// ReadEnvGroups parses .env into the same panel-visible groups +// server-panel.py's parse_env_template() parses out of .env.example +// (identical header/format rules -- see that function's docstring). .env +// carries the same comments/group headers as .env.example because +// WriteEnvValues always rewrites it from .env.example's exact text with +// just the values swapped in, so .env is the more current source once it +// exists -- it reflects fields added to .env.example after this install's +// .env was first created only once WriteEnvValues has run again, same as +// server-panel.py. Falls back to .env.example when .env doesn't exist yet +// (a fresh install before Setup has written one). func (m *Manager) ReadEnvGroups() ([]EnvGroup, error) { - tmplPath := filepath.Join(m.Root, ".env.example") + tmplPath := filepath.Join(m.Root, ".env") tmplData, err := os.ReadFile(tmplPath) + if os.IsNotExist(err) { + tmplPath = filepath.Join(m.Root, ".env.example") + tmplData, err = os.ReadFile(tmplPath) + } if os.IsNotExist(err) { // A nil slice here would marshal to JSON null instead of [] -- the // admin UI's env editor unconditionally calls .reduce()/.map() on - // this response and crashes on null. .env.example isn't shipped - // inside the container image, so this path is the normal case in - // production, not an edge case. + // this response and crashes on null. Neither .env nor .env.example + // exists inside the container image before Setup has run, so this + // path is the normal pre-setup case, not an edge case. return []EnvGroup{}, nil } if err != nil { - return nil, fmt.Errorf("read .env.example: %w", err) + return nil, fmt.Errorf("read %s: %w", filepath.Base(tmplPath), err) } envValues, err := m.readEnvFile() if err != nil { @@ -612,7 +622,7 @@ func (m *Manager) ReadEnvGroups() ([]EnvGroup, error) { inCommentRun = false } - out := groups[:0] + out := make([]EnvGroup, 0, len(groups)) for _, g := range groups { if len(g.Fields) > 0 { out = append(out, g)