v0.319.0: the app page in panels, the login in one place, a password the box never had is said in words
gates / gates (push) Successful in 52s

- style A (operator 2026-10-16): hero, tab bar and every section are panels on --bg-1, one width, 16px apart;
  the install page's form groups use the same rule; the settings tab's 700px inner wrapper is gone
- „Belépés az alkalmazásba": name + password side by side, fetched on demand (R-254, R-709); pairing in app_login.go
- the empty-value rule: no stored value -> a sentence, never an eye; page and reveal endpoint share storedValuePresent
- „Cím és kulcsok" is rows (no Domain input, no disabled storage select, no install form); hint under the title
- tests red-proved: app_page_tuning_test.go (4); 36 app-page parity fixtures re-captured, 4 new; install fixtures unchanged

Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
This commit is contained in:
2026-10-16 12:12:06 +02:00
parent 8d44416610
commit d170a2aecf
58 changed files with 6046 additions and 1776 deletions
@@ -17,7 +17,7 @@
„Adatok es mentes" holds everything about the FILES, including the backup card that used to
sit on this page. */}}
<div class="deploy-container">
<div class="app-settings-tab">
{{if .FlashSuccess}}<div class="flash flash-success">{{.FlashSuccess}}</div>{{end}}
{{if .FlashError}}<div class="flash flash-error">{{.FlashError}}</div>{{end}}
@@ -62,13 +62,13 @@
{{/* 3. THE REST OF WHAT THE APP WANTS FROM THE HOUSEHOLD. */}}
{{- if .InstallHoldClosed}}
<div class="app-section" style="margin-top:1rem" id="install-hold-card">
<div class="app-section" id="install-hold-card">
<h3>{{T "app_info.install_hold_title"}}</h3>
<p>{{T "app_info.install_hold_closed"}}</p>
</div>
{{- end}}
{{- if .SetupGateClosed}}
<div class="app-section" style="margin-top:1rem" id="setup-gate-card">
<div class="app-section" id="setup-gate-card">
<h3>{{T "app_info.setup_gate_title"}}</h3>
<p>{{T "app_info.setup_gate_closed"}}</p>{{- if .SignupNative}}
<p class="form-hint" data-restarts-once="true">{{T "app_info.restarts_once"}}</p>{{- end}}
@@ -84,7 +84,7 @@
{{- end}}
{{- if .CloseSignupOffered}}
<div class="app-section" style="margin-top:1rem" id="close-signup-card">
<div class="app-section" id="close-signup-card">
<h3>{{T "app_info.signup_title"}}</h3>
<p>{{T "app_info.close_signup_text"}}</p>{{- if .SignupNative}}
<p class="form-hint" data-restarts-once="true">{{T "app_info.restarts_once"}}</p>{{- end}}
@@ -93,7 +93,7 @@
</div>
{{- end}}
{{- if or .SignupClosed .SignupOpenUntil}}
<div class="app-section" style="margin-top:1rem" id="signup-card">
<div class="app-section" id="signup-card">
<h3>{{T "app_info.signup_title"}}</h3>
{{- if .SignupOpenUntil}}
<p>{{T "app_info.signup_open_until"}} {{.SignupOpenUntil}}</p>
@@ -121,6 +121,35 @@
{{/* The logins, each already its own guarded card on the details page. */}}
<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">
<h3>{{T "app_page.login_title"}}</h3>
<p>{{T "app_page.login_desc"}}</p>
<div class="app-login-pair">
{{- range .LoginFields}}
<div class="app-login-field" data-login-field="{{.EnvVar}}">
<label class="app-login-label" for="field-{{.EnvVar}}">{{.Label}}</label>
{{- if .Restored}}
<p class="form-hint" data-login-restored="{{.EnvVar}}">{{T "deploy.login_from_backup"}}</p>
{{- else if not .Stored}}
<p class="form-hint" data-login-not-stored="{{.EnvVar}}">{{if .IsName}}{{T "app_page.login_no_name"}}{{else}}{{T "app_page.login_no_password"}}{{end}}</p>
{{- else if .OnDemand}}
<div class="input-with-button">
<input type="password" id="field-{{.EnvVar}}" class="form-control" value="" placeholder="••••••••••••" readonly>
<button type="button" class="btn btn-sm btn-outline" data-login-reveal="{{.EnvVar}}" onclick="revealAutoField('{{$.Stack.Name}}','{{.EnvVar}}', this, 'field-{{.EnvVar}}')">{{T "deploy.megjelenites"}}</button>
</div>
{{- else}}
<input type="text" id="field-{{.EnvVar}}" class="form-control" value="{{.Value}}" readonly>
{{- end}}
</div>
{{- end}}
</div>
</div>
{{end}}
{{if .InitialCreds}}
<div class="app-section">
<h3>{{T "app_info.kezdeti_belepesi_adatok"}}</h3>
@@ -173,8 +202,8 @@
household reached by pressing „Beallitasok" on an installed app. The hint lives with the
fields it is about, at the bottom of this tab. */}}
{{if .StorageInfo}}
<div class="deploy-storage-info">
<h4>{{T "deploy.adattarolas"}}</h4>
<div class="app-section">
<h3>{{T "deploy.adattarolas"}}</h3>
<div class="settings-grid">
<div class="settings-row">
<span class="settings-label">{{T "deploy.tarhely"}}</span>
@@ -201,8 +230,8 @@
</div>
{{end}}
{{if .StaleData}}
<div class="deploy-stale-data">
<h4>{{T "deploy.korabbi_adatok"}}</h4>
<div class="app-section deploy-stale-data">
<h3>{{T "deploy.korabbi_adatok"}}</h3>
<p class="form-hint" style="margin-bottom:1rem">
{{T "deploy.az_alkalmazas_adatainak_masolata_megtala"}}
</p>
@@ -230,7 +259,7 @@
</div>
{{end}}
{{if .HasIntegrations}}
<div class="app-optional-config">
<div class="app-section">
<h3>{{T "deploy.integraciok"}}</h3>
<p class="config-group-desc">
{{T "deploy.mas_telepitett_alkalmazasokkal_valo_ossz"}}
@@ -290,7 +319,7 @@
{{end}}
{{if .GeoGlobalEnabled}}
<div class="app-optional-config">
<div class="app-section">
<h3>{{T "deploy.foldrajzi_korlatozas"}}</h3>
<p class="config-group-desc">
{{T "deploy.az_alkalmazas_egyeni_orszagkorlatozas_ne"}}
@@ -425,7 +454,7 @@
{{end}}
{{if .HasOptionalConfig}}
<div class="app-optional-config">
<div class="app-section">
<h3>{{T "deploy.opcionalis_beallitasok"}}</h3>
{{range .OptionalConfig}}
<div class="config-group">
@@ -519,7 +548,7 @@
{{end}}
{{if .AppEmailSupported}}
<div class="app-optional-config">
<div class="app-section">
<h3>{{T "deploy.email_kuldes"}}</h3>
<p class="config-group-desc">
{{T "deploy.ez_az_alkalmazas_tud_emailt"}}
@@ -542,18 +571,38 @@
</form>
</div>
{{end}}
<div class="app-section">
{{/* „CIM ES KULCSOK" (v0.319.0) — what was set at install, as rows. The hint that says they
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). */}}
<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>
{{if .EffectiveSubdomain}}
<div class="app-field-row">
<div class="app-field-label">{{T "app_page.address"}}</div>
<div class="app-field-value" data-app-address-row="1">{{.EffectiveSubdomain}}.{{.Domain}}</div>
</div>
{{end}}
<form id="deploy-form" class="deploy-form">
{{template "app_deploy_fields" .}}
</form>
<p class="app-field-hint" data-readonly-hint="1">{{T "app_page.installed_readonly"}}</p>
{{if .CurrentHDDPath}}
<div class="app-field-row">
<div class="app-field-label">{{T "deploy.tarhely"}}</div>
<div class="app-field-value" data-app-storage-row="1">{{if .CurrentHDDPathMissing}}{{T "deploy.nem_elerheto"}}{{else if .CurrentHDDLabel}}{{.CurrentHDDLabel}}{{else}}<span class="mono">{{.CurrentHDDPath}}</span>{{end}}</div>
</div>
{{end}}
{{- range .SettingsRows}}
<div class="app-field-row" data-setting-row="{{.EnvVar}}">
<div class="app-field-label">{{.Label}}</div>
{{- if .OnDemand}}
<div class="app-field-value input-with-button">
<input type="password" id="auto-field-{{.EnvVar}}" class="form-control" value="" placeholder="••••••••••••" readonly aria-label="{{.Label}}">
<button type="button" class="btn btn-sm btn-outline" onclick="revealAutoField('{{$.Stack.Name}}','{{.EnvVar}}', this)">{{T "deploy.megjelenites"}}</button>
</div>
{{- else}}
<div class="app-field-value">{{.Value}}</div>
{{- end}}
</div>
{{- end}}
</div>
</div>
@@ -612,46 +661,9 @@ function icCopyPw(btn) {
{{template "app_press_script" .}}
<script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
// Check on page load
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
// R-254 site two: on an already-deployed app the value is NOT in this page — it is fetched on
// demand, and the server records the act. Nothing caches it between presses.
// R-709 (v0.280.0): an installed app's `type: password` value, fetched on demand like a secret.
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || '{{T "deploy.a_lekeres_nem_sikerult"}}'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('{{T "deploy.a_lekeres_nem_sikerult"}}'); });
}
// R-254 site two: on an already-deployed app no secret is in this page — it is fetched on demand,
// and the server records the act. Nothing caches it between presses. Since v0.319.0 the login
// panel's name and password use this too (R-709: a `type: password` value, fetched like a secret).
function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return;