diff --git a/CHANGELOG.md b/CHANGELOG.md index ce2088f..f0180f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,31 @@ +## Unreleased (2026-10-08, afternoon) — three dashboard layout fixes: the Apps card header (R-909), the launcher on a phone (R-907), the empty „+5 more warnings" (R-906) — ships with tomorrow's release + +**MinAgent: 0.131.0** (unchanged — nothing on the wire, nothing the agent sees). Hungarian text: not one string +changed; three Apps-page parity fixtures re-captured for the markup alone (below). + +- **R-909 — the Apps card header.** The title row and every tag were siblings in ONE `space-between` flex row; with + two or more tags (any installed app) the one-word name ran under them, and the logo was centred on the name block, + so a card with an address carried its logo lower. Now every card has the same two rows: logo + name (+ the address + under it, top-aligned), then the tags in their own `.stack-tags` row, left-aligned and wrapping. The address is one + line cut with an ellipsis (`.subdomain-text`, `min-width: 0`), full address in `title`, „↗" on the same line; the + unpublished-route warning stays under it. `TestStackCardHeaderLayout` (both languages, all 8 `stacks_full` cards + + the CSS rules; red against 05e12921: no tags row, no address title, `space-between`, `align-items: center`). +- **R-907 — the launcher's share button on a phone.** In the 768 px block `.page-header` wraps, so the button drops + to its own line under the title and the domain chip, its text kept whole (`white-space: nowrap`). It applies to + every page header on a phone (a header that fits is unchanged). `TestR907_PageHeaderWrapsOnAPhone` (red against + 05e12921: the phone block had no `.page-header` rule). +- **R-906 — „+ 5 további figyelmeztetés" with nothing above it.** `GetAlerts` capped the list at five over ALL alerts, + and the layout then dropped the Inline and other-page ones; on 9202 the five visible ones were inline „not on a + separate drive" warnings, so only the overflow line rendered and the hub-off warning was cut. New + `AlertManager.GetBannerAlerts(page, lang)` filters with the layout's own rule (`bannerShows`) BEFORE the cap; + `baseData` and the monitoring page use it. `GetAlerts` is unchanged for the debug dump. Tests `TestR906_*` (the 9202 + case on /launcher, /stacks, /backups, /monitoring, hu and en: no overflow line, the hub-off banner shown; red with + the two calls pointed back at `GetAlerts` — every page showed the overflow line and no warning). +- **Parity fixtures re-captured, in their own commit:** `stacks_full`, `stacks_unhealthy_stop`, + `stacks_install_interrupted`. Measured: each equals its predecessor with exactly the template change applied + (wrapper `stack-title-text`, the address span + `title`, the `stack-tags` row) — byte-for-byte, 3 of 3 — and the + visible text is identical. No other fixture moved. + ## Unreleased (2026-10-08) — a daytime press never cancels the night's whole-guest backup (R-899; operator ruling 2026-10-08, option A); an honest answer when older recovery packages were not checked, and the operator's mail (R-304) — ships with tomorrow's release **MinAgent: 0.131.0** (unchanged — the new `trigger=manual` query is ignored by an older agent, which keeps running the diff --git a/REUSE.md b/REUSE.md index 5245cf3..833bc2b 100644 --- a/REUSE.md +++ b/REUSE.md @@ -63,6 +63,7 @@ | `backup.ErrOffsiteQuota` | controller/internal/backup/offbox.go | sentinel | `ClassifyOffsiteFailure` telling a quota over-run apart | The other arms of that switch stay TEXT matches on purpose — they are restic's and ssh's own English output, which we neither write nor translate | | `monitor.WarnKind*` + `HealthReport.addWarning` / `WarningKindAt` | controller/internal/monitor/healthcheck.go | `(text, kind string)` | a health warning whose PLACEMENT the dashboard decides | Internal only: `internal/report/builder.go` copies Status/Issues/Warnings, so kinds never reach the hub (pinned) | | `monitor.MsgRef` + `HealthReport.addWarningMsg` / `addIssue` / `WarningMsgAt` / `IssueMsgAt` | controller/internal/monitor/healthcheck.go | `MsgRef{Key, Args}` beside the wire text | a health warning/issue whose BANNER must follow the household's language (R-516 item 10) | The wire text (report health.*) stays byte-identical; `AlertManager.Refresh` renders the key and falls back to the text when Key is empty. Every issue goes through `addIssue` so the parallel slices cannot drift (pinned) | +| `AlertManager.GetBannerAlerts(page, lang)` + `bannerShows` (R-906, 2026-10-08) | controller/internal/web/alerts.go | the layout banner's list for ONE page, capped AFTER the page filter | any page that fills `data["Alerts"]` (`baseData` does it for you) | `GetAlerts` keeps Inline and other pages' alerts (debug dump, placement tests) and must not feed a banner: its cap counted alerts the layout then dropped, so a page showed „+ 5 további figyelmeztetés" and nothing above it. `bannerShows` mirrors the layout's own filter (pinned: `TestR906_*`) | | `settings.OffboxTarget.LastWarningKind` + `backup.OffboxWarnNoAppsSelected` | controller/internal/settings/settings.go | persisted string | the Távoli mentés page's stale-note substitution | Written and cleared with `LastWarning`; the text fallback in `offboxWarningDisplay` is LEGACY only (kind == "") and is removed when R-570 closes | ### HTTP/JSON envelopes + flash messages diff --git a/controller/internal/web/alerts.go b/controller/internal/web/alerts.go index 072a43c..0dd93c7 100644 --- a/controller/internal/web/alerts.go +++ b/controller/internal/web/alerts.go @@ -338,8 +338,40 @@ func (am *AlertManager) Refresh(report *monitor.HealthReport, cfg *config.Config am.mu.Unlock() } -// GetAlerts returns a copy of the current alerts, optionally excluding specific IDs. +// GetAlerts returns a copy of the current alerts, optionally excluding specific IDs. It keeps the +// Inline and page-bound alerts (the debug dump and the placement tests read them); a page's banner +// reads GetBannerAlerts instead. func (am *AlertManager) GetAlerts(lang string, excludeIDs ...string) []Alert { + return am.collectAlerts(lang, nil, excludeIDs...) +} + +// GetBannerAlerts returns only the alerts the layout banner shows on page — no Inline alert, no alert +// whose PageOnly leaves page out — and caps THAT list. R-906: the cap used to count every alert and +// the layout dropped the other pages' alerts afterwards, so a page could show „+ 5 további +// figyelmeztetés" with no warning above it (9202, nine inline disk warnings). Pinned by +// TestR906_OverflowCountsOnlyThisPagesBannerAlerts. +func (am *AlertManager) GetBannerAlerts(page, lang string) []Alert { + return am.collectAlerts(lang, func(a Alert) bool { return bannerShows(a, page) }) +} + +// bannerShows is the layout's own filter (layout.html: not Inline, and PageOnly empty or naming the +// page), in Go, so the cap and the template agree on what a page shows. +func bannerShows(a Alert, page string) bool { + if a.Inline { + return false + } + if len(a.PageOnly) == 0 { + return true + } + for _, p := range a.PageOnly { + if p == page { + return true + } + } + return false +} + +func (am *AlertManager) collectAlerts(lang string, keep func(Alert) bool, excludeIDs ...string) []Alert { am.mu.RLock() defer am.mu.RUnlock() @@ -377,6 +409,19 @@ func (am *AlertManager) GetAlerts(lang string, excludeIDs ...string) []Alert { result = append(result, a) } + if keep != nil { + kept := result[:0] + for _, a := range result { + if keep(a) { + kept = append(kept, a) + } + } + result = kept + if len(result) == 0 { + return nil + } + } + // Cap at 5 visible alerts if len(result) > 5 { overflow := len(result) - 5 diff --git a/controller/internal/web/dashboard_layout_test.go b/controller/internal/web/dashboard_layout_test.go new file mode 100644 index 0000000..cea4e63 --- /dev/null +++ b/controller/internal/web/dashboard_layout_test.go @@ -0,0 +1,137 @@ +package web + +import ( + "regexp" + "strings" + "testing" +) + +// Dashboard layout fixes, 2026-10-08 (REPORT-dashboard-layout.md). A Go test cannot measure pixels; these pin the +// markup and the rules the layout stands on, and the screenshots (felhom.eu/documentation/audits/ +// dashboard-layout-2026-10-08/) show the consequence. + +// cssRule returns the body of the top-level rule whose selector line is exactly `selector {`. +func cssRule(t *testing.T, css, selector string) string { + t.Helper() + i := strings.Index(css, "\n"+selector+" {") + if i < 0 { + t.Fatalf("style.css has no top-level %q rule", selector) + } + body := css[i+len(selector)+3:] + return body[:strings.IndexByte(body, '}')] +} + +// The Apps card header. Before: the title row and every tag were siblings in ONE space-between flex row; with two +// or more tags (any installed app) the one-word name ran under them, and the logo was centred on the name block, +// so a card with an address carried it lower than one without. +// +// RED-PROOF (REPORT): against 05e12921's stacks.html and style.css this fails — no .stack-tags, a tag before it, +// no address title, and the header rule still `justify-content: space-between`. +func TestStackCardHeaderLayout(t *testing.T) { + s := i18nTestServer(t) + var full i18nCase + for _, c := range i18nCases() { + if c.name == "stacks_full" { + full = c + } + } + if full.name == "" { + t.Fatal("no stacks_full parity case") + } + addrRe := regexp.MustCompile(`\s*([^<]*) ↗\s*`) + for _, lang := range []string{"hu", "en"} { + page := renderI18nCase(t, s, lang, full) + cards := strings.Split(page, `