From 6099716bf49d48ff5c958d320c8a49b5e0cf2f24 Mon Sep 17 00:00:00 2001 From: Mix Date: Fri, 16 Oct 2026 00:09:53 +0300 Subject: [PATCH] design: remove the 1.x panel-ui mockups and the redesign's stage-1 directions docs/assets/panel-ui (the 1.x experiment, superseded by the redesign) and docs/assets/panel-redesign/{a,b,c} (the three stage-1 directions; C was taken) are history - the plan says so - and nothing builds, checks or links them but the page that compared the directions. That page, docs/assets/panel-redesign/index.html, which the documents link as the way into the mockups, is now that: the screen map and what the design holds, with a line saying where the removed folders are (git history). build.py and check.py: 32 screens, 0 violations. Co-Authored-By: Claude Opus 5.5 --- docs/assets/panel-redesign/a/domain.html | 164 ----- docs/assets/panel-redesign/a/login.html | 37 - docs/assets/panel-redesign/a/status.html | 136 ---- docs/assets/panel-redesign/a/theme.css | 206 ------ docs/assets/panel-redesign/b/domain.html | 212 ------ docs/assets/panel-redesign/b/login.html | 36 - docs/assets/panel-redesign/b/status.html | 168 ----- docs/assets/panel-redesign/b/theme.css | 80 --- docs/assets/panel-redesign/c/domain.html | 200 ------ docs/assets/panel-redesign/c/login.html | 38 -- docs/assets/panel-redesign/c/status.html | 117 ---- docs/assets/panel-redesign/c/theme.css | 98 --- docs/assets/panel-redesign/index.html | 93 +-- docs/assets/panel-ui/app.html | 44 -- docs/assets/panel-ui/backup.html | 43 -- docs/assets/panel-ui/deliveries.html | 91 --- docs/assets/panel-ui/delivery.html | 77 --- docs/assets/panel-ui/dmarc-domain.html | 88 --- docs/assets/panel-ui/dmarc-report-fail.html | 88 --- docs/assets/panel-ui/dmarc-report.html | 88 --- docs/assets/panel-ui/dmarc.html | 91 --- docs/assets/panel-ui/domain-delete.html | 33 - docs/assets/panel-ui/domain.html | 260 ------- docs/assets/panel-ui/domains.html | 53 -- docs/assets/panel-ui/help.html | 46 -- docs/assets/panel-ui/inbound-backup.html | 92 --- docs/assets/panel-ui/inbound-delete.html | 33 - docs/assets/panel-ui/inbound-domain.html | 94 --- docs/assets/panel-ui/inbound.html | 58 -- docs/assets/panel-ui/index.html | 188 ----- docs/assets/panel-ui/login.html | 28 - docs/assets/panel-ui/mail-queue.html | 54 -- docs/assets/panel-ui/mock.css | 715 -------------------- docs/assets/panel-ui/settings.html | 73 -- docs/assets/panel-ui/setup.html | 30 - docs/assets/panel-ui/shell.js | 276 -------- docs/assets/panel-ui/status.html | 125 ---- docs/assets/panel-ui/system-log.html | 27 - docs/assets/panel-ui/system.css | 128 ---- docs/assets/panel-ui/system.html | 298 -------- docs/assets/panel-ui/user-delete.html | 27 - docs/assets/panel-ui/user-form.html | 42 -- docs/assets/panel-ui/users.html | 29 - 43 files changed, 11 insertions(+), 4893 deletions(-) delete mode 100644 docs/assets/panel-redesign/a/domain.html delete mode 100644 docs/assets/panel-redesign/a/login.html delete mode 100644 docs/assets/panel-redesign/a/status.html delete mode 100644 docs/assets/panel-redesign/a/theme.css delete mode 100644 docs/assets/panel-redesign/b/domain.html delete mode 100644 docs/assets/panel-redesign/b/login.html delete mode 100644 docs/assets/panel-redesign/b/status.html delete mode 100644 docs/assets/panel-redesign/b/theme.css delete mode 100644 docs/assets/panel-redesign/c/domain.html delete mode 100644 docs/assets/panel-redesign/c/login.html delete mode 100644 docs/assets/panel-redesign/c/status.html delete mode 100644 docs/assets/panel-redesign/c/theme.css delete mode 100644 docs/assets/panel-ui/app.html delete mode 100644 docs/assets/panel-ui/backup.html delete mode 100644 docs/assets/panel-ui/deliveries.html delete mode 100644 docs/assets/panel-ui/delivery.html delete mode 100644 docs/assets/panel-ui/dmarc-domain.html delete mode 100644 docs/assets/panel-ui/dmarc-report-fail.html delete mode 100644 docs/assets/panel-ui/dmarc-report.html delete mode 100644 docs/assets/panel-ui/dmarc.html delete mode 100644 docs/assets/panel-ui/domain-delete.html delete mode 100644 docs/assets/panel-ui/domain.html delete mode 100644 docs/assets/panel-ui/domains.html delete mode 100644 docs/assets/panel-ui/help.html delete mode 100644 docs/assets/panel-ui/inbound-backup.html delete mode 100644 docs/assets/panel-ui/inbound-delete.html delete mode 100644 docs/assets/panel-ui/inbound-domain.html delete mode 100644 docs/assets/panel-ui/inbound.html delete mode 100644 docs/assets/panel-ui/index.html delete mode 100644 docs/assets/panel-ui/login.html delete mode 100644 docs/assets/panel-ui/mail-queue.html delete mode 100644 docs/assets/panel-ui/mock.css delete mode 100644 docs/assets/panel-ui/settings.html delete mode 100644 docs/assets/panel-ui/setup.html delete mode 100644 docs/assets/panel-ui/shell.js delete mode 100644 docs/assets/panel-ui/status.html delete mode 100644 docs/assets/panel-ui/system-log.html delete mode 100644 docs/assets/panel-ui/system.css delete mode 100644 docs/assets/panel-ui/system.html delete mode 100644 docs/assets/panel-ui/user-delete.html delete mode 100644 docs/assets/panel-ui/user-form.html delete mode 100644 docs/assets/panel-ui/users.html diff --git a/docs/assets/panel-redesign/a/domain.html b/docs/assets/panel-redesign/a/domain.html deleted file mode 100644 index 8c9bb97..0000000 --- a/docs/assets/panel-redesign/a/domain.html +++ /dev/null @@ -1,164 +0,0 @@ - - - - - -example.org · Outbound domains · SelfPost - - - - - - - -
-
- -
-
- - -
-
-
-

Outbound domains /

-

example.org

-
- 1 284 messages in 30 days · peak 96 msg/h · average 1.8 msg/h -
- -
-
01

Connection

The same for every domain
-
-
-
Server
-
mail.example.org
-
-
-
Ports
-
465 — SSL/TLS (implicit)
587 — STARTTLS (submission)
-
-
-
- -
-
- 02

DNS records

- Checked 3 min ago -
-
- -
-

DKIM

publishedNot a secret · selector sp2026
-
-
Host
sp2026._domainkey.example.org
-
Type
TXT
-
Value
v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx3kqV0mJd8e2f1Yb7Qw5uT9sLr4nHc6pZaXo1vKe0yWm2gBt8RjN5dCq3hUf7lPs9aEi4oMz6xTb1kGv0nYw…IDAQAB
-
-
- -
-

SPF

mismatchMerge into the existing record — never publish a second one
-
-
Host
example.org
-
Type
TXT
-
Value
v=spf1 ip4:203.0.113.25 ~all
-
In DNS now
v=spf1 include:_spf.google.com ~all
-
-

The published record does not authorize 203.0.113.25. Add ip4:203.0.113.25 before ~all.

-
- -
-

DMARC

publishedReport address from Settings
-
-
Host
_dmarc.example.org
-
Type
TXT
-
Value
v=DMARC1; p=none; rua=mailto:dmarc@mail.example.org
-
-

p=none does not affect delivery. Tighten to quarantine, then reject, once the reports look clean.

-
-
- -
-
- 03

Applications

- SASL logins that may send as this domain - Add application -
- - - - - - - - - - - - - - - - - - -
LoginMay send as30 daysLimits
*@example.org1 102 msg · peak 96/h200 / hEditNew passwordDelete
alerts@example.org, noc@example.org182 msg · peak 14/hdomain 2 IPsEditNew passwordDelete
-
- -
-
04

Domain settings

Rarely changed
-
- DMARC reportsSame as Settings — dmarc@mail.example.orgdefault -

The form opens here.

-
-
- Rate limitAuto ×3 — 288 messages / hour, recalculated yesterdayactive -

The form opens here.

-
-
- ExportDKIM key and applications as one file, optionally encrypted -

The form opens here.

-
-
- Delete domainRemoves the DKIM key and both applications -

The confirmation opens here.

-
-
- - -
- - - diff --git a/docs/assets/panel-redesign/a/login.html b/docs/assets/panel-redesign/a/login.html deleted file mode 100644 index 30d06be..0000000 --- a/docs/assets/panel-redesign/a/login.html +++ /dev/null @@ -1,37 +0,0 @@ - - - - - -Sign in · SelfPost - - - - - - - - - - - - diff --git a/docs/assets/panel-redesign/a/status.html b/docs/assets/panel-redesign/a/status.html deleted file mode 100644 index da61c4f..0000000 --- a/docs/assets/panel-redesign/a/status.html +++ /dev/null @@ -1,136 +0,0 @@ - - - - - -Overview · SelfPost - - - - - - - -
-
- -
-
- -
-
-
-

mail.example.org

-

Overview

-
- Updated 14:02:31 UTC · refreshes every 10 s -
- -
-
SELFPOSTWARN1 OF 6 CHECKS
-
-

Mail is flowing. One thing needs attention.

-

The TLS certificate expires in 12 days and has not renewed yet. Everything else is healthy.

-
-
- -
-
01

Checks

Open a row for its detail
- -
- Processes5 of 5 runningok -
- - - - - - -
ProgramStateDetail
postfixrunningpid 41, uptime 18 days
opendkimrunningpid 39, uptime 18 days
-
-
- -
- TLS certificateExpires in 12 days — renewal has not runwarn -
-
-
Expires
2026-10-03 09:14 UTC
-
Names
mail.example.org
-
-

Renewal normally runs 30 days before expiry. Check that port 80 is reachable and look for acme lines in the system log.

-
-
- -
- MachineCPU 12 % · memory 41 % · ↓ 18 kB/s ↑ 44 kB/sok -
- - - - - - - -
ResourceUsageDetail
CPU12 %2 cores, load 0.21
Memory41 %812 MB of 1.9 GB
Network↓ 18 kB/s · ↑ 44 kB/seth0: 2.1 GB in, 6.4 GB out
-
-
- -
- QueuesOutbound empty · inbound emptyok -

No messages are waiting. Open the outbound queue or the inbound queue.

-
- -
- Milter sockets2 of 2 answeringok -
- - - - - - -
MilterStateDetail
opendkimokinet:127.0.0.1:8891
journal-milterokunix:/run/selfpost/journal.sock
-
-
- -
- Hostname and reverse DNSmail.example.org ⇄ 203.0.113.25ok -
-
-
Hostname
mail.example.org
-
Lookup
mail.example.org → 203.0.113.25
203.0.113.25 → mail.example.org
-
-
-
-
-
- - -
- - - diff --git a/docs/assets/panel-redesign/a/theme.css b/docs/assets/panel-redesign/a/theme.css deleted file mode 100644 index e5dffaa..0000000 --- a/docs/assets/panel-redesign/a/theme.css +++ /dev/null @@ -1,206 +0,0 @@ -/* Direction A — "Blank" (the postal form). Pico CSS v2 carries every generic - control (type, buttons, inputs, tables, details, dropdown); this file maps - the brand onto Pico's variables and adds only what is SelfPost's own: - the two-row header, the sheet, numbered form sections, the postmark and - the DNS record block. */ - -/* :root[...] rather than [data-theme=light] alone: Pico declares its light - palette on :root:not([data-theme=dark]), which a bare attribute selector - loses to. */ -:root[data-theme=light] { - --pico-font-family-sans-serif: var(--sp-sans); - --pico-font-family-monospace: var(--sp-mono); - --pico-font-family: var(--sp-sans); - --pico-border-radius: 2px; - --pico-spacing: .9rem; - --pico-typography-spacing-vertical: .9rem; - --pico-form-element-spacing-vertical: .5rem; - --pico-form-element-spacing-horizontal: .75rem; - - --pico-background-color: var(--sp-paper); - --pico-color: var(--sp-ink); - --pico-muted-color: var(--sp-muted); - --pico-muted-border-color: var(--sp-rule); - --pico-h1-color: var(--sp-ink); --pico-h2-color: var(--sp-ink); - --pico-h3-color: var(--sp-ink); --pico-h4-color: var(--sp-ink); - - --pico-primary: var(--sp-brick); - --pico-primary-background: var(--sp-brick); - --pico-primary-border: var(--sp-brick); - --pico-primary-underline: rgb(122 59 46 / .35); - --pico-primary-hover: var(--sp-brick-dark); - --pico-primary-hover-background: var(--sp-brick-dark); - --pico-primary-hover-border: var(--sp-brick-dark); - --pico-primary-hover-underline: var(--sp-brick-dark); - --pico-primary-focus: rgb(122 59 46 / .25); - --pico-primary-inverse: #fff; - - --pico-secondary: var(--sp-ink-2); - --pico-secondary-background: var(--sp-ink-2); - --pico-secondary-border: var(--sp-ink-2); - --pico-secondary-hover: var(--sp-ink); - --pico-secondary-hover-background: var(--sp-ink); - --pico-secondary-hover-border: var(--sp-ink); - --pico-secondary-focus: rgb(18 22 28 / .2); - --pico-secondary-inverse: #fff; - - --pico-form-element-background-color: #fff; - --pico-form-element-border-color: var(--sp-rule-strong); - --pico-form-element-active-border-color: var(--sp-brick); - --pico-form-element-focus-color: rgb(122 59 46 / .25); - --pico-table-border-color: var(--sp-rule); - --pico-code-background-color: var(--sp-well); - --pico-code-color: var(--sp-ink); - --pico-dropdown-background-color: #fff; - --pico-dropdown-border-color: var(--sp-rule); - --pico-dropdown-hover-background-color: var(--sp-brick-tint); -} - -/* Pico grows the root font with the viewport (up to 131%). An ops panel wants - one size everywhere. */ -:root { font-size: 15px; } -body { min-height: 100vh; display: flex; flex-direction: column; } -h1 { font-size: 1.7rem; font-weight: 600; margin: 0; } -small, .sp-muted { color: var(--sp-muted); } -code, .sp-mono { font-family: var(--sp-mono); font-size: .88em; } - -/* ---- header: sections on the first row, the section's pages on the second */ -.sp-wrap { width: 100%; max-width: 72rem; margin-inline: auto; padding-inline: 1.5rem; } -.sp-bar { background: var(--sp-sheet); border-bottom: 1px solid var(--sp-rule); } -.sp-bar nav { align-items: center; } -.sp-bar nav li { padding-block: .45rem; } -.sp-brand img { height: 46px; width: auto; display: block; } -.sp-sections a { - font-family: var(--sp-mono); font-size: .8rem; font-weight: 600; - letter-spacing: .09em; text-transform: uppercase; text-decoration: none; - color: var(--sp-muted); padding: .5rem .8rem; margin: 0; border-radius: 0; -} -.sp-sections a:hover { color: var(--sp-ink); } -.sp-sections a[aria-current] { color: var(--sp-brick); box-shadow: inset 0 -3px 0 var(--sp-brick); } -.sp-user summary { font-size: .9rem; } -.sp-pages { background: var(--sp-paper); border-bottom: 1px solid var(--sp-rule); } -.sp-pages .sp-wrap { display: flex; gap: 1.6rem; } -.sp-pages a { color: var(--sp-muted); text-decoration: none; padding: .6rem 0; font-size: .95rem; } -.sp-pages a:hover { color: var(--sp-ink); } -.sp-pages a[aria-current] { color: var(--sp-ink); font-weight: 600; box-shadow: inset 0 -2px 0 var(--sp-ink); } - -/* ---- the sheet: one column, one width, every page */ -main.sp-wrap { flex: 1; padding-block: 1.8rem 2.5rem; } -.sp-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 1rem; flex-wrap: wrap; margin-bottom: 1.4rem; } -.sp-head .sp-kicker { font-family: var(--sp-mono); font-size: .75rem; letter-spacing: .1em; text-transform: uppercase; color: var(--sp-muted); margin: 0 0 .15rem; } -.sp-head .sp-kicker a { color: inherit; } -.sp-head-actions { display: flex; gap: .5rem; align-items: center; } -.sp-head-actions > * { margin: 0; } -.sp-sheet { background: var(--sp-sheet); border: 1px solid var(--sp-rule); margin-bottom: 1.4rem; } -.sp-sheet > header { - display: flex; align-items: baseline; gap: .8rem; padding: .7rem 1.2rem; - border-bottom: 1px solid var(--sp-rule); -} -.sp-sheet > header .sp-no { font-family: var(--sp-mono); font-size: .75rem; color: var(--sp-brick); font-weight: 600; } -.sp-sheet > header h2 { - font-family: var(--sp-mono); font-size: .8rem; font-weight: 600; letter-spacing: .1em; - text-transform: uppercase; margin: 0; flex: 1; -} -.sp-sheet > header small { font-size: .85rem; } -.sp-sheet > .sp-body { padding: 1.1rem 1.2rem; } -.sp-sheet > .sp-body > :last-child { margin-bottom: 0; } -.sp-foot { color: var(--sp-muted); font-size: .8rem; border-top: 1px solid var(--sp-rule); padding-block: 1rem; } -.sp-foot a { color: inherit; } - -/* ---- status tag and the postmark */ -.sp-tag { - display: inline-block; font-family: var(--sp-mono); font-size: .72rem; font-weight: 600; - letter-spacing: .08em; text-transform: uppercase; padding: .1rem .5rem; - border: 1px solid var(--sp-idle-rule); background: var(--sp-idle-bg); color: var(--sp-idle); - border-radius: 2px; white-space: nowrap; text-decoration: none; -} -.sp-tag.ok { color: var(--sp-ok); background: var(--sp-ok-bg); border-color: var(--sp-ok-rule); } -.sp-tag.warn { color: var(--sp-warn); background: var(--sp-warn-bg); border-color: var(--sp-warn-rule); } -.sp-tag.fail { color: var(--sp-fail); background: var(--sp-fail-bg); border-color: var(--sp-fail-rule); } - -.sp-verdict { display: flex; align-items: center; gap: 1.5rem; padding: 1.3rem 1.4rem; } -.sp-verdict h2 { margin: 0 0 .2rem; font-size: 1.35rem; font-weight: 600; } -.sp-verdict p { margin: 0; color: var(--sp-muted); } -.sp-postmark { - flex: none; width: 5.6rem; height: 5.6rem; border-radius: 50%; - border: 2px solid currentColor; outline: 1px solid currentColor; outline-offset: 3px; - display: grid; place-content: center; text-align: center; transform: rotate(-9deg); - font-family: var(--sp-mono); font-weight: 600; letter-spacing: .08em; line-height: 1.15; - color: var(--sp-ok); -} -.sp-postmark.warn { color: var(--sp-warn); } -.sp-postmark.fail { color: var(--sp-fail); } -.sp-postmark b { font-size: 1.25rem; display: block; } -.sp-postmark span { font-size: .55rem; display: block; } - -/* ---- ledger: one row per check, the row opens into its detail */ -.sp-ledger details { margin: 0; padding: 0; border-bottom: 1px solid var(--sp-rule); } -.sp-ledger details:last-child { border-bottom: 0; } -.sp-ledger summary { - display: grid; grid-template-columns: 13rem 1fr auto 1.2rem; gap: 1rem; align-items: center; - padding: .75rem 1.2rem; margin: 0; color: var(--sp-ink); -} -.sp-ledger summary::after { grid-column: 4; margin: 0; float: none; } -.sp-ledger summary:hover { background: var(--sp-paper); } -.sp-ledger summary strong { font-weight: 600; } -.sp-ledger summary span.sp-muted { font-size: .93rem; } -.sp-ledger .sp-detail { padding: .2rem 1.2rem 1.1rem 15.2rem; } -.sp-ledger .sp-detail table { margin: 0; font-size: .93rem; } -.sp-ledger .sp-detail > :last-child { margin-bottom: 0; } -.sp-ledger meter { width: 9rem; height: .7rem; vertical-align: middle; margin-right: .5rem; } - -/* ---- label / value / copy: the DNS record block and connection settings */ -.sp-record { padding: 1.1rem 1.2rem; border-bottom: 1px solid var(--sp-rule); } -.sp-record:last-child { border-bottom: 0; } -.sp-record-head { display: flex; align-items: center; gap: .7rem; margin-bottom: .7rem; } -.sp-record-head h3 { margin: 0; font-size: 1.05rem; font-weight: 600; } -.sp-record-head small { margin-left: auto; } -.sp-kv { display: grid; grid-template-columns: 6.5rem 1fr; gap: .45rem 1rem; align-items: start; margin: 0; } -.sp-kv dt { - font-family: var(--sp-mono); font-size: .72rem; letter-spacing: .08em; text-transform: uppercase; - color: var(--sp-muted); padding-top: .5rem; margin: 0; -} -.sp-kv dd { margin: 0; display: flex; gap: .4rem; align-items: stretch; min-width: 0; } -.sp-value { - flex: 1; min-width: 0; font-family: var(--sp-mono); font-size: .86rem; line-height: 1.5; - background: var(--sp-well); border: 1px solid var(--sp-rule); padding: .4rem .65rem; - overflow-wrap: anywhere; border-radius: 2px; -} -.sp-value.plain { background: none; border-color: transparent; padding-left: 0; font-family: var(--sp-sans); font-size: .95rem; } -.sp-copy { - flex: none; align-self: flex-start; height: 2.15rem; margin: 0; padding: 0 .7rem; font-size: .8rem; - --pico-background-color: #fff; --pico-border-color: var(--sp-rule-strong); --pico-color: var(--sp-ink-2); -} -.sp-copy:hover { --pico-background-color: var(--sp-paper); --pico-border-color: var(--sp-ink-2); --pico-color: var(--sp-ink); } -.sp-note { margin: .7rem 0 0 7.5rem; font-size: .9rem; color: var(--sp-muted); } -.sp-note.fail { color: var(--sp-fail); } -.sp-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); gap: 0 2.4rem; } - -/* ---- applications */ -.sp-apps { margin: 0; font-size: .95rem; } -.sp-apps td, .sp-apps th { padding-inline: 1.2rem .4rem; vertical-align: middle; } -.sp-apps th { font-family: var(--sp-mono); font-size: .72rem; letter-spacing: .08em; text-transform: uppercase; color: var(--sp-muted); font-weight: 400; } -.sp-apps td:last-child { text-align: right; padding-right: 1.2rem; white-space: nowrap; } -.sp-apps .sp-login { font-family: var(--sp-mono); font-weight: 600; } -.sp-link-btn { font-size: .9rem; margin-left: .9rem; } -.sp-link-btn.danger { color: var(--sp-fail); --pico-underline: rgb(180 35 24 / .35); text-decoration-color: rgb(180 35 24 / .35); } -.sp-btn-sm { padding: .35rem .8rem; font-size: .9rem; margin: 0; width: auto; } - -/* ---- sign-in */ -.sp-signin { flex: 1; display: grid; place-content: center; padding: 2rem 1.5rem; } -.sp-signin-card { width: 23rem; max-width: 100%; } -.sp-signin-card img { display: block; width: 15rem; height: auto; margin: 0 auto 1.6rem; } -.sp-signin-card .sp-body { padding: 1.4rem 1.4rem 1.2rem; } -.sp-signin-card button { margin-bottom: 0; } -.sp-tagline { text-align: center; font-family: var(--sp-mono); font-size: .75rem; letter-spacing: .16em; color: var(--sp-muted); margin-top: 1.2rem; } - -@media (max-width: 48rem) { - .sp-ledger summary { grid-template-columns: 1fr auto 1.2rem; } - .sp-ledger summary span.sp-muted { grid-column: 1 / -1; grid-row: 2; } - .sp-ledger summary::after { grid-column: 3; } - .sp-ledger .sp-detail { padding-left: 1.2rem; } - .sp-kv { grid-template-columns: 1fr; gap: .15rem; } - .sp-kv dt { padding-top: .5rem; } - .sp-note { margin-left: 0; } - .sp-pages .sp-wrap { overflow-x: auto; white-space: nowrap; } -} diff --git a/docs/assets/panel-redesign/b/domain.html b/docs/assets/panel-redesign/b/domain.html deleted file mode 100644 index c5053bd..0000000 --- a/docs/assets/panel-redesign/b/domain.html +++ /dev/null @@ -1,212 +0,0 @@ - - - - - -example.org · Outbound domains · SelfPost - - - - - - -
- - - -
- - -
-
-
- -
-
-

DNS records

Checked 3 min ago
- -
-
-

DKIM

published - Not a secret · selector sp2026 -
-
-
- -
-
-
- - -
-
- -
-
-
-
- -
-
-

SPF

mismatch - Merge into the existing record — never publish a second one -
-
-
- -
-
-
- - -
-
- -
-
-
- - -
The published record does not authorize 203.0.113.25. Add ip4:203.0.113.25 before ~all.
-
-
-
- -
-
-

DMARC

published - Report address from Settings -
-
-
- -
-
-
- - -
-
- -
-
-
-
-
-
- -
-
-

Connection

-
-
-
Server
mail.example.org
-
Ports
465 SSL/TLS
587 STARTTLS
-
-

The same for every domain. Authenticate with an application login.

-
-
- -
-

Sent, 30 days

-
-
-
Messages
1 284
-
Peak
96 /h
-
Average
1.8 /h
-
-
-
- - -
- -
-
-

Applications

SASL logins that may send as this domain
-
- - - - - - - - - - - - - - -
LoginMay send as30 daysLimits
prod-server*@example.org1 102 msg · peak 96/h200 / h
alertsalerts@example.org, noc@example.org182 msg · peak 14/hdomain 2 IPs
-
-
-
- -
-
-
- - -
-
- - diff --git a/docs/assets/panel-redesign/b/login.html b/docs/assets/panel-redesign/b/login.html deleted file mode 100644 index b85d14a..0000000 --- a/docs/assets/panel-redesign/b/login.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - - -Sign in · SelfPost - - - - - -
-
-
-
-
-

Sign in

-
-
- - -
-
- - -
- -
-
-
-
YOUR SMTP. YOUR RULES.
-
© 2026 Mix · License (AGPL-3.0) · Source · No warranty
-
-
- - diff --git a/docs/assets/panel-redesign/b/status.html b/docs/assets/panel-redesign/b/status.html deleted file mode 100644 index 9573fc2..0000000 --- a/docs/assets/panel-redesign/b/status.html +++ /dev/null @@ -1,168 +0,0 @@ - - - - - -Overview · SelfPost - - - - - - -
- - - -
- - -
-
- - - -
-
Machine
CPU 12 % · mem 41 %
OK
-
Processes
5 of 5 running
OK
-
Queues
Both empty
OK
-
TLS certificate
Expires in 12 days
Warn
-
Milter sockets
2 of 2 answering
OK
-
Reverse DNS
Forward = reverse
OK
-
- -
-
-
-

Machine

ok
-
- - - - - - - -
ResourceUsageDetail
CPU12 %2 cores, load 0.21
Memory41 %812 MB of 1.9 GB
Network↓ 18 kB/s · ↑ 44 kB/seth0: 2.1 GB in, 6.4 GB out
-
-
-
- -
-
-

Processes

ok
-
- - - - - - - - - -
ProgramStateDetail
postfixrunningpid 41, uptime 18 days
opendkimrunningpid 39, uptime 18 days
journal-milterrunningpid 40, uptime 18 days
panelrunningpid 44, uptime 18 days
dmarc-ingestrunningpid 47, uptime 18 days
-
-
-
- -
-
-

TLS certificate

warn
-
-
-
Expires
2026-10-03 09:14 UTC
-
Names
mail.example.org
-
-

Renewal normally runs 30 days before expiry. Check port 80 and the system log.

-
-
-
- -
-
-

Milter sockets

ok
- - - - - - -
MilterState
opendkim
inet:127.0.0.1:8891
ok
journal-milter
unix:/run/selfpost/journal.sock
ok
-
-
- -
-
-

Hostname and reverse DNS

ok
-
-
-
Hostname
mail.example.org
-
Lookup
→ 203.0.113.25
← mail.example.org
-
-
- -
-
-
- -
-
- - -
-
- - diff --git a/docs/assets/panel-redesign/b/theme.css b/docs/assets/panel-redesign/b/theme.css deleted file mode 100644 index 8a5a59a..0000000 --- a/docs/assets/panel-redesign/b/theme.css +++ /dev/null @@ -1,80 +0,0 @@ -/* Direction B — "Console". Tabler (Bootstrap 5 admin kit) carries the shell, - cards, tables, badges, input groups and the sign-in page as shipped; this - file only re-points Tabler's variables at the brand and adds the two things - Tabler has no word for: a sidebar group label and the DNS record block. */ - -:root, [data-bs-theme=light] { - --tblr-font-sans-serif: var(--sp-sans); - --tblr-font-monospace: var(--sp-mono); - --tblr-body-bg: var(--sp-paper); - --tblr-body-color: var(--sp-ink); - --tblr-secondary: var(--sp-muted); - --tblr-border-color: var(--sp-rule); - --tblr-border-radius: 3px; - --tblr-border-radius-lg: 4px; - - --tblr-primary: #7a3b2e; - --tblr-primary-rgb: 122, 59, 46; - --tblr-link-color: #7a3b2e; - --tblr-link-color-rgb: 122, 59, 46; - --tblr-link-hover-color: #632f25; - --tblr-link-hover-color-rgb: 99, 47, 37; - - --tblr-green: #2c6b43; --tblr-green-rgb: 44, 107, 67; - --tblr-yellow: #b5700f; --tblr-yellow-rgb: 181, 112, 15; - --tblr-red: #b42318; --tblr-red-rgb: 180, 35, 24; - --tblr-success: #2c6b43; --tblr-success-rgb: 44, 107, 67; - --tblr-warning: #b5700f; --tblr-warning-rgb: 181, 112, 15; - --tblr-danger: #b42318; --tblr-danger-rgb: 180, 35, 24; -} -body { font-feature-settings: normal; } -.btn-primary { --tblr-btn-hover-bg: #632f25; --tblr-btn-active-bg: #632f25; } -code, .font-monospace { font-family: var(--sp-mono); } - -/* ---- sidebar: ink, with the stamp on its own paper */ -.navbar-vertical { - --tblr-navbar-bg: var(--sp-ink); background: var(--sp-ink); - --tblr-navbar-color: #aab2bd; --tblr-navbar-hover-color: #fff; --tblr-navbar-active-color: #fff; - --tblr-navbar-border-color: transparent; -} -.navbar-vertical .navbar-brand { padding: 1.1rem 0 .9rem; justify-content: flex-start; } -.navbar-vertical .navbar-brand img { height: 52px; width: auto; } -.navbar-vertical .nav-link { padding-block: .42rem; } -.navbar-vertical .nav-link-icon { font-size: 1.15rem; } -.navbar-vertical .nav-item.active > .nav-link { - background: rgb(255 255 255 / .07); box-shadow: inset 3px 0 0 #c4705e; color: #fff; -} -.sp-nav-label { - font-family: var(--sp-mono); font-size: .66rem; font-weight: 600; letter-spacing: .14em; - text-transform: uppercase; color: #6f7a88; padding: 1.1rem 1.5rem .3rem; -} -.sp-nav-foot { margin-top: auto; padding-bottom: .8rem; border-top: 1px solid rgb(255 255 255 / .08); padding-top: .6rem; } -.sp-nav-user { color: #6f7a88; font-size: .8rem; padding: .3rem 1.5rem; } - -/* ---- page */ -.page-pretitle { font-family: var(--sp-mono); letter-spacing: .1em; } -.card-title { font-weight: 600; } -.table thead th { font-family: var(--sp-mono); letter-spacing: .08em; } -.sp-tile .card-body { display: flex; align-items: center; gap: .8rem; padding: .85rem 1rem; } -.sp-tile .sp-tile-icon { font-size: 1.5rem; color: var(--sp-muted); } -.sp-tile .fw-semibold { line-height: 1.2; } -.sp-tile .status { margin-left: auto; } -progress.sp-bar { - appearance: none; -webkit-appearance: none; width: 8rem; height: .4rem; border: 0; - border-radius: 1rem; overflow: hidden; vertical-align: middle; margin-right: .6rem; background: var(--sp-well); -} -progress.sp-bar::-webkit-progress-bar { background: var(--sp-well); } -progress.sp-bar::-webkit-progress-value { background: var(--tblr-green); } -progress.sp-bar::-moz-progress-bar { background: var(--tblr-green); } - -/* ---- the DNS record block: Tabler's read-only input group, plus a status */ -.sp-record + .sp-record { border-top: 1px solid var(--sp-rule); } -.sp-record .form-control[readonly], .sp-record textarea[readonly] { - font-family: var(--sp-mono); font-size: .8rem; background: var(--sp-well); -} -.sp-record textarea { resize: none; } -.sp-record .form-label { font-size: .75rem; color: var(--sp-muted); margin-bottom: .25rem; } - -.sp-foot { color: var(--sp-muted); font-size: .8rem; } -.sp-signin-mark { width: 15rem; height: auto; } -.sp-tagline { font-family: var(--sp-mono); font-size: .72rem; letter-spacing: .16em; color: var(--sp-muted); } diff --git a/docs/assets/panel-redesign/c/domain.html b/docs/assets/panel-redesign/c/domain.html deleted file mode 100644 index 7059e5c..0000000 --- a/docs/assets/panel-redesign/c/domain.html +++ /dev/null @@ -1,200 +0,0 @@ - - - - - -example.org · Outbound domains · SelfPost - - - - - - - - -
-
-
-
-
-

Outbound domains /

-

example.org

-
-
-
-
-
- Add application -
-
-
- -
-
- -
- -
-
-

DNS records

Checked 3 min ago
- -
-

DKIM

publishedNot a secret · selector sp2026
-
-
- -
-
-
-
-
- -
-
-
- -
-

SPF

mismatchMerge into the existing record — never publish a second one
-
-
- -
-
-
-
-
- -
-
-
- -
-

The published record does not authorize 203.0.113.25. Add ip4:203.0.113.25 before ~all.

-
-
- -
-

DMARC

publishedReport address from Settings
-
-
- -
-
-
-
-
- -
-
-
-
- -
-

Applications

SASL logins that may send as this domain
- - - - - - - - - - - - - - -
LoginMay send as30 daysLimits
prod-server*@example.org1 102 msg · peak 96/h200 / h
alertsalerts@example.org, noc@example.org182 msg · peak 14/hdomain 2 IPs
-
- -
-
-
-

Connection

Same for every domain
-

mail.example.org

-

465 SSL/TLS · 587 STARTTLS

-
-
-
-
-

Sent, 30 days

-
-
1 284messages
-
96peak / h
-
1.8average / h
-
-
-
-
-
-
- - -
-
- - - diff --git a/docs/assets/panel-redesign/c/login.html b/docs/assets/panel-redesign/c/login.html deleted file mode 100644 index 4574b43..0000000 --- a/docs/assets/panel-redesign/c/login.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - - -Sign in · SelfPost - - - - - - - - - - diff --git a/docs/assets/panel-redesign/c/status.html b/docs/assets/panel-redesign/c/status.html deleted file mode 100644 index a3a42ff..0000000 --- a/docs/assets/panel-redesign/c/status.html +++ /dev/null @@ -1,117 +0,0 @@ - - - - - -Overview · SelfPost - - - - - - - - -
-
-
-
-
-

mail.example.org

-

Overview

-
-
-
Updated 14:02:31 UTC · refreshes every 10 s
-
- - - -
-
-
-

Outbound domains

- - - - - - - -
DomainDNSApps30 days
example.orgDKIM SPF DMARC21 284 msg
shop.example.orgDKIM SPF DMARC118 920 msg
notify.acme.ioDKIM SPF DMARC3402 msg
-
-
-
-
-

Inbound domains

- - - - - - -
DomainMXUpstream
lists.example.orgMXmx.internal:25
acme.ioMX10.0.4.12:2525
-
-
-
- - -
-
- - - diff --git a/docs/assets/panel-redesign/c/theme.css b/docs/assets/panel-redesign/c/theme.css deleted file mode 100644 index f41edce..0000000 --- a/docs/assets/panel-redesign/c/theme.css +++ /dev/null @@ -1,98 +0,0 @@ -/* Direction C — "Counter". Bulma v1 (pure CSS, no JavaScript, no data: URIs) - carries the navbar with hover dropdowns, columns, boxes, tables, tags, - the side menu and input-with-addon fields. This file points Bulma's HSL - variables at the brand and adds the health strip and the record block. */ - -:root { - --bulma-family-primary: var(--sp-sans); - --bulma-family-secondary: var(--sp-sans); - --bulma-family-code: var(--sp-mono); - --bulma-primary-h: 10deg; --bulma-primary-s: 45%; --bulma-primary-l: 33%; - --bulma-link-h: 10deg; --bulma-link-s: 45%; --bulma-link-l: 33%; - --bulma-success-h: 138deg; --bulma-success-s: 42%; --bulma-success-l: 30%; - --bulma-warning-h: 36deg; --bulma-warning-s: 85%; --bulma-warning-l: 55%; - --bulma-danger-h: 4deg; --bulma-danger-s: 76%; --bulma-danger-l: 40%; - --bulma-radius: 6px; --bulma-radius-large: 10px; - --bulma-body-size: 15px; - /* Bulma derives link text and the on-primary colour from lightness steps; - at the brick's 33 % both land off-brand, so they are pinned. */ - --bulma-link-text: var(--sp-brick); - --bulma-link-text-hover: var(--sp-brick-dark); -} -.button.is-primary, .button.is-primary:hover { color: #fff; } -a strong { color: inherit; } -html { background: var(--sp-paper); } -body { background: var(--sp-paper); color: var(--sp-ink); min-height: 100vh; display: flex; flex-direction: column; } -main { flex: 1; } -code { background: var(--sp-well); color: var(--sp-ink); border-radius: 3px; padding: .1em .35em; font-size: .85em; } - -/* ---- navbar: brick, the stamp on it, hover dropdowns */ -.navbar.is-primary { --bulma-navbar-height: 4rem; } -.navbar-brand img { max-height: 3rem; height: 3rem; } -.navbar.is-primary .navbar-item, .navbar.is-primary .navbar-link { color: var(--sp-on-brick); font-weight: 500; } -.navbar.is-primary .navbar-link::after { border-color: var(--sp-on-brick); } -.navbar.is-primary .navbar-start > .navbar-item.is-active, -.navbar.is-primary .navbar-start > .has-dropdown.sp-current > .navbar-link { box-shadow: inset 0 -3px 0 var(--sp-on-brick); } -.navbar-dropdown { border-top: 0; box-shadow: 0 8px 24px rgb(18 22 28 / .14); } -.navbar.is-primary .navbar-dropdown .navbar-item { color: var(--sp-ink); font-weight: 400; } -.navbar.is-primary .navbar-dropdown .navbar-item.is-active { color: var(--sp-brick); font-weight: 600; background: var(--sp-brick-tint); } - -/* ---- page */ -.sp-page { padding-block: 2rem 2.5rem; } -.sp-kicker { font-family: var(--sp-mono); font-size: .75rem; letter-spacing: .1em; text-transform: uppercase; color: var(--sp-muted); } -.sp-kicker a { color: inherit; } -.title { color: var(--sp-ink); font-weight: 600; } -.box { box-shadow: none; border: 1px solid var(--sp-rule); } -.box > .sp-box-head { display: flex; align-items: center; gap: .75rem; margin-bottom: 1rem; } -.box > .sp-box-head h2 { font-size: 1.1rem; font-weight: 600; margin: 0; } -.box > .sp-box-head .sp-end { margin-left: auto; color: var(--sp-muted); font-size: .85rem; display: flex; gap: .6rem; align-items: center; } -.table { background: transparent; } -.table thead th { font-family: var(--sp-mono); font-size: .7rem; font-weight: 400; letter-spacing: .08em; text-transform: uppercase; color: var(--sp-muted); } -.table td { vertical-align: middle; } -.tag { font-family: var(--sp-mono); font-weight: 600; letter-spacing: .04em; } -.sp-muted { color: var(--sp-muted); } -.sp-mono { font-family: var(--sp-mono); } - -/* ---- health strip */ -.sp-health { display: grid; grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr)); gap: 0; padding: 0; overflow: hidden; } -.sp-health > a { - display: block; padding: 1rem 1.2rem; color: var(--sp-ink); border-right: 1px solid var(--sp-rule); - box-shadow: inset 0 3px 0 var(--sp-ok); -} -.sp-health > a:last-child { border-right: 0; } -.sp-health > a:hover { background: var(--sp-paper); } -.sp-health > a.warn { box-shadow: inset 0 3px 0 #d9962b; background: var(--sp-warn-bg); } -.sp-health > a.fail { box-shadow: inset 0 3px 0 var(--sp-fail); background: var(--sp-fail-bg); } -.sp-health .sp-h-name { font-family: var(--sp-mono); font-size: .68rem; letter-spacing: .08em; text-transform: uppercase; color: var(--sp-muted); } -.sp-health .sp-h-value { font-size: 1.15rem; font-weight: 600; line-height: 1.3; } -.sp-health .sp-h-sub { font-size: .8rem; color: var(--sp-muted); } - -/* ---- domain page */ -.sp-side { position: sticky; top: 1.5rem; } -.menu-label { font-family: var(--sp-mono); letter-spacing: .1em; } -.menu-list a { display: flex; align-items: center; gap: .5rem; } -.menu-list a .tag { margin-left: auto; } -.sp-record + .sp-record { border-top: 1px solid var(--sp-rule); margin-top: 1.25rem; padding-top: 1.25rem; } -.sp-record-head { display: flex; align-items: center; gap: .6rem; margin-bottom: .75rem; } -.sp-record-head h3 { font-size: 1rem; font-weight: 600; margin: 0; } -.sp-record-head small { margin-left: auto; color: var(--sp-muted); } -.sp-record .label { font-size: .75rem; font-weight: 400; color: var(--sp-muted); margin-bottom: .25rem; } -.sp-record .input[readonly], .sp-record .textarea[readonly] { - font-family: var(--sp-mono); font-size: .82rem; background: var(--sp-well); box-shadow: none; -} -.sp-record .textarea { resize: none; min-height: 0; } -.sp-record .field:not(:last-child) { margin-bottom: .6rem; } -.sp-row { display: grid; grid-template-columns: 1fr 7rem; gap: .75rem; } -.sp-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; } -.sp-stats b { display: block; font-size: 1.5rem; font-weight: 600; line-height: 1.2; } - -/* ---- sign-in */ -.sp-signin { min-height: 100vh; margin: 0; } -.sp-signin-brand { background: var(--sp-brick); display: grid; place-content: center; text-align: center; padding: 3rem; } -.sp-signin-brand img { width: 20rem; max-width: 100%; height: auto; } -.sp-signin-brand p { font-family: var(--sp-mono); letter-spacing: .18em; font-size: .8rem; color: var(--sp-on-brick); margin-top: 1.6rem; } -.sp-signin-form { display: flex; flex-direction: column; justify-content: center; padding: 3rem; } -.sp-signin-form form { width: 21rem; max-width: 100%; margin-inline: auto; } -.sp-foot { color: var(--sp-muted); font-size: .8rem; padding-block: 1.2rem; } -.sp-foot a { color: inherit; text-decoration: underline; } -.sp-signin-form .sp-foot { text-align: center; margin-top: 2.5rem; } diff --git a/docs/assets/panel-redesign/index.html b/docs/assets/panel-redesign/index.html index c5371b8..da6a3b1 100644 --- a/docs/assets/panel-redesign/index.html +++ b/docs/assets/panel-redesign/index.html @@ -3,19 +3,21 @@ -Panel redesign · three directions +Panel mockups
-

SelfPost · panel redesign · stage 2

-

The accepted direction, every screen

-

Direction C (Bulma) was picked as the base: the important things in view, details one - or two clicks away. It takes the postal signature of A — the postmark verdict, numbered mono section - heads, the stamp's perforated paper edge under a brick bar that carries the wordmark — and the icons of B. Thirty-four screens and a components page live in - panel/. Serve this folder from the repository root: the pages load the panel's own - fonts and mark by relative path.

+

SelfPost · panel mockups

+

Every screen of the panel

+

The accepted design (the plan): Bulma as the base, the + important things in view, details one or two clicks away, with a postal signature — the postmark verdict, numbered + mono section heads, the stamp's perforated paper edge under a brick bar that carries the wordmark — and Tabler icons. + The screens and a components page live in panel/; a page of the running panel that + disagrees with its mockup is wrong. Serve this folder from the repository root: the pages load the panel's own fonts + and mark by relative path. The three directions of the redesign's first stage and the earlier 1.x mockups were + removed on 2026-10-16; they are in the git history.

Screen map

@@ -44,7 +46,7 @@
-

What changed against today's panel

+

What changed against the 1.x panel

  • Overview shows verdicts, Health shows tables. Process lists, socket paths and interface counters moved to Server › Health, one click from each card.
  • @@ -60,79 +62,6 @@
  • panel/build.py wraps the fragments in panel/src/ with the shared navbar; the built pages are committed, so nothing has to be run to view them.
- -

Stage 1 · three directions on three key screens

-

Kept for the record: Overview, an outbound domain and sign-in, drawn three ways.

- -

Shared by all three

-
    -
  • Navigation by mail direction. Overview · Outbound (Domains, Log, Queue, DMARC reports) · - Inbound (Domains, Log, Queue) · Server (System log, Backup, Users, Settings). Account, Help and - Sign out live with the user name. Eleven flat entries become four groups.
  • -
  • One block per DNS record. Today "DNS status" and "records to publish" show every host twice. - Here a record is one block: its status, what to publish, and — only on a mismatch — what DNS holds now.
  • -
  • The domain page stops being nine cards. DNS and applications are the page; limits, report address, - export and delete are "rarely changed" and sit behind one click.
  • -
  • Applications are a table; editing one is its own form page instead of a three-column panel inside a list row.
  • -
  • Brand carriers stay: the stamp, brick, IBM Plex. No inline style or script in any mockup.
  • -
- -

The directions

-
-
-

A · Pico CSS 2.1

-

Blank

-

The panel as a postal form: one centred sheet, numbered sections, mono field labels, a postmark for the - verdict. Overview is a ledger — one row per check, the row opens into its detail. Sections on the first - header row, the section's pages on the second.

- -
-
-

B · Tabler 1.5 (Bootstrap 5)

-

Console

-

The classic admin kit: ink sidebar with every page one click away, status tiles, cards and tables in a - 12-column grid, read-only input groups with Copy. Almost nothing is ours; it also looks the most like - every other admin panel.

- -
-
-

C · Bulma 1.0

-

Counter

-

Brick top bar with CSS-only hover menus. Overview is the whole service on one screen: a health strip over - the outbound and inbound domain lists. The domain page gets a sticky side menu. Softer, rounder, the - boldest use of the brand colour.

- -
-
- -

What each one costs

-
- - - - - - - - - - - - - - - - -
A · PicoB · TablerC · Bulma
LicenceMITMITMIT
Vendored CSS (minified)83 KB694 KB678 KB
JavaScriptnonenone for these screens; Bootstrap bundle only if dropdowns / modals are wantednone
Build stepnonenonenone (a Sass build could trim it; not needed)
CSP default-src 'self'needs img-src 'self' data: — 14 data: SVGs (select chevron, checkbox tick)needs img-src 'self' data: — 28 data: SVGsworks as is — no data: URIs
Our own CSS in the mockup≈ 200 lines≈ 80 lines≈ 100 lines
Markup stylesemantic HTML, few classes — closest to today's templatesclass-heavy Bootstrap markupclass-based, moderate
Own componentssheet, ledger row, postmark, record blocksidebar group label, record blockhealth strip, record block
Recognisablemostleastmiddle
-
- -

Open points

-
    -
  • Icons in B come from Tabler Icons (MIT) through its webfont for the mockup; the panel would vendor only the used glyphs as an SVG sprite.
  • -
  • Inbound log rides on the agreed inbound-antispam-panel journal; Inbound queue is a new filter over the one Postfix queue and needs its own roadmap row.
  • -
  • Dark scheme and the narrow-screen shell are not drawn at this stage; all three libraries ship both.
  • -
  • The earlier panel-ui experiment and panel-redesign.md are superseded by whichever direction is accepted.
  • -
diff --git a/docs/assets/panel-ui/app.html b/docs/assets/panel-ui/app.html deleted file mode 100644 index 23cb394..0000000 --- a/docs/assets/panel-ui/app.html +++ /dev/null @@ -1,44 +0,0 @@ - - - - - -SelfPost — panel mockups - - - - -

Макеты собраны отдельными страницами. Откройте Status или оглавление.

- - diff --git a/docs/assets/panel-ui/backup.html b/docs/assets/panel-ui/backup.html deleted file mode 100644 index f83e70b..0000000 --- a/docs/assets/panel-ui/backup.html +++ /dev/null @@ -1,43 +0,0 @@ - - - - - -Backup & migration — SelfPost mockups - - - - - -
-

Backup & migration

-
-
-

Full backup

-

Download a self-contained backup — data/ (database, DKIM keys, credentials, Postfix queue), docker-compose.yml, .env, and certs/. Extract into an empty project directory on a new machine, adjust hostname if needed, and start the same SelfPost version before first boot. The reverse-proxy vhost is not included.

-

The backup file is a secret. Encrypting it is the simplest way to store it: the download is then a .spbk that only the password opens.

-
- -
- - -

Keep this password: without it the file cannot be opened.

-
-
-
-
-
-

Import a domain

-

Move a single domain here from another SelfPost instance — plain .json or encrypted .spde. 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.

- - - - -

Needed for a .spde file. Leave empty for plain .json.

-
-
-
-
- - - diff --git a/docs/assets/panel-ui/deliveries.html b/docs/assets/panel-ui/deliveries.html deleted file mode 100644 index 6004baa..0000000 --- a/docs/assets/panel-ui/deliveries.html +++ /dev/null @@ -1,91 +0,0 @@ - - - - - -Deliveries — SelfPost mockups - - - - - -
-

Deliveries

-
-
- -
-
- -
- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
TimeFromToSubjectStatus
2026-08-15 20:14:02billing@example.comada@example.netInvoice #4412deferredDetails
2026-08-15 20:11:40news@example.comlist-bounces@example.netAugust digestdeliveredDetails
2026-08-15 19:02:11alerts@alerts.example.comnoreply@blocked.exampleDisk 92% on web-3bouncedDetails
2026-08-15 18:44:09news@example.comsam@example.orgAugust digestdeliveredDetails
- -

Page 1 of 4 · Older →

-
-
-
- - - diff --git a/docs/assets/panel-ui/delivery.html b/docs/assets/panel-ui/delivery.html deleted file mode 100644 index 16fef89..0000000 --- a/docs/assets/panel-ui/delivery.html +++ /dev/null @@ -1,77 +0,0 @@ - - - - - -Invoice #4412 — SelfPost mockups - - - - - -
-
-

Invoice #4412

-

- billing@example.com - → - ada@example.net - deferred -

- ← Back to deliveries -
-
-
-

Message

-
-
Domainexample.com
-
Applicationbilling
-
Accepted2026-08-15 20:14:02 UTC
-
Status reported2026-08-15 20:14:08 UTC
-
Queue id4C3A1E2F1A
-
Journal id1842
-
-
-
-

History

-
    -
  1. -

    2026-08-15 20:14:02 UTC

    -

    accepted Received by the relay

    -

    SASL login billing, queued as 4C3A1E2F1A.

    -
  2. -
  3. -

    2026-08-15 20:14:08 UTC

    -

    deferred Receiving MX asked to try later

    -

    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.

    -
  4. -
  5. -

    not yet

    -

    delivery Waiting on the next retry

    -
  6. -
-
-
-
-
-

Delivery log

- - - - - - - -
TimeMessage
20:14:02postfix/smtpd[221]: 4C3A1E2F1A: client=203.0.113.40, sasl_username=billing
20:14:02postfix/cleanup[224]: 4C3A1E2F1A: message-id=<4412@example.com>
20:14:08postfix/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)
-
-

20:14:08

- status=deferred (450 4.2.1 mailbox busy) -

20:14:02

- client=203.0.113.40, sasl_username=billing -
-
-
-
- - - diff --git a/docs/assets/panel-ui/dmarc-domain.html b/docs/assets/panel-ui/dmarc-domain.html deleted file mode 100644 index 2c41e09..0000000 --- a/docs/assets/panel-ui/dmarc-domain.html +++ /dev/null @@ -1,88 +0,0 @@ - - - - - -example.com — DMARC — SelfPost mockups - - - - - -
-
-

example.com candidate

- ← All DMARC reports - ← Back to example.com -
-
-
-

Last 7 days pass

-

98% aligned. Tightening p= looks reasonable.

-

Domain DNS and rua=

-
-
-

Third-party senders

-

google.com / 66.102.0.0/20 failed SPF and DKIM 6 times. Everyone else is this relay.

-
-
-
-
-

Reports

- - - - - - - - - - - - - - - - - - - - -
ReceivedReporterWindowPassFail
2026-08-15 06:12google.com14 Aug4126View
2026-08-14 06:08google.com13 Aug3902View
- -
-
-
-
-

Sources · last 7 days

- - - - - - -
SourcePassFailDisposition
203.0.113.10 (this relay)8020none
google.com / 66.102.0.0/2008none
- -
-
-
- - - diff --git a/docs/assets/panel-ui/dmarc-report-fail.html b/docs/assets/panel-ui/dmarc-report-fail.html deleted file mode 100644 index ccfee61..0000000 --- a/docs/assets/panel-ui/dmarc-report-fail.html +++ /dev/null @@ -1,88 +0,0 @@ - - - - - -google.com report — alerts.example.com — SelfPost mockups - - - - - -
-
-

google.com · 14 Aug

-

- alerts.example.com - 88 pass - 19 fail -

- ← All DMARC reports -
-
-
-

Report

-
-
Reportergoogle.com
-
Report id3178944098765432109
-
Window2026-08-14 00:00 – 2026-08-15 00:00 UTC
-
Received2026-08-15 04:40 UTC
-
Contactnoreply-dmarc-support@google.com
-
-
-
-

Published policy

-
-
Domainalerts.example.com
-
p / sp / pctnone / none / 100
-
adkim / aspfr / r
-
ruadmarc@mail.example.org
-
-

A third-party source is not in SPF or DKIM. Do not tighten p= until that sender is gone or aligned.

-
-
-
-
-

Records

- - - - - - - - - - - - - - - - - - - - -
SourceCountDispositionSPFDKIMHeader from
203.0.113.10 (this relay)88nonepasspassalerts.example.com
198.51.100.80 (unknown)19nonefailfailalerts.example.com
-
    -
  • -
    - this relay · 88pass - 203.0.113.10 · SPF pass · DKIM pass -
    -
  • -
  • -
    - unknown · 19fail - 198.51.100.80 · SPF fail · DKIM fail -
    -
  • -
-

Parsed from the aggregate XML. Forensic (ruf=) samples are not stored.

-
-
-
- - - diff --git a/docs/assets/panel-ui/dmarc-report.html b/docs/assets/panel-ui/dmarc-report.html deleted file mode 100644 index fb376d7..0000000 --- a/docs/assets/panel-ui/dmarc-report.html +++ /dev/null @@ -1,88 +0,0 @@ - - - - - -google.com report — example.com — SelfPost mockups - - - - - -
-
-

google.com · 14 Aug

-

- example.com - 412 pass - 6 fail -

- ← Back to example.com -
-
-
-

Report

-
-
Reportergoogle.com
-
Report id3178944012345678901
-
Window2026-08-14 00:00 – 2026-08-15 00:00 UTC
-
Received2026-08-15 06:12 UTC
-
Contactnoreply-dmarc-support@google.com
-
-
-
-

Published policy

-
-
Domainexample.com
-
p / sp / pctnone / none / 100
-
adkim / aspfr / r
-
ruadmarc@mail.example.org
-
-

p=none does not affect delivery. Failures here are forwarding through Google, not this relay.

-
-
-
-
-

Records

- - - - - - - - - - - - - - - - - - - - -
SourceCountDispositionSPFDKIMHeader from
203.0.113.10 (this relay)412nonepasspassexample.com
66.102.1.44 (google.com)6nonefailfailexample.com
-
    -
  • -
    - this relay · 412pass - 203.0.113.10 · SPF pass · DKIM pass -
    -
  • -
  • -
    - google.com · 6fail - 66.102.1.44 · SPF fail · DKIM fail -
    -
  • -
-

Parsed from the aggregate XML. Forensic (ruf=) samples are not stored.

-
-
-
- - - diff --git a/docs/assets/panel-ui/dmarc.html b/docs/assets/panel-ui/dmarc.html deleted file mode 100644 index a7478b5..0000000 --- a/docs/assets/panel-ui/dmarc.html +++ /dev/null @@ -1,91 +0,0 @@ - - - - - -DMARC reports — SelfPost mockups - - - - - -
-
-

DMARC reports candidate

-
-

Aggregate reports SelfPost accepted for rua=. Forensic (ruf=) is out of scope. Open a report for the parsed XML — not a dashboard.

-
-
-

Ingest ok

-

Last report 6 hours ago. Port 25 accepts only the configured report address.

-
-
-

This week

-

14 kept, 0 parse failures. Older summaries are pruned.

-
-
-
-
-

Recent reports

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ReceivedDomainReporterWindowPassFail
2026-08-15 06:12example.comgoogle.com14 Aug4126View
2026-08-15 04:40alerts.example.comgoogle.com14 Aug8819View
2026-08-14 06:08example.comgoogle.com13 Aug3902View
- -

Each row is one aggregate XML SelfPost parsed. Domain names open the roll-up for that sending domain.

-
-
-
- - - diff --git a/docs/assets/panel-ui/domain-delete.html b/docs/assets/panel-ui/domain-delete.html deleted file mode 100644 index 1a08e1f..0000000 --- a/docs/assets/panel-ui/domain-delete.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - - -Delete example.com — SelfPost mockups - - - - - -
-
-

Delete example.com

- ← Back to example.com -
-
-
-

Confirm deletion

-

You are about to delete example.com. This will:

-
    -
  • permanently delete its DKIM signing key;
  • -
  • delete all 2 bound applications, including their SASL credentials;
  • -
  • reload OpenDKIM so the domain is no longer signed.
  • -
-

This cannot be undone.

-
-
-
-
- - - diff --git a/docs/assets/panel-ui/domain.html b/docs/assets/panel-ui/domain.html deleted file mode 100644 index 3c75277..0000000 --- a/docs/assets/panel-ui/domain.html +++ /dev/null @@ -1,260 +0,0 @@ - - - - - -example.com — SelfPost mockups - - - - - -
-
-

example.com

- ← All domains -
-
-
-
-

New application password

- -
-

Shown once only and not stored. Copy it now.

- -
newsletter
- -
xK.9fQ2m-pL7wR
-
-
-
-
-
-

DNS status ok

- -
-
-
-
- -
-
- - mail._domainkey.example.com -
-
- - TXT -
-
-
-
- -
-
- - example.com -
-
- - TXT -
-
-
-
-
-
- -
-
- - _dmarc.example.com -
-
- - TXT -
-
-

p=none; rua points at SelfPost ingest.

-
-
- -

Not required (rua= is on a domain SelfPost accepts).

-
-
-
-
-
-
-
-
-
-

DKIM and SPF records

- -
-

DKIM

-
-
- -
mail._domainkey.example.com
-
-
- - TXT -
-
- -
v=DKIM1; k=rsa; p=MIIBIjANBgkqh…
-

SPF

-
v=spf1 ip4:203.0.113.10 -all
-
-
-
-

DMARC record

- -
-
-
- -
_dmarc.example.com
-
-
- - TXT -
-
- -
v=DMARC1; p=none; rua=mailto:dmarc@mail.example.org
-

Open DMARC reports for this domain.

-
-
-
-
-
-

Connection settings

- -
- -
mail.example.org
- - 465 — SSL/TLS (implicit) -587 — STARTTLS (submission) -
-
-
-

Add an application

- -
-
- - - - -
-
-
-
-
-
-
-

Applications

- -
-
    -
  • - -

    Any address of the domain — *@example.com

    -
    - - - - -
    -
    -
    -

    Address mode

    - -
    -
    -

    Trusted-IP override active

    - -
    -
    - - -
    -
    - - -
    -
    -
    -
    -
    -
    -
  • -
  • - -

    Fixed list — invoices@example.com

    -
    - - - -
    -
  • -
-
-
-
-
-
-

DMARC reports

- -
- - -
-
-
-
-

Level-2 rate limit active

-
-
-
- - -
-
- - -
-
-
-
-
-
-
-
-

Export domain

- -
-

Secret file — transfer securely, or encrypt as .spde.

-
- -
- - -
-
-
-
-
-

Danger zone

-

Deletes the DKIM key and every application on this domain.

- -
-
-
- - - diff --git a/docs/assets/panel-ui/domains.html b/docs/assets/panel-ui/domains.html deleted file mode 100644 index 4c41a02..0000000 --- a/docs/assets/panel-ui/domains.html +++ /dev/null @@ -1,53 +0,0 @@ - - - - - -Domains — SelfPost mockups - - - - - -
-

Domains

-
-
-

Domains

-
- -
- - -
-
- - - - - - - - - - - - - - - - - - -
DomainDNSSelectorApps
example.comokmail2Delete
alerts.example.comwarnmail1Delete
- -

The DNS badge is the worst of DKIM, SPF and DMARC. Open a domain for details.

-
-
-
- - - diff --git a/docs/assets/panel-ui/help.html b/docs/assets/panel-ui/help.html deleted file mode 100644 index 9d64b90..0000000 --- a/docs/assets/panel-ui/help.html +++ /dev/null @@ -1,46 +0,0 @@ - - - - - -Help — SelfPost mockups - - - - - -
-
-

Help candidate

-
-

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.

-
-
-

On this panel

-
    -
  • Machine — kernel counters and the rate window
  • -
  • TLS certificate — port 465, reverse-proxy mount
  • -
  • Hostname / reverse DNS — forward-confirmed PTR at the hosting provider
  • -
  • Mail queue retries — time-based, no attempt budget
  • -
  • Inbound — not mailboxes; listed recipients or any address at the domain
  • -
  • Domain page — DNS, records, connection, applications, export (drawer from each card’s «?»)
  • -
-

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.

-
-
-

Machine

-

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”.

-
-
-

TLS certificate

-

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.

-
-
-

Hostname / reverse DNS

-

Forward-confirmed reverse DNS: the A/AAAA for SELFPOST_HOSTNAME 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.

-
-
-
- - - diff --git a/docs/assets/panel-ui/inbound-backup.html b/docs/assets/panel-ui/inbound-backup.html deleted file mode 100644 index bf0b1f6..0000000 --- a/docs/assets/panel-ui/inbound-backup.html +++ /dev/null @@ -1,92 +0,0 @@ - - - - - -backup.example.net — SelfPost mockups - - - - - -
-
-

backup.example.net

- ← All inbound domains -
-
-
-

DNS status error

-

Cached a few minutes — use Re-check after publishing. Unlike outbound, inbound needs an MX pointing at this server.

- -
-
- - backup.example.net -
-
- - MX -
-
- - 10 mail.primary.example.net. -

No MX points at mail.example.org (this server). Publish the record below, or wait for DNS to propagate and Re-check.

-
-
-
-
-
-

Upstream

-

Where accepted mail is handed off. Not a mailbox.

- - - - -
-
-
-

MX record to publish

-

Add this MX so the internet delivers here. Keep any existing primary MX if this is backup-MX.

-
-
- -
backup.example.net
-
-
- - MX -
-
- -
20 mail.example.org.
-
-
-
-
-

Valid recipients

-

Who this domain accepts on port 25. Same idea as an application’s address mode: a list, or any address at the domain.

- - -
- - -

Unknown recipients are rejected at RCPT so this relay does not generate backscatter.

-
-

Every address at this domain is accepted and forwarded. Prefer a list unless the upstream rejects unknowns — otherwise this relay may generate backscatter.

-
-
-
-

Danger zone

-

Stops accepting mail for this domain. Does not touch outbound sending domains.

- -
-
-
- - - diff --git a/docs/assets/panel-ui/inbound-delete.html b/docs/assets/panel-ui/inbound-delete.html deleted file mode 100644 index b469692..0000000 --- a/docs/assets/panel-ui/inbound-delete.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - - -Delete lists.example.com — SelfPost mockups - - - - - -
-
-

Delete lists.example.com

- ← Back to lists.example.com -
-
-
-

Confirm deletion

-

You are about to stop accepting inbound mail for lists.example.com. This will:

-
    -
  • remove it from relay_domains and the recipient map;
  • -
  • stop forwarding to 10.0.0.8:25;
  • -
  • leave outbound sending domains untouched.
  • -
-

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.

-
-
-
-
- - - diff --git a/docs/assets/panel-ui/inbound-domain.html b/docs/assets/panel-ui/inbound-domain.html deleted file mode 100644 index 199d8f1..0000000 --- a/docs/assets/panel-ui/inbound-domain.html +++ /dev/null @@ -1,94 +0,0 @@ - - - - - -lists.example.com — SelfPost mockups - - - - - -
-
-

lists.example.com

- ← All inbound domains -
-
-
-

DNS status ok

-

Cached a few minutes — use Re-check after publishing. Unlike outbound, inbound needs an MX pointing at this server.

- -
-
- - lists.example.com -
-
- - MX -
-
- - 10 mail.example.org. -20 mail.primary.example.net. -

An MX points at mail.example.org (this server). Other MX values are the domain’s own primaries — they are not an error.

-
-
-
-
-
-

Upstream

-

Where accepted mail is handed off. Not a mailbox.

- - - - -
-
-
-

MX record to publish

-

Add this MX so the internet delivers here. Keep any existing primary MX if this is backup-MX.

-
-
- -
lists.example.com
-
-
- - MX -
-
- -
10 mail.example.org.
-
-
-
-
-

Valid recipients

-

Who this domain accepts on port 25. Same idea as an application’s address mode: a list, or any address at the domain.

- - -
- - -

Unknown recipients are rejected at RCPT so this relay does not generate backscatter.

-
-

Every address at this domain is accepted and forwarded. Prefer a list unless the upstream rejects unknowns — otherwise this relay may generate backscatter.

-
-
-
-

Danger zone

-

Stops accepting mail for this domain. Does not touch outbound sending domains.

- -
-
-
- - - diff --git a/docs/assets/panel-ui/inbound.html b/docs/assets/panel-ui/inbound.html deleted file mode 100644 index 07e15e7..0000000 --- a/docs/assets/panel-ui/inbound.html +++ /dev/null @@ -1,58 +0,0 @@ - - - - - -Inbound — SelfPost mockups - - - - - -
-
-

Inbound 1.x

-
-

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.

-
-
-

Forwarding

-
- -
- - -
-
- - - - - - - - - - - - - - - - - - - - -
DomainDNSUpstreamRecipientsTLS
lists.example.comok10.0.0.8:2512 listedrequiredDelete
backup.example.neterror192.0.2.20:25anyoffDelete
- -

The DNS 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 Re-check button.

-
-
-
- - - diff --git a/docs/assets/panel-ui/index.html b/docs/assets/panel-ui/index.html deleted file mode 100644 index c35e09c..0000000 --- a/docs/assets/panel-ui/index.html +++ /dev/null @@ -1,188 +0,0 @@ - - - - - -SelfPost — макеты панели - - - - -
- -
-

макеты · не панель

-

SelfPost — полное обновление интерфейса

-

Проект системы — как собирать экраны, чтобы не чинить пустые колонки, высоту полей и ряды кнопок по одному. Каждый экран — отдельная страница, не простыня с якорями. Знак и палитра из листа утверждения не меняются. HTML в docs/assets/panel-ui/, не вёрстка internal/web.

- -
- -
-

Система, не рескин

-

Первый click-through повторил сетку живой панели, поэтому те же сбои чинились по одному. Грамматика задаёт регионы (measure / pair / fill), карточку со слотом справки, field-row (Host и Type одной высоты) и actions-row. Макеты собраны только из этого: один экран — один HTML-файл.

-
- -
-

Ширина окна

-

Сейчас оболочка центрируется, колонка контента упирается в 64rem, формы ещё уже — 48rem. На широком мониторе поля пустые. Растянуть поля на 100% окна нельзя — они становятся нечитаемыми. Две независимые карточки рядом — можно.

-

Гибрид

-

Навигация прижата влево. Ops-страницы (Status, Deliveries, очередь, лог, Backup, Settings, DMARC, inbound) занимают остаток до 90rem (~1440px). Backup — полный бэкап ‖ импорт. Settings — учётные данные ‖ DMARC (у domain-admin остаётся одна узкая карточка). Таблица Users — fill. Одиночные формы (подтверждения, форма пользователя) остаются 42rem и выровнены влево. Login и setup — по-прежнему узкий центрированный блок.

-
-
-
сейчас, окно ~1600px
-
- nav - 64rem - пусто -
-
-
-
макет, ops-страница
-
- nav - контент до 90rem -
-
-
-
макет, форма
-
- nav - 42rem - поле не растягивает инпуты -
-
-
-
На телефоне это не «узкий десктоп». Таблицы из шести колонок складываются в список. Навигация — выезжающая колонка на CSS-checkbox, без обязательного JavaScript (как панели Edit у приложений уже сейчас).
-
- -
-

Информационная архитектура

-
-
-

Global administrator

-
    -
  • Status
  • -
  • Domains → domain → delete
  • -
  • Deliveries → message
  • -
  • Mail queue
  • -
  • System log
  • -
  • Inbound 1.x — скрыт, пока INBOUND_RELAY_ENABLE выключен
  • -
  • DMARC reports candidate
  • -
  • Backup, Users, Settings
  • -
  • Help candidate — пункт навбара и drawer с карточек Status и страницы домена
  • -
-
-
-

Domain-admin

-
    -
  • Domains (только назначенные) → domain → DMARC reports этого домена
  • -
  • Deliveries → message, включая пояснение retry
  • -
  • Settings — только учётные данные панели
  • -
  • Help drawer
  • -
  • Нет Status, очереди, system log, Backup, Users, inbound, глобального DMARC hub
  • -
-
-
-

В прототипе роль, inbound-флаг, ширина и тема переключаются в шапке. contributing и schema-squash экранов не имеют.

-
- -
-

Знак и новые иконки

-

Утверждённый stamp не заменяется. Для узкой шапки телефона предлагается уже существующий SP-значок (фавикон), а не компактный wordmark: на 390px высота важнее, чем прочитать «SelfPost» второй раз.

-
-
- Полный знак -
эталон · login / setup
-
-
- Компактный знак -
колонка навбара
-
-
- SP -
телефон · шапка drawer
-
-
-

Иконки новых пунктов

-

Тот же штрих 1.5, что у Status / Domains. Не пиктограммы-эмодзи и не заливка.

-
-
- - Inbound -
-
- - DMARC -
-
- - Help -
-
-
- -
-

Экраны

-

Система

- -

Вход

- -

Текущая панель

- -

Roadmap

- -

Аварийный телефон

- -
- -
-

Как смотреть

-

Откройте любой экран из списка — это отдельная страница, не якорь в одном файле. Шапка прототипа — не часть панели: роль, Desktop/Phone, inbound, светлая/тёмная. Копирайт экранов английский, как в продукте. Drawer Help открывается с «?» на Status или с карточек домена; на карточках остаются показания и колонка Detail.

-

После утверждения макетов вёрстка panel.css и шаблонов — отдельная задача. CSP и progressive enhancement в этом HTML не воспроизводятся один в один: здесь допустимы вещи, которые в панели останутся в файле стилей.

-
- -
- - diff --git a/docs/assets/panel-ui/login.html b/docs/assets/panel-ui/login.html deleted file mode 100644 index 0f5b2a0..0000000 --- a/docs/assets/panel-ui/login.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - - -Sign in — SelfPost mockups - - - - - -
- SelfPost -

Sign in

-
-
- - - - -
-
-
-

© Mixeme · License (AGPL-3.0)

-
- - - diff --git a/docs/assets/panel-ui/mail-queue.html b/docs/assets/panel-ui/mail-queue.html deleted file mode 100644 index b2a234e..0000000 --- a/docs/assets/panel-ui/mail-queue.html +++ /dev/null @@ -1,54 +0,0 @@ - - - - - -Mail queue — SelfPost mockups - - - - - -
-

Mail queue

-
-
-

How delivery retries work

-

This Postfix’s policy, read once at panel start. There is no maximum attempt count — only time.

-
-
First retry5 minutes
-
Later retriesdoubling, cap 1 h 7 min
-
Kept in queue5 days
-
Thenbounced
-
-
-
-
-
-

Pending messages

- - - - - - - -
Queue idAgeFromToSize
4C3A1E2F1A18 minbilling@example.comada@example.net12 KiB
4C3A1E301011 minnews@example.compat@slow.example48 KiB
4C3A1E31024 minbilling@example.comada@example.net9 KiB
- - - -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. -
-
-
- - - diff --git a/docs/assets/panel-ui/mock.css b/docs/assets/panel-ui/mock.css deleted file mode 100644 index e194093..0000000 --- a/docs/assets/panel-ui/mock.css +++ /dev/null @@ -1,715 +0,0 @@ -/* 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); } diff --git a/docs/assets/panel-ui/settings.html b/docs/assets/panel-ui/settings.html deleted file mode 100644 index 981c04f..0000000 --- a/docs/assets/panel-ui/settings.html +++ /dev/null @@ -1,73 +0,0 @@ - - - - - -Settings — SelfPost mockups - - - - - -
-

Settings

-
-
-
-

Panel credentials

-

These are the credentials for this control panel only. Applications keep their own logins and passwords, which are not affected.

- - - - -
-

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.

-
-
-

DMARC aggregate reports

-

Default rua= for every sending domain (overridable per domain). When ingest is on, this can be an address SelfPost accepts.

- - -

When rua= points at another domain, that hub must publish a report-authorisation record. DMARC reports in the panel.

-
-
- -
mail.example.org._report._dmarc.example.com
-
-
- - TXT -
-
- -
v=DMARC1;
- -

Published at mail.example.org._report._dmarc.example.com — aggregate reports addressed to dmarc@mail.example.org are authorised.

-
-
-
-
-
-

Sending rate limits

-

Level 1 is set in Compose; restart the container to change it. Domain and application ceilings live on each domain’s page.

-
-
-

Level 1 — per client IP

- 100 messages / 60 seconds -

RATE_LIMIT_MESSAGES_PER_IP / RATE_LIMIT_WINDOW_SECONDS. Hard ceiling for every connecting IP; the panel cannot raise a domain or application limit above this.

-
-
-

Level 2 — domain

-

Optional ceiling for all senders on a domain. When unset, only level 1 applies. Must be ≤ level 1.

-
-
-

Level 2 — application

-

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).

-
-
-
-
-
- - - diff --git a/docs/assets/panel-ui/setup.html b/docs/assets/panel-ui/setup.html deleted file mode 100644 index 9931c5d..0000000 --- a/docs/assets/panel-ui/setup.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - -Create administrator — SelfPost mockups - - - - - -
- SelfPost -

Create administrator

-
-

This one-time link creates the single panel administrator. After you submit, the link stops working for good.

-
- - - - - - -
-
-
-
- - - diff --git a/docs/assets/panel-ui/shell.js b/docs/assets/panel-ui/shell.js deleted file mode 100644 index b32ef7c..0000000 --- a/docs/assets/panel-ui/shell.js +++ /dev/null @@ -1,276 +0,0 @@ -/* 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", - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' - ); - - 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 = - '"; - - var sprite = - '"; - - function icon(id) { - return ''; - } - - var navHtml = - '"; - - var phone = - '
' + - '" + - '' + - '' + - '' + - "
"; - - var help = - '' + - '"; - - var app = document.createElement("div"); - app.className = "app"; - app.innerHTML = navHtml + '
' + phone + "
"; - 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(); -})(); diff --git a/docs/assets/panel-ui/status.html b/docs/assets/panel-ui/status.html deleted file mode 100644 index 39c2f30..0000000 --- a/docs/assets/panel-ui/status.html +++ /dev/null @@ -1,125 +0,0 @@ - - - - - -Status — SelfPost mockups - - - - - -
-
-

Status

- -
- -
-
-

Overall warn

-

Running, with warnings below.

-
-
- -
-
-

Mail queue warn

-

3 Kbytes in 3 Requests.

- -
-
-

TLS certificate ok

- - 2026-11-02 12:00 UTC -

Valid for another 78 day(s).

-
-
- -
-
-

Milter sockets ok

- - - - - - -
MilterStateDetail
OpenDKIMokListening
send-logokListening
-
-
-

Hostname and reverse DNS ok

- - mail.example.org - - 203.0.113.10 → mail.example.org -

mail.example.org resolves to 203.0.113.10 and the reverse lookup points back at it.

-
-
-
- -
-
-

Inbound warn

-

INBOUND_RELAY_ENABLE is on. Port 25 accepts mail for 2 domains and forwards it upstream — not to local mailboxes.

-

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.

-

Inbound domains

-
-
- -
-
-

Machine ok

- - - - - - - - - - - - - - - - - - - -
ResourceUsageDetail
CPU12% 12%4 cores · 4 threads
Memory41% 41%1.6 GiB used of 4.0 GiB.
Network↓ 2.0 KiB/s
↑ 1.0 KiB/s
eth0: 1.0 MiB in, 512.0 KiB out
-
-
-

Processes ok

- - - - - - - - - - -
ProgramStateDetail
opendkimRUNNINGpid 21, uptime 3 days, 4:12:01
panelRUNNINGpid 18, uptime 3 days, 4:12:03
postfixRUNNINGpid 42, uptime 3 days, 4:11:58
postfix-reloadSTOPPEDNot started
cert-reloadSTOPPEDNot started
logrotateSTOPPEDNot started
-
-
- -
-
-

Configuration

-

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.

-
-
-
-

SelfPost 1.2.3 · © Mixeme · License (AGPL-3.0)

-
- - - diff --git a/docs/assets/panel-ui/system-log.html b/docs/assets/panel-ui/system-log.html deleted file mode 100644 index fbdda11..0000000 --- a/docs/assets/panel-ui/system-log.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - -System log — SelfPost mockups - - - - - -
-

System log

-
-
-

Recent log entries

- 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) -
-
-
- - - diff --git a/docs/assets/panel-ui/system.css b/docs/assets/panel-ui/system.css deleted file mode 100644 index f4cf77a..0000000 --- a/docs/assets/panel-ui/system.css +++ /dev/null @@ -1,128 +0,0 @@ -/* 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; } diff --git a/docs/assets/panel-ui/system.html b/docs/assets/panel-ui/system.html deleted file mode 100644 index 02a4f62..0000000 --- a/docs/assets/panel-ui/system.html +++ /dev/null @@ -1,298 +0,0 @@ - - - - - -SelfPost — система панели - - - - - -
- -
-

система · не рескин

-

SelfPost — грамматика интерфейса

-

Это проект системы. Экраны складываются из регионов и контролов. Запрещённые состояния нельзя «починить паддингом» — их не из чего собрать. Знак, кирпич и IBM Plex не меняются. Живая панель (internal/web) пока не трогается.

- - -
- -
-

Зачем

-

Первый макет скопировал грамматику живой панели: страница помечается ops или form, карточки стопкой, пара полей — два независимых .code, Save внутри <form>, Delete снаружи. Дальше каждая мелочь чинилась отдельно — как в panel.css годами. Новый интерфейс должен не давать собрать пустую колонку, разные высоты Host/Type и кнопки на двух строках.

-

Три оси, которые больше не выбираются «на глаз» у каждой страницы:

-
    -
  1. Регион — как блок занимает ширину окна.
  2. -
  3. Карточка — заголовок, справка, тело, действия.
  4. -
  5. Контрол — одно поле или пара полей одной высоты.
  6. -
-
- -
-

Регионы

-

Страница — это stack регионов, не класс на <main>. Оболочка одна: навбар прижат влево, контент забирает остаток. Навбар не прыгает, когда меняется содержимое.

- - - - - - - - - - - - - - - - - - - - - - - - -
РегионШиринаКогда
measureдо 42rem, влевоОдна читаемая форма: login, setup, подтверждение удаления, форма пользователя. Не «вся страница Settings».
pairдве колонки до 90remДве равноправные задачи на одном экране. Один ребёнок — сам сжимается в measure (domain-admin без DMARC).
fillдо 90remТаблица, лог, DNS-статус, список приложений. Ячейки не переносят однострочные значения; карточка скроллится по X.
stackколонка с зазоромВертикальный порядок регионов. Зазор даёт gap, не .card + .card.
- -
-
Нельзя — страница-форма, две задачи стопкой
-
-
-

Full backup

-

Карточка узкая. Справа пустое поле на 1600px.

- -
-
-

Import a domain

-

Вторая задача под первой — та же пустота.

- -
-
-
- -
-
Надо — pair
-
-
-

Full backup

-

Секрет. Шифрование — внутри карточки, поля не растягиваются на 1440px.

-
-
-
-

Import a domain

-

Тот же экран, вторая задача. Не «ещё одна форма ниже».

-
-
-
-
-
- -
-

Карточка

-

Один хром: card-head (заголовок + слот «?»), тело, при необходимости actions-row. Справка — часть хрома, её не расставляют после того, как карточки уже собраны. Нет «?» — слот пустой, заголовки соседних карточек всё равно на одной линии.

-

Показания (очередь, PTR, CPU) остаются на карточке. В drawer уходит только то, чего на карточке быть не должно: что такое kernel counter, зачем PTR у провайдера, почему пароль показывают один раз.

-
-
Хром карточки
-
-
-
-

DNS status ok

- ? -
-

Чтение и Re-check здесь. Абзац «зачем MX» — в справке.

-
-
-
-

Danger zone

-
-

Без «?»: действие очевидное. Карточка всё равно пара к форме, не на всю ширину.

- -
-
-
-
- -
-

Контролы

-

Одно поле — field (подпись + контроль). Два поля в ряд — field-row: подписи в первой сетке-строке, значения во второй. Высота значений общая, потому что это одна строка грида, а не два блока с подобранным padding. Баг «Type ниже Host» (b0ebe06) из этой разметки не собирается.

-

field-row с узкой второй колонкой — Host / name ‖ Type. field-row equal — два равноправных инпута (лимит и окно).

- -
-
field-row — Host ‖ Type, с Copy и без
-
-
-

With Copy

-
-
- -
- mail._domainkey.example.com - -
-
-
- - TXT -
-
-
-
-

Lookup, no Copy

-
-
- - lists.example.com -
-
- - MX -
-
-
-
-
- -
-
field-row equal — лимит ‖ окно
-
-
-

Level-2 rate limit

-
-
- - -
-
- - -
-
-
-
-
-
- -
-

Действия

-

Primary, secondary и danger — всегда actions-row. Для вёрстки неважно, POST это или переход на confirm: ряд один. Форма либо оборачивает всю карточку, либо кнопки несут form=. Нельзя оставить Submit внутри блочной формы, а Delete — следующим соседом: блок формы занимает строку целиком.

-
-
Save и Delete в одном ряду
-
-
-

Edit user

- - -
- - Delete user -
-
-
-
-
- -
-

Нельзя

-

Если хочется добавить правило «только на этой странице» — сначала проверить, какого региона не хватило.

-
    -
  • Класс на main (ops / form) как способ выбрать ширину. Ширину выбирает регион.
  • -
  • Одиночная карточка на 90rem с двумя полями ввода. Это measure или pair.
  • -
  • Danger zone отдельным fill. Она вторая колонка последней пары (получатели ‖ удалить, экспорт ‖ удалить).
  • -
  • Два .code рядом с разным padding, чтобы «почти совпало». Только field-row.
  • -
  • Submit внутри <form>, danger-ссылка после </form>.
  • -
  • Перенос однострочного поля, адреса, статуса, hostname. nowrap + горизонтальный скролл карточки.
  • -
  • Status из одних бейджей без Detail / без строки очереди / без PTR.
  • -
  • Телефон как уменьшенный десктоп с шестиколоночной таблицей. Таблица → список; pair → одна колонка; field-row остаётся парой.
  • -
  • Копировать разметку internal/web/view/templates «как есть» в макет. Рецепт экрана — ниже, не шаблон Go.
  • -
-
- -
-

Рецепты экранов

-

Экраны — отдельные HTML-файлы рядом с этой спецификацией. Собираются только так:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ЭкранСтек регионов
Login / setupцентр оболочки, measure 24rem, полный stamp
Statusstack: overall → pair очередь‖TLS → pair milters‖PTR → inbound если есть → pair machine‖processes → configuration. На карточках остаются числа и Detail.
Domains / Inbound / Deliveries / log / queuefill таблица. Добавление домена — поле в той же карточке списка (measure внутри, не вторая карточка и не инпут на 90rem).
Domainfill DNS status → pair DKIM/SPF ‖ DMARC → pair connection ‖ add app → fill applications → pair domain settings (две половины) → pair export ‖ danger. «?» на рабочих карточках.
Inbound domainfill MX DNS → pair upstream ‖ MX to publish → pair recipients ‖ danger
DMARChub: pair ingest ‖ this week, затем fill список отчётов. Домен: roll-up (pair 7 days ‖ third-party, fill reports + sources). Один XML: просмотр — pair report ‖ policy, fill records. Не дашборд, не ruf=.
Backuppair full backup ‖ import
Settingsglobal: pair credentials ‖ DMARC, затем fill rate limits. Domain-admin: один ребёнок в pair → сам measure
Usersfill таблица. Create/Edit — measure + actions-row
Confirm deletemeasure одна карточка
-
- -
-

Телефон

-

Аварийный доступ, не продукт. pair складывается в одну колонку. Таблица заменяется списком (как сейчас в прототипе). field-row не складывается: Type — токен. Навбар — выезжающая колонка на checkbox, без обязательного JS. Знак в шапке — SP-иконка, не второй wordmark.

-
- -
-

Как внедрять

-
    -
  1. Новый экран или правка макета — только классы из system.css (stack, pair, measure, fill, field-row, actions-row).
  2. -
  3. Не добавлять исключения в mock.css «для этой страницы», если это ширина, высота пары полей или ряд кнопок.
  4. -
  5. Click-through — отдельные страницы (status.html, domain.html, …), не простыня с якорями. Оболочка общая: shell.js.
  6. -
  7. Вёрстка internal/web — отдельная задача после утверждения системы, не параллельный рескин шаблонов.
  8. -
-

Классы живут в docs/assets/panel-ui/system.css. Этот файл — спецификация. Экраны — status.html и соседние страницы; оглавление — index.html.

-
- -
- - diff --git a/docs/assets/panel-ui/user-delete.html b/docs/assets/panel-ui/user-delete.html deleted file mode 100644 index 80daf4f..0000000 --- a/docs/assets/panel-ui/user-delete.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - -Delete ops-alerts — SelfPost mockups - - - - - -
-
-

Delete ops-alerts

- ← Back to ops-alerts -
-
-
-

Confirm deletion

-

You are about to delete the panel user ops-alerts. A signed-in session for this user stops working immediately.

-
-
-
-
- - - diff --git a/docs/assets/panel-ui/user-form.html b/docs/assets/panel-ui/user-form.html deleted file mode 100644 index 5d43347..0000000 --- a/docs/assets/panel-ui/user-form.html +++ /dev/null @@ -1,42 +0,0 @@ - - - - - -Edit user — SelfPost mockups - - - - - -
-
-

Edit user

- ← Back to users -
-
-
-
- - - - - - -
- Assigned domains -

Required for domain administrators.

- - -
-
- - Delete user -
-
-
-
-
- - - diff --git a/docs/assets/panel-ui/users.html b/docs/assets/panel-ui/users.html deleted file mode 100644 index 37bbe3b..0000000 --- a/docs/assets/panel-ui/users.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - -Users — SelfPost mockups - - - - - -
-

Users

-
-
-

Create user

- - - - - - -
UsernameRoleDomains
adminGlobalAllEdit
ops-alertsDomain adminalerts.example.comEdit
-
-
-
- - -