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 <page>/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 <noreply@anthropic.com>
This commit is contained in:
mix
2026-09-21 22:02:12 +03:00
co-authored by Claude Fable 5.1
parent 1bcfca1586
commit 5e0adb2de5
29 changed files with 293 additions and 78 deletions
+6 -3
View File
@@ -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`)
+141 -12
View File
@@ -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: `/<group>/<page>`, 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 `<page>/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, `<h1>`, 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**