overview-head: the mockup's sentence, made from the cards

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 <noreply@anthropic.com>
This commit is contained in:
mix
2026-10-17 12:48:34 +03:00
co-authored by Claude Opus 5.5
parent 9aebe1efe4
commit 1eec1c6011
6 changed files with 85 additions and 14 deletions
+7
View File
@@ -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.
+3 -1
View File
@@ -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
@@ -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</a> (4 of 5 answering)`} {
for _, want := range []string{`>` + tt.stamp + `<`, `Milter sockets</a>: 4 of 5 answering`} {
if !strings.Contains(page, want) {
t.Errorf("action %q: Overview is missing %q", tt.action, want)
}
+34 -10
View File
@@ -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 {
+38
View File
@@ -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)
}
}
}
+2 -2
View File
@@ -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,
`<h1 class="title is-3">Running, with warnings.</h1>`,
`<h1 class="title is-3">Mail is flowing. One thing needs attention.</h1>`,
`class="sp-postmark sp-warn"`, `<b>WARN</b>`, `1 OF 6 CHECKS`,
`Needs attention: <a href="/server/health">TLS certificate</a> (Expires in 12 days).`,
`<a href="/server/health">TLS certificate</a>: expires in 12 days.`,
`Updated 14:02:31<br>refreshes on its own`, `<p class="sp-kicker">mail.example.org</p>`,
`class="sp-warn"`, // the certificate card
`href="/server/health"`, `>All details</a>`,