release: 1.2.3
Close CHANGELOG [Unreleased] as 1.2.3; bump the compose pin and image references. Includes domain detail layout polish and Domains add-row. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -508,8 +508,20 @@ meter { width: 5rem; height: 0.7rem; vertical-align: middle; margin-right: 0.4re
|
||||
beside them would otherwise win the width and leave "Memory" broken across
|
||||
two lines. */
|
||||
.metric { white-space: nowrap; }
|
||||
/* Copy sits at the top of the value — right for a long DKIM key. The row's
|
||||
.code uses the compact button's vertical padding so a one-line value is the
|
||||
same height as Copy and does not leave the button hanging under an empty
|
||||
band of padding. */
|
||||
.code-row { display: flex; align-items: flex-start; gap: 0.5rem; }
|
||||
.code-row .code { flex: 1; min-width: 0; }
|
||||
.code-row .code {
|
||||
flex: 1; min-width: 0; margin-top: 0;
|
||||
padding-top: 0.45rem; padding-bottom: 0.45rem;
|
||||
}
|
||||
/* A short field with its submit on the same row (Add domain). The button
|
||||
keeps the card's usual filled look; its top margin is for stacked forms. */
|
||||
.input-row { display: flex; align-items: stretch; gap: 0.5rem; }
|
||||
.input-row input { flex: 1; min-width: 0; }
|
||||
.input-row button { margin-top: 0; flex: none; white-space: nowrap; }
|
||||
|
||||
/* Two cards abreast, on the one page that has a pair of them worth reading
|
||||
together (a delivery's facts beside its history). auto-fit with a minimum
|
||||
@@ -557,6 +569,15 @@ meter { width: 5rem; height: 0.7rem; vertical-align: middle; margin-right: 0.4re
|
||||
}
|
||||
.field-pair > div { min-width: 0; }
|
||||
.field-pair label { margin-top: 0.45rem; }
|
||||
/* Host / name beside a DNS Type that is always TXT — size the type column to
|
||||
that token rather than giving it half the row. */
|
||||
.field-pair.host-type {
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
}
|
||||
.field-type { width: max-content; }
|
||||
.field-type .code {
|
||||
width: fit-content; min-width: 2.75rem; text-align: center; box-sizing: border-box;
|
||||
}
|
||||
/* The subject heads a delivery's page, and it is the one heading in the panel
|
||||
whose text we do not control: it may be a hundred characters with nothing to
|
||||
break on. It wraps to as many lines as it needs (this is the page's name, not
|
||||
@@ -646,7 +667,7 @@ button.copy, .actions button, .actions > label.toggle, .actions a.danger, .nav b
|
||||
}
|
||||
button.copy:hover, .actions button:hover, .actions > label.toggle:hover,
|
||||
.actions a.danger:hover, .nav button:hover { background: var(--surface-bg-hover); }
|
||||
button.copy { flex: none; margin-top: 0.3rem; }
|
||||
button.copy { flex: none; }
|
||||
.actions button.danger, .actions a.danger, .nav button.danger {
|
||||
color: var(--danger-fg); background: var(--danger-bg); border-color: var(--danger-border);
|
||||
}
|
||||
|
||||
@@ -8,14 +8,14 @@
|
||||
<h2>Add a sending domain</h2>
|
||||
<form method="post" action="/domains">
|
||||
<label for="name">Domain</label>
|
||||
<input id="name" name="name" type="text" placeholder="example.com"
|
||||
autocomplete="off" autocapitalize="none" spellcheck="false"
|
||||
value="{{.FormName}}" autofocus required>
|
||||
<div class="input-row">
|
||||
<input id="name" name="name" type="text" placeholder="example.com"
|
||||
autocomplete="off" autocapitalize="none" spellcheck="false"
|
||||
value="{{.FormName}}" autofocus required>
|
||||
<button type="submit">Add domain</button>
|
||||
</div>
|
||||
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
|
||||
<button type="submit">Add domain</button>
|
||||
</form>
|
||||
<p class="muted">A DKIM key is generated for the domain; you then publish the
|
||||
shown DNS record. Adding a domain does not create an application.</p>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
|
||||
@@ -37,39 +37,93 @@
|
||||
<div class="check-cols">
|
||||
<div class="check-col">
|
||||
<label>DKIM <span class="st st-{{.DNS.DKIM.Status}}">{{.DNS.DKIM.Status}}</span></label>
|
||||
<p class="muted">{{.Record.Name}}</p>
|
||||
<div class="field-pair host-type">
|
||||
<div>
|
||||
<label>Host / name</label>
|
||||
<span class="code">{{.Record.Name}}</span>
|
||||
</div>
|
||||
<div class="field-type">
|
||||
<label>Type</label>
|
||||
<span class="code">TXT</span>
|
||||
</div>
|
||||
</div>
|
||||
{{if .DNS.DKIM.Records}}
|
||||
<label>Value</label>
|
||||
<span class="code">{{range .DNS.DKIM.Records}}{{.}}
|
||||
{{end}}</span>
|
||||
{{end}}
|
||||
{{if ne .DNS.DKIM.Status "ok"}}
|
||||
<p class="{{if eq .DNS.DKIM.Status "unknown"}}muted{{else}}error{{end}}">{{.DNS.DKIM.Detail}}</p>
|
||||
{{end}}
|
||||
{{if .DNS.DKIM.Records}}<span class="code">{{range .DNS.DKIM.Records}}{{.}}
|
||||
{{end}}</span>{{end}}
|
||||
</div>
|
||||
|
||||
<div class="check-col">
|
||||
<label>SPF <span class="st st-{{.DNS.SPF.Status}}">{{.DNS.SPF.Status}}</span></label>
|
||||
<p class="muted">{{.Domain.Name}}</p>
|
||||
<div class="field-pair host-type">
|
||||
<div>
|
||||
<label>Host / name</label>
|
||||
<span class="code">{{.Domain.Name}}</span>
|
||||
</div>
|
||||
<div class="field-type">
|
||||
<label>Type</label>
|
||||
<span class="code">TXT</span>
|
||||
</div>
|
||||
</div>
|
||||
{{if .DNS.SPF.Records}}
|
||||
<label>Value</label>
|
||||
<span class="code">{{range .DNS.SPF.Records}}{{.}}
|
||||
{{end}}</span>
|
||||
{{end}}
|
||||
{{if ne .DNS.SPF.Status "ok"}}
|
||||
<p class="{{if eq .DNS.SPF.Status "unknown"}}muted{{else}}error{{end}}">{{.DNS.SPF.Detail}}</p>
|
||||
{{end}}
|
||||
<p class="muted">Shallow check: literal address only, no <code>include:</code> /
|
||||
<code>redirect=</code>.</p>
|
||||
{{if .DNS.SPF.Records}}<span class="code">{{range .DNS.SPF.Records}}{{.}}
|
||||
{{end}}</span>{{end}}
|
||||
</div>
|
||||
|
||||
<div class="check-col">
|
||||
<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}}
|
||||
<div class="field-pair host-type">
|
||||
<div>
|
||||
<label>Host / name</label>
|
||||
<span class="code">{{.DMARCName}}</span>
|
||||
</div>
|
||||
<div class="field-type">
|
||||
<label>Type</label>
|
||||
<span class="code">TXT</span>
|
||||
</div>
|
||||
</div>
|
||||
{{if .DNS.DMARC.Records}}
|
||||
<label>Value</label>
|
||||
<span class="code">{{range .DNS.DMARC.Records}}{{.}}
|
||||
{{end}}</span>
|
||||
{{end}}
|
||||
{{if ne .DNS.DMARC.Status "ok"}}
|
||||
<p class="{{if eq .DNS.DMARC.Status "unknown"}}muted{{else}}error{{end}}">{{.DNS.DMARC.Detail}}</p>
|
||||
{{else}}
|
||||
<p class="muted">{{.DNS.DMARC.Detail}}</p>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="check-col">
|
||||
{{if .DNS.DMARCReportAuth.Status}}
|
||||
<label>Report authorization <span class="st st-{{.DNS.DMARCReportAuth.Status}}">{{.DNS.DMARCReportAuth.Status}}</span></label>
|
||||
<div class="field-pair host-type">
|
||||
<div>
|
||||
<label>Host / name</label>
|
||||
<span class="code">{{.ReportAuthName}}</span>
|
||||
</div>
|
||||
<div class="field-type">
|
||||
<label>Type</label>
|
||||
<span class="code">TXT</span>
|
||||
</div>
|
||||
</div>
|
||||
{{if .DNS.DMARCReportAuth.Records}}
|
||||
<label>Value</label>
|
||||
<span class="code">{{range .DNS.DMARCReportAuth.Records}}{{.}}
|
||||
{{end}}</span>
|
||||
{{end}}
|
||||
<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}}
|
||||
{{else}}
|
||||
<label>Report authorization</label>
|
||||
<p class="muted">Not required (no external <code>rua=</code>).</p>
|
||||
@@ -83,18 +137,24 @@
|
||||
</div>
|
||||
|
||||
<div class="split">
|
||||
<div class="card" id="dkim">
|
||||
<h2>DKIM record</h2>
|
||||
<div class="card" id="dkim-spf">
|
||||
<h2>DKIM and SPF records</h2>
|
||||
|
||||
<label>Host / name</label>
|
||||
<div class="code-row">
|
||||
<span class="code">{{.Record.Name}}</span>
|
||||
<button type="button" class="copy">Copy</button>
|
||||
<p class="check-col-title">DKIM</p>
|
||||
<div class="field-pair host-type">
|
||||
<div>
|
||||
<label>Host / name</label>
|
||||
<div class="code-row">
|
||||
<span class="code">{{.Record.Name}}</span>
|
||||
<button type="button" class="copy">Copy</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-type">
|
||||
<label>Type</label>
|
||||
<span class="code">TXT</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label>Type</label>
|
||||
<span class="code">TXT</span>
|
||||
|
||||
<label>Value</label>
|
||||
<div class="code-row">
|
||||
<span class="code">{{.Record.Value}}</span>
|
||||
@@ -103,21 +163,22 @@
|
||||
|
||||
<p class="muted">Not a secret. Signed with selector
|
||||
<strong>{{.Domain.DKIMSelector}}</strong>.</p>
|
||||
</div>
|
||||
|
||||
<div class="card" id="spf-dmarc">
|
||||
<h2>SPF and DMARC records</h2>
|
||||
|
||||
<p class="check-col-title">SPF</p>
|
||||
<label>Host / name</label>
|
||||
<div class="code-row">
|
||||
<span class="code">{{.Domain.Name}}</span>
|
||||
<button type="button" class="copy">Copy</button>
|
||||
<div class="field-pair host-type">
|
||||
<div>
|
||||
<label>Host / name</label>
|
||||
<div class="code-row">
|
||||
<span class="code">{{.Domain.Name}}</span>
|
||||
<button type="button" class="copy">Copy</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-type">
|
||||
<label>Type</label>
|
||||
<span class="code">TXT</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label>Type</label>
|
||||
<span class="code">TXT</span>
|
||||
|
||||
<label>Value</label>
|
||||
<div class="code-row">
|
||||
<span class="code">{{.SPFExample}}</span>
|
||||
@@ -126,17 +187,25 @@
|
||||
|
||||
<p class="muted">Merge into an existing SPF if the domain already has one —
|
||||
do not publish a second record.</p>
|
||||
</div>
|
||||
|
||||
<p class="check-col-title">DMARC</p>
|
||||
<label>Host / name</label>
|
||||
<div class="code-row">
|
||||
<span class="code">{{.DMARCName}}</span>
|
||||
<button type="button" class="copy">Copy</button>
|
||||
<div class="card" id="dmarc">
|
||||
<h2>DMARC record</h2>
|
||||
|
||||
<div class="field-pair host-type">
|
||||
<div>
|
||||
<label>Host / name</label>
|
||||
<div class="code-row">
|
||||
<span class="code">{{.DMARCName}}</span>
|
||||
<button type="button" class="copy">Copy</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-type">
|
||||
<label>Type</label>
|
||||
<span class="code">TXT</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label>Type</label>
|
||||
<span class="code">TXT</span>
|
||||
|
||||
<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>
|
||||
@@ -151,15 +220,20 @@
|
||||
|
||||
{{if .NeedsReportAuth}}
|
||||
<p class="check-col-title">Report authorization</p>
|
||||
<label>Host / name</label>
|
||||
<div class="code-row">
|
||||
<span class="code">{{.ReportAuthName}}</span>
|
||||
<button type="button" class="copy">Copy</button>
|
||||
<div class="field-pair host-type">
|
||||
<div>
|
||||
<label>Host / name</label>
|
||||
<div class="code-row">
|
||||
<span class="code">{{.ReportAuthName}}</span>
|
||||
<button type="button" class="copy">Copy</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-type">
|
||||
<label>Type</label>
|
||||
<span class="code">TXT</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label>Type</label>
|
||||
<span class="code">TXT</span>
|
||||
|
||||
<label>Value</label>
|
||||
<div class="code-row">
|
||||
<span class="code">{{.ReportAuthValue}}</span>
|
||||
@@ -174,8 +248,8 @@
|
||||
</div>
|
||||
|
||||
<div class="split">
|
||||
<div class="card" id="settings">
|
||||
<h2>Sending server settings</h2>
|
||||
<div class="card" id="connection">
|
||||
<h2>Connection settings</h2>
|
||||
<p class="muted">Same for every domain. Authenticate with an application
|
||||
login from below.</p>
|
||||
|
||||
@@ -193,8 +267,8 @@
|
||||
create or regenerate.</p>
|
||||
</div>
|
||||
|
||||
{{/* Create form beside settings, mirroring "Add a sending domain" above the
|
||||
domains list. */}}
|
||||
{{/* Create form beside connection 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">
|
||||
@@ -270,7 +344,10 @@
|
||||
</form>
|
||||
</div>
|
||||
<div class="check-col">
|
||||
<p class="check-col-title">Rate limit</p>
|
||||
<p class="check-col-title">Optional level-2 rate limit</p>
|
||||
<p class="muted">Caps this application only. The domain level-2
|
||||
limit (<a href="#domain-settings">Domain settings</a>) and the
|
||||
global level-1 limit still apply.</p>
|
||||
<form id="rl-{{.ID}}" method="post" action="/applications/{{.ID}}/ratelimit">
|
||||
<label>Expected client IPs (one per line or comma-separated)</label>
|
||||
<textarea name="allowed_ips" rows="2" placeholder="203.0.113.10">{{.IPsText}}</textarea>
|
||||
|
||||
@@ -207,7 +207,7 @@ func TestOnlyThePagesMadeOfDataDeclareThemselvesWide(t *testing.T) {
|
||||
}
|
||||
|
||||
// The domain page pairs cards the same way Status does: three .split rows
|
||||
// (DKIM|SPF+DMARC, settings|add-app, export|danger). DNS status, Applications
|
||||
// (DKIM+SPF|DMARC, connection|add-app, export|danger). DNS status, Applications
|
||||
// and Domain settings are full-width; DNS status and Domain settings (and the
|
||||
// application Edit panel) use .check-cols. Losing a row silently stacks again.
|
||||
func TestDomainDetailPageHasPairedCards(t *testing.T) {
|
||||
@@ -230,8 +230,8 @@ func TestDomainDetailPageHasPairedCards(t *testing.T) {
|
||||
t.Error("application Edit mode and Rate limit should be one Edit button")
|
||||
}
|
||||
for _, id := range []string{
|
||||
`id="dkim"`, `id="dns-status"`, `id="spf-dmarc"`,
|
||||
`id="settings"`, `id="add-application"`, `id="applications"`,
|
||||
`id="dkim-spf"`, `id="dns-status"`, `id="dmarc"`,
|
||||
`id="connection"`, `id="add-application"`, `id="applications"`,
|
||||
`id="domain-settings"`, `id="export"`, `id="danger"`,
|
||||
} {
|
||||
if !strings.Contains(src, id) {
|
||||
@@ -241,11 +241,8 @@ func TestDomainDetailPageHasPairedCards(t *testing.T) {
|
||||
if strings.Contains(src, `id="rate-limit"`) {
|
||||
t.Error("domain rate limit should live inside domain-settings, not its own card")
|
||||
}
|
||||
if strings.Contains(src, `id="spf"`) && !strings.Contains(src, `id="spf-dmarc"`) {
|
||||
t.Error("standalone SPF card should be merged into spf-dmarc")
|
||||
}
|
||||
if regexp.MustCompile(`id="dmarc"`).MatchString(src) {
|
||||
t.Error("standalone DMARC card should be merged into spf-dmarc")
|
||||
if strings.Contains(src, `id="spf-dmarc"`) {
|
||||
t.Error("SPF should sit with DKIM, not with DMARC")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user