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

This commit is contained in:
Astra 2026-09-14 19:01:21 +01:00
commit 39f5468b8b
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)