Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c6a75ce775 | |||
| f47cf39802 | |||
| 9edb39a8ee | |||
| f517e8da41 |
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
description: Panel UI mockups use the design system primitives, not a reskin of live templates.
|
||||||
|
globs: docs/assets/panel-ui/**
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Panel UI system
|
||||||
|
|
||||||
|
Source of truth: [docs/assets/panel-ui/system.html](docs/assets/panel-ui/system.html) and `system.css`.
|
||||||
|
|
||||||
|
Do not copy `internal/web/view/templates` into mockups. Do not mark `<main>` as `ops`/`form` to pick width. Do not fix empty columns, Host/Type height, or split Save/Delete with one-off CSS.
|
||||||
|
|
||||||
|
Compose screens from `stack`, `pair`, `measure`, `fill`, `field-row`, `actions-row`:
|
||||||
|
- Two peer jobs → `pair` (one child shrinks to `measure`).
|
||||||
|
- Tables/logs/DNS → `fill` (nowrap + overflow-x on the card). Add-to-list is a `measure` row inside that card, not a second card above it.
|
||||||
|
- Confirm/login/user form → `measure`.
|
||||||
|
- Host ‖ Type and two equal inputs → `field-row` (shared grid row, not two `.code` paddings).
|
||||||
|
- Save + Delete → `actions-row` inside the card; never submit inside `<form>` and danger after `</form>`.
|
||||||
|
- Danger zone is the second column of the last `pair`, never a full-width `fill`.
|
||||||
|
- Card chrome is title + optional `?` + body + actions. Help is not sprinkled later.
|
||||||
|
- Phone: `pair` stacks; `field-row` does not; tables become lists.
|
||||||
|
- One screen is one HTML file. Shared chrome is `shell.js`. Do not put every screen in one hash-SPA.
|
||||||
@@ -5,6 +5,35 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- docs: HTML mockups for a full panel UI refresh
|
||||||
|
([docs/assets/panel-ui/](docs/assets/panel-ui/index.html)) — current screens,
|
||||||
|
agreed and candidate roadmap surfaces (queue-retries, inbound-relay,
|
||||||
|
dmarc-reports, panel-docs), a hybrid width (ops pages use the window, forms
|
||||||
|
keep a reading measure), and a 390px emergency layout. The stamp and brick
|
||||||
|
palette stay; extra nav icons are proposals only. Design artefact; the
|
||||||
|
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`).
|
||||||
|
|
||||||
|
- docs: panel UI design system and mockups rebuilt as separate pages
|
||||||
|
(`system.html`, `status.html`, `domain.html`, …), not one hash sheet.
|
||||||
|
Regions are `stack` / `pair` / `measure` / `fill`; Host ‖ Type is
|
||||||
|
`field-row`; Save + Delete is `actions-row`. Shared chrome is `shell.js`.
|
||||||
|
|
||||||
|
- docs: panel UI mockups — add-domain sits in the list card (Domains and
|
||||||
|
Inbound); DMARC candidate screens drill into a domain roll-up and a
|
||||||
|
parsed aggregate report (aligned vs third-party fail), not a hub-only
|
||||||
|
summary.
|
||||||
|
|
||||||
## [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,
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>SelfPost — panel mockups</title>
|
||||||
|
<link rel="icon" href="../selfpost-icon-16.svg" type="image/svg+xml">
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var map = {
|
||||||
|
login: "login.html",
|
||||||
|
setup: "setup.html",
|
||||||
|
status: "status.html",
|
||||||
|
domains: "domains.html",
|
||||||
|
domain: "domain.html",
|
||||||
|
"domain-delete": "domain-delete.html",
|
||||||
|
deliveries: "deliveries.html",
|
||||||
|
delivery: "delivery.html",
|
||||||
|
"mail-queue": "mail-queue.html",
|
||||||
|
"system-log": "system-log.html",
|
||||||
|
backup: "backup.html",
|
||||||
|
users: "users.html",
|
||||||
|
"user-form": "user-form.html",
|
||||||
|
"user-delete": "user-delete.html",
|
||||||
|
settings: "settings.html",
|
||||||
|
inbound: "inbound.html",
|
||||||
|
"inbound-domain": "inbound-domain.html",
|
||||||
|
"inbound-backup": "inbound-backup.html",
|
||||||
|
"inbound-delete": "inbound-delete.html",
|
||||||
|
dmarc: "dmarc.html",
|
||||||
|
"dmarc-domain": "dmarc-domain.html",
|
||||||
|
"dmarc-report": "dmarc-report.html",
|
||||||
|
"dmarc-report-fail": "dmarc-report-fail.html",
|
||||||
|
help: "help.html"
|
||||||
|
};
|
||||||
|
var h = location.hash.replace(/^#/, "");
|
||||||
|
location.replace((map[h] || "status.html") + location.search);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>Макеты собраны отдельными страницами. Откройте <a href="status.html">Status</a> или <a href="index.html">оглавление</a>.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Backup & migration — SelfPost mockups</title>
|
||||||
|
<link rel="icon" href="../selfpost-icon-16.svg" type="image/svg+xml">
|
||||||
|
<link rel="stylesheet" href="mock.css">
|
||||||
|
<link rel="stylesheet" href="system.css">
|
||||||
|
</head>
|
||||||
|
<body class="page-backup" data-page="backup" data-nav="backup" data-title="Backup" data-global-only="1">
|
||||||
|
<main class="stack">
|
||||||
|
<h1>Backup & migration</h1>
|
||||||
|
<div class="pair">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Full backup</h2>
|
||||||
|
<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>
|
||||||
|
<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">
|
||||||
|
<label class="check"><input type="checkbox" checked> <span>Encrypt with a password</span></label>
|
||||||
|
<div class="encrypt-fields">
|
||||||
|
<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 class="actions-row"><button type="button">Download full backup</button></div>
|
||||||
|
</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>
|
||||||
|
<div class="actions-row"><button type="button">Import domain</button></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<script src="shell.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Deliveries — SelfPost mockups</title>
|
||||||
|
<link rel="icon" href="../selfpost-icon-16.svg" type="image/svg+xml">
|
||||||
|
<link rel="stylesheet" href="mock.css">
|
||||||
|
<link rel="stylesheet" href="system.css">
|
||||||
|
</head>
|
||||||
|
<body class="page-deliveries" data-page="deliveries" data-nav="deliveries" data-title="Deliveries">
|
||||||
|
<main class="stack">
|
||||||
|
<h1>Deliveries</h1>
|
||||||
|
<form class="toolbar fill" action="#" onsubmit="return false">
|
||||||
|
<div class="field"><label>Domain</label>
|
||||||
|
<select><option>All domains</option><option>example.com</option><option>alerts.example.com</option></select>
|
||||||
|
</div>
|
||||||
|
<div class="field"><label>Application</label>
|
||||||
|
<select><option>All applications</option><option>newsletter</option><option>billing</option></select>
|
||||||
|
</div>
|
||||||
|
<button type="button">Filter</button>
|
||||||
|
</form>
|
||||||
|
<div class="fill">
|
||||||
|
<div class="card">
|
||||||
|
<table class="desk-only">
|
||||||
|
<thead><tr><th>Time</th><th>From</th><th>To</th><th>Subject</th><th>Status</th><th></th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="time muted">2026-08-15 20:14:02</td>
|
||||||
|
<td>billing@example.com</td>
|
||||||
|
<td>ada@example.net</td>
|
||||||
|
<td class="subject"><span>Invoice #4412</span></td>
|
||||||
|
<td class="status"><span class="st st-warn">deferred</span></td>
|
||||||
|
<td class="actions"><a href="delivery.html">Details</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="time muted">2026-08-15 20:11:40</td>
|
||||||
|
<td>news@example.com</td>
|
||||||
|
<td>list-bounces@example.net</td>
|
||||||
|
<td class="subject"><span>August digest</span></td>
|
||||||
|
<td class="status"><span class="st st-ok">delivered</span></td>
|
||||||
|
<td class="actions"><a href="delivery.html">Details</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="time muted">2026-08-15 19:02:11</td>
|
||||||
|
<td>alerts@alerts.example.com</td>
|
||||||
|
<td>noreply@blocked.example</td>
|
||||||
|
<td class="subject"><span>Disk 92% on web-3</span></td>
|
||||||
|
<td class="status"><span class="st st-error">bounced</span></td>
|
||||||
|
<td class="actions"><a href="delivery.html">Details</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="time muted">2026-08-15 18:44:09</td>
|
||||||
|
<td>news@example.com</td>
|
||||||
|
<td>sam@example.org</td>
|
||||||
|
<td class="subject"><span>August digest</span></td>
|
||||||
|
<td class="status"><span class="st st-ok">delivered</span></td>
|
||||||
|
<td class="actions"><a href="delivery.html">Details</a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<ul class="phone-list phone-only">
|
||||||
|
<li>
|
||||||
|
<a href="delivery.html">
|
||||||
|
<span class="st st-warn">deferred</span><span class="when">20:14</span>
|
||||||
|
<span class="meta">billing@example.com → ada@example.net</span>
|
||||||
|
<span class="subj">Invoice #4412</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="delivery.html">
|
||||||
|
<span class="st st-ok">delivered</span><span class="when">20:11</span>
|
||||||
|
<span class="meta">news@example.com → list-bounces@example.net</span>
|
||||||
|
<span class="subj">August digest</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="delivery.html">
|
||||||
|
<span class="st st-error">bounced</span><span class="when">19:02</span>
|
||||||
|
<span class="meta">alerts@alerts.example.com → noreply@blocked.example</span>
|
||||||
|
<span class="subj">Disk 92% on web-3</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p class="muted">Page 1 of 4 · <a href="deliveries.html">Older →</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<script src="shell.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Invoice #4412 — SelfPost mockups</title>
|
||||||
|
<link rel="icon" href="../selfpost-icon-16.svg" type="image/svg+xml">
|
||||||
|
<link rel="stylesheet" href="mock.css">
|
||||||
|
<link rel="stylesheet" href="system.css">
|
||||||
|
</head>
|
||||||
|
<body class="page-delivery" data-page="delivery" data-nav="deliveries" data-title="Message">
|
||||||
|
<main class="stack">
|
||||||
|
<div>
|
||||||
|
<h1 class="subject">Invoice #4412</h1>
|
||||||
|
<p class="route">
|
||||||
|
<span class="addr">billing@example.com</span>
|
||||||
|
<span class="arrow">→</span>
|
||||||
|
<span class="addr">ada@example.net</span>
|
||||||
|
<span class="st st-warn">deferred</span>
|
||||||
|
</p>
|
||||||
|
<a class="back" href="deliveries.html">← Back to deliveries</a>
|
||||||
|
</div>
|
||||||
|
<div class="pair">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Message</h2>
|
||||||
|
<div class="facts">
|
||||||
|
<div class="fact"><span class="fact-label">Domain</span><span class="fact-value">example.com</span></div>
|
||||||
|
<div class="fact"><span class="fact-label">Application</span><span class="fact-value">billing</span></div>
|
||||||
|
<div class="fact"><span class="fact-label">Accepted</span><span class="fact-value">2026-08-15 20:14:02 UTC</span></div>
|
||||||
|
<div class="fact"><span class="fact-label">Status reported</span><span class="fact-value">2026-08-15 20:14:08 UTC</span></div>
|
||||||
|
<div class="fact"><span class="fact-label">Queue id</span><span class="fact-value mono">4C3A1E2F1A</span></div>
|
||||||
|
<div class="fact"><span class="fact-label">Journal id</span><span class="fact-value mono">1842</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>History</h2>
|
||||||
|
<ol class="timeline">
|
||||||
|
<li class="event lvl-ok">
|
||||||
|
<p class="event-time">2026-08-15 20:14:02 UTC</p>
|
||||||
|
<p class="event-title"><span class="st st-ok">accepted</span> Received by the relay</p>
|
||||||
|
<p class="event-detail muted">SASL login billing, queued as 4C3A1E2F1A.</p>
|
||||||
|
</li>
|
||||||
|
<li class="event lvl-warn">
|
||||||
|
<p class="event-time">2026-08-15 20:14:08 UTC</p>
|
||||||
|
<p class="event-title"><span class="st st-warn">deferred</span> Receiving MX asked to try later</p>
|
||||||
|
<p class="event-detail muted">Postfix retries: first after 5 minutes, then with increasing gaps up to 1 hour 7 minutes, for up to 5 days. There is no fixed attempt count — a deferred message stays in the queue until it is delivered or that lifetime runs out.</p>
|
||||||
|
</li>
|
||||||
|
<li class="event pending">
|
||||||
|
<p class="event-time">not yet</p>
|
||||||
|
<p class="event-title"><span class="st st-unknown">delivery</span> Waiting on the next retry</p>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="fill">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Delivery log</h2>
|
||||||
|
<table class="log desk-only">
|
||||||
|
<thead><tr><th>Time</th><th>Message</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td class="time muted">20:14:02</td><td class="log-text">postfix/smtpd[221]: 4C3A1E2F1A: client=203.0.113.40, sasl_username=billing</td></tr>
|
||||||
|
<tr><td class="time muted">20:14:02</td><td class="log-text">postfix/cleanup[224]: 4C3A1E2F1A: message-id=<4412@example.com></td></tr>
|
||||||
|
<tr><td class="time muted">20:14:08</td><td class="log-text">postfix/smtp[230]: 4C3A1E2F1A: to=<ada@example.net>, relay=mx.example.net[198.51.100.20]:25, delay=6, status=deferred (450 4.2.1 mailbox busy)</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="phone-only">
|
||||||
|
<p class="muted" style="margin:0.6rem 0 0.2rem">20:14:08</p>
|
||||||
|
<span class="code">status=deferred (450 4.2.1 mailbox busy)</span>
|
||||||
|
<p class="muted" style="margin:0.6rem 0 0.2rem">20:14:02</p>
|
||||||
|
<span class="code">client=203.0.113.40, sasl_username=billing</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<script src="shell.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>example.com — DMARC — SelfPost mockups</title>
|
||||||
|
<link rel="icon" href="../selfpost-icon-16.svg" type="image/svg+xml">
|
||||||
|
<link rel="stylesheet" href="mock.css">
|
||||||
|
<link rel="stylesheet" href="system.css">
|
||||||
|
</head>
|
||||||
|
<body class="page-dmarc-domain" data-page="dmarc-domain" data-nav="dmarc" data-title="example.com DMARC">
|
||||||
|
<main class="stack">
|
||||||
|
<div>
|
||||||
|
<h1>example.com <span class="tag future">candidate</span></h1>
|
||||||
|
<a class="back g-only" href="dmarc.html">← All DMARC reports</a>
|
||||||
|
<a class="back" href="domain.html">← Back to example.com</a>
|
||||||
|
</div>
|
||||||
|
<div class="pair">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Last 7 days <span class="st st-ok">pass</span></h2>
|
||||||
|
<p>98% aligned. Tightening <code>p=</code> looks reasonable.</p>
|
||||||
|
<p class="muted"><a href="domain.html">Domain DNS and rua=</a></p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>Third-party senders</h2>
|
||||||
|
<p>google.com / 66.102.0.0/20 failed SPF and DKIM 6 times. Everyone else is this relay.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="fill">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Reports</h2>
|
||||||
|
<table class="desk-only">
|
||||||
|
<thead><tr><th>Received</th><th>Reporter</th><th>Window</th><th>Pass</th><th>Fail</th><th></th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="time muted">2026-08-15 06:12</td>
|
||||||
|
<td class="muted">google.com</td>
|
||||||
|
<td>14 Aug</td>
|
||||||
|
<td>412</td>
|
||||||
|
<td>6</td>
|
||||||
|
<td class="actions"><a href="dmarc-report.html">View</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="time muted">2026-08-14 06:08</td>
|
||||||
|
<td class="muted">google.com</td>
|
||||||
|
<td>13 Aug</td>
|
||||||
|
<td>390</td>
|
||||||
|
<td>2</td>
|
||||||
|
<td class="actions"><a href="dmarc-report.html">View</a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<ul class="phone-list phone-only">
|
||||||
|
<li>
|
||||||
|
<a href="dmarc-report.html">
|
||||||
|
<span class="st st-ok">412 pass</span><span class="when">14 Aug</span>
|
||||||
|
<span class="meta">google.com · 6 fail</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="dmarc-report.html">
|
||||||
|
<span class="st st-ok">390 pass</span><span class="when">13 Aug</span>
|
||||||
|
<span class="meta">google.com · 2 fail</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="fill">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Sources · last 7 days</h2>
|
||||||
|
<table class="desk-only">
|
||||||
|
<thead><tr><th>Source</th><th>Pass</th><th>Fail</th><th>Disposition</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td class="muted">203.0.113.10 (this relay)</td><td>802</td><td>0</td><td><span class="st st-ok">none</span></td></tr>
|
||||||
|
<tr><td class="muted">google.com / 66.102.0.0/20</td><td>0</td><td>8</td><td><span class="st st-warn">none</span></td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<ul class="phone-list phone-only">
|
||||||
|
<li><a href="dmarc-report.html"><span>this relay</span><span class="st st-ok">802 pass</span></a></li>
|
||||||
|
<li><a href="dmarc-report.html"><span>google.com / 66.102.0.0/20</span><span class="st st-warn">8 fail</span></a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<script src="shell.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>google.com report — alerts.example.com — SelfPost mockups</title>
|
||||||
|
<link rel="icon" href="../selfpost-icon-16.svg" type="image/svg+xml">
|
||||||
|
<link rel="stylesheet" href="mock.css">
|
||||||
|
<link rel="stylesheet" href="system.css">
|
||||||
|
</head>
|
||||||
|
<body class="page-dmarc-report-fail" data-page="dmarc-report-fail" data-nav="dmarc" data-title="DMARC report">
|
||||||
|
<main class="stack">
|
||||||
|
<div>
|
||||||
|
<h1 class="subject">google.com · 14 Aug</h1>
|
||||||
|
<p class="route">
|
||||||
|
<span class="addr">alerts.example.com</span>
|
||||||
|
<span class="st st-ok">88 pass</span>
|
||||||
|
<span class="st st-error">19 fail</span>
|
||||||
|
</p>
|
||||||
|
<a class="back" href="dmarc.html">← All DMARC reports</a>
|
||||||
|
</div>
|
||||||
|
<div class="pair">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Report</h2>
|
||||||
|
<div class="facts">
|
||||||
|
<div class="fact"><span class="fact-label">Reporter</span><span class="fact-value">google.com</span></div>
|
||||||
|
<div class="fact"><span class="fact-label">Report id</span><span class="fact-value mono">3178944098765432109</span></div>
|
||||||
|
<div class="fact"><span class="fact-label">Window</span><span class="fact-value">2026-08-14 00:00 – 2026-08-15 00:00 UTC</span></div>
|
||||||
|
<div class="fact"><span class="fact-label">Received</span><span class="fact-value">2026-08-15 04:40 UTC</span></div>
|
||||||
|
<div class="fact"><span class="fact-label">Contact</span><span class="fact-value">noreply-dmarc-support@google.com</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card attn">
|
||||||
|
<h2>Published policy</h2>
|
||||||
|
<div class="facts">
|
||||||
|
<div class="fact"><span class="fact-label">Domain</span><span class="fact-value">alerts.example.com</span></div>
|
||||||
|
<div class="fact"><span class="fact-label">p / sp / pct</span><span class="fact-value mono">none / none / 100</span></div>
|
||||||
|
<div class="fact"><span class="fact-label">adkim / aspf</span><span class="fact-value mono">r / r</span></div>
|
||||||
|
<div class="fact"><span class="fact-label">rua</span><span class="fact-value">dmarc@mail.example.org</span></div>
|
||||||
|
</div>
|
||||||
|
<p class="muted">A third-party source is not in SPF or DKIM. Do not tighten <code>p=</code> until that sender is gone or aligned.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="fill">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Records</h2>
|
||||||
|
<table class="desk-only">
|
||||||
|
<thead><tr><th>Source</th><th>Count</th><th>Disposition</th><th>SPF</th><th>DKIM</th><th>Header from</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="muted">203.0.113.10 (this relay)</td>
|
||||||
|
<td>88</td>
|
||||||
|
<td><span class="st st-ok">none</span></td>
|
||||||
|
<td><span class="st st-ok">pass</span></td>
|
||||||
|
<td><span class="st st-ok">pass</span></td>
|
||||||
|
<td>alerts.example.com</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="muted">198.51.100.80 (unknown)</td>
|
||||||
|
<td>19</td>
|
||||||
|
<td><span class="st st-error">none</span></td>
|
||||||
|
<td><span class="st st-error">fail</span></td>
|
||||||
|
<td><span class="st st-error">fail</span></td>
|
||||||
|
<td>alerts.example.com</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<ul class="phone-list phone-only">
|
||||||
|
<li>
|
||||||
|
<div class="item">
|
||||||
|
<span>this relay · 88</span><span class="st st-ok">pass</span>
|
||||||
|
<span class="meta">203.0.113.10 · SPF pass · DKIM pass</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div class="item">
|
||||||
|
<span>unknown · 19</span><span class="st st-error">fail</span>
|
||||||
|
<span class="meta">198.51.100.80 · SPF fail · DKIM fail</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p class="muted">Parsed from the aggregate XML. Forensic (<code>ruf=</code>) samples are not stored.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<script src="shell.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>google.com report — example.com — SelfPost mockups</title>
|
||||||
|
<link rel="icon" href="../selfpost-icon-16.svg" type="image/svg+xml">
|
||||||
|
<link rel="stylesheet" href="mock.css">
|
||||||
|
<link rel="stylesheet" href="system.css">
|
||||||
|
</head>
|
||||||
|
<body class="page-dmarc-report" data-page="dmarc-report" data-nav="dmarc" data-title="DMARC report">
|
||||||
|
<main class="stack">
|
||||||
|
<div>
|
||||||
|
<h1 class="subject">google.com · 14 Aug</h1>
|
||||||
|
<p class="route">
|
||||||
|
<span class="addr">example.com</span>
|
||||||
|
<span class="st st-ok">412 pass</span>
|
||||||
|
<span class="st st-warn">6 fail</span>
|
||||||
|
</p>
|
||||||
|
<a class="back" href="dmarc-domain.html">← Back to example.com</a>
|
||||||
|
</div>
|
||||||
|
<div class="pair">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Report</h2>
|
||||||
|
<div class="facts">
|
||||||
|
<div class="fact"><span class="fact-label">Reporter</span><span class="fact-value">google.com</span></div>
|
||||||
|
<div class="fact"><span class="fact-label">Report id</span><span class="fact-value mono">3178944012345678901</span></div>
|
||||||
|
<div class="fact"><span class="fact-label">Window</span><span class="fact-value">2026-08-14 00:00 – 2026-08-15 00:00 UTC</span></div>
|
||||||
|
<div class="fact"><span class="fact-label">Received</span><span class="fact-value">2026-08-15 06:12 UTC</span></div>
|
||||||
|
<div class="fact"><span class="fact-label">Contact</span><span class="fact-value">noreply-dmarc-support@google.com</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>Published policy</h2>
|
||||||
|
<div class="facts">
|
||||||
|
<div class="fact"><span class="fact-label">Domain</span><span class="fact-value">example.com</span></div>
|
||||||
|
<div class="fact"><span class="fact-label">p / sp / pct</span><span class="fact-value mono">none / none / 100</span></div>
|
||||||
|
<div class="fact"><span class="fact-label">adkim / aspf</span><span class="fact-value mono">r / r</span></div>
|
||||||
|
<div class="fact"><span class="fact-label">rua</span><span class="fact-value">dmarc@mail.example.org</span></div>
|
||||||
|
</div>
|
||||||
|
<p class="muted"><code>p=none</code> does not affect delivery. Failures here are forwarding through Google, not this relay.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="fill">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Records</h2>
|
||||||
|
<table class="desk-only">
|
||||||
|
<thead><tr><th>Source</th><th>Count</th><th>Disposition</th><th>SPF</th><th>DKIM</th><th>Header from</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="muted">203.0.113.10 (this relay)</td>
|
||||||
|
<td>412</td>
|
||||||
|
<td><span class="st st-ok">none</span></td>
|
||||||
|
<td><span class="st st-ok">pass</span></td>
|
||||||
|
<td><span class="st st-ok">pass</span></td>
|
||||||
|
<td>example.com</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="muted">66.102.1.44 (google.com)</td>
|
||||||
|
<td>6</td>
|
||||||
|
<td><span class="st st-warn">none</span></td>
|
||||||
|
<td><span class="st st-error">fail</span></td>
|
||||||
|
<td><span class="st st-error">fail</span></td>
|
||||||
|
<td>example.com</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<ul class="phone-list phone-only">
|
||||||
|
<li>
|
||||||
|
<div class="item">
|
||||||
|
<span>this relay · 412</span><span class="st st-ok">pass</span>
|
||||||
|
<span class="meta">203.0.113.10 · SPF pass · DKIM pass</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div class="item">
|
||||||
|
<span>google.com · 6</span><span class="st st-error">fail</span>
|
||||||
|
<span class="meta">66.102.1.44 · SPF fail · DKIM fail</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p class="muted">Parsed from the aggregate XML. Forensic (<code>ruf=</code>) samples are not stored.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<script src="shell.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>DMARC reports — SelfPost mockups</title>
|
||||||
|
<link rel="icon" href="../selfpost-icon-16.svg" type="image/svg+xml">
|
||||||
|
<link rel="stylesheet" href="mock.css">
|
||||||
|
<link rel="stylesheet" href="system.css">
|
||||||
|
</head>
|
||||||
|
<body class="page-dmarc" data-page="dmarc" data-nav="dmarc" data-title="DMARC" data-global-only="1">
|
||||||
|
<main class="stack">
|
||||||
|
<div class="page-head">
|
||||||
|
<h1>DMARC reports <span class="tag future">candidate</span></h1>
|
||||||
|
</div>
|
||||||
|
<p class="muted">Aggregate reports SelfPost accepted for <code>rua=</code>. Forensic (<code>ruf=</code>) is out of scope. Open a report for the parsed XML — not a dashboard.</p>
|
||||||
|
<div class="pair">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Ingest <span class="st st-ok">ok</span></h2>
|
||||||
|
<p>Last report 6 hours ago. Port 25 accepts only the configured report address.</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>This week</h2>
|
||||||
|
<p>14 kept, 0 parse failures. Older summaries are pruned.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="fill">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Recent reports</h2>
|
||||||
|
<table class="desk-only">
|
||||||
|
<thead><tr><th>Received</th><th>Domain</th><th>Reporter</th><th>Window</th><th>Pass</th><th>Fail</th><th></th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="time muted">2026-08-15 06:12</td>
|
||||||
|
<td><a href="dmarc-domain.html">example.com</a></td>
|
||||||
|
<td class="muted">google.com</td>
|
||||||
|
<td>14 Aug</td>
|
||||||
|
<td>412</td>
|
||||||
|
<td>6</td>
|
||||||
|
<td class="actions"><a href="dmarc-report.html">View</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="time muted">2026-08-15 04:40</td>
|
||||||
|
<td>alerts.example.com</td>
|
||||||
|
<td class="muted">google.com</td>
|
||||||
|
<td>14 Aug</td>
|
||||||
|
<td>88</td>
|
||||||
|
<td>19</td>
|
||||||
|
<td class="actions"><a href="dmarc-report-fail.html">View</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="time muted">2026-08-14 06:08</td>
|
||||||
|
<td><a href="dmarc-domain.html">example.com</a></td>
|
||||||
|
<td class="muted">google.com</td>
|
||||||
|
<td>13 Aug</td>
|
||||||
|
<td>390</td>
|
||||||
|
<td>2</td>
|
||||||
|
<td class="actions"><a href="dmarc-report.html">View</a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<ul class="phone-list phone-only">
|
||||||
|
<li>
|
||||||
|
<a href="dmarc-report.html">
|
||||||
|
<span class="st st-ok">412 pass</span><span class="when">06:12</span>
|
||||||
|
<span class="meta">example.com · google.com · 14 Aug</span>
|
||||||
|
<span class="subj">6 fail</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="dmarc-report-fail.html">
|
||||||
|
<span class="st st-error">19 fail</span><span class="when">04:40</span>
|
||||||
|
<span class="meta">alerts.example.com · google.com · 14 Aug</span>
|
||||||
|
<span class="subj">88 pass</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="dmarc-report.html">
|
||||||
|
<span class="st st-ok">390 pass</span><span class="when">14 Aug</span>
|
||||||
|
<span class="meta">example.com · google.com · 13 Aug</span>
|
||||||
|
<span class="subj">2 fail</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p class="muted">Each row is one aggregate XML SelfPost parsed. Domain names open the roll-up for that sending domain.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<script src="shell.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Delete example.com — SelfPost mockups</title>
|
||||||
|
<link rel="icon" href="../selfpost-icon-16.svg" type="image/svg+xml">
|
||||||
|
<link rel="stylesheet" href="mock.css">
|
||||||
|
<link rel="stylesheet" href="system.css">
|
||||||
|
</head>
|
||||||
|
<body class="page-domain-delete" data-page="domain-delete" data-nav="domains" data-title="Delete domain">
|
||||||
|
<main class="stack">
|
||||||
|
<div>
|
||||||
|
<h1>Delete example.com</h1>
|
||||||
|
<a class="back" href="domain.html">← Back to example.com</a>
|
||||||
|
</div>
|
||||||
|
<div class="measure">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Confirm deletion</h2>
|
||||||
|
<p>You are about to delete <strong>example.com</strong>. This will:</p>
|
||||||
|
<ul>
|
||||||
|
<li>permanently delete its DKIM signing key;</li>
|
||||||
|
<li>delete <strong>all 2 bound applications</strong>, including their SASL credentials;</li>
|
||||||
|
<li>reload OpenDKIM so the domain is no longer signed.</li>
|
||||||
|
</ul>
|
||||||
|
<p class="muted">This cannot be undone.</p>
|
||||||
|
<div class="actions-row"><button type="button" class="danger">Delete example.com</button></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<script src="shell.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>example.com — SelfPost mockups</title>
|
||||||
|
<link rel="icon" href="../selfpost-icon-16.svg" type="image/svg+xml">
|
||||||
|
<link rel="stylesheet" href="mock.css">
|
||||||
|
<link rel="stylesheet" href="system.css">
|
||||||
|
</head>
|
||||||
|
<body class="page-domain" data-page="domain" data-nav="domains" data-title="example.com">
|
||||||
|
<main class="stack">
|
||||||
|
<div>
|
||||||
|
<h1>example.com</h1>
|
||||||
|
<a class="back" href="domains.html">← All domains</a>
|
||||||
|
</div>
|
||||||
|
<div class="measure">
|
||||||
|
<div class="card credential" id="new-credential">
|
||||||
|
<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>
|
||||||
|
<label>Login</label>
|
||||||
|
<div class="code-row"><span class="code">newsletter</span><button type="button" class="copy">Copy</button></div>
|
||||||
|
<label>Password</label>
|
||||||
|
<div class="code-row"><span class="code">xK.9fQ2m-pL7wR</span><button type="button" class="copy">Copy</button></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="fill">
|
||||||
|
<div class="card" id="dns-status">
|
||||||
|
<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="stack">
|
||||||
|
<div class="pair">
|
||||||
|
<div>
|
||||||
|
<label>DKIM <span class="st st-ok">ok</span></label>
|
||||||
|
<div class="field-row">
|
||||||
|
<div class="field">
|
||||||
|
<label>Host / name</label>
|
||||||
|
<span class="code">mail._domainkey.example.com</span>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Type</label>
|
||||||
|
<span class="code">TXT</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>SPF <span class="st st-ok">ok</span></label>
|
||||||
|
<div class="field-row">
|
||||||
|
<div class="field">
|
||||||
|
<label>Host / name</label>
|
||||||
|
<span class="code">example.com</span>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Type</label>
|
||||||
|
<span class="code">TXT</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pair">
|
||||||
|
<div>
|
||||||
|
<label>DMARC <span class="st st-ok">ok</span></label>
|
||||||
|
<div class="field-row">
|
||||||
|
<div class="field">
|
||||||
|
<label>Host / name</label>
|
||||||
|
<span class="code">_dmarc.example.com</span>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Type</label>
|
||||||
|
<span class="code">TXT</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="muted">p=none; rua points at SelfPost ingest.</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Report authorization</label>
|
||||||
|
<p class="muted">Not required (rua= is on a domain SelfPost accepts).</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="actions-row"><button type="button">Re-check</button></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pair">
|
||||||
|
<div class="card" id="dkim-spf">
|
||||||
|
<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>
|
||||||
|
<div class="field-row">
|
||||||
|
<div class="field">
|
||||||
|
<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">
|
||||||
|
<label>Type</label>
|
||||||
|
<span class="code">TXT</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label>Value</label>
|
||||||
|
<div class="code-row"><span class="code">v=DKIM1; k=rsa; p=MIIBIjANBgkqh…</span><button type="button" class="copy">Copy</button></div>
|
||||||
|
<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>
|
||||||
|
<div class="card" id="dmarc-record">
|
||||||
|
<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-row">
|
||||||
|
<div class="field">
|
||||||
|
<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">
|
||||||
|
<label>Type</label>
|
||||||
|
<span class="code">TXT</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
<p class="muted"><a href="dmarc-domain.html">Open DMARC reports</a> for this domain.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pair">
|
||||||
|
<div class="card" id="connection">
|
||||||
|
<div class="card-head">
|
||||||
|
<h2>Connection settings</h2>
|
||||||
|
<label class="help-link" for="help-connection" title="How to connect">?</label>
|
||||||
|
</div>
|
||||||
|
<label>Server</label>
|
||||||
|
<div class="code-row"><span class="code">mail.example.org</span><button type="button" class="copy">Copy</button></div>
|
||||||
|
<label>Port and encryption</label>
|
||||||
|
<span class="code">465 — SSL/TLS (implicit)
|
||||||
|
587 — STARTTLS (submission)</span>
|
||||||
|
</div>
|
||||||
|
<div class="card" id="add-application">
|
||||||
|
<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">
|
||||||
|
<label>Login</label>
|
||||||
|
<input placeholder="prod-server" autocomplete="off">
|
||||||
|
<label>Address mode</label>
|
||||||
|
<select><option>Any address of the domain</option><option>Specific addresses (list)</option></select>
|
||||||
|
<div class="actions-row"><button type="button">Create application</button></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="fill">
|
||||||
|
<div class="card" id="applications">
|
||||||
|
<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">
|
||||||
|
<li class="app-item">
|
||||||
|
<p class="app-login">newsletter</p>
|
||||||
|
<p class="app-addr muted">Any address of the domain — *@example.com</p>
|
||||||
|
<div class="actions">
|
||||||
|
<input class="panel-toggle t-edit" id="edit-1" type="checkbox" checked>
|
||||||
|
<label class="toggle for-edit" for="edit-1">Edit <span class="st st-ok">limit</span></label>
|
||||||
|
<button type="button">New password</button>
|
||||||
|
<button type="button" class="danger">Delete</button>
|
||||||
|
<div class="panel panel-edit">
|
||||||
|
<div class="pair">
|
||||||
|
<div>
|
||||||
|
<p class="check-col-title">Address mode</p>
|
||||||
|
<select><option selected>Any address of the domain</option><option>Specific addresses (list)</option></select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="check-col-title">Trusted-IP override <span class="st st-ok">active</span></p>
|
||||||
|
<textarea rows="1">203.0.113.40</textarea>
|
||||||
|
<div class="field-row equal">
|
||||||
|
<div class="field">
|
||||||
|
<label>Message limit</label>
|
||||||
|
<input value="80">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Window (seconds)</label>
|
||||||
|
<input value="60">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="app-item">
|
||||||
|
<p class="app-login">billing</p>
|
||||||
|
<p class="app-addr muted">Fixed list — invoices@example.com</p>
|
||||||
|
<div class="actions">
|
||||||
|
<button type="button">Edit</button>
|
||||||
|
<button type="button">New password</button>
|
||||||
|
<button type="button" class="danger">Delete</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pair">
|
||||||
|
<div class="card" id="domain-settings-dmarc">
|
||||||
|
<div class="card-head">
|
||||||
|
<h2>DMARC reports</h2>
|
||||||
|
<label class="help-link" for="help-domain-settings" title="Reports and rate limits">?</label>
|
||||||
|
</div>
|
||||||
|
<label>Aggregate reports (rua=)</label>
|
||||||
|
<select><option selected>Same as Settings (dmarc@mail.example.org)</option><option>No aggregate reports</option><option>Custom address</option></select>
|
||||||
|
<div class="actions-row"><button type="button">Save DMARC report settings</button></div>
|
||||||
|
</div>
|
||||||
|
<div class="card" id="domain-settings-limit">
|
||||||
|
<div class="card-head">
|
||||||
|
<h2>Level-2 rate limit <span class="st st-ok">active</span></h2>
|
||||||
|
</div>
|
||||||
|
<div class="field-row equal">
|
||||||
|
<div class="field">
|
||||||
|
<label>Message limit</label>
|
||||||
|
<input value="40">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Window (seconds)</label>
|
||||||
|
<input value="60">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="actions-row"><button type="button">Save limit</button></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pair">
|
||||||
|
<div class="card" id="export-domain">
|
||||||
|
<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>
|
||||||
|
<div class="encrypt">
|
||||||
|
<label class="check"><input type="checkbox"> <span>Encrypt with a password</span></label>
|
||||||
|
<div class="encrypt-fields">
|
||||||
|
<label>Password</label><input type="password">
|
||||||
|
<label>Repeat password</label><input type="password">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="actions-row"><button type="button">Export domain</button></div>
|
||||||
|
</div>
|
||||||
|
<div class="card g-only">
|
||||||
|
<h2>Danger zone</h2>
|
||||||
|
<p class="muted">Deletes the DKIM key and every application on this domain.</p>
|
||||||
|
<div class="actions-row"><a class="danger" href="domain-delete.html">Delete domain</a></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<script src="shell.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Domains — SelfPost mockups</title>
|
||||||
|
<link rel="icon" href="../selfpost-icon-16.svg" type="image/svg+xml">
|
||||||
|
<link rel="stylesheet" href="mock.css">
|
||||||
|
<link rel="stylesheet" href="system.css">
|
||||||
|
</head>
|
||||||
|
<body class="page-domains" data-page="domains" data-nav="domains" data-title="Domains">
|
||||||
|
<main class="stack">
|
||||||
|
<div class="page-head"><h1>Domains</h1></div>
|
||||||
|
<div class="fill">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Domains</h2>
|
||||||
|
<form class="measure g-only" action="#" onsubmit="return false;">
|
||||||
|
<label for="dname">Add a sending domain</label>
|
||||||
|
<div class="input-row">
|
||||||
|
<input id="dname" placeholder="example.com" autocomplete="off">
|
||||||
|
<button type="button">Add domain</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<table class="desk-only">
|
||||||
|
<thead><tr><th>Domain</th><th>DNS</th><th>Selector</th><th>Apps</th><th></th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><a href="domain.html">example.com</a></td>
|
||||||
|
<td><a class="st st-ok" href="domain.html">ok</a></td>
|
||||||
|
<td class="muted">mail</td>
|
||||||
|
<td>2</td>
|
||||||
|
<td class="actions g-only"><a class="danger" href="domain-delete.html">Delete</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><a href="domain.html">alerts.example.com</a></td>
|
||||||
|
<td><a class="st st-warn" href="domain.html">warn</a></td>
|
||||||
|
<td class="muted">mail</td>
|
||||||
|
<td>1</td>
|
||||||
|
<td class="actions g-only"><a class="danger" href="domain-delete.html">Delete</a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<ul class="phone-list phone-only">
|
||||||
|
<li><a href="domain.html"><span>example.com</span><span class="st st-ok">ok</span><span class="meta">2 apps · selector mail</span></a></li>
|
||||||
|
<li><a href="domain.html"><span>alerts.example.com</span><span class="st st-warn">warn</span><span class="meta">1 app · selector mail</span></a></li>
|
||||||
|
</ul>
|
||||||
|
<p class="muted">The DNS badge is the worst of DKIM, SPF and DMARC. Open a domain for details.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<script src="shell.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Help — SelfPost mockups</title>
|
||||||
|
<link rel="icon" href="../selfpost-icon-16.svg" type="image/svg+xml">
|
||||||
|
<link rel="stylesheet" href="mock.css">
|
||||||
|
<link rel="stylesheet" href="system.css">
|
||||||
|
</head>
|
||||||
|
<body class="page-help" data-page="help" data-nav="help" data-title="Help">
|
||||||
|
<main class="stack">
|
||||||
|
<div class="page-head">
|
||||||
|
<h1>Help <span class="tag future">candidate</span></h1>
|
||||||
|
</div>
|
||||||
|
<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="measure stack">
|
||||||
|
<div class="card">
|
||||||
|
<h2>On this panel</h2>
|
||||||
|
<ul>
|
||||||
|
<li><a href="help.html">Machine</a> — kernel counters and the rate window</li>
|
||||||
|
<li><a href="help.html">TLS certificate</a> — port 465, reverse-proxy mount</li>
|
||||||
|
<li><a href="help.html">Hostname / reverse DNS</a> — forward-confirmed PTR at the hosting provider</li>
|
||||||
|
<li><a href="help.html">Mail queue retries</a> — time-based, no attempt budget</li>
|
||||||
|
<li><a href="help.html">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>
|
||||||
|
<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 class="card">
|
||||||
|
<h2>Machine</h2>
|
||||||
|
<p>CPU and memory are the container’s own readings, not the host’s spare capacity. Network is a short window, not a daily total. High CPU with an empty queue usually means something else on the box — not SelfPost “being slow to send”.</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>TLS certificate</h2>
|
||||||
|
<p>Port 465 presents the certificate the reverse proxy (or the image) mounted. The panel does not issue certificates. A warn here is “expires soon”; an error is “missing or unreadable”, and clients will refuse submission.</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>Hostname / reverse DNS</h2>
|
||||||
|
<p>Forward-confirmed reverse DNS: the A/AAAA for <code>SELFPOST_HOSTNAME</code> must reverse to that same name. PTR is set at the hosting provider, not in this panel. Receiving networks use this pair as a cheap reputation check.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<script src="shell.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>backup.example.net — SelfPost mockups</title>
|
||||||
|
<link rel="icon" href="../selfpost-icon-16.svg" type="image/svg+xml">
|
||||||
|
<link rel="stylesheet" href="mock.css">
|
||||||
|
<link rel="stylesheet" href="system.css">
|
||||||
|
</head>
|
||||||
|
<body class="page-inbound-backup" data-page="inbound-backup" data-nav="inbound" data-title="backup.example.net" data-global-only="1">
|
||||||
|
<main class="stack">
|
||||||
|
<div>
|
||||||
|
<h1>backup.example.net</h1>
|
||||||
|
<a class="back" href="inbound.html">← All inbound domains</a>
|
||||||
|
</div>
|
||||||
|
<div class="fill">
|
||||||
|
<div class="card">
|
||||||
|
<h2>DNS status <span class="st st-error">error</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>
|
||||||
|
<label>MX <span class="st st-error">error</span></label>
|
||||||
|
<div class="field-row">
|
||||||
|
<div class="field">
|
||||||
|
<label>Host / name</label>
|
||||||
|
<span class="code">backup.example.net</span>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<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 class="actions-row"><button type="button">Re-check</button></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pair">
|
||||||
|
<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>
|
||||||
|
<div class="actions-row"><button type="button">Save upstream</button></div>
|
||||||
|
</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-row">
|
||||||
|
<div class="field">
|
||||||
|
<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">
|
||||||
|
<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="pair">
|
||||||
|
<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>
|
||||||
|
<div class="actions-row"><button type="button">Save recipients</button></div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>Danger zone</h2>
|
||||||
|
<p class="muted">Stops accepting mail for this domain. Does not touch outbound sending domains.</p>
|
||||||
|
<div class="actions-row"><a class="danger" href="inbound-delete.html">Delete inbound domain</a></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<script src="shell.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Delete lists.example.com — SelfPost mockups</title>
|
||||||
|
<link rel="icon" href="../selfpost-icon-16.svg" type="image/svg+xml">
|
||||||
|
<link rel="stylesheet" href="mock.css">
|
||||||
|
<link rel="stylesheet" href="system.css">
|
||||||
|
</head>
|
||||||
|
<body class="page-inbound-delete" data-page="inbound-delete" data-nav="inbound" data-title="Delete inbound" data-global-only="1">
|
||||||
|
<main class="stack">
|
||||||
|
<div>
|
||||||
|
<h1>Delete lists.example.com</h1>
|
||||||
|
<a class="back" href="inbound-domain.html">← Back to lists.example.com</a>
|
||||||
|
</div>
|
||||||
|
<div class="measure">
|
||||||
|
<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>
|
||||||
|
<div class="actions-row"><button type="button" class="danger">Delete lists.example.com</button></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<script src="shell.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>lists.example.com — SelfPost mockups</title>
|
||||||
|
<link rel="icon" href="../selfpost-icon-16.svg" type="image/svg+xml">
|
||||||
|
<link rel="stylesheet" href="mock.css">
|
||||||
|
<link rel="stylesheet" href="system.css">
|
||||||
|
</head>
|
||||||
|
<body class="page-inbound-domain" data-page="inbound-domain" data-nav="inbound" data-title="lists.example.com" data-global-only="1">
|
||||||
|
<main class="stack">
|
||||||
|
<div>
|
||||||
|
<h1>lists.example.com</h1>
|
||||||
|
<a class="back" href="inbound.html">← All inbound domains</a>
|
||||||
|
</div>
|
||||||
|
<div class="fill">
|
||||||
|
<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>
|
||||||
|
<label>MX <span class="st st-ok">ok</span></label>
|
||||||
|
<div class="field-row">
|
||||||
|
<div class="field">
|
||||||
|
<label>Host / name</label>
|
||||||
|
<span class="code">lists.example.com</span>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<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 class="actions-row"><button type="button">Re-check</button></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pair">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Upstream</h2>
|
||||||
|
<p class="muted">Where accepted mail is handed off. Not a mailbox.</p>
|
||||||
|
<label>Host</label><input value="10.0.0.8">
|
||||||
|
<label>Port</label><input value="25">
|
||||||
|
<label>TLS to upstream</label>
|
||||||
|
<select><option>Opportunistic</option><option selected>Required</option><option>Off</option></select>
|
||||||
|
<div class="actions-row"><button type="button">Save upstream</button></div>
|
||||||
|
</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-row">
|
||||||
|
<div class="field">
|
||||||
|
<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">
|
||||||
|
<label>Type</label>
|
||||||
|
<span class="code">MX</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label>Value</label>
|
||||||
|
<div class="code-row"><span class="code">10 mail.example.org.</span><button type="button" class="copy">Copy</button></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pair">
|
||||||
|
<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" 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
|
||||||
|
abuse@lists.example.com</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>
|
||||||
|
<div class="actions-row"><button type="button">Save recipients</button></div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>Danger zone</h2>
|
||||||
|
<p class="muted">Stops accepting mail for this domain. Does not touch outbound sending domains.</p>
|
||||||
|
<div class="actions-row"><a class="danger" href="inbound-delete.html">Delete inbound domain</a></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<script src="shell.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Inbound — SelfPost mockups</title>
|
||||||
|
<link rel="icon" href="../selfpost-icon-16.svg" type="image/svg+xml">
|
||||||
|
<link rel="stylesheet" href="mock.css">
|
||||||
|
<link rel="stylesheet" href="system.css">
|
||||||
|
</head>
|
||||||
|
<body class="page-inbound" data-page="inbound" data-nav="inbound" data-title="Inbound" data-global-only="1">
|
||||||
|
<main class="stack">
|
||||||
|
<div class="page-head">
|
||||||
|
<h1>Inbound <span class="tag">1.x</span></h1>
|
||||||
|
</div>
|
||||||
|
<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="fill">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Forwarding</h2>
|
||||||
|
<form class="measure" action="#" onsubmit="return false">
|
||||||
|
<label for="iname">Add inbound domain</label>
|
||||||
|
<div class="input-row">
|
||||||
|
<input id="iname" placeholder="lists.example.com">
|
||||||
|
<button type="button">Add domain</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<table class="desk-only">
|
||||||
|
<thead><tr><th>Domain</th><th>DNS</th><th>Upstream</th><th>Recipients</th><th>TLS</th><th></th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><a href="inbound-domain.html">lists.example.com</a></td>
|
||||||
|
<td><a class="st st-ok" href="inbound-domain.html">ok</a></td>
|
||||||
|
<td class="muted">10.0.0.8:25</td>
|
||||||
|
<td>12 listed</td>
|
||||||
|
<td><span class="st st-ok">required</span></td>
|
||||||
|
<td class="actions"><a class="danger" href="inbound-delete.html">Delete</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><a href="inbound-backup.html">backup.example.net</a></td>
|
||||||
|
<td><a class="st st-error" href="inbound-backup.html">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.html">Delete</a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<ul class="phone-list phone-only">
|
||||||
|
<li><a href="inbound-domain.html"><span>lists.example.com</span><span class="st st-ok">ok</span><span class="meta">12 listed · 10.0.0.8:25</span></a></li>
|
||||||
|
<li><a href="inbound-backup.html"><span>backup.example.net</span><span class="st st-error">error</span><span class="meta">any recipient · 192.0.2.20:25</span></a></li>
|
||||||
|
</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>
|
||||||
|
<script src="shell.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>SelfPost — макеты панели</title>
|
||||||
|
<link rel="icon" href="../selfpost-icon-16.svg" type="image/svg+xml">
|
||||||
|
<link rel="stylesheet" href="mock.css">
|
||||||
|
</head>
|
||||||
|
<body class="doc">
|
||||||
|
<div class="wrap">
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<p class="eyebrow">макеты · не панель</p>
|
||||||
|
<h1>Self<b>Post</b> — полное обновление интерфейса</h1>
|
||||||
|
<p>Проект системы — как собирать экраны, чтобы не чинить пустые колонки, высоту полей и ряды кнопок по одному. Каждый экран — отдельная страница, не простыня с якорями. Знак и палитра из листа утверждения не меняются. HTML в <code>docs/assets/panel-ui/</code>, не вёрстка <code>internal/web</code>.</p>
|
||||||
|
<div class="cta-row">
|
||||||
|
<a class="cta" href="status.html">Открыть Status</a>
|
||||||
|
<a class="cta quiet" href="system.html">Система интерфейса</a>
|
||||||
|
<a class="cta quiet" href="status.html?view=phone">Аварийный телефон</a>
|
||||||
|
<a class="cta quiet" href="status.html?theme=dark">Status, тёмная тема</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Система, не рескин</h2>
|
||||||
|
<p>Первый click-through повторил сетку живой панели, поэтому те же сбои чинились по одному. <a href="system.html">Грамматика</a> задаёт регионы (<code>measure</code> / <code>pair</code> / <code>fill</code>), карточку со слотом справки, <code>field-row</code> (Host и Type одной высоты) и <code>actions-row</code>. Макеты собраны только из этого: один экран — один HTML-файл.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Ширина окна</h2>
|
||||||
|
<p>Сейчас оболочка центрируется, колонка контента упирается в <code>64rem</code>, формы ещё уже — <code>48rem</code>. На широком мониторе поля пустые. Растянуть поля на 100% окна нельзя — они становятся нечитаемыми. Две независимые карточки рядом — можно.</p>
|
||||||
|
<h3>Гибрид</h3>
|
||||||
|
<p>Навигация прижата влево. Ops-страницы (Status, Deliveries, очередь, лог, Backup, Settings, DMARC, inbound) занимают остаток до <strong>90rem (~1440px)</strong>. Backup — полный бэкап ‖ импорт. Settings — учётные данные ‖ DMARC (у domain-admin остаётся одна узкая карточка). Таблица Users — <code>fill</code>. Одиночные формы (подтверждения, форма пользователя) остаются <strong>42rem и выровнены влево</strong>. Login и setup — по-прежнему узкий центрированный блок.</p>
|
||||||
|
<div class="compare">
|
||||||
|
<div>
|
||||||
|
<div class="row">сейчас, окно ~1600px</div>
|
||||||
|
<div class="bar">
|
||||||
|
<i class="navc">nav</i>
|
||||||
|
<i class="col" style="width:42%">64rem</i>
|
||||||
|
<i class="empty">пусто</i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="row">макет, ops-страница</div>
|
||||||
|
<div class="bar">
|
||||||
|
<i class="navc">nav</i>
|
||||||
|
<i class="fill">контент до 90rem</i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="row">макет, форма</div>
|
||||||
|
<div class="bar">
|
||||||
|
<i class="navc">nav</i>
|
||||||
|
<i class="col" style="width:28%">42rem</i>
|
||||||
|
<i class="empty">поле не растягивает инпуты</i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="note">На телефоне это не «узкий десктоп». Таблицы из шести колонок складываются в список. Навигация — выезжающая колонка на CSS-checkbox, без обязательного JavaScript (как панели Edit у приложений уже сейчас).</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Информационная архитектура</h2>
|
||||||
|
<div class="ia">
|
||||||
|
<div>
|
||||||
|
<h3>Global administrator</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Status</li>
|
||||||
|
<li>Domains → domain → delete</li>
|
||||||
|
<li>Deliveries → message</li>
|
||||||
|
<li>Mail queue</li>
|
||||||
|
<li>System log</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>Backup, Users, Settings</li>
|
||||||
|
<li>Help <span class="tag future">candidate</span> — пункт навбара и drawer с карточек Status и страницы домена</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3>Domain-admin</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Domains (только назначенные) → domain → DMARC reports этого домена</li>
|
||||||
|
<li>Deliveries → message, включая пояснение retry</li>
|
||||||
|
<li>Settings — только учётные данные панели</li>
|
||||||
|
<li>Help drawer</li>
|
||||||
|
<li>Нет Status, очереди, system log, Backup, Users, inbound, глобального DMARC hub</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="muted" style="margin-top:1rem">В прототипе роль, inbound-флаг, ширина и тема переключаются в шапке. <code>contributing</code> и <code>schema-squash</code> экранов не имеют.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Знак и новые иконки</h2>
|
||||||
|
<p>Утверждённый stamp не заменяется. Для узкой шапки телефона предлагается уже существующий SP-значок (фавикон), а не компактный wordmark: на 390px высота важнее, чем прочитать «SelfPost» второй раз.</p>
|
||||||
|
<div class="mark-row">
|
||||||
|
<figure>
|
||||||
|
<img src="../selfpost-stamp.svg" width="220" height="100" alt="Полный знак">
|
||||||
|
<figcaption>эталон · login / setup</figcaption>
|
||||||
|
</figure>
|
||||||
|
<figure>
|
||||||
|
<img src="../selfpost-stamp-compact.svg" width="160" height="73" alt="Компактный знак">
|
||||||
|
<figcaption>колонка навбара</figcaption>
|
||||||
|
</figure>
|
||||||
|
<figure>
|
||||||
|
<img src="../selfpost-icon.svg" width="64" height="64" alt="SP">
|
||||||
|
<figcaption>телефон · шапка drawer</figcaption>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<h3>Иконки новых пунктов</h3>
|
||||||
|
<p class="muted">Тот же штрих 1.5, что у Status / Domains. Не пиктограммы-эмодзи и не заливка.</p>
|
||||||
|
<div class="icons-row">
|
||||||
|
<div class="icon-card">
|
||||||
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2.5 9.5h11"/><path d="M8 2.75v6.2"/><path d="M5.4 6.4 8 9.05 10.6 6.4"/><path d="M3.2 12.6h9.6"/></svg>
|
||||||
|
<span>Inbound</span>
|
||||||
|
</div>
|
||||||
|
<div class="icon-card">
|
||||||
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 1.85 2.75 3.7v4.2c0 3.15 2.15 5.2 5.25 6.25 3.1-1.05 5.25-3.1 5.25-6.25V3.7Z"/><path d="M5.4 8.05 7.15 9.8 10.7 6.2"/></svg>
|
||||||
|
<span>DMARC</span>
|
||||||
|
</div>
|
||||||
|
<div class="icon-card">
|
||||||
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="8" cy="8" r="6.25"/><path d="M8 7.2V11.4"/><path d="M8 5.05v.01"/></svg>
|
||||||
|
<span>Help</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Экраны</h2>
|
||||||
|
<h3>Система</h3>
|
||||||
|
<div class="screen-index">
|
||||||
|
<a href="system.html">Грамматика и живые образцы</a>
|
||||||
|
</div>
|
||||||
|
<h3>Вход</h3>
|
||||||
|
<div class="screen-index">
|
||||||
|
<a href="login.html">Sign in</a>
|
||||||
|
<a href="setup.html">Create administrator</a>
|
||||||
|
</div>
|
||||||
|
<h3>Текущая панель</h3>
|
||||||
|
<div class="screen-index">
|
||||||
|
<a href="status.html">Status</a>
|
||||||
|
<a href="domains.html">Domains</a>
|
||||||
|
<a href="domain.html">Domain detail</a>
|
||||||
|
<a href="domain-delete.html">Delete domain</a>
|
||||||
|
<a href="deliveries.html">Deliveries</a>
|
||||||
|
<a href="delivery.html">Delivery (deferred + retries)</a>
|
||||||
|
<a href="mail-queue.html">Mail queue + retry policy</a>
|
||||||
|
<a href="system-log.html">System log</a>
|
||||||
|
<a href="backup.html">Backup</a>
|
||||||
|
<a href="users.html">Users</a>
|
||||||
|
<a href="user-form.html">Edit user</a>
|
||||||
|
<a href="user-delete.html">Delete user</a>
|
||||||
|
<a href="settings.html">Settings</a>
|
||||||
|
</div>
|
||||||
|
<h3>Roadmap</h3>
|
||||||
|
<div class="screen-index">
|
||||||
|
<a href="mail-queue.html">queue-retries — карточка на Mail queue</a>
|
||||||
|
<a href="delivery.html">queue-retries — history deferred</a>
|
||||||
|
<a href="inbound.html">Inbound list (колонка DNS = MX)</a>
|
||||||
|
<a href="inbound-domain.html">Inbound domain — DNS ok</a>
|
||||||
|
<a href="inbound-backup.html">Inbound domain — DNS error</a>
|
||||||
|
<a href="inbound-delete.html">Delete inbound domain</a>
|
||||||
|
<a href="status.html?inbound=0">Inbound выключен (пункта нет)</a>
|
||||||
|
<a href="dmarc.html">DMARC reports hub <span class="tag future">candidate</span></a>
|
||||||
|
<a href="dmarc-domain.html">DMARC — domain roll-up</a>
|
||||||
|
<a href="dmarc-report.html">DMARC report — aligned</a>
|
||||||
|
<a href="dmarc-report-fail.html">DMARC report — third-party fail</a>
|
||||||
|
<a href="help.html">Help <span class="tag future">candidate</span></a>
|
||||||
|
</div>
|
||||||
|
<h3>Аварийный телефон</h3>
|
||||||
|
<div class="screen-index">
|
||||||
|
<a href="status.html?view=phone">Status 390px — Overall, then queue/cert, then the rest</a>
|
||||||
|
<a href="mail-queue.html?view=phone">Mail queue 390px</a>
|
||||||
|
<a href="deliveries.html?view=phone">Deliveries 390px — список, не таблица</a>
|
||||||
|
<a href="delivery.html?view=phone">Delivery 390px</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section style="border-bottom:none">
|
||||||
|
<h2>Как смотреть</h2>
|
||||||
|
<p>Откройте любой экран из списка — это отдельная страница, не якорь в одном файле. Шапка прототипа — не часть панели: роль, Desktop/Phone, inbound, светлая/тёмная. Копирайт экранов английский, как в продукте. Drawer Help открывается с «?» на Status или с карточек домена; на карточках остаются показания и колонка Detail.</p>
|
||||||
|
<p class="muted">После утверждения макетов вёрстка <code>panel.css</code> и шаблонов — отдельная задача. CSP и progressive enhancement в этом HTML не воспроизводятся один в один: здесь допустимы вещи, которые в панели останутся в файле стилей.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Sign in — SelfPost mockups</title>
|
||||||
|
<link rel="icon" href="../selfpost-icon-16.svg" type="image/svg+xml">
|
||||||
|
<link rel="stylesheet" href="mock.css">
|
||||||
|
<link rel="stylesheet" href="system.css">
|
||||||
|
</head>
|
||||||
|
<body class="page-login" data-page="login" data-nav="login" data-title="Sign in" data-auth="1">
|
||||||
|
<main class="auth">
|
||||||
|
<img class="mark" src="../selfpost-stamp.svg" width="330" height="150" alt="SelfPost">
|
||||||
|
<h1>Sign in</h1>
|
||||||
|
<div class="card">
|
||||||
|
<form action="status.html">
|
||||||
|
<label for="u">Username</label>
|
||||||
|
<input id="u" name="username" autocomplete="username">
|
||||||
|
<label for="p">Password</label>
|
||||||
|
<input id="p" name="password" type="password" autocomplete="current-password">
|
||||||
|
<div class="actions-row"><button type="submit">Sign in</button></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<p class="version" style="text-align:center">© Mixeme · <a href="#">License (AGPL-3.0)</a></p>
|
||||||
|
</main>
|
||||||
|
<script src="shell.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Mail queue — SelfPost mockups</title>
|
||||||
|
<link rel="icon" href="../selfpost-icon-16.svg" type="image/svg+xml">
|
||||||
|
<link rel="stylesheet" href="mock.css">
|
||||||
|
<link rel="stylesheet" href="system.css">
|
||||||
|
</head>
|
||||||
|
<body class="page-mail-queue" data-page="mail-queue" data-nav="queue" data-title="Mail queue" data-global-only="1">
|
||||||
|
<main class="stack">
|
||||||
|
<h1>Mail queue</h1>
|
||||||
|
<div class="fill">
|
||||||
|
<div class="card">
|
||||||
|
<h2>How delivery retries work</h2>
|
||||||
|
<p class="muted">This Postfix’s policy, read once at panel start. There is no maximum attempt count — only time.</p>
|
||||||
|
<div class="facts retry-facts">
|
||||||
|
<div class="fact"><span class="fact-label">First retry</span><span class="fact-value">5 minutes</span></div>
|
||||||
|
<div class="fact"><span class="fact-label">Later retries</span><span class="fact-value">doubling, cap 1 h 7 min</span></div>
|
||||||
|
<div class="fact"><span class="fact-label">Kept in queue</span><span class="fact-value">5 days</span></div>
|
||||||
|
<div class="fact"><span class="fact-label">Then</span><span class="fact-value">bounced</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="fill">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Pending messages</h2>
|
||||||
|
<table class="desk-only">
|
||||||
|
<thead><tr><th>Queue id</th><th>Age</th><th>From</th><th>To</th><th>Size</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td class="mono">4C3A1E2F1A</td><td class="time muted">18 min</td><td>billing@example.com</td><td>ada@example.net</td><td>12 KiB</td></tr>
|
||||||
|
<tr><td class="mono">4C3A1E3010</td><td class="time muted">11 min</td><td>news@example.com</td><td>pat@slow.example</td><td>48 KiB</td></tr>
|
||||||
|
<tr><td class="mono">4C3A1E3102</td><td class="time muted">4 min</td><td>billing@example.com</td><td>ada@example.net</td><td>9 KiB</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<ul class="phone-list phone-only">
|
||||||
|
<li><a href="delivery.html"><span>4C3A1E2F1A</span><span class="when">18 min</span><span class="meta">billing@example.com → ada@example.net</span></a></li>
|
||||||
|
<li><a href="delivery.html"><span>4C3A1E3010</span><span class="when">11 min</span><span class="meta">news@example.com → pat@slow.example</span></a></li>
|
||||||
|
<li><a href="delivery.html"><span>4C3A1E3102</span><span class="when">4 min</span><span class="meta">billing@example.com → ada@example.net</span></a></li>
|
||||||
|
</ul>
|
||||||
|
<label>postqueue -p</label>
|
||||||
|
<span class="code">-Queue ID- --Size-- ----Arrival Time---- -Sender/Recipient-------
|
||||||
|
4C3A1E2F1A* 12288 Sat Aug 15 20:14:02 billing@example.com
|
||||||
|
ada@example.net
|
||||||
|
4C3A1E3010 49152 Sat Aug 15 20:21:18 news@example.com
|
||||||
|
pat@slow.example
|
||||||
|
-- 3 Kbytes in 3 Requests.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<script src="shell.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,715 @@
|
|||||||
|
/* SelfPost panel UI mockups. Tokens match internal/web/view/static/panel.css.
|
||||||
|
This file is a design artifact, not the panel stylesheet. */
|
||||||
|
|
||||||
|
@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@200;300;400;500;600;700&display=swap");
|
||||||
|
|
||||||
|
:root {
|
||||||
|
color-scheme: light dark;
|
||||||
|
--font-sans: "IBM Plex Sans", system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
||||||
|
--font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||||
|
--bg: #f4f2ed; --fg: #12161c;
|
||||||
|
--muted: #6b7684;
|
||||||
|
--card-bg: #fff;
|
||||||
|
--border: #dedcd7;
|
||||||
|
--control-border: #cbc8c1;
|
||||||
|
--input-bg: #fff;
|
||||||
|
--code-bg: #efede9;
|
||||||
|
--surface-bg: #eae7e0; --surface-bg-hover: #e0dcd3; --surface-open-bg: #d8d3c8;
|
||||||
|
--nav-active-bg: #ede4de;
|
||||||
|
--accent-fill: #7a3b2e; --accent-fill-hover: #632f25; --accent-text: #7a3b2e;
|
||||||
|
--on-accent: #fff;
|
||||||
|
--flash-bg: #edf6ef; --flash-border: #c2e0cc; --flash-fg: #2c6b43;
|
||||||
|
--credential-bg: #fcf6e4; --credential-border: #e0c874;
|
||||||
|
--danger-fill: #b42318; --danger-fill-hover: #912018;
|
||||||
|
--danger-bg: #fbedea; --danger-border: #efccc4; --danger-fg: #b42318; --danger-bg-hover: #f7dfda;
|
||||||
|
--st-ok-bg: #edf6ef; --st-ok-fg: #2c6b43; --st-ok-border: #c2e0cc;
|
||||||
|
--st-warn-bg: #fbf2e2; --st-warn-fg: #8a5510; --st-warn-border: #ebd5a6;
|
||||||
|
--st-error-bg: #fbedea; --st-error-fg: #b42318; --st-error-border: #efccc4;
|
||||||
|
--st-unknown-bg: #efede9; --st-unknown-fg: #6b7684; --st-unknown-border: #dedcd7;
|
||||||
|
--nav-w: 14rem;
|
||||||
|
--ops-max: 90rem;
|
||||||
|
--form-max: 42rem;
|
||||||
|
--auth-max: 24rem;
|
||||||
|
--gallery-h: 3.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-tokens {
|
||||||
|
--bg: #16181b; --fg: #e9e6e0;
|
||||||
|
--muted: #9aa1a9;
|
||||||
|
--card-bg: #1d2024;
|
||||||
|
--border: #2c2f34;
|
||||||
|
--control-border: #3a3e44;
|
||||||
|
--input-bg: #14161a;
|
||||||
|
--code-bg: #14161a;
|
||||||
|
--surface-bg: #23262b; --surface-bg-hover: #2c3036; --surface-open-bg: #343941;
|
||||||
|
--nav-active-bg: #2a1f1b;
|
||||||
|
--accent-fill: #8e4535; --accent-fill-hover: #a0503e; --accent-text: #ce7b66;
|
||||||
|
--flash-bg: #132318; --flash-border: #22452f; --flash-fg: #7fcb9b;
|
||||||
|
--credential-bg: #26210d; --credential-border: #5e5013;
|
||||||
|
--danger-fill: #9b2c22; --danger-fill-hover: #b0342a;
|
||||||
|
--danger-bg: #2a1412; --danger-border: #5e2721; --danger-fg: #eb9b92; --danger-bg-hover: #381a17;
|
||||||
|
--st-ok-bg: #132318; --st-ok-fg: #7fcb9b; --st-ok-border: #22452f;
|
||||||
|
--st-warn-bg: #2a2109; --st-warn-fg: #e5be72; --st-warn-border: #5e4b12;
|
||||||
|
--st-error-bg: #2a1412; --st-error-fg: #eb9b92; --st-error-border: #5e2721;
|
||||||
|
--st-unknown-bg: #23262b; --st-unknown-fg: #9aa1a9; --st-unknown-border: #2c2f34;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root { color-scheme: dark; }
|
||||||
|
html:not(.force-light) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
html { scrollbar-gutter: stable; }
|
||||||
|
body {
|
||||||
|
font: 400 15px/1.5 var(--font-sans);
|
||||||
|
margin: 0; background: var(--bg); color: var(--fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctrl { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
|
||||||
|
|
||||||
|
/* —— Gallery chrome (index + prototype toolbar) —— */
|
||||||
|
.gallery {
|
||||||
|
position: sticky; top: 0; z-index: 40;
|
||||||
|
display: flex; flex-wrap: wrap; align-items: center; gap: 0.55rem 1rem;
|
||||||
|
min-height: var(--gallery-h);
|
||||||
|
padding: 0.45rem 1rem;
|
||||||
|
background: var(--surface-bg);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
.gallery a { color: var(--accent-text); }
|
||||||
|
.gallery .brand-mini {
|
||||||
|
font-family: var(--font-mono); font-size: 0.7rem; font-weight: 600;
|
||||||
|
letter-spacing: 0.12em; text-transform: uppercase; color: var(--accent-text);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.gallery .seg {
|
||||||
|
display: flex; flex-wrap: wrap; align-items: center; gap: 0.2rem 0.35rem;
|
||||||
|
}
|
||||||
|
.gallery .seg > span { color: var(--muted); margin-right: 0.15rem; }
|
||||||
|
.gallery .seg label {
|
||||||
|
margin: 0; font-weight: 500; cursor: pointer;
|
||||||
|
padding: 0.2rem 0.55rem; border: 1px solid var(--control-border);
|
||||||
|
border-radius: 5px; background: var(--card-bg); color: var(--fg);
|
||||||
|
}
|
||||||
|
#role-global:checked ~ .gallery label[for="role-global"],
|
||||||
|
#role-domain:checked ~ .gallery label[for="role-domain"],
|
||||||
|
#vp-desktop:checked ~ .gallery label[for="vp-desktop"],
|
||||||
|
#vp-phone:checked ~ .gallery label[for="vp-phone"],
|
||||||
|
#theme-light:checked ~ .gallery label[for="theme-light"],
|
||||||
|
#theme-dark:checked ~ .gallery label[for="theme-dark"] {
|
||||||
|
background: var(--nav-active-bg); color: var(--accent-text);
|
||||||
|
border-color: var(--accent-fill); font-weight: 600;
|
||||||
|
}
|
||||||
|
.gallery .check-lab {
|
||||||
|
display: flex; align-items: center; gap: 0.35rem; margin: 0; font-weight: 500; cursor: pointer;
|
||||||
|
}
|
||||||
|
.gallery .check-lab input { width: auto; margin: 0; }
|
||||||
|
.tag {
|
||||||
|
display: inline-block; font-family: var(--font-mono); font-size: 0.68rem;
|
||||||
|
font-weight: 500; letter-spacing: 0.06em; text-transform: uppercase;
|
||||||
|
padding: 0.12rem 0.4rem; border-radius: 4px;
|
||||||
|
background: var(--surface-bg); color: var(--muted); border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.tag.future { color: var(--st-warn-fg); border-color: var(--st-warn-border); background: var(--st-warn-bg); }
|
||||||
|
|
||||||
|
body:has(#theme-dark:checked) { background: #16181b; color: #e9e6e0; }
|
||||||
|
|
||||||
|
#theme-dark:checked ~ .gallery,
|
||||||
|
#theme-dark:checked ~ .app,
|
||||||
|
#theme-dark:checked ~ .help-drawer {
|
||||||
|
--bg: #16181b; --fg: #e9e6e0;
|
||||||
|
--muted: #9aa1a9;
|
||||||
|
--card-bg: #1d2024;
|
||||||
|
--border: #2c2f34;
|
||||||
|
--control-border: #3a3e44;
|
||||||
|
--input-bg: #14161a;
|
||||||
|
--code-bg: #14161a;
|
||||||
|
--surface-bg: #23262b; --surface-bg-hover: #2c3036; --surface-open-bg: #343941;
|
||||||
|
--nav-active-bg: #2a1f1b;
|
||||||
|
--accent-fill: #8e4535; --accent-fill-hover: #a0503e; --accent-text: #ce7b66;
|
||||||
|
--flash-bg: #132318; --flash-border: #22452f; --flash-fg: #7fcb9b;
|
||||||
|
--credential-bg: #26210d; --credential-border: #5e5013;
|
||||||
|
--danger-fill: #9b2c22; --danger-fill-hover: #b0342a;
|
||||||
|
--danger-bg: #2a1412; --danger-border: #5e2721; --danger-fg: #eb9b92; --danger-bg-hover: #381a17;
|
||||||
|
--st-ok-bg: #132318; --st-ok-fg: #7fcb9b; --st-ok-border: #22452f;
|
||||||
|
--st-warn-bg: #2a2109; --st-warn-fg: #e5be72; --st-warn-border: #5e4b12;
|
||||||
|
--st-error-bg: #2a1412; --st-error-fg: #eb9b92; --st-error-border: #5e2721;
|
||||||
|
--st-unknown-bg: #23262b; --st-unknown-fg: #9aa1a9; --st-unknown-border: #2c2f34;
|
||||||
|
background: var(--bg); color: var(--fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* —— Prototype shell: nav docked left, content uses remaining width —— */
|
||||||
|
.app {
|
||||||
|
display: flex; align-items: flex-start; min-height: calc(100vh - var(--gallery-h));
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
.stage { flex: 1 1 auto; min-width: 0; }
|
||||||
|
.nav {
|
||||||
|
position: sticky; top: var(--gallery-h); align-self: stretch;
|
||||||
|
flex: none; width: var(--nav-w);
|
||||||
|
display: flex; flex-direction: column; gap: 0.75rem;
|
||||||
|
padding: 1.1rem 0.85rem 1.4rem;
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
background: var(--bg);
|
||||||
|
max-height: calc(100vh - var(--gallery-h)); overflow-y: auto;
|
||||||
|
}
|
||||||
|
.nav .links, .nav .session { display: flex; flex-direction: column; gap: 0.1rem; }
|
||||||
|
.nav .session { margin-top: auto; padding-top: 0.75rem; border-top: 1px solid var(--border); }
|
||||||
|
.nav .session .muted { padding: 0 0.6rem; font-size: 0.85rem; overflow-wrap: anywhere; }
|
||||||
|
.nav .session-user { display: flex; align-items: center; gap: 0.5rem; }
|
||||||
|
.nav .brand { padding: 0; display: block; }
|
||||||
|
.nav .brand img { display: block; width: 100%; height: auto; }
|
||||||
|
.nav a, .nav [aria-current], .nav .current {
|
||||||
|
display: flex; align-items: center; gap: 0.5rem;
|
||||||
|
padding: 0.35rem 0.6rem; border-radius: 5px; text-decoration: none;
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
|
.nav a:hover { background: var(--surface-bg); }
|
||||||
|
.nav [aria-current], .nav .current {
|
||||||
|
font-weight: 600; color: var(--accent-text); background: var(--nav-active-bg);
|
||||||
|
box-shadow: inset 2px 0 0 var(--accent-fill);
|
||||||
|
}
|
||||||
|
body.page-status .n-status,
|
||||||
|
body.page-domains .n-domains,
|
||||||
|
body.page-domain .n-domains,
|
||||||
|
body.page-domain-delete .n-domains,
|
||||||
|
body.page-deliveries .n-deliveries,
|
||||||
|
body.page-delivery .n-deliveries,
|
||||||
|
body.page-mail-queue .n-queue,
|
||||||
|
body.page-system-log .n-log,
|
||||||
|
body.page-inbound .n-inbound,
|
||||||
|
body.page-inbound-domain .n-inbound,
|
||||||
|
body.page-inbound-backup .n-inbound,
|
||||||
|
body.page-inbound-delete .n-inbound,
|
||||||
|
body.page-dmarc .n-dmarc,
|
||||||
|
body.page-dmarc-domain .n-dmarc,
|
||||||
|
body.page-dmarc-report .n-dmarc,
|
||||||
|
body.page-dmarc-report-fail .n-dmarc,
|
||||||
|
body.page-backup .n-backup,
|
||||||
|
body.page-users .n-users,
|
||||||
|
body.page-user-form .n-users,
|
||||||
|
body.page-user-delete .n-users,
|
||||||
|
body.page-help .n-help,
|
||||||
|
body.page-settings .n-settings {
|
||||||
|
font-weight: 600; color: var(--accent-text); background: var(--nav-active-bg);
|
||||||
|
box-shadow: inset 2px 0 0 var(--accent-fill);
|
||||||
|
}
|
||||||
|
.nav .icon { width: 1rem; height: 1rem; flex: none; }
|
||||||
|
.nav button, .nav .btn-ghost {
|
||||||
|
display: flex; align-items: center; gap: 0.5rem;
|
||||||
|
margin: 0; padding: 0.35rem 0.6rem; font: inherit; font-size: 0.95rem; font-weight: 400;
|
||||||
|
color: var(--danger-fg); background: var(--danger-bg); border: 1px solid var(--danger-border);
|
||||||
|
border-radius: 5px; cursor: pointer; text-decoration: none; width: 100%;
|
||||||
|
}
|
||||||
|
.nav button:hover, .nav .btn-ghost:hover { background: var(--danger-bg-hover); }
|
||||||
|
|
||||||
|
.phone-bar { display: none; }
|
||||||
|
.phone-bar .icon { width: 1.1rem; height: 1.1rem; }
|
||||||
|
.mono { font-family: var(--font-mono); }
|
||||||
|
.sprite { position: absolute; width: 0; height: 0; overflow: hidden; }
|
||||||
|
|
||||||
|
#feat-inbound:checked ~ .gallery label[for="feat-inbound"] {
|
||||||
|
background: var(--nav-active-bg); color: var(--accent-text);
|
||||||
|
border-color: var(--accent-fill);
|
||||||
|
}
|
||||||
|
.gallery label[for="feat-inbound"] {
|
||||||
|
margin: 0; font-weight: 500; cursor: pointer;
|
||||||
|
padding: 0.2rem 0.55rem; border: 1px solid var(--control-border);
|
||||||
|
border-radius: 5px; background: var(--card-bg); color: var(--fg);
|
||||||
|
}
|
||||||
|
#role-domain:checked ~ .gallery .g-only { display: none; }
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1 1 auto; min-width: 0; width: 100%;
|
||||||
|
padding: 1.5rem 1.5rem 2.5rem;
|
||||||
|
}
|
||||||
|
main.auth {
|
||||||
|
max-width: none; display: flex; flex-direction: column; align-items: center;
|
||||||
|
padding-top: 3rem;
|
||||||
|
}
|
||||||
|
main.auth > * { width: 100%; max-width: var(--auth-max); margin-left: auto; margin-right: auto; }
|
||||||
|
main.stack > .page-head,
|
||||||
|
main.stack > h1 { margin-bottom: 0; }
|
||||||
|
main.stack > .back { margin: 0; }
|
||||||
|
main.stack > .route { margin-top: 0; margin-bottom: 0; }
|
||||||
|
main.stack > p.muted { margin-top: 0; margin-bottom: 0; }
|
||||||
|
main.stack > .toolbar { margin-bottom: 0; max-width: var(--ops-max); }
|
||||||
|
|
||||||
|
body.page-login .nav,
|
||||||
|
body.page-setup .nav,
|
||||||
|
body.page-login .phone-bar,
|
||||||
|
body.page-setup .phone-bar { display: none !important; }
|
||||||
|
body.page-login .app,
|
||||||
|
body.page-setup .app { display: block; }
|
||||||
|
|
||||||
|
#role-domain:checked ~ .app .g-only { display: none !important; }
|
||||||
|
#role-domain:checked ~ .app .pair:has(> .g-only) {
|
||||||
|
display: block;
|
||||||
|
max-width: var(--form-max);
|
||||||
|
}
|
||||||
|
#feat-inbound:not(:checked) ~ .app .in-only { display: none !important; }
|
||||||
|
#feat-inbound:checked ~ .app .in-off { display: none !important; }
|
||||||
|
|
||||||
|
h1 { font-size: 1.5rem; font-weight: 300; letter-spacing: -0.01em; margin: 0 0 1rem; }
|
||||||
|
h1.subject { overflow-wrap: anywhere; }
|
||||||
|
h2 { font-size: 1.05rem; font-weight: 600; margin: 0 0 0.4rem; }
|
||||||
|
.mark { display: block; width: 100%; max-width: 24rem; height: auto; margin-bottom: 1.4rem; }
|
||||||
|
.card {
|
||||||
|
background: var(--card-bg); border: 1px solid var(--border); border-radius: 6px;
|
||||||
|
padding: 1.25rem 1.4rem; margin: 0;
|
||||||
|
}
|
||||||
|
.card.narrow { max-width: var(--auth-max); }
|
||||||
|
.card + .card { margin-top: 1rem; }
|
||||||
|
.card.credential { border-color: var(--credential-border); background: var(--credential-bg); }
|
||||||
|
.muted { color: var(--muted); }
|
||||||
|
.error { color: var(--danger-fg); margin: 0.6rem 0 0; font-weight: 600; }
|
||||||
|
a { color: var(--accent-text); }
|
||||||
|
.back { display: block; margin: -0.4rem 0 1rem; }
|
||||||
|
.flash {
|
||||||
|
background: var(--flash-bg); border: 1px solid var(--flash-border); color: var(--flash-fg);
|
||||||
|
padding: 0.7rem 1rem; border-radius: 6px; margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.flash.error { background: var(--danger-bg); border-color: var(--danger-border); color: var(--danger-fg); }
|
||||||
|
.version { margin-top: 1.6rem; text-align: right; font-size: 0.8rem; color: var(--muted); }
|
||||||
|
.version a { color: inherit; }
|
||||||
|
|
||||||
|
label { display: block; font-weight: 600; margin: 0.9rem 0 0.3rem; }
|
||||||
|
label.check {
|
||||||
|
display: flex; align-items: center; gap: 0.5rem; margin: 0.45rem 0 0; font-weight: 600; cursor: pointer;
|
||||||
|
}
|
||||||
|
label.check input { width: auto; margin: 0; padding: 0; border: 0; background: none; }
|
||||||
|
input, select, textarea {
|
||||||
|
width: 100%; padding: 0.55rem 0.7rem; font-family: inherit; font-size: 1rem;
|
||||||
|
border: 1px solid var(--control-border); border-radius: 5px; background: var(--input-bg); color: inherit;
|
||||||
|
}
|
||||||
|
textarea { resize: vertical; }
|
||||||
|
fieldset {
|
||||||
|
margin: 0.9rem 0 0; padding: 0.55rem 0.85rem 0.85rem;
|
||||||
|
border: 1px solid var(--control-border); border-radius: 5px;
|
||||||
|
}
|
||||||
|
fieldset legend { padding: 0 0.25rem; font-weight: 600; }
|
||||||
|
fieldset > .muted { margin: 0.15rem 0 0.35rem; font-size: 0.85rem; font-weight: 400; }
|
||||||
|
|
||||||
|
button, a.btn, a.danger {
|
||||||
|
display: inline-block; margin-top: 1.1rem; padding: 0.6rem 1.1rem;
|
||||||
|
font: inherit; font-size: 1rem; font-weight: 600; text-decoration: none;
|
||||||
|
color: var(--on-accent); background: var(--accent-fill); border: 0; border-radius: 5px; cursor: pointer;
|
||||||
|
}
|
||||||
|
button:hover, a.btn:hover { background: var(--accent-fill-hover); }
|
||||||
|
button.danger, a.danger { background: var(--danger-fill); color: var(--on-accent); }
|
||||||
|
button.danger:hover, a.danger:hover { background: var(--danger-fill-hover); }
|
||||||
|
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 {
|
||||||
|
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;
|
||||||
|
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-warn { background: var(--st-warn-bg); color: var(--st-warn-fg); border-color: var(--st-warn-border); }
|
||||||
|
.st-error { background: var(--st-error-bg); color: var(--st-error-fg); border-color: var(--st-error-border); }
|
||||||
|
.st-unknown { background: var(--st-unknown-bg); color: var(--st-unknown-fg); border-color: var(--st-unknown-border); }
|
||||||
|
a.st, a.st:hover { color: inherit; text-decoration: none; }
|
||||||
|
|
||||||
|
table { width: 100%; border-collapse: collapse; }
|
||||||
|
th, td {
|
||||||
|
text-align: left; padding: 0.5rem 0.45rem; border-bottom: 1px solid var(--border);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
font-family: var(--font-mono); font-size: 0.75rem; font-weight: 500;
|
||||||
|
text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted);
|
||||||
|
}
|
||||||
|
td.actions { text-align: right; }
|
||||||
|
.metric { white-space: nowrap; }
|
||||||
|
.card:has(table) { overflow-x: auto; }
|
||||||
|
td.subject span {
|
||||||
|
display: block; max-width: 22rem;
|
||||||
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code {
|
||||||
|
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);
|
||||||
|
border-radius: 5px; padding: 0.7rem 0.8rem; margin: 0.3rem 0 0;
|
||||||
|
}
|
||||||
|
.code-row { display: flex; align-items: flex-start; gap: 0.5rem; }
|
||||||
|
.code-row .code { flex: 1; min-width: 0; margin-top: 0; padding-top: 0.45rem; padding-bottom: 0.45rem; }
|
||||||
|
.input-row { display: flex; align-items: stretch; gap: 0.5rem; }
|
||||||
|
.input-row input { flex: 1; min-width: 0; }
|
||||||
|
.input-row button { margin-top: 0; flex: none; white-space: nowrap; }
|
||||||
|
|
||||||
|
.split {
|
||||||
|
display: grid; grid-template-columns: repeat(auto-fit, minmax(22rem, 1fr));
|
||||||
|
gap: 1rem; margin-top: 1rem;
|
||||||
|
}
|
||||||
|
h1 + .split, h1 + form > .split { margin-top: 0; }
|
||||||
|
.split > .card { min-width: 0; }
|
||||||
|
.split > .card + .card { margin-top: 0; }
|
||||||
|
.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 {
|
||||||
|
display: grid; grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
|
||||||
|
gap: 1rem 1.2rem; margin-top: 1rem;
|
||||||
|
}
|
||||||
|
.check-col { min-width: 0; }
|
||||||
|
.check-col-title { margin: 0.85rem 0 0.35rem; font-size: 1.05rem; font-weight: 600; }
|
||||||
|
.field-pair {
|
||||||
|
display: grid; grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
|
||||||
|
gap: 0 1rem; margin-top: 0.45rem;
|
||||||
|
}
|
||||||
|
.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 .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; }
|
||||||
|
.fact { min-width: 0; padding: 0.5rem 0.7rem; border-radius: 6px; background: var(--surface-bg); }
|
||||||
|
.fact-label {
|
||||||
|
display: block; font-family: var(--font-mono); font-size: 0.7rem; font-weight: 500;
|
||||||
|
text-transform: uppercase; letter-spacing: 0.1em; color: var(--muted);
|
||||||
|
}
|
||||||
|
.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; }
|
||||||
|
|
||||||
|
.timeline {
|
||||||
|
list-style: none; display: flex; flex-direction: column; gap: 1.1rem;
|
||||||
|
margin: 1rem 0 0; padding: 0.2rem 0 0.2rem 1.4rem; border-left: 2px solid var(--border);
|
||||||
|
}
|
||||||
|
.event { position: relative; min-width: 0; }
|
||||||
|
.event::before {
|
||||||
|
content: ""; position: absolute; left: -1.85rem; top: 0.3rem;
|
||||||
|
width: 0.65rem; height: 0.65rem; border-radius: 50%;
|
||||||
|
background: var(--card-bg); border: 2px solid var(--control-border);
|
||||||
|
}
|
||||||
|
.event.lvl-ok::before { border-color: var(--st-ok-fg); background: var(--st-ok-bg); }
|
||||||
|
.event.lvl-warn::before { border-color: var(--st-warn-fg); background: var(--st-warn-bg); }
|
||||||
|
.event.lvl-error::before { border-color: var(--st-error-fg); background: var(--st-error-bg); }
|
||||||
|
.event.pending { opacity: 0.7; }
|
||||||
|
.event.pending::before { border-style: dashed; }
|
||||||
|
.event-time { margin: 0; font-family: var(--font-mono); font-size: 0.75rem; color: var(--muted); }
|
||||||
|
.event-title { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: 0.15rem 0 0; font-weight: 600; }
|
||||||
|
.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 .addr { font-family: var(--font-mono); font-size: 0.9rem; white-space: nowrap; overflow-x: auto; max-width: 100%; }
|
||||||
|
.route .arrow { color: var(--muted); }
|
||||||
|
|
||||||
|
table.log { margin-top: 1rem; }
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.copy, .actions button, .actions > label.toggle, .actions a.danger {
|
||||||
|
margin: 0; padding: 0.45rem 0.7rem; font-size: 0.8rem; font-weight: 600;
|
||||||
|
border-radius: 5px; white-space: nowrap;
|
||||||
|
background: var(--surface-bg); color: var(--accent-text); border: 1px solid var(--control-border);
|
||||||
|
}
|
||||||
|
button.copy:hover, .actions button:hover, .actions > label.toggle:hover { background: var(--surface-bg-hover); }
|
||||||
|
.actions button.danger, .actions a.danger { color: var(--danger-fg); background: var(--danger-bg); border-color: var(--danger-border); }
|
||||||
|
.actions { display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: center; }
|
||||||
|
|
||||||
|
.apps { list-style: none; margin: 1rem 0 0; padding: 0; }
|
||||||
|
.app-item { padding: 0.9rem 0; border-top: 1px solid var(--border); }
|
||||||
|
.app-item:last-child { padding-bottom: 0; }
|
||||||
|
.app-login { margin: 0; font-family: var(--font-mono); font-weight: 600; }
|
||||||
|
.app-addr { margin: 0.15rem 0 0; white-space: nowrap; overflow-x: auto; }
|
||||||
|
.app-item .actions { margin-top: 0.7rem; }
|
||||||
|
.app-item .actions > .panel-toggle {
|
||||||
|
position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; pointer-events: none;
|
||||||
|
}
|
||||||
|
.app-item .actions > .panel { display: none; flex: 1 0 100%; }
|
||||||
|
.app-item .actions > .t-edit:checked ~ .panel-edit { display: block; }
|
||||||
|
.app-item .actions > .t-edit:checked ~ .for-edit { background: var(--surface-open-bg); }
|
||||||
|
.app-item .actions > .panel button { margin-top: 0.9rem; }
|
||||||
|
|
||||||
|
.encrypt { margin-top: 1.2rem; }
|
||||||
|
.encrypt-fields {
|
||||||
|
display: none; margin-top: 0.8rem; margin-left: 1.6rem; padding-left: 0.9rem;
|
||||||
|
border-left: 2px solid var(--border);
|
||||||
|
}
|
||||||
|
.encrypt:has(input[type="checkbox"]:checked) .encrypt-fields { display: block; }
|
||||||
|
.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 {
|
||||||
|
display: flex; flex-wrap: wrap; gap: 0.6rem 1rem; align-items: end;
|
||||||
|
padding: 0.85rem 1rem; margin-bottom: 1rem;
|
||||||
|
background: var(--card-bg); border: 1px solid var(--border); border-radius: 6px;
|
||||||
|
}
|
||||||
|
.toolbar .field { flex: 1 1 10rem; min-width: 8rem; }
|
||||||
|
.toolbar label { margin-top: 0; font-size: 0.8rem; }
|
||||||
|
.toolbar button { margin-top: 0; }
|
||||||
|
|
||||||
|
/* Status: hero + dense grid */
|
||||||
|
.status-hero {
|
||||||
|
display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.75rem 1.2rem;
|
||||||
|
padding: 1rem 1.25rem; margin-bottom: 1rem;
|
||||||
|
background: var(--card-bg); border: 1px solid var(--border); border-radius: 6px;
|
||||||
|
}
|
||||||
|
.status-hero.attn,
|
||||||
|
.card.attn { border-color: var(--st-warn-border); }
|
||||||
|
.status-hero h1 { margin: 0; }
|
||||||
|
.status-hero .lead { margin: 0; color: var(--fg); }
|
||||||
|
.status-grid {
|
||||||
|
display: grid; grid-template-columns: repeat(auto-fit, minmax(17.5rem, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.status-grid .card { margin: 0; }
|
||||||
|
.status-grid .card.attn { border-color: var(--st-warn-border); }
|
||||||
|
.metric-row {
|
||||||
|
display: flex; justify-content: space-between; gap: 0.75rem; align-items: baseline;
|
||||||
|
padding: 0.35rem 0; border-bottom: 1px solid var(--border); font-size: 0.92rem;
|
||||||
|
}
|
||||||
|
.metric-row:last-child { border-bottom: 0; }
|
||||||
|
.metric-row .k { font-weight: 600; }
|
||||||
|
.metric-row .v { font-family: var(--font-mono); font-size: 0.85rem; }
|
||||||
|
meter { width: 5rem; height: 0.7rem; vertical-align: middle; margin-right: 0.4rem; }
|
||||||
|
.card-head {
|
||||||
|
display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.card-head h2 { margin: 0; }
|
||||||
|
.help-link {
|
||||||
|
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);
|
||||||
|
text-decoration: none; font-family: var(--font-mono); font-size: 0.75rem; font-weight: 600;
|
||||||
|
background: var(--surface-bg); cursor: pointer; margin: 0;
|
||||||
|
}
|
||||||
|
.help-link:hover { color: var(--accent-text); border-color: var(--accent-fill); }
|
||||||
|
|
||||||
|
.page-head {
|
||||||
|
display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 0.5rem 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.page-head h1 { margin: 0; }
|
||||||
|
|
||||||
|
/* Compact retry policy */
|
||||||
|
.retry-facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); gap: 0.5rem; margin-top: 0.7rem; }
|
||||||
|
|
||||||
|
.phone-list { display: none; list-style: none; margin: 0; padding: 0; }
|
||||||
|
.phone-list li { border-bottom: 1px solid var(--border); }
|
||||||
|
.phone-list a,
|
||||||
|
.phone-list .item {
|
||||||
|
display: grid; grid-template-columns: 1fr auto; gap: 0.15rem 0.7rem;
|
||||||
|
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 .meta { 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; }
|
||||||
|
|
||||||
|
/* Help drawer — CSS checkbox, no script required */
|
||||||
|
.help-drawer {
|
||||||
|
display: none; position: fixed; top: var(--gallery-h); right: 0; bottom: 0;
|
||||||
|
width: min(26rem, 100vw); z-index: 30;
|
||||||
|
background: var(--card-bg); border-left: 1px solid var(--border);
|
||||||
|
padding: 1.25rem 1.35rem 2rem; overflow-y: auto;
|
||||||
|
}
|
||||||
|
html:has(input[name="help"]:checked):not(:has(#help-off:checked)) .help-drawer,
|
||||||
|
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 {
|
||||||
|
display: none; position: fixed; inset: var(--gallery-h) 0 0 0; z-index: 25;
|
||||||
|
background: rgba(18, 22, 28, 0.28);
|
||||||
|
}
|
||||||
|
.help-drawer h2 { margin-top: 1.2rem; }
|
||||||
|
.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 {
|
||||||
|
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);
|
||||||
|
border: 1px solid var(--control-border); border-radius: 5px; cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Phone preview (gallery toggle) and real narrow windows */
|
||||||
|
.nav-burger { display: none; }
|
||||||
|
|
||||||
|
#vp-phone:checked ~ .app {
|
||||||
|
position: relative;
|
||||||
|
width: 390px; margin: 0.75rem auto 2rem; min-height: 760px;
|
||||||
|
border: 1px solid var(--border); border-radius: 6px; overflow: hidden;
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
#vp-phone:checked ~ .app .nav {
|
||||||
|
position: absolute; top: 0; left: 0; bottom: 0;
|
||||||
|
height: auto; max-height: none;
|
||||||
|
width: min(16rem, 82%); z-index: 20;
|
||||||
|
transform: translateX(-110%);
|
||||||
|
border-right: 1px solid var(--border); background: var(--card-bg);
|
||||||
|
}
|
||||||
|
#nav-open:checked ~ .app .nav { transform: translateX(0); }
|
||||||
|
#vp-phone:checked ~ .app .phone-bar {
|
||||||
|
display: flex; align-items: center; gap: 0.55rem;
|
||||||
|
margin: 0; padding: 0.65rem 0.85rem;
|
||||||
|
border-bottom: 1px solid var(--border); background: var(--card-bg);
|
||||||
|
}
|
||||||
|
#vp-phone:checked ~ .app .nav-burger {
|
||||||
|
display: inline-flex; align-items: center; justify-content: center;
|
||||||
|
width: 2.1rem; height: 2.1rem; margin: 0; padding: 0;
|
||||||
|
border: 1px solid var(--control-border); border-radius: 5px;
|
||||||
|
background: var(--surface-bg); cursor: pointer;
|
||||||
|
}
|
||||||
|
#vp-phone:checked ~ .app .phone-mark { width: 1.7rem; height: 1.7rem; }
|
||||||
|
#vp-phone:checked ~ .app .phone-bar .grow { flex: 1; font-weight: 600; }
|
||||||
|
#vp-phone:checked ~ .app main { padding: 1.5rem 0.9rem 2rem; }
|
||||||
|
#vp-phone:checked ~ .app .desk-only { display: none !important; }
|
||||||
|
#vp-phone:checked ~ .app .phone-only { display: block; }
|
||||||
|
#vp-phone:checked ~ .app .phone-list { display: block; }
|
||||||
|
#vp-phone:checked ~ .app .status-grid { display: flex; flex-direction: column; }
|
||||||
|
#vp-phone:checked ~ .app .status-grid .attn { order: -1; }
|
||||||
|
#vp-phone:checked ~ .app .pair { grid-template-columns: 1fr; }
|
||||||
|
#vp-phone:checked ~ .app .split,
|
||||||
|
#vp-phone:checked ~ .app .check-cols,
|
||||||
|
#vp-phone:checked ~ .app .field-pair:not(.host-type),
|
||||||
|
#vp-phone:checked ~ .app .facts,
|
||||||
|
#vp-phone:checked ~ .app .retry-facts {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
#vp-phone:checked ~ .app td.subject span { max-width: none; }
|
||||||
|
#vp-phone:checked ~ .help-drawer { width: 100vw; }
|
||||||
|
|
||||||
|
.phone-only { display: none; }
|
||||||
|
|
||||||
|
@media (max-width: 52rem) {
|
||||||
|
.app { min-height: calc(100vh - var(--gallery-h)); }
|
||||||
|
.nav {
|
||||||
|
position: fixed; top: var(--gallery-h); left: 0; bottom: 0;
|
||||||
|
z-index: 20; width: min(16rem, 84vw); max-height: none;
|
||||||
|
transform: translateX(-110%);
|
||||||
|
border-right: 1px solid var(--border); background: var(--card-bg);
|
||||||
|
}
|
||||||
|
#nav-open:checked ~ .app .nav { transform: translateX(0); }
|
||||||
|
.phone-bar {
|
||||||
|
display: flex; align-items: center; gap: 0.55rem;
|
||||||
|
margin: 0; padding: 0.65rem 0.85rem;
|
||||||
|
border-bottom: 1px solid var(--border); background: var(--card-bg);
|
||||||
|
}
|
||||||
|
.nav-burger {
|
||||||
|
display: inline-flex; align-items: center; justify-content: center;
|
||||||
|
width: 2.1rem; height: 2.1rem; margin: 0; padding: 0;
|
||||||
|
border: 1px solid var(--control-border); border-radius: 5px;
|
||||||
|
background: var(--surface-bg); cursor: pointer;
|
||||||
|
}
|
||||||
|
.phone-mark { width: 1.7rem; height: 1.7rem; }
|
||||||
|
.phone-bar .grow { flex: 1; font-weight: 600; }
|
||||||
|
main { padding: 1.5rem 0.9rem 2rem; }
|
||||||
|
.desk-only { display: none !important; }
|
||||||
|
.phone-only, .phone-list { display: block; }
|
||||||
|
.status-grid { display: flex; flex-direction: column; }
|
||||||
|
.status-grid .attn { order: -1; }
|
||||||
|
.split, .pair, #settings .split, #backup > .split,
|
||||||
|
.check-cols, .field-pair:not(.host-type), .facts, .retry-facts { grid-template-columns: 1fr; }
|
||||||
|
.help-drawer { width: 100vw; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Index page */
|
||||||
|
.wrap { max-width: 58rem; margin: 0 auto; padding: 2.5rem 1.25rem 5rem; }
|
||||||
|
.wrap > header { border-bottom: 2px solid var(--accent-fill); padding-bottom: 1.5rem; margin-bottom: 0.5rem; }
|
||||||
|
.eyebrow {
|
||||||
|
font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.14em;
|
||||||
|
text-transform: uppercase; color: var(--accent-text); margin: 0 0 0.7rem;
|
||||||
|
}
|
||||||
|
.wrap h1 { font-size: clamp(1.7rem, 4vw, 2.4rem); font-weight: 200; }
|
||||||
|
.wrap h1 b { font-weight: 600; }
|
||||||
|
.wrap > header p { margin: 0; max-width: 62ch; color: var(--muted); }
|
||||||
|
.wrap section { border-bottom: 1px solid var(--border); padding: 2.2rem 0; }
|
||||||
|
.wrap h2 {
|
||||||
|
font-size: 0.8rem; font-weight: 500; margin: 0 0 1.1rem;
|
||||||
|
letter-spacing: 0.1em; text-transform: uppercase; font-family: var(--font-mono); color: var(--muted);
|
||||||
|
}
|
||||||
|
.wrap h3 { font-size: 1.05rem; font-weight: 600; margin: 1.2rem 0 0.4rem; }
|
||||||
|
.note {
|
||||||
|
background: #f1eae4; border-left: 2px solid var(--accent-fill);
|
||||||
|
padding: 0.9rem 1rem; font-size: 0.92rem; line-height: 1.6; max-width: 64ch; margin-top: 1rem;
|
||||||
|
}
|
||||||
|
#theme-dark:checked ~ .doc .note { background: #2a1f1b; }
|
||||||
|
.compare {
|
||||||
|
display: grid; grid-template-columns: 1fr; gap: 0.75rem; margin-top: 0.8rem;
|
||||||
|
}
|
||||||
|
.compare .row {
|
||||||
|
font-family: var(--font-mono); font-size: 0.75rem; color: var(--muted);
|
||||||
|
}
|
||||||
|
.bar {
|
||||||
|
display: flex; height: 2.1rem; border: 1px solid var(--border); border-radius: 4px; overflow: hidden; background: var(--card-bg);
|
||||||
|
}
|
||||||
|
.bar i { display: flex; align-items: center; justify-content: center; font-style: normal; font-size: 0.7rem; }
|
||||||
|
.bar .navc { width: 14%; background: var(--surface-bg); color: var(--fg); border-right: 1px solid var(--border); }
|
||||||
|
.bar .col { background: var(--nav-active-bg); color: var(--accent-text); }
|
||||||
|
.bar .empty { flex: 1; background: var(--code-bg); color: var(--muted); }
|
||||||
|
.bar .fill { flex: 1; background: var(--st-ok-bg); color: var(--st-ok-fg); }
|
||||||
|
.ia { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
|
||||||
|
@media (max-width: 40rem) { .ia { grid-template-columns: 1fr; } }
|
||||||
|
.ia ul { margin: 0.3rem 0 0; padding-left: 1.1rem; }
|
||||||
|
.ia li { margin: 0.2rem 0; }
|
||||||
|
.screen-index { display: grid; grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); gap: 0.4rem 1.5rem; }
|
||||||
|
.screen-index a { display: block; padding: 0.25rem 0; }
|
||||||
|
.icons-row { display: flex; flex-wrap: wrap; gap: 1.2rem; align-items: end; margin-top: 0.8rem; }
|
||||||
|
.icon-card {
|
||||||
|
background: var(--card-bg); border: 1px solid var(--border); border-radius: 6px;
|
||||||
|
padding: 1rem 1.1rem 0.85rem; text-align: center; min-width: 7.5rem;
|
||||||
|
}
|
||||||
|
.icon-card svg { width: 1.5rem; height: 1.5rem; display: block; margin: 0 auto 0.45rem; }
|
||||||
|
.icon-card span { display: block; font-family: var(--font-mono); font-size: 0.7rem; color: var(--muted); }
|
||||||
|
.mark-row { display: flex; flex-wrap: wrap; gap: 1.5rem; align-items: end; }
|
||||||
|
.mark-row figure { margin: 0; }
|
||||||
|
.mark-row figcaption { font-family: var(--font-mono); font-size: 0.7rem; color: var(--muted); margin-top: 0.4rem; }
|
||||||
|
|
||||||
|
.cta {
|
||||||
|
display: inline-block; margin-top: 1rem; padding: 0.65rem 1.2rem;
|
||||||
|
background: var(--accent-fill); color: var(--on-accent); text-decoration: none;
|
||||||
|
font-weight: 600; border-radius: 5px;
|
||||||
|
}
|
||||||
|
.cta:hover { background: var(--accent-fill-hover); color: var(--on-accent); }
|
||||||
|
.cta-row { display: flex; flex-wrap: wrap; gap: 0.6rem; margin-top: 1rem; }
|
||||||
|
.cta.quiet {
|
||||||
|
background: var(--surface-bg); color: var(--accent-text); border: 1px solid var(--control-border);
|
||||||
|
}
|
||||||
|
.cta.quiet:hover { background: var(--surface-bg-hover); color: var(--accent-text); }
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Settings — SelfPost mockups</title>
|
||||||
|
<link rel="icon" href="../selfpost-icon-16.svg" type="image/svg+xml">
|
||||||
|
<link rel="stylesheet" href="mock.css">
|
||||||
|
<link rel="stylesheet" href="system.css">
|
||||||
|
</head>
|
||||||
|
<body class="page-settings" data-page="settings" data-nav="settings" data-title="Settings">
|
||||||
|
<main class="stack">
|
||||||
|
<h1>Settings</h1>
|
||||||
|
<form class="stack" action="#" onsubmit="return false">
|
||||||
|
<div class="pair">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Panel credentials</h2>
|
||||||
|
<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>Username</label><input value="admin" autocomplete="username">
|
||||||
|
<label>Current password</label><input type="password" autocomplete="current-password">
|
||||||
|
<label>New password</label><input type="password" autocomplete="new-password">
|
||||||
|
<label>Confirm new password</label><input type="password">
|
||||||
|
<div class="actions-row"><button type="button">Save changes</button></div>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<div class="card g-only">
|
||||||
|
<h2>DMARC aggregate reports</h2>
|
||||||
|
<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>
|
||||||
|
<label>Default report address</label>
|
||||||
|
<input type="email" value="dmarc@mail.example.org">
|
||||||
|
<p class="muted">When <code>rua=</code> points at another domain, that hub must publish a report-authorisation record. <a href="dmarc.html">DMARC reports</a> in the panel.</p>
|
||||||
|
<div class="field-row">
|
||||||
|
<div class="field">
|
||||||
|
<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">
|
||||||
|
<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>
|
||||||
|
</form>
|
||||||
|
<div class="fill g-only">
|
||||||
|
<div class="card" id="rate-limits">
|
||||||
|
<h2>Sending rate limits</h2>
|
||||||
|
<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="check-cols">
|
||||||
|
<div class="check-col">
|
||||||
|
<p class="check-col-title">Level 1 — per client IP</p>
|
||||||
|
<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>
|
||||||
|
</main>
|
||||||
|
<script src="shell.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Create administrator — SelfPost mockups</title>
|
||||||
|
<link rel="icon" href="../selfpost-icon-16.svg" type="image/svg+xml">
|
||||||
|
<link rel="stylesheet" href="mock.css">
|
||||||
|
<link rel="stylesheet" href="system.css">
|
||||||
|
</head>
|
||||||
|
<body class="page-setup" data-page="setup" data-nav="setup" data-title="Create administrator" data-auth="1">
|
||||||
|
<main class="auth">
|
||||||
|
<img class="mark" src="../selfpost-stamp.svg" width="330" height="150" alt="SelfPost">
|
||||||
|
<h1>Create administrator</h1>
|
||||||
|
<div class="card">
|
||||||
|
<p class="muted">This one-time link creates the single panel administrator. After you submit, the link stops working for good.</p>
|
||||||
|
<form action="status.html">
|
||||||
|
<label>Username</label>
|
||||||
|
<input autocomplete="username">
|
||||||
|
<label>Password</label>
|
||||||
|
<input type="password" autocomplete="new-password">
|
||||||
|
<label>Confirm password</label>
|
||||||
|
<input type="password" autocomplete="new-password">
|
||||||
|
<div class="actions-row"><button type="submit">Create administrator</button></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<script src="shell.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
/* Shared chrome for panel UI page mockups. Each screen is its own HTML file;
|
||||||
|
this script injects radios, gallery, nav, sprite, and the help drawer so
|
||||||
|
file:// viewing does not need a module fetch. */
|
||||||
|
(function () {
|
||||||
|
if (document.body.dataset.shell === "1") return;
|
||||||
|
document.body.dataset.shell = "1";
|
||||||
|
var main = document.querySelector("main");
|
||||||
|
if (!main) return;
|
||||||
|
|
||||||
|
var page = document.body.getAttribute("data-page") || "";
|
||||||
|
var navKey = document.body.getAttribute("data-nav") || page;
|
||||||
|
var title = document.body.getAttribute("data-title") || document.title;
|
||||||
|
var globalOnly = document.body.getAttribute("data-global-only") === "1";
|
||||||
|
|
||||||
|
document.body.insertAdjacentHTML("afterbegin",
|
||||||
|
'<input class="ctrl" type="radio" name="role" id="role-global" checked>' +
|
||||||
|
'<input class="ctrl" type="radio" name="role" id="role-domain">' +
|
||||||
|
'<input class="ctrl" type="radio" name="vp" id="vp-desktop" checked>' +
|
||||||
|
'<input class="ctrl" type="radio" name="vp" id="vp-phone">' +
|
||||||
|
'<input class="ctrl" type="radio" name="theme" id="theme-light" checked>' +
|
||||||
|
'<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="nav-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">'
|
||||||
|
);
|
||||||
|
|
||||||
|
var q = new URLSearchParams(location.search);
|
||||||
|
if (q.get("view") === "phone") document.getElementById("vp-phone").checked = true;
|
||||||
|
if (q.get("role") === "domain") document.getElementById("role-domain").checked = true;
|
||||||
|
if (q.get("theme") === "dark") document.getElementById("theme-dark").checked = true;
|
||||||
|
if (q.get("inbound") === "0") document.getElementById("feat-inbound").checked = false;
|
||||||
|
|
||||||
|
var gallery =
|
||||||
|
'<header class="gallery">' +
|
||||||
|
'<a class="brand-mini" href="index.html">Макеты</a>' +
|
||||||
|
'<div class="seg"><span>Роль</span>' +
|
||||||
|
'<label for="role-global">Global</label>' +
|
||||||
|
'<label for="role-domain">Domain-admin</label></div>' +
|
||||||
|
'<div class="seg"><span>Ширина</span>' +
|
||||||
|
'<label for="vp-desktop">Desktop</label>' +
|
||||||
|
'<label for="vp-phone">Phone</label></div>' +
|
||||||
|
'<div class="seg"><span>Тема</span>' +
|
||||||
|
'<label for="theme-light">Light</label>' +
|
||||||
|
'<label for="theme-dark">Dark</label></div>' +
|
||||||
|
'<label class="g-only" for="feat-inbound">Inbound</label>' +
|
||||||
|
'<a href="index.html">Оглавление</a>' +
|
||||||
|
'<a href="system.html">Система</a>' +
|
||||||
|
"</header>";
|
||||||
|
|
||||||
|
var sprite =
|
||||||
|
'<svg class="sprite" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">' +
|
||||||
|
'<symbol id="i-status" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M1.25 8.5h2.9L6.2 3.4l3.1 9.4 1.9-4.3h3.55"/></symbol>' +
|
||||||
|
'<symbol id="i-domains" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="6.25"/><path d="M1.9 8h12.2"/><path d="M8 1.75c1.85 1.8 2.8 4 2.8 6.25S9.85 12.45 8 14.25C6.15 12.45 5.2 10.25 5.2 8S6.15 3.55 8 1.75Z"/></symbol>' +
|
||||||
|
'<symbol id="i-deliveries" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14.25 1.75 1.6 6.6l5 2.05 2.05 5z"/><path d="M14.25 1.75 6.6 8.65"/></symbol>' +
|
||||||
|
'<symbol id="i-queue" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M1.75 9.5h3.3l1 1.75h3.9l1-1.75h3.3v3.05a1.2 1.2 0 0 1-1.2 1.2H2.95a1.2 1.2 0 0 1-1.2-1.2z"/><path d="M1.75 9.5 3.4 3.2a1.25 1.25 0 0 1 1.2-.95h6.8a1.25 1.25 0 0 1 1.2.95l1.65 6.3"/></symbol>' +
|
||||||
|
'<symbol id="i-log" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3.75 1.75h5.1l3.4 3.4v8.05a1.05 1.05 0 0 1-1.05 1.05H3.75a1.05 1.05 0 0 1-1.05-1.05V2.8a1.05 1.05 0 0 1 1.05-1.05Z"/><path d="M8.85 1.75v3.4h3.4"/><path d="M5.35 8.6h5.3M5.35 11.1h3.5"/></symbol>' +
|
||||||
|
'<symbol id="i-inbound" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 9.5h11"/><path d="M8 2.75v6.2"/><path d="M5.4 6.4 8 9.05 10.6 6.4"/><path d="M3.2 12.6h9.6"/></symbol>' +
|
||||||
|
'<symbol id="i-dmarc" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 1.85 2.75 3.7v4.2c0 3.15 2.15 5.2 5.25 6.25 3.1-1.05 5.25-3.1 5.25-6.25V3.7Z"/><path d="M5.4 8.05 7.15 9.8 10.7 6.2"/></symbol>' +
|
||||||
|
'<symbol id="i-backup" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2.75" y="1.75" width="10.5" height="12.5" rx="1.15"/><path d="M2.75 8h10.5"/><path d="M6.4 4.85h3.2M6.4 11.15h3.2"/></symbol>' +
|
||||||
|
'<symbol id="i-users" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10.9 3.1a2.1 2.1 0 0 1 0 4.2"/><path d="M14.7 14.25a3.8 3.8 0 0 0-3.9-3.65"/><circle cx="5.5" cy="5.2" r="2.5"/><path d="M1.4 14.25a4.8 4.8 0 0 1 8.2 0"/></symbol>' +
|
||||||
|
'<symbol id="i-help" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="6.25"/><path d="M8 7.2V11.4"/><path d="M8 5.05v.01"/></symbol>' +
|
||||||
|
'<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></symbol>' +
|
||||||
|
'<symbol id="i-account" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="5.4" r="2.75"/><path d="M2.9 14.25a5.1 5.1 0 0 1 10.2 0"/></symbol>' +
|
||||||
|
'<symbol id="i-out" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M6.1 14.25H3.65a1.15 1.15 0 0 1-1.15-1.15V2.9a1.15 1.15 0 0 1 1.15-1.15H6.1"/><path d="M10.6 11.15 13.75 8 10.6 4.85"/><path d="M13.75 8H6.35"/></symbol>' +
|
||||||
|
'<symbol id="i-menu" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M2.5 4h11M2.5 8h11M2.5 12h11"/></symbol>' +
|
||||||
|
"</svg>";
|
||||||
|
|
||||||
|
function icon(id) {
|
||||||
|
return '<svg class="icon"><use href="#' + id + '"/></svg>';
|
||||||
|
}
|
||||||
|
|
||||||
|
var navHtml =
|
||||||
|
'<nav class="nav">' +
|
||||||
|
'<a class="brand" href="status.html"><img src="../selfpost-stamp-compact.svg" width="220" height="100" alt="SelfPost"></a>' +
|
||||||
|
'<div class="links">' +
|
||||||
|
'<a class="n-status g-only" href="status.html">' + icon("i-status") + "Status</a>" +
|
||||||
|
'<a class="n-domains" href="domains.html">' + icon("i-domains") + "Domains</a>" +
|
||||||
|
'<a class="n-deliveries" href="deliveries.html">' + icon("i-deliveries") + "Deliveries</a>" +
|
||||||
|
'<a class="n-queue g-only" href="mail-queue.html">' + icon("i-queue") + "Mail queue</a>" +
|
||||||
|
'<a class="n-log g-only" href="system-log.html">' + icon("i-log") + "System log</a>" +
|
||||||
|
'<a class="n-inbound g-only in-only" href="inbound.html">' + icon("i-inbound") + "Inbound</a>" +
|
||||||
|
'<a class="n-dmarc g-only" href="dmarc.html">' + icon("i-dmarc") + 'DMARC <span class="tag future">1.x</span></a>' +
|
||||||
|
'<a class="n-backup g-only" href="backup.html">' + icon("i-backup") + "Backup</a>" +
|
||||||
|
'<a class="n-users g-only" href="users.html">' + icon("i-users") + "Users</a>" +
|
||||||
|
'<a class="n-help" href="help.html">' + icon("i-help") + 'Help <span class="tag future">1.x</span></a>' +
|
||||||
|
"</div>" +
|
||||||
|
'<div class="session">' +
|
||||||
|
'<span class="session-user muted">' + icon("i-account") + "User: admin</span>" +
|
||||||
|
'<a class="n-settings" href="settings.html">' + icon("i-settings") + "Settings</a>" +
|
||||||
|
'<a class="btn-ghost" href="login.html">' + icon("i-out") + "Sign out</a>" +
|
||||||
|
"</div>" +
|
||||||
|
"</nav>";
|
||||||
|
|
||||||
|
var phone =
|
||||||
|
'<div class="phone-bar">' +
|
||||||
|
'<label class="nav-burger" for="nav-open" title="Menu">' + icon("i-menu") + "</label>" +
|
||||||
|
'<img class="phone-mark" src="../selfpost-icon.svg" width="28" height="28" alt="">' +
|
||||||
|
'<span class="grow phone-title"></span>' +
|
||||||
|
'<label class="help-link" for="help-index" title="Help">?</label>' +
|
||||||
|
"</div>";
|
||||||
|
|
||||||
|
var help =
|
||||||
|
'<label class="help-scrim" for="help-off"></label>' +
|
||||||
|
'<aside class="help-drawer">' +
|
||||||
|
'<label class="help-close" for="help-off">Close</label>' +
|
||||||
|
'<article class="help-pane help-pane-index">' +
|
||||||
|
"<h2>Help</h2>" +
|
||||||
|
"<p>Short notes for the card you opened — not a second copy of the guide.</p>" +
|
||||||
|
'<p class="muted">Status</p>' +
|
||||||
|
'<ul class="toc"><li><label for="help-status">Status checks</label></li></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>";
|
||||||
|
|
||||||
|
var app = document.createElement("div");
|
||||||
|
app.className = "app";
|
||||||
|
app.innerHTML = navHtml + '<div class="stage">' + phone + "</div>";
|
||||||
|
main.parentNode.insertBefore(app, main);
|
||||||
|
app.insertAdjacentHTML("beforebegin", gallery + sprite);
|
||||||
|
app.querySelector(".stage").appendChild(main);
|
||||||
|
app.insertAdjacentHTML("afterend", help);
|
||||||
|
|
||||||
|
var cur = app.querySelector(".n-" + navKey);
|
||||||
|
if (cur) cur.setAttribute("aria-current", "page");
|
||||||
|
var grow = app.querySelector(".phone-title");
|
||||||
|
if (grow) grow.textContent = title;
|
||||||
|
|
||||||
|
var skip = { "index.html": 1, "system.html": 1, "app.html": 1 };
|
||||||
|
|
||||||
|
function qs() {
|
||||||
|
var p = new URLSearchParams();
|
||||||
|
if (document.getElementById("vp-phone").checked) p.set("view", "phone");
|
||||||
|
if (document.getElementById("role-domain").checked) p.set("role", "domain");
|
||||||
|
if (document.getElementById("theme-dark").checked) p.set("theme", "dark");
|
||||||
|
if (!document.getElementById("feat-inbound").checked) p.set("inbound", "0");
|
||||||
|
var s = p.toString();
|
||||||
|
return s ? "?" + s : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function withQuery(href) {
|
||||||
|
if (!href) return href;
|
||||||
|
if (href.charAt(0) === "#" || /^(https?:|mailto:|javascript:)/i.test(href)) return href;
|
||||||
|
var hash = "";
|
||||||
|
var path = href;
|
||||||
|
var hashAt = href.indexOf("#");
|
||||||
|
if (hashAt >= 0) {
|
||||||
|
hash = href.slice(hashAt);
|
||||||
|
path = href.slice(0, hashAt);
|
||||||
|
}
|
||||||
|
var qAt = path.indexOf("?");
|
||||||
|
var file = qAt >= 0 ? path.slice(0, qAt) : path;
|
||||||
|
var extra = qAt >= 0 ? path.slice(qAt + 1) : "";
|
||||||
|
var base = file.split("/").pop();
|
||||||
|
if (!base || !/\.html$/i.test(base) || skip[base]) return href;
|
||||||
|
var p = new URLSearchParams(extra);
|
||||||
|
var curQs = new URLSearchParams(qs().replace(/^\?/, ""));
|
||||||
|
["view", "role", "theme", "inbound"].forEach(function (k) {
|
||||||
|
if (curQs.has(k)) p.set(k, curQs.get(k));
|
||||||
|
else p.delete(k);
|
||||||
|
});
|
||||||
|
var s = p.toString();
|
||||||
|
return base + (s ? "?" + s : "") + hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rewriteLinks() {
|
||||||
|
document.querySelectorAll("a[href]").forEach(function (a) {
|
||||||
|
var raw = a.getAttribute("href");
|
||||||
|
if (!raw) return;
|
||||||
|
a.setAttribute("href", withQuery(raw));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function brandHref() {
|
||||||
|
var brand = document.querySelector(".nav .brand");
|
||||||
|
if (!brand) return;
|
||||||
|
brand.setAttribute("href", withQuery(
|
||||||
|
document.getElementById("role-domain").checked ? "domains.html" : "status.html"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
function gate() {
|
||||||
|
brandHref();
|
||||||
|
if (!document.getElementById("role-domain").checked) return;
|
||||||
|
if (globalOnly) location.replace(withQuery("domains.html"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncUrl() {
|
||||||
|
if (history.replaceState) {
|
||||||
|
history.replaceState(null, "", location.pathname.split("/").pop() + qs() + location.hash);
|
||||||
|
}
|
||||||
|
rewriteLinks();
|
||||||
|
brandHref();
|
||||||
|
gate();
|
||||||
|
}
|
||||||
|
|
||||||
|
["role-global", "role-domain", "vp-desktop", "vp-phone", "theme-light", "theme-dark", "feat-inbound"].forEach(function (id) {
|
||||||
|
var el = document.getElementById(id);
|
||||||
|
if (el) el.addEventListener("change", syncUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll(".nav a").forEach(function (a) {
|
||||||
|
a.addEventListener("click", function () {
|
||||||
|
var open = document.getElementById("nav-open");
|
||||||
|
if (open) open.checked = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
rewriteLinks();
|
||||||
|
gate();
|
||||||
|
})();
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Status — SelfPost mockups</title>
|
||||||
|
<link rel="icon" href="../selfpost-icon-16.svg" type="image/svg+xml">
|
||||||
|
<link rel="stylesheet" href="mock.css">
|
||||||
|
<link rel="stylesheet" href="system.css">
|
||||||
|
</head>
|
||||||
|
<body class="page-status" data-page="status" data-nav="status" data-title="Status" data-global-only="1">
|
||||||
|
<main class="stack">
|
||||||
|
<div class="page-head">
|
||||||
|
<h1>Status</h1>
|
||||||
|
<label class="help-link" for="help-status" title="What these checks mean">?</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="fill">
|
||||||
|
<div class="card" id="overall">
|
||||||
|
<h2>Overall <span class="st st-warn">warn</span></h2>
|
||||||
|
<p class="muted">Running, with warnings below.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pair">
|
||||||
|
<div class="card attn" id="queue">
|
||||||
|
<h2>Mail queue <span class="st st-warn">warn</span></h2>
|
||||||
|
<p>3 Kbytes in 3 Requests.</p>
|
||||||
|
<div class="actions-row"><a class="btn" href="mail-queue.html">View queue</a></div>
|
||||||
|
</div>
|
||||||
|
<div class="card" id="certificate">
|
||||||
|
<h2>TLS certificate <span class="st st-ok">ok</span></h2>
|
||||||
|
<label>Expires</label>
|
||||||
|
<span class="code">2026-11-02 12:00 UTC</span>
|
||||||
|
<p class="muted">Valid for another 78 day(s).</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pair">
|
||||||
|
<div class="card" id="sockets">
|
||||||
|
<h2>Milter sockets <span class="st st-ok">ok</span></h2>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Milter</th><th>State</th><th>Detail</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><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>
|
||||||
|
<span class="code">mail.example.org</span>
|
||||||
|
<label>Forward and reverse lookup</label>
|
||||||
|
<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>
|
||||||
|
<div class="actions-row"><button type="button">Re-check DNS</button></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="fill in-only" id="inbound-status">
|
||||||
|
<div class="card">
|
||||||
|
<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>
|
||||||
|
<p><a href="inbound.html">Inbound domains</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pair">
|
||||||
|
<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="fill">
|
||||||
|
<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>
|
||||||
|
<div class="actions-row"><button type="button">Reload configuration</button></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="version">SelfPost 1.2.3 · © Mixeme · <a href="#">License (AGPL-3.0)</a></p>
|
||||||
|
</main>
|
||||||
|
<script src="shell.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>System log — SelfPost mockups</title>
|
||||||
|
<link rel="icon" href="../selfpost-icon-16.svg" type="image/svg+xml">
|
||||||
|
<link rel="stylesheet" href="mock.css">
|
||||||
|
<link rel="stylesheet" href="system.css">
|
||||||
|
</head>
|
||||||
|
<body class="page-system-log" data-page="system-log" data-nav="log" data-title="System log" data-global-only="1">
|
||||||
|
<main class="stack">
|
||||||
|
<h1>System log</h1>
|
||||||
|
<div class="fill">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Recent log entries</h2>
|
||||||
|
<span class="code">Aug 15 20:14:08 mail postfix/smtp[230]: 4C3A1E2F1A: to=<ada@example.net>, relay=mx.example.net[198.51.100.20]:25, delay=6, delays=0.2/0.1/0.4/5.3, dsn=4.2.1, status=deferred (450 4.2.1 mailbox busy)
|
||||||
|
Aug 15 20:14:02 mail postfix/qmgr[119]: 4C3A1E2F1A: from=<billing@example.com>, size=12288, nrcpt=1 (queue active)
|
||||||
|
Aug 15 20:14:02 mail postfix/smtpd[221]: 4C3A1E2F1A: client=203.0.113.40[203.0.113.40], sasl_method=PLAIN, sasl_username=billing
|
||||||
|
Aug 15 20:11:40 mail postfix/smtp[228]: 4B19D0AA01: to=<list-bounces@example.net>, relay=mx.example.net[198.51.100.20]:25, delay=0.9, status=sent (250 2.0.0 Ok)
|
||||||
|
Aug 15 20:02:11 mail postfix/smtp[226]: 4B19C0BB12: to=<noreply@blocked.example>, status=bounced (host mx.blocked.example[203.0.113.99] said: 550 5.7.1 rejected)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<script src="shell.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
/* Panel UI system primitives.
|
||||||
|
Source of truth: system.html. Screens are separate HTML files composed from
|
||||||
|
these classes. Do not “fix” empty columns, Host/Type height, or split
|
||||||
|
actions by one-off rules — compose with these instead. */
|
||||||
|
|
||||||
|
.stack {
|
||||||
|
display: flex; flex-direction: column; gap: 1rem;
|
||||||
|
width: 100%; min-width: 0;
|
||||||
|
}
|
||||||
|
.stack > .card,
|
||||||
|
.stack > .pair,
|
||||||
|
.stack > .measure,
|
||||||
|
.stack > .fill { margin-top: 0; }
|
||||||
|
.stack > .card + .card { margin-top: 0; }
|
||||||
|
|
||||||
|
.measure { width: 100%; max-width: var(--form-max); min-width: 0; }
|
||||||
|
|
||||||
|
.fill { width: 100%; max-width: var(--ops-max); min-width: 0; }
|
||||||
|
.fill > .card { margin-top: 0; }
|
||||||
|
.fill .card:has(table) { overflow-x: auto; }
|
||||||
|
.card > .measure { margin-bottom: 1rem; }
|
||||||
|
|
||||||
|
/* Two peer jobs. One child → reading measure (domain-admin Settings).
|
||||||
|
Never a full-width lonely card on an ops page. */
|
||||||
|
.pair {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||||
|
gap: 1rem;
|
||||||
|
width: 100%; max-width: var(--ops-max); min-width: 0;
|
||||||
|
}
|
||||||
|
.pair > * { min-width: 0; }
|
||||||
|
.card > .stack { min-width: 0; }
|
||||||
|
.pair > .card { margin-top: 0; }
|
||||||
|
.pair > .card + .card { margin-top: 0; }
|
||||||
|
.pair:has(> :only-child) {
|
||||||
|
display: block;
|
||||||
|
max-width: var(--form-max);
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-row {
|
||||||
|
display: flex; flex-wrap: wrap; gap: 0.6rem; align-items: center;
|
||||||
|
margin-top: 1.1rem;
|
||||||
|
}
|
||||||
|
.actions-row > button,
|
||||||
|
.actions-row > a.btn,
|
||||||
|
.actions-row > a.danger { margin-top: 0; }
|
||||||
|
|
||||||
|
/* Two labelled controls on one row. Labels share row 1, values share row 2
|
||||||
|
so they are the same height by construction — not by matching padding. */
|
||||||
|
.field-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
grid-template-rows: auto minmax(2.5rem, auto);
|
||||||
|
column-gap: 1rem;
|
||||||
|
align-items: stretch;
|
||||||
|
margin-top: 0.45rem;
|
||||||
|
}
|
||||||
|
.field-row.equal { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
|
||||||
|
.field-row > .field { display: contents; }
|
||||||
|
.field-row > .field:first-child > * { grid-column: 1; }
|
||||||
|
.field-row > .field:last-child > * { grid-column: 2; }
|
||||||
|
.field-row > .field > label { grid-row: 1; margin-top: 0.45rem; }
|
||||||
|
.field-row > .field > :not(label) {
|
||||||
|
grid-row: 2; align-self: stretch; min-width: 0;
|
||||||
|
margin-top: 0.3rem; box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.field-row > .field > .code,
|
||||||
|
.field-row > .field > .code-row {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.field-row > .field > .code {
|
||||||
|
display: flex; align-items: center;
|
||||||
|
padding-top: 0.45rem; padding-bottom: 0.45rem;
|
||||||
|
}
|
||||||
|
.field-row > .field:last-child > .code {
|
||||||
|
width: fit-content; min-width: 2.75rem;
|
||||||
|
justify-content: center; text-align: center;
|
||||||
|
}
|
||||||
|
.field-row .code-row {
|
||||||
|
display: flex; align-items: stretch; gap: 0.5rem; height: 100%;
|
||||||
|
}
|
||||||
|
.field-row .code-row .code {
|
||||||
|
flex: 1; min-width: 0; margin-top: 0; height: auto;
|
||||||
|
display: flex; align-items: center;
|
||||||
|
padding-top: 0.45rem; padding-bottom: 0.45rem;
|
||||||
|
}
|
||||||
|
.field-row .code-row .copy { margin-top: 0; align-self: stretch; }
|
||||||
|
|
||||||
|
.card-head {
|
||||||
|
display: flex; align-items: center; justify-content: space-between; gap: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.card-head h2 { margin: 0; }
|
||||||
|
|
||||||
|
@media (max-width: 52rem) {
|
||||||
|
.pair { grid-template-columns: 1fr; }
|
||||||
|
/* field-row stays two columns: Type is a token, not a second form. */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Specimens on the system page */
|
||||||
|
.sys-wrap { max-width: 70rem; }
|
||||||
|
.sys-wrap > header p,
|
||||||
|
.sys-wrap section > p,
|
||||||
|
.sys-wrap li { max-width: 68ch; }
|
||||||
|
.sys-toc {
|
||||||
|
display: flex; flex-wrap: wrap; gap: 0.35rem 1.1rem;
|
||||||
|
margin: 1rem 0 0; padding: 0; list-style: none;
|
||||||
|
}
|
||||||
|
.sys-toc a { font-size: 0.92rem; }
|
||||||
|
.specimen {
|
||||||
|
margin: 1rem 0 0; padding: 0.9rem 1rem 1.1rem;
|
||||||
|
border: 1px dashed var(--border); border-radius: 6px; background: var(--code-bg);
|
||||||
|
}
|
||||||
|
.specimen > figcaption {
|
||||||
|
font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase; color: var(--muted); margin: 0 0 0.7rem;
|
||||||
|
}
|
||||||
|
.specimen.bad {
|
||||||
|
border-color: var(--danger-border); background: var(--danger-bg);
|
||||||
|
}
|
||||||
|
.specimen.bad > figcaption { color: var(--danger-fg); }
|
||||||
|
.recipes {
|
||||||
|
width: 100%; margin-top: 0.8rem; font-size: 0.92rem;
|
||||||
|
}
|
||||||
|
.recipes th, .recipes td { white-space: normal; vertical-align: top; }
|
||||||
|
.recipes code { font-size: 0.82rem; }
|
||||||
|
.forbid { margin: 0.4rem 0 0; padding-left: 1.1rem; }
|
||||||
|
.forbid li { margin: 0.35rem 0; }
|
||||||
@@ -0,0 +1,298 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>SelfPost — система панели</title>
|
||||||
|
<link rel="icon" href="../selfpost-icon-16.svg" type="image/svg+xml">
|
||||||
|
<link rel="stylesheet" href="mock.css">
|
||||||
|
<link rel="stylesheet" href="system.css">
|
||||||
|
</head>
|
||||||
|
<body class="doc">
|
||||||
|
<div class="wrap sys-wrap">
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<p class="eyebrow">система · не рескин</p>
|
||||||
|
<h1>Self<b>Post</b> — грамматика интерфейса</h1>
|
||||||
|
<p>Это проект системы. Экраны складываются из регионов и контролов. Запрещённые состояния нельзя «починить паддингом» — их не из чего собрать. Знак, кирпич и IBM Plex не меняются. Живая панель (<code>internal/web</code>) пока не трогается.</p>
|
||||||
|
<div class="cta-row">
|
||||||
|
<a class="cta" href="#regions">Регионы</a>
|
||||||
|
<a class="cta quiet" href="#controls">Контролы</a>
|
||||||
|
<a class="cta quiet" href="#recipes">Экраны</a>
|
||||||
|
<a class="cta quiet" href="status.html">Макеты экранов</a>
|
||||||
|
</div>
|
||||||
|
<ul class="sys-toc">
|
||||||
|
<li><a href="#why">Зачем</a></li>
|
||||||
|
<li><a href="#regions">Регионы</a></li>
|
||||||
|
<li><a href="#card">Карточка</a></li>
|
||||||
|
<li><a href="#controls">Контролы</a></li>
|
||||||
|
<li><a href="#actions">Действия</a></li>
|
||||||
|
<li><a href="#forbid">Нельзя</a></li>
|
||||||
|
<li><a href="#recipes">Рецепты экранов</a></li>
|
||||||
|
<li><a href="#phone">Телефон</a></li>
|
||||||
|
<li><a href="#apply">Как внедрять</a></li>
|
||||||
|
</ul>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section id="why">
|
||||||
|
<h2>Зачем</h2>
|
||||||
|
<p>Первый макет скопировал грамматику живой панели: страница помечается <code>ops</code> или <code>form</code>, карточки стопкой, пара полей — два независимых <code>.code</code>, Save внутри <code><form></code>, Delete снаружи. Дальше каждая мелочь чинилась отдельно — как в <code>panel.css</code> годами. Новый интерфейс должен <strong>не давать собрать</strong> пустую колонку, разные высоты Host/Type и кнопки на двух строках.</p>
|
||||||
|
<p>Три оси, которые больше не выбираются «на глаз» у каждой страницы:</p>
|
||||||
|
<ol>
|
||||||
|
<li><strong>Регион</strong> — как блок занимает ширину окна.</li>
|
||||||
|
<li><strong>Карточка</strong> — заголовок, справка, тело, действия.</li>
|
||||||
|
<li><strong>Контрол</strong> — одно поле или пара полей одной высоты.</li>
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="regions">
|
||||||
|
<h2>Регионы</h2>
|
||||||
|
<p>Страница — это <code>stack</code> регионов, не класс на <code><main></code>. Оболочка одна: навбар прижат влево, контент забирает остаток. Навбар не прыгает, когда меняется содержимое.</p>
|
||||||
|
<table class="recipes">
|
||||||
|
<thead><tr><th>Регион</th><th>Ширина</th><th>Когда</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>measure</code></td>
|
||||||
|
<td>до 42rem, влево</td>
|
||||||
|
<td>Одна читаемая форма: login, setup, подтверждение удаления, форма пользователя. Не «вся страница Settings».</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>pair</code></td>
|
||||||
|
<td>две колонки до 90rem</td>
|
||||||
|
<td>Две равноправные задачи на одном экране. Один ребёнок — сам сжимается в <code>measure</code> (domain-admin без DMARC).</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>fill</code></td>
|
||||||
|
<td>до 90rem</td>
|
||||||
|
<td>Таблица, лог, DNS-статус, список приложений. Ячейки не переносят однострочные значения; карточка скроллится по X.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>stack</code></td>
|
||||||
|
<td>колонка с зазором</td>
|
||||||
|
<td>Вертикальный порядок регионов. Зазор даёт <code>gap</code>, не <code>.card + .card</code>.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<figure class="specimen bad">
|
||||||
|
<figcaption>Нельзя — страница-форма, две задачи стопкой</figcaption>
|
||||||
|
<div class="measure">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Full backup</h2>
|
||||||
|
<p class="muted">Карточка узкая. Справа пустое поле на 1600px.</p>
|
||||||
|
<button type="button">Download</button>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>Import a domain</h2>
|
||||||
|
<p class="muted">Вторая задача под первой — та же пустота.</p>
|
||||||
|
<button type="button">Import</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<figure class="specimen">
|
||||||
|
<figcaption>Надо — pair</figcaption>
|
||||||
|
<div class="pair">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Full backup</h2>
|
||||||
|
<p class="muted">Секрет. Шифрование — внутри карточки, поля не растягиваются на 1440px.</p>
|
||||||
|
<div class="actions-row"><button type="button">Download full backup</button></div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>Import a domain</h2>
|
||||||
|
<p class="muted">Тот же экран, вторая задача. Не «ещё одна форма ниже».</p>
|
||||||
|
<div class="actions-row"><button type="button">Import domain</button></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</figure>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="card">
|
||||||
|
<h2>Карточка</h2>
|
||||||
|
<p>Один хром: <code>card-head</code> (заголовок + слот «?»), тело, при необходимости <code>actions-row</code>. Справка — часть хрома, её не расставляют после того, как карточки уже собраны. Нет «?» — слот пустой, заголовки соседних карточек всё равно на одной линии.</p>
|
||||||
|
<p>Показания (очередь, PTR, CPU) остаются на карточке. В drawer уходит только то, чего на карточке быть не должно: что такое kernel counter, зачем PTR у провайдера, почему пароль показывают один раз.</p>
|
||||||
|
<figure class="specimen">
|
||||||
|
<figcaption>Хром карточки</figcaption>
|
||||||
|
<div class="pair">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-head">
|
||||||
|
<h2>DNS status <span class="st st-ok">ok</span></h2>
|
||||||
|
<span class="help-link" title="What these checks mean">?</span>
|
||||||
|
</div>
|
||||||
|
<p class="muted">Чтение и Re-check здесь. Абзац «зачем MX» — в справке.</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-head">
|
||||||
|
<h2>Danger zone</h2>
|
||||||
|
</div>
|
||||||
|
<p class="muted">Без «?»: действие очевидное. Карточка всё равно пара к форме, не на всю ширину.</p>
|
||||||
|
<div class="actions-row"><a class="danger" href="#forbid">Delete inbound domain</a></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</figure>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="controls">
|
||||||
|
<h2>Контролы</h2>
|
||||||
|
<p>Одно поле — <code>field</code> (подпись + контроль). Два поля в ряд — <code>field-row</code>: подписи в первой сетке-строке, значения во второй. Высота значений общая, потому что это одна строка грида, а не два блока с подобранным padding. Баг «Type ниже Host» (<code>b0ebe06</code>) из этой разметки не собирается.</p>
|
||||||
|
<p><code>field-row</code> с узкой второй колонкой — Host / name ‖ Type. <code>field-row equal</code> — два равноправных инпута (лимит и окно).</p>
|
||||||
|
|
||||||
|
<figure class="specimen">
|
||||||
|
<figcaption>field-row — Host ‖ Type, с Copy и без</figcaption>
|
||||||
|
<div class="pair">
|
||||||
|
<div class="card">
|
||||||
|
<h2>With Copy</h2>
|
||||||
|
<div class="field-row">
|
||||||
|
<div class="field">
|
||||||
|
<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">
|
||||||
|
<label>Type</label>
|
||||||
|
<span class="code">TXT</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>Lookup, no Copy</h2>
|
||||||
|
<div class="field-row">
|
||||||
|
<div class="field">
|
||||||
|
<label>Host / name</label>
|
||||||
|
<span class="code">lists.example.com</span>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Type</label>
|
||||||
|
<span class="code">MX</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<figure class="specimen">
|
||||||
|
<figcaption>field-row equal — лимит ‖ окно</figcaption>
|
||||||
|
<div class="measure">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Level-2 rate limit</h2>
|
||||||
|
<div class="field-row equal">
|
||||||
|
<div class="field">
|
||||||
|
<label>Message limit</label>
|
||||||
|
<input value="40">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Window (seconds)</label>
|
||||||
|
<input value="60">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</figure>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="actions">
|
||||||
|
<h2>Действия</h2>
|
||||||
|
<p>Primary, secondary и danger — всегда <code>actions-row</code>. Для вёрстки неважно, POST это или переход на confirm: ряд один. Форма либо оборачивает всю карточку, либо кнопки несут <code>form=</code>. Нельзя оставить Submit внутри блочной формы, а Delete — следующим соседом: блок формы занимает строку целиком.</p>
|
||||||
|
<figure class="specimen">
|
||||||
|
<figcaption>Save и Delete в одном ряду</figcaption>
|
||||||
|
<div class="measure">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Edit user</h2>
|
||||||
|
<label>Username</label>
|
||||||
|
<input value="ops-alerts">
|
||||||
|
<div class="actions-row">
|
||||||
|
<button type="button">Save</button>
|
||||||
|
<a class="danger" href="#forbid">Delete user</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</figure>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="forbid">
|
||||||
|
<h2>Нельзя</h2>
|
||||||
|
<p>Если хочется добавить правило «только на этой странице» — сначала проверить, какого региона не хватило.</p>
|
||||||
|
<ul class="forbid">
|
||||||
|
<li>Класс на <code>main</code> (<code>ops</code> / <code>form</code>) как способ выбрать ширину. Ширину выбирает регион.</li>
|
||||||
|
<li>Одиночная карточка на 90rem с двумя полями ввода. Это <code>measure</code> или <code>pair</code>.</li>
|
||||||
|
<li>Danger zone отдельным <code>fill</code>. Она вторая колонка последней пары (получатели ‖ удалить, экспорт ‖ удалить).</li>
|
||||||
|
<li>Два <code>.code</code> рядом с разным padding, чтобы «почти совпало». Только <code>field-row</code>.</li>
|
||||||
|
<li>Submit внутри <code><form></code>, danger-ссылка после <code></form></code>.</li>
|
||||||
|
<li>Перенос однострочного поля, адреса, статуса, hostname. <code>nowrap</code> + горизонтальный скролл карточки.</li>
|
||||||
|
<li>Status из одних бейджей без Detail / без строки очереди / без PTR.</li>
|
||||||
|
<li>Телефон как уменьшенный десктоп с шестиколоночной таблицей. Таблица → список; <code>pair</code> → одна колонка; <code>field-row</code> остаётся парой.</li>
|
||||||
|
<li>Копировать разметку <code>internal/web/view/templates</code> «как есть» в макет. Рецепт экрана — ниже, не шаблон Go.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="recipes">
|
||||||
|
<h2>Рецепты экранов</h2>
|
||||||
|
<p>Экраны — отдельные HTML-файлы рядом с этой спецификацией. Собираются только так:</p>
|
||||||
|
<table class="recipes">
|
||||||
|
<thead><tr><th>Экран</th><th>Стек регионов</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><a href="login.html">Login</a> / <a href="setup.html">setup</a></td>
|
||||||
|
<td>центр оболочки, <code>measure</code> 24rem, полный stamp</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><a href="status.html">Status</a></td>
|
||||||
|
<td><code>stack</code>: overall → <code>pair</code> очередь‖TLS → <code>pair</code> milters‖PTR → inbound если есть → <code>pair</code> machine‖processes → configuration. На карточках остаются числа и Detail.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><a href="domains.html">Domains</a> / <a href="inbound.html">Inbound</a> / <a href="deliveries.html">Deliveries</a> / <a href="system-log.html">log</a> / <a href="mail-queue.html">queue</a></td>
|
||||||
|
<td><code>fill</code> таблица. Добавление домена — поле в той же карточке списка (<code>measure</code> внутри, не вторая карточка и не инпут на 90rem).</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><a href="domain.html">Domain</a></td>
|
||||||
|
<td><code>fill</code> DNS status → <code>pair</code> DKIM/SPF ‖ DMARC → <code>pair</code> connection ‖ add app → <code>fill</code> applications → <code>pair</code> domain settings (две половины) → <code>pair</code> export ‖ danger. «?» на рабочих карточках.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><a href="inbound-domain.html">Inbound domain</a></td>
|
||||||
|
<td><code>fill</code> MX DNS → <code>pair</code> upstream ‖ MX to publish → <code>pair</code> recipients ‖ danger</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><a href="dmarc.html">DMARC</a></td>
|
||||||
|
<td>hub: <code>pair</code> ingest ‖ this week, затем <code>fill</code> список отчётов. Домен: <a href="dmarc-domain.html">roll-up</a> (<code>pair</code> 7 days ‖ third-party, <code>fill</code> reports + sources). Один XML: <a href="dmarc-report.html">просмотр</a> — <code>pair</code> report ‖ policy, <code>fill</code> records. Не дашборд, не <code>ruf=</code>.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><a href="backup.html">Backup</a></td>
|
||||||
|
<td><code>pair</code> full backup ‖ import</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><a href="settings.html">Settings</a></td>
|
||||||
|
<td>global: <code>pair</code> credentials ‖ DMARC, затем <code>fill</code> rate limits. Domain-admin: один ребёнок в <code>pair</code> → сам <code>measure</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><a href="users.html">Users</a></td>
|
||||||
|
<td><code>fill</code> таблица. <a href="user-form.html">Create/Edit</a> — <code>measure</code> + <code>actions-row</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><a href="domain-delete.html">Confirm delete</a></td>
|
||||||
|
<td><code>measure</code> одна карточка</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="phone">
|
||||||
|
<h2>Телефон</h2>
|
||||||
|
<p>Аварийный доступ, не продукт. <code>pair</code> складывается в одну колонку. Таблица заменяется списком (как сейчас в прототипе). <code>field-row</code> не складывается: Type — токен. Навбар — выезжающая колонка на checkbox, без обязательного JS. Знак в шапке — SP-иконка, не второй wordmark.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="apply" style="border-bottom:none">
|
||||||
|
<h2>Как внедрять</h2>
|
||||||
|
<ol>
|
||||||
|
<li>Новый экран или правка макета — только классы из <code>system.css</code> (<code>stack</code>, <code>pair</code>, <code>measure</code>, <code>fill</code>, <code>field-row</code>, <code>actions-row</code>).</li>
|
||||||
|
<li>Не добавлять исключения в <code>mock.css</code> «для этой страницы», если это ширина, высота пары полей или ряд кнопок.</li>
|
||||||
|
<li>Click-through — отдельные страницы (<code>status.html</code>, <code>domain.html</code>, …), не простыня с якорями. Оболочка общая: <code>shell.js</code>.</li>
|
||||||
|
<li>Вёрстка <code>internal/web</code> — отдельная задача после утверждения системы, не параллельный рескин шаблонов.</li>
|
||||||
|
</ol>
|
||||||
|
<p class="muted">Классы живут в <code>docs/assets/panel-ui/system.css</code>. Этот файл — спецификация. Экраны — <a href="status.html">status.html</a> и соседние страницы; оглавление — <a href="index.html">index.html</a>.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Delete ops-alerts — SelfPost mockups</title>
|
||||||
|
<link rel="icon" href="../selfpost-icon-16.svg" type="image/svg+xml">
|
||||||
|
<link rel="stylesheet" href="mock.css">
|
||||||
|
<link rel="stylesheet" href="system.css">
|
||||||
|
</head>
|
||||||
|
<body class="page-user-delete" data-page="user-delete" data-nav="users" data-title="Delete user" data-global-only="1">
|
||||||
|
<main class="stack">
|
||||||
|
<div>
|
||||||
|
<h1>Delete ops-alerts</h1>
|
||||||
|
<a class="back" href="user-form.html">← Back to ops-alerts</a>
|
||||||
|
</div>
|
||||||
|
<div class="measure">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Confirm deletion</h2>
|
||||||
|
<p>You are about to delete the panel user <strong>ops-alerts</strong>. A signed-in session for this user stops working immediately.</p>
|
||||||
|
<div class="actions-row"><button type="button" class="danger">Delete ops-alerts</button></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<script src="shell.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Edit user — SelfPost mockups</title>
|
||||||
|
<link rel="icon" href="../selfpost-icon-16.svg" type="image/svg+xml">
|
||||||
|
<link rel="stylesheet" href="mock.css">
|
||||||
|
<link rel="stylesheet" href="system.css">
|
||||||
|
</head>
|
||||||
|
<body class="page-user-form" data-page="user-form" data-nav="users" data-title="Edit user" data-global-only="1">
|
||||||
|
<main class="stack">
|
||||||
|
<div>
|
||||||
|
<h1>Edit user</h1>
|
||||||
|
<a class="back" href="users.html">← Back to users</a>
|
||||||
|
</div>
|
||||||
|
<div class="measure">
|
||||||
|
<div class="card">
|
||||||
|
<form action="#" onsubmit="return false">
|
||||||
|
<label>Username</label>
|
||||||
|
<input value="ops-alerts">
|
||||||
|
<label>Password (leave empty to keep)</label>
|
||||||
|
<input type="password">
|
||||||
|
<label>Role</label>
|
||||||
|
<select><option selected>Domain administrator</option><option>Global administrator</option></select>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Assigned domains</legend>
|
||||||
|
<p class="muted">Required for domain administrators.</p>
|
||||||
|
<label class="check"><input type="checkbox"> example.com</label>
|
||||||
|
<label class="check"><input type="checkbox" checked> alerts.example.com</label>
|
||||||
|
</fieldset>
|
||||||
|
<div class="actions-row">
|
||||||
|
<button type="button">Save</button>
|
||||||
|
<a class="danger" href="user-delete.html">Delete user</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<script src="shell.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Users — SelfPost mockups</title>
|
||||||
|
<link rel="icon" href="../selfpost-icon-16.svg" type="image/svg+xml">
|
||||||
|
<link rel="stylesheet" href="mock.css">
|
||||||
|
<link rel="stylesheet" href="system.css">
|
||||||
|
</head>
|
||||||
|
<body class="page-users" data-page="users" data-nav="users" data-title="Users" data-global-only="1">
|
||||||
|
<main class="stack">
|
||||||
|
<h1>Users</h1>
|
||||||
|
<div class="fill">
|
||||||
|
<div class="card">
|
||||||
|
<p><a href="user-form.html">Create user</a></p>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Username</th><th>Role</th><th>Domains</th><th></th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>admin</td><td>Global</td><td class="muted">All</td><td class="actions"><a href="user-form.html">Edit</a></td></tr>
|
||||||
|
<tr><td>ops-alerts</td><td>Domain admin</td><td class="muted">alerts.example.com</td><td class="actions"><a href="user-form.html">Edit</a></td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<script src="shell.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user