The dns_record partial draws a record whose tag is "published" and that
has no "In DNS now" (Record.Folded) as a <details> whose summary is the
head - name, tag, "Show values" - with the host, type and value behind
it, as the mockup of fffee9c. Every other record stays a <div>, open.
One partial, so the outbound domain's page, the inbound domain's and
Settings › Notification mail change together; panel.css takes the
mockup's rules, help.go the help's sentence word for word.
Re-check DNS posts and redirects to the page, which is drawn from the
fresh check: a record that has just turned right comes back folded. No
page that draws records polls them, so nothing has to keep a fold open.
Tests: the dns_record partial folds only a published record and closes
the block it opened (TestDNSRecordFoldsOnlyWhenPublished); the mismatch
test reads the folded head. go vet + go test (26 packages) and make e2e
on the test server for this tree.
CHANGELOG [Unreleased]; roadmap row done, its section says what was
decided.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,17 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
|
||||
- **A DNS record DNS already holds as shown is folded** (dns-record-collapse):
|
||||
on an outbound domain's page, an inbound domain's and in Server ›
|
||||
Settings › Notification mail, a *published* record is one line - its
|
||||
name, its tag and *Show values*, which opens the host, type and value to
|
||||
copy them again. A record that is missing, weak, wrong or not checked
|
||||
stays open as before, with *In DNS now* where it differs. A `<details>`
|
||||
block, no script; *Re-check DNS* draws the page again, so a record that
|
||||
has just turned right comes back folded.
|
||||
|
||||
## [2.8.0] - 2026-10-16
|
||||
|
||||
The panel signs outgoing mail with DKIM itself, and OpenDKIM - a 2018 beta
|
||||
|
||||
+11
-1
@@ -40,7 +40,7 @@ in `git log` and [CHANGELOG.md](../CHANGELOG.md).
|
||||
| local-time | The panel shows time, and counts its days, in a time zone other than UTC | agreed (2026-10-13; design questions open) | — | — |
|
||||
| notification-mail-recheck | The Notification mail records on Server › Settings can be checked again from where they are shown | agreed (2026-10-15) | — | — |
|
||||
| settings-save-row | Server › Settings: the Save button no longer stands alone under a long right column | agreed (2026-10-15) | — | — |
|
||||
| dns-record-collapse | A DNS record that is right is shown folded: its name and its tag, the values on request | agreed (2026-10-15) | — | — |
|
||||
| dns-record-collapse | A DNS record that is right is shown folded: its name and its tag, the values on request | done 2026-10-16, unreleased | — | — |
|
||||
| go-milter-upstream | Prepare an issue and a PR for go-milter: its header map outlives a message and is unbounded | prepared 2026-10-16, the owner posts | — | [third_party/go-milter/PATCHES.md](../third_party/go-milter/PATCHES.md) |
|
||||
| own-mta-builds | Analysis: build Postfix and OpenDKIM ourselves, patched, instead of Debian's packages | analysed 2026-10-15: no own builds; three rows proposed instead | — | [own-mta-builds-2026-10-15.md](own-mta-builds-2026-10-15.md) |
|
||||
| port25-bare-newline | The inbound relay's port 25 refuses bare newlines in DATA (`smtpd_forbid_bare_newline=normalize`) | shipped `2.6.1` | — | — |
|
||||
@@ -730,6 +730,16 @@ and closes without it under the panel's CSP. Whether a fold remembers being
|
||||
opened across the polls of a page, and whether *Re-check DNS* folds a record
|
||||
that has just turned right, are for the mockup.
|
||||
|
||||
**Done (2026-10-16):** a record whose tag is *published* and that has no
|
||||
*In DNS now* is a `<details>` whose summary is the head: name, tag and
|
||||
*Show values* (the eye of the icon subset - it has no chevron), the
|
||||
values behind it. The head's note is hidden while folded, or a long one
|
||||
(the server's SPF in a half column) wraps the fold to two lines; it shows
|
||||
when opened. The two questions answered themselves: no page that draws
|
||||
records polls them, and *Re-check DNS* posts and draws the page again, so a
|
||||
fold is never kept across a refresh and a record that has just turned
|
||||
right comes back folded.
|
||||
|
||||
## notification-mail-recheck
|
||||
|
||||
**Problem (owner, 2026-10-15):** Server › Settings shows the DNS records of
|
||||
|
||||
@@ -413,6 +413,11 @@ type Record struct {
|
||||
Problem Text
|
||||
}
|
||||
|
||||
// Folded says the record is drawn folded to its head: DNS holds it as shown,
|
||||
// so its values are of use only to copy again. A record that is missing,
|
||||
// weak, wrong or not checked stays open.
|
||||
func (r Record) Folded() bool { return r.Status.Status == "published" && r.InDNS == "" }
|
||||
|
||||
// HostField is the Copy field of the record's host.
|
||||
func (r Record) HostField() CopyField { return CopyField{Label: "Host / name", Value: r.Host} }
|
||||
|
||||
|
||||
@@ -265,7 +265,7 @@ func TestDNSRecordShowsInDNSNowOnlyOnAMismatch(t *testing.T) {
|
||||
Host: "sp2026._domainkey.example.org", Type: "TXT", Value: "v=DKIM1; p=AAA", ValueRows: 3,
|
||||
}))
|
||||
mustContain(t, ok,
|
||||
`<div class="sp-record"><div class="sp-record-head"><h3>DKIM</h3><span class="tag is-success is-light">published</span><small>selector</small></div>`,
|
||||
`<details class="sp-record"><summary class="sp-record-head"><h3>DKIM</h3><span class="tag is-success is-light">published</span><small>selector</small><span class="sp-record-toggle"><i class="ti ti-eye"></i><span>Show values</span><span>Hide values</span></span></summary>`,
|
||||
`<div class="sp-row">`, `<input class="input" readonly value="sp2026._domainkey.example.org">`,
|
||||
`<label class="label">Type</label><div class="control"><input class="input" readonly value="TXT">`,
|
||||
`<textarea class="textarea" rows="3" readonly>v=DKIM1; p=AAA</textarea>`)
|
||||
@@ -281,6 +281,28 @@ func TestDNSRecordShowsInDNSNowOnlyOnAMismatch(t *testing.T) {
|
||||
`<input class="input" readonly value="v=spf1 ip4:203.0.113.25 ~all">`)
|
||||
}
|
||||
|
||||
// Only a record DNS holds as shown is folded; one that needs work stays open
|
||||
// with its values in view, and the fold closes where it opened.
|
||||
func TestDNSRecordFoldsOnlyWhenPublished(t *testing.T) {
|
||||
for _, status := range []string{"mismatch", "fail", "warn", "unknown"} {
|
||||
out := squash(partial(t, "dns_record", Record{Name: "SPF", Status: Tag{Status: status},
|
||||
Host: "example.org", Type: "TXT", Value: "v=spf1 a -all"}))
|
||||
mustContain(t, out, `<div class="sp-record"><div class="sp-record-head"><h3>SPF</h3>`, `value="v=spf1 a -all"`)
|
||||
mustNotContain(t, out, "<details", "<summary", "sp-record-toggle")
|
||||
}
|
||||
ok := squash(partial(t, "dns_record", Record{Name: "MX", Status: Tag{Status: "published"},
|
||||
Host: "example.org", Type: "MX", Value: "10 mail.example.org."}))
|
||||
if !strings.HasPrefix(ok, `<details class="sp-record">`) || !strings.HasSuffix(ok, `</details>`) {
|
||||
t.Errorf("a published record is not one <details> block:\n%s", ok)
|
||||
}
|
||||
if strings.Contains(ok, "<details open") {
|
||||
t.Errorf("a published record is drawn open:\n%s", ok)
|
||||
}
|
||||
if (Record{Status: Tag{Status: "published"}, InDNS: "x"}).Folded() {
|
||||
t.Error("a record with an In DNS now value is folded")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopyFieldSingleLineAndMultiLine(t *testing.T) {
|
||||
one := squash(partial(t, "copy_field", CopyField{Label: "Login", Value: "prod-server", Help: Plain("Keep it.")}))
|
||||
mustContain(t, one,
|
||||
|
||||
@@ -157,7 +157,7 @@ var overviewTopics = []HelpTopic{
|
||||
|
||||
var outboundTopics = []HelpTopic{
|
||||
{ID: HelpDNS, Title: "DNS records", Body: []Text{
|
||||
Rich("DKIM proves the message was signed by this relay, SPF lists the addresses allowed to send for the domain, DMARC tells receivers what to do when both fail and where to report. Publish all three exactly as shown; merge SPF into an existing record rather than adding a second one. A domain's status is the worst of its three records, and results are cached for a few minutes, so use Re-check DNS after publishing. The SPF check is shallow: it looks for this server's address in the record itself and does not follow ",
|
||||
Rich("DKIM proves the message was signed by this relay, SPF lists the addresses allowed to send for the domain, DMARC tells receivers what to do when both fail and where to report. Publish all three exactly as shown; merge SPF into an existing record rather than adding a second one. A domain's status is the worst of its three records, and results are cached for a few minutes, so use Re-check DNS after publishing. A record DNS already holds as shown is folded to its name and status; Show values opens it to copy them again. The SPF check is shallow: it looks for this server's address in the record itself and does not follow ",
|
||||
Code("include:"), " or ", Code("redirect="), ", so a record that relies on them is reported as a warning, not a failure. ",
|
||||
Code("p=none"), " does not affect delivery; tighten to ", Code("quarantine"), ", then ", Code("reject"),
|
||||
", once the reports look clean. A domain's report address is set in its Domain settings."),
|
||||
|
||||
@@ -269,6 +269,17 @@ code { background: var(--sp-well); color: var(--sp-ink); border-radius: 3px; pad
|
||||
.sp-record-head { display: flex; align-items: center; gap: .6rem; margin-bottom: .75rem; }
|
||||
.sp-record-head h3 { font-size: 1rem; font-weight: 600; margin: 0; }
|
||||
.sp-record-head small { margin-left: auto; color: var(--sp-muted); text-align: right; }
|
||||
/* A record DNS already holds as shown is folded to its head: a <details>
|
||||
whose summary is the head, opened without script. */
|
||||
details.sp-record > summary { list-style: none; cursor: pointer; }
|
||||
details.sp-record > summary::-webkit-details-marker { display: none; }
|
||||
details.sp-record:not([open]) > summary { margin-bottom: 0; }
|
||||
details.sp-record:not([open]) > summary small { display: none; }
|
||||
details.sp-record[open] .sp-record-head small + .sp-record-toggle { margin-left: 0; }
|
||||
.sp-record-toggle { margin-left: auto; display: inline-flex; align-items: center; gap: .3rem; color: var(--sp-brick); font-size: var(--sp-fs-s); white-space: nowrap; }
|
||||
.sp-record-toggle span + span { display: none; }
|
||||
details.sp-record[open] .sp-record-toggle span { display: none; }
|
||||
details.sp-record[open] .sp-record-toggle span + span { display: inline; }
|
||||
.sp-record .label, .sp-form .label { font-weight: 500; color: var(--sp-ink-2); margin-bottom: .25rem; }
|
||||
.sp-record .label { font-weight: 400; color: var(--sp-muted); }
|
||||
.input[readonly], .textarea[readonly] { font-family: var(--sp-mono); font-size: var(--sp-fs-s); background: var(--sp-well); box-shadow: none; }
|
||||
@@ -324,7 +335,8 @@ code { background: var(--sp-well); color: var(--sp-ink); border-radius: 3px; pad
|
||||
.sp-health { grid-template-columns: repeat(2, 1fr); }
|
||||
.sp-row, .sp-pair { grid-template-columns: 1fr; }
|
||||
.sp-record-head { flex-wrap: wrap; }
|
||||
.sp-record-head small { margin-left: 0; text-align: left; flex-basis: 100%; }
|
||||
.sp-record-head small { margin-left: 0; text-align: left; flex-basis: 100%; order: 1; }
|
||||
details.sp-record[open] .sp-record-head small + .sp-record-toggle { margin-left: auto; }
|
||||
.sp-postmark { display: none; }
|
||||
.sp-cards { display: flex; flex-direction: column; padding-inline: .75rem; }
|
||||
.sp-cards > .column { display: contents; }
|
||||
|
||||
@@ -131,10 +131,14 @@
|
||||
{{define "health_card"}}<a href="{{.Href}}"{{if eq .Level "warn"}} class="sp-warn"{{else if eq .Level "fail"}} class="sp-fail"{{end}}><span class="sp-h-icon"><i class="ti {{.Icon}}"></i></span><span><span class="sp-h-name">{{.Name}}</span><span class="sp-h-value">{{.Value}}</span><span class="sp-h-sub">{{.Sub}}</span></span></a>{{end}}
|
||||
|
||||
{{/* dns_record is one DNS record (input: Record): its status, what to publish
|
||||
with a Copy button, and "In DNS now" only when it differs. */}}
|
||||
{{define "dns_record"}}<div class="sp-record">
|
||||
<div class="sp-record-head"><h3>{{.Name}}</h3>{{if .Status.Status}}{{template "tag" .Status}}{{end}}{{if .Note}}<small>{{template "inline" .Note}}</small>{{end}}</div>
|
||||
{{if .Host}}<div class="sp-row">
|
||||
with a Copy button, and "In DNS now" only when it differs. A record DNS
|
||||
already holds as shown is Folded: a <details> whose summary is the head,
|
||||
the values behind Show values. */}}
|
||||
{{define "dns_record"}}{{if .Folded}}<details class="sp-record">
|
||||
<summary class="sp-record-head">{{template "dns_record_head" .}}<span class="sp-record-toggle"><i class="ti ti-eye"></i><span>Show values</span><span>Hide values</span></span></summary>
|
||||
{{else}}<div class="sp-record">
|
||||
<div class="sp-record-head">{{template "dns_record_head" .}}</div>
|
||||
{{end}}{{if .Host}}<div class="sp-row">
|
||||
{{template "copy_field" .HostField}}
|
||||
<div class="field"><label class="label">Type</label><div class="control"><input class="input" readonly value="{{.Type}}"></div></div>
|
||||
</div>
|
||||
@@ -143,7 +147,8 @@
|
||||
<div class="control"><input class="input is-danger" readonly value="{{.InDNS}}"></div>
|
||||
{{if .Problem}}<p class="help is-danger">{{template "inline" .Problem}}</p>
|
||||
{{end}}</div>
|
||||
{{end}}</div>{{end}}
|
||||
{{end}}{{if .Folded}}</details>{{else}}</div>{{end}}{{end}}
|
||||
{{define "dns_record_head"}}<h3>{{.Name}}</h3>{{if .Status.Status}}{{template "tag" .Status}}{{end}}{{if .Note}}<small>{{template "inline" .Note}}</small>{{end}}{{end}}
|
||||
|
||||
{{/* record_open starts the padded block a record sits in and record_close ends
|
||||
it, for what is not a DNS record but is laid out like one: the fields of a
|
||||
|
||||
Reference in New Issue
Block a user