Compare commits

..

2 commits

Author SHA1 Message Date
39f5468b8b Merge branch 'fix/admin-env-groups-nil-reduce'
Some checks failed
CI / Go tests (push) Has been cancelled
CI / Admin web build (push) Has been cancelled
CI / Grammy store bot (push) Has been cancelled
CI / Docker main topology smoke (push) Has been cancelled
2026-09-14 19:01:21 +01:00
8850554500 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 <noreply@anthropic.com>
2026-09-14 19:01:18 +01:00
2 changed files with 22 additions and 12 deletions

View file

@ -415,9 +415,9 @@ function EnvSection() {
setError(""); setError("");
try { try {
const g = await api.serverEnv(); const g = await api.serverEnv();
setGroups(g); setGroups(g ?? []);
const next: Record<string, string> = {}; const next: Record<string, string> = {};
for (const group of g) { for (const group of g ?? []) {
for (const field of group.fields) { for (const field of group.fields) {
next[field.key] = field.value; next[field.key] = field.value;
} }

View file

@ -517,23 +517,33 @@ type EnvGroup struct {
Fields []EnvField `json:"fields"` Fields []EnvField `json:"fields"`
} }
// ReadEnvGroups parses .env.example into the same panel-visible groups // ReadEnvGroups parses .env into the same panel-visible groups
// server-panel.py's parse_env_template() does (identical header/format // server-panel.py's parse_env_template() parses out of .env.example
// rules -- see that function's docstring), then fills in each field's // (identical header/format rules -- see that function's docstring). .env
// current effective value from .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) { func (m *Manager) ReadEnvGroups() ([]EnvGroup, error) {
tmplPath := filepath.Join(m.Root, ".env.example") tmplPath := filepath.Join(m.Root, ".env")
tmplData, err := os.ReadFile(tmplPath) 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) { if os.IsNotExist(err) {
// A nil slice here would marshal to JSON null instead of [] -- the // A nil slice here would marshal to JSON null instead of [] -- the
// admin UI's env editor unconditionally calls .reduce()/.map() on // admin UI's env editor unconditionally calls .reduce()/.map() on
// this response and crashes on null. .env.example isn't shipped // this response and crashes on null. Neither .env nor .env.example
// inside the container image, so this path is the normal case in // exists inside the container image before Setup has run, so this
// production, not an edge case. // path is the normal pre-setup case, not an edge case.
return []EnvGroup{}, nil return []EnvGroup{}, nil
} }
if err != 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() envValues, err := m.readEnvFile()
if err != nil { if err != nil {
@ -612,7 +622,7 @@ func (m *Manager) ReadEnvGroups() ([]EnvGroup, error) {
inCommentRun = false inCommentRun = false
} }
out := groups[:0] out := make([]EnvGroup, 0, len(groups))
for _, g := range groups { for _, g := range groups {
if len(g.Fields) > 0 { if len(g.Fields) > 0 {
out = append(out, g) out = append(out, g)