From 1eec1c6011933a500d5d9da99c0fba99d19b21b0 Mon Sep 17 00:00:00 2001 From: Mix Date: Sat, 17 Oct 2026 12:48:34 +0300 Subject: [PATCH] overview-head: the mockup's sentence, made from the cards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The owner's "1" (2026-10-17), the two shown side by side; design 9aebe1e. overviewHead writes "Mail is flowing. Nothing needs attention." when every card is fine, "Mail is flowing. One thing needs attention." (or "N things need") for warnings, and "Mail may not be flowing. ..." when a card fails; the lead names each card that is not fine, linked, with its value inside the sentence ("TLS certificate: expires in 12 days."), separated by semicolons. lowerFirst lowers a value's first letter unless it is an abbreviation ("CPU 98 %"); every value the cards write is a phrase. Tests: TestOverviewHeadSentences (the three states, two cards, an abbreviation), the Overview page and fragment, TestFilterDownIsAMissingSocket. go vet + go test (26 packages) and make e2e on the test server for this tree. Docs: guide ยง Overview, CHANGELOG [Unreleased]. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 7 +++ docs/guide.md | 4 +- .../handlers/handlers_filter_socket_test.go | 2 +- internal/web/view/overview.go | 44 ++++++++++++++----- internal/web/view/overview_head_test.go | 38 ++++++++++++++++ internal/web/view/pages_test.go | 4 +- 6 files changed, 85 insertions(+), 14 deletions(-) create mode 100644 internal/web/view/overview_head_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 7dc0315..996f949 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version ## [Unreleased] +### Changed + +- **Overview's head says it as the mockup does** (overview-head): "Mail is + flowing. One thing needs attention." - or "Nothing needs attention", or + "Mail may not be flowing" when a check fails - and under it each check + that is not fine with its value ("TLS certificate: expires in 12 days."). + ## [2.14.0] - 2026-10-17 Password reset by e-mail, to a confirmed address. Migration 0013; schema 13. diff --git a/docs/guide.md b/docs/guide.md index f902ad1..6476a76 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -455,7 +455,9 @@ assumptions stay here in `docs/guide.md`. ### Overview and Health **Overview** (`/overview`, global role) shows the stamp โ€” the worst of its -checks โ€” and one card per check: machine (processor and memory), supervised +checks โ€” with a sentence beside it ("Mail is flowing. One thing needs +attention.", "Mail may not be flowing." when a check fails) and, under it, +each check that is not fine with its value; then one card per check: machine (processor and memory), supervised processes (Postfix, the panel - whose roles include the DKIM signer - and their helpers), TLS certificate, queue, milter sockets and reverse DNS. A check the panel could not run shows as a warning, never as fine. Below the cards are the outbound domain list and, with the inbound relay diff --git a/internal/web/handlers/handlers_filter_socket_test.go b/internal/web/handlers/handlers_filter_socket_test.go index 3a30b46..5883fb6 100644 --- a/internal/web/handlers/handlers_filter_socket_test.go +++ b/internal/web/handlers/handlers_filter_socket_test.go @@ -162,7 +162,7 @@ func TestFilterDownIsAMissingSocket(t *testing.T) { rec := httptest.NewRecorder() h.HandleOverviewFragment(rec, reqAs("/overview/fragment", globalPrincipal)) page := rec.Body.String() - for _, want := range []string{`>` + tt.stamp + `<`, `Milter sockets (4 of 5 answering)`} { + for _, want := range []string{`>` + tt.stamp + `<`, `Milter sockets: 4 of 5 answering`} { if !strings.Contains(page, want) { t.Errorf("action %q: Overview is missing %q", tt.action, want) } diff --git a/internal/web/view/overview.go b/internal/web/view/overview.go index 6816dcb..0f066ae 100644 --- a/internal/web/view/overview.go +++ b/internal/web/view/overview.go @@ -10,6 +10,7 @@ import ( "strconv" "strings" "time" + "unicode" ) // Overview is the data of the Overview page. Cards are the checks of Health @@ -89,10 +90,12 @@ func (o *Overview) HeadOOB() Head { return h } -// overviewHead is the stamp and the sentence under it. The stamp takes the -// worst card, counts the cards that are not fine, and the lead names them with -// their detail so the operator sees what to open without scanning the grid. A -// card that could not be read is a warning, never a pass (cardLevel). +// overviewHead is the stamp and the sentence under it (mockup overview.html, +// the owner's "1" of 2026-10-17). The stamp takes the worst card and counts +// the cards that are not fine; the title says whether mail flows and how many +// things need attention, and the lead names them with their values so the +// operator sees what to open without scanning the grid. A card that could not +// be read is a warning, never a pass (cardLevel). func overviewHead(host string, cards []HealthCard, updated time.Time) Head { level, bad := LevelOK, []HealthCard{} for _, c := range cards { @@ -105,12 +108,14 @@ func overviewHead(host string, cards []HealthCard, updated time.Time) Head { } } - title, word, count := "Everything is running normally.", "OK", len(cards) + title, word, count := "Mail is flowing. Nothing needs attention.", "OK", len(cards) switch level { case LevelWarn: - title, word, count = "Running, with warnings.", "WARN", len(bad) + title, word, count = "Mail is flowing. "+needAttention(len(bad)), "WARN", len(bad) case LevelFail: - title, word, count = "A component needs attention.", "FAIL", len(bad) + // A failed card is Postfix, a milter socket, a queue that cannot be + // read: what can stop mail. + title, word, count = "Mail may not be flowing. "+needAttention(len(bad)), "FAIL", len(bad) } h := Head{ @@ -121,18 +126,37 @@ func overviewHead(host string, cards []HealthCard, updated time.Time) Head { Note: []string{"Updated " + updated.Local().Format("15:04:05"), "refreshes on its own"}, } if len(bad) > 0 { - lead := Text{{Text: "Needs attention: "}} + var lead Text for i, c := range bad { if i > 0 { - lead = append(lead, Inline{Text: ", "}) + lead = append(lead, Inline{Text: "; "}) } - lead = append(lead, Link(c.Href, c.Name), Inline{Text: " (" + c.Value + ")"}) + lead = append(lead, Link(c.Href, c.Name), Inline{Text: ": " + lowerFirst(c.Value)}) } h.Lead = append(lead, Inline{Text: "."}) } return h } +// needAttention is the second sentence of the title for n things. +func needAttention(n int) string { + if n == 1 { + return "One thing needs attention." + } + return strconv.Itoa(n) + " things need attention." +} + +// lowerFirst writes a card's value inside a sentence: "Expires in 12 days" +// becomes "expires in 12 days", but a word that starts with two capitals - an +// abbreviation, "CPU 98 %" - stays as it is. +func lowerFirst(s string) string { + r := []rune(s) + if len(r) > 1 && unicode.IsUpper(r[0]) && unicode.IsLower(r[1]) { + r[0] = unicode.ToLower(r[0]) + } + return string(r) +} + // FormatCount writes a whole number with its thousands separated by a no-break // space: "1 284". func FormatCount(n int64) string { diff --git a/internal/web/view/overview_head_test.go b/internal/web/view/overview_head_test.go new file mode 100644 index 0000000..04cc034 --- /dev/null +++ b/internal/web/view/overview_head_test.go @@ -0,0 +1,38 @@ +package view + +import ( + "testing" + "time" +) + +// The head says whether mail flows and how many things need attention, and +// the lead names each with its value inside the sentence (mockup +// overview.html; the owner's "1", 2026-10-17). +func TestOverviewHeadSentences(t *testing.T) { + ok := HealthCard{Name: "Processes", Value: "5 of 5 running", Href: "/server/health"} + cert := HealthCard{Name: "TLS certificate", Value: "Expires in 12 days", Href: "/server/health", Level: LevelWarn} + cpu := HealthCard{Name: "Machine", Value: "CPU 98 %", Href: "/server/health", Level: LevelWarn} + postfix := HealthCard{Name: "Processes", Value: "4 of 5 running", Href: "/server/health", Level: LevelFail} + for _, c := range []struct { + name string + cards []HealthCard + title, lead string + }{ + {"all fine", []HealthCard{ok}, "Mail is flowing. Nothing needs attention.", ""}, + {"one warning", []HealthCard{ok, cert}, "Mail is flowing. One thing needs attention.", "TLS certificate: expires in 12 days."}, + {"two warnings", []HealthCard{cert, cpu}, "Mail is flowing. 2 things need attention.", "TLS certificate: expires in 12 days; Machine: CPU 98 %."}, + {"a failure", []HealthCard{postfix, cert}, "Mail may not be flowing. 2 things need attention.", "Processes: 4 of 5 running; TLS certificate: expires in 12 days."}, + } { + h := overviewHead("mail.example.org", c.cards, time.Now()) + if h.Title != c.title { + t.Errorf("%s: title %q, want %q", c.name, h.Title, c.title) + } + var lead string + for _, in := range h.Lead { + lead += in.Text + } + if lead != c.lead { + t.Errorf("%s: lead %q, want %q", c.name, lead, c.lead) + } + } +} diff --git a/internal/web/view/pages_test.go b/internal/web/view/pages_test.go index 475508b..d4608e6 100644 --- a/internal/web/view/pages_test.go +++ b/internal/web/view/pages_test.go @@ -60,9 +60,9 @@ func pageLacks(t *testing.T, page, out string, gones ...string) { func TestOverviewHeadFollowsTheCards(t *testing.T) { out := renderSignedIn(t, "overview", overviewFixture()) pageHas(t, "Overview", out, - `

Running, with warnings.

`, + `

Mail is flowing. One thing needs attention.

`, `class="sp-postmark sp-warn"`, `WARN`, `1 OF 6 CHECKS`, - `Needs attention: TLS certificate (Expires in 12 days).`, + `TLS certificate: expires in 12 days.`, `Updated 14:02:31
refreshes on its own`, `

mail.example.org

`, `class="sp-warn"`, // the certificate card `href="/server/health"`, `>All details`,