panel-redesign: an idle one-shot helper is "on demand", not "stopped"

The owner's choice (2026-10-10) for the last line of the Processes table
on Server > Health: postfix-reload runs when the configuration is
reloaded and exits, so between runs supervisord calls it STOPPED - a
word that reads as a fault beside six running programs, in a box that
rightly says ok. Its row now has the neutral tag "on demand" and says
when it last ran (or that it has not run since the container started).
A one-shot in trouble keeps supervisord's word and level, and a daemon
that is stopped is still an error.

  go test -count=1 ./internal/web/...: ok

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mix
2026-10-10 23:45:05 +03:00
co-authored by Claude Opus 5.5
parent baa053d250
commit c36bd60022
3 changed files with 44 additions and 3 deletions
+3
View File
@@ -221,6 +221,9 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
running. The Milter sockets card no longer clips its second line with three
sockets, and a DKIM value is shown whole instead of scrolling inside its
field.
- Server › Health shows the idle reload helper (`postfix-reload`) as
**on demand**, with when it last ran, instead of `stopped` beside the
running programs (owner, 2026-10-10).
- The confirmation before deleting a user names the domains that follow
that user's default report address and will be left without one; it used
to say the domains were not affected (replaced before release — see the
+19 -3
View File
@@ -150,9 +150,7 @@ func healthPage(m view.Meta, c map[string]any, flash string) *view.Health {
page.Processes.End = view.Verdict(string(c["ProcessStatus"].(health.Status)))
page.ProcessError = c["ProcessError"].(bool)
for _, p := range c["Processes"].([]health.Process) {
page.ProcessRows = append(page.ProcessRows, view.ProcessRow{
Name: p.Name, State: view.Tag{Status: string(p.Status), Label: strings.ToLower(p.State)}, Detail: p.Detail,
})
page.ProcessRows = append(page.ProcessRows, processRow(p))
}
cert := c["Cert"].(health.Certificate)
@@ -194,6 +192,24 @@ func healthPage(m view.Meta, c map[string]any, flash string) *view.Health {
return page
}
// processRow is one line of the Processes table. A one-shot helper that sits
// idle is not "stopped" the way a daemon is: it runs when asked and exits, so
// its row says "on demand" in a neutral tag and when it last ran, instead of a
// word that reads as a fault beside the running programs.
func processRow(p health.Process) view.ProcessRow {
if p.OneShot && p.Status == health.StatusOK && p.State != "RUNNING" {
detail := "Runs when the configuration is reloaded"
switch {
case p.Detail == "" || strings.EqualFold(p.Detail, "Not started"):
detail += "; not run since the container started"
default:
detail += "; last run: " + p.Detail
}
return view.ProcessRow{Name: p.Name, State: view.Tag{Status: "off", Label: "on demand"}, Detail: detail}
}
return view.ProcessRow{Name: p.Name, State: view.Tag{Status: string(p.Status), Label: strings.ToLower(p.State)}, Detail: p.Detail}
}
// machineRows is the Machine table: processor and memory as bars with their
// figure, the network as rates, and for each the sentence the sampler gave. A
// reading that could not be taken keeps its row, without a figure.
@@ -1,6 +1,7 @@
package handlers
import (
"strings"
"testing"
"github.com/mixeme/selfpost/internal/dnscheck"
@@ -105,3 +106,24 @@ func TestProcessesCardLeavesOutTheIdleOneShot(t *testing.T) {
t.Errorf("card = %q / %q, want 1 of 1 running / postfix-reload", card.Value, card.Sub)
}
}
// An idle one-shot helper is "on demand", not "stopped"; a daemon keeps its
// state word, and a one-shot in trouble keeps its own.
func TestProcessRowOfAnIdleOneShotSaysOnDemand(t *testing.T) {
idle := processRow(health.Process{Name: "postfix-reload", State: "STOPPED", Detail: "Not started", Status: health.StatusOK, OneShot: true})
if idle.State.Label != "on demand" || idle.State.Status != "off" || !strings.Contains(idle.Detail, "not run since the container started") {
t.Errorf("idle one-shot = %+v", idle)
}
ran := processRow(health.Process{Name: "postfix-reload", State: "EXITED", Detail: "Oct 10 08:42 PM", Status: health.StatusOK, OneShot: true})
if ran.State.Label != "on demand" || !strings.HasSuffix(ran.Detail, "last run: Oct 10 08:42 PM") {
t.Errorf("one-shot that ran = %+v", ran)
}
bad := processRow(health.Process{Name: "postfix-reload", State: "FATAL", Detail: "Exited too quickly", Status: health.StatusError, OneShot: true})
if bad.State.Label != "fatal" || bad.State.Status != "error" || bad.Detail != "Exited too quickly" {
t.Errorf("one-shot in trouble = %+v", bad)
}
daemon := processRow(health.Process{Name: "postfix", State: "STOPPED", Detail: "Oct 10 08:42 PM", Status: health.StatusError})
if daemon.State.Label != "stopped" || daemon.State.Status != "error" {
t.Errorf("stopped daemon = %+v", daemon)
}
}