f30a80841e
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
148 lines
9.2 KiB
HTML
148 lines
9.2 KiB
HTML
{{/* THE APP PAGE'S SHARED SHELL (v0.315.0).
|
|
|
|
One header and one set of banners for all four tabs, so a held app says it is held on the log
|
|
tab too and the household never has to go back to the details page to find out why.
|
|
|
|
Order is load-bearing and is the alert-placement rule (R-571), unchanged: the page-level
|
|
banners sit ABOVE the tab bar, because they are about the APP, not about the tab. An inline
|
|
alert belongs inside the tab that owns it.
|
|
|
|
The header carries NO Start/Restart/Stop. They used to sit here and read as menu buttons; they
|
|
are now the first card of the „Beallitasok" tab, where a state change belongs. */}}
|
|
|
|
{{define "app_shell_start"}}
|
|
<a href="/stacks" class="app-back">{{T "app_info.alkalmazasok"}}</a>
|
|
|
|
<div class="app-hero">
|
|
<img class="app-hero-logo" src="{{logoURL .Meta.Slug}}"
|
|
alt="{{.Meta.DisplayName}}" data-fallback="/static/app-placeholder.svg"
|
|
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 .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}}">● {{stateLabel .Stack.State}}</span>
|
|
{{if .Stack.Orphaned}}<span class="app-chip">{{T "common.elavult"}}</span>{{end}}
|
|
{{template "meta_badge" (lifecycleBadge .Meta)}}
|
|
{{template "meta_badge" (updateBadge .Stack)}}{{if and (gt .Stack.LadderStepsLeft 0) (not .Stack.HoldReason)}}<span class="app-meta" data-ladder-steps="{{.Stack.LadderStepsLeft}}">{{T "app_info.ladder_steps_left"}} {{.Stack.LadderStepsLeft}}</span>{{end}}
|
|
{{/* v0.316.0: the auto-update SUCCESS line is meta text here, not a banner below.
|
|
A success that asked nothing of the household is a date, not news; the banners
|
|
under the hero stay for the things that DO need them (a failed update, a hold,
|
|
lost files, a missing drive) — R-571's placement rule is untouched. */}}
|
|
{{if .EffectiveSubdomain}}<span class="app-meta" data-app-address="1">{{.EffectiveSubdomain}}.{{.Domain}}</span>{{end}}
|
|
{{if .AutoUpdateLine}}<span class="app-meta" data-auto-update="done">{{.AutoUpdateLine}}</span>{{end}}
|
|
{{else}}
|
|
<span class="app-chip">~{{.Meta.Resources.MemRequest}} RAM</span>
|
|
{{if .Meta.Category}}<span class="app-chip">{{.Meta.Category}}</span>{{end}}
|
|
{{if .Meta.Resources.NeedsHDD}}<span class="app-chip">{{T "common.hdd_szukseges"}}</span>{{end}}
|
|
{{if .Meta.Resources.HungarianUI}}<span class="app-chip">{{T "common.magyar_felulet"}}</span>{{end}}
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
{{if and .Deployed (or (and .AppOperational .EffectiveSubdomain) .FilesLink)}}
|
|
<div class="app-hero-actions">
|
|
{{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}}
|
|
<div class="app-hero-actions">
|
|
<button class="btn btn-sm btn-danger" onclick="deleteOrphanStack('{{.Stack.Name}}')">{{T "common.torles"}}</button>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
|
|
{{template "app_banners" .}}
|
|
{{template "app_tab_bar" .}}
|
|
{{end}}
|
|
|
|
{{/* app_banners — every page-level alert the app can carry, on EVERY tab. Each keeps the marker it
|
|
had on the details page, so the tests and the operator's greps still find it. */}}
|
|
{{define "app_banners"}}
|
|
{{- if .Meta.IsAbandoned}}
|
|
<div class="alert alert-warning" style="margin-top:.75rem">{{T "app_info.az_alkalmazas_fejlesztoje_felhagyott_a"}}</div>
|
|
{{- end}}
|
|
{{- if .LostFiles}}
|
|
<div class="app-info-card alert-warning" style="margin-top:1rem" id="lost-files-card">
|
|
<h3>{{T "app_info.lost_files_title"}}</h3>
|
|
<p>{{T "app_info.lost_files_text"}}{{if .LostBootAt}} <strong data-lost-boot-at>{{.LostBootAt}}</strong>{{end}}</p>
|
|
<ul data-lost-files>
|
|
{{- range .LostFiles}}
|
|
<li><span class="mono">{{.Name}}</span>{{if .Taken}} — {{T "app_info.lost_files_taken"}} {{.Taken}}{{end}}</li>
|
|
{{- end}}
|
|
</ul>
|
|
<p>{{T "app_info.lost_files_hint"}}</p>
|
|
<button type="button" class="btn btn-sm btn-outline" onclick="appPress(this, '/apps/{{.Meta.Slug}}/lost-files/dismiss')">{{T "app_info.lost_files_dismiss_btn"}}</button>
|
|
<span class="form-hint app-press-err" style="display:none;color:var(--red)"></span>
|
|
</div>
|
|
{{- end}}
|
|
{{if .Stack.Updating}}
|
|
<div class="alert alert-info" style="margin-top:1rem" data-update-phase="{{.Stack.UpdatePhase}}">{{updatePhaseText .Stack}}</div>
|
|
{{else if and .Stack.HoldReason (eq .Stack.HoldKind "unhealthy_stop")}}
|
|
<div class="alert alert-warning" style="margin-top:1rem" data-unhealthy-stop="true">{{holdText .Stack}}</div>
|
|
{{else if .Stack.HoldReason}}
|
|
<div class="alert alert-error" style="margin-top:1rem" data-held="true">{{holdText .Stack}}{{if not .Stack.HoldNoWholeCopy}} <a href="/backups/apps" class="btn btn-sm btn-outline">{{T "app_info.mentesek"}}</a>{{end}}{{if .HoldLogKept}}<br><span data-hold-log="true">{{T "app_info.hold_log_kept"}}</span> <a href="/apps/{{.Meta.Slug}}/logs" class="btn btn-sm btn-outline">{{T "app_info.naplo"}}</a>{{end}}</div>
|
|
{{else if .Stack.UpdateError}}
|
|
<div class="alert alert-warning" style="margin-top:1rem" data-update-error="true">{{updateErrorText .Stack}}</div>
|
|
{{else if .UpdateUndoneLine}}
|
|
<div class="alert alert-info" style="margin-top:1rem" data-update-undone="true">{{.UpdateUndoneLine}}</div>
|
|
{{end}}{{if and .Stack.HealthProbe .Stack.HealthProbe.NotChecked}}{{/* R-772: the declared health check could not run — said, never shown as healthy. */}}
|
|
<p class="form-hint" style="margin-top:0.5rem" data-health-not-checked="true">{{T "health.no_probe_container"}}</p>{{end}}
|
|
{{if .MissingStorageLabel}}
|
|
<div class="alert alert-warning" style="margin-top:1rem">
|
|
<strong><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> {{T "app_info.hianyzo_tarhely"}} {{.MissingStorageLabel}}</strong><br>
|
|
{{T "app_info.ennek_az_alkalmazasnak_az_adattaroloja"}}
|
|
</div>
|
|
{{end}}
|
|
{{end}}
|
|
|
|
{{/* app_tab_bar — plain links, no JavaScript. An alert's link must land ON a tab, and the log
|
|
poller must not run on the other three; hidden panels give neither. */}}
|
|
{{define "app_tab_bar"}}
|
|
{{- if .AppTabs}}
|
|
<nav class="app-tabs" aria-label="{{T "app_page.tabs_label"}}">
|
|
{{- range .AppTabs}}
|
|
<a class="app-tab{{if .Active}} app-tab-active{{end}}" href="{{.Href}}" data-tab="{{.Key}}"
|
|
{{if .Active}}aria-current="page" aria-selected="true"{{else}}aria-selected="false"{{end}}>{{.Label}}</a>
|
|
{{- end}}
|
|
</nav>
|
|
{{- end}}
|
|
{{end}}
|
|
|
|
{{/* app_press_script — the shared POST-and-reload used by every card button on the page. It was the
|
|
details page's own script; the cards moved to tabs and it came with them. */}}
|
|
{{define "app_press_script"}}
|
|
<script>
|
|
function appPress(btn, url) {
|
|
var err = btn.parentNode.querySelector('.app-press-err');
|
|
if (err) { err.style.display = 'none'; }
|
|
btn.disabled = true;
|
|
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (j) {
|
|
if (!j.ok) { throw new Error(j.error || ''); }
|
|
window.location.reload();
|
|
})
|
|
.catch(function (e) { btn.disabled = false; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
|
|
}
|
|
// v0.315.0: one script for EVERY after-start field (Part 4). The env var is the only thing that
|
|
// varies, and the server decides what the value means — see after_start.go.
|
|
function appAfterStart(btn, slug, envVar) {
|
|
var box = btn.closest('.after-start-card'), err = box.querySelector('.app-press-err');
|
|
var input = box.querySelector('input');
|
|
err.style.display = 'none'; btn.disabled = true;
|
|
fetch('/apps/' + slug + '/after-start/' + envVar, {
|
|
method: 'POST',
|
|
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
|
credentials: 'same-origin',
|
|
body: JSON.stringify({value: (input ? input.value : '').trim()})
|
|
}).then(function (r) { return r.json(); })
|
|
.then(function (j) { if (!j.ok) { throw new Error(j.error || ''); } window.location.reload(); })
|
|
.catch(function (e) { btn.disabled = false; err.textContent = e.message; err.style.display = 'inline'; });
|
|
}
|
|
</script>
|
|
{{end}}
|