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:
mix
2026-10-09 22:07:10 +03:00
co-authored by Claude Opus 5.5
parent b6feac2200
commit 783dabbea5
12 changed files with 311 additions and 20 deletions
+6
View File
@@ -60,6 +60,12 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
together or not at all — also at creation, so an application can be added together or not at all — also at creation, so an application can be added
with its limit already in place. The page that shows a generated password with its limit already in place. The page that shows a generated password
is the response to its POST only and is sent with `Cache-Control: no-store`. is the response to its POST only and is sent with `Cache-Control: no-store`.
- panel-redesign (stage 1): **Server › Health** (`/server/health`) holds the
tables of the old Status page and its two actions, *Re-check DNS* and
*Reload configuration*. **Overview** (`/overview`) is reduced to one
verdict per check — machine, processes, TLS certificate, queue, milter
sockets, reverse DNS — each leading to its table on Health; a check that
could not run shows as a warning, never as fine. Both read the same checks.
### Changed ### Changed
+1 -1
View File
@@ -509,7 +509,7 @@ only when both are done. Every stage-2 step ends with the evidence of
- [x] Inbound delegation: `requireInboundDomain`, lists and log filtered in the query, two-list user form data, per-route 404 tests for both roles including another tenant's id — **Opus** - [x] Inbound delegation: `requireInboundDomain`, lists and log filtered in the query, two-list user form data, per-route 404 tests for both roles including another tenant's id — **Opus**
- [x] Routes renamed per § Routes: new paths, one fragment name, old paths removed, subtree `requireGlobal()` for `/server/`; links in the old templates retargeted so the panel works on every commit — **Opus** - [x] Routes renamed per § Routes: new paths, one fragment name, old paths removed, subtree `requireGlobal()` for `/server/`; links in the old templates retargeted so the panel works on every commit — **Opus**
- [x] The application form as one POST; `out-app-created` as the response with `Cache-Control: no-store` — **Opus** - [x] The application form as one POST; `out-app-created` as the response with `Cache-Control: no-store` — **Opus**
- [ ] Server › Health route and handler (tables and *Reload configuration* from `/status`); Overview handler reduced to verdicts — **Opus** - [x] Server › Health route and handler (tables and *Reload configuration* from `/status`); Overview handler reduced to verdicts — **Opus**
**Stage 2 — pages, in groups** (each step: partial calls only, page off the ratchet list, evidence pairs) **Stage 2 — pages, in groups** (each step: partial calls only, page off the ratchet list, evidence pairs)
+1 -1
View File
@@ -39,7 +39,7 @@ in `git log` and [CHANGELOG.md](../CHANGELOG.md).
| panel-notifications | E-mail notifications about important events | candidate | — | — | | panel-notifications | E-mail notifications about important events | candidate | — | — |
| delivery-log-storage | Keep a message's `mail.log` lines with its send-log row | candidate | — | — | | delivery-log-storage | Keep a message's `mail.log` lines with its send-log row | candidate | — | — |
| password-reset | Password reset by e-mail | candidate | — | — | | password-reset | Password reset by e-mail | candidate | — | — |
| panel-redesign | Panel redesign (accepted mockups, design contract) | agreed | 14/27 | [plans/panel-redesign.md](plans/panel-redesign.md) | | panel-redesign | Panel redesign (accepted mockups, design contract) | agreed | 15/27 | [plans/panel-redesign.md](plans/panel-redesign.md) |
| schema-squash | Squash SQLite migrations into a 2.x baseline | built in panel-redesign (stage 1) | — | [plans/panel-redesign.md](plans/panel-redesign.md) | | schema-squash | Squash SQLite migrations into a 2.x baseline | built in panel-redesign (stage 1) | — | [plans/panel-redesign.md](plans/panel-redesign.md) |
**Recommended order** (not binding; owner, 2026-10-09: feature order follows **Recommended order** (not binding; owner, 2026-10-09: feature order follows
+5 -3
View File
@@ -38,9 +38,11 @@ var globalOnlyRoutes = []route{
{"POST", "/server/backup", func(h *Handlers) http.HandlerFunc { return h.HandleBackup }, nil}, {"POST", "/server/backup", func(h *Handlers) http.HandlerFunc { return h.HandleBackup }, nil},
{"POST", "/server/backup/import", func(h *Handlers) http.HandlerFunc { return h.HandleImportDomain }, nil}, {"POST", "/server/backup/import", func(h *Handlers) http.HandlerFunc { return h.HandleImportDomain }, nil},
{"GET", "/overview", func(h *Handlers) http.HandlerFunc { return h.HandleStatus }, nil}, {"GET", "/overview", func(h *Handlers) http.HandlerFunc { return h.HandleOverview }, nil},
{"GET", "/overview/fragment", func(h *Handlers) http.HandlerFunc { return h.HandleStatusFragment }, nil}, {"GET", "/server/health", func(h *Handlers) http.HandlerFunc { return h.HandleHealth }, nil},
{"POST", "/server/health/recheck", func(h *Handlers) http.HandlerFunc { return h.HandleStatusRecheck }, nil}, {"GET", "/server/health/fragment", func(h *Handlers) http.HandlerFunc { return h.HandleHealthFragment }, nil},
{"GET", "/overview/fragment", func(h *Handlers) http.HandlerFunc { return h.HandleOverviewFragment }, nil},
{"POST", "/server/health/recheck", func(h *Handlers) http.HandlerFunc { return h.HandleHealthRecheck }, nil},
{"GET", "/outbound/queue", func(h *Handlers) http.HandlerFunc { return h.HandleMailQueue }, nil}, {"GET", "/outbound/queue", func(h *Handlers) http.HandlerFunc { return h.HandleMailQueue }, nil},
{"GET", "/outbound/queue/fragment", func(h *Handlers) http.HandlerFunc { return h.HandleMailQueueBody }, nil}, {"GET", "/outbound/queue/fragment", func(h *Handlers) http.HandlerFunc { return h.HandleMailQueueBody }, nil},
+1 -1
View File
@@ -198,7 +198,7 @@ func (h *Handlers) HandleReload(w http.ResponseWriter, r *http.Request) {
return return
} }
} }
http.Redirect(w, r, "/overview?reloaded=1", http.StatusSeeOther) http.Redirect(w, r, "/server/health?reloaded=1", http.StatusSeeOther)
} }
func (h *Handlers) lookupDomain(w http.ResponseWriter, r *http.Request) (store.Domain, bool) { func (h *Handlers) lookupDomain(w http.ResponseWriter, r *http.Request) (store.Domain, bool) {
+143 -8
View File
@@ -1,42 +1,177 @@
package handlers package handlers
import ( import (
"fmt"
"net/http" "net/http"
"strings" "strings"
"github.com/mixeme/selfpost/internal/dnscheck"
"github.com/mixeme/selfpost/internal/health" "github.com/mixeme/selfpost/internal/health"
"github.com/mixeme/selfpost/internal/web/auth" "github.com/mixeme/selfpost/internal/web/auth"
"github.com/mixeme/selfpost/internal/web/view"
) )
func (h *Handlers) HandleStatus(w http.ResponseWriter, r *http.Request) { // The old Status page is two pages in 2.0 (plan § Information architecture):
//
// - Overview answers "is mail flowing?" with one verdict per check, each a
// card that leads to its detail;
// - Server › Health is everything behind those cards — the tables — and the
// home of the two actions, Re-check DNS and Reload configuration.
//
// Both read the same checks (healthChecks), so a card and its table can never
// disagree. Until stage 2 gives each page its own template they share the old
// one, switched by .Overview.
// HandleOverview shows the verdicts.
func (h *Handlers) HandleOverview(w http.ResponseWriter, r *http.Request) {
if _, ok := h.requireGlobal(w, r); !ok { if _, ok := h.requireGlobal(w, r); !ok {
return return
} }
data := h.statusBody() data := h.overviewBody()
data["Title"] = "SelfPost — status" data["Title"] = "SelfPost — overview"
data["User"] = auth.CurrentUser(r) data["User"] = auth.CurrentUser(r)
data["Active"] = "status" data["Active"] = "status"
data["IsGlobal"] = true data["IsGlobal"] = true
h.view.Render(w, http.StatusOK, "status", data)
}
// HandleOverviewFragment is the polled part of Overview.
func (h *Handlers) HandleOverviewFragment(w http.ResponseWriter, r *http.Request) {
if _, ok := h.requireGlobal(w, r); !ok {
return
}
h.view.RenderFragment(w, http.StatusOK, "status_body", h.overviewBody())
}
// HandleHealth shows every check in full, with the two server actions.
func (h *Handlers) HandleHealth(w http.ResponseWriter, r *http.Request) {
if _, ok := h.requireGlobal(w, r); !ok {
return
}
data := h.healthChecks()
data["Title"] = "SelfPost — health"
data["User"] = auth.CurrentUser(r)
data["Active"] = "health"
data["IsGlobal"] = true
data["Flash"] = statusFlash(r) data["Flash"] = statusFlash(r)
h.view.Render(w, http.StatusOK, "status", data) h.view.Render(w, http.StatusOK, "status", data)
} }
func (h *Handlers) HandleStatusFragment(w http.ResponseWriter, r *http.Request) { // HandleHealthFragment is the polled part of Health.
func (h *Handlers) HandleHealthFragment(w http.ResponseWriter, r *http.Request) {
if _, ok := h.requireGlobal(w, r); !ok { if _, ok := h.requireGlobal(w, r); !ok {
return return
} }
h.view.RenderFragment(w, http.StatusOK, "status_body", h.statusBody()) h.view.RenderFragment(w, http.StatusOK, "status_body", h.healthChecks())
} }
func (h *Handlers) HandleStatusRecheck(w http.ResponseWriter, r *http.Request) { // HandleHealthRecheck re-runs the DNS checks of the server ignoring the cache.
func (h *Handlers) HandleHealthRecheck(w http.ResponseWriter, r *http.Request) {
if _, ok := h.requireGlobal(w, r); !ok { if _, ok := h.requireGlobal(w, r); !ok {
return return
} }
h.dns.Server(h.cfg.Hostname, true) h.dns.Server(h.cfg.Hostname, true)
http.Redirect(w, r, "/overview?rechecked=1", http.StatusSeeOther) http.Redirect(w, r, "/server/health?rechecked=1", http.StatusSeeOther)
} }
func (h *Handlers) statusBody() map[string]any { // overviewBody reduces the checks to what Overview shows: the overall verdict
// and one card per check. The cards are the typed input of the health_card
// partial of the component kit, so the stage-2 page renders them as they are.
func (h *Handlers) overviewBody() map[string]any {
c := h.healthChecks()
return map[string]any{
"Overview": true,
"Hostname": c["Hostname"],
"OverallStatus": c["OverallStatus"],
"OverallHeading": c["OverallHeading"],
"Cards": healthCards(c),
}
}
// healthCards turns the checks into the six cards of Overview, in the order of
// the tables on Health. Each card links to the table behind it.
func healthCards(c map[string]any) []view.HealthCard {
machine := c["Machine"].(health.Machine)
procs := c["Processes"].([]health.Process)
cert := c["Cert"].(health.Certificate)
sockets := c["Sockets"].([]health.Socket)
ptr := c["PTR"].(dnscheck.Result)
card := func(name, icon, href string, status health.Status, value, sub string) view.HealthCard {
return view.HealthCard{Name: name, Icon: icon, Href: href, Level: cardLevel(status), Value: value, Sub: sub}
}
cpu, ram := "No reading", ""
if machine.CPU.Measured {
cpu = "CPU " + machine.CPU.BusyText()
}
if machine.Memory.Measured {
ram = "RAM " + machine.Memory.PctText()
}
running := 0
for _, p := range procs {
if p.Status == health.StatusOK {
running++
}
}
procValue := fmt.Sprintf("%d of %d running", running, len(procs))
if c["ProcessError"].(bool) {
procValue = "Could not be read"
}
certValue := cert.Detail
if cert.Status == health.StatusOK || cert.Status == health.StatusWarn {
certValue = fmt.Sprintf("Expires in %d days", cert.DaysLeft)
}
queueValue := c["QueueSummary"].(string)
if e := c["QueueError"].(string); e != "" {
queueValue = "Could not be read"
}
answering := 0
for _, s := range sockets {
if s.Present {
answering++
}
}
ptrValue := "Forward = reverse"
if ptr.Status != health.StatusOK {
ptrValue = "Needs attention"
if ptr.Status == health.StatusUnknown {
ptrValue = "Not checked"
}
}
return []view.HealthCard{
card("Machine", "ti-cpu", "/server/health#machine", machine.Status, cpu, ram),
card("Processes", "ti-server-cog", "/server/health#processes", c["ProcessStatus"].(health.Status), procValue, ""),
card("TLS certificate", "ti-certificate", "/server/health#certificate", cert.Status, certValue, cert.Subject),
card("Queue", "ti-stack-2", "/outbound/queue", c["QueueStatus"].(health.Status), queueValue, "outbound"),
card("Milter sockets", "ti-plug-connected", "/server/health#sockets", c["SocketStatus"].(health.Status),
fmt.Sprintf("%d of %d answering", answering, len(sockets)), ""),
card("Reverse DNS", "ti-arrows-exchange", "/server/health#hostname", ptr.Status, ptrValue, c["Hostname"].(string)),
}
}
// cardLevel maps the status of a check onto the three states of a card. A
// check that could not run is shown as a warning: the card must not look fine
// when nothing was verified.
func cardLevel(s health.Status) string {
switch s {
case health.StatusOK:
return ""
case health.StatusError:
return "fail"
default:
return "warn"
}
}
// healthChecks runs every server check once.
func (h *Handlers) healthChecks() map[string]any {
procs, procErr := health.Processes() procs, procErr := health.Processes()
procStatus := health.StatusUnknown procStatus := health.StatusUnknown
if procErr != nil { if procErr != nil {
@@ -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")
}
}
-1
View File
@@ -10,7 +10,6 @@
# components.html, components.go, layout.html, GET /server/components # components.html, components.go, layout.html, GET /server/components
# @routes the pre-2.0 route table is still in place (stage 1) # @routes the pre-2.0 route table is still in place (stage 1)
# <name> a page of the view engine that is still the old template # <name> a page of the view engine that is still the old template
@routes
backup backup
dashboard dashboard
deliveries deliveries
+6 -1
View File
@@ -3,8 +3,11 @@
{{define "wide"}}wide{{end}} {{define "wide"}}wide{{end}}
{{define "content"}} {{define "content"}}
{{/* One legacy template for two pages until stage 2 gives each its own:
Overview (the verdicts) and Server › Health (the tables and the two
server actions). .Overview says which one this is. */}}
<div class="page-head"> <div class="page-head">
<h1>Status</h1> <h1>{{if .Overview}}Overview{{else}}Health{{end}}</h1>
<label class="help-link" for="help-status" title="What these checks mean">?</label> <label class="help-link" for="help-status" title="What these checks mean">?</label>
</div> </div>
@@ -12,6 +15,7 @@
{{template "status_body" .}} {{template "status_body" .}}
{{if not .Overview}}
<div class="card" id="configuration"> <div class="card" id="configuration">
<h2>Configuration</h2> <h2>Configuration</h2>
<p class="muted">Regenerates the OpenDKIM and Postfix configuration from the <p class="muted">Regenerates the OpenDKIM and Postfix configuration from the
@@ -24,3 +28,4 @@
</form> </form>
</div> </div>
{{end}} {{end}}
{{end}}
@@ -1,5 +1,29 @@
{{define "status_body"}} {{define "status_body"}}
{{if .Overview}}
{{/* Overview: one verdict per check, each leading to its table on Health. */}}
<div id="status-body" data-poll aria-live="polite" hx-get="/overview/fragment" hx-trigger="load" hx-swap="outerHTML"> <div id="status-body" data-poll aria-live="polite" hx-get="/overview/fragment" hx-trigger="load" hx-swap="outerHTML">
<div class="card" id="overall">
<h2>Overall <span class="st st-{{.OverallStatus}}">{{.OverallStatus}}</span></h2>
<p class="muted">{{.OverallHeading}} <a href="/server/health">All details</a></p>
</div>
<div class="card" id="verdicts">
<h2>Server health</h2>
<table>
<thead><tr><th>Check</th><th>Verdict</th><th>Detail</th></tr></thead>
<tbody>
{{range .Cards}}
<tr>
<td><a href="{{.Href}}">{{.Name}}</a></td>
<td><span class="st st-{{if eq .Level "fail"}}error{{else if eq .Level "warn"}}warn{{else}}ok{{end}}">{{.Value}}</span></td>
<td class="muted">{{.Sub}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
{{else}}
<div id="status-body" data-poll aria-live="polite" hx-get="/server/health/fragment" hx-trigger="load" hx-swap="outerHTML">
<div class="card" id="overall"> <div class="card" id="overall">
<h2>Overall <span class="st st-{{.OverallStatus}}">{{.OverallStatus}}</span></h2> <h2>Overall <span class="st st-{{.OverallStatus}}">{{.OverallStatus}}</span></h2>
<p class="muted">{{.OverallHeading}}</p> <p class="muted">{{.OverallHeading}}</p>
@@ -127,3 +151,4 @@
</div> </div>
</div> </div>
{{end}} {{end}}
{{end}}
+1 -1
View File
@@ -424,7 +424,7 @@ func TestStatusPageRendersEveryCheck(t *testing.T) {
for _, want := range []string{ for _, want := range []string{
"opendkim", "FATAL", "Mail queue is empty", "mail.example.com", "opendkim", "FATAL", "Mail queue is empty", "mail.example.com",
"203.0.113.10 → no PTR record", `action="/server/health/reload"`, "203.0.113.10 → no PTR record", `action="/server/health/reload"`,
`hx-get="/overview/fragment"`, `class="st st-error"`, `hx-get="/server/health/fragment"`, `class="st st-error"`,
// Three .split rows inside the polled fragment: machine|processes, // Three .split rows inside the polled fragment: machine|processes,
// queue|certificate, and sockets|hostname. Ids stay on the cards. // queue|certificate, and sockets|hostname. Ids stay on the cards.
`id="processes"`, `id="machine"`, `id="queue"`, `id="certificate"`, `id="sockets"`, `id="hostname"`, `id="processes"`, `id="machine"`, `id="queue"`, `id="certificate"`, `id="sockets"`, `id="hostname"`,
+5 -3
View File
@@ -184,8 +184,8 @@ func (s *Server) muxes() (public, authed *http.ServeMux) {
// Overview. Its handler checks the role itself: the page is global-only // Overview. Its handler checks the role itself: the page is global-only
// but does not live under /server/. // but does not live under /server/.
authed.HandleFunc("GET /overview", h.HandleStatus) authed.HandleFunc("GET /overview", h.HandleOverview)
authed.HandleFunc("GET /overview/fragment", h.HandleStatusFragment) authed.HandleFunc("GET /overview/fragment", h.HandleOverviewFragment)
// Outbound: checked per domain in the handlers (lookupDomain, // Outbound: checked per domain in the handlers (lookupDomain,
// lookupApplication), adding and deleting by role. // lookupApplication), adding and deleting by role.
@@ -246,7 +246,9 @@ func (s *Server) muxes() (public, authed *http.ServeMux) {
} }
authed.HandleFunc(pattern, globalOnly(handler)) authed.HandleFunc(pattern, globalOnly(handler))
} }
server("POST /server/health/recheck", h.HandleStatusRecheck) server("GET /server/health", h.HandleHealth)
server("GET /server/health/fragment", h.HandleHealthFragment)
server("POST /server/health/recheck", h.HandleHealthRecheck)
server("POST /server/health/reload", h.HandleReload) server("POST /server/health/reload", h.HandleReload)
server("GET /server/log", h.HandleSystemLog) server("GET /server/log", h.HandleSystemLog)
server("GET /server/log/fragment", h.HandleSystemLogBody) server("GET /server/log/fragment", h.HandleSystemLogBody)