Decided by the owner on 2026-10-13 (in place of preflight), taken up on
2026-10-16; mockups b2dcefc, cc33890, 0feaa55 (his: the greeting on a row
of its own, the name check a tag beside Names).
- The certificate: health.Certificate keeps its names (DNS names, or the
CN without any) and ForHost holds it to SELFPOST_HOSTNAME as a TLS
client does - VerifyHostname over the DNS names, the CN only without
any, as OpenSSL's X509_check_host and so a sending Postfix still take it
(src/tls/tls_client.c:439, 513). A certificate for another name fails;
the tag beside Names says matches / not for the hostname.
- The greeting: postconf -x -h smtpd_banner, read once at the panel's
start like the served certificate's path (main.cf is not rewritten
after), is what smtpd writes after "220 " (src/smtpd/smtpd.c:5979). A
first word other than the hostname warns, with what to set. A fact may
now take the whole row (Fact.Wide, sp-wide) and carry a tag beside its
label (Fact.LabelTag); both on the kit page.
- The proxy: auth.Module.Seen says the address the panel counts sign-ins
against, the peer, and whether X-Forwarded-For was read or left unread
for an untrusted peer; the Configuration card shows "Your address" and,
in the second case, warns that every visitor shares one sign-in limit
and names the /32 (or /128) to put in TRUSTED_PROXY_CIDR.
Tests: TestCertificateForHost (CN-only, DNS names, a wildcard, the CN
ignored beside DNS names), TestSeenAddress, TestHealthSaysWhereItSeesTheViewer,
TestHealthGreetingAndCertificateName; e2e reads the stand's right state on
Health (matches, "220 mail.e2e.test ESMTP", a direct client). go vet + go
test (26 packages) and make e2e on the test server for this tree.
Docs: help (word for word with the mockup), guide § Overview and Health,
CHANGELOG [Unreleased] § Added, roadmap row done.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The panel's signer is the only one. Gone: the opendkim package and its
configuration, the syslog-relay and opendkim programs, /dev/log and
/run/opendkim, the reload by USR1 through supervisord
(internal/supervisor/supervisor.go), internal/syslogrelay, the port of
OpenDKIM's From: parser (internal/milter/mailparse.go and its probe,
harness and test/upstream scripts), OPENDKIM_SOCKET and OPENDKIM_DIR.
Keys: internal/domain/keys.go (Keys, NewKeys, DKIM_DIR=/data/dkim) keeps
the keys and writes a selector table, /data/dkim/selectors ("<name>
<selector>"), which the signer reads in place of the KeyTable - it reads
no database, as the sender check reads none. The table is written before
the signer serves (EnsureServerSigned moved there from the notifier).
Keys and table 0600, directories 0700, the panel's. The entrypoint copies
/data/opendkim/keys once, as the panel user, into /data/dkim.new renamed
when whole, and stops the start if the copy fails; /data/opendkim is left
for a rollback to 2.7.x.
Sender check: one RFC 5322 address as a mail program reads it, held
against the bindings; the 256-byte refusal and the texts naming OpenDKIM
go. Allowed() folded the domain through strings.ToLower, which makes the
Kelvin sign a "k" - the comparison with OpenDKIM's reading had hidden it;
it folds ASCII only now.
Health: two dkim-signer rows (applications, notifications) in Milter
sockets, opendkim out of the liveness set; the card no longer takes any
row with a note for the spam filter. Texts as in the design commit.
KILL stays in deploy/docker-compose.yml: measured on the test server,
root without it cannot signal a process of another uid - supervisord
stopping the panel, Postfix's master its children. The plan's "only use
was the reload" was wrong; the comment now gives the real uses.
e2e: one signature, the panel's, verified by dkimpy; the /data/dkim modes;
the OpenDKIM log and process checks, testNeverSignedForAnother and the
cited opendkim, libopendkim11 and libmilter1.0.1 go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
M14, M15: queryEach gives each resolver 2 s within the check's context and
asks the one that answered last first; an address or PTR lookup not
answered is Unknown, an answered mismatch beside it still an error;
Checker.Server keeps the last answer's addresses, so SPF does not go Unknown
with it. Notifications report faults only, now tested with SPF unknown
alone and beside a wrong DKIM.
DMARC reports: tests of malformed XML, gzip and zip bombs, a message over
the limit, the service's Resync, hosted suggestion and validation. The
finding: both limits cut silently and an oversized report failed as
damaged; readAtMost refuses it for its size (MaxMessage 10 MiB, moved into
IngestMessage).
healthReadings replaces map[string]any for Health and Overview.
L5: an inbound upstream has one TLS mode - a second domain to the same
[host]:port with another mode is refused, naming the first; the TLS policy
map holds each next hop once.
Tested on the test server: go vet + go test of 25 packages, make e2e (all
pass, 631 s) on this tree.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follows the design commit before it: Machine, Processes, TLS certificate, Milter sockets and Reverse DNS lead to /server/health as it opens; so does the link in the Needs attention line, which takes its address from the card.
go vet ./internal/web/...: ok
go test -count=1 ./internal/web/...: ok
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Seen by the owner on the test server, on real data (2026-10-10).
- Overview's Processes card said "7 of 7 running" with six programs
running and postfix-reload idle. The card counts the programs that
are meant to stay up; a one-shot helper (health.Process.OneShot) is
left out of both numbers and named only when it is in trouble.
- The Milter sockets card clipped its second line ("OpenDKIM,
send-log,...") once the spam filter was the third socket. The line
names the sockets that do not answer when some do not; with all
answering it is their names, or "incl. the spam filter" - the
mockup's words - when the filter is among them.
- A real 2048-bit DKIM value did not fit the three rows of its field
and scrolled inside it. The field is as tall as the value needs
(3 to 12 rows).
The fixtures had a shortened key, two sockets and no one-shot program,
so none of this showed in the evidence screenshots.
go test -count=1 ./internal/health/... ./internal/web/...: ok
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stage 2, step 2. Written by Sonnet, reviewed by Opus.
- Four pages named after their mockups: overview, health, account,
settings, each with a typed page struct (view/overview.go, health.go,
account.go), a fixture and tests. The engine page "status" is gone
(status.html, status_body.html deleted); "settings" is Server >
Settings only. status and settings are off legacy_pages.txt.
- Overview: the stamp and its sentence are derived from the six cards
(the worst card sets the level and the lead names the cards that are
not fine); the outbound and inbound domain tables are built from the
existing service calls. Without the inbound relay the outbound box
takes the full width.
- Both pages refresh themselves as the old Status page did. Health
polls its first row of boxes and swaps the second out of band.
Overview polls its Server health box and the page head follows out of
band (GET /overview/fragment), so stamp, sentence and time move with
the cards.
- Kit, attributes only (no class, no CSS, markup unchanged when empty):
Box.Poll and Head.ID / Head.OOB.
- healthCards: the Queue card says "Empty" / "N queued" instead of
postqueue's line, which was clipped in a card; Processes and Milter
sockets carry a sub line. OverallStatus / OverallHeading are gone.
- Handlers pass typed data; routes, POST targets, field names,
validation and authorization are unchanged.
States the mockups do not draw, all from existing partials: errors and
successes as the kit flash above the boxes; Health with an unreadable
process list, a certificate without facts, a missing host name; Account
with the report authorization not published / not found / not checked
and with hosted reports off; empty domain tables as empty_state.
Differences from the mockups, for the owner:
- Overview title is one of "Everything is running normally." /
"Running, with warnings." / "A component needs attention.", not
"Mail is flowing..." (the checks do not prove that); the note says
"Updated HH:MM:SS UTC" without an interval, Health's lead says
"Refreshes automatically" (panel.js polls at 5 s active, 30 s idle);
- one Queue card ("outbound"), two milter sockets: what this build has;
- the days column is the stats window, min(30, retention);
- Health: no "check port 80" paragraph (the certificate's own detail
says what to check); a failing socket shows its detail under the path;
figures come from the sampler ("12%", "MiB");
- Account: the DMARC form keeps the address field for "Another
address" (hidden unless chosen); the count "N of M now" is the
domains following this user's default;
- Settings: Level 2 links the domain list only;
- /help#checks and /help#dmarc open Help at the top until the Help step.
Evidence
- docs/assets/panel-redesign/evidence/overview.png, health.png,
account.png, settings.png: 1440 px wide, full height, headless Edge,
real templates served locally with their fixtures - NOT from the
container (no Docker on this machine).
- panel/audit.js on the four pages at 1440 and 1280: 612 texts
checked, problems: [].
- Polling was exercised in a browser against a local fixture server
with document.hidden overridden (the pane was hidden): one element
per id after a swap, no leftover hx-swap-oob, about one request per
5 s. The first request of a visible tab was not observed.
- go test -count=1 ./internal/web/...
ok internal/web
ok internal/web/auth
ok internal/web/handlers
ok internal/web/validate
ok internal/web/view
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>