docs-audit: the guide and the panel's words against the code

The owner's "проверь, что документация актуальна с кодом" (2026-10-16);
design c5edbd8 and f1cc3f3 settled which side moves.

The panel follows its mockups: an inbound domain's MX box ends with
"Checked N ago" (dnscheck.InboundMXCheckedAt, from the cache) and its side
menu has See also › Log for this domain (the Inbound log, filtered); a
deferred message's history ends with the open dot of the wait in the
queue, the lifetime from this Postfix; Health's lead says six cards;
Overview's head says it refreshes on its own; DMARC's hint for an
unaligned third party is the mockup's sentence.

Help, word for word with the mockup and true to the code: the
application's password kept by the server (sasldb2) for an export, a
From:/Sender: refusal one row of the Outbound log, every backup password
check counted (AllowPasswordChange before the compare), a password set
for another user ends all their sessions, the limit mail daily per
sending domain. The Filter lists topic and its menu entry are no longer
shown to a domain administrator: their page is 404 to one
(requireInboundGlobal), and Help shows only what its viewer can open.

guide.md: Health's readings in the order of the boxes, with Notification
mail; the menu and role tables have the Inbound log and Filter lists; port
25's chain has the inbound journal; level 2 does not depend on the client
address, the client list refuses a message without one; four kinds of
refusal (wrong Sender); the log is filtered, not searched, and a message
is traced by its row; a bounced row shows only the queue lifetime;
TRUSTED_PROXY_CIDR serves sign-in and setup, the account-change limit is
per user; notifications' first check and hourly ceiling. The Apache
vhost's comment names mod_rewrite.

Tests: TestHelpHasTheInboundLogAndFilterListsTopics (the topic per
viewer), TestDeliveryPageDeferredUsesRetryPolicy (the wait),
TestInDomainShowsTheMockupsBoxes (checked, the log link), the Overview
head; the DMARC tests take the new hint. go vet + go test (26 packages)
and make e2e on the test server for this tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mix
2026-10-17 02:32:44 +03:00
co-authored by Claude Opus 5.5
parent f1cc3f3b37
commit ddafc96bab
18 changed files with 156 additions and 52 deletions
+1 -1
View File
@@ -255,7 +255,7 @@ func TightenPolicyHint(pass, fail int, sources []SourceHint) string {
if fail > 0 {
for _, s := range sources {
if s.FailCount > 0 && !s.ThisRelay {
return "A third-party source is not aligned. Do not tighten p= until that sender is fixed or removed."
return "Failures come from a source that is not this relay — review before tightening p=."
}
}
}
+13
View File
@@ -264,6 +264,19 @@ func (c *Checker) InboundMX(name, hostname string, force bool) Result {
return result
}
// InboundMXCheckedAt is when the MX of name was last looked up for
// hostname, zero when it has not been: the cache is filled when it is, and
// keeps the result domainTTL.
func (c *Checker) InboundMXCheckedAt(name, hostname string) time.Time {
c.mu.Lock()
defer c.mu.Unlock()
entry, ok := c.inbound[name+"\x00"+hostname]
if !ok {
return time.Time{}
}
return entry.expires.Add(-c.domainTTL)
}
func (c *Checker) checkInboundMX(ctx context.Context, name, hostname string) Result {
mxs, err := c.resolver.LookupMX(ctx, name)
if err != nil {
@@ -109,12 +109,12 @@ func TestDMARCDomainShowsSourcesAndWhatToDoAboutThem(t *testing.T) {
has(t, "domain", body,
`<h1 class="title is-3">example.org</h1>`, `<a href="/outbound/dmarc">DMARC reports</a> /`,
`class="sp-postmark sp-warn"`, `<b>99.6%</b>`, `PASS · 7 DAYS`,
"1\u00a0903 pass · 7 fail in the last 7 days. A third-party source is not aligned. Do not tighten <code>p=</code>",
"1\u00a0903 pass · 7 fail in the last 7 days. Failures come from a source that is not this relay — review before tightening <code>p=</code>",
`href="/outbound/domains/1"`, `<h2>Sources · 7 days</h2>`, `198.51.100.44`, `203.0.113.25`, `<h2>Reports</h2>`,
`href="/outbound/dmarc/reports/`+idStr(s.reportID)+`">View</a>`)
// 198.51.100.44 passed nothing, so its disposition is amber.
has(t, "domain", body, `<td>0</td><td>7</td><td><span class="tag is-warning is-light">none</span></td>`)
lacks(t, "domain", body, `this relay`)
lacks(t, "domain", body, `>this relay</span>`)
}
func TestDMARCDomainWithoutReportsSaysSo(t *testing.T) {
@@ -6,6 +6,7 @@ import (
"net/http"
"strconv"
"sync"
"time"
"github.com/mixeme/selfpost/internal/dnscheck"
"github.com/mixeme/selfpost/internal/health"
@@ -201,8 +202,12 @@ type inboundDetailView struct {
func (h *Handlers) renderInboundDetail(w http.ResponseWriter, r *http.Request, status int, d store.InboundDomain, extra inboundDetailView) {
mx := dnscheck.Result{Status: health.StatusUnknown}
var checked string
if h.dns != nil && h.cfg.Hostname != "" {
mx = h.dns.InboundMX(d.Name, h.cfg.Hostname, false)
if at := h.dns.InboundMXCheckedAt(d.Name, h.cfg.Hostname); !at.IsZero() {
checked = view.FormatAge(at, time.Now())
}
}
on, milter, action := h.inboundFilter()
page := view.NewInDomain(h.shellMeta(r), view.InDomainInput{
@@ -210,6 +215,8 @@ func (h *Handlers) renderInboundDetail(w http.ResponseWriter, r *http.Request, s
RecipientMode: d.RecipientMode, RecipientCount: d.RecipientCount, Addresses: d.Recipients,
}).
WithMX(h.cfg.Hostname, view.DNSCheck{Status: string(mx.Status), Detail: mx.Detail, Found: mx.Records}).
WithChecked(checked).
WithLog(view.InLogHref(view.InLogFilter{Domain: d.Name}, 1)).
WithFilter(on, milter, action).
WithResult(inboundFlash(r), firstNonEmpty(extra.FormErr, extra.TransportErr, extra.RecipientErr))
h.view.Render(w, status, "in-domain", page)
+5 -1
View File
@@ -212,9 +212,13 @@ func deliveryEvents(row store.SendLogRow, policy postfix.RetryPolicy, sendsAs vi
return append(events, step(row.UpdatedAt, "ok", "Delivered",
"The receiving server accepted the message. That is as far as this server can see — what the recipient's mailbox then did with it is not reported back."))
case store.StatusDeferred:
// And the wait that follows, as an open dot: the next attempt is
// Postfix's, at a time the panel does not know.
return append(events, step(row.UpdatedAt, "warn", "Deferred, will be retried",
fmt.Sprintf("The receiving server could not take the message yet. Postfix retries: first after %s, then with increasing gaps up to %s, for up to %s. There is no fixed attempt count — a deferred message stays in the queue until it is delivered or that lifetime runs out.",
policy.FirstRetry(), policy.BackoffCap(), policy.QueueLifetime())))
policy.FirstRetry(), policy.BackoffCap(), policy.QueueLifetime())),
view.Step{Time: view.StepTime(time.Time{}), Level: view.StepLevel("", false),
Text: view.Plain(fmt.Sprintf("Still in the queue; the next attempt is Postfix's own, and after %s it is bounced.", policy.QueueLifetime()))})
case store.StatusBounced:
if row.Reason == store.ReasonReconcile {
// Neither the receiving server nor Postfix said so: the panel
@@ -565,6 +565,9 @@ func TestDeliveryPageDeferredUsesRetryPolicy(t *testing.T) {
"first after 10 minutes",
"up to about 1 hour 7 minutes",
"for up to 2 days",
// And the wait after it, an open dot with no time (mockup
// out-message.html).
`<li class="sp-pending"><time>not yet</time>Still in the queue; the next attempt is Postfix&#39;s own, and after 2 days it is bounced.</li>`,
} {
if !strings.Contains(out, want) {
t.Errorf("deferred history is missing %q:\n%s", want, out)
+1 -1
View File
@@ -50,7 +50,7 @@ func TestDMARCDomainStampAndLead(t *testing.T) {
pageHas(t, "DMARC domain", out,
`<h1 class="title is-3">shop.example.org</h1>`, `<a href="/outbound/dmarc">DMARC reports</a> /`,
`class="sp-postmark sp-warn"`, `<span>DMARC</span><b>99.6%</b><span>PASS · 7 DAYS</span>`,
"11 204 pass · 41 fail in the last 7 days. A third-party source is not aligned. Do not tighten <code>p=</code> until that sender is fixed or removed.",
"11 204 pass · 41 fail in the last 7 days. Failures come from a source that is not this relay — review before tightening <code>p=</code>.",
`<a class="button" href="/outbound/domains/2"><i class="ti ti-world-upload"></i>Domain page</a>`,
`<h2>Sources · 7 days</h2>`, `203.0.113.25 <span class="tag is-light">this relay</span>`,
`<td>0</td><td>41</td><td><span class="tag is-warning is-light">none</span></td>`,
+4 -3
View File
@@ -290,8 +290,8 @@ func outMessageFixture() *OutMessage {
{Time: StepTime(at), Strong: "Accepted and queued", Text: Plain(" — Postfix accepted the message over an authenticated submission and the journal-milter recorded it.")},
{Time: StepTime(at.Add(2 * time.Second)), Strong: "Deferred, will be retried", Level: LevelWarn,
Text: Plain(" — the receiving server could not take the message yet. Postfix retries: first after 5 minutes, then with increasing gaps up to 1 hour 7 minutes, for up to 5 days.")},
{Time: StepTime(time.Time{}), Strong: "Waiting for a delivery report", Level: LevelPending,
Text: Plain(" — the next attempt is made by Postfix on its own; the queue shows what it is still holding.")},
{Time: StepTime(time.Time{}), Level: LevelPending,
Text: Plain("Still in the queue; the next attempt is Postfix's own, and after 5 days it is bounced.")},
})
return p.WithDeliveryLog([]LogLine{
{Time: "13:58:07", Text: "postfix/smtpd[2114]: 4XcB7k2Jm9z1: client=unknown[198.51.100.7], sasl_username=alerts"},
@@ -324,7 +324,7 @@ func dmarcHubFixture() *DMARCHub {
// with one source that is not this relay failing, and three reports.
func dmarcDomainFixture() *DMARCDomain {
p := NewDMARCDomain(admin(), 2, "shop.example.org", true, 11204, 41, 7,
"A third-party source is not aligned. Do not tighten p= until that sender is fixed or removed.")
"Failures come from a source that is not this relay — review before tightening p=.")
row := func(id int64, at, reporter, window string, pass, fail int) DMARCReportRow {
return DMARCReportRow{Received: at, Reporter: reporter, Window: window, Pass: pass, Fail: fail, Href: DMARCReportHref(id)}
}
@@ -379,6 +379,7 @@ func inDomainFixture() *InDomain {
TLSMode: "encrypt", RecipientMode: "list", RecipientCount: 4,
Addresses: []string{"announce@lists.example.org", "dev@lists.example.org", "owner@lists.example.org", "security@lists.example.org"}})
p.WithMX("mail.example.org", DNSCheck{Status: "ok", Found: []string{"10 mail.example.org."}})
p.WithChecked("1 min ago").WithLog("/inbound/log?domain=lists.example.org")
p.WithFilter(true, "inet:antispam:11332", "accept")
return p
}
+1 -1
View File
@@ -187,7 +187,7 @@ func NewHealth(m Meta, flash string) *Health {
Head: Head{
Kicker: "Server",
Title: "Health",
Lead: Plain("Everything behind the cards on Overview. Refreshes automatically."),
Lead: Plain("Everything behind the six cards on Overview. Refreshes automatically."),
Actions: []Action{{Label: "Re-check DNS", Icon: "ti-refresh", Post: "/server/health/recheck"}},
},
// Numbered by importance; the odd ones are the left stack.
+31 -7
View File
@@ -75,6 +75,23 @@ func topicLink(topic, title string) *HelpLink {
return &HelpLink{Href: "/help#" + topic, Title: title}
}
// withoutTopic is a group's topics and menu without the topic id.
func withoutTopic(topics []HelpTopic, menu []MenuItem, id string) ([]HelpTopic, []MenuItem) {
var t []HelpTopic
for _, x := range topics {
if x.ID != id {
t = append(t, x)
}
}
var m []MenuItem
for _, x := range menu {
if x.Href != "#"+id {
m = append(m, x)
}
}
return t, m
}
// reachesOutbound is whether the viewer has anything in Outbound: the global
// role, or a domain administrator with an outbound domain.
func (m Meta) reachesOutbound() bool { return m.IsGlobal || m.HasOutbound }
@@ -116,14 +133,21 @@ func NewHelp(m Meta, inbound bool) *Help {
})
}
if inbound && (m.IsGlobal || m.HasInbound) {
add("Inbound", inboundTopics, []MenuItem{
topics, menu := inboundTopics, []MenuItem{
{Label: "Backup-MX and forwarding", Icon: "ti-inbox", Href: "#" + HelpInbound},
{Label: "Upstream", Icon: "ti-arrow-forward-up", Href: "#" + HelpUpstream},
{Label: "Valid recipients", Icon: "ti-address-book", Href: "#" + HelpRecipients},
{Label: "Spam filter", Icon: "ti-shield-check", Href: "#" + HelpFilter},
{Label: "Inbound log", Icon: "ti-list-details", Href: "#" + HelpInboundLog},
{Label: "Filter lists", Icon: "ti-filter", Href: "#" + HelpFilterLists},
})
}
// The filter lists are the whole server's and their page is 404 to
// a domain administrator (requireInboundGlobal), so their topic is
// not shown to one either.
if !m.IsGlobal {
topics, menu = withoutTopic(topics, menu, HelpFilterLists)
}
add("Inbound", topics, menu)
}
if m.IsGlobal {
add("Server", serverTopics, []MenuItem{
@@ -174,7 +198,7 @@ var outboundTopics = []HelpTopic{
", once the reports look clean. A domain's report address is set in its Domain settings."),
}},
{ID: HelpApps, Title: "Applications", Body: []Text{
Plain("An application is one SASL login, unique across all domains. It may send as any address of its domain or only as listed addresses, and, with a list of client IPs set, only from those addresses. The password is shown once at creation or regeneration and is not stored in readable form."),
Plain("An application is one SASL login, unique across all domains. It may send as any address of its domain or only as listed addresses, and, with a list of client IPs set, only from those addresses. The password is shown once, at creation or regeneration, and the panel does not show it again; the server keeps it, so a domain export can carry it."),
}},
{ID: HelpSenderCheck, Title: "Sender check", Body: []Text{
Rich("An application sends only as itself, and that holds for every sender a message names. The envelope sender is held against the login by Postfix. The ",
@@ -188,7 +212,7 @@ var outboundTopics = []HelpTopic{
Rich("Level 1 is a per-IP ceiling set in ", Code(".env"), ". Level 2 is optional, per domain and per application, entered by hand or computed from the last 30 days of sending, and can never exceed level 1. An application's own limit replaces its domain's."),
}},
{ID: HelpLog, Title: "Outbound log", Body: []Text{
Rich("One row per message and recipient, with the status Postfix last reported: queued (accepted, nothing reported yet), sent (the receiving server took it; what the mailbox does next is not reported back), deferred (not taken yet, Postfix tries again), bounced (failed for good, or the queue lifetime ran out; or closed by the panel when Postfix no longer held the message and ", Code("mail.log"), " never said what became of it) and rejected (refused before queueing, so it never reached the queue: under a level-2 rate limit, from a client address the application does not allow, or for a ", Code("From:"), " or ", Code("Sender:"), " line that is not the application's - the message's page says which). A message's page lists these steps and the lines Postfix wrote about it in ",
Rich("One row per message and recipient (a message refused for its From: or Sender: line is one row, however many recipients it had), with the status Postfix last reported: queued (accepted, nothing reported yet), sent (the receiving server took it; what the mailbox does next is not reported back), deferred (not taken yet, Postfix tries again), bounced (failed for good, or the queue lifetime ran out; or closed by the panel when Postfix no longer held the message and ", Code("mail.log"), " never said what became of it) and rejected (refused before queueing, so it never reached the queue: under a level-2 rate limit, from a client address the application does not allow, or for a ", Code("From:"), " or ", Code("Sender:"), " line that is not the application's - the message's page says which). A message's page lists these steps and the lines Postfix wrote about it in ",
Code("mail.log"), ", found by queue id - and for a message refused before it had one, Postfix's one line about the refusal. The lines are kept with the row: they stay on the page after that file has rotated, for as long as the row does. A message accepted before version 2.2.0 shows them only while they are still in the current file. Rows are deleted after the retention set in Server › Settings, 90 days by default, and their lines with them. A domain administrator sees only the rows of their own domains."),
}},
{ID: HelpQueue, Title: "Queue and retries", Body: []Text{
@@ -245,7 +269,7 @@ var serverTopics = []HelpTopic{
{ID: HelpBackup, Title: "Backup and moving a domain", Body: []Text{
Rich("Full backup downloads one archive of the whole instance: ", Code("data/"), " (the database, DKIM keys and application credentials), ",
Code("docker-compose.yml"), ", ", Code(".env"), " and ", Code("certs/"),
". Both it and a domain's export ask for your account password - the one you sign in with - at every download, so a session left open is not enough to take them; wrong guesses count against the same limit as a password change."+
". Both it and a domain's export ask for your account password - the one you sign in with - at every download, so a session left open is not enough to take them; every attempt, right or wrong, counts against the limit of a password change, ten in 15 minutes."+
" The Postfix queue is not in it. The backup is taken while the server is running, and Postfix keeps moving messages through the queue, "+
"so a copy could hold a message twice or miss it; mail still waiting for delivery is therefore not carried over, and a restored server "+
"starts with an empty queue. To keep the queue, stop the container and archive the project directory instead. The private key in ", Code("certs/"),
@@ -260,7 +284,7 @@ var serverTopics = []HelpTopic{
{ID: HelpUsers, Title: "Users and roles", Body: []Text{
Rich("The two roles are a reach, not a rank: every user is an administrator and signs in with a username and password. Global reaches the whole server — Overview, Server, the queue and every domain — and is the only role that adds or deletes domains and users. Domain reaches only the domains assigned to it, outbound and inbound separately (the same name on both lists is two assignments); ",
Em("All"), " includes domains added later, and a domain user needs at least one domain or ", Em("All"),
" on either list. The only global user cannot be demoted or deleted, and you cannot delete the account you are signed in with. A new password set here signs that user out of their other sessions."),
" on either list. The only global user cannot be demoted or deleted, and you cannot delete the account you are signed in with. A new password set here signs that user out everywhere; set for yourself, it keeps the session you set it from."),
}},
{ID: HelpSettings, Title: "Settings", Body: []Text{
Rich("Settings holds what is true of the whole instance. The time zone is the container's, ", Code("TZ"), " in ", Code(".env"),
@@ -271,7 +295,7 @@ var serverTopics = []HelpTopic{
Code(".env"), ": changing it needs a restart, and no level-2 limit can exceed it. Notifications are turned on or off here for everyone."),
}},
{ID: HelpNotifications, Title: "Notifications", Body: []Text{
Rich("The panel writes to its users when something needs them, from ", Code("postmaster@"), " the server's hostname. Four things are reported. A sending domain's SPF, DKIM or DMARC record stops matching: sent once two hourly checks in a row have found it so, and once more when the record matches again. A level-2 rate limit refuses mail: sent at the first refusal, then at most once a day for the same limit. The TLS certificate is 14 days from expiry, and again at 3 days if it is still the same certificate. A message has waited in the queue for more than three days, with one more message when none is left. A global user is told all of it; a domain administrator is told about the DNS records and limits of their own domains.",
Rich("The panel writes to its users when something needs them, from ", Code("postmaster@"), " the server's hostname. Four things are reported. A sending domain's SPF, DKIM or DMARC record stops matching: sent once two hourly checks in a row have found it so, and once more when the record matches again. A level-2 rate limit refuses mail: sent at the first refusal, then at most once a day for the same sending domain. The TLS certificate is 14 days from expiry, and again at 3 days if it is still the same certificate. A message has waited in the queue for more than three days, with one more message when none is left. A global user is told all of it; a domain administrator is told about the DNS records and limits of their own domains.",
Br(), Br(),
"Each user turns notifications on or off under Account and needs an e-mail there; Server › Settings turns them off for everyone. They are the panel's own mail: not rows of the Outbound log, and not counted against any limit.",
Br(), Br(),
+6
View File
@@ -186,6 +186,12 @@ func TestHelpHasTheInboundLogAndFilterListsTopics(t *testing.T) {
if want := v.inbound && (v.meta.IsGlobal || v.meta.HasInbound); shown != want {
t.Errorf("%s: inbound log topic shown = %v, want %v", v.name, shown, want)
}
// The filter lists' page is the global role's alone (404 to anyone
// else), and so is their topic and its menu entry.
lists := strings.Contains(out, `id="filter-lists"`) || strings.Contains(out, `href="#filter-lists"`)
if want := v.inbound && v.meta.IsGlobal; lists != want {
t.Errorf("%s: filter lists topic shown = %v, want %v", v.name, lists, want)
}
}
if HelpInboundLog != "inbound-log" || HelpFilterLists != "filter-lists" {
t.Errorf("anchors %q and %q", HelpInboundLog, HelpFilterLists)
+22
View File
@@ -257,6 +257,7 @@ type InDomain struct {
recipientCount int
recipientMode string
filterOn bool
logHref string
DNS Box
Record Record
@@ -395,6 +396,22 @@ func (p *InDomain) WithFilter(on bool, milter, action string) *InDomain {
return p
}
// WithChecked says, as the MX box's end slot, how long ago the record was
// checked (FormatAge; "" says nothing), as a sending domain's records do.
func (p *InDomain) WithChecked(checked string) *InDomain {
if checked != "" {
p.DNS.End = Plain("Checked " + checked)
}
return p
}
// WithLog links the domain's rows of the Inbound log from the side menu's
// See also, as a sending domain's page links its own log.
func (p *InDomain) WithLog(href string) *InDomain {
p.logHref = href
return p
}
// WithResult shows the result of the last action, or its refusal, between the
// head and the boxes.
func (p *InDomain) WithResult(flash, formErr string) *InDomain {
@@ -420,6 +437,11 @@ func (p *InDomain) Menu() SideMenu {
{Label: "Spam filter", Icon: "ti-shield-check", Href: "#filter", Tag: filterTag(p.filterOn).Tag},
}}
menu := SideMenu{Groups: []MenuGroup{here}}
if p.logHref != "" {
menu.Groups = append(menu.Groups, MenuGroup{Label: "See also", Items: []MenuItem{
{Label: "Log for this domain", Icon: "ti-list-details", Href: p.logHref},
}})
}
if p.IsGlobal {
menu.Groups = append(menu.Groups, MenuGroup{Label: "Rarely changed", Items: []MenuItem{
{Label: "Delete domain", Icon: "ti-trash", Href: InboundDomainHref(p.id) + "/delete", Danger: true},
+2 -1
View File
@@ -17,9 +17,10 @@ func TestInDomainShowsTheMockupsBoxes(t *testing.T) {
`<option value="list" selected>Listed addresses only</option>`, `name="addresses"`,
"announce@lists.example.org\ndev@lists.example.org\nowner@lists.example.org\nsecurity@lists.example.org",
`<h2>Spam filter</h2>`, `href="#filter"`, `href="/inbound/domains/1/delete"`, `Delete domain`,
`Checked 1 min ago`, `<a href="/inbound/log?domain=lists.example.org"><i class="ti ti-list-details"></i>Log for this domain</a>`,
)
// What has no source in this build is not drawn.
pageLacks(t, "Inbound domain", out, `rspamd`, `quarantine`, `Quarantine`, `Log for this domain`, `/inbound/quarantine`, `Allow and deny`)
pageLacks(t, "Inbound domain", out, `rspamd`, `quarantine`, `Quarantine`, `/inbound/quarantine`, `Allow and deny`)
}
// The Spam filter box states what the server is set up with, and what of it the
+1 -1
View File
@@ -118,7 +118,7 @@ func overviewHead(host string, cards []HealthCard, updated time.Time) Head {
Kicker: host,
Title: title,
ID: "overview-head",
Note: []string{"Updated " + updated.Local().Format("15:04:05")},
Note: []string{"Updated " + updated.Local().Format("15:04:05"), "refreshes on its own"},
}
if len(bad) > 0 {
lead := Text{{Text: "Needs attention: "}}
+1 -1
View File
@@ -63,7 +63,7 @@ func TestOverviewHeadFollowsTheCards(t *testing.T) {
`<h1 class="title is-3">Running, with warnings.</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).`,
`Updated 14:02:31`, `<p class="sp-kicker">mail.example.org</p>`,
`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>`,
)