dns-record-collapse: a published record is drawn folded
test / test (push) Waiting to run

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:
mix
2026-10-16 12:34:28 +03:00
co-authored by Claude Opus 5.5
parent fffee9cb5d
commit 51f303c466
7 changed files with 74 additions and 9 deletions
+5
View File
@@ -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} }
+23 -1
View File
@@ -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,
+1 -1
View File
@@ -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."),
+13 -1
View File
@@ -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; }
+10 -5
View File
@@ -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