7f3e58ff0a
gates / gates (push) Failing after 49s
v0.315.0 worked and looked, in the operator's words, messy: boxes of different widths stacked down
the page, with the same card shape around things you act on and things you only read. The mockup
approved on 2026-10-15 replaces it, and each of its three rules has a reason:
ONE COLUMN OF ONE WIDTH (820px). Differing widths read as a page assembled by accident.
SECTIONS ARE HAIRLINES, NOT CARDS. A card says "this is a thing you act on"; most of an app page
is a thing you READ, so a card there is a promise the content cannot keep.
A BOX MEANS SOMETHING. Exactly two remain: blue when the app is waiting for the household,
amber when something is risky. The exception-colour principle applied to shape as well.
What moved:
- the hero loses its box: a 64px logo tile, the name at 26px, the tagline, then hairline chips
(state, lifecycle, update) and meta text (the address, the last automatic update);
- the tabs are links with a 2px blue underline under the open one — no pills, no fills;
- sections are `padding: 28px 0` with a bottom hairline; rows are a 200px label column so every
value on the page starts at the same x, and stack under 720px;
- the after-start callout is now the FIRST thing on Beallitasok. A household opening a Plex that
wants its claim code should meet the code box, not a Restart button;
- "2. mentes beallitasa — <app>" and "<app> — Exportalas" are gone: they were the headings of
pages that no longer exist, on a tab that already says which app this is;
- the settings tab gains "Cim es kulcsok" with one address row, and the disabled subdomain input
that said the same thing is skipped for an installed app (the install wizard still asks);
- the "this app is already installed, the settings are read-only" BANNER becomes the hint under
the fields it is about;
- the install wizard gets the same back link, hero and column, so pressing Telepites does not
land the household on a page that looks like a different product.
THE AUTO-UPDATE SUCCESS BANNER IS GONE. It was a full-width blue box above the tabs, level with
"the update failed" and "your files are missing". A success that asked nothing of the household is
a date, and it is now meta text in the hero. Every banner that DOES need a decision — a failed
update, a hold, lost files, a missing drive, an interrupted install — is untouched, above the tabs,
exactly where R-571 puts it.
Tests: TestAppPage_OneColumnLayout (one column, one aria-current, no card-shaped section, no
success banner), TestAppPage_AutoUpdateSuccessIsMetaNotBanner, TestAppPage_CalloutsOnlyForAskAndWarn
(one blue callout on a waiting app and it comes first; one amber on a same-disk backup; none on the
good case). Four red-proofs recorded in REPORT.
app_row_dedup_gate convicted `.app-row`, correctly: that class already means "a row in the app
list". The app page's label/value row is `.app-field-row`. Restoring the list's own rule after an
over-broad rename was part of the fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117kRTSgs3rJWUFGxyoWN6Y
145 lines
8.7 KiB
HTML
145 lines
8.7 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 .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 .AppOperational .EffectiveSubdomain}}
|
|
<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>
|
|
</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}}
|