panel-redesign: Server > Health; Overview reduced to verdicts; @routes off
Stage 1, step 5 - and the end of stage 1.
- GET /server/health (+ /fragment) shows every check in full and is the
home of POST .../recheck and POST .../reload, which now return there.
- GET /overview (+ /fragment) shows the overall verdict and six cards -
machine, processes, TLS certificate, queue, milter sockets, reverse DNS -
each a view.HealthCard (the typed input of the kit's health_card) that
links to its table on Health. A check that could not run is a warning,
not "ok".
- Both pages read healthChecks(), so a card and its table cannot disagree.
Until stage 2 they share the old status template, switched by a flag.
legacy_pages.txt: @routes removed. TestRoutesFollowNavigation is enforced
from here on: every path of plan section Routes is registered with its
method, no pre-2.0 path is answered, and no template links to a path the
router does not serve.
Not in this step, left for the Overview page of stage 2: the outbound and
inbound domain summaries the mockup shows under the cards.
Tests: the six cards from one reading of the checks (values, levels, links,
and the unverified case); the two modes of the page - Overview without the
tables and actions, Health with them, each polling its own fragment.
go test ./... green except the two Windows-only failures in internal/domain
and internal/logtail.
Roadmap 15/27.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mixeme/selfpost/internal/dnscheck"
|
||||
"github.com/mixeme/selfpost/internal/health"
|
||||
)
|
||||
|
||||
// checksFixture is one reading of every server check, shaped as healthChecks
|
||||
// returns it.
|
||||
func checksFixture() map[string]any {
|
||||
return map[string]any{
|
||||
"Processes": []health.Process{
|
||||
{Name: "opendkim", State: "RUNNING", Status: health.StatusOK},
|
||||
{Name: "postfix", State: "FATAL", Status: health.StatusError},
|
||||
},
|
||||
"ProcessError": false,
|
||||
"ProcessStatus": health.StatusError,
|
||||
"QueueSummary": "Mail queue is empty",
|
||||
"QueueError": "",
|
||||
"QueueStatus": health.StatusOK,
|
||||
"Machine": health.Machine{Status: health.StatusUnknown},
|
||||
"Cert": health.Certificate{Subject: "mail.example.org", DaysLeft: 12, Status: health.StatusWarn, Detail: "Expires soon."},
|
||||
"Sockets": []health.Socket{
|
||||
{Name: "OpenDKIM", Present: true, Status: health.StatusOK},
|
||||
{Name: "send-log", Present: false, Status: health.StatusWarn},
|
||||
},
|
||||
"SocketStatus": health.StatusWarn,
|
||||
"Hostname": "mail.example.org",
|
||||
"PTR": dnscheck.Result{Status: health.StatusOK},
|
||||
"OverallStatus": health.StatusError,
|
||||
"OverallHeading": "A component needs attention — see the details below.",
|
||||
}
|
||||
}
|
||||
|
||||
// Overview is one verdict per check: six cards in the order of the tables on
|
||||
// Health, each saying in words what its table would, and each leading there.
|
||||
func TestHealthCards(t *testing.T) {
|
||||
cards := healthCards(checksFixture())
|
||||
want := []struct{ name, level, value, href string }{
|
||||
{"Machine", "warn", "No reading", "/server/health#machine"},
|
||||
{"Processes", "fail", "1 of 2 running", "/server/health#processes"},
|
||||
{"TLS certificate", "warn", "Expires in 12 days", "/server/health#certificate"},
|
||||
{"Queue", "", "Mail queue is empty", "/outbound/queue"},
|
||||
{"Milter sockets", "warn", "1 of 2 answering", "/server/health#sockets"},
|
||||
{"Reverse DNS", "", "Forward = reverse", "/server/health#hostname"},
|
||||
}
|
||||
if len(cards) != len(want) {
|
||||
t.Fatalf("%d cards, want %d", len(cards), len(want))
|
||||
}
|
||||
for i, w := range want {
|
||||
c := cards[i]
|
||||
if c.Name != w.name || c.Level != w.level || c.Value != w.value || c.Href != w.href || c.Icon == "" {
|
||||
t.Errorf("card %d = %+v, want %+v", i, c, w)
|
||||
}
|
||||
}
|
||||
|
||||
// A check that could not run must not look fine.
|
||||
c := checksFixture()
|
||||
c["ProcessError"] = true
|
||||
c["ProcessStatus"] = health.StatusUnknown
|
||||
c["PTR"] = dnscheck.Result{Status: health.StatusUnknown}
|
||||
cards = healthCards(c)
|
||||
if cards[1].Level != "warn" || cards[1].Value != "Could not be read" || cards[5].Level != "warn" || cards[5].Value != "Not checked" {
|
||||
t.Errorf("unverified checks render as %+v and %+v", cards[1], cards[5])
|
||||
}
|
||||
}
|
||||
|
||||
// The two pages share the checks but not the content: Overview shows the
|
||||
// verdicts and polls its own fragment; Health shows the tables, polls its own,
|
||||
// and is the only place with the two server actions.
|
||||
func TestOverviewAndHealthFragments(t *testing.T) {
|
||||
h, _ := settingsServer(t)
|
||||
render := func(data map[string]any) string {
|
||||
t.Helper()
|
||||
var buf bytes.Buffer
|
||||
if err := h.view.Page("status").ExecuteTemplate(&buf, "content", data); err != nil {
|
||||
t.Fatalf("render: %v", err)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
full := checksFixture()
|
||||
overview := render(map[string]any{
|
||||
"Overview": true, "OverallStatus": full["OverallStatus"], "OverallHeading": full["OverallHeading"],
|
||||
"Cards": healthCards(full),
|
||||
})
|
||||
for _, want := range []string{
|
||||
"<h1>Overview</h1>", `hx-get="/overview/fragment"`, `href="/server/health#certificate"`,
|
||||
"Expires in 12 days", "1 of 2 running", `href="/server/health"`,
|
||||
} {
|
||||
if !strings.Contains(overview, want) {
|
||||
t.Errorf("Overview is missing %q", want)
|
||||
}
|
||||
}
|
||||
for _, gone := range []string{`action="/server/health/reload"`, `action="/server/health/recheck"`, `id="processes"`, "<meter", `hx-get="/server/health/fragment"`} {
|
||||
if strings.Contains(overview, gone) {
|
||||
t.Errorf("Overview still carries %q — the tables and the actions live on Health", gone)
|
||||
}
|
||||
}
|
||||
|
||||
healthPage := render(full)
|
||||
for _, want := range []string{
|
||||
"<h1>Health</h1>", `hx-get="/server/health/fragment"`, `action="/server/health/reload"`,
|
||||
`action="/server/health/recheck"`, `id="processes"`, `id="certificate"`, `id="sockets"`, `id="hostname"`, `id="machine"`,
|
||||
} {
|
||||
if !strings.Contains(healthPage, want) {
|
||||
t.Errorf("Health is missing %q", want)
|
||||
}
|
||||
}
|
||||
if strings.Contains(healthPage, `hx-get="/overview/fragment"`) || strings.Contains(healthPage, `id="verdicts"`) {
|
||||
t.Error("Health polls or shows the Overview fragment")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user