Dashboard layout: Apps card header (R-909), launcher header wraps on a phone (R-907), overflow counted after the page filter (R-906)

- Apps card: title row (logo top-aligned, name, address cut with an ellipsis and full in title) over a wrapping
  .stack-tags row; the tags no longer share a space-between row with the name.
- Phone (768px block): .page-header wraps; the share button drops to its own line, text whole.
- GetBannerAlerts(page, lang): the layout's filter before the cap; baseData and /monitoring use it.
- Tests: TestStackCardHeaderLayout, TestR907_PageHeaderWrapsOnAPhone, TestR906_* (all red against 05e12921).
- No Hungarian string changed. The three stacks parity fixtures are re-captured in the next commit
  (this commit alone fails TestI18nParity on them; both are pushed together).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
This commit is contained in:
2026-10-08 14:32:06 +02:00
parent 05e129267a
commit d5f2e47ea1
8 changed files with 330 additions and 9 deletions
@@ -0,0 +1,80 @@
package web
import (
"fmt"
"io"
"log"
"strings"
"testing"
"time"
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
"gitea.dooplex.hu/admin/felhom-controller/internal/monitor"
)
// R-906 — the banner showed „+ 5 további figyelmeztetés" with no warning above it. 9202 (2026-10-08) carried nine
// „not on a separate drive" warnings (Inline, PageOnly dashboard/monitoring) and the hub-off warning: the cap took the
// first five of all ten, counted five into the overflow line, and the layout then dropped all five visible ones,
// because they belong under the storage bars, not in the banner. Every page showed only the overflow line, and the
// hub-off warning — the one that DID belong in the banner — was cut.
//
// RED-PROOF (REPORT-dashboard-layout.md): point baseData and the monitoring handler back at GetAlerts and every page fails
// (overflow line present, hub-off banner missing).
func TestR906_OverflowCountsOnlyThisPagesBannerAlerts(t *testing.T) {
s := newDashboardServer(t, time.Time{})
am := NewAlertManager(log.New(io.Discard, "", 0))
var warnings, kinds []string
for i := 0; i < 9; i++ {
warnings = append(warnings, fmt.Sprintf("Az adattároló (/mnt/hdd/app%d) nem külön meghajtón van", i))
kinds = append(kinds, monitor.WarnKindStorageNotSeparate)
}
cfg := &config.Config{}
cfg.Hub.Enabled = false // the hub-off warning: the one banner alert this box has
cfg.Backup.Enabled = true
am.Refresh(&monitor.HealthReport{Status: "warn", Warnings: warnings, WarningKinds: kinds}, cfg, nil, false, "")
s.alertManager = am
if n := len(am.alerts); n != 10 {
t.Fatalf("setup: %d alerts, want 10 (nine inline disk warnings + hub off)", n)
}
for _, path := range []string{"/launcher", "/stacks", "/backups", "/monitoring", "/launcher?lang=en", "/stacks?lang=en"} {
rec := getPage(t, s, path)
if rec.Code != 200 {
t.Fatalf("GET %s = %d", path, rec.Code)
}
body := rec.Body.String()
if strings.Contains(body, "alert-banner-info") {
t.Errorf("%s: an overflow line is shown, but this page shows no more than one banner alert", path)
}
if got := strings.Count(body, "alert-banner-warning"); got != 1 {
t.Errorf("%s: %d warning banners, want 1 (the hub-off warning)", path, got)
}
}
}
// The cap still works: six alerts that ALL belong in the banner show five and „+ 1".
func TestR906_CapStillCountsBannerAlerts(t *testing.T) {
am := NewAlertManager(log.New(io.Discard, "", 0))
var warnings []string
for i := 0; i < 5; i++ {
warnings = append(warnings, fmt.Sprintf("CPU warning %d", i))
}
cfg := &config.Config{}
cfg.Hub.Enabled = false
cfg.Backup.Enabled = true
am.Refresh(&monitor.HealthReport{Status: "warn", Warnings: warnings, WarningKinds: make([]string, 5)}, cfg, nil, false, "")
got := am.GetBannerAlerts("launcher", "hu")
if len(got) != 6 || got[5].ID != "overflow" || !strings.Contains(got[5].Message, "1") {
t.Fatalf("six banner alerts: got %d, last %+v — want five and an overflow of 1", len(got), got[len(got)-1])
}
// An Inline/PageOnly alert never enters the banner list, on any page.
for _, page := range []string{"dashboard", "monitoring", "launcher"} {
for _, a := range am.GetBannerAlerts(page, "hu") {
if !bannerShows(a, page) && a.ID != "overflow" {
t.Errorf("%s: %q is not a banner alert on this page", page, a.ID)
}
}
}
}