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:
@@ -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
@@ -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**
|
||||
|
||||
Reference in New Issue
Block a user