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:
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>`,
|
||||
|
||||
Reference in New Issue
Block a user