Restore Status readings, inbound MX DNS and recipient modes, two-column Backup and Settings, domain-card help, and Host/name Type field height. Co-Authored-By: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -15,6 +15,15 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
|
|||||||
palette stay; extra nav icons are proposals only. Design artefact; the
|
palette stay; extra nav icons are proposals only. Design artefact; the
|
||||||
running panel is unchanged.
|
running panel is unchanged.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- docs: panel UI mockups after review — Status restored to live readings
|
||||||
|
(queue line, PTR, machine/process tables, Configuration); inbound MX DNS
|
||||||
|
check, recipient mode (list or any), and Danger zone beside recipients;
|
||||||
|
Backup and Settings as two-column ops pages; Save and Delete user on one
|
||||||
|
row; help «?» on domain cards; Host/name ‖ Type height matched to the live
|
||||||
|
panel (`b0ebe06`).
|
||||||
|
|
||||||
## [1.3.0] - 2026-08-14
|
## [1.3.0] - 2026-08-14
|
||||||
|
|
||||||
Security and quality after 1.2.5: domain-admin send-log authorization,
|
Security and quality after 1.2.5: domain-admin send-log authorization,
|
||||||
|
|||||||
+533
-148
@@ -17,7 +17,17 @@
|
|||||||
<input class="ctrl" type="radio" name="theme" id="theme-dark">
|
<input class="ctrl" type="radio" name="theme" id="theme-dark">
|
||||||
<input class="ctrl" type="checkbox" id="feat-inbound" checked>
|
<input class="ctrl" type="checkbox" id="feat-inbound" checked>
|
||||||
<input class="ctrl" type="checkbox" id="nav-open">
|
<input class="ctrl" type="checkbox" id="nav-open">
|
||||||
<input class="ctrl" type="checkbox" id="help-open">
|
<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">
|
||||||
|
|
||||||
<header class="gallery">
|
<header class="gallery">
|
||||||
<a class="brand-mini" href="index.html">Макеты</a>
|
<a class="brand-mini" href="index.html">Макеты</a>
|
||||||
@@ -92,82 +102,156 @@
|
|||||||
<span class="pt pt-queue">Mail queue</span>
|
<span class="pt pt-queue">Mail queue</span>
|
||||||
<span class="pt pt-log">System log</span>
|
<span class="pt pt-log">System log</span>
|
||||||
<span class="pt pt-inbound">Inbound</span>
|
<span class="pt pt-inbound">Inbound</span>
|
||||||
|
<span class="pt pt-inbound-domain">lists.example.com</span>
|
||||||
|
<span class="pt pt-inbound-backup">backup.example.net</span>
|
||||||
<span class="pt pt-dmarc">DMARC</span>
|
<span class="pt pt-dmarc">DMARC</span>
|
||||||
<span class="pt pt-backup">Backup</span>
|
<span class="pt pt-backup">Backup</span>
|
||||||
<span class="pt pt-users">Users</span>
|
<span class="pt pt-users">Users</span>
|
||||||
<span class="pt pt-help">Help</span>
|
<span class="pt pt-help">Help</span>
|
||||||
<span class="pt pt-settings">Settings</span>
|
<span class="pt pt-settings">Settings</span>
|
||||||
</span>
|
</span>
|
||||||
<label class="help-link" for="help-open" title="Help">?</label>
|
<label class="help-link" for="help-index" title="Help">?</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- STATUS -->
|
<!-- STATUS -->
|
||||||
<main id="status" class="screen ops">
|
<main id="status" class="screen ops">
|
||||||
<div class="status-hero attn">
|
<div class="page-head">
|
||||||
<h1>Status <span class="st st-warn">warn</span></h1>
|
<h1>Status</h1>
|
||||||
<p class="lead">3 messages deferred — first retry in about 4 minutes. Everything else is up.</p>
|
<label class="help-link" for="help-status" title="What these checks mean">?</label>
|
||||||
<form class="inline" action="#" onsubmit="return false"><button type="button">Reload configuration</button></form>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="status-grid">
|
|
||||||
<article class="card attn">
|
<div class="card" id="overall">
|
||||||
<div class="card-head">
|
<h2>Overall <span class="st st-warn">warn</span></h2>
|
||||||
<h2>Mail queue <span class="st st-warn">warn</span></h2>
|
<p class="muted">Running, with warnings below.</p>
|
||||||
<label class="help-link" for="help-open">?</label>
|
</div>
|
||||||
</div>
|
|
||||||
<p>3 deferred, 0 bounce-hold. Oldest 18 minutes.</p>
|
<div class="split">
|
||||||
|
<div class="card attn" id="queue">
|
||||||
|
<h2>Mail queue <span class="st st-warn">warn</span></h2>
|
||||||
|
<p>3 Kbytes in 3 Requests.</p>
|
||||||
<a class="btn" href="#mail-queue">View queue</a>
|
<a class="btn" href="#mail-queue">View queue</a>
|
||||||
</article>
|
</div>
|
||||||
<article class="card in-only">
|
<div class="card" id="certificate">
|
||||||
<div class="card-head">
|
<h2>TLS certificate <span class="st st-ok">ok</span></h2>
|
||||||
<h2>Inbound <span class="st st-ok">ok</span></h2>
|
|
||||||
</div>
|
|
||||||
<p>Accepting on port 25 for 1 domain. Last forward 2 minutes ago.</p>
|
|
||||||
<a href="#inbound">Inbound domains</a>
|
|
||||||
</article>
|
|
||||||
<article class="card in-off">
|
|
||||||
<div class="card-head"><h2>Inbound</h2></div>
|
|
||||||
<p class="muted">Module off (<code>INBOUND_RELAY_ENABLE=false</code>). The outbound path is unchanged.</p>
|
|
||||||
</article>
|
|
||||||
<article class="card">
|
|
||||||
<div class="card-head">
|
|
||||||
<h2>Machine <span class="st st-ok">ok</span></h2>
|
|
||||||
<label class="help-link" for="help-open">?</label>
|
|
||||||
</div>
|
|
||||||
<div class="metric-row"><span class="k">CPU</span><span class="v"><meter value="12" min="0" max="100" low="70" high="90" optimum="10"></meter>12%</span></div>
|
|
||||||
<div class="metric-row"><span class="k">Memory</span><span class="v"><meter value="41" min="0" max="100" low="70" high="90" optimum="10"></meter>41%</span></div>
|
|
||||||
<div class="metric-row"><span class="k">Network</span><span class="v">↓ 48 KiB/s · ↑ 12 KiB/s</span></div>
|
|
||||||
</article>
|
|
||||||
<article class="card">
|
|
||||||
<div class="card-head"><h2>Processes <span class="st st-ok">ok</span></h2></div>
|
|
||||||
<div class="metric-row"><span class="k">postfix</span><span class="st st-ok">RUNNING</span></div>
|
|
||||||
<div class="metric-row"><span class="k">opendkim</span><span class="st st-ok">RUNNING</span></div>
|
|
||||||
<div class="metric-row"><span class="k">panel</span><span class="st st-ok">RUNNING</span></div>
|
|
||||||
</article>
|
|
||||||
<article class="card">
|
|
||||||
<div class="card-head">
|
|
||||||
<h2>TLS certificate <span class="st st-ok">ok</span></h2>
|
|
||||||
<label class="help-link" for="help-open">?</label>
|
|
||||||
</div>
|
|
||||||
<label>Expires</label>
|
<label>Expires</label>
|
||||||
<span class="code">2026-11-02 12:00 UTC</span>
|
<span class="code">2026-11-02 12:00 UTC</span>
|
||||||
<p class="muted">Let’s Encrypt, 78 days left. Mounted for port 465.</p>
|
<p class="muted">Valid for another 78 day(s).</p>
|
||||||
</article>
|
</div>
|
||||||
<article class="card">
|
</div>
|
||||||
<div class="card-head"><h2>Milter sockets <span class="st st-ok">ok</span></h2></div>
|
|
||||||
<div class="metric-row"><span class="k">opendkim</span><span class="st st-ok">ok</span></div>
|
<div class="split">
|
||||||
<div class="metric-row"><span class="k">journal</span><span class="st st-ok">ok</span></div>
|
<div class="card" id="sockets">
|
||||||
</article>
|
<h2>Milter sockets <span class="st st-ok">ok</span></h2>
|
||||||
<article class="card">
|
<table>
|
||||||
<div class="card-head">
|
<thead><tr><th>Milter</th><th>State</th><th>Detail</th></tr></thead>
|
||||||
<h2>Hostname / rDNS <span class="st st-ok">ok</span></h2>
|
<tbody>
|
||||||
<label class="help-link" for="help-open">?</label>
|
<tr>
|
||||||
</div>
|
<td>OpenDKIM</td>
|
||||||
|
<td><span class="st st-ok">ok</span></td>
|
||||||
|
<td class="muted">Listening</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>send-log</td>
|
||||||
|
<td><span class="st st-ok">ok</span></td>
|
||||||
|
<td class="muted">Listening</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="card" id="hostname">
|
||||||
|
<h2>Hostname and reverse DNS <span class="st st-ok">ok</span></h2>
|
||||||
<label>Server hostname</label>
|
<label>Server hostname</label>
|
||||||
<span class="code">mail.example.org</span>
|
<span class="code">mail.example.org</span>
|
||||||
<label>Forward and reverse</label>
|
<label>Forward and reverse lookup</label>
|
||||||
<span class="code">mail.example.org → 203.0.113.10 → mail.example.org</span>
|
<span class="code">203.0.113.10 → mail.example.org</span>
|
||||||
|
<p class="muted">mail.example.org resolves to 203.0.113.10 and the reverse lookup points back at it.</p>
|
||||||
<form class="inline" action="#" onsubmit="return false"><button type="button">Re-check DNS</button></form>
|
<form class="inline" action="#" onsubmit="return false"><button type="button">Re-check DNS</button></form>
|
||||||
</article>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card in-only" id="inbound-status">
|
||||||
|
<h2>Inbound <span class="st st-warn">warn</span></h2>
|
||||||
|
<p><code>INBOUND_RELAY_ENABLE</code> is on. Port 25 accepts mail for 2 domains and forwards it upstream — not to local mailboxes.</p>
|
||||||
|
<p class="muted">One domain has no MX pointing at this server. Recipients are a list or any address at the domain. Open Inbound for the list, MX checks, upstream, and recipient maps.</p>
|
||||||
|
<a href="#inbound">Inbound domains</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="split">
|
||||||
|
<div class="card" id="machine">
|
||||||
|
<h2>Machine <span class="st st-ok">ok</span></h2>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th class="metric">Resource</th><th>Usage</th><th>Detail</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="metric">CPU</td>
|
||||||
|
<td class="metric"><meter value="12" min="0" max="100" low="70" high="90" optimum="10">12%</meter> 12%</td>
|
||||||
|
<td class="muted">4 cores · 4 threads</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="metric">Memory</td>
|
||||||
|
<td class="metric"><meter value="41" min="0" max="100" low="70" high="90" optimum="10">41%</meter> 41%</td>
|
||||||
|
<td class="muted">1.6 GiB used of 4.0 GiB.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="metric">Network</td>
|
||||||
|
<td class="metric">↓ 2.0 KiB/s<br>↑ 1.0 KiB/s</td>
|
||||||
|
<td class="muted">
|
||||||
|
<div>eth0: 1.0 MiB in, 512.0 KiB out</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="card" id="processes">
|
||||||
|
<h2>Processes <span class="st st-ok">ok</span></h2>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Program</th><th>State</th><th>Detail</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>opendkim</td>
|
||||||
|
<td><span class="st st-ok">RUNNING</span></td>
|
||||||
|
<td class="muted">pid 21, uptime 3 days, 4:12:01</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>panel</td>
|
||||||
|
<td><span class="st st-ok">RUNNING</span></td>
|
||||||
|
<td class="muted">pid 18, uptime 3 days, 4:12:03</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>postfix</td>
|
||||||
|
<td><span class="st st-ok">RUNNING</span></td>
|
||||||
|
<td class="muted">pid 42, uptime 3 days, 4:11:58</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>postfix-reload</td>
|
||||||
|
<td><span class="st st-ok">STOPPED</span></td>
|
||||||
|
<td class="muted">Not started</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>cert-reload</td>
|
||||||
|
<td><span class="st st-ok">STOPPED</span></td>
|
||||||
|
<td class="muted">Not started</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>logrotate</td>
|
||||||
|
<td><span class="st st-ok">STOPPED</span></td>
|
||||||
|
<td class="muted">Not started</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" id="configuration">
|
||||||
|
<h2>Configuration</h2>
|
||||||
|
<p class="muted">Regenerates the OpenDKIM and Postfix configuration from the
|
||||||
|
database and reloads both daemons. Use it if you edited the files by hand,
|
||||||
|
restored a backup, or the running configuration looks out of step with the
|
||||||
|
domain and application lists. It does not touch the mail queue or the TLS
|
||||||
|
certificate, and it is safe to run at any time.</p>
|
||||||
|
<form class="inline" action="#" onsubmit="return false">
|
||||||
|
<button type="button">Reload configuration</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<p class="version">SelfPost 1.2.3 · © Mixeme · <a href="#">License (AGPL-3.0)</a></p>
|
<p class="version">SelfPost 1.2.3 · © Mixeme · <a href="#">License (AGPL-3.0)</a></p>
|
||||||
</main>
|
</main>
|
||||||
@@ -253,7 +337,10 @@
|
|||||||
<h1>example.com</h1>
|
<h1>example.com</h1>
|
||||||
<a class="back" href="#domains">← All domains</a>
|
<a class="back" href="#domains">← All domains</a>
|
||||||
<div class="card credential" id="new-credential">
|
<div class="card credential" id="new-credential">
|
||||||
<h2>New application password</h2>
|
<div class="card-head">
|
||||||
|
<h2>New application password</h2>
|
||||||
|
<label class="help-link" for="help-password" title="Why this is shown once">?</label>
|
||||||
|
</div>
|
||||||
<p class="muted">Shown <strong>once only</strong> and not stored. Copy it now.</p>
|
<p class="muted">Shown <strong>once only</strong> and not stored. Copy it now.</p>
|
||||||
<label>Login</label>
|
<label>Login</label>
|
||||||
<div class="code-row"><span class="code">newsletter</span><button type="button" class="copy">Copy</button></div>
|
<div class="code-row"><span class="code">newsletter</span><button type="button" class="copy">Copy</button></div>
|
||||||
@@ -261,7 +348,10 @@
|
|||||||
<div class="code-row"><span class="code">xK.9fQ2m-pL7wR</span><button type="button" class="copy">Copy</button></div>
|
<div class="code-row"><span class="code">xK.9fQ2m-pL7wR</span><button type="button" class="copy">Copy</button></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card" id="dns-status">
|
<div class="card" id="dns-status">
|
||||||
<h2>DNS status <span class="st st-ok">ok</span></h2>
|
<div class="card-head">
|
||||||
|
<h2>DNS status <span class="st st-ok">ok</span></h2>
|
||||||
|
<label class="help-link" for="help-dns" title="What these checks mean">?</label>
|
||||||
|
</div>
|
||||||
<div class="check-cols">
|
<div class="check-cols">
|
||||||
<div class="check-col">
|
<div class="check-col">
|
||||||
<label>DKIM <span class="st st-ok">ok</span></label>
|
<label>DKIM <span class="st st-ok">ok</span></label>
|
||||||
@@ -284,8 +374,11 @@
|
|||||||
<form class="inline" action="#" onsubmit="return false"><button type="button">Re-check</button></form>
|
<form class="inline" action="#" onsubmit="return false"><button type="button">Re-check</button></form>
|
||||||
</div>
|
</div>
|
||||||
<div class="split">
|
<div class="split">
|
||||||
<div class="card">
|
<div class="card" id="dkim-spf">
|
||||||
<h2>DKIM and SPF records</h2>
|
<div class="card-head">
|
||||||
|
<h2>DKIM and SPF records</h2>
|
||||||
|
<label class="help-link" for="help-records" title="What to publish">?</label>
|
||||||
|
</div>
|
||||||
<p class="check-col-title">DKIM</p>
|
<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">mail._domainkey.example.com</span><button type="button" class="copy">Copy</button></div></div><div class="field-type"><label>Type</label><span class="code">TXT</span></div></div>
|
<div class="field-pair host-type"><div><label>Host / name</label><div class="code-row"><span class="code">mail._domainkey.example.com</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>Value</label>
|
<label>Value</label>
|
||||||
@@ -293,8 +386,11 @@
|
|||||||
<p class="check-col-title">SPF</p>
|
<p class="check-col-title">SPF</p>
|
||||||
<div class="code-row"><span class="code">v=spf1 ip4:203.0.113.10 -all</span><button type="button" class="copy">Copy</button></div>
|
<div class="code-row"><span class="code">v=spf1 ip4:203.0.113.10 -all</span><button type="button" class="copy">Copy</button></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card" id="dmarc-record">
|
||||||
<h2>DMARC record</h2>
|
<div class="card-head">
|
||||||
|
<h2>DMARC record</h2>
|
||||||
|
<label class="help-link" for="help-dmarc" title="What this record does">?</label>
|
||||||
|
</div>
|
||||||
<div class="field-pair host-type"><div><label>Host / name</label><div class="code-row"><span class="code">_dmarc.example.com</span><button type="button" class="copy">Copy</button></div></div><div class="field-type"><label>Type</label><span class="code">TXT</span></div></div>
|
<div class="field-pair host-type"><div><label>Host / name</label><div class="code-row"><span class="code">_dmarc.example.com</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>Value <span class="muted">(from Settings)</span></label>
|
<label>Value <span class="muted">(from Settings)</span></label>
|
||||||
<div class="code-row"><span class="code">v=DMARC1; p=none; rua=mailto:dmarc@mail.example.org</span><button type="button" class="copy">Copy</button></div>
|
<div class="code-row"><span class="code">v=DMARC1; p=none; rua=mailto:dmarc@mail.example.org</span><button type="button" class="copy">Copy</button></div>
|
||||||
@@ -302,16 +398,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="split">
|
<div class="split">
|
||||||
<div class="card">
|
<div class="card" id="connection">
|
||||||
<h2>Connection settings</h2>
|
<div class="card-head">
|
||||||
|
<h2>Connection settings</h2>
|
||||||
|
<label class="help-link" for="help-connection" title="How to connect">?</label>
|
||||||
|
</div>
|
||||||
<label>Server</label>
|
<label>Server</label>
|
||||||
<div class="code-row"><span class="code">mail.example.org</span><button type="button" class="copy">Copy</button></div>
|
<div class="code-row"><span class="code">mail.example.org</span><button type="button" class="copy">Copy</button></div>
|
||||||
<label>Port and encryption</label>
|
<label>Port and encryption</label>
|
||||||
<span class="code">465 — SSL/TLS (implicit)
|
<span class="code">465 — SSL/TLS (implicit)
|
||||||
587 — STARTTLS (submission)</span>
|
587 — STARTTLS (submission)</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card" id="add-application">
|
||||||
<h2>Add an application</h2>
|
<div class="card-head">
|
||||||
|
<h2>Add an application</h2>
|
||||||
|
<label class="help-link" for="help-apps" title="What an application is">?</label>
|
||||||
|
</div>
|
||||||
<form action="#" onsubmit="return false">
|
<form action="#" onsubmit="return false">
|
||||||
<label>Login</label>
|
<label>Login</label>
|
||||||
<input placeholder="prod-server" autocomplete="off">
|
<input placeholder="prod-server" autocomplete="off">
|
||||||
@@ -321,8 +423,11 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card" id="applications">
|
||||||
<h2>Applications</h2>
|
<div class="card-head">
|
||||||
|
<h2>Applications</h2>
|
||||||
|
<label class="help-link" for="help-apps" title="Address mode and rate limits">?</label>
|
||||||
|
</div>
|
||||||
<ul class="apps">
|
<ul class="apps">
|
||||||
<li class="app-item">
|
<li class="app-item">
|
||||||
<p class="app-login">newsletter</p>
|
<p class="app-login">newsletter</p>
|
||||||
@@ -361,8 +466,11 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card" id="domain-settings">
|
||||||
<h2>Domain settings</h2>
|
<div class="card-head">
|
||||||
|
<h2>Domain settings</h2>
|
||||||
|
<label class="help-link" for="help-domain-settings" title="Reports and rate limits">?</label>
|
||||||
|
</div>
|
||||||
<div class="check-cols">
|
<div class="check-cols">
|
||||||
<div class="check-col">
|
<div class="check-col">
|
||||||
<p class="check-col-title">DMARC reports</p>
|
<p class="check-col-title">DMARC reports</p>
|
||||||
@@ -381,8 +489,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="split">
|
<div class="split">
|
||||||
<div class="card">
|
<div class="card" id="export-domain">
|
||||||
<h2>Export domain</h2>
|
<div class="card-head">
|
||||||
|
<h2>Export domain</h2>
|
||||||
|
<label class="help-link" for="help-export" title="What the file contains">?</label>
|
||||||
|
</div>
|
||||||
<p class="muted"><strong>Secret file</strong> — transfer securely, or encrypt as <code>.spde</code>.</p>
|
<p class="muted"><strong>Secret file</strong> — transfer securely, or encrypt as <code>.spde</code>.</p>
|
||||||
<div class="encrypt">
|
<div class="encrypt">
|
||||||
<label class="check"><input type="checkbox"> <span>Encrypt with a password</span></label>
|
<label class="check"><input type="checkbox"> <span>Encrypt with a password</span></label>
|
||||||
@@ -608,27 +719,33 @@ Aug 15 20:02:11 mail postfix/smtp[226]: 4B19C0BB12: to=<noreply@blocked.examp
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- BACKUP -->
|
<!-- BACKUP -->
|
||||||
<main id="backup" class="screen form">
|
<main id="backup" class="screen ops">
|
||||||
<h1>Backup & migration</h1>
|
<h1>Backup & migration</h1>
|
||||||
<div class="card">
|
<div class="split">
|
||||||
<h2>Full backup</h2>
|
<div class="card">
|
||||||
<p class="muted">Download a full backup of all persistent state. Restore into a container of the <strong>same SelfPost version</strong>. TLS certificates and the mail queue are not included. The file is a secret.</p>
|
<h2>Full backup</h2>
|
||||||
<div class="encrypt">
|
<p class="muted">Download a full backup of all persistent state — the database, every domain’s DKIM key and the application credentials. Restore into a container of the <strong>same SelfPost version</strong>, with the same data mount, before first start. TLS certificates and the mail queue are not included.</p>
|
||||||
<label class="check"><input type="checkbox" checked> <span>Encrypt with a password</span></label>
|
<p class="muted"><strong>The backup file is a secret.</strong> Encrypting it is the simplest way to store it: the download is then a <code>.spbk</code> that only the password opens.</p>
|
||||||
<div class="encrypt-fields">
|
<div class="encrypt">
|
||||||
<label>Password</label><input type="password" placeholder="at least 8 characters">
|
<label class="check"><input type="checkbox" checked> <span>Encrypt with a password</span></label>
|
||||||
<label>Repeat password</label><input type="password">
|
<div class="encrypt-fields">
|
||||||
<p class="muted">Keep this password: without it the file cannot be opened.</p>
|
<label>Password</label><input type="password" placeholder="at least 8 characters">
|
||||||
|
<label>Repeat password</label><input type="password">
|
||||||
|
<p class="muted">Keep this password: without it the file cannot be opened.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button type="button">Download full backup</button>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>Import a domain</h2>
|
||||||
|
<p class="muted">Move a single domain here from another SelfPost instance — plain <code>.json</code> or encrypted <code>.spde</code>. Its DKIM key and application passwords come across, so the published DNS record needs no change. The export file is a secret, like a full backup.</p>
|
||||||
|
<label>Domain export file</label>
|
||||||
|
<input type="file">
|
||||||
|
<label>Password</label>
|
||||||
|
<input type="password" autocomplete="off">
|
||||||
|
<p class="muted">Needed for a <code>.spde</code> file. Leave empty for plain <code>.json</code>.</p>
|
||||||
|
<button type="button">Import domain</button>
|
||||||
</div>
|
</div>
|
||||||
<button type="button">Download full backup</button>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<h2>Import a domain</h2>
|
|
||||||
<p class="muted">Plain <code>.json</code> or encrypted <code>.spde</code>.</p>
|
|
||||||
<label>Domain export file</label>
|
|
||||||
<input type="file">
|
|
||||||
<button type="button">Import domain</button>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@@ -665,9 +782,11 @@ Aug 15 20:02:11 mail postfix/smtp[226]: 4B19C0BB12: to=<noreply@blocked.examp
|
|||||||
<label class="check"><input type="checkbox"> example.com</label>
|
<label class="check"><input type="checkbox"> example.com</label>
|
||||||
<label class="check"><input type="checkbox" checked> alerts.example.com</label>
|
<label class="check"><input type="checkbox" checked> alerts.example.com</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<button type="button">Save</button>
|
<div class="form-actions">
|
||||||
|
<button type="button">Save</button>
|
||||||
|
<a class="danger" href="#user-delete">Delete user</a>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<a class="danger" href="#user-delete">Delete user</a>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@@ -683,31 +802,60 @@ Aug 15 20:02:11 mail postfix/smtp[226]: 4B19C0BB12: to=<noreply@blocked.examp
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- SETTINGS -->
|
<!-- SETTINGS -->
|
||||||
<main id="settings" class="screen form">
|
<main id="settings" class="screen ops">
|
||||||
<h1>Settings</h1>
|
<h1>Settings</h1>
|
||||||
<div class="card">
|
<form action="#" onsubmit="return false">
|
||||||
<h2>Panel credentials</h2>
|
<div class="split">
|
||||||
<p class="muted">These are the credentials for this control panel only. Applications keep their own logins.</p>
|
<div class="card">
|
||||||
<label>Username</label><input value="admin" autocomplete="username">
|
<h2>Panel credentials</h2>
|
||||||
<label>Current password</label><input type="password" autocomplete="current-password">
|
<p class="muted">These are the credentials for this control panel only. Applications keep their own logins and passwords, which are not affected.</p>
|
||||||
<label>New password</label><input type="password" autocomplete="new-password">
|
<label>Username</label><input value="admin" autocomplete="username">
|
||||||
<label>Confirm new password</label><input type="password">
|
<label>Current password</label><input type="password" autocomplete="current-password">
|
||||||
</div>
|
<label>New password</label><input type="password" autocomplete="new-password">
|
||||||
<div class="card g-only">
|
<label>Confirm new password</label><input type="password">
|
||||||
<h2>DMARC aggregate reports</h2>
|
</div>
|
||||||
<p class="muted">Default <code>rua=</code> for every sending domain. When ingest is on, this can be an address SelfPost accepts.</p>
|
<div class="card g-only">
|
||||||
<label>Default report address</label>
|
<h2>DMARC aggregate reports</h2>
|
||||||
<input type="email" value="dmarc@mail.example.org">
|
<p class="muted">Default <code>rua=</code> for every sending domain (overridable per domain). When ingest is on, this can be an address SelfPost accepts.</p>
|
||||||
<p class="muted"><a href="#dmarc">DMARC reports</a> in the panel.</p>
|
<label>Default report address</label>
|
||||||
</div>
|
<input type="email" value="dmarc@mail.example.org">
|
||||||
<button type="button">Save changes</button>
|
<p class="muted">When <code>rua=</code> points at another domain, that hub must publish a report-authorisation record. <a href="#dmarc">DMARC reports</a> in the panel.</p>
|
||||||
|
<div class="field-pair host-type">
|
||||||
|
<div>
|
||||||
|
<label>Host / name</label>
|
||||||
|
<div class="code-row"><span class="code">mail.example.org._report._dmarc.example.com</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>Value</label>
|
||||||
|
<div class="code-row"><span class="code">v=DMARC1;</span><button type="button" class="copy">Copy</button></div>
|
||||||
|
<label>Report authorization DNS <span class="st st-ok">ok</span></label>
|
||||||
|
<p class="muted">Published at mail.example.org._report._dmarc.example.com — aggregate reports addressed to dmarc@mail.example.org are authorised.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="button">Save changes</button>
|
||||||
|
<p class="muted">Leave both new-password fields empty to change the username or DMARC address only. Changing the password signs out every other session; this one stays signed in.</p>
|
||||||
|
</form>
|
||||||
<div class="card" id="rate-limits">
|
<div class="card" id="rate-limits">
|
||||||
<h2>Sending rate limits</h2>
|
<h2>Sending rate limits</h2>
|
||||||
<p class="muted">Level 1 is set in Compose. Domain and application ceilings live on each domain’s page.</p>
|
<p class="muted">Level 1 is set in Compose; restart the container to change it. Domain and application ceilings live on each domain’s page.</p>
|
||||||
<div class="facts">
|
<div class="check-cols">
|
||||||
<div class="fact"><span class="fact-label">Level 1 / IP</span><span class="fact-value">100 / 60s</span></div>
|
<div class="check-col">
|
||||||
<div class="fact"><span class="fact-label">Level 2 domain</span><span class="fact-value">optional, ≤ L1</span></div>
|
<p class="check-col-title">Level 1 — per client IP</p>
|
||||||
<div class="fact"><span class="fact-label">App override</span><span class="fact-value">trusted IPs only</span></div>
|
<span class="code">100 messages / 60 seconds</span>
|
||||||
|
<p class="muted"><code>RATE_LIMIT_MESSAGES_PER_IP</code> / <code>RATE_LIMIT_WINDOW_SECONDS</code>. Hard ceiling for every connecting IP; the panel cannot raise a domain or application limit above this.</p>
|
||||||
|
</div>
|
||||||
|
<div class="check-col">
|
||||||
|
<p class="check-col-title">Level 2 — domain</p>
|
||||||
|
<p class="muted">Optional ceiling for <em>all</em> senders on a domain. When unset, only level 1 applies. Must be ≤ level 1.</p>
|
||||||
|
</div>
|
||||||
|
<div class="check-col">
|
||||||
|
<p class="check-col-title">Level 2 — application</p>
|
||||||
|
<p class="muted">Optional override for trusted IPs: a ceiling strictly above the domain limit (still ≤ level 1). Those IPs skip the domain check; everyone else stays under the domain (or level 1).</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
@@ -717,7 +865,7 @@ Aug 15 20:02:11 mail postfix/smtp[226]: 4B19C0BB12: to=<noreply@blocked.examp
|
|||||||
<div class="page-head">
|
<div class="page-head">
|
||||||
<h1>Inbound <span class="tag">1.x</span></h1>
|
<h1>Inbound <span class="tag">1.x</span></h1>
|
||||||
</div>
|
</div>
|
||||||
<p class="muted">Backup-MX / forwarder. Accepts on port 25 only for listed domains; unknown recipients are rejected at RCPT. Off by default in Compose.</p>
|
<p class="muted">Backup-MX / forwarder. Accepts on port 25 only for listed domains. Recipients are either an allow-list or any address at that domain. Off by default in Compose.</p>
|
||||||
<div class="card" style="margin-bottom:1rem">
|
<div class="card" style="margin-bottom:1rem">
|
||||||
<h2>Add inbound domain</h2>
|
<h2>Add inbound domain</h2>
|
||||||
<form action="#" onsubmit="return false">
|
<form action="#" onsubmit="return false">
|
||||||
@@ -731,20 +879,31 @@ Aug 15 20:02:11 mail postfix/smtp[226]: 4B19C0BB12: to=<noreply@blocked.examp
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Forwarding</h2>
|
<h2>Forwarding</h2>
|
||||||
<table class="desk-only">
|
<table class="desk-only">
|
||||||
<thead><tr><th>Domain</th><th>Upstream</th><th>Recipients</th><th>TLS</th><th></th></tr></thead>
|
<thead><tr><th>Domain</th><th>DNS</th><th>Upstream</th><th>Recipients</th><th>TLS</th><th></th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="#inbound-domain">lists.example.com</a></td>
|
<td><a href="#inbound-domain">lists.example.com</a></td>
|
||||||
|
<td><a class="st st-ok" href="#inbound-domain">ok</a></td>
|
||||||
<td class="muted">10.0.0.8:25</td>
|
<td class="muted">10.0.0.8:25</td>
|
||||||
<td>12 listed</td>
|
<td>12 listed</td>
|
||||||
<td><span class="st st-ok">required</span></td>
|
<td><span class="st st-ok">required</span></td>
|
||||||
<td class="actions"><a href="#inbound-domain">Edit</a></td>
|
<td class="actions"><a class="danger" href="#inbound-delete">Delete</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><a href="#inbound-backup">backup.example.net</a></td>
|
||||||
|
<td><a class="st st-error" href="#inbound-backup">error</a></td>
|
||||||
|
<td class="muted">192.0.2.20:25</td>
|
||||||
|
<td>any</td>
|
||||||
|
<td><span class="st st-unknown">off</span></td>
|
||||||
|
<td class="actions"><a class="danger" href="#inbound-delete">Delete</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<ul class="phone-list phone-only">
|
<ul class="phone-list phone-only">
|
||||||
<li><a href="#inbound-domain"><span>lists.example.com</span><span class="st st-ok">tls</span><span class="pair">→ 10.0.0.8:25 · 12 recipients</span></a></li>
|
<li><a href="#inbound-domain"><span>lists.example.com</span><span class="st st-ok">ok</span><span class="pair">12 listed · 10.0.0.8:25</span></a></li>
|
||||||
|
<li><a href="#inbound-backup"><span>backup.example.net</span><span class="st st-error">error</span><span class="pair">any recipient · 192.0.2.20:25</span></a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<p class="muted">The <em>DNS</em> badge is the MX check: at least one MX must point at this server. Results are cached for a few minutes; open a domain for the lookup and a <em>Re-check</em> button.</p>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@@ -752,6 +911,32 @@ Aug 15 20:02:11 mail postfix/smtp[226]: 4B19C0BB12: to=<noreply@blocked.examp
|
|||||||
<main id="inbound-domain" class="screen ops">
|
<main id="inbound-domain" class="screen ops">
|
||||||
<h1>lists.example.com</h1>
|
<h1>lists.example.com</h1>
|
||||||
<a class="back" href="#inbound">← All inbound domains</a>
|
<a class="back" href="#inbound">← All inbound domains</a>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>DNS status <span class="st st-ok">ok</span></h2>
|
||||||
|
<p class="muted">Cached a few minutes — use <em>Re-check</em> after publishing. Unlike outbound, inbound needs an MX pointing at this server.</p>
|
||||||
|
<div class="check-cols">
|
||||||
|
<div class="check-col">
|
||||||
|
<label>MX <span class="st st-ok">ok</span></label>
|
||||||
|
<div class="field-pair host-type">
|
||||||
|
<div>
|
||||||
|
<label>Host / name</label>
|
||||||
|
<span class="code">lists.example.com</span>
|
||||||
|
</div>
|
||||||
|
<div class="field-type">
|
||||||
|
<label>Type</label>
|
||||||
|
<span class="code">MX</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label>Value</label>
|
||||||
|
<span class="code">10 mail.example.org.
|
||||||
|
20 mail.primary.example.net.</span>
|
||||||
|
<p class="muted">An MX points at mail.example.org (this server). Other MX values are the domain’s own primaries — they are not an error.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form class="inline" action="#" onsubmit="return false"><button type="button">Re-check</button></form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="split">
|
<div class="split">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Upstream</h2>
|
<h2>Upstream</h2>
|
||||||
@@ -763,26 +948,147 @@ Aug 15 20:02:11 mail postfix/smtp[226]: 4B19C0BB12: to=<noreply@blocked.examp
|
|||||||
<button type="button">Save upstream</button>
|
<button type="button">Save upstream</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>DNS — MX</h2>
|
<h2>MX record to publish</h2>
|
||||||
<p class="muted">Unlike outbound, inbound needs an MX pointing at this server.</p>
|
<p class="muted">Add this MX so the internet delivers here. Keep any existing primary MX if this is backup-MX.</p>
|
||||||
<label>Host / name</label>
|
<div class="field-pair host-type">
|
||||||
<div class="code-row"><span class="code">lists.example.com</span><button type="button" class="copy">Copy</button></div>
|
<div>
|
||||||
|
<label>Host / name</label>
|
||||||
|
<div class="code-row"><span class="code">lists.example.com</span><button type="button" class="copy">Copy</button></div>
|
||||||
|
</div>
|
||||||
|
<div class="field-type">
|
||||||
|
<label>Type</label>
|
||||||
|
<span class="code">MX</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<label>Value</label>
|
<label>Value</label>
|
||||||
<div class="code-row"><span class="code">10 mail.example.org.</span><button type="button" class="copy">Copy</button></div>
|
<div class="code-row"><span class="code">10 mail.example.org.</span><button type="button" class="copy">Copy</button></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="split">
|
||||||
<h2>Valid recipients</h2>
|
<div class="card rcpt-mode">
|
||||||
<p class="muted">Unknown recipients are rejected at RCPT so this relay does not generate backscatter. One address per line; empty means reject all until listed.</p>
|
<h2>Valid recipients</h2>
|
||||||
<textarea rows="6">staff@lists.example.com
|
<p class="muted">Who this domain accepts on port 25. Same idea as an application’s address mode: a list, or any address at the domain.</p>
|
||||||
|
<label>Who to accept</label>
|
||||||
|
<select>
|
||||||
|
<option value="list" selected>Listed addresses only</option>
|
||||||
|
<option value="any">Any recipient at this domain</option>
|
||||||
|
</select>
|
||||||
|
<div class="rcpt-list">
|
||||||
|
<label>Addresses (one per line or comma-separated)</label>
|
||||||
|
<textarea rows="6">staff@lists.example.com
|
||||||
postmaster@lists.example.com
|
postmaster@lists.example.com
|
||||||
abuse@lists.example.com</textarea>
|
abuse@lists.example.com</textarea>
|
||||||
<button type="button">Save recipients</button>
|
<p class="muted">Unknown recipients are rejected at RCPT so this relay does not generate backscatter.</p>
|
||||||
|
</div>
|
||||||
|
<p class="rcpt-any muted">Every address at this domain is accepted and forwarded. Prefer a list unless the upstream rejects unknowns — otherwise this relay may generate backscatter.</p>
|
||||||
|
<button type="button">Save recipients</button>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>Danger zone</h2>
|
||||||
|
<p class="muted">Stops accepting mail for this domain. Does not touch outbound sending domains.</p>
|
||||||
|
<a class="danger" href="#inbound-delete">Delete inbound domain</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- INBOUND DOMAIN (MX error) -->
|
||||||
|
<main id="inbound-backup" class="screen ops">
|
||||||
|
<h1>backup.example.net</h1>
|
||||||
|
<a class="back" href="#inbound">← All inbound domains</a>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Danger zone</h2>
|
<h2>DNS status <span class="st st-error">error</span></h2>
|
||||||
<p class="muted">Stops accepting mail for this domain. Does not touch outbound sending domains.</p>
|
<p class="muted">Cached a few minutes — use <em>Re-check</em> after publishing. Unlike outbound, inbound needs an MX pointing at this server.</p>
|
||||||
<button type="button" class="danger">Remove inbound domain</button>
|
<div class="check-cols">
|
||||||
|
<div class="check-col">
|
||||||
|
<label>MX <span class="st st-error">error</span></label>
|
||||||
|
<div class="field-pair host-type">
|
||||||
|
<div>
|
||||||
|
<label>Host / name</label>
|
||||||
|
<span class="code">backup.example.net</span>
|
||||||
|
</div>
|
||||||
|
<div class="field-type">
|
||||||
|
<label>Type</label>
|
||||||
|
<span class="code">MX</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label>Value</label>
|
||||||
|
<span class="code">10 mail.primary.example.net.</span>
|
||||||
|
<p class="error">No MX points at mail.example.org (this server). Publish the record below, or wait for DNS to propagate and Re-check.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form class="inline" action="#" onsubmit="return false"><button type="button">Re-check</button></form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="split">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Upstream</h2>
|
||||||
|
<p class="muted">Where accepted mail is handed off. Not a mailbox.</p>
|
||||||
|
<label>Host</label><input value="192.0.2.20">
|
||||||
|
<label>Port</label><input value="25">
|
||||||
|
<label>TLS to upstream</label>
|
||||||
|
<select><option>Opportunistic</option><option>Required</option><option selected>Off</option></select>
|
||||||
|
<button type="button">Save upstream</button>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>MX record to publish</h2>
|
||||||
|
<p class="muted">Add this MX so the internet delivers here. Keep any existing primary MX if this is backup-MX.</p>
|
||||||
|
<div class="field-pair host-type">
|
||||||
|
<div>
|
||||||
|
<label>Host / name</label>
|
||||||
|
<div class="code-row"><span class="code">backup.example.net</span><button type="button" class="copy">Copy</button></div>
|
||||||
|
</div>
|
||||||
|
<div class="field-type">
|
||||||
|
<label>Type</label>
|
||||||
|
<span class="code">MX</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label>Value</label>
|
||||||
|
<div class="code-row"><span class="code">20 mail.example.org.</span><button type="button" class="copy">Copy</button></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="split">
|
||||||
|
<div class="card rcpt-mode">
|
||||||
|
<h2>Valid recipients</h2>
|
||||||
|
<p class="muted">Who this domain accepts on port 25. Same idea as an application’s address mode: a list, or any address at the domain.</p>
|
||||||
|
<label>Who to accept</label>
|
||||||
|
<select>
|
||||||
|
<option value="list">Listed addresses only</option>
|
||||||
|
<option value="any" selected>Any recipient at this domain</option>
|
||||||
|
</select>
|
||||||
|
<div class="rcpt-list">
|
||||||
|
<label>Addresses (one per line or comma-separated)</label>
|
||||||
|
<textarea rows="6">postmaster@backup.example.net
|
||||||
|
abuse@backup.example.net</textarea>
|
||||||
|
<p class="muted">Unknown recipients are rejected at RCPT so this relay does not generate backscatter.</p>
|
||||||
|
</div>
|
||||||
|
<p class="rcpt-any muted">Every address at this domain is accepted and forwarded. Prefer a list unless the upstream rejects unknowns — otherwise this relay may generate backscatter.</p>
|
||||||
|
<button type="button">Save recipients</button>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>Danger zone</h2>
|
||||||
|
<p class="muted">Stops accepting mail for this domain. Does not touch outbound sending domains.</p>
|
||||||
|
<a class="danger" href="#inbound-delete">Delete inbound domain</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- INBOUND DELETE -->
|
||||||
|
<main id="inbound-delete" class="screen form">
|
||||||
|
<h1>Delete lists.example.com</h1>
|
||||||
|
<a class="back" href="#inbound-domain">← Back to lists.example.com</a>
|
||||||
|
<div class="card">
|
||||||
|
<h2>Confirm deletion</h2>
|
||||||
|
<p>You are about to stop accepting inbound mail for <strong>lists.example.com</strong>. This will:</p>
|
||||||
|
<ul>
|
||||||
|
<li>remove it from <code>relay_domains</code> and the recipient map;</li>
|
||||||
|
<li>stop forwarding to <strong>10.0.0.8:25</strong>;</li>
|
||||||
|
<li>leave outbound sending domains untouched.</li>
|
||||||
|
</ul>
|
||||||
|
<p class="muted">This cannot be undone from a backup of inbound maps alone unless you restore one. Remove the MX if you do not plan to re-add the domain.</p>
|
||||||
|
<form action="#" onsubmit="return false">
|
||||||
|
<button type="button" class="danger">Delete lists.example.com</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@@ -829,7 +1135,7 @@ abuse@lists.example.com</textarea>
|
|||||||
<div class="page-head">
|
<div class="page-head">
|
||||||
<h1>Help <span class="tag future">candidate</span></h1>
|
<h1>Help <span class="tag future">candidate</span></h1>
|
||||||
</div>
|
</div>
|
||||||
<p class="muted">Short operator notes inside the panel — not a second copy of the full guide. Seeded from the Status blurbs that left the cards.</p>
|
<p class="muted">Short operator notes inside the panel — not a second copy of the full guide. Seeded from the Status explanations that do not belong on the cards (what a kernel counter is, why PTR is set at the host, what Reload does not touch). The cards themselves keep their readings, Detail columns, and the Configuration control.</p>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>On this panel</h2>
|
<h2>On this panel</h2>
|
||||||
<ul>
|
<ul>
|
||||||
@@ -837,9 +1143,10 @@ abuse@lists.example.com</textarea>
|
|||||||
<li><a href="#help">TLS certificate</a> — port 465, reverse-proxy mount</li>
|
<li><a href="#help">TLS certificate</a> — port 465, reverse-proxy mount</li>
|
||||||
<li><a href="#help">Hostname / reverse DNS</a> — forward-confirmed PTR at the hosting provider</li>
|
<li><a href="#help">Hostname / reverse DNS</a> — forward-confirmed PTR at the hosting provider</li>
|
||||||
<li><a href="#help">Mail queue retries</a> — time-based, no attempt budget</li>
|
<li><a href="#help">Mail queue retries</a> — time-based, no attempt budget</li>
|
||||||
<li><a href="#help">Inbound</a> — not mailboxes; reject unknown recipients</li>
|
<li><a href="#help">Inbound</a> — not mailboxes; listed recipients or any address at the domain</li>
|
||||||
|
<li>Domain page — DNS, records, connection, applications, export (drawer from each card’s «?»)</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p class="muted">The same texts open in the drawer from a card’s «?» — so a Status page can stay dense.</p>
|
<p class="muted">The same texts open in the drawer from Status’s «?» — so a card can stay a reading, not a paragraph, without throwing the reading away.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Machine</h2>
|
<h2>Machine</h2>
|
||||||
@@ -858,17 +1165,94 @@ abuse@lists.example.com</textarea>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label class="help-scrim" for="help-open"></label>
|
<label class="help-scrim" for="help-off"></label>
|
||||||
<aside class="help-drawer">
|
<aside class="help-drawer">
|
||||||
<label class="help-close" for="help-open">Close</label>
|
<label class="help-close" for="help-off">Close</label>
|
||||||
<h2>Machine</h2>
|
|
||||||
<p>CPU and memory are the container’s readings. Network is a short rate window. These numbers explain load; they do not replace the queue.</p>
|
<article class="help-pane help-pane-index">
|
||||||
<h2>TLS certificate</h2>
|
<h2>Help</h2>
|
||||||
<p>Presented on port 465. Issued and renewed outside SelfPost. Warn = expires soon; error = missing, and submission will fail.</p>
|
<p>Short notes for the card you opened — not a second copy of the guide.</p>
|
||||||
<h2>Hostname / reverse DNS</h2>
|
<p class="muted">Status</p>
|
||||||
<p>The hostname must forward to this IP and the PTR must come back to the same name. Set PTR at the provider.</p>
|
<ul class="toc">
|
||||||
<h2>Mail queue</h2>
|
<li><label for="help-status">Status checks</label></li>
|
||||||
<p>Deferred mail is retried on a time schedule (first delay, doubling cap, queue lifetime). There is no “attempt 3 of N”.</p>
|
</ul>
|
||||||
|
<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. Network is a short rate window. These numbers explain load; they do not replace the queue.</p>
|
||||||
|
<h2>TLS certificate</h2>
|
||||||
|
<p>Presented on port 465. Issued and renewed outside SelfPost. Warn = expires soon; error = missing, and submission will fail.</p>
|
||||||
|
<h2>Hostname / reverse DNS</h2>
|
||||||
|
<p>The hostname must forward to this IP and the PTR must come back to the same name. Set PTR at the provider.</p>
|
||||||
|
<h2>Mail queue</h2>
|
||||||
|
<p>Deferred mail is retried on a time schedule (first delay, doubling cap, queue lifetime). There is no “attempt 3 of N”.</p>
|
||||||
|
<p class="more muted"><label for="help-index">All topics</label></p>
|
||||||
|
</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>
|
||||||
|
<p class="more muted"><label for="help-index">All topics</label></p>
|
||||||
|
</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>
|
||||||
|
<p class="more muted"><label for="help-index">All topics</label></p>
|
||||||
|
</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>
|
||||||
|
<p class="more muted"><label for="help-index">All topics</label></p>
|
||||||
|
</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>
|
||||||
|
<p class="more muted"><label for="help-index">All topics</label></p>
|
||||||
|
</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>
|
||||||
|
<p class="more muted"><label for="help-index">All topics</label></p>
|
||||||
|
</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>
|
||||||
|
<p class="more muted"><label for="help-index">All topics</label></p>
|
||||||
|
</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>
|
||||||
|
<p class="more muted"><label for="help-index">All topics</label></p>
|
||||||
|
</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>
|
||||||
|
<p class="more muted"><label for="help-index">All topics</label></p>
|
||||||
|
</article>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -883,7 +1267,8 @@ abuse@lists.example.com</textarea>
|
|||||||
var h = location.hash.slice(1);
|
var h = location.hash.slice(1);
|
||||||
var blocked = {
|
var blocked = {
|
||||||
status: 1, "mail-queue": 1, "system-log": 1, backup: 1, users: 1,
|
status: 1, "mail-queue": 1, "system-log": 1, backup: 1, users: 1,
|
||||||
"user-form": 1, "user-delete": 1, inbound: 1, "inbound-domain": 1, dmarc: 1
|
"user-form": 1, "user-delete": 1, inbound: 1, "inbound-domain": 1,
|
||||||
|
"inbound-backup": 1, "inbound-delete": 1, dmarc: 1
|
||||||
};
|
};
|
||||||
if (blocked[h] || !h) location.hash = "domains";
|
if (blocked[h] || !h) location.hash = "domains";
|
||||||
var brand = document.querySelector(".nav .brand");
|
var brand = document.querySelector(".nav .brand");
|
||||||
|
|||||||
@@ -23,9 +23,9 @@
|
|||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2>Ширина окна</h2>
|
<h2>Ширина окна</h2>
|
||||||
<p>Сейчас оболочка центрируется, колонка контента упирается в <code>64rem</code>, формы ещё уже — <code>48rem</code>. На широком мониторе поля пустые. Растянуть всё на 100% окна нельзя: поля Settings и Backup становятся нечитаемыми.</p>
|
<p>Сейчас оболочка центрируется, колонка контента упирается в <code>64rem</code>, формы ещё уже — <code>48rem</code>. На широком мониторе поля пустые. Растянуть поля на 100% окна нельзя — они становятся нечитаемыми. Две независимые карточки рядом — можно.</p>
|
||||||
<h3>Гибрид</h3>
|
<h3>Гибрид</h3>
|
||||||
<p>Навигация прижата влево. Ops-страницы (Status, Deliveries, очередь, лог, DMARC, inbound) занимают остаток до <strong>90rem (~1440px)</strong>. Формы остаются <strong>42rem и выровнены влево</strong> в колонке, а не висят по центру пустого поля. Login и setup — по-прежнему узкий центрированный блок.</p>
|
<p>Навигация прижата влево. Ops-страницы (Status, Deliveries, очередь, лог, Backup, Settings, DMARC, inbound) занимают остаток до <strong>90rem (~1440px)</strong>. Backup — полный бэкап ‖ импорт. Settings — учётные данные ‖ DMARC (у domain-admin остаётся одна узкая карточка). Одиночные формы (Users, подтверждения) остаются <strong>42rem и выровнены влево</strong>. Login и setup — по-прежнему узкий центрированный блок.</p>
|
||||||
<div class="compare">
|
<div class="compare">
|
||||||
<div>
|
<div>
|
||||||
<div class="row">сейчас, окно ~1600px</div>
|
<div class="row">сейчас, окно ~1600px</div>
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
<li>Inbound <span class="tag">1.x</span> — скрыт, пока <code>INBOUND_RELAY_ENABLE</code> выключен</li>
|
<li>Inbound <span class="tag">1.x</span> — скрыт, пока <code>INBOUND_RELAY_ENABLE</code> выключен</li>
|
||||||
<li>DMARC reports <span class="tag future">candidate</span></li>
|
<li>DMARC reports <span class="tag future">candidate</span></li>
|
||||||
<li>Backup, Users, Settings</li>
|
<li>Backup, Users, Settings</li>
|
||||||
<li>Help <span class="tag future">candidate</span> — пункт навбара и drawer с карточек Status</li>
|
<li>Help <span class="tag future">candidate</span> — пункт навбара и drawer с карточек Status и страницы домена</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -147,15 +147,17 @@
|
|||||||
<div class="screen-index">
|
<div class="screen-index">
|
||||||
<a href="app.html#mail-queue">queue-retries — карточка на Mail queue</a>
|
<a href="app.html#mail-queue">queue-retries — карточка на Mail queue</a>
|
||||||
<a href="app.html#delivery">queue-retries — history deferred</a>
|
<a href="app.html#delivery">queue-retries — history deferred</a>
|
||||||
<a href="app.html#inbound">Inbound list</a>
|
<a href="app.html#inbound">Inbound list (колонка DNS = MX)</a>
|
||||||
<a href="app.html#inbound-domain">Inbound domain</a>
|
<a href="app.html#inbound-domain">Inbound domain — DNS ok</a>
|
||||||
|
<a href="app.html#inbound-backup">Inbound domain — DNS error</a>
|
||||||
|
<a href="app.html#inbound-delete">Delete inbound domain</a>
|
||||||
<a href="app.html?inbound=0#status">Inbound выключен (пункта нет)</a>
|
<a href="app.html?inbound=0#status">Inbound выключен (пункта нет)</a>
|
||||||
<a href="app.html#dmarc">DMARC reports <span class="tag future">candidate</span></a>
|
<a href="app.html#dmarc">DMARC reports <span class="tag future">candidate</span></a>
|
||||||
<a href="app.html#help">Help <span class="tag future">candidate</span></a>
|
<a href="app.html#help">Help <span class="tag future">candidate</span></a>
|
||||||
</div>
|
</div>
|
||||||
<h3>Аварийный телефон</h3>
|
<h3>Аварийный телефон</h3>
|
||||||
<div class="screen-index">
|
<div class="screen-index">
|
||||||
<a href="app.html?view=phone#status">Status 390px — overall и warn сверху</a>
|
<a href="app.html?view=phone#status">Status 390px — Overall, then queue/cert, then the rest</a>
|
||||||
<a href="app.html?view=phone#mail-queue">Mail queue 390px</a>
|
<a href="app.html?view=phone#mail-queue">Mail queue 390px</a>
|
||||||
<a href="app.html?view=phone#deliveries">Deliveries 390px — список, не таблица</a>
|
<a href="app.html?view=phone#deliveries">Deliveries 390px — список, не таблица</a>
|
||||||
<a href="app.html?view=phone#delivery">Delivery 390px</a>
|
<a href="app.html?view=phone#delivery">Delivery 390px</a>
|
||||||
@@ -164,7 +166,7 @@
|
|||||||
|
|
||||||
<section style="border-bottom:none">
|
<section style="border-bottom:none">
|
||||||
<h2>Как смотреть</h2>
|
<h2>Как смотреть</h2>
|
||||||
<p>Откройте <a href="app.html#status">app.html</a> в браузере. Шапка прототипа — не часть панели: роль, Desktop/Phone, inbound, светлая/тёмная. Копирайт экранов английский, как в продукте. Drawer Help открывается с карточек Status (кнопка «?») или из навбара.</p>
|
<p>Откройте <a href="app.html#status">app.html</a> в браузере. Шапка прототипа — не часть панели: роль, Desktop/Phone, inbound, светлая/тёмная. Копирайт экранов английский, как в продукте. Drawer Help открывается с «?» на Status или с карточек домена; на карточках остаются показания и колонка Detail.</p>
|
||||||
<p class="muted">После утверждения макетов вёрстка <code>panel.css</code> и шаблонов — отдельная задача. CSP и progressive enhancement в этом HTML не воспроизводятся один в один: здесь допустимы вещи, которые в панели останутся в файле стилей.</p>
|
<p class="muted">После утверждения макетов вёрстка <code>panel.css</code> и шаблонов — отдельная задача. CSP и progressive enhancement в этом HTML не воспроизводятся один в один: здесь допустимы вещи, которые в панели останутся в файле стилей.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -182,6 +182,8 @@ body:has(#mail-queue:target) .n-queue,
|
|||||||
body:has(#system-log:target) .n-log,
|
body:has(#system-log:target) .n-log,
|
||||||
body:has(#inbound:target) .n-inbound,
|
body:has(#inbound:target) .n-inbound,
|
||||||
body:has(#inbound-domain:target) .n-inbound,
|
body:has(#inbound-domain:target) .n-inbound,
|
||||||
|
body:has(#inbound-backup:target) .n-inbound,
|
||||||
|
body:has(#inbound-delete:target) .n-inbound,
|
||||||
body:has(#dmarc:target) .n-dmarc,
|
body:has(#dmarc:target) .n-dmarc,
|
||||||
body:has(#backup:target) .n-backup,
|
body:has(#backup:target) .n-backup,
|
||||||
body:has(#users:target) .n-users,
|
body:has(#users:target) .n-users,
|
||||||
@@ -205,7 +207,9 @@ body:has(#delivery:target) .pt-delivery,
|
|||||||
body:has(#mail-queue:target) .pt-queue,
|
body:has(#mail-queue:target) .pt-queue,
|
||||||
body:has(#system-log:target) .pt-log,
|
body:has(#system-log:target) .pt-log,
|
||||||
body:has(#inbound:target) .pt-inbound,
|
body:has(#inbound:target) .pt-inbound,
|
||||||
body:has(#inbound-domain:target) .pt-inbound,
|
body:has(#inbound-domain:target) .pt-inbound-domain,
|
||||||
|
body:has(#inbound-backup:target) .pt-inbound-backup,
|
||||||
|
body:has(#inbound-delete:target) .pt-inbound-domain,
|
||||||
body:has(#dmarc:target) .pt-dmarc,
|
body:has(#dmarc:target) .pt-dmarc,
|
||||||
body:has(#backup:target) .pt-backup,
|
body:has(#backup:target) .pt-backup,
|
||||||
body:has(#users:target) .pt-users,
|
body:has(#users:target) .pt-users,
|
||||||
@@ -263,6 +267,10 @@ body:has(#login:target) .app,
|
|||||||
body:has(#setup:target) .app { display: block; }
|
body:has(#setup:target) .app { display: block; }
|
||||||
|
|
||||||
#role-domain:checked ~ .app .g-only { display: none !important; }
|
#role-domain:checked ~ .app .g-only { display: none !important; }
|
||||||
|
#role-domain:checked ~ .app #settings .split {
|
||||||
|
display: block;
|
||||||
|
max-width: var(--form-max);
|
||||||
|
}
|
||||||
#feat-inbound:not(:checked) ~ .app .in-only { display: none !important; }
|
#feat-inbound:not(:checked) ~ .app .in-only { display: none !important; }
|
||||||
#feat-inbound:checked ~ .app .in-off { display: none !important; }
|
#feat-inbound:checked ~ .app .in-off { display: none !important; }
|
||||||
|
|
||||||
@@ -315,11 +323,17 @@ button:hover, a.btn:hover { background: var(--accent-fill-hover); }
|
|||||||
button.danger, a.danger { background: var(--danger-fill); color: var(--on-accent); }
|
button.danger, a.danger { background: var(--danger-fill); color: var(--on-accent); }
|
||||||
button.danger:hover, a.danger:hover { background: var(--danger-fill-hover); }
|
button.danger:hover, a.danger:hover { background: var(--danger-fill-hover); }
|
||||||
form.inline { display: inline; margin: 0; }
|
form.inline { display: inline; margin: 0; }
|
||||||
|
.form-actions {
|
||||||
|
display: flex; flex-wrap: wrap; gap: 0.6rem; align-items: center;
|
||||||
|
margin-top: 1.1rem;
|
||||||
|
}
|
||||||
|
.form-actions > button, .form-actions > a.btn, .form-actions > a.danger { margin-top: 0; }
|
||||||
|
|
||||||
.st {
|
.st {
|
||||||
display: inline-block; padding: 0.14rem 0.45rem 0.28rem; border-radius: 4px;
|
display: inline-block; padding: 0.14rem 0.45rem 0.28rem; border-radius: 4px;
|
||||||
font-family: var(--font-mono); font-size: 0.78rem; font-weight: 500; line-height: 1;
|
font-family: var(--font-mono); font-size: 0.78rem; font-weight: 500; line-height: 1;
|
||||||
letter-spacing: 0.02em; vertical-align: middle; border: 1px solid transparent;
|
letter-spacing: 0.02em; vertical-align: middle; border: 1px solid transparent;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.st-ok { background: var(--st-ok-bg); color: var(--st-ok-fg); border-color: var(--st-ok-border); }
|
.st-ok { background: var(--st-ok-bg); color: var(--st-ok-fg); border-color: var(--st-ok-border); }
|
||||||
.st-warn { background: var(--st-warn-bg); color: var(--st-warn-fg); border-color: var(--st-warn-border); }
|
.st-warn { background: var(--st-warn-bg); color: var(--st-warn-fg); border-color: var(--st-warn-border); }
|
||||||
@@ -328,20 +342,25 @@ form.inline { display: inline; margin: 0; }
|
|||||||
a.st, a.st:hover { color: inherit; text-decoration: none; }
|
a.st, a.st:hover { color: inherit; text-decoration: none; }
|
||||||
|
|
||||||
table { width: 100%; border-collapse: collapse; }
|
table { width: 100%; border-collapse: collapse; }
|
||||||
th, td { text-align: left; padding: 0.5rem 0.45rem; border-bottom: 1px solid var(--border); overflow-wrap: anywhere; }
|
th, td {
|
||||||
|
text-align: left; padding: 0.5rem 0.45rem; border-bottom: 1px solid var(--border);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
th {
|
th {
|
||||||
font-family: var(--font-mono); font-size: 0.75rem; font-weight: 500;
|
font-family: var(--font-mono); font-size: 0.75rem; font-weight: 500;
|
||||||
text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted);
|
text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted);
|
||||||
}
|
}
|
||||||
td.time, td.status { white-space: nowrap; }
|
td.actions { text-align: right; }
|
||||||
td.actions { text-align: right; white-space: nowrap; }
|
.metric { white-space: nowrap; }
|
||||||
|
.card:has(table) { overflow-x: auto; }
|
||||||
td.subject span {
|
td.subject span {
|
||||||
display: block; max-width: 22rem;
|
display: block; max-width: 22rem;
|
||||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code {
|
.code {
|
||||||
display: block; white-space: pre-wrap; word-break: break-all; font-family: var(--font-mono);
|
display: block; white-space: pre; overflow-x: auto; overflow-wrap: normal;
|
||||||
|
word-break: normal; font-family: var(--font-mono);
|
||||||
font-size: 0.85rem; background: var(--code-bg); border: 1px solid var(--border);
|
font-size: 0.85rem; background: var(--code-bg); border: 1px solid var(--border);
|
||||||
border-radius: 5px; padding: 0.7rem 0.8rem; margin: 0.3rem 0 0;
|
border-radius: 5px; padding: 0.7rem 0.8rem; margin: 0.3rem 0 0;
|
||||||
}
|
}
|
||||||
@@ -355,9 +374,15 @@ td.subject span {
|
|||||||
display: grid; grid-template-columns: repeat(auto-fit, minmax(22rem, 1fr));
|
display: grid; grid-template-columns: repeat(auto-fit, minmax(22rem, 1fr));
|
||||||
gap: 1rem; margin-top: 1rem;
|
gap: 1rem; margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
h1 + .split, h1 + form > .split { margin-top: 0; }
|
||||||
.split > .card { min-width: 0; }
|
.split > .card { min-width: 0; }
|
||||||
.split > .card + .card { margin-top: 0; }
|
.split > .card + .card { margin-top: 0; }
|
||||||
.split + .card, .card + .split { margin-top: 1rem; }
|
.split + .card, .card + .split, form:has(.split) + .card { margin-top: 1rem; }
|
||||||
|
#settings form { width: 100%; max-width: none; }
|
||||||
|
#settings .split,
|
||||||
|
#backup > .split {
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
.check-cols {
|
.check-cols {
|
||||||
display: grid; grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
|
display: grid; grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
|
||||||
@@ -369,9 +394,21 @@ td.subject span {
|
|||||||
display: grid; grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
|
display: grid; grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
|
||||||
gap: 0 1rem; margin-top: 0.45rem;
|
gap: 0 1rem; margin-top: 0.45rem;
|
||||||
}
|
}
|
||||||
.field-pair.host-type { grid-template-columns: minmax(0, 1fr) auto; }
|
.field-pair > div { min-width: 0; }
|
||||||
|
.field-pair label { margin-top: 0.45rem; }
|
||||||
|
/* Host / name beside Type — size the type column to that token rather than
|
||||||
|
giving it half the row. Vertical padding matches .code-row .code so Type is
|
||||||
|
the same height as Host when Copy sits beside it. */
|
||||||
|
.field-pair.host-type {
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
}
|
||||||
|
.field-pair.host-type .code {
|
||||||
|
padding-top: 0.45rem; padding-bottom: 0.45rem;
|
||||||
|
}
|
||||||
.field-type { width: max-content; }
|
.field-type { width: max-content; }
|
||||||
.field-type .code { width: fit-content; min-width: 2.75rem; text-align: center; padding: 0.45rem; }
|
.field-type .code {
|
||||||
|
width: fit-content; min-width: 2.75rem; text-align: center; box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
.facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr)); gap: 0.5rem; margin-top: 1rem; }
|
.facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr)); gap: 0.5rem; margin-top: 1rem; }
|
||||||
.fact { min-width: 0; padding: 0.5rem 0.7rem; border-radius: 6px; background: var(--surface-bg); }
|
.fact { min-width: 0; padding: 0.5rem 0.7rem; border-radius: 6px; background: var(--surface-bg); }
|
||||||
@@ -379,7 +416,7 @@ td.subject span {
|
|||||||
display: block; font-family: var(--font-mono); font-size: 0.7rem; font-weight: 500;
|
display: block; font-family: var(--font-mono); font-size: 0.7rem; font-weight: 500;
|
||||||
text-transform: uppercase; letter-spacing: 0.1em; color: var(--muted);
|
text-transform: uppercase; letter-spacing: 0.1em; color: var(--muted);
|
||||||
}
|
}
|
||||||
.fact-value { display: block; margin-top: 0.1rem; overflow-wrap: anywhere; }
|
.fact-value { display: block; margin-top: 0.1rem; white-space: nowrap; overflow-x: auto; }
|
||||||
.fact-value.mono { font-family: var(--font-mono); font-size: 0.85rem; }
|
.fact-value.mono { font-family: var(--font-mono); font-size: 0.85rem; }
|
||||||
|
|
||||||
.timeline {
|
.timeline {
|
||||||
@@ -402,12 +439,15 @@ td.subject span {
|
|||||||
.event-detail { margin: 0.2rem 0 0; font-size: 0.9rem; }
|
.event-detail { margin: 0.2rem 0 0; font-size: 0.9rem; }
|
||||||
|
|
||||||
.route { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin-top: -0.5rem; margin-bottom: 1rem; }
|
.route { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin-top: -0.5rem; margin-bottom: 1rem; }
|
||||||
.route .addr { font-family: var(--font-mono); font-size: 0.9rem; overflow-wrap: anywhere; }
|
.route .addr { font-family: var(--font-mono); font-size: 0.9rem; white-space: nowrap; overflow-x: auto; max-width: 100%; }
|
||||||
.route .arrow { color: var(--muted); }
|
.route .arrow { color: var(--muted); }
|
||||||
|
|
||||||
table.log { margin-top: 1rem; }
|
table.log { margin-top: 1rem; }
|
||||||
table.log th:first-child, table.log td.time { width: 1%; }
|
table.log th:first-child, table.log td.time { width: 1%; }
|
||||||
table.log td.log-text { font-family: var(--font-mono); font-size: 0.8rem; white-space: pre-wrap; }
|
table.log td.log-text {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.8rem; white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
button.copy, .actions button, .actions > label.toggle, .actions a.danger {
|
button.copy, .actions button, .actions > label.toggle, .actions a.danger {
|
||||||
margin: 0; padding: 0.45rem 0.7rem; font-size: 0.8rem; font-weight: 600;
|
margin: 0; padding: 0.45rem 0.7rem; font-size: 0.8rem; font-weight: 600;
|
||||||
@@ -422,7 +462,7 @@ button.copy:hover, .actions button:hover, .actions > label.toggle:hover { backgr
|
|||||||
.app-item { padding: 0.9rem 0; border-top: 1px solid var(--border); }
|
.app-item { padding: 0.9rem 0; border-top: 1px solid var(--border); }
|
||||||
.app-item:last-child { padding-bottom: 0; }
|
.app-item:last-child { padding-bottom: 0; }
|
||||||
.app-login { margin: 0; font-family: var(--font-mono); font-weight: 600; }
|
.app-login { margin: 0; font-family: var(--font-mono); font-weight: 600; }
|
||||||
.app-addr { margin: 0.15rem 0 0; word-break: break-all; }
|
.app-addr { margin: 0.15rem 0 0; white-space: nowrap; overflow-x: auto; }
|
||||||
.app-item .actions { margin-top: 0.7rem; }
|
.app-item .actions { margin-top: 0.7rem; }
|
||||||
.app-item .actions > .panel-toggle {
|
.app-item .actions > .panel-toggle {
|
||||||
position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; pointer-events: none;
|
position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; pointer-events: none;
|
||||||
@@ -440,6 +480,10 @@ button.copy:hover, .actions button:hover, .actions > label.toggle:hover { backgr
|
|||||||
.encrypt:has(input[type="checkbox"]:checked) .encrypt-fields { display: block; }
|
.encrypt:has(input[type="checkbox"]:checked) .encrypt-fields { display: block; }
|
||||||
.encrypt-fields label { margin-top: 0.7rem; }
|
.encrypt-fields label { margin-top: 0.7rem; }
|
||||||
|
|
||||||
|
.rcpt-list, .rcpt-any { display: none; }
|
||||||
|
.rcpt-mode:has(option[value="list"]:checked) .rcpt-list { display: block; }
|
||||||
|
.rcpt-mode:has(option[value="any"]:checked) .rcpt-any { display: block; }
|
||||||
|
|
||||||
.toolbar {
|
.toolbar {
|
||||||
display: flex; flex-wrap: wrap; gap: 0.6rem 1rem; align-items: end;
|
display: flex; flex-wrap: wrap; gap: 0.6rem 1rem; align-items: end;
|
||||||
padding: 0.85rem 1rem; margin-bottom: 1rem;
|
padding: 0.85rem 1rem; margin-bottom: 1rem;
|
||||||
@@ -455,7 +499,8 @@ button.copy:hover, .actions button:hover, .actions > label.toggle:hover { backgr
|
|||||||
padding: 1rem 1.25rem; margin-bottom: 1rem;
|
padding: 1rem 1.25rem; margin-bottom: 1rem;
|
||||||
background: var(--card-bg); border: 1px solid var(--border); border-radius: 6px;
|
background: var(--card-bg); border: 1px solid var(--border); border-radius: 6px;
|
||||||
}
|
}
|
||||||
.status-hero.attn { border-color: var(--st-warn-border); background: var(--st-warn-bg); }
|
.status-hero.attn,
|
||||||
|
.card.attn { border-color: var(--st-warn-border); }
|
||||||
.status-hero h1 { margin: 0; }
|
.status-hero h1 { margin: 0; }
|
||||||
.status-hero .lead { margin: 0; color: var(--fg); }
|
.status-hero .lead { margin: 0; color: var(--fg); }
|
||||||
.status-grid {
|
.status-grid {
|
||||||
@@ -480,7 +525,7 @@ meter { width: 5rem; height: 0.7rem; vertical-align: middle; margin-right: 0.4re
|
|||||||
flex: none; width: 1.35rem; height: 1.35rem; display: inline-flex; align-items: center; justify-content: center;
|
flex: none; width: 1.35rem; height: 1.35rem; display: inline-flex; align-items: center; justify-content: center;
|
||||||
border-radius: 50%; border: 1px solid var(--control-border); color: var(--muted);
|
border-radius: 50%; border: 1px solid var(--control-border); color: var(--muted);
|
||||||
text-decoration: none; font-family: var(--font-mono); font-size: 0.75rem; font-weight: 600;
|
text-decoration: none; font-family: var(--font-mono); font-size: 0.75rem; font-weight: 600;
|
||||||
background: var(--surface-bg);
|
background: var(--surface-bg); cursor: pointer; margin: 0;
|
||||||
}
|
}
|
||||||
.help-link:hover { color: var(--accent-text); border-color: var(--accent-fill); }
|
.help-link:hover { color: var(--accent-text); border-color: var(--accent-fill); }
|
||||||
|
|
||||||
@@ -500,7 +545,7 @@ meter { width: 5rem; height: 0.7rem; vertical-align: middle; margin-right: 0.4re
|
|||||||
padding: 0.75rem 0.1rem; text-decoration: none; color: var(--fg);
|
padding: 0.75rem 0.1rem; text-decoration: none; color: var(--fg);
|
||||||
}
|
}
|
||||||
.phone-list .when { font-family: var(--font-mono); font-size: 0.75rem; color: var(--muted); }
|
.phone-list .when { font-family: var(--font-mono); font-size: 0.75rem; color: var(--muted); }
|
||||||
.phone-list .pair { grid-column: 1 / -1; font-family: var(--font-mono); font-size: 0.82rem; overflow-wrap: anywhere; }
|
.phone-list .pair { grid-column: 1 / -1; font-family: var(--font-mono); font-size: 0.82rem; white-space: nowrap; overflow-x: auto; }
|
||||||
.phone-list .subj { grid-column: 1 / -1; margin: 0; }
|
.phone-list .subj { grid-column: 1 / -1; margin: 0; }
|
||||||
|
|
||||||
/* Help drawer — CSS checkbox, no script required */
|
/* Help drawer — CSS checkbox, no script required */
|
||||||
@@ -510,14 +555,35 @@ meter { width: 5rem; height: 0.7rem; vertical-align: middle; margin-right: 0.4re
|
|||||||
background: var(--card-bg); border-left: 1px solid var(--border);
|
background: var(--card-bg); border-left: 1px solid var(--border);
|
||||||
padding: 1.25rem 1.35rem 2rem; overflow-y: auto;
|
padding: 1.25rem 1.35rem 2rem; overflow-y: auto;
|
||||||
}
|
}
|
||||||
#help-open:checked ~ .help-drawer { display: block; }
|
html:has(input[name="help"]:checked):not(:has(#help-off:checked)) .help-drawer,
|
||||||
#help-open:checked ~ .help-scrim { display: block; }
|
html:has(input[name="help"]:checked):not(:has(#help-off:checked)) .help-scrim { display: block; }
|
||||||
|
.help-pane { display: none; }
|
||||||
|
html:has(#help-index:checked) .help-pane-index,
|
||||||
|
html:has(#help-status:checked) .help-pane-status,
|
||||||
|
html:has(#help-password:checked) .help-pane-password,
|
||||||
|
html:has(#help-dns:checked) .help-pane-dns,
|
||||||
|
html:has(#help-records:checked) .help-pane-records,
|
||||||
|
html:has(#help-dmarc:checked) .help-pane-dmarc,
|
||||||
|
html:has(#help-connection:checked) .help-pane-connection,
|
||||||
|
html:has(#help-apps:checked) .help-pane-apps,
|
||||||
|
html:has(#help-domain-settings:checked) .help-pane-domain-settings,
|
||||||
|
html:has(#help-export:checked) .help-pane-export { display: block; }
|
||||||
.help-scrim {
|
.help-scrim {
|
||||||
display: none; position: fixed; inset: var(--gallery-h) 0 0 0; z-index: 25;
|
display: none; position: fixed; inset: var(--gallery-h) 0 0 0; z-index: 25;
|
||||||
background: rgba(18, 22, 28, 0.28);
|
background: rgba(18, 22, 28, 0.28);
|
||||||
}
|
}
|
||||||
.help-drawer h2 { margin-top: 1.2rem; }
|
.help-drawer h2 { margin-top: 1.2rem; }
|
||||||
.help-drawer h2:first-of-type { margin-top: 0; }
|
.help-drawer h2:first-of-type,
|
||||||
|
.help-pane h2 { margin-top: 0; }
|
||||||
|
.help-drawer .toc { list-style: none; margin: 0.7rem 0 0; padding: 0; }
|
||||||
|
.help-drawer .toc li { margin: 0.35rem 0 0; }
|
||||||
|
.help-drawer .toc label {
|
||||||
|
margin: 0; font-weight: 600; color: var(--accent-text); cursor: pointer;
|
||||||
|
}
|
||||||
|
.help-drawer .more { margin-top: 1.2rem; font-size: 0.85rem; }
|
||||||
|
.help-drawer .more label {
|
||||||
|
display: inline; margin: 0; font-weight: 600; color: var(--accent-text); cursor: pointer;
|
||||||
|
}
|
||||||
.help-close {
|
.help-close {
|
||||||
position: absolute; top: 0.8rem; right: 0.8rem; margin: 0; padding: 0.35rem 0.6rem;
|
position: absolute; top: 0.8rem; right: 0.8rem; margin: 0; padding: 0.35rem 0.6rem;
|
||||||
font-size: 0.8rem; font-weight: 600; background: var(--surface-bg); color: var(--fg);
|
font-size: 0.8rem; font-weight: 600; background: var(--surface-bg); color: var(--fg);
|
||||||
@@ -562,7 +628,7 @@ meter { width: 5rem; height: 0.7rem; vertical-align: middle; margin-right: 0.4re
|
|||||||
#vp-phone:checked ~ .app .status-grid .attn { order: -1; }
|
#vp-phone:checked ~ .app .status-grid .attn { order: -1; }
|
||||||
#vp-phone:checked ~ .app .split,
|
#vp-phone:checked ~ .app .split,
|
||||||
#vp-phone:checked ~ .app .check-cols,
|
#vp-phone:checked ~ .app .check-cols,
|
||||||
#vp-phone:checked ~ .app .field-pair,
|
#vp-phone:checked ~ .app .field-pair:not(.host-type),
|
||||||
#vp-phone:checked ~ .app .facts,
|
#vp-phone:checked ~ .app .facts,
|
||||||
#vp-phone:checked ~ .app .retry-facts {
|
#vp-phone:checked ~ .app .retry-facts {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
@@ -599,7 +665,8 @@ meter { width: 5rem; height: 0.7rem; vertical-align: middle; margin-right: 0.4re
|
|||||||
.phone-only, .phone-list { display: block; }
|
.phone-only, .phone-list { display: block; }
|
||||||
.status-grid { display: flex; flex-direction: column; }
|
.status-grid { display: flex; flex-direction: column; }
|
||||||
.status-grid .attn { order: -1; }
|
.status-grid .attn { order: -1; }
|
||||||
.split, .check-cols, .field-pair, .facts, .retry-facts { grid-template-columns: 1fr; }
|
.split, #settings .split, #backup > .split,
|
||||||
|
.check-cols, .field-pair:not(.host-type), .facts, .retry-facts { grid-template-columns: 1fr; }
|
||||||
.help-drawer { width: 100vw; }
|
.help-drawer { width: 100vw; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user