panel-redesign: links into Domain settings carry no anchor

Follows the design commit before it: the domain page's menu entries
Report address / Rate limit / Export, the DMARC record's note and the
delete page's "Export the domain" lead to /outbound/domains/{id}/settings
as it opens, not scrolled to a box.

  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:57:35 +03:00
co-authored by Claude Opus 5.5
parent 5b1a868ccf
commit c5086510f0
4 changed files with 13 additions and 13 deletions
+3 -3
View File
@@ -105,7 +105,7 @@ func TestNewDomainHasNoReports(t *testing.T) {
// The domain the stand started with has none either. // The domain the stand started with has none either.
has(t, "domain page", pageOf(t, s.h.HandleDomainDetail, "/outbound/domains/1"), has(t, "domain page", pageOf(t, s.h.HandleDomainDetail, "/outbound/domains/1"),
`Report address: <a href="/outbound/domains/1/settings#reports">no reports</a>`) `Report address: <a href="/outbound/domains/1/settings">no reports</a>`)
} }
// Each of the three choices is saved, comes back selected, and is what the // Each of the three choices is saved, comes back selected, and is what the
@@ -130,7 +130,7 @@ func TestDomainReportAddressThreeChoices(t *testing.T) {
has(t, "settings after hosted", settings(), `<option value="hosted" selected>`) has(t, "settings after hosted", settings(), `<option value="hosted" selected>`)
lacks(t, "settings after hosted", settings(), `value="`+hostedForExample+`"`) // the typed field stays empty lacks(t, "settings after hosted", settings(), `value="`+hostedForExample+`"`) // the typed field stays empty
has(t, "domain page after hosted", pageOf(t, s.h.HandleDomainDetail, "/outbound/domains/1"), has(t, "domain page after hosted", pageOf(t, s.h.HandleDomainDetail, "/outbound/domains/1"),
`Report address: <a href="/outbound/domains/1/settings#reports">`+hostedEscaped+`</a>`, `Report address: <a href="/outbound/domains/1/settings">`+hostedEscaped+`</a>`,
`rua=mailto:`+hostedEscaped) `rua=mailto:`+hostedEscaped)
// The ingest allow-list was rebuilt for the new address. // The ingest allow-list was rebuilt for the new address.
if last := maps.got[len(maps.got)-1]; !contains(last.Recipients, hostedForExample) { if last := maps.got[len(maps.got)-1]; !contains(last.Recipients, hostedForExample) {
@@ -145,7 +145,7 @@ func TestDomainReportAddressThreeChoices(t *testing.T) {
} }
has(t, "settings after custom", settings(), `<option value="custom" selected>`, `value="reports@hub.example"`) has(t, "settings after custom", settings(), `<option value="custom" selected>`, `value="reports@hub.example"`)
has(t, "domain page after custom", pageOf(t, s.h.HandleDomainDetail, "/outbound/domains/1"), has(t, "domain page after custom", pageOf(t, s.h.HandleDomainDetail, "/outbound/domains/1"),
`Report address: <a href="/outbound/domains/1/settings#reports">reports@hub.example</a>`, `rua=mailto:reports@hub.example`, `Report address: <a href="/outbound/domains/1/settings">reports@hub.example</a>`, `rua=mailto:reports@hub.example`,
`<h3>Report authorization</h3>`) // the address is on another domain than the senders `<h3>Report authorization</h3>`) // the address is on another domain than the senders
if last := maps.got[len(maps.got)-1]; contains(last.Recipients, hostedForExample) || contains(last.Recipients, "reports@hub.example") { if last := maps.got[len(maps.got)-1]; contains(last.Recipients, hostedForExample) || contains(last.Recipients, "reports@hub.example") {
t.Errorf("the allow-list after choosing another host: %v", last.Recipients) t.Errorf("the allow-list after choosing another host: %v", last.Recipients)
@@ -98,7 +98,7 @@ func TestDomainPageShowsRecordsApplicationsAndConnection(t *testing.T) {
`<td class="sp-mono sp-nowrap"><strong>prod-server</strong></td>`, `alerts@<wbr>example.org`, `200 / h`, `2 IPs`, `<td class="sp-mono sp-nowrap"><strong>prod-server</strong></td>`, `alerts@<wbr>example.org`, `200 / h`, `2 IPs`,
`href="/outbound/domains/1/applications/`+idStr(a.ID)+`">Edit</a>`, `href="/outbound/domains/1/applications/`+idStr(a.ID)+`">Edit</a>`,
`mail.example.org`, `STARTTLS (submission)`, `action="/outbound/domains/1/dns-recheck"`, `mail.example.org`, `STARTTLS (submission)`, `action="/outbound/domains/1/dns-recheck"`,
`href="/outbound/log?domain=example.org"`, `href="/outbound/domains/1/settings#reports"`, `href="/outbound/domains/1/delete"`) `href="/outbound/log?domain=example.org"`, `href="/outbound/domains/1/settings"`, `href="/outbound/domains/1/delete"`)
// The flash the redirects carry. // The flash the redirects carry.
rec := send(s.h.HandleDomainDetail, &globalPrincipal, "GET", "/outbound/domains/1?appsaved=1", s.paths, nil) rec := send(s.h.HandleDomainDetail, &globalPrincipal, "GET", "/outbound/domains/1?appsaved=1", s.paths, nil)
@@ -339,7 +339,7 @@ func TestDeleteConfirmNamesTheApplicationsThatGoWithTheDomain(t *testing.T) {
t.Fatalf("delete confirmation = %d:\n%s", rec.Code, body) t.Fatalf("delete confirmation = %d:\n%s", rec.Code, body)
} }
has(t, "delete", body, `<h1 class="title is-3">Delete example.org</h1>`, `permanently delete its DKIM signing key;`, has(t, "delete", body, `<h1 class="title is-3">Delete example.org</h1>`, `permanently delete its DKIM signing key;`,
`<form method="post" action="/outbound/domains/1/delete">`, `Delete example.org</button>`, `href="/outbound/domains/1/settings#export"`) `<form method="post" action="/outbound/domains/1/delete">`, `Delete example.org</button>`, `href="/outbound/domains/1/settings"`)
lacks(t, "delete", body, `credentials`) lacks(t, "delete", body, `credentials`)
for _, login := range []string{"prod-server", "alerts"} { for _, login := range []string{"prod-server", "alerts"} {
+5 -5
View File
@@ -196,7 +196,7 @@ type DMARCRecordInput struct {
// delivery, so the standing note says how to tighten it. // delivery, so the standing note says how to tighten it.
func DMARCRecord(in DMARCRecordInput) Record { func DMARCRecord(in DMARCRecordInput) Record {
r := Record{Name: "DMARC", Host: in.Host, Type: "TXT", Value: in.Value, r := Record{Name: "DMARC", Host: in.Host, Type: "TXT", Value: in.Value,
Note: Rich("Report address: ", Link(in.SettingsHref+"#reports", in.Source))} Note: Rich("Report address: ", Link(in.SettingsHref, in.Source))}
reports := Rich("reports") reports := Rich("reports")
if in.ReportsHref != "" { if in.ReportsHref != "" {
reports = Rich(Link(in.ReportsHref, "reports")) reports = Rich(Link(in.ReportsHref, "reports"))
@@ -497,9 +497,9 @@ func (p *OutDomain) Menu() SideMenu {
} }
settings := DomainHref(p.id) + "/settings" settings := DomainHref(p.id) + "/settings"
rare := MenuGroup{Label: "Rarely changed", Items: []MenuItem{ rare := MenuGroup{Label: "Rarely changed", Items: []MenuItem{
{Label: "Report address", Icon: "ti-mail-forward", Href: settings + "#reports"}, {Label: "Report address", Icon: "ti-mail-forward", Href: settings},
{Label: "Rate limit", Icon: "ti-gauge", Href: settings + "#limit", Tag: p.limitTag}, {Label: "Rate limit", Icon: "ti-gauge", Href: settings, Tag: p.limitTag},
{Label: "Export", Icon: "ti-file-export", Href: settings + "#export"}, {Label: "Export", Icon: "ti-file-export", Href: settings},
}} }}
if p.canDelete { if p.canDelete {
rare.Items = append(rare.Items, MenuItem{Label: "Delete domain", Icon: "ti-trash", Href: DomainHref(p.id) + "/delete", Danger: true}) rare.Items = append(rare.Items, MenuItem{Label: "Delete domain", Icon: "ti-trash", Href: DomainHref(p.id) + "/delete", Danger: true})
@@ -748,7 +748,7 @@ func NewOutDomainDelete(m Meta, id int64, name string, logins []string) *OutDoma
Confirm: "Delete " + name, Confirm: "Delete " + name,
KeepHref: href, KeepHref: href,
Moving: Box{Icon: "ti-file-export", Title: "Moving it instead?"}, Moving: Box{Icon: "ti-file-export", Title: "Moving it instead?"},
MovingText: Rich(Link(href+"/settings#export", "Export the domain"), MovingText: Rich(Link(href+"/settings", "Export the domain"),
" first. The file carries the key and the application passwords, so the new instance needs no DNS change."), " first. The file carries the key and the application passwords, so the new instance needs no DNS change."),
} }
if n := appsPhrase(len(logins)); n != "" { if n := appsPhrase(len(logins)); n != "" {
+3 -3
View File
@@ -99,7 +99,7 @@ func TestOutDomainRecordsSayWhatDNSHolds(t *testing.T) {
`sp2026._domainkey.example.org`, `<textarea class="textarea" rows="3" readonly>`, `sp2026._domainkey.example.org`, `<textarea class="textarea" rows="3" readonly>`,
`<h3>SPF</h3>`, `<span class="tag is-danger is-light">mismatch</span>`, `v=spf1 ip4:203.0.113.25 ~all`, `<h3>SPF</h3>`, `<span class="tag is-danger is-light">mismatch</span>`, `v=spf1 ip4:203.0.113.25 ~all`,
`In DNS now`, `v=spf1 include:_spf.google.com ~all`, `Shallow check: <code>include:</code> is not followed.`, `In DNS now`, `v=spf1 include:_spf.google.com ~all`, `Shallow check: <code>include:</code> is not followed.`,
`<h3>DMARC</h3>`, `Report address: <a href="/outbound/domains/1/settings#reports">example.org@dmarc.mail.example.org</a>`, `<h3>DMARC</h3>`, `Report address: <a href="/outbound/domains/1/settings">example.org@dmarc.mail.example.org</a>`,
`<code>p=none</code> does not affect delivery.`, `<a href="/outbound/dmarc/domains/1">reports</a>`, `<code>p=none</code> does not affect delivery.`, `<a href="/outbound/dmarc/domains/1">reports</a>`,
) )
if got := strings.Count(out, `In DNS now`); got != 1 { if got := strings.Count(out, `In DNS now`); got != 1 {
@@ -180,7 +180,7 @@ func TestOutDomainMenuFollowsTheViewer(t *testing.T) {
out := renderSignedIn(t, "out-domain", outDomainFixture()) out := renderSignedIn(t, "out-domain", outDomainFixture())
pageHas(t, "Domain", out, pageHas(t, "Domain", out,
`href="#dns"`, `href="#apps"`, `href="#connection"`, `href="/outbound/log?domain=example.org"`, `href="/outbound/dmarc/domains/1"`, `href="#dns"`, `href="#apps"`, `href="#connection"`, `href="/outbound/log?domain=example.org"`, `href="/outbound/dmarc/domains/1"`,
`href="/outbound/domains/1/settings#reports"`, `href="/outbound/domains/1/settings#limit"`, `href="/outbound/domains/1/settings#export"`, `href="/outbound/domains/1/settings"`, `href="/outbound/domains/1/settings"`, `href="/outbound/domains/1/settings"`,
`href="/outbound/domains/1/delete"`, `id="dns"`, `id="apps"`, `id="connection"`, `href="/outbound/domains/1/delete"`, `id="dns"`, `id="apps"`, `id="connection"`,
`class="sp-help" href="/help#dns"`, `class="sp-help" href="/help#apps"`, `class="sp-help" href="/help#dns"`, `class="sp-help" href="/help#apps"`,
) )
@@ -437,7 +437,7 @@ func TestOutDomainDeleteNamesWhatGoesWithIt(t *testing.T) {
`reload OpenDKIM so the domain is no longer signed.`, `Inbound domains are not touched.`, `reload OpenDKIM so the domain is no longer signed.`, `Inbound domains are not touched.`,
`<form method="post" action="/outbound/domains/1/delete">`, `class="button is-danger"`, `Delete example.org</button>`, `<form method="post" action="/outbound/domains/1/delete">`, `class="button is-danger"`, `Delete example.org</button>`,
`href="/outbound/domains/1">Keep it</a>`, `href="/outbound/domains/1">Keep it</a>`,
`<h2>Moving it instead?</h2>`, `<a href="/outbound/domains/1/settings#export">Export the domain</a>`, `<h2>Moving it instead?</h2>`, `<a href="/outbound/domains/1/settings">Export the domain</a>`,
) )
// No applications: nothing to say about them. // No applications: nothing to say about them.