From 5e0adb2de573e272efe26b5d0c6e08dbd815b708 Mon Sep 17 00:00:00 2001 From: Mix Date: Mon, 21 Sep 2026 22:02:12 +0300 Subject: [PATCH] docs: record the owner's decisions on routes, compatibility and inbound roles panel-redesign.md gains four sections. Routes: paths follow the new names and structure (/overview, /outbound/..., /inbound/..., /server/..., /account), with one /fragment name, the application form as a single POST and TestRoutesFollowNavigation. No compatibility: the redesign is 2.0.0, old paths are removed without redirects, the migration chain is replaced by one baseline and 2.0 starts from an empty data directory (the only live installation is the owner's); schema-squash ships as a step of this plan without its upgrade gate. Who sees what: a domain administrator manages assigned inbound domains, granted separately from outbound (user_inbound_domains, per-domain authorization under /inbound/). Room for roadmap items: where antispam lists, quarantine and preflight land, and that a feature's screens are drawn mockup-first in this system. Mockups: the Reload configuration box the first pass missed (now on Server > Health), the flash component and the sp-help link that replaces the help drawer, and the user form with two assignment lists. The antispam plan no longer points its mockup at the superseded panel-ui folder. Checklist 3/21. Co-Authored-By: Claude Fable 5.1 --- CHANGELOG.md | 19 +++ docs/assets/panel-redesign/panel/account.html | 2 +- docs/assets/panel-redesign/panel/check.py | 5 +- .../panel-redesign/panel/components.html | 7 +- docs/assets/panel-redesign/panel/health.html | 8 + .../panel-redesign/panel/in-domain.html | 2 +- .../panel-redesign/panel/in-domains.html | 2 +- .../panel/out-domain-settings.html | 2 +- .../panel-redesign/panel/out-domain.html | 4 +- .../assets/panel-redesign/panel/outlines.json | 6 +- .../assets/panel-redesign/panel/overview.html | 2 +- .../panel-redesign/panel/src/account.html | 2 +- .../panel-redesign/panel/src/components.html | 7 +- .../panel-redesign/panel/src/health.html | 8 + .../panel-redesign/panel/src/in-domain.html | 2 +- .../panel-redesign/panel/src/in-domains.html | 2 +- .../panel/src/out-domain-settings.html | 2 +- .../panel-redesign/panel/src/out-domain.html | 4 +- .../panel-redesign/panel/src/overview.html | 2 +- .../panel-redesign/panel/src/user-delete.html | 14 +- .../assets/panel-redesign/panel/src/user.html | 22 ++- .../panel-redesign/panel/src/users.html | 12 +- docs/assets/panel-redesign/panel/theme.css | 8 + .../panel-redesign/panel/user-delete.html | 14 +- docs/assets/panel-redesign/panel/user.html | 22 ++- docs/assets/panel-redesign/panel/users.html | 12 +- docs/plans/inbound-antispam-panel.md | 9 +- docs/plans/panel-redesign.md | 153 ++++++++++++++++-- docs/roadmap.md | 17 +- 29 files changed, 293 insertions(+), 78 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91de086..c0a591b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,25 @@ document is not part of the release. ### Added +- Panel redesign, two owner decisions recorded in + `docs/plans/panel-redesign.md`: **no compatibility** — the redesign is + `2.0.0`, old paths are removed without redirects, the SQLite migration chain + is replaced by one baseline and 2.0 starts from an empty data directory + (schema-squash ships as a step of this plan, without its upgrade gate; the + only live installation is the owner's); and + **inbound delegation** — a domain administrator manages assigned inbound + domains, granted separately from outbound (`user_inbound_domains`, a second + list on the user form, per-domain authorization under `/inbound/`). Mockups `users`, `user`, + `user-delete` redrawn for the two lists. Checklist `3/21`. +- Panel redesign, routes: the owner decided that paths follow the new names + and structure. `docs/plans/panel-redesign.md` § Routes maps every screen to + its new path (`/overview`, `/outbound/…`, `/inbound/…`, `/server/…`, + `/account`) with the rules that come along — one `/fragment` name, the + application form as a single POST, 301s from old GET paths for one MINOR, + subtree `requireGlobal()` under security review, and + `TestRoutesFollowNavigation`. Mockups gained what the first pass missed: the + *Reload configuration* box (now on Server › Health), the `flash` component + and the `sp-help` link that replaces the help drawer. Checklist `1/17`. - Panel redesign, design contract: [docs/plans/panel-redesign.md](docs/plans/panel-redesign.md) rewritten for the accepted direction — three layers (Bulma as shipped, adapters, own `sp-` diff --git a/docs/assets/panel-redesign/panel/account.html b/docs/assets/panel-redesign/panel/account.html index f617ef4..bff95f8 100644 --- a/docs/assets/panel-redesign/panel/account.html +++ b/docs/assets/panel-redesign/panel/account.html @@ -93,7 +93,7 @@

What happens

Changing the password signs out every other session of this account. This one stays signed in.

-

A domain administrator sees this same page, and nothing under Server.

+

A domain administrator sees this same page, the Outbound and Inbound domains assigned to them, and nothing under Server.

diff --git a/docs/assets/panel-redesign/panel/check.py b/docs/assets/panel-redesign/panel/check.py index d099d78..34cfb72 100644 --- a/docs/assets/panel-redesign/panel/check.py +++ b/docs/assets/panel-redesign/panel/check.py @@ -25,7 +25,7 @@ VOID = {"input", "img", "br", "hr", "meta", "link"} BULMA = set(""" box button buttons checkbox column columns control field file file-cta file-input file-label file-name has-addons has-icons-left has-name has-text-danger help icon input label menu menu-label -menu-list progress select table tag textarea title +menu-list notification progress select table tag textarea title is-3 is-4 is-5 is-6 is-7 is-9 is-active is-danger is-expanded is-fullwidth is-gapless is-half is-hoverable is-left is-light is-primary is-size-7 is-small is-success is-text is-warning mb-1 mb-2 mb-3 mb-4 ml-2 mt-4 mt-5 @@ -197,7 +197,8 @@ def main(): if not box.kids or not box.kids[0].has("sp-box-head"): errors.append(f"{name}: a box must start with box_head") for k in top[1:]: - ok = k.has("box") or k.has("columns") or (k.tag == "form" and k.has("sp-form")) + # A flash is conditional, so it is allowed at the top level but never part of an outline. + ok = k.has("box") or k.has("columns") or k.has("notification") or (k.tag == "form" and k.has("sp-form")) if not ok: errors.append(f"{name}: top-level <{k.tag} class=\"{' '.join(k.classes)}\"> — a page is boxes, columns or one form") if name != "components": diff --git a/docs/assets/panel-redesign/panel/components.html b/docs/assets/panel-redesign/panel/components.html index 1569397..f8c77c5 100644 --- a/docs/assets/panel-redesign/panel/components.html +++ b/docs/assets/panel-redesign/panel/components.html @@ -72,6 +72,9 @@ +
flash — the result of the last action, between page_head and the first box. Success, or…
+
…flash, error — a failure that belongs to the page, not to one field. A field's error stays under the field in Bulma's help is-danger.
+
00

Page layouts

A page is exactly one of these
@@ -115,8 +118,8 @@
-
02

box · box_head

The end slot: a note, a tag, a link or a filter form
-

box_body — padded content. A table or a run of sp-record blocks sits directly in the box, without box_body.

+
02

box · box_head

The end slot: a note, a tag, a link or a filter form
+

The after the end slot is sp-help: an optional link to the section's topic on the Help page. It replaces today's help drawer.

box_body — padded content. A table or a run of sp-record blocks sits directly in the box, without box_body.

box_foot — a legend, pagingend slot
diff --git a/docs/assets/panel-redesign/panel/health.html b/docs/assets/panel-redesign/panel/health.html index a614601..008e77a 100644 --- a/docs/assets/panel-redesign/panel/health.html +++ b/docs/assets/panel-redesign/panel/health.html @@ -141,6 +141,14 @@
+
+
06

Configuration

Safe to run at any time
+
+

Regenerates the OpenDKIM and Postfix configuration from the database and reloads both daemons. Use it after editing the files by hand, after restoring a backup, or when the running configuration looks out of step with the domain and application lists. It does not touch the queue or the TLS certificate.

+
+
+
+
diff --git a/docs/assets/panel-redesign/panel/in-domain.html b/docs/assets/panel-redesign/panel/in-domain.html index 0a879e9..abe0291 100644 --- a/docs/assets/panel-redesign/panel/in-domain.html +++ b/docs/assets/panel-redesign/panel/in-domain.html @@ -96,7 +96,7 @@
-
01

MX record

Checked 1 min ago
+
01

MX record

Checked 1 min ago

MX

publishedKeep any existing primary MX if this is a backup-MX
diff --git a/docs/assets/panel-redesign/panel/in-domains.html b/docs/assets/panel-redesign/panel/in-domains.html index e94bf97..2bdb8d8 100644 --- a/docs/assets/panel-redesign/panel/in-domains.html +++ b/docs/assets/panel-redesign/panel/in-domains.html @@ -78,7 +78,7 @@
-

Mail is not accepted until an upstream host is saved on the domain's page.

+

Mail is not accepted until an upstream host is saved on the domain's page. Adding and deleting domains is for a global administrator; a domain administrator sees the list without this form and without Delete.

diff --git a/docs/assets/panel-redesign/panel/out-domain-settings.html b/docs/assets/panel-redesign/panel/out-domain-settings.html index f860550..2111761 100644 --- a/docs/assets/panel-redesign/panel/out-domain-settings.html +++ b/docs/assets/panel-redesign/panel/out-domain-settings.html @@ -108,7 +108,7 @@
-
02

Rate limit

active
+
02

Rate limit

active
diff --git a/docs/assets/panel-redesign/panel/out-domain.html b/docs/assets/panel-redesign/panel/out-domain.html index d18e786..0bbe548 100644 --- a/docs/assets/panel-redesign/panel/out-domain.html +++ b/docs/assets/panel-redesign/panel/out-domain.html @@ -101,7 +101,7 @@
-
01

DNS records

Checked 3 min ago
+
01

DNS records

Checked 3 min ago

DKIM

publishedNot a secret · selector sp2026
@@ -142,7 +142,7 @@
-
02

Applications

SASL logins that may send as this domain
+
02

Applications

SASL logins that may send as this domain
diff --git a/docs/assets/panel-redesign/panel/outlines.json b/docs/assets/panel-redesign/panel/outlines.json index 5d14443..b19f830 100644 --- a/docs/assets/panel-redesign/panel/outlines.json +++ b/docs/assets/panel-redesign/panel/outlines.json @@ -107,7 +107,8 @@ ] } ] - } + }, + "box[Configuration]:body" ], "help": [ "head", @@ -385,7 +386,8 @@ }, { "is-6": [ - "box[Assigned domains]:body" + "box[Outbound domains]:body", + "box[Inbound domains]:body" ] } ] diff --git a/docs/assets/panel-redesign/panel/overview.html b/docs/assets/panel-redesign/panel/overview.html index 760ec30..43d56ee 100644 --- a/docs/assets/panel-redesign/panel/overview.html +++ b/docs/assets/panel-redesign/panel/overview.html @@ -73,7 +73,7 @@
-
01

Server health

+
01

Server health

Machine
CPU 12 % · RAM 41 %
↓ 18 kB/s · ↑ 44 kB/s
Processes
5 of 5 running
up 18 days
diff --git a/docs/assets/panel-redesign/panel/src/account.html b/docs/assets/panel-redesign/panel/src/account.html index 48723ab..6a952f8 100644 --- a/docs/assets/panel-redesign/panel/src/account.html +++ b/docs/assets/panel-redesign/panel/src/account.html @@ -30,7 +30,7 @@

What happens

Changing the password signs out every other session of this account. This one stays signed in.

-

A domain administrator sees this same page, and nothing under Server.

+

A domain administrator sees this same page, the Outbound and Inbound domains assigned to them, and nothing under Server.

diff --git a/docs/assets/panel-redesign/panel/src/components.html b/docs/assets/panel-redesign/panel/src/components.html index dff015e..b536dc7 100644 --- a/docs/assets/panel-redesign/panel/src/components.html +++ b/docs/assets/panel-redesign/panel/src/components.html @@ -9,6 +9,9 @@ +
flash — the result of the last action, between page_head and the first box. Success, or…
+
…flash, error — a failure that belongs to the page, not to one field. A field's error stays under the field in Bulma's help is-danger.
+
00

Page layouts

A page is exactly one of these
@@ -52,8 +55,8 @@
-
02

box · box_head

The end slot: a note, a tag, a link or a filter form
-

box_body — padded content. A table or a run of sp-record blocks sits directly in the box, without box_body.

+
02

box · box_head

The end slot: a note, a tag, a link or a filter form
+

The after the end slot is sp-help: an optional link to the section's topic on the Help page. It replaces today's help drawer.

box_body — padded content. A table or a run of sp-record blocks sits directly in the box, without box_body.

box_foot — a legend, pagingend slot
diff --git a/docs/assets/panel-redesign/panel/src/health.html b/docs/assets/panel-redesign/panel/src/health.html index 0a75ad7..cdb959d 100644 --- a/docs/assets/panel-redesign/panel/src/health.html +++ b/docs/assets/panel-redesign/panel/src/health.html @@ -77,3 +77,11 @@
+ +
+
06

Configuration

Safe to run at any time
+
+

Regenerates the OpenDKIM and Postfix configuration from the database and reloads both daemons. Use it after editing the files by hand, after restoring a backup, or when the running configuration looks out of step with the domain and application lists. It does not touch the queue or the TLS certificate.

+ +
+
diff --git a/docs/assets/panel-redesign/panel/src/in-domain.html b/docs/assets/panel-redesign/panel/src/in-domain.html index 3e68ac6..56d6960 100644 --- a/docs/assets/panel-redesign/panel/src/in-domain.html +++ b/docs/assets/panel-redesign/panel/src/in-domain.html @@ -33,7 +33,7 @@
-
01

MX record

Checked 1 min ago
+
01

MX record

Checked 1 min ago

MX

publishedKeep any existing primary MX if this is a backup-MX
diff --git a/docs/assets/panel-redesign/panel/src/in-domains.html b/docs/assets/panel-redesign/panel/src/in-domains.html index 2072bb6..6bc4b75 100644 --- a/docs/assets/panel-redesign/panel/src/in-domains.html +++ b/docs/assets/panel-redesign/panel/src/in-domains.html @@ -15,7 +15,7 @@
-

Mail is not accepted until an upstream host is saved on the domain's page.

+

Mail is not accepted until an upstream host is saved on the domain's page. Adding and deleting domains is for a global administrator; a domain administrator sees the list without this form and without Delete.

diff --git a/docs/assets/panel-redesign/panel/src/out-domain-settings.html b/docs/assets/panel-redesign/panel/src/out-domain-settings.html index f8b8d24..7c83efc 100644 --- a/docs/assets/panel-redesign/panel/src/out-domain-settings.html +++ b/docs/assets/panel-redesign/panel/src/out-domain-settings.html @@ -45,7 +45,7 @@
-
02

Rate limit

active
+
02

Rate limit

active
diff --git a/docs/assets/panel-redesign/panel/src/out-domain.html b/docs/assets/panel-redesign/panel/src/out-domain.html index 7827b1e..34ad36a 100644 --- a/docs/assets/panel-redesign/panel/src/out-domain.html +++ b/docs/assets/panel-redesign/panel/src/out-domain.html @@ -38,7 +38,7 @@
-
01

DNS records

Checked 3 min ago
+
01

DNS records

Checked 3 min ago

DKIM

publishedNot a secret · selector sp2026
@@ -79,7 +79,7 @@
-
02

Applications

SASL logins that may send as this domain
+
02

Applications

SASL logins that may send as this domain
LoginMay send as30 daysLimits
diff --git a/docs/assets/panel-redesign/panel/src/overview.html b/docs/assets/panel-redesign/panel/src/overview.html index 4fadf8f..6d23e29 100644 --- a/docs/assets/panel-redesign/panel/src/overview.html +++ b/docs/assets/panel-redesign/panel/src/overview.html @@ -10,7 +10,7 @@
-
01

Server health

+
01

Server health

Machine
CPU 12 % · RAM 41 %
↓ 18 kB/s · ↑ 44 kB/s
Processes
5 of 5 running
up 18 days
diff --git a/docs/assets/panel-redesign/panel/src/user-delete.html b/docs/assets/panel-redesign/panel/src/user-delete.html index ae7a7eb..3c5ad1d 100644 --- a/docs/assets/panel-redesign/panel/src/user-delete.html +++ b/docs/assets/panel-redesign/panel/src/user-delete.html @@ -1,8 +1,8 @@ - +
-

Users / shop-team /

-

Delete shop-team

+

Users / acme-ops /

+

Delete acme-ops

@@ -11,15 +11,15 @@

This cannot be undone

-

Deleting the panel user shop-team will:

+

Deleting the panel user acme-ops will:

  • end any signed-in session of this user at once;
  • -
  • remove the assignment to shop.example.org.
  • +
  • remove the assignments to notify.acme.io (outbound) and acme.io (inbound).
-

The domain, its applications and their passwords are not affected.

+

The domains, their applications and passwords, upstreams and recipients are not affected.

- + Keep it
diff --git a/docs/assets/panel-redesign/panel/src/user.html b/docs/assets/panel-redesign/panel/src/user.html index 5738428..e0e7b3b 100644 --- a/docs/assets/panel-redesign/panel/src/user.html +++ b/docs/assets/panel-redesign/panel/src/user.html @@ -1,8 +1,8 @@ - +

Users /

-

shop-team

+

acme-ops

The same form, empty, creates a new user.

Delete user @@ -14,22 +14,30 @@
01

Sign-in

-
+

Leave empty to keep the current one.

-

A global administrator sees everything, including Inbound and Server. The only global administrator cannot be demoted or deleted.

+

A global administrator sees everything, including Server, and is the only one who adds or deletes domains. The only global administrator cannot be demoted or deleted.

-
02

Assigned domains

Required for a domain administrator
+
02

Outbound domains

DNS records, applications, log
-
-
+
+
+
+
+
+
03

Inbound domains

Upstream, recipients, log
+
+
+
+

Granted separately from outbound: the same name on both lists is two assignments. A domain administrator needs at least one domain on either list.

diff --git a/docs/assets/panel-redesign/panel/src/users.html b/docs/assets/panel-redesign/panel/src/users.html index b1c7dbe..00961de 100644 --- a/docs/assets/panel-redesign/panel/src/users.html +++ b/docs/assets/panel-redesign/panel/src/users.html @@ -3,19 +3,21 @@

Server

Users

-

Who may sign in to this panel. A domain administrator sees only the outbound domains assigned to them.

+

Who may sign in to this panel. A domain administrator sees only the domains assigned to them — outbound and inbound are granted separately.

Create user
+
User acme-ops created.
+
01

Panel users

3 users
LoginMay send as30 daysLimits
- + - - - + + +
UsernameRoleDomains
UsernameRoleOutbound domainsInbound domains
admin youglobalAllEdit
shop-teamdomain adminshop.example.orgEdit
acme-opsdomain adminnotify.acme.ioEdit
admin youglobalAllAllEdit
shop-teamdomain adminshop.example.org—Edit
acme-opsdomain adminnotify.acme.ioacme.ioEdit
diff --git a/docs/assets/panel-redesign/panel/theme.css b/docs/assets/panel-redesign/panel/theme.css index 3a83c0c..f31dfa6 100644 --- a/docs/assets/panel-redesign/panel/theme.css +++ b/docs/assets/panel-redesign/panel/theme.css @@ -87,6 +87,14 @@ code { background: var(--sp-well); color: var(--sp-ink); border-radius: 3px; pad .sp-box-head .sp-no { font-family: var(--sp-mono); font-size: .75rem; font-weight: 600; color: var(--sp-brick); } .sp-box-head h2 { font-family: var(--sp-mono); font-size: .8rem; font-weight: 600; letter-spacing: .1em; text-transform: uppercase; margin: 0; } .sp-box-head .sp-end { margin-left: auto; color: var(--sp-muted); font-size: .85rem; display: flex; gap: .6rem; align-items: center; } +.sp-box-head .sp-help { color: var(--sp-muted); line-height: 1; } +.sp-box-head .sp-help:hover { color: var(--sp-brick); } +.sp-box-head .sp-end + .sp-help { margin-left: 0; } +.sp-box-head h2 + .sp-help { margin-left: auto; } +.notification { border: 1px solid transparent; padding: .8rem 1.25rem; } +.notification.is-success.is-light { border-color: var(--sp-ok-rule); } +.notification.is-danger.is-light { border-color: var(--sp-fail-rule); } +.notification .ti { margin-right: .5rem; } .box > .sp-box-body { padding: 1.25rem; } .box > .sp-box-body > :last-child { margin-bottom: 0; } .box > .table { margin: 0; } diff --git a/docs/assets/panel-redesign/panel/user-delete.html b/docs/assets/panel-redesign/panel/user-delete.html index e0f5783..13c5a11 100644 --- a/docs/assets/panel-redesign/panel/user-delete.html +++ b/docs/assets/panel-redesign/panel/user-delete.html @@ -3,7 +3,7 @@ -Delete shop-team · SelfPost +Delete acme-ops · SelfPost @@ -64,8 +64,8 @@
-

Users / shop-team /

-

Delete shop-team

+

Users / acme-ops /

+

Delete acme-ops

@@ -74,15 +74,15 @@

This cannot be undone

-

Deleting the panel user shop-team will:

+

Deleting the panel user acme-ops will:

  • end any signed-in session of this user at once;
  • -
  • remove the assignment to shop.example.org.
  • +
  • remove the assignments to notify.acme.io (outbound) and acme.io (inbound).
-

The domain, its applications and their passwords are not affected.

+

The domains, their applications and passwords, upstreams and recipients are not affected.

- + Keep it
diff --git a/docs/assets/panel-redesign/panel/user.html b/docs/assets/panel-redesign/panel/user.html index d3a5b86..220c7f8 100644 --- a/docs/assets/panel-redesign/panel/user.html +++ b/docs/assets/panel-redesign/panel/user.html @@ -3,7 +3,7 @@ -shop-team · Users · SelfPost +acme-ops · Users · SelfPost @@ -65,7 +65,7 @@

Users /

-

shop-team

+

acme-ops

The same form, empty, creates a new user.

Delete user @@ -77,22 +77,30 @@
01

Sign-in

-
+

Leave empty to keep the current one.

-

A global administrator sees everything, including Inbound and Server. The only global administrator cannot be demoted or deleted.

+

A global administrator sees everything, including Server, and is the only one who adds or deletes domains. The only global administrator cannot be demoted or deleted.

-
02

Assigned domains

Required for a domain administrator
+
02

Outbound domains

DNS records, applications, log
-
-
+
+
+
+
+
+
03

Inbound domains

Upstream, recipients, log
+
+
+
+

Granted separately from outbound: the same name on both lists is two assignments. A domain administrator needs at least one domain on either list.

diff --git a/docs/assets/panel-redesign/panel/users.html b/docs/assets/panel-redesign/panel/users.html index dc1d9e9..f595d66 100644 --- a/docs/assets/panel-redesign/panel/users.html +++ b/docs/assets/panel-redesign/panel/users.html @@ -66,19 +66,21 @@

Server

Users

-

Who may sign in to this panel. A domain administrator sees only the outbound domains assigned to them.

+

Who may sign in to this panel. A domain administrator sees only the domains assigned to them — outbound and inbound are granted separately.

Create user
+
User acme-ops created.
+
01

Panel users

3 users
- + - - - + + +
UsernameRoleDomains
UsernameRoleOutbound domainsInbound domains
admin youglobalAllEdit
shop-teamdomain adminshop.example.orgEdit
acme-opsdomain adminnotify.acme.ioEdit
admin youglobalAllAllEdit
shop-teamdomain adminshop.example.org—Edit
acme-opsdomain adminnotify.acme.ioacme.ioEdit
diff --git a/docs/plans/inbound-antispam-panel.md b/docs/plans/inbound-antispam-panel.md index 4b528c9..48d4b96 100644 --- a/docs/plans/inbound-antispam-panel.md +++ b/docs/plans/inbound-antispam-panel.md @@ -121,9 +121,12 @@ No separate feature flag beyond inbound relay + existing antispam env vars. ### UI -New nav item **Inbound spam** (or subsection under **Inbound**) — journal table -with filters (decision, domain, date range), list management on the same page or -a tab. Mockup: add `docs/assets/panel-ui/inbound_spam.html` in the panel step. +The journal is **Inbound › Log** and list management is a new **Inbound › +Filter lists** page (global-only: the lists are instance-wide) — see +[panel-redesign.md](panel-redesign.md) § Room for roadmap items. The journal +table has filters (decision, domain, date range). Mockups are drawn first, in +`docs/assets/panel-redesign/panel/src/` under that plan's design contract — +not in the superseded `docs/assets/panel-ui/`. ## Schema (migration `0011_inbound_spam_log.sql`) diff --git a/docs/plans/panel-redesign.md b/docs/plans/panel-redesign.md index d9c28ff..a9be311 100644 --- a/docs/plans/panel-redesign.md +++ b/docs/plans/panel-redesign.md @@ -2,7 +2,7 @@ **Status:** candidate **Date:** 2026-09-21 (replaces the 2026-09-08 givens) -**Version:** `1.x` MINOR once agreed. +**Version:** `2.0.0` MAJOR — a breaking release by the owner's decision (§ No compatibility). --- @@ -57,10 +57,108 @@ Navigation is grouped by mail direction. Page titles carry the group | Server | Health · System log · Backup · Users · Settings | Health is new (the tables that left `/status`) | | User menu | Account · Help · Sign out | Account is the credentials half of `/settings` | -Visibility flags (global vs domain-admin, inbound, DMARC) stay as in -[web.go](../../internal/web/web.go): a hidden page leaves no gap in the menu. -Whether paths are renamed to match (`/outbound/log`, …) with redirects from -the old ones is decided in checklist step 3, not by the implementer. +A hidden page leaves no gap in the menu. The inbound and DMARC feature flags +stay as in [web.go](../../internal/web/web.go); who sees what changes — see +§ Who sees what. + +## No compatibility + +**Decided 2026-09-21 (owner): nothing is kept compatible across the redesign, +and the migration chain is deleted to zero.** The redesign is `2.0.0`. + +- **URLs.** Old paths are not redirected; they are gone. Bookmarks break once. +- **Schema.** `internal/store/migrations/` is emptied and replaced by one + baseline, `0001_init.sql`, equal to the 2.0 schema (including the new + assignment table below). No historical `admin` table, no create-then-drop. + This is [schema-squash](../roadmap.md#schema-squash) with its trigger now + decided and without the upgrade gate that item planned: 2.0 starts from an + empty data directory. +- **Docs.** `schema-migrations.md` shrinks to the 2.0 baseline; the 1.x chain + stays in git history. + +The only live installation is the owner's, and it is set up again on 2.0. +Nothing is built to ease that move, and nothing needs to be. + +## Who sees what + +**Decided 2026-09-21 (owner): a domain administrator manages inbound domains +too, and that is granted separately from outbound.** Today every inbound +handler is `requireGlobal()` and `user_domains` references outbound domains +only. + +| | Global administrator | Domain administrator | +|---|---|---| +| Overview, Server | yes | no (404) | +| Outbound: domain, applications, settings, log, DMARC | all domains | assigned outbound domains | +| Inbound: domain (upstream, recipients), log | all domains | assigned inbound domains | +| Add / delete a domain, either direction | yes | no | +| Queues, either direction | yes | no, until the queue is a filterable table | +| Account, Help | yes | yes | + +- New table `user_inbound_domains (user_id, inbound_domain_id)`, mirroring + `user_domains`; both live in the 2.0 baseline. The same name on both lists + is two assignments — the two kinds of domain stay separate entities. +- The user form has two lists (mockup `user.html`); a domain administrator + needs at least one assignment on either. +- A group with nothing assigned is absent from that user's menu. +- Authorization helpers come in pairs: `requireDomain(id)` and + `requireInboundDomain(id)`; lists and logs are filtered by assignment in the + query, not in the template. + +## Routes + +**Decided 2026-09-21 (owner): paths are renamed to follow the new names and +structure.** A URL reads like the menu: `//`, an entity under its +list, an action under the thing it changes. + +| Screen (mockup) | New | Today | +|---|---|---| +| overview | `GET /overview` | `/status` | +| health | `GET /server/health` · `POST …/recheck` · `POST …/reload` | tables of `/status` · `/status/recheck` · `/reload` | +| out-domains | `GET, POST /outbound/domains` | `/domains` | +| out-domain | `GET /outbound/domains/{id}` · `POST …/dns-recheck` | `/domains/{id}` | +| out-domain-settings | `GET /outbound/domains/{id}/settings` · `POST …/settings/reports` · `…/settings/ratelimit` · `…/settings/ratelimit/recalc` · `…/settings/export` | cards of `/domains/{id}` · `…/dmarc` · `…/ratelimit` · `…/ratelimit/recalc` · `…/export` | +| out-domain-delete | `GET, POST /outbound/domains/{id}/delete` | `/domains/{id}/delete` | +| out-app | `GET, POST /outbound/domains/{id}/applications/new` · `GET, POST …/applications/{aid}` · `POST …/{aid}/ratelimit/recalc` · `POST …/{aid}/delete` | `POST /domains/{id}/applications` · `POST /applications/{aid}/mode`, `/authips`, `/ratelimit` · `…/recalc` · `…/delete` | +| out-app-created | the response of `POST …/applications/new` and `POST …/{aid}/password`; no GET path, `Cache-Control: no-store` | a card on `/domains/{id}` after the POST | +| out-log · out-message | `GET /outbound/log` · `GET /outbound/log/{id}` | `/deliveries` · `/deliveries/{id}` | +| out-queue | `GET /outbound/queue` | `/mail-queue` | +| dmarc · dmarc-domain · dmarc-report | `GET /outbound/dmarc` · `…/dmarc/domains/{id}` · `…/dmarc/reports/{id}` | `/dmarc/…` | +| in-domains · in-domain | `GET, POST /inbound/domains` · `GET /inbound/domains/{id}` · `POST …/dns-recheck`, `…/upstream`, `…/recipients` | `/inbound` · `/inbound/{id}/…` | +| in-domain-delete | `GET, POST /inbound/domains/{id}/delete` | `/inbound/{id}/delete` | +| in-log · in-queue | `GET /inbound/log` · `GET /inbound/queue` | — (new, see below) | +| system-log | `GET /server/log` | `/system-log` | +| backup | `GET, POST /server/backup` · `POST /server/backup/import` | `/backup` · `POST /domains/import` | +| users · user · user-delete | `/server/users` · `…/new` · `…/{uid}` · `…/{uid}/delete` | `/users/…` | +| settings | `GET, POST /server/settings` | the instance half of `/settings` | +| account | `GET, POST /account` | the credentials half of `/settings` (today `/account` redirects there) | +| help · components | `GET /help` · `GET /server/components` | `/help` · — | + +Unchanged: `/healthz`, `/license`, `/static/`, `/setup/{token}`, `/login`, +`/logout`. `GET /` goes to `/overview`, or to `/outbound/domains` for a domain +administrator. + +Rules that come with the rename: + +- **One fragment name.** Every polled page answers `/fragment` + (`/overview/fragment`, `/outbound/log/fragment`, …) — today it is + `fragment`, `rows` and `body`. +- **The application form is one POST.** The mockup has a single *Save + application*; sender, IP allow-list and rate limit are validated and saved + together, or not at all. The three separate POSTs go away. +- **No redirects.** Old paths, GET and POST alike, are removed (§ No + compatibility), including today's `/account` → `/settings` redirect. The + full list goes under *Removed* in the CHANGELOG. +- **Authorization can follow the tree — for `/server/` only.** Everything + under `/server/` is global-only, so `requireGlobal()` can guard that subtree + once instead of once per handler. `/outbound/` and `/inbound/` are checked + per domain (§ Who sees what). Both are security-relevant: they go through + the security review, and a per-route test asserts 404 for a domain + administrator on every path they must not reach — including another + tenant's domain id. +- `TestRoutesFollowNavigation` — every path in this table is registered with + the listed methods, every menu entry points at a registered path, every old + path answers 404, and no template contains an old path. **Needs its own roadmap decision before it is built** (the mockups show the target; none of this is smuggled in with the restyle): @@ -68,11 +166,36 @@ target; none of this is smuggled in with the restyle): - Queue as a table — parse `postqueue -j` instead of printing `postqueue -p`. - Inbound queue — the same queue filtered by recipient domain. - Inbound log — rides on `inbound-antispam-panel` (`1.10.0`). -- Server › Health — a route for the tables removed from Overview. +- Server › Health — a route for the tables removed from Overview, and the + home of *Reload configuration* (today a card on `/status`). Until a row above is agreed, its page ships in the old content inside the new shell, and its menu entry is hidden if the page does not exist. +## Room for roadmap items + +The mockups draw today's features plus the four rows above. The other open +roadmap items are **not drawn yet**; each has a place, and none needs a new +component or layout. A feature's screens are drawn mockup-first in this system +(`panel/src/`, `build.py`, `check.py --write`) as the first step of that +feature's own plan — not invented during its implementation. + +| Roadmap item | Where it lands | Built from | Drawn | +|---|---|---|---| +| inbound-antispam-panel (agreed, `1.10.0`) | journal = **Inbound › Log** (add client IP and engine to the mockup's columns; `quarantine` joins the decisions); lists = new **Inbound › Filter lists**, global-only because the lists are instance-wide | list layout: add form + table, `status_tag`, filter form in `box_head` | log: yes, to be adjusted · lists: **no** | +| inbound-quarantine (candidate) | new **Inbound › Quarantine**, list + detail with *Release* / *Discard* | list layout; detail like `out-message` with `facts`, `log_pane`, a `sp-danger-zone` box for Discard | **no** | +| preflight (candidate) | new **Server › Preflight**, next to Health: Health is what runs now, Preflight is the deeper on-demand installation check with a test e-mail form | `postmark` verdict, a table of checks with `status_tag`, `facts`, one form box | **no** | +| csrf-tokens | no screen; every form gains a hidden field — forms are rendered through one helper so the token cannot be forgotten | — | n/a | +| template-data-typing | no screen; the component partials are the natural first users of typed data | — | n/a | +| structured-logging, contributing, review follow-ups | none | — | n/a | + +The sibling strip under the navbar holds five or six entries per group without +wrapping; Inbound would reach five (Domains · Log · Queue · Quarantine · +Filter lists), Server six. Past that a group is split rather than squeezed. + +Ordering with 2.0: inbound-antispam-panel ships first in `1.10.0` and adds its +migration to the 1.x chain as planned; the 2.0 baseline then folds it in. + ## Three layers 1. **Bulma, as shipped.** Vendored `bulma.min.css`, never edited. Only the @@ -88,7 +211,8 @@ shell, and its menu entry is hidden if the page does not exist. |---|---| | `page_head` | path, `

`, lead, actions; optionally a `postmark` | | `postmark` | the round verdict stamp: ok / `sp-warn` / `sp-fail` | -| `box`, `box_head`, `box_foot` | numbered mono head with an end slot; variants `sp-danger-zone`, `sp-credential` | +| `box`, `box_head`, `box_foot` | numbered mono head with an end slot and an optional `sp-help` link to the section's Help topic (replaces the help drawer); variants `sp-danger-zone`, `sp-credential` | +| `flash` | Bulma `notification is-success / is-danger is-light` between `page_head` and the first box; a field's error stays in `help is-danger` | | `health_card` | Overview only; ok / `sp-warn` / `sp-fail`; links to its detail | | `dns_record` | one block per record: status, what to publish, "in DNS now" only on a mismatch | | `facts` | label / value grid, `sp-big` for headline numbers | @@ -179,21 +303,26 @@ reviewed. - Weakening CSP; a JS bundler; a front-end framework. ## Implementation checklist - No code until the roadmap status is **agreed**. +- [x] Owner decision recorded here: keep today's paths or rename with redirects — **owner** (2026-09-21: rename, § Routes) +- [x] Owner decision: no compatibility, migrations to zero, `2.0.0` — **owner** (2026-09-21, § No compatibility) +- [x] Owner decision: inbound is delegated per user, separately from outbound — **owner** (2026-09-21, § Who sees what) - [ ] Vendor Bulma 1.0.4 and the used Tabler icons (MIT) under `/static`, NOTICE and development.md rows, pinned checksum — **Haiku** -- [ ] Guard tests from the table above, red against today's templates where expected; `design-first` CI step — **Opus** -- [ ] Owner decision recorded here: keep today's paths or rename with redirects — **owner** +- [ ] Guard tests from the table above and `TestRoutesFollowNavigation`, red against today's panel where expected; `design-first` CI step — **Opus** +- [ ] Schema from zero: single `0001_init.sql` baseline with `user_inbound_domains`, store tests, `schema-migrations.md` rewritten — **Opus** +- [ ] Inbound delegation: `requireInboundDomain`, filtered lists and log, two-list user form, per-route 404 tests for both roles — **Opus** +- [ ] Routes renamed per § Routes: new paths, one fragment name, old paths removed, subtree `requireGlobal()` for `/server/` — **Opus** - [ ] `panel.css` from `panel/theme.css`; `components.html` partials; `status_tag`; a `/components` page behind the global role rendering every partial — **Sonnet** - [ ] `layout.html`: navbar, perforated edge, sibling strip, user menu, footer; visibility flags — **Sonnet** - [ ] Signed-out pages: login, setup — **Sonnet** - [ ] Overview (verdicts) and Account / Settings split — **Sonnet** -- [ ] Outbound: domains, domain, domain settings, application form, shown-once password, delete — **Sonnet** +- [ ] Outbound: domains, domain, domain settings, application form as one POST, shown-once password, delete — **Sonnet** - [ ] Outbound: log, message, DMARC hub / domain / report — **Sonnet** - [ ] Inbound: domains, domain, delete — **Sonnet** - [ ] Server: system log, backup, users, user form, delete; Help — **Sonnet** - [ ] Remove the old `panel.css` rules and classes nothing references; vocabulary test green with no allow-list — **Sonnet** - [ ] Technical review of the whole against the mockups, evidence pairs complete — **Opus** -- [ ] guide.md screenshots and wording for the new navigation — **Sonnet** +- [ ] Security review of the route rename, subtree authorization and inbound delegation — **Fable** +- [ ] guide.md screenshots and wording for the new navigation; architecture.md § Panel HTTP surface and § Persistence; security.md roles; *Removed* in the CHANGELOG with one line that 2.0 starts from an empty data directory — **Sonnet** - [ ] `go vet`, `go test ./...`, e2e suite — **Haiku** diff --git a/docs/roadmap.md b/docs/roadmap.md index f43441f..223ee25 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -36,7 +36,7 @@ in `git log` and [CHANGELOG.md](../CHANGELOG.md). | template-data-typing | Typed structs for template data instead of `map[string]any` | candidate | — | — | | structured-logging | `log/slog` with levels and fields | candidate | — | — | | review-2026-08-followups | Remaining findings of the 2026-08-19 code review | candidate | — | — | -| panel-redesign | Panel redesign (accepted mockups, design contract) | candidate | 0/15 | [plans/panel-redesign.md](plans/panel-redesign.md) | +| panel-redesign | Panel redesign (accepted mockups, design contract) | candidate | 3/21 | [plans/panel-redesign.md](plans/panel-redesign.md) | | schema-squash | Squash SQLite migrations into a 2.x baseline | **2.x** | — | — | **Recommended order** (not binding): the next feature is @@ -307,7 +307,11 @@ postmark / numbered heads / perforated edge, icons). Contract and checklist: **Boundary:** templates, one `panel.css`, shared partials, vendored Bulma and icons. Navigation regroups by mail direction (Overview · Outbound · Inbound · -Server). Four things the mockups show need their own decision and are not +Server) and paths are renamed to match. **No compatibility is kept** (owner, +2026-09-21): no redirects, the migration chain is replaced by one baseline +and 2.0 starts from an empty data directory — the only live installation is +the owner's. Inbound domains become delegable to a +domain administrator, separately from outbound. Four things the mockups show need their own decision and are not smuggled in with the restyle: queue as a table (`postqueue -j`), inbound queue, inbound log (rides on inbound-antispam-panel), Server › Health. No CSP change, no JavaScript widgets, no second framework. @@ -322,7 +326,7 @@ of their own — answered by guard tests that land before the templates, a `design-first` CI step and evidence-based acceptance (plan § Enforcement). Competes with feature work; Bulma is ~680 kB minified, embedded once. -**Version:** `1.x` MINOR when agreed; `candidate` until then. +**Version:** `2.0.0` MAJOR; `candidate` until agreed. --- @@ -348,6 +352,13 @@ replace the squash upgrade gate. it); the gate in [schema-migrations.md](schema-migrations.md) is tested; the operator guide says a 2.x image will not open an unfinished 1.x database. +**Trigger decided 2026-09-21:** [panel-redesign](#panel-redesign) is the 2.0 +cut, and the owner dropped the upgrade gate with it — the chain is deleted to +zero and 2.0 starts from an empty data directory. The +**Boundary** and **Done when** above are superseded on that point by +[plans/panel-redesign.md](plans/panel-redesign.md) § No compatibility; this +item ships as a step of that plan, not on its own. + **Dependencies / risks:** a decided 2.x cut (another breaking change, or an explicit major). Squashing the current short chain is not a reason to cut 2.x on its own. A missed gate leaves a mid-chain 1.x database silently stuck.