release: 1.8.0
test / test (push) Waiting to run

Add in-panel Help: CSS-only drawer with seeded Status and domain topics,
«?» entry points on cards, and guide.md boundary for what stays in-repo.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-18 23:00:08 +03:00
parent 27aeadc71d
commit 33ee08f7b6
15 changed files with 486 additions and 22 deletions
+32 -8
View File
@@ -67,7 +67,10 @@
{{if .NewCred}}
<div class="card credential" id="new-credential">
<h2>New application password</h2>
<div class="card-head">
<h2>New application password</h2>
{{template "card_help" (dict "ID" "password" "Title" "Why this is shown once")}}
</div>
<p class="muted">Shown <strong>once only</strong> and not stored. Copy it now
— if it is lost, regenerate a new one.</p>
<label>Login</label>
@@ -85,7 +88,10 @@
{{/* Two rows of two checks (.check-cols). */}}
<div class="card" id="dns-status">
<h2>DNS status <span class="st st-{{.DNS.Overall}}">{{.DNS.Overall}}</span></h2>
<div class="card-head">
<h2>DNS status <span class="st st-{{.DNS.Overall}}">{{.DNS.Overall}}</span></h2>
{{template "card_help" (dict "ID" "dns" "Title" "What these checks mean")}}
</div>
<p class="muted">Cached a few minutes — use <em>Re-check</em> after
publishing.</p>
@@ -141,7 +147,10 @@
<div class="split">
<div class="card" id="dkim-spf">
<h2>DKIM and SPF records</h2>
<div class="card-head">
<h2>DKIM and SPF records</h2>
{{template "card_help" (dict "ID" "records" "Title" "What to publish")}}
</div>
<p class="check-col-title">DKIM</p>
{{template "host_type_copy" .Record.Name}}
@@ -159,7 +168,10 @@
</div>
<div class="card" id="dmarc">
<h2>DMARC record</h2>
<div class="card-head">
<h2>DMARC record</h2>
{{template "card_help" (dict "ID" "dmarc" "Title" "What this record does")}}
</div>
{{template "host_type_copy" .DMARCName}}
@@ -191,7 +203,10 @@
<div class="split">
<div class="card" id="connection">
<h2>Connection settings</h2>
<div class="card-head">
<h2>Connection settings</h2>
{{template "card_help" (dict "ID" "connection" "Title" "How to connect")}}
</div>
<p class="muted">Same for every domain. Authenticate with an application
login from below.</p>
@@ -262,7 +277,10 @@
</div>
<div class="card" id="applications">
<h2>Applications</h2>
<div class="card-head">
<h2>Applications</h2>
{{template "card_help" (dict "ID" "apps" "Title" "What an application is")}}
</div>
<p class="muted">SASL logins for this domain — wildcard (*@domain) or a fixed
address list.</p>
@@ -376,7 +394,10 @@
</div>
<div class="card" id="domain-settings">
<h2>Domain settings</h2>
<div class="card-head">
<h2>Domain settings</h2>
{{template "card_help" (dict "ID" "domain-settings" "Title" "Reports and rate limits")}}
</div>
<div class="check-cols check-cols-rows">
<div class="check-col">
@@ -464,7 +485,10 @@
<div class="split">
<div class="card" id="export">
<h2>Export domain</h2>
<div class="card-head">
<h2>Export domain</h2>
{{template "card_help" (dict "ID" "export" "Title" "What the file contains")}}
</div>
<p class="muted"><strong>Secret file</strong> — transfer securely, or encrypt
below as <code>.spde</code>.</p>
{{if .ExportErr}}<p class="error">{{.ExportErr}}</p>{{end}}
+62
View File
@@ -0,0 +1,62 @@
{{define "content"}}
<h1>Help</h1>
<p class="muted">Short operator notes inside the panel — not a second copy of
the full guide. The cards keep their readings and controls; this page (and the
drawer opened from each card's «?») holds the explanations that do not belong
on the card itself.</p>
<div class="card">
<h2>On this panel</h2>
{{if .IsGlobal}}
<ul>
<li><label for="help-status">Status checks</label> — Machine, TLS, PTR,
queue</li>
<li><a href="/mail-queue">Mail queue retries</a> — time-based, no attempt
budget (on the queue page)</li>
{{if .InboundEnabled}}
<li><a href="/inbound">Inbound</a> — not mailboxes; listed recipients or any
address at the domain</li>
{{end}}
</ul>
{{end}}
<p class="muted">Domain page</p>
<ul>
<li><label for="help-dns">DNS status</label></li>
<li><label for="help-records">DKIM and SPF records</label></li>
<li><label for="help-dmarc">DMARC record</label></li>
<li><label for="help-connection">Connection settings</label></li>
<li><label for="help-apps">Applications</label></li>
<li><label for="help-domain-settings">Domain settings</label></li>
<li><label for="help-export">Export domain</label></li>
</ul>
<p class="muted">Topics marked as labels open the same text in the help
drawer. For installation, backup, and environment variables see the
<a href="https://github.com/mixeme/selfpost/blob/main/docs/guide.md">operator
guide</a> in the repository.</p>
</div>
{{if .IsGlobal}}
<div class="card">
<h2>Machine</h2>
<p>CPU and memory are the container's own readings, not the host's spare
capacity. Network is a short window, not a daily total. High CPU with an empty
queue usually means something else on the box — not SelfPost being slow to
send.</p>
</div>
<div class="card">
<h2>TLS certificate</h2>
<p>Port 465 presents the certificate the reverse proxy (or the image)
mounted. The panel does not issue certificates. A warn here is expires soon;
an error is missing or unreadable, and clients will refuse submission.</p>
</div>
<div class="card">
<h2>Hostname / reverse DNS</h2>
<p>Forward-confirmed reverse DNS: the A/AAAA for
<code>SELFPOST_HOSTNAME</code> must reverse to that same name. PTR is set at
the hosting provider, not in this panel.</p>
</div>
{{end}}
{{end}}
@@ -0,0 +1,157 @@
{{/* Hidden radios drive which help pane is visible — CSS :has(), no script. */}}
{{define "help_inputs"}}
<input class="ctrl" type="radio" name="help" id="help-off" checked>
<input class="ctrl" type="radio" name="help" id="help-index">
<input class="ctrl" type="radio" name="help" id="help-status">
<input class="ctrl" type="radio" name="help" id="help-password">
<input class="ctrl" type="radio" name="help" id="help-dns">
<input class="ctrl" type="radio" name="help" id="help-records">
<input class="ctrl" type="radio" name="help" id="help-dmarc">
<input class="ctrl" type="radio" name="help" id="help-connection">
<input class="ctrl" type="radio" name="help" id="help-apps">
<input class="ctrl" type="radio" name="help" id="help-domain-settings">
<input class="ctrl" type="radio" name="help" id="help-export">
{{end}}
{{define "help_more"}}<p class="more muted"><label for="help-index">All topics</label>{{if .}} · <a href="/help">Help page</a>{{end}}</p>{{end}}
{{/* card_help — «?» on a card head; .ID is the topic slug (dns, records, …). */}}
{{define "card_help"}}
<label class="help-link" for="help-{{.ID}}" title="{{.Title}}">?</label>
{{end}}
{{define "help_drawer"}}
<label class="help-scrim" for="help-off"></label>
<aside class="help-drawer">
<label class="help-close" for="help-off">Close</label>
<article class="help-pane help-pane-index">
<h2>Help</h2>
<p>Short notes for the card you opened — not a second copy of the
<a href="https://github.com/mixeme/selfpost/blob/main/docs/guide.md">operator guide</a>.</p>
{{if .IsGlobal}}
<p class="muted">Status</p>
<ul class="toc"><li><label for="help-status">Status checks</label></li></ul>
{{end}}
<p class="muted">Domain</p>
<ul class="toc">
<li><label for="help-password">New application password</label></li>
<li><label for="help-dns">DNS status</label></li>
<li><label for="help-records">DKIM and SPF records</label></li>
<li><label for="help-dmarc">DMARC record</label></li>
<li><label for="help-connection">Connection settings</label></li>
<li><label for="help-apps">Applications</label></li>
<li><label for="help-domain-settings">Domain settings</label></li>
<li><label for="help-export">Export domain</label></li>
</ul>
</article>
<article class="help-pane help-pane-status">
<h2>Status checks</h2>
<p>The cards keep the readings. This drawer is what used to sit under them
as paragraphs.</p>
<h2>Machine</h2>
<p>CPU and memory are the container's readings, not the host's spare
capacity. Network is a short rate window between refreshes, not a daily
total. High CPU with an empty queue usually means something else on the box
— not SelfPost being slow to send.</p>
<h2>TLS certificate</h2>
<p>Port 465 presents the certificate the reverse proxy (or the image)
mounted. The panel does not issue certificates. A warn here is expires
soon; an error is missing or unreadable, and clients will refuse
submission.</p>
<h2>Hostname / reverse DNS</h2>
<p>Forward-confirmed reverse DNS: the A/AAAA for
<code>SELFPOST_HOSTNAME</code> must reverse to that same name. PTR is set
at the hosting provider, not in this panel. Receiving networks use this
pair as a cheap reputation check.</p>
<h2>Mail queue</h2>
<p>Deferred mail is retried on a time schedule (first delay, doubling cap,
queue lifetime). There is no attempt budget — see the retry-policy card on
<em>Mail queue</em>.</p>
{{if .InboundEnabled}}
<h2>Inbound</h2>
<p>When <code>INBOUND_RELAY_ENABLE</code> is on, port 25 accepts mail for
configured domains and forwards it upstream — not to local mailboxes.
Recipients are a list or any address at the domain.</p>
{{end}}
{{template "help_more" true}}
</article>
<article class="help-pane help-pane-password">
<h2>New application password</h2>
<p>Shown <strong>once only</strong> and not stored. Copy it now — if it is
lost, regenerate a new one. The previous password stops working
immediately.</p>
{{template "help_more" true}}
</article>
<article class="help-pane help-pane-dns">
<h2>DNS status</h2>
<p>The badge is the worst of DKIM, SPF and DMARC. Results are cached a few
minutes — use <em>Re-check</em> after publishing.</p>
<p>SPF is a shallow check: the literal address only, no
<code>include:</code> or <code>redirect=</code>. Report authorization is
required only when <code>rua=</code> points at a domain this server does
not accept.</p>
{{template "help_more" true}}
</article>
<article class="help-pane help-pane-records">
<h2>DKIM and SPF records</h2>
<p>DKIM is not a secret. The selector on this page is the one this server
signs with. Merge the SPF example into an existing record if the domain
already has one — do not publish a second TXT.</p>
{{template "help_more" true}}
</article>
<article class="help-pane help-pane-dmarc">
<h2>DMARC record</h2>
<p><code>p=none</code> does not affect delivery. Tighten to
<code>p=quarantine</code> then <code>p=reject</code> once reports look
clean. The report address is set under Domain settings (or the Settings
default).</p>
{{template "help_more" true}}
</article>
<article class="help-pane help-pane-connection">
<h2>Connection settings</h2>
<p>Same host for every domain. Authenticate with an application login from
this page. Auth is required on every port. The password is shown once at
create or regenerate.</p>
<p>465 is implicit TLS; 587 is STARTTLS submission when that port is
enabled.</p>
{{template "help_more" true}}
</article>
<article class="help-pane help-pane-apps">
<h2>Applications</h2>
<p>SASL logins for this domain. Login is unique across domains; letters,
digits, <code>.</code>, <code>-</code> and <code>_</code>. The password is
shown once.</p>
<p>Address mode is which From addresses this application may use: any
address of the domain, or a fixed list. A trusted-IP override gives those
clients a higher ceiling than the domain (still ≤ level 1) and skips the
domain check; everyone else uses the domain limit if set, otherwise level
1.</p>
{{template "help_more" true}}
</article>
<article class="help-pane help-pane-domain-settings">
<h2>Domain settings</h2>
<p>Aggregate reports (<code>rua=</code>) inherit the Settings default, or
you override them per domain. Level 2 is an optional ceiling for all
senders on this domain; it must be ≤ level 1. Application overrides live on
each application.</p>
{{template "help_more" true}}
</article>
<article class="help-pane help-pane-export">
<h2>Export domain</h2>
<p>The file is a secret: it carries the DKIM key and application
passwords, so published DNS does not have to change on the other instance.
Transfer it securely, or encrypt it as <code>.spde</code>.</p>
{{template "help_more" true}}
</article>
</aside>
{{end}}
+4
View File
@@ -22,6 +22,7 @@
<script src="/static/panel.js" defer></script>
</head>
<body>
{{if .User}}{{template "help_inputs" .}}{{end}}
{{/* Two columns: the navigation on the left and the page beside it, centred
together (see .shell in panel.css). The signed-out pages have no navigation
and the shell holds their card alone. */}}
@@ -50,6 +51,7 @@
</footer>
</main>
</div>
{{if .User}}{{template "help_drawer" .}}{{end}}
</body>
</html>{{end}}
@@ -76,6 +78,7 @@
{{if .DMARCEnabled}}
{{if eq .Active "dmarc"}}<span aria-current="page">{{template "icon-dmarc"}}DMARC</span>{{else}}<a href="/dmarc">{{template "icon-dmarc"}}DMARC</a>{{end}}
{{end}}
{{if eq .Active "help"}}<span aria-current="page">{{template "icon-help"}}Help</span>{{else}}<a href="/help">{{template "icon-help"}}Help</a>{{end}}
{{if eq .Active "deliveries"}}<span aria-current="page">{{template "icon-deliveries"}}Deliveries</span>{{else}}<a href="/deliveries">{{template "icon-deliveries"}}Deliveries</a>{{end}}
{{if .IsGlobal}}
{{if eq .Active "mail_queue"}}<span aria-current="page">{{template "icon-mail-queue"}}Mail queue</span>{{else}}<a href="/mail-queue">{{template "icon-mail-queue"}}Mail queue</a>{{end}}
@@ -120,6 +123,7 @@
{{define "icon-domains"}}<svg class="icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="8" cy="8" r="6.25"/><path d="M1.9 8h12.2"/><path d="M8 1.75c1.85 1.8 2.8 4 2.8 6.25S9.85 12.45 8 14.25C6.15 12.45 5.2 10.25 5.2 8S6.15 3.55 8 1.75Z"/></svg>{{end}}
{{define "icon-inbound"}}<svg class="icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2.5 9.5h11"/><path d="M8 2.75v6.2"/><path d="M5.4 6.4 8 9.05 10.6 6.4"/><path d="M3.2 12.6h9.6"/></svg>{{end}}
{{define "icon-dmarc"}}<svg class="icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2.5 4.5h11v7H2.5z"/><path d="M5 7.5h6M5 9.5h4"/></svg>{{end}}
{{define "icon-help"}}<svg class="icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="8" cy="8" r="6.25"/><path d="M8 7.2V11.4"/><path d="M8 5.05v.01"/></svg>{{end}}
{{define "icon-deliveries"}}<svg class="icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14.25 1.75 1.6 6.6l5 2.05 2.05 5z"/><path d="M14.25 1.75 6.6 8.65"/></svg>{{end}}
{{define "icon-mail-queue"}}<svg class="icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1.75 9.5h3.3l1 1.75h3.9l1-1.75h3.3v3.05a1.2 1.2 0 0 1-1.2 1.2H2.95a1.2 1.2 0 0 1-1.2-1.2z"/><path d="M1.75 9.5 3.4 3.2a1.25 1.25 0 0 1 1.2-.95h6.8a1.25 1.25 0 0 1 1.2.95l1.65 6.3"/></svg>{{end}}
{{define "icon-system-log"}}<svg class="icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.75 1.75h5.1l3.4 3.4v8.05a1.05 1.05 0 0 1-1.05 1.05H3.75a1.05 1.05 0 0 1-1.05-1.05V2.8a1.05 1.05 0 0 1 1.05-1.05Z"/><path d="M8.85 1.75v3.4h3.4"/><path d="M5.35 8.6h5.3M5.35 11.1h3.5"/></svg>{{end}}
+4 -1
View File
@@ -3,7 +3,10 @@
{{define "wide"}}wide{{end}}
{{define "content"}}
<h1>Status</h1>
<div class="page-head">
<h1>Status</h1>
<label class="help-link" for="help-status" title="What these checks mean">?</label>
</div>
{{if .Flash}}<div class="flash">{{.Flash}}</div>{{end}}