docs: iterate panel UI mockups from review
test / test (push) Waiting to run

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:
2026-08-16 00:24:46 +03:00
parent f517e8da41
commit 9edb39a8ee
4 changed files with 638 additions and 175 deletions
+9
View File
@@ -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
View File
@@ -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">Lets 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">&larr; All domains</a> <a class="back" href="#domains">&larr; 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=&lt;noreply@blocked.examp
</main> </main>
<!-- BACKUP --> <!-- BACKUP -->
<main id="backup" class="screen form"> <main id="backup" class="screen ops">
<h1>Backup &amp; migration</h1> <h1>Backup &amp; 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 domains 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=&lt;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=&lt;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 domains page.</p> <p class="muted">Level 1 is set in Compose; restart the container to change it. Domain and application ceilings live on each domains 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=&lt;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=&lt;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=&lt;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">&larr; All inbound domains</a> <a class="back" href="#inbound">&larr; 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 domains 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=&lt;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 applications 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">&larr; 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 applications 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">&larr; 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 cards «?»)</li>
</ul> </ul>
<p class="muted">The same texts open in the drawer from a cards «?» — so a Status page can stay dense.</p> <p class="muted">The same texts open in the drawer from Statuss «?» — 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 containers 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 containers 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");
+9 -7
View File
@@ -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>
+87 -20
View File
@@ -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; }
} }