panel: pair domain detail cards like Status (SPF | DMARC)
test / test (push) Has been cancelled

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-12 15:33:41 +03:00
parent 1d30605533
commit 44e79c4120
3 changed files with 264 additions and 245 deletions
+4
View File
@@ -7,6 +7,10 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
### Changed ### Changed
- panel: the domain detail page is wide with paired cards (DKIM ‖ DNS status;
SPF ‖ DMARC; sending settings ‖ add application; rate limit ‖ export).
Applications and Danger stay full-width; SPF and DMARC are separate cards;
section blurbs are shorter.
- panel: page URLs, browser titles, and headings are aligned — **Settings** is - panel: page URLs, browser titles, and headings are aligned — **Settings** is
now `/settings` (legacy `/account` redirects with 308); the domains list title now `/settings` (legacy `/account` redirects with 308); the domains list title
is `SelfPost — domains`; Status, Users, and user create/edit titles match their is `SelfPost — domains`; Status, Users, and user create/edit titles match their
+233 -244
View File
@@ -1,3 +1,7 @@
{{/* Wide so the .split pairs fill the column rather than the 48rem reading
measure (same pattern as Status). */}}
{{define "wide"}}wide{{end}}
{{define "content"}} {{define "content"}}
<h1>{{.Domain.Name}}</h1> <h1>{{.Domain.Name}}</h1>
@@ -9,8 +13,8 @@
{{if .NewCred}} {{if .NewCred}}
<div class="card credential" id="new-credential"> <div class="card credential" id="new-credential">
<h2>New application password</h2> <h2>New application password</h2>
<p class="muted">This password is shown <strong>once only</strong> and is not <p class="muted">Shown <strong>once only</strong> and not stored. Copy it now
stored. Copy it now — if it is lost, regenerate a new one.</p> — if it is lost, regenerate a new one.</p>
<label>Login</label> <label>Login</label>
<div class="code-row"> <div class="code-row">
<span class="code">{{.NewCred.Login}}</span> <span class="code">{{.NewCred.Login}}</span>
@@ -24,202 +28,197 @@
</div> </div>
{{end}} {{end}}
<div class="card" id="dkim"> <div class="split">
<h2>DKIM DNS record</h2> <div class="card" id="dkim">
<p class="muted">Publish this TXT record in the DNS for <strong>{{.Domain.Name}}</strong>. <h2>DKIM DNS record</h2>
It is not a secret and can be viewed at any time.</p> <p class="muted">Publish this TXT for <strong>{{.Domain.Name}}</strong>.
It is not a secret.</p>
<label>Host / name</label> <label>Host / name</label>
<div class="code-row"> <div class="code-row">
<span class="code">{{.Record.Name}}</span> <span class="code">{{.Record.Name}}</span>
<button type="button" class="copy">Copy</button> <button type="button" class="copy">Copy</button>
</div>
<label>Type</label>
<span class="code">TXT</span>
<label>Value</label>
<div class="code-row">
<span class="code">{{.Record.Value}}</span>
<button type="button" class="copy">Copy</button>
</div>
<p class="muted">Mail is signed with selector <strong>{{.Domain.DKIMSelector}}</strong>.</p>
</div>
<div class="card" id="spf-dmarc">
<h2>SPF and DMARC records</h2>
<p class="muted">These two are not generated the way the DKIM record above is —
they are policy, and the domain may already publish an SPF record for other
senders. SelfPost is a send-only relay — most operators have no inbox on the
sending domain, so the suggested DMARC record omits <code>rua=</code> unless a
report address is configured below or in <a href="/settings">Settings</a>.
Publish both as TXT records.</p>
<label>SPF — host / name</label>
<div class="code-row">
<span class="code">{{.Domain.Name}}</span>
<button type="button" class="copy">Copy</button>
</div>
<label>SPF — value</label>
<div class="code-row">
<span class="code">{{.SPFExample}}</span>
<button type="button" class="copy">Copy</button>
</div>
<p class="muted">A domain may have only one SPF record. If it already has one,
do not add a second — add this server's mechanism to the existing record
instead, before its <code>all</code> term.</p>
<form method="post" action="/domains/{{.Domain.ID}}/dmarc">
<label for="dmarc_rua_mode">DMARC aggregate reports (rua=)</label>
<select id="dmarc_rua_mode" name="dmarc_rua_mode">
<option value="inherit"{{if eq .DMARCRuaMode "inherit"}} selected{{end}}>Same as Settings{{if .ProfileDMARCEmail}} ({{.ProfileDMARCEmail}}){{end}}</option>
<option value="none"{{if eq .DMARCRuaMode "none"}} selected{{end}}>No aggregate reports</option>
<option value="custom"{{if eq .DMARCRuaMode "custom"}} selected{{end}}>Custom address</option>
</select>
<label for="dmarc_rua_email">Custom report address</label>
<input id="dmarc_rua_email" name="dmarc_rua_email" type="email"
autocapitalize="none" spellcheck="false" value="{{.DMARCRuaCustom}}"
placeholder="reports@your-mail-domain.com">
<button type="submit">Save DMARC report settings</button>
</form>
<label>DMARC — host / name</label>
<div class="code-row">
<span class="code">{{.DMARCName}}</span>
<button type="button" class="copy">Copy</button>
</div>
<label>DMARC — value{{if eq .DMARCSource "settings"}} <span class="muted">(from Settings)</span>{{else if eq .DMARCSource "custom"}} <span class="muted">(custom)</span>{{else if eq .DMARCSource "none"}} <span class="muted">(no reports)</span>{{end}}</label>
<div class="code-row">
<span class="code">{{.DMARCExample}}</span>
<button type="button" class="copy">Copy</button>
</div>
{{if .SameDomainRUA}}
<p class="error">The report address is on this sending domain. SelfPost does
not receive inbound mail — use a mailbox elsewhere or wait for in-panel report
reception in a future release.</p>
{{end}}
{{if .NeedsReportAuth}}
<label>Report authorization — host / name</label>
<div class="code-row">
<span class="code">{{.ReportAuthName}}</span>
<button type="button" class="copy">Copy</button>
</div>
<label>Report authorization — value</label>
<div class="code-row">
<span class="code">{{.ReportAuthValue}}</span>
<button type="button" class="copy">Copy</button>
</div>
{{end}}
<p class="muted"><code>p=none</code> changes nothing about delivery. Omit
<code>rua=</code> on a send-only domain, or point it at a mailbox that
receives inbound mail. Tighten to <code>p=quarantine</code> and then
<code>p=reject</code> once aggregate reports look clean.</p>
</div>
<div class="card" id="dns-status">
<h2>DNS status <span class="st st-{{.DNS.Overall}}">{{.DNS.Overall}}</span></h2>
<p class="muted">What DNS publishes for <strong>{{.Domain.Name}}</strong> right
now, checked against the key this server signs with. Results are cached for a
few minutes — after publishing a record, use <em>Re-check</em>.</p>
<label>DKIM <span class="st st-{{.DNS.DKIM.Status}}">{{.DNS.DKIM.Status}}</span></label>
<p class="{{if eq .DNS.DKIM.Status "ok"}}muted{{else}}error{{end}}">{{.DNS.DKIM.Detail}}</p>
{{if .DNS.DKIM.Records}}<span class="code">{{range .DNS.DKIM.Records}}{{.}}
{{end}}</span>{{end}}
<label>SPF <span class="st st-{{.DNS.SPF.Status}}">{{.DNS.SPF.Status}}</span></label>
<p class="{{if eq .DNS.SPF.Status "ok"}}muted{{else}}error{{end}}">{{.DNS.SPF.Detail}}</p>
{{if .DNS.SPF.Records}}<span class="code">{{range .DNS.SPF.Records}}{{.}}
{{end}}</span>{{end}}
<p class="muted">The SPF check is deliberately shallow: it looks for a
mechanism that literally covers this server's address and does not follow
<code>include:</code> or <code>redirect=</code>, so a record that authorises
the server through an include is reported as “cannot tell”, not as a failure.</p>
<label>DMARC <span class="st st-{{.DNS.DMARC.Status}}">{{.DNS.DMARC.Status}}</span></label>
<p class="{{if eq .DNS.DMARC.Status "ok"}}muted{{else}}error{{end}}">{{.DNS.DMARC.Detail}}</p>
{{if .DNS.DMARC.Records}}<span class="code">{{range .DNS.DMARC.Records}}{{.}}
{{end}}</span>{{end}}
{{if .DNS.DMARCReportAuth.Status}}
<label>Report authorization <span class="st st-{{.DNS.DMARCReportAuth.Status}}">{{.DNS.DMARCReportAuth.Status}}</span></label>
<p class="{{if eq .DNS.DMARCReportAuth.Status "ok"}}muted{{else}}error{{end}}">{{.DNS.DMARCReportAuth.Detail}}</p>
{{if .DNS.DMARCReportAuth.Records}}<span class="code">{{range .DNS.DMARCReportAuth.Records}}{{.}}
{{end}}</span>{{end}}
{{end}}
<form class="inline" method="post" action="/domains/{{.Domain.ID}}/dns-recheck">
<button type="submit">Re-check</button>
</form>
</div>
<div class="card" id="settings">
<h2>Sending server settings</h2>
<p class="muted">Point the mail client or script at these settings and
authenticate with an application login and password from the
<strong>Applications</strong> section below. They are the same for every
domain on this server.</p>
<label>Server</label>
<div class="code-row">
<span class="code">{{.Hostname}}</span>
<button type="button" class="copy">Copy</button>
</div>
<label>Port and encryption</label>
<span class="code">465 — SSL/TLS (implicit){{if .SubmissionEnabled}}
587 — STARTTLS (submission){{end}}</span>
<p class="muted">Authentication is required on every port. The username is the
application's login (see the table below) and the password is the one shown
once when that application was created or its password regenerated — if it was
lost, generate a new one.</p>
</div>
{{/* Create form above the list, the same order the domains page uses for
"Add a sending domain" above "Domains". */}}
<div class="card" id="add-application">
<h2>Add an application</h2>
<form method="post" action="/domains/{{.Domain.ID}}/applications">
<label for="login">Login</label>
<input id="login" name="login" type="text" placeholder="prod-server"
autocomplete="off" autocapitalize="none" spellcheck="false"
value="{{.FormLogin}}" required>
<label for="mode">Address mode</label>
<select id="mode" name="mode" data-list-mode="{{.List}}">
<option value="{{.Wildcard}}" {{if eq .FormMode .Wildcard}}selected{{end}}>Any address of the domain</option>
<option value="{{.List}}" {{if eq .FormMode .List}}selected{{end}}>Specific addresses (list)</option>
</select>
<div data-addresses>
<label for="addresses">Addresses (one per line or comma-separated)</label>
<textarea id="addresses" name="addresses" rows="3"
placeholder="alerts@{{.Domain.Name}}">{{.FormAddrs}}</textarea>
</div> </div>
{{if .Error}}<p class="error">{{.Error}}</p>{{end}} <label>Type</label>
<button type="submit">Create application</button> <span class="code">TXT</span>
</form>
<p class="muted">A strong password is generated and shown once. The login must <label>Value</label>
be unique across all domains and may contain letters, digits, '.', '-' and '_'.</p> <div class="code-row">
<span class="code">{{.Record.Value}}</span>
<button type="button" class="copy">Copy</button>
</div>
<p class="muted">Signed with selector <strong>{{.Domain.DKIMSelector}}</strong>.</p>
</div>
<div class="card" id="dns-status">
<h2>DNS status <span class="st st-{{.DNS.Overall}}">{{.DNS.Overall}}</span></h2>
<p class="muted">Cached a few minutes — use <em>Re-check</em> after
publishing.</p>
<label>DKIM <span class="st st-{{.DNS.DKIM.Status}}">{{.DNS.DKIM.Status}}</span></label>
<p class="{{if eq .DNS.DKIM.Status "ok"}}muted{{else}}error{{end}}">{{.DNS.DKIM.Detail}}</p>
{{if .DNS.DKIM.Records}}<span class="code">{{range .DNS.DKIM.Records}}{{.}}
{{end}}</span>{{end}}
<label>SPF <span class="st st-{{.DNS.SPF.Status}}">{{.DNS.SPF.Status}}</span></label>
<p class="{{if eq .DNS.SPF.Status "ok"}}muted{{else}}error{{end}}">{{.DNS.SPF.Detail}}</p>
{{if .DNS.SPF.Records}}<span class="code">{{range .DNS.SPF.Records}}{{.}}
{{end}}</span>{{end}}
<p class="muted">Shallow check: literal address only, no
<code>include:</code> / <code>redirect=</code>.</p>
<label>DMARC <span class="st st-{{.DNS.DMARC.Status}}">{{.DNS.DMARC.Status}}</span></label>
<p class="{{if eq .DNS.DMARC.Status "ok"}}muted{{else}}error{{end}}">{{.DNS.DMARC.Detail}}</p>
{{if .DNS.DMARC.Records}}<span class="code">{{range .DNS.DMARC.Records}}{{.}}
{{end}}</span>{{end}}
{{if .DNS.DMARCReportAuth.Status}}
<label>Report authorization <span class="st st-{{.DNS.DMARCReportAuth.Status}}">{{.DNS.DMARCReportAuth.Status}}</span></label>
<p class="{{if eq .DNS.DMARCReportAuth.Status "ok"}}muted{{else}}error{{end}}">{{.DNS.DMARCReportAuth.Detail}}</p>
{{if .DNS.DMARCReportAuth.Records}}<span class="code">{{range .DNS.DMARCReportAuth.Records}}{{.}}
{{end}}</span>{{end}}
{{end}}
<form class="inline" method="post" action="/domains/{{.Domain.ID}}/dns-recheck">
<button type="submit">Re-check</button>
</form>
</div>
</div>
<div class="split">
<div class="card" id="spf">
<h2>SPF record</h2>
<p class="muted">Suggested TXT. If the domain already has SPF, merge this
server's mechanism into it — do not add a second record.</p>
<label>Host / name</label>
<div class="code-row">
<span class="code">{{.Domain.Name}}</span>
<button type="button" class="copy">Copy</button>
</div>
<label>Value</label>
<div class="code-row">
<span class="code">{{.SPFExample}}</span>
<button type="button" class="copy">Copy</button>
</div>
</div>
<div class="card" id="dmarc">
<h2>DMARC record</h2>
<p class="muted">Policy TXT. SelfPost is send-only — omit <code>rua=</code>
or point it at a mailbox elsewhere (<a href="/settings">Settings</a> default
or custom below).</p>
<form method="post" action="/domains/{{.Domain.ID}}/dmarc">
<label for="dmarc_rua_mode">Aggregate reports (rua=)</label>
<select id="dmarc_rua_mode" name="dmarc_rua_mode">
<option value="inherit"{{if eq .DMARCRuaMode "inherit"}} selected{{end}}>Same as Settings{{if .ProfileDMARCEmail}} ({{.ProfileDMARCEmail}}){{end}}</option>
<option value="none"{{if eq .DMARCRuaMode "none"}} selected{{end}}>No aggregate reports</option>
<option value="custom"{{if eq .DMARCRuaMode "custom"}} selected{{end}}>Custom address</option>
</select>
<label for="dmarc_rua_email">Custom report address</label>
<input id="dmarc_rua_email" name="dmarc_rua_email" type="email"
autocapitalize="none" spellcheck="false" value="{{.DMARCRuaCustom}}"
placeholder="reports@your-mail-domain.com">
<button type="submit">Save DMARC report settings</button>
</form>
<label>Host / name</label>
<div class="code-row">
<span class="code">{{.DMARCName}}</span>
<button type="button" class="copy">Copy</button>
</div>
<label>Value{{if eq .DMARCSource "settings"}} <span class="muted">(from Settings)</span>{{else if eq .DMARCSource "custom"}} <span class="muted">(custom)</span>{{else if eq .DMARCSource "none"}} <span class="muted">(no reports)</span>{{end}}</label>
<div class="code-row">
<span class="code">{{.DMARCExample}}</span>
<button type="button" class="copy">Copy</button>
</div>
{{if .SameDomainRUA}}
<p class="error">The report address is on this sending domain. SelfPost does
not receive inbound mail — use a mailbox elsewhere or wait for in-panel report
reception in a future release.</p>
{{end}}
{{if .NeedsReportAuth}}
<label>Report authorization — host / name</label>
<div class="code-row">
<span class="code">{{.ReportAuthName}}</span>
<button type="button" class="copy">Copy</button>
</div>
<label>Report authorization — value</label>
<div class="code-row">
<span class="code">{{.ReportAuthValue}}</span>
<button type="button" class="copy">Copy</button>
</div>
{{end}}
<p class="muted"><code>p=none</code> does not affect delivery. Tighten to
<code>p=quarantine</code> then <code>p=reject</code> once reports look clean.</p>
</div>
</div>
<div class="split">
<div class="card" id="settings">
<h2>Sending server settings</h2>
<p class="muted">Same for every domain. Authenticate with an application
login from below.</p>
<label>Server</label>
<div class="code-row">
<span class="code">{{.Hostname}}</span>
<button type="button" class="copy">Copy</button>
</div>
<label>Port and encryption</label>
<span class="code">465 — SSL/TLS (implicit){{if .SubmissionEnabled}}
587 — STARTTLS (submission){{end}}</span>
<p class="muted">Auth required on every port. The password is shown once at
create or regenerate.</p>
</div>
{{/* Create form beside settings, mirroring "Add a sending domain" above the
domains list. */}}
<div class="card" id="add-application">
<h2>Add an application</h2>
<form method="post" action="/domains/{{.Domain.ID}}/applications">
<label for="login">Login</label>
<input id="login" name="login" type="text" placeholder="prod-server"
autocomplete="off" autocapitalize="none" spellcheck="false"
value="{{.FormLogin}}" required>
<label for="mode">Address mode</label>
<select id="mode" name="mode" data-list-mode="{{.List}}">
<option value="{{.Wildcard}}" {{if eq .FormMode .Wildcard}}selected{{end}}>Any address of the domain</option>
<option value="{{.List}}" {{if eq .FormMode .List}}selected{{end}}>Specific addresses (list)</option>
</select>
<div data-addresses>
<label for="addresses">Addresses (one per line or comma-separated)</label>
<textarea id="addresses" name="addresses" rows="3"
placeholder="alerts@{{.Domain.Name}}">{{.FormAddrs}}</textarea>
</div>
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
<button type="submit">Create application</button>
</form>
<p class="muted">Password shown once. Login unique across domains; letters,
digits, '.', '-' and '_'.</p>
</div>
</div> </div>
<div class="card" id="applications"> <div class="card" id="applications">
<h2>Applications</h2> <h2>Applications</h2>
<p class="muted">Each application is a SASL login/password an app or script <p class="muted">SASL logins for this domain — wildcard (*@domain) or a fixed
uses to send mail as this domain. A login may send from any address of the address list.</p>
domain (<em>wildcard</em>) or only from a fixed list of addresses.</p>
{{if .Apps}} {{if .Apps}}
<ul class="apps"> <ul class="apps">
@@ -298,79 +297,69 @@
{{end}} {{end}}
</div> </div>
<div class="card" id="rate-limit"> <div class="split">
<h2>Sending rate limit (domain)</h2> <div class="card" id="rate-limit">
<p class="muted">Optional level-2 limit (spec 7.4): cap how many messages this <h2>Sending rate limit (domain)</h2>
domain may send from its expected client IP(s) within a time window, summed <p class="muted">Optional level-2 cap across this domain's applications from
across all its applications. It counts messages — one message to many the listed client IPs. Empty IP list = inactive (level-1 only).</p>
recipients counts once. Leave the IP list empty to disable it and rely only on
the global level-1 limit. Applications that send from changing IPs should be
left unbound here.</p>
<p class="muted">Status: <p class="muted">Status:
{{if .DomainHasRL}}<strong>active</strong>{{else}}inactive (level-1 only){{end}}.</p> {{if .DomainHasRL}}<strong>active</strong>{{else}}inactive (level-1 only){{end}}.</p>
<form method="post" action="/domains/{{.Domain.ID}}/ratelimit"> <form method="post" action="/domains/{{.Domain.ID}}/ratelimit">
<label for="d_ips">Expected client IPs (one per line or comma-separated)</label> <label for="d_ips">Expected client IPs (one per line or comma-separated)</label>
<textarea id="d_ips" name="allowed_ips" rows="2" <textarea id="d_ips" name="allowed_ips" rows="2"
placeholder="203.0.113.10">{{.DomainRLIPs}}</textarea> placeholder="203.0.113.10">{{.DomainRLIPs}}</textarea>
<label for="d_max">Message limit</label> <label for="d_max">Message limit</label>
<input id="d_max" name="max_messages" type="number" min="1" <input id="d_max" name="max_messages" type="number" min="1"
value="{{.DomainRLMax}}" placeholder="1000"> value="{{.DomainRLMax}}" placeholder="1000">
<label for="d_win">Window (seconds)</label> <label for="d_win">Window (seconds)</label>
<input id="d_win" name="window_seconds" type="number" min="1" value="{{.DomainRLWin}}"> <input id="d_win" name="window_seconds" type="number" min="1" value="{{.DomainRLWin}}">
<button type="submit">Save limit</button> <button type="submit">Save limit</button>
</form> </form>
{{if .DomainHasRL}} {{if .DomainHasRL}}
<form class="inline" method="post" action="/domains/{{.Domain.ID}}/ratelimit" <form class="inline" method="post" action="/domains/{{.Domain.ID}}/ratelimit"
data-confirm="Remove the domain rate limit? Only the global level-1 limit will apply."> data-confirm="Remove the domain rate limit? Only the global level-1 limit will apply.">
<input type="hidden" name="clear" value="1"> <input type="hidden" name="clear" value="1">
<button type="submit" class="danger">Remove limit</button> <button type="submit" class="danger">Remove limit</button>
</form> </form>
{{end}} {{end}}
</div> </div>
<div class="card" id="export"> <div class="card" id="export">
<h2>Export domain</h2> <h2>Export domain</h2>
<p class="muted">Download this domain to move it to another SelfPost instance: <p class="muted">DKIM key, selector and application passwords for another
its DKIM key, selector and every application with its working password. On SelfPost instance. DNS stays the same on import.</p>
import the DNS record stays the same, so no DNS change is needed.</p> <p class="muted"><strong>Secret file</strong> — transfer securely, or encrypt
<p class="muted"><strong>The export file is a secret</strong> — it contains the below as <code>.spde</code>.</p>
private DKIM key and application passwords. Transfer it securely and delete it {{if .ExportErr}}<p class="error">{{.ExportErr}}</p>{{end}}
after the import, or encrypt it below and move a <code>.spde</code> file <form method="post" action="/domains/{{.Domain.ID}}/export">
(SelfPost domain export) instead; the import form asks for the password.</p> {{template "encryptfields" .}}
{{if .ExportErr}}<p class="error">{{.ExportErr}}</p>{{end}} <button type="submit">Export domain</button>
<form method="post" action="/domains/{{.Domain.ID}}/export"> </form>
{{template "encryptfields" .}} </div>
<button type="submit">Export domain</button>
</form>
</div> </div>
<div class="card" id="danger"> <div class="card" id="danger">
<h2>Danger zone</h2> <h2>Danger zone</h2>
<p class="muted">Deleting this domain also deletes its DKIM key and every <p class="muted">Deletes the DKIM key and every application on this domain.</p>
application bound to it.</p>
<a class="danger" href="/domains/{{.Domain.ID}}/delete">Delete domain</a> <a class="danger" href="/domains/{{.Domain.ID}}/delete">Delete domain</a>
</div> </div>
{{end}} {{end}}
{{/* The domain page's section index, shown in the navigation column (see the {{/* Section index in the navigation column (layout.html). The freshly generated
"sections" block in layout.html). This is the panel's longest page — the password is only listed while it is on the page. */}}
DNS records to publish, the checks on them, the applications and two rate
limits — and setting a domain up means going back and forth between them.
The freshly generated password is only listed while it is on the page: it
is the one card that is not always there, and the one nothing should scroll
away from silently. */}}
{{define "sections"}} {{define "sections"}}
<div class="sections"> <div class="sections">
<p class="sections-title">On this page</p> <p class="sections-title">On this page</p>
{{if .NewCred}}<a href="#new-credential">New application password</a>{{end}} {{if .NewCred}}<a href="#new-credential">New application password</a>{{end}}
<a href="#dkim">DKIM DNS record</a> <a href="#dkim">DKIM DNS record</a>
<a href="#spf-dmarc">SPF and DMARC records</a>
<a href="#dns-status">DNS status</a> <a href="#dns-status">DNS status</a>
<a href="#spf">SPF record</a>
<a href="#dmarc">DMARC record</a>
<a href="#settings">Sending server settings</a> <a href="#settings">Sending server settings</a>
<a href="#add-application">Add an application</a> <a href="#add-application">Add an application</a>
<a href="#applications">Applications</a> <a href="#applications">Applications</a>
+27 -1
View File
@@ -264,7 +264,7 @@ func TestOnlyThePagesMadeOfDataDeclareThemselvesWide(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("New: %v", err) t.Fatalf("New: %v", err)
} }
wide := map[string]bool{"settings": true, "deliveries": true, "delivery": true, "mail_queue": true, "status": true, "system_log": true} wide := map[string]bool{"settings": true, "deliveries": true, "delivery": true, "mail_queue": true, "status": true, "system_log": true, "domain_detail": true}
for name, page := range engine.Pages() { for name, page := range engine.Pages() {
var buf bytes.Buffer var buf bytes.Buffer
if err := page.ExecuteTemplate(&buf, "wide", nil); err != nil { if err := page.ExecuteTemplate(&buf, "wide", nil); err != nil {
@@ -280,6 +280,32 @@ func TestOnlyThePagesMadeOfDataDeclareThemselvesWide(t *testing.T) {
} }
} }
// The domain page pairs cards the same way Status does: four .split rows
// (DKIM|DNS, SPF|DMARC, settings|add-app, rate-limit|export) with Applications
// and Danger full-width. Losing a row silently stacks the page again.
func TestDomainDetailPageHasPairedCards(t *testing.T) {
body, err := fs.ReadFile(assetsFS, "templates/domain_detail.html")
if err != nil {
t.Fatalf("read domain_detail: %v", err)
}
src := string(body)
if got := strings.Count(src, `class="split"`); got != 4 {
t.Errorf("domain detail has %d .split rows, want 4", got)
}
for _, id := range []string{
`id="dkim"`, `id="dns-status"`, `id="spf"`, `id="dmarc"`,
`id="settings"`, `id="add-application"`, `id="applications"`,
`id="rate-limit"`, `id="export"`, `id="danger"`,
} {
if !strings.Contains(src, id) {
t.Errorf("domain detail is missing %s", id)
}
}
if strings.Contains(src, `id="spf-dmarc"`) {
t.Error("domain detail still has the combined spf-dmarc card; SPF and DMARC are separate")
}
}
// Drill-down pages carry an up-link directly under the heading and above the // Drill-down pages carry an up-link directly under the heading and above the
// cards. A link at the bottom of a form is easy to miss and drifts from the // cards. A link at the bottom of a form is easy to miss and drifts from the
// rest of the panel, so the shared back_link template is mandatory on those // rest of the panel, so the shared back_link template is mandatory on those