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 - + +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +