d170a2aecf
gates / gates (push) Successful in 52s
- style A (operator 2026-10-16): hero, tab bar and every section are panels on --bg-1, one width, 16px apart; the install page's form groups use the same rule; the settings tab's 700px inner wrapper is gone - „Belépés az alkalmazásba": name + password side by side, fetched on demand (R-254, R-709); pairing in app_login.go - the empty-value rule: no stored value -> a sentence, never an eye; page and reveal endpoint share storedValuePresent - „Cím és kulcsok" is rows (no Domain input, no disabled storage select, no install form); hint under the title - tests red-proved: app_page_tuning_test.go (4); 36 app-page parity fixtures re-captured, 4 new; install fixtures unchanged Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
160 lines
5.9 KiB
Go
160 lines
5.9 KiB
Go
package web
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
|
)
|
|
|
|
// THE LOGIN PANEL AND THE „CIM ES KULCSOK" ROWS (v0.319.0) — the settings tab's view of what was
|
|
// set at install.
|
|
//
|
|
// Until v0.318.0 the settings tab re-rendered the install wizard's form with every input disabled:
|
|
// the generated login name sat in „Automatikusan generalt ertekek" next to a disabled Domain input,
|
|
// and the password sat three sections lower among the other install answers. The household had to
|
|
// find two halves of one login. Now one panel holds the pair, and the rest are label/value rows.
|
|
//
|
|
// THE EMPTY-VALUE RULE. A field the box holds no value for is SAID, never offered an eye: the reveal
|
|
// endpoint answers 404 for it („Ehhez a mezohoz nincs mentett ertek"), which is a refusal the
|
|
// household could only meet by pressing. Measured 2026-10-16 on demo-hp: Calibre-Web was installed
|
|
// on 2026-08-21, its template gained ADMIN_PASSWORD on 2026-09-29, and InjectMissingFields never
|
|
// fills a `type: password` field — so the box never had one (audits/app-page-tuning-2026-10-16/P0).
|
|
// The page and the endpoint decide „stored" with ONE function, storedValuePresent, so the page
|
|
// cannot offer what the endpoint would refuse. Pinned by TestAppPage_NoStoredValueShowsSentence.
|
|
//
|
|
// SECRETS. A `type: secret` name and every `type: password` value stay off the page (R-254, R-709);
|
|
// only the boolean Stored reaches the template. A `type: text` name is not a secret — the old
|
|
// disabled input carried it as its value — and is shown as text.
|
|
|
|
// storedValuePresent is the one definition of „the box holds a value for this field", shared by the
|
|
// settings tab and appAutoFieldRevealHandler's 404 branch.
|
|
func storedValuePresent(val string) bool { return strings.TrimSpace(val) != "" }
|
|
|
|
// AppLoginField is one half of the login panel. Value is set ONLY for a plain-text name; a secret
|
|
// or a password never carries its value here.
|
|
type AppLoginField struct {
|
|
EnvVar string
|
|
Label string
|
|
IsName bool // the name half; false for the password half
|
|
OnDemand bool // fetched on demand through /stacks/{name}/auto-field/reveal
|
|
Value string // a `type: text` name only
|
|
Stored bool // the box holds a non-blank value
|
|
Restored bool // R-694: a restore generated it, so it is not the app's login and is never shown
|
|
}
|
|
|
|
// AppSettingRow is one label/value row of „Cim es kulcsok". An OnDemand row carries no value; the
|
|
// eye fetches it.
|
|
type AppSettingRow struct {
|
|
EnvVar string
|
|
Label string
|
|
Value string
|
|
OnDemand bool
|
|
}
|
|
|
|
// loginNameSuffixes are the env-var endings the catalog uses for the name next to a password:
|
|
// ADMIN_USER + ADMIN_PASSWORD (calibre-web), ADMIN_EMAIL + ADMIN_PASSWORD (kimai),
|
|
// NEXTCLOUD_ADMIN_USER + NEXTCLOUD_ADMIN_PASSWORD, and so on — measured over the catalog on
|
|
// 2026-10-16, every pair shares the prefix.
|
|
var loginNameSuffixes = []string{"USER", "USERNAME", "EMAIL"}
|
|
|
|
// loginFieldIndexes returns, in deploy_fields order, the indexes of the login fields: every
|
|
// `type: password` field (not asked after start), each preceded by its name field when the
|
|
// template has one with the same prefix and type `secret` or `text`. A password with no clean
|
|
// pair stands alone.
|
|
func loginFieldIndexes(meta *stacks.Metadata) []int {
|
|
if meta == nil {
|
|
return nil
|
|
}
|
|
byEnv := make(map[string]int, len(meta.DeployFields))
|
|
for i, f := range meta.DeployFields {
|
|
byEnv[f.EnvVar] = i
|
|
}
|
|
var out []int
|
|
for i, f := range meta.DeployFields {
|
|
if f.Type != "password" || f.AfterStart {
|
|
continue
|
|
}
|
|
// ADMIN_PASSWORD -> prefix "ADMIN_" -> ADMIN_USER / ADMIN_USERNAME / ADMIN_EMAIL.
|
|
prefix := strings.TrimSuffix(f.EnvVar, "PASSWORD")
|
|
if prefix != f.EnvVar {
|
|
for _, suf := range loginNameSuffixes {
|
|
if j, ok := byEnv[prefix+suf]; ok {
|
|
if t := meta.DeployFields[j].Type; (t == "secret" || t == "text") && !meta.DeployFields[j].AfterStart {
|
|
out = append(out, j)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
out = append(out, i)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// appLoginFields builds the panel from the DECRYPTED env (before any value is removed from it).
|
|
func appLoginFields(meta *stacks.Metadata, env map[string]string, restored map[string]bool) []AppLoginField {
|
|
var out []AppLoginField
|
|
for _, i := range loginFieldIndexes(meta) {
|
|
f := meta.DeployFields[i]
|
|
lf := AppLoginField{EnvVar: f.EnvVar, Label: f.Label, IsName: true, Stored: storedValuePresent(env[f.EnvVar])}
|
|
switch f.Type {
|
|
case "password":
|
|
lf.IsName, lf.OnDemand, lf.Restored = false, true, restored[f.EnvVar]
|
|
case "secret":
|
|
lf.OnDemand = true
|
|
default:
|
|
lf.Value = env[f.EnvVar]
|
|
}
|
|
out = append(out, lf)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// appSettingRows is everything else an installed app was given at install, as rows: the generated
|
|
// secrets (eye) and the plain answers (text). Skipped, because the page says them elsewhere: the
|
|
// domain and the subdomain (the „Cim" row), the data folder (the „Tarhely" row), the login fields
|
|
// (the login panel), and a field asked after start (its own card).
|
|
func appSettingRows(meta *stacks.Metadata, env map[string]string, yes, no string) []AppSettingRow {
|
|
if meta == nil {
|
|
return nil
|
|
}
|
|
login := map[int]bool{}
|
|
for _, i := range loginFieldIndexes(meta) {
|
|
login[i] = true
|
|
}
|
|
var out []AppSettingRow
|
|
for i, f := range meta.DeployFields {
|
|
if login[i] || f.AfterStart {
|
|
continue
|
|
}
|
|
val := env[f.EnvVar]
|
|
switch f.Type {
|
|
case "domain", "subdomain", "path", "password":
|
|
continue
|
|
case "secret":
|
|
if storedValuePresent(val) {
|
|
out = append(out, AppSettingRow{EnvVar: f.EnvVar, Label: f.Label, OnDemand: true})
|
|
}
|
|
continue
|
|
case "select":
|
|
for _, o := range f.Options {
|
|
if o.Value == val {
|
|
val = o.Label
|
|
break
|
|
}
|
|
}
|
|
case "boolean":
|
|
if val == "true" {
|
|
val = yes
|
|
} else {
|
|
val = no
|
|
}
|
|
}
|
|
if !storedValuePresent(val) {
|
|
continue
|
|
}
|
|
out = append(out, AppSettingRow{EnvVar: f.EnvVar, Label: f.Label, Value: val})
|
|
}
|
|
return out
|
|
}
|