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
@@ -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)