From 7f3e58ff0ad32a0178306adcc76ccc8975291d23 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Thu, 15 Oct 2026 20:29:56 +0200 Subject: [PATCH] =?UTF-8?q?The=20app=20page=20as=20one=20column=20?= =?UTF-8?q?=E2=80=94=20the=20approved=20mockup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 — " and " — 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 Claude-Session: https://claude.ai/code/session_0117kRTSgs3rJWUFGxyoWN6Y --- controller/internal/i18n/locales/en.json | 3 + controller/internal/i18n/locales/hu.json | 3 + controller/internal/web/app_layout_test.go | 162 ++++++++++++++++++ controller/internal/web/app_page_test.go | 10 +- .../internal/web/templates/app_data.html | 28 +-- .../internal/web/templates/app_fields.html | 4 + .../internal/web/templates/app_guide.html | 20 ++- .../internal/web/templates/app_logs.html | 2 + .../internal/web/templates/app_settings.html | 85 +++++---- .../internal/web/templates/app_shell.html | 61 ++++--- controller/internal/web/templates/deploy.html | 37 ++-- controller/internal/web/templates/style.css | 146 ++++++++++++---- .../i18n_parity/app_data_deployed.html | 62 ++++--- .../testdata/i18n_parity/app_data_export.html | 53 +++--- .../i18n_parity/app_data_not_hdd.html | 54 +++--- .../i18n_parity/app_data_not_hdd_absent.html | 54 +++--- .../app_data_not_hdd_protected.html | 54 +++--- .../app_data_not_hdd_same_disk.html | 54 +++--- .../app_data_notarget_disabled.html | 52 +++--- .../i18n_parity/app_data_target_auto.html | 52 +++--- .../i18n_parity/app_data_target_ssd.html | 54 +++--- .../i18n_parity/app_guide_available.html | 39 ++--- .../testdata/i18n_parity/app_guide_full.html | 65 ++++--- .../i18n_parity/app_guide_installable.html | 39 ++--- .../i18n_parity/app_guide_no_guide.html | 43 +++-- .../web/testdata/i18n_parity/app_logs.html | 47 +++-- .../i18n_parity/app_settings_after_start.html | 127 +++++++------- .../app_settings_close_signup.html | 91 +++++----- .../app_settings_close_signup_native.html | 91 +++++----- .../i18n_parity/app_settings_deployed.html | 106 ++++++------ .../i18n_parity/app_settings_hold_log.html | 89 +++++----- .../app_settings_install_hold.html | 91 +++++----- .../app_settings_known_login_changed.html | 91 +++++----- .../app_settings_ladder_steps.html | 89 +++++----- .../i18n_parity/app_settings_lost_files.html | 89 +++++----- .../i18n_parity/app_settings_not_checked.html | 89 +++++----- .../app_settings_operator_creds.html | 95 +++++----- .../app_settings_restored_login.html | 114 ++++++------ .../i18n_parity/app_settings_running.html | 114 ++++++------ .../app_settings_setup_gate_button.html | 91 +++++----- .../app_settings_setup_gate_native.html | 91 +++++----- .../app_settings_setup_gate_probe.html | 91 +++++----- .../app_settings_signup_closed.html | 91 +++++----- .../app_settings_signup_native.html | 91 +++++----- ...pp_settings_signup_native_open_failed.html | 91 +++++----- .../app_settings_signup_window.html | 91 +++++----- .../i18n_parity/app_settings_stopped.html | 110 ++++++------ .../i18n_parity/app_settings_updating.html | 89 +++++----- .../web/testdata/i18n_parity/deploy_new.html | 50 +++--- .../i18n_parity/deploy_new_blocked.html | 50 +++--- .../deploy_new_memory_blocked.html | 50 +++--- 51 files changed, 1856 insertions(+), 1689 deletions(-) create mode 100644 controller/internal/web/app_layout_test.go diff --git a/controller/internal/i18n/locales/en.json b/controller/internal/i18n/locales/en.json index fa2b34b..33ad86d 100644 --- a/controller/internal/i18n/locales/en.json +++ b/controller/internal/i18n/locales/en.json @@ -114,6 +114,9 @@ "app_import.tarolo": "Storage", "app_import.titkos": "Encrypted", "app_import.visszaallitas_inditasa": "Start restore", + "app_page.address_and_keys": "Address and keys", + "app_page.address": "Address", + "app_page.installed_readonly": "These were created at install time and cannot be changed.", "app_page.after_start_btn": "Save", "app_page.after_start_title": "One more step", "app_page.faq": "Frequently asked questions", diff --git a/controller/internal/i18n/locales/hu.json b/controller/internal/i18n/locales/hu.json index 347edd9..2f5d4c0 100644 --- a/controller/internal/i18n/locales/hu.json +++ b/controller/internal/i18n/locales/hu.json @@ -110,6 +110,9 @@ "app_import.tarolo": "Tároló", "app_import.titkos": "Titkos", "app_import.visszaallitas_inditasa": "Visszaállítás indítása", + "app_page.address_and_keys": "Cím és kulcsok", + "app_page.address": "Cím", + "app_page.installed_readonly": "Ezek telepítéskor jöttek létre, és nem módosíthatók.", "app_page.after_start_btn": "Mentés", "app_page.after_start_title": "Még egy lépés", "app_page.faq": "Gyakori kérdések", diff --git a/controller/internal/web/app_layout_test.go b/controller/internal/web/app_layout_test.go new file mode 100644 index 0000000..479823f --- /dev/null +++ b/controller/internal/web/app_layout_test.go @@ -0,0 +1,162 @@ +package web + +import ( + "strings" + "testing" + + "gitea.dooplex.hu/admin/felhom-controller/internal/stacks" +) + +// THE ONE-COLUMN LAYOUT (v0.316.0, the operator-approved mockup of 2026-10-15). +// +// These assert STRUCTURE, not strings: the rules the mockup is made of are "one column", "sections +// are hairlines, not cards" and "a box means something". Each is checkable in the rendered HTML, +// and each had a counter-example in v0.315.0 — which looked, in the operator's words, messy. + +// TestAppPage_OneColumnLayout — per tab: one column, exactly one current tab, the auto-update +// SUCCESS banner gone and its date in the meta line instead, and no section drawn as a card. +// +// RED-PROOF (REPORT.md): put the success banner back in app_shell.html -> the banner assertion +// fails; drop .app-page from a tab -> the column assertion fails. Both reverted. +func TestAppPage_OneColumnLayout(t *testing.T) { + s := appPageHarness(t) + for _, tab := range []string{"", "/settings", "/data", "/guide", "/logs"} { + body := appGet(t, s, "/apps/adventurelog"+tab).Body.String() + where := "/apps/adventurelog" + tab + + // ONE COLUMN: the content sits in exactly one .app-page wrapper. + if n := strings.Count(body, `class="app-page`); n != 1 { + t.Errorf("%s: %d .app-page wrappers, want exactly 1 — the column is what makes the "+ + "widths agree", where, n) + } + + // EXACTLY ONE current tab. Two would be a lie about where the household is; none would + // leave the bar looking like a row of links, which is what v0.315.0 shipped. + if n := strings.Count(body, `aria-current="page"`); n != 1 { + t.Errorf("%s: %d aria-current=\"page\", want exactly 1", where, n) + } + + // THE SUCCESS BANNER IS NEVER A BANNER. (Whether this app HAS one is the sub-test below; + // here the rule is that no tab ever renders it as an alert.) + if strings.Contains(body, `class="alert alert-info" style="margin-top:1rem" data-auto-update="done"`) { + t.Errorf("%s: the auto-update SUCCESS banner is back", where) + } + + // NO SECTION IS A CARD. .app-info-card carries a background, a border and a radius; inside + // the app page nothing may use it — a section is a hairline. + if strings.Contains(body, "app-info-card") { + t.Errorf("%s: a section is still drawn as a card (app-info-card)", where) + } + + // The tab bar and the hero are on every tab, and the hero is not a box. + for _, marker := range []string{`class="app-back"`, `class="app-hero"`, `class="app-tabs"`} { + if !strings.Contains(body, marker) { + t.Errorf("%s: missing %s", where, marker) + } + } + if strings.Contains(body, `class="app-info-hero"`) { + t.Errorf("%s: the hero is still the boxed one", where) + } + } +} + +// TestAppPage_AutoUpdateSuccessIsMetaNotBanner — the other half of the rule, on a fixture that +// actually has an automatic update to report. v0.315.0 gave it a full-width blue banner above the +// tabs, level with "the update failed" and "your files are missing"; a success that asked nothing +// of the household is a date. +// +// RED-PROOF (REPORT.md): restore the `{{else if .AutoUpdateLine}}` banner branch in +// app_shell.html -> this fails on the banner assertion. Reverted. +func TestAppPage_AutoUpdateSuccessIsMetaNotBanner(t *testing.T) { + st := i18nStack("privatebin", "PrivateBin", stacks.StateRunning, true) + body := renderBackupPage(t, "app_settings", map[string]interface{}{ + "Page": "stacks", "Title": "PrivateBin", "Domain": "example.hu", "CSRFToken": "tok", + "Stack": st, "Meta": st.Meta, "AppInfo": st.Meta.AppInfo, + "Deployed": true, "AppOperational": true, + "AutoUpdateLine": "A frissites 2026. 09. 28-kor sikerult.", + // An installed app has a tab bar, and the point of this test is WHERE the line sits + // relative to it. + "ActiveTab": appTabSettings, + "AppTabs": []AppTabLink{ + {Key: appTabSettings, Href: "/apps/privatebin/settings", Label: "Beallitasok", Active: true}, + {Key: appTabData, Href: "/apps/privatebin/data", Label: "Adatok"}, + }, + }) + if !strings.Contains(body, `class="app-meta" data-auto-update="done"`) { + t.Error("the auto-update success line is not the meta line") + } + if strings.Contains(body, `class="alert alert-info" style="margin-top:1rem" data-auto-update="done"`) { + t.Error("the auto-update success is still a banner") + } + // It sits in the hero, above the tabs — not in the banner strip under it. + meta, tabs := strings.Index(body, `data-auto-update="done"`), strings.Index(body, `class="app-tabs"`) + if meta < 0 || tabs < 0 || meta > tabs { + t.Errorf("the meta line must be in the hero, above the tab bar (meta=%d tabs=%d)", meta, tabs) + } +} + +// TestAppPage_CalloutsOnlyForAskAndWarn (Scenario E) — the two boxes, and nothing else. +// +// RED-PROOF (REPORT.md): draw an informational section as a callout -> the count assertions fail. +func TestAppPage_CalloutsOnlyForAskAndWarn(t *testing.T) { + // An app WAITING for the household: exactly one blue callout, and it is the after-start card. + t.Run("waiting for a value -> one blue callout", func(t *testing.T) { + s := afterStartHarness(t, "plex", "PLEX_CLAIM", true) + body := appGet(t, s, "/apps/plex").Body.String() + if n := strings.Count(body, "app-callout-ask"); n != 1 { + t.Errorf("%d blue callouts, want exactly 1 (the after-start card)", n) + } + if n := strings.Count(body, "app-callout-warn"); n != 0 { + t.Errorf("%d amber callouts on an app with nothing wrong, want 0 — amber is for a "+ + "deviation, and being asked for a code is not one", n) + } + // It is the FIRST section: the app is waiting, so nothing else goes above it. + ask := strings.Index(body, "app-callout-ask") + run := strings.Index(body, `data-run-card="1"`) + if ask < 0 || run < 0 || ask > run { + t.Errorf("the after-start callout must come before the Mukodes card (ask=%d run=%d)", ask, run) + } + }) + + // An app with nothing to ask: no blue callout anywhere. + t.Run("nothing to ask -> no blue callout", func(t *testing.T) { + s := appPageHarness(t) + for _, tab := range []string{"", "/settings", "/data", "/guide", "/logs"} { + body := appGet(t, s, "/apps/adventurelog"+tab).Body.String() + if n := strings.Count(body, "app-callout-ask"); n != 0 { + t.Errorf("/apps/adventurelog%s: %d blue callouts, want 0", tab, n) + } + } + }) + + // The amber callout is reserved for the risky fact — the app's data sharing a disk with its + // backup. Rendered straight, because the harness app is not an HDD app. + t.Run("backup on the same disk -> one amber callout", func(t *testing.T) { + st := i18nStack("privatebin", "PrivateBin", stacks.StateRunning, true) + d := map[string]interface{}{ + "Page": "stacks", "Title": "PrivateBin", "Domain": "example.hu", "CSRFToken": "tok", + "Stack": st, "Meta": st.Meta, "AppInfo": st.Meta.AppInfo, + "Deployed": true, "AppOperational": true, + "StackName": "privatebin", "DisplayName": "PrivateBin", + "Tier2": map[string]interface{}{"IsHDDApp": false}, + "SystemBackup": "same_disk", + } + body := renderBackupPage(t, "app_data", d) + if n := strings.Count(body, "app-callout-warn"); n != 1 { + t.Errorf("%d amber callouts, want exactly 1 (the same-disk warning)", n) + } + if n := strings.Count(body, "app-callout-ask"); n != 0 { + t.Errorf("%d blue callouts on the data tab, want 0", n) + } + if !strings.Contains(body, `data-system-backup="same_disk"`) { + t.Error("the same-disk marker is gone") + } + // The honest branch when the whole-system backup IS on its own drive is NOT a warning. + d["SystemBackup"] = "protected" + ok := renderBackupPage(t, "app_data", d) + if n := strings.Count(ok, "app-callout-warn"); n != 0 { + t.Errorf("a protected whole-system backup drew %d amber callouts, want 0 — amber is a "+ + "deviation, and this is the good case", n) + } + }) +} diff --git a/controller/internal/web/app_page_test.go b/controller/internal/web/app_page_test.go index 991164d..545a4e8 100644 --- a/controller/internal/web/app_page_test.go +++ b/controller/internal/web/app_page_test.go @@ -149,15 +149,15 @@ func TestAppPage_DeployedDefaultsToSettings(t *testing.T) { // The header does not. This is the whole point of the Mukodes card: the three buttons used to // sit in the page header and read as menu items. - // The header is everything before the hero block; app-info-hero is the design system's existing - // hero class, which the shell reuses rather than inventing one with no rules behind it. - hi, hj := strings.Index(body, `class="page-header"`), strings.Index(body, `class="app-info-hero"`) + // v0.316.0: the header IS the hero — a back link, the name, the chips and (at most) „Megnyitas". + // Everything above the tab bar is therefore what must carry no state-change button. + hi, hj := strings.Index(body, `class="app-back"`), strings.Index(body, `class="app-tabs"`) if hi < 0 || hj < 0 || hj < hi { - t.Fatalf("the shell's header/hero markers are gone (page-header=%d app-info-hero=%d)", hi, hj) + t.Fatalf("the shell's hero/tab markers are gone (app-back=%d app-tabs=%d)", hi, hj) } header := body[hi:hj] if strings.Contains(header, "stackAction(") { - t.Errorf("the page header must carry no state-change button, got: %q", header) + t.Errorf("the hero must carry no state-change button — they belong in the Mukodes card, got: %q", header) } // Part 3: Raspberry Pi is no longer supported, so neither badge may appear. diff --git a/controller/internal/web/templates/app_data.html b/controller/internal/web/templates/app_data.html index 6d461c2..ae416dd 100644 --- a/controller/internal/web/templates/app_data.html +++ b/controller/internal/web/templates/app_data.html @@ -1,5 +1,6 @@ {{define "app_data"}} {{template "layout_start" .}} +
{{template "app_shell_start" .}} {{/* „ADATOK ES MENTES" — everything about this app's FILES, in the order the household asks it: @@ -17,7 +18,7 @@ {{if .FlashError}}
{{.FlashError}}
{{end}} {{if .DataPathCards}} -
+

{{T "app_info.hova_tegyem_a_fajlokat"}}

{{T "app_info.ezeket_a_mappakat_a_fajlkezeloben"}}

@@ -35,7 +36,7 @@ {{end}} {{if and .Stack.Deployed .MigrateTargets}} -
+

{{T "app_info.athelyezes_masik_tarhelyre"}}

{{T "app_info.ennek_az_alkalmazasnak_az_adatait"}}

@@ -97,8 +98,10 @@ function appMigrate(btn,app,label){
{{if .Tier2}} -

{{T "tier2_config.2_mentes_beallitasa"}}

-
+{{/* v0.316.0: a SECTION, not a page title over a card. „2. mentés beállítása — " was the + heading of a page that no longer exists; the tab already says which app this is. */}} +
+

{{T "tier2_config.2_mentes_beallitasa"}}

{{with .Tier2}}

{{T "tier2_config.a_2_mentes_egy_masik"}} @@ -106,19 +109,19 @@ function appMigrate(btn,app,label){ {{if not .IsHDDApp}} {{if eq $.SystemBackup "protected"}} -

+
{{T "tier2_config.rendszerlemez.vedett"}}
{{else if eq $.SystemBackup "same_disk"}} -
+
{{T "tier2_config.rendszerlemez.ugyanaz"}}
{{else if eq $.SystemBackup "absent"}} -
+
{{T "tier2_config.rendszerlemez.hianyzik"}}
{{else}} -
+
{{T "tier2_config.rendszerlemez.ismeretlen"}}
{{end}} @@ -151,7 +154,7 @@ function appMigrate(btn,app,label){
{{if .EffectiveIsSSD}} -
+
{{T "tier2_config.jelenleg_csak_a_belso_ssd"}}
{{end}} @@ -186,9 +189,9 @@ function appMigrate(btn,app,label){ {{end}} {{if .ExportOffered}} -

{{T "app_export.exportalas"}}

-
-

{{T "app_export.mentes_helye"}}

+
+

{{T "app_export.exportalas"}}

+

{{T "app_export.mentes_helye"}}

+ +
+ +
+ {{- end}} + + {{/* 2. MUKODES. Stop asks first — it takes the app away from everyone in the house, and the protected stacks refuse it server-side regardless (traefik, cloudflared, the controller itself and always samba). */}} -
+

{{T "app_page.mukodes"}}

{{stateLabel .Stack.State}}

@@ -36,15 +60,15 @@
-{{/* 2. WHAT THE APP STILL WANTS FROM THE HOUSEHOLD. */}} +{{/* 3. THE REST OF WHAT THE APP WANTS FROM THE HOUSEHOLD. */}} {{- if .InstallHoldClosed}} -
+

{{T "app_info.install_hold_title"}}

{{T "app_info.install_hold_closed"}}

{{- end}} {{- if .SetupGateClosed}} -
+

{{T "app_info.setup_gate_title"}}

{{T "app_info.setup_gate_closed"}}

{{- if .SignupNative}}

{{T "app_info.restarts_once"}}

{{- end}} @@ -59,26 +83,8 @@
{{- end}} - {{/* The generic after-start card (Part 4, D2 = A). The catalog says a value is asked after the - app starts and supplies its label; the ACTION is Go code in after_start.go, keyed by slug and - env var. A field with no registered action renders nothing and POSTs 404 — the catalog - cannot make the box run anything. */}} - {{- range .AfterStartFields}} -
-

{{T "app_page.after_start_title"}}

- {{if .Description}}

{{.Description}}

{{end}} -
- - -
- -
- {{- end}} {{- if .CloseSignupOffered}} -
+

{{T "app_info.signup_title"}}

{{T "app_info.close_signup_text"}}

{{- if .SignupNative}}

{{T "app_info.restarts_once"}}

{{- end}} @@ -87,7 +93,7 @@
{{- end}} {{- if or .SignupClosed .SignupOpenUntil}} -
+

{{T "app_info.signup_title"}}

{{- if .SignupOpenUntil}}

{{T "app_info.signup_open_until"}} {{.SignupOpenUntil}}

@@ -114,9 +120,9 @@ {{- end}} {{/* The logins, each already its own guarded card on the details page. */}} -
+
{{if .InitialCreds}} -
+

{{T "app_info.kezdeti_belepesi_adatok"}}

{{if .InitialCreds.Username}} @@ -151,7 +157,7 @@ {{end}} {{if and .AppInfo.DefaultCreds (not .DefaultLoginReplaced)}} -
+

{{T "app_info.alapertelmezett_belepes"}}

{{.AppInfo.DefaultCreds}}

{{if .KnownLoginLine}}

{{.KnownLoginLine}}

{{else}}

{{T "app_info.az_elso_bejelentkezes_utan_azonnal_2"}}

{{end}} @@ -162,10 +168,10 @@
{{end}}
-{{/* 3. THE SETTINGS. Everything below came from the install page's deployed mode, unchanged. */}} -
- {{T "deploy.ez_az_alkalmazas_mar_telepitve"}} -
+{{/* 3. THE SETTINGS. v0.316.0: the „this app is already installed, the settings below are + read-only" BANNER is gone — it was a box saying what a one-line hint says, on a tab the + 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}}

{{T "deploy.adattarolas"}}

@@ -536,9 +542,19 @@
{{end}} - +
+

{{T "app_page.address_and_keys"}}

+ {{if .EffectiveSubdomain}} +
+
{{T "app_page.address"}}
+
{{.EffectiveSubdomain}}.{{.Domain}}
+
+ {{end}} + {{template "app_deploy_fields" .}} - + +

{{T "app_page.installed_readonly"}}

+
{{if .InitialCreds}} @@ -711,5 +727,6 @@ function deleteStaleDataRun(stackName, stalePath, btn) { +{{/* .app-page */}} {{template "layout_end" .}} {{end}} diff --git a/controller/internal/web/templates/app_shell.html b/controller/internal/web/templates/app_shell.html index 9988911..e14bf36 100644 --- a/controller/internal/web/templates/app_shell.html +++ b/controller/internal/web/templates/app_shell.html @@ -11,36 +11,45 @@ are now the first card of the „Beallitasok" tab, where a state change belongs. */}} {{define "app_shell_start"}} - +{{T "app_info.alkalmazasok"}} -
- + -
- {{if .AppInfo.Tagline}}

{{.AppInfo.Tagline}}

{{else}}

{{.Meta.Description}}

{{end}} -
- ~{{.Meta.Resources.MemRequest}} RAM - {{.Meta.Category}} - {{if .Meta.Resources.NeedsHDD}}{{T "common.hdd_szukseges"}}{{end}} - {{if .Meta.Resources.HungarianUI}}{{T "common.magyar_felulet"}}{{end}} +
+

{{.Meta.DisplayName}}

+ {{if .AppInfo.Tagline}}

{{.AppInfo.Tagline}}

{{else if .Meta.Description}}

{{.Meta.Description}}

{{end}} +
+ {{if .Deployed}} + ● {{stateLabel .Stack.State}} + {{if .Stack.Orphaned}}{{T "common.elavult"}}{{end}} + {{template "meta_badge" (lifecycleBadge .Meta)}} + {{template "meta_badge" (updateBadge .Stack)}}{{if and (gt .Stack.LadderStepsLeft 0) (not .Stack.HoldReason)}}{{T "app_info.ladder_steps_left"}} {{.Stack.LadderStepsLeft}}{{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}}{{.EffectiveSubdomain}}.{{.Domain}}{{end}} + {{if .AutoUpdateLine}}{{.AutoUpdateLine}}{{end}} + {{else}} + ~{{.Meta.Resources.MemRequest}} RAM + {{if .Meta.Category}}{{.Meta.Category}}{{end}} + {{if .Meta.Resources.NeedsHDD}}{{T "common.hdd_szukseges"}}{{end}} + {{if .Meta.Resources.HungarianUI}}{{T "common.magyar_felulet"}}{{end}} + {{end}}
+ {{if and .Deployed .AppOperational .EffectiveSubdomain}} + + {{end}} + {{if and .Deployed .Stack.Orphaned}} +
+ +
+ {{end}}
{{template "app_banners" .}} @@ -77,8 +86,6 @@
{{updateErrorText .Stack}}
{{else if .UpdateUndoneLine}}
{{.UpdateUndoneLine}}
-{{else if .AutoUpdateLine}} -
{{.AutoUpdateLine}}
{{end}}{{if and .Stack.HealthProbe .Stack.HealthProbe.NotChecked}}{{/* R-772: the declared health check could not run — said, never shown as healthy. */}}

{{T "health.no_probe_container"}}

{{end}} {{if .MissingStorageLabel}} diff --git a/controller/internal/web/templates/deploy.html b/controller/internal/web/templates/deploy.html index 754918e..891bafe 100644 --- a/controller/internal/web/templates/deploy.html +++ b/controller/internal/web/templates/deploy.html @@ -9,35 +9,26 @@ Vissza and Megsem go to the APP, not to the list: the household got here from the app's page and that is where cancelling belongs. */}} - +{{T "deploy.vissza"}} - -
+
{{if .FlashSuccess}}
{{.FlashSuccess}}
{{end}} {{if .FlashError}}
{{.FlashError}}
{{end}} {{- if .KnownLoginLine}}
{{.KnownLoginLine}}
{{end}} -
- + -
-

{{.Meta.DisplayName}}

- {{if .Meta.Description}}

{{.Meta.Description}}

{{end}} -
- {{if .Meta.Resources.MemRequest}}~{{.Meta.Resources.MemRequest}}{{end}} - {{if .Meta.Resources.NeedsHDD}}{{T "common.hdd_szukseges"}}{{end}} - {{if .Meta.Resources.HungarianUI}}{{T "common.magyar_felulet"}}{{end}} +
+

{{.Meta.DisplayName}} — {{T "deploy.telepites"}}

+ {{if .Meta.Description}}

{{.Meta.Description}}

{{end}} +
+ {{if .Meta.Resources.MemRequest}}~{{.Meta.Resources.MemRequest}} RAM{{end}} + {{if .Meta.Resources.NeedsHDD}}{{T "common.hdd_szukseges"}}{{end}} + {{if .Meta.Resources.HungarianUI}}{{T "common.magyar_felulet"}}{{end}} + {{T "deploy.reszletes_leiras_kepernyokepek"}}
- - {{T "deploy.reszletes_leiras_kepernyokepek"}} -
diff --git a/controller/internal/web/templates/style.css b/controller/internal/web/templates/style.css index 8d53b34..f889940 100644 --- a/controller/internal/web/templates/style.css +++ b/controller/internal/web/templates/style.css @@ -389,7 +389,7 @@ h3 { border: 1px solid var(--line); border-radius: var(--radius); } -.stack-list .app-row { +.stack-list .app-field-row { background: transparent; border: none; border-top: 1px solid var(--line-soft); @@ -1300,50 +1300,138 @@ a.stat-card:hover { .login-footer a:hover { color: var(--blue); } /* --- App Info Page --- */ -/* THE APP PAGE'S TAB BAR (v0.315.0) — links, not panels. - Design system v2: 2px radius, hairline borders, no shadow, and blue reserved for the tab the - household is ON. An inactive tab is --text-2: a tab bar is navigation, not a deviation. */ +/* ── THE APP PAGE (v0.316.0) — one column, hairlines, boxes only where they mean something ─────── + Operator-approved mockup, 2026-10-15. The rules, and why each one: + ONE COLUMN of ONE WIDTH. Boxes of different widths read as a page assembled by accident. + SECTIONS ARE SEPARATED BY HAIRLINES, not drawn as 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 it cannot keep. + A BOX MEANS SOMETHING. Exactly two exist: blue when the app is waiting for the household, amber + when something is risky. The exception-colour principle, applied to shape as well as colour. + Tokens only — no raw hexes, 2px radius, no shadows. */ + +.app-page { + max-width: 820px; + padding: 32px 48px 64px; +} +/* The log is the one tab whose content is genuinely wide: log lines are long, and wrapping them at + 820px makes them unreadable. */ +.app-page-wide { max-width: none; } + +.app-back { + display: inline-block; + font-size: 13px; + color: var(--text-2); + text-decoration: none; + margin-bottom: 14px; +} +.app-back:hover { color: var(--text-1); } + +/* HERO — no box, no border. The app's identity is a heading, not a card. */ +.app-hero { + display: flex; + align-items: flex-start; + gap: 20px; + flex-wrap: wrap; +} +.app-hero-logo { + width: 64px; height: 64px; + min-width: 64px; + border: 1px solid var(--line); + background: var(--bg-1); + border-radius: var(--radius); + object-fit: contain; + padding: 8px; +} +.app-hero-text { flex: 1 1 320px; min-width: 0; } +.app-hero-name { font-size: 26px; font-weight: 700; color: var(--text-1); margin: 0; line-height: 1.2; } +.app-hero-tagline { font-size: 15px; color: var(--text-2); margin: 4px 0 10px; } +.app-hero-actions { margin-left: auto; } + +.app-chips { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } +.app-chip { + font-size: 12px; font-weight: 600; + border: 1px solid var(--line); + border-radius: var(--radius); + padding: 3px 8px; + color: var(--text-1); + white-space: nowrap; +} +/* The meta line carries what used to be a full-width green banner saying the automatic update + worked. A success that needed no decision is not news; it is a date. */ +.app-meta { font-size: 12px; color: var(--text-3); } + +/* TABS — links with a line under the open one. No pills, no fills. */ .app-tabs { display: flex; - gap: .25rem; + gap: 28px; flex-wrap: wrap; - margin: 1.25rem 0 1.5rem; + margin: 28px 0 8px; border-bottom: 1px solid var(--line); } .app-tab { - padding: .55rem 1rem; + font-size: 15px; font-weight: 600; + padding: 12px 0; color: var(--text-2); text-decoration: none; - font-size: .9rem; - border: 1px solid transparent; - border-bottom: none; - border-radius: var(--radius) var(--radius) 0 0; - /* The bar's own 1px line is what the active tab sits on, so each tab reaches it. */ + border-bottom: 2px solid transparent; margin-bottom: -1px; white-space: nowrap; } -.app-tab:hover { color: var(--text-1); background: var(--bg-1); } -.app-tab-active { - color: var(--blue-bright); - background: var(--bg-1); - border-color: var(--line); - border-bottom: 1px solid var(--bg-1); -} -.app-tab-active:hover { color: var(--blue-bright); } -@media (max-width: 640px) { - .app-tabs { gap: 0; } - .app-tab { padding: .55rem .6rem; font-size: .85rem; } -} +.app-tab:hover { color: var(--text-1); } +.app-tab-active { color: var(--text-1); border-bottom-color: var(--blue-bright); } -/* „Még egy lépés" — a value the app wants once it is running (Part 4). It is a request, not a - fault, so it stays neutral: the blue hairline marks it as the thing to do next. */ -.after-start-card { border-left: 2px solid var(--blue); } +/* SECTIONS — a hairline between, nothing around. */ +.app-section { + padding: 28px 0; + border-bottom: 1px solid var(--line-soft); + background: none; + border-left: 0; border-right: 0; border-top: 0; + border-radius: 0; + margin: 0; +} +.app-section:last-child { border-bottom: 0; } +.app-section > h3, .app-section > h4 { + font-size: 17px; font-weight: 700; color: var(--text-1); margin: 0 0 6px; +} +.app-section > p { font-size: 14px; color: var(--text-2); line-height: 1.55; margin: 0 0 16px; } + +/* ROWS — label and value, one grid so every value on the page starts at the same x. */ +.app-field-row { + display: grid; + grid-template-columns: 200px minmax(0, 1fr); + gap: 16px; + padding: 10px 0; + align-items: baseline; +} +.app-field-label { font-size: 14px; color: var(--text-2); } +.app-field-value { font-size: 14px; color: var(--text-1); min-width: 0; } +.app-field-hint { font-size: 12px; color: var(--text-3); margin: 6px 0 0; } + +/* CALLOUTS — the only boxes on the page. */ +.app-callout { + padding: 14px 16px; + border-radius: var(--radius); + font-size: 14px; + border: 1px solid var(--line); +} +.app-callout > h3, .app-callout > h4 { font-size: 15px; font-weight: 700; margin: 0 0 6px; color: var(--text-1); } +/* „The app is waiting for you" — a request, never a fault, so blue and never amber. */ +.app-callout-ask { border-color: var(--blue); background: var(--blue-dim); } +/* „Something here is risky" — the exception colour, and only on a deviation. */ +.app-callout-warn { border-color: var(--warn); background: var(--warn-dim); } -/* The guide's FAQ. */ .app-faq { margin: .5rem 0 0; } .app-faq dt { color: var(--text-1); font-weight: 600; margin-top: .85rem; } .app-faq dd { color: var(--text-2); margin: .25rem 0 0; padding: 0; } +@media (max-width: 720px) { + .app-page { padding: 20px 16px 48px; } + .app-field-row { grid-template-columns: 1fr; gap: 4px; } + .app-hero-actions { margin-left: 0; flex-basis: 100%; } + .app-tabs { gap: 18px; } + .app-tab { font-size: 14px; } +} + .app-info-hero { display: flex; align-items: center; @@ -2759,7 +2847,7 @@ a.stat-card:hover { .no-js .nav-burger { display: none; } .content { margin-left: 0; padding: 1rem; } body { flex-direction: column; } - .app-row { flex-wrap: wrap; } + .app-field-row { flex-wrap: wrap; } .app-row-actions { width: 100%; justify-content: flex-end; } .stack-grid { grid-template-columns: 1fr; } /* R-907: a page header (title, domain chip, buttons) wraps instead of running off the screen; on the diff --git a/controller/internal/web/testdata/i18n_parity/app_data_deployed.html b/controller/internal/web/testdata/i18n_parity/app_data_deployed.html index c04b92d..2ffb354 100644 --- a/controller/internal/web/testdata/i18n_parity/app_data_deployed.html +++ b/controller/internal/web/testdata/i18n_parity/app_data_deployed.html @@ -8,7 +8,7 @@ PrivateBin — Felhom.eu - + +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +