From 88505545002bc2598ca318939613470691ab3718 Mon Sep 17 00:00:00 2001 From: Astra Date: Mon, 14 Sep 2026 18:59:24 +0100 Subject: [PATCH] admin: fix server settings env editor crashing on empty template ReadEnvGroups returned a nil slice (out := groups[:0]) whenever no group ended up with fields, which marshals to JSON null instead of []. The admin UI's EnvSection calls groups.reduce()/iterates unconditionally, so that null crashed the Server Settings page. Also switch the source of the panel structure from .env.example to .env once .env exists, falling back to .env.example only pre-setup: .env already carries the same group headers and comments after any save (WriteEnvValues always rewrites it from .env.example's exact text), so it's the more current source of what's actually configured. Co-Authored-By: Claude Sonnet 5 --- .../web/src/pages/ServerSettingsPage.tsx | 4 +-- internal/procctl/procctl.go | 30 ++++++++++++------- 2 files changed, 22 insertions(+), 12 deletions(-) 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)