e66d4669d6
gates / gates (push) Failing after 47s
An app was six pages — details, install/settings, logs, export, the per-app backup panel and a
details card for moving data — and every "Vissza" on them went to the app LIST, so there was no way
back to the app you were looking at. It is now ONE page with four tab URLs under /apps/{slug}/,
one shared header and one set of banners.
settings Mukodes (Start/Restart/Stop, Stop confirms) + the cards that want something from the
household + the install-time and optional settings. The default for an installed app,
RENDERED at the bare URL and never redirected to.
data the data folders, move to another drive, the nightly copy, the 2nd-copy setting, export
guide tagline, screenshots, use cases, first steps, the catalog's guide, the FAQ, the docs
link — and the WHOLE page for an app that is not installed
logs the log, and the only tab whose poller runs
The tabs are links, not panels: a link in an alert has to land ON a tab, and the log poller must not
run on the other three. Start/Restart/Stop left the page header, where they read as menu buttons.
The old /stacks/{name}/{deploy,logs,export,backup} GETs answer 302 — never 301, which a browser
caches beyond anyone's reach — to the tab that now holds them, query string carried through, with
name->slug resolved through the stack list rather than assumed equal. Every POST on those paths is
untouched: the backup form, the app-email toggle and the reveal endpoints keep their URLs.
A value asked AFTER the app starts is now generic (D2). The catalog marks a field `after_start: true`
and supplies its label; the ACTION is Go, in a registry keyed by slug + env var. The catalog can
never declare what runs — a declared field with no registered action renders nothing, POSTs 404 and
logs one WARN with the env var KEY, never the value. A red-proof found the second half of that guard
unpinned: deleting the "the catalog must have DECLARED this field" check left the suite green, and a
POST for an undeclared field then reached Plex and opened the setup gate.
TestAfterStart_RegistryAloneIsNotAnInvitation pins it.
New catalog block app_info.guide (sections + faq + checked_version), parsed, localised field by
field and rendered with the household's own address filled in. It renders only when checked_version
names the image tag its author clicked through. No template carries one yet.
The Pi and "csak x86" badges are gone; PiCompatible and the catalog key stay this release, or a box
on 0.314.0 would read "csak x86" on every app (R-1067).
template_id_gate.py now follows {{template}} includes, with a scope decoy proving the walk did not
become vacuous.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117kRTSgs3rJWUFGxyoWN6Y
138 lines
8.2 KiB
HTML
138 lines
8.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"}}
|
|
<div class="page-header">
|
|
<div style="display:flex;align-items:center;gap:1rem">
|
|
<a href="/stacks" class="btn btn-sm btn-outline">{{T "app_info.alkalmazasok"}}</a>
|
|
<h2>{{.Meta.DisplayName}}</h2>
|
|
</div>
|
|
<div style="display:flex;align-items:center;gap:.5rem">
|
|
{{if .Deployed}}
|
|
<span class="stack-state-badge state-{{stateColor .Stack.State}}">{{stateLabel .Stack.State}}</span>
|
|
{{if .Stack.Orphaned}}<span class="badge badge-orphaned">{{T "common.elavult"}}</span>{{end}}
|
|
{{template "meta_badge" (lifecycleBadge .Meta)}}
|
|
{{template "meta_badge" (updateBadge .Stack)}}{{if and (gt .Stack.LadderStepsLeft 0) (not .Stack.HoldReason)}}<small class="text-muted" data-ladder-steps="{{.Stack.LadderStepsLeft}}">{{T "app_info.ladder_steps_left"}} {{.Stack.LadderStepsLeft}}</small>{{end}}
|
|
{{if and .AppOperational .EffectiveSubdomain}}<a href="https://{{.EffectiveSubdomain}}.{{.Domain}}{{.Meta.OpenPath}}" target="_blank" class="btn btn-sm btn-primary" data-app-open="1">{{T "app_info.megnyitas"}}</a>{{end}}
|
|
{{if .Stack.Orphaned}}<button class="btn btn-sm btn-danger" onclick="deleteOrphanStack('{{.Stack.Name}}')">{{T "common.torles"}}</button>{{end}}
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
|
|
<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">
|
|
{{if .AppInfo.Tagline}}<p class="app-info-tagline">{{.AppInfo.Tagline}}</p>{{else}}<p class="app-info-tagline">{{.Meta.Description}}</p>{{end}}
|
|
<div class="stack-meta-badges">
|
|
<span class="meta-badge">~{{.Meta.Resources.MemRequest}} RAM</span>
|
|
<span class="meta-badge">{{.Meta.Category}}</span>
|
|
{{if .Meta.Resources.NeedsHDD}}<span class="meta-badge meta-badge-warn">{{T "common.hdd_szukseges"}}</span>{{end}}
|
|
{{if .Meta.Resources.HungarianUI}}<span class="meta-badge meta-badge-ok">{{T "common.magyar_felulet"}}</span>{{end}}
|
|
</div>
|
|
</div>
|
|
</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>
|
|
{{else if .AutoUpdateLine}}
|
|
<div class="alert alert-info" style="margin-top:1rem" data-auto-update="done">{{.AutoUpdateLine}}</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}}
|