Files
felhom-controller/controller/internal/web/templates/app_shell.html
T
admin fe0228c877
gates / gates (push) Failing after 46s
v0.315.0: style the app page's tab bar, and reuse the hero classes that already have rules
FOUND IN A BROWSER, and findable no other way. Every endpoint-level marker passed and the page was
unusable: the tab bar rendered as four bare underlined links run together, and the app logo filled a
third of the screen. The shell had been written with new class names — app-hero, app-hero-logo,
app-tabs — and only the first two of those had any CSS at all, under the names the design system
already uses.

  - the shell now reuses .app-info-hero / .app-info-logo / .app-info-hero-text, which carry the
    80px logo box, the hairline border and the 2px radius. Inventing a parallel name is what left
    the logo unsized; REUSE.md's first rule, learned again.
  - .app-tabs / .app-tab / .app-tab-active are genuinely new and are now styled to design system
    v2: 2px radius, hairline --line border, no shadow, --text-2 for an inactive tab and
    --blue-bright only for the one the household is on (blue marks where you are, not a deviation),
    the active tab sitting on the bar's own line, and a narrower gutter under 640px.
  - .after-start-card gets a blue left hairline — it is a request, not a fault, so no amber.
  - .app-faq gets its dt/dd rules.

TestAppPage_DeployedDefaultsToSettings sliced the header at `class="app-hero"`; it now slices at the
reused class and fails loudly if either marker is gone, instead of slicing on -1. The 36 app-page
parity fixtures are recaptured — and no other fixture moved, which is the evidence that the shell
change reaches only the app page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117kRTSgs3rJWUFGxyoWN6Y
2026-10-15 19:04:57 +02:00

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-info-hero">
<img class="app-info-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-info-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}}