v0.320.0: FileBrowser is part of the box again; every app says where its login is; a „Fájlok" button
gates / gates (push) Successful in 50s

FileBrowser (and traefik, cloudflared, samba) has no app.yaml, so Deployed is false. Since v0.315.0 the
app page read Deployed alone and showed FileBrowser "not installed", with Install and without its login.
A protected infra stack is now installed by definition on the app page (Deployed || Protected, as every
other surface already counted): settings/guide/log tabs, Restart only, no install page, no data tab.
The Install press would have deployed the infra stack as an app: DeployStack and the deploy API now
refuse a protected stack.

In the operator state (the box does not know FileBrowser's password - both demo boxes) the login panel
offers "Új jelszó beállítása": the file browser's own CLI with the server stopped (spiked on demo-hp:
new 200, old 401, users/shares/settings unchanged), verified, stored encrypted, state generated.

Every installed app's settings tab has the login panel with exactly one answer: the box's fields, the
login the app wrote (InitialCreds), or a sentence chosen by the catalog's new app_info.login_kind
(first_visit default, email, none; none behind the family gate says the family sign-in is the login).

"Fájlok ↗" opens the library folder, else the first declared folder, else a media app's first present
drive's media folder. The data tab lists each drive's media folders; a folder whose drive is not bound
into FileBrowser is text, never a link.

Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-16 15:19:14 +02:00
parent 39bf56298d
commit f30a80841e
59 changed files with 5669 additions and 104 deletions
@@ -508,6 +508,62 @@ func i18nCases() []i18nCase {
d["HoldLogKept"] = true
return d
}})
// v0.320.0 — logins and folders: the infra app page (FileBrowser), the two login sentences a catalog key
// chooses, and the „Fajlok" button with a media folder and a folder whose drive is away. Captured when born.
infraFB := func() map[string]interface{} {
d := i18nLayoutData("stacks", "FileBrowser")
st := stacks.Stack{Name: "filebrowser", Deployed: false, Protected: true, State: stacks.StateRunning}
st.Meta = stacks.Metadata{DisplayName: "FileBrowser", Slug: "filebrowser"}
d["Stack"], d["Meta"], d["AppInfo"] = st, st.Meta, st.Meta.AppInfo
d["Infra"] = true
d["InfraInfo"] = &InfraMeta{DisplayName: "FileBrowser", Description: "Fájlkezelő — a tárhely fájljainak böngészése a böngészőből.", Linked: true}
d["EffectiveSubdomain"] = "files"
d["Deployed"], d["AppOperational"] = true, true
return d
}
base = append(base, i18nCase{"app_settings_infra_filebrowser", "app_settings", func() map[string]interface{} {
d := infraFB()
d["AppTabs"] = []AppTabLink{{Key: appTabSettings, Href: "/apps/filebrowser/settings", Label: "Beállítások", Active: true},
{Key: appTabGuide, Href: "/apps/filebrowser/guide", Label: "Útmutató"}, {Key: appTabLogs, Href: "/apps/filebrowser/logs", Label: "Napló"}}
d["InitialCreds"] = &stacks.ExtractedCreds{Available: true, Username: "admin", Note: "A Fájlkezelő belépése."}
d["InitialCredsHasPassword"] = true
return d
}}, i18nCase{"app_guide_infra_filebrowser", "app_guide", func() map[string]interface{} {
d := infraFB()
d["AppTabs"] = []AppTabLink{{Key: appTabSettings, Href: "/apps/filebrowser/settings", Label: "Beállítások"},
{Key: appTabGuide, Href: "/apps/filebrowser/guide", Label: "Útmutató", Active: true}, {Key: appTabLogs, Href: "/apps/filebrowser/logs", Label: "Napló"}}
return d
}}, i18nCase{"app_settings_login_email", "app_settings", func() map[string]interface{} {
d := i18nLayoutData("stacks", "Rallly")
st := i18nStack("rallly", "Rallly", stacks.StateRunning, true)
st.Meta.AppInfo.LoginKind = stacks.LoginKindEmail
d["Stack"], d["Meta"], d["AppInfo"] = st, st.Meta, st.Meta.AppInfo
return d
}}, i18nCase{"app_settings_login_none", "app_settings", func() map[string]interface{} {
d := i18nLayoutData("stacks", "BentoPDF")
st := i18nStack("bentopdf", "BentoPDF", stacks.StateRunning, true)
st.Meta.AppInfo.LoginKind = stacks.LoginKindNone
d["Stack"], d["Meta"], d["AppInfo"] = st, st.Meta, st.Meta.AppInfo
return d
}}, i18nCase{"app_settings_login_family", "app_settings", func() map[string]interface{} {
d := i18nLayoutData("stacks", "MeTube")
st := i18nStack("metube", "MeTube", stacks.StateRunning, true)
st.Meta.AppInfo.LoginKind = stacks.LoginKindNone
st.Meta.FamilyGate = true
d["Stack"], d["Meta"], d["AppInfo"] = st, st.Meta, st.Meta.AppInfo
return d
}}, i18nCase{"app_data_files_media", "app_data", func() map[string]interface{} {
d := i18nLayoutData("stacks", "Plex")
st := i18nStack("plex", "Plex", stacks.StateRunning, true)
d["Stack"], d["Meta"], d["AppInfo"] = st, st.Meta, st.Meta.AppInfo
d["DisplayName"], d["StackName"] = "Plex", "plex"
d["FilesLink"] = "https://files.example.hu/files/HDD%201/media"
d["DataPathCards"] = []DataPathCard{
{Label: "Filmek — HDD 1", Link: "https://files.example.hu/files/HDD%201/media/movies", IsMedia: true, Role: stacks.RoleLibrary},
{Label: "Filmek — HDD 2", IsMedia: true, Role: stacks.RoleLibrary}}
return d
}})
// v0.315.0 — THE APP PAGE'S OWN TABS. Three cases, each for a tab whose content no earlier case
// covered: the data tab's folder cards and move offer, the guide tab with a full guide, and the
// guide tab's „no guide yet" line. Captured when born.
@@ -601,6 +657,17 @@ func fillI18nAppShell(s *Server, lang, tmpl string, d map[string]interface{}) {
if _, set := d["AppTabs"]; !set {
d["AppTabs"] = s.appTabLinksLang(lang, slug, deployed, tab)
}
// v0.320.0: the settings tab's login panel answer, chosen by the handler's own function from the same data.
if _, set := d["LoginAnswer"]; !set && tab == appTabSettings && deployed {
var meta *stacks.Metadata
switch m := d["Meta"].(type) {
case stacks.Metadata:
meta = &m
case *stacks.Metadata:
meta = m
}
d["LoginAnswer"] = loginAnswerFor(d, meta)
}
}
func renderI18nCase(t *testing.T, s *Server, lang string, c i18nCase) string {