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
@@ -25,13 +25,16 @@
{{range .DataPathCards}}
<div class="datapath-row">
<div class="datapath-head">
<a href="{{.Link}}" target="_blank" rel="noopener" class="datapath-link">{{.Label}} ↗</a>
{{- /* v0.320.0: a folder whose FileBrowser source is not on the box now is text, never a link. */}}
{{if .Link}}<a href="{{.Link}}" target="_blank" rel="noopener" class="datapath-link"{{if .IsMedia}} data-media-folder="1"{{end}}>{{.Label}} ↗</a>{{else}}<span class="datapath-link" data-folder-unavailable="1">{{.Label}}</span> <span class="datapath-space">{{T "datapath.unavailable"}}</span>{{end}}
{{if .FreeSpace}}<span class="datapath-space">{{.FreeSpace}}</span>{{end}}
</div>
{{if .Consequence}}<p class="datapath-note">{{.Consequence}}</p>{{end}}
</div>
{{end}}
</div>
{{/* v0.320.0: the file manager asks for its own login the first time — say so, and where it is. */}}
<p class="form-hint" data-fb-login-note="1">{{T "app_page.files_login_note"}} <a href="/apps/filebrowser">{{T "app_page.files_login_link"}}</a></p>
</div>
{{end}}
@@ -105,6 +105,12 @@ function appShotGone(img) {
</div>
{{- end}}
</div>
{{else if .InfraInfo}}
{{/* v0.320.0: part of the box — what it is, in two lines, instead of „a guide is still to come". */}}
<div class="app-section" data-infra-guide="1">
<p>{{.InfraInfo.Description}}</p>
<p>{{T "app_page.infra_guide"}}</p>
</div>
{{else}}
<div class="app-section" data-guide-missing="1"><p>{{T "app_page.guide_missing"}}</p></div>
{{end}}
@@ -51,7 +51,11 @@
<h3>{{T "app_page.mukodes"}}</h3>
<p class="form-hint" data-run-state="{{.Stack.State}}">{{stateLabel .Stack.State}}</p>
<div style="display:flex;gap:.5rem;flex-wrap:wrap;margin-top:.5rem">
{{if isOperational .Stack.State}}
{{if .Infra}}
{{/* v0.320.0: part of the box — Restart only, as the app list offers it; the box starts it
again by itself, and Stop is refused server-side for every protected stack. */}}
{{if isOperational .Stack.State}}<button class="btn btn-sm btn-warning" data-run-action="restart" onclick="stackAction(event, '{{.Stack.Name}}', 'restart')">{{T "common.ujrainditas"}}</button>{{end}}
{{else if isOperational .Stack.State}}
<button class="btn btn-sm btn-warning" data-run-action="restart" onclick="stackAction(event, '{{.Stack.Name}}', 'restart')">{{T "common.ujrainditas"}}</button>
<button class="btn btn-sm btn-danger" data-run-action="stop" onclick="var b=this; felhomConfirm(b, '{{T "app_page.stop_confirm"}}', function(){ stackAction(event, '{{$.Stack.Name}}', 'stop'); })">{{T "deploy.leallitas"}}</button>
{{else}}
@@ -123,10 +127,15 @@
<div class="app-sections">
{{/* v0.319.0 — „BELEPES AZ ALKALMAZASBA": the name and the password of the app's first login,
side by side. The values are fetched on demand (R-254, R-709); the page learns only whether
the box holds one (app_login.go), and a field it holds none for is a sentence, not an eye. */}}
{{if .LoginFields}}
<div class="app-section" id="app-login-panel" data-login-panel="1">
the box holds one (app_login.go), and a field it holds none for is a sentence, not an eye.
v0.320.0 — EVERY installed app has this panel, with exactly ONE answer (app_login.go
loginAnswerFor): the box's fields · the login the app wrote (InitialCreds, FileBrowser's too) ·
or a sentence for an app the box holds no login for (first visit / e-mail / none). */}}
{{if .LoginAnswer}}
<div class="app-section" id="app-login-panel" data-login-panel="1" data-login-answer="{{.LoginAnswer}}">
<h3>{{T "app_page.login_title"}}</h3>
{{- if eq .LoginAnswer "fields"}}
<p>{{T "app_page.login_desc"}}</p>
<div class="app-login-pair">
{{- range .LoginFields}}
@@ -147,12 +156,7 @@
</div>
{{- end}}
</div>
</div>
{{end}}
{{if .InitialCreds}}
<div class="app-section">
<h3>{{T "app_info.kezdeti_belepesi_adatok"}}</h3>
{{- else if eq .LoginAnswer "initial"}}
<table class="initcred-table">
{{if .InitialCreds.Username}}
<tr>
@@ -163,7 +167,7 @@
{{if .InitialCredsOperatorSet}}
<tr>
<td class="initcred-label" style="padding:.25rem .75rem .25rem 0;color:var(--text-3);white-space:nowrap">{{T "common.jelszo"}}</td>
<td>{{T "app_info.az_uzemelteto_allitotta_be"}}</td>
<td data-fb-operator="1">{{T "app_page.fb_operator_set"}}</td>
</tr>
{{else}}
<tr>
@@ -182,6 +186,25 @@
</table>
{{if .InitialCreds.Note}}<p class="app-info-creds-warn">{{.InitialCreds.Note}}</p>{{end}}
{{if not .InitialCredsOperatorSet}}<p class="app-info-creds-warn">{{T "app_info.az_elso_bejelentkezes_utan_azonnal"}}</p>{{end}}
{{- if .InitialCredsOperatorSet}}
{{/* v0.320.0: the box does not know this password, so it offers to set a new one (stacks
ResetFileBrowserAdminPassword — the file browser's own CLI, spiked on demo-hp). POST + CSRF,
confirmed first: the old password stops working. */}}
<div style="margin-top:.75rem">
<button type="button" class="btn btn-sm btn-outline" data-fb-reset="1" onclick="var b=this; felhomConfirm(b, '{{T "app_page.fb_reset_confirm"}}', function(){ appPress(b, '/apps/{{.Meta.Slug}}/initial-credentials/reset'); })">{{T "app_page.fb_reset_btn"}}</button>
<span class="form-hint app-press-err" style="display:none;color:var(--red)"></span>
</div>
{{- end}}
{{- else if eq .LoginAnswer "email"}}
<p data-login-kind="email">{{T "app_page.login_kind_email"}}</p>
{{- else if and (eq .LoginAnswer "none") .Meta.FamilyGate}}
{{/* No login of its own, behind the family gate (decision 64): the family sign-in IS its login. */}}
<p data-login-kind="family">{{T "app_page.login_kind_family"}}</p>
{{- else if eq .LoginAnswer "none"}}
<p data-login-kind="none">{{T "app_page.login_kind_none"}}</p>
{{- else}}
<p data-login-kind="first_visit">{{T "app_page.login_kind_first_visit"}}</p>
{{- end}}
</div>
{{end}}
@@ -575,6 +598,7 @@
cannot be changed sits under the title it is about, not at the bottom of the tab. The domain,
the subdomain and the data folder are the „Cim" and „Tarhely" rows; the login is the panel
above; everything else is a row here (app_login.go appSettingRows). */}}
{{- if not .Infra}}
<div class="app-section" data-address-keys="1">
<h3>{{T "app_page.address_and_keys"}}</h3>
<p class="app-field-hint" data-readonly-hint="1">{{T "app_page.installed_readonly"}}</p>
@@ -604,6 +628,7 @@
</div>
{{- end}}
</div>
{{- end}}
</div>
{{if .InitialCreds}}
@@ -19,7 +19,7 @@
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='{{logoPNGURL .Meta.Slug}}';}else if(this.dataset.fallback&&this.dataset.step==='1'){this.dataset.step='2';this.src=this.dataset.fallback;}else{this.onerror=null;this.style.visibility='hidden';}">
<div class="app-hero-text">
<h2 class="app-hero-name">{{.Meta.DisplayName}}</h2>
{{if .AppInfo.Tagline}}<p class="app-hero-tagline">{{.AppInfo.Tagline}}</p>{{else if .Meta.Description}}<p class="app-hero-tagline">{{.Meta.Description}}</p>{{end}}
{{if .InfraInfo}}<p class="app-hero-tagline" data-infra-app="1">{{.InfraInfo.Description}}</p>{{else if .AppInfo.Tagline}}<p class="app-hero-tagline">{{.AppInfo.Tagline}}</p>{{else if .Meta.Description}}<p class="app-hero-tagline">{{.Meta.Description}}</p>{{end}}
<div class="app-chips">
{{if .Deployed}}
<span class="app-chip" data-state-chip="{{.Stack.State}}">&#9679; {{stateLabel .Stack.State}}</span>
@@ -40,9 +40,12 @@
{{end}}
</div>
</div>
{{if and .Deployed .AppOperational .EffectiveSubdomain}}
{{if and .Deployed (or (and .AppOperational .EffectiveSubdomain) .FilesLink)}}
<div class="app-hero-actions">
<a href="https://{{.EffectiveSubdomain}}.{{.Domain}}{{.Meta.OpenPath}}" target="_blank" class="btn btn-primary" data-app-open="1">{{T "app_info.megnyitas"}}</a>
{{if and .AppOperational .EffectiveSubdomain}}<a href="https://{{.EffectiveSubdomain}}.{{.Domain}}{{.Meta.OpenPath}}" target="_blank" class="btn btn-primary" data-app-open="1">{{T "app_info.megnyitas"}}</a>{{end}}
{{/* v0.320.0: one of the app's folders in FileBrowser (datapath_card.go filesButtonLink). FileBrowser may
ask for its own login first; the button never promises one click. */}}
{{if .FilesLink}}<a href="{{.FilesLink}}" target="_blank" rel="noopener" class="btn btn-outline" data-app-files="1">{{T "app_page.files_btn"}}</a>{{end}}
</div>
{{end}}
{{if and .Deployed .Stack.Orphaned}}
+1 -1
View File
@@ -1360,7 +1360,7 @@ a.stat-card:hover {
.app-hero-text { flex: 1 1 320px; min-width: 0; }
.app-hero-name { font-size: 26px; font-weight: 700; color: var(--text-1); margin: 0; line-height: 1.2; }
.app-hero-tagline { font-size: 15px; color: var(--text-2); margin: 4px 0 10px; }
.app-hero-actions { margin-left: auto; }
.app-hero-actions { margin-left: auto; display: flex; gap: .5rem; flex-wrap: wrap; }
.app-chips { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.app-chip {