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 @@
Cover sheet
+
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.