docs: panel header, type scale, account e-mail and the fixes of a full review
test / test (push) Canceled after 0s
test / test (push) Canceled after 0s
Header. The navbar carries the wordmark instead of the stamp - at nav size the stamp dissolves into the brick bar - and under the bar runs the stamp's paper margin with the perforation along its outer edge: round holes, flat teeth, ink hairline, 8px under a 60px bar as on the mark. docs/assets/selfpost-wordmark.svg and -ink are the stamp's own outlines; the header comparison sheet is removed. Type and tables. About 40 % of the text measured under 13px. Body is 16px with two tokens under it, 14px for supporting text and 13px for upper-case mono labels and tags; Bulma's small sizes are overridden or dropped (sp-small replaces is-size-7, is-small only on a progress bar). Every table sits in a table-container, addresses break after the @, and all screens fit without scrolling at 1280px and up. Contrast. A sweep of every text node against its real ground found the solid red button at 3:1, the muted grey at 4.1:1, the amber postmark and Bulma's menu labels just under 4.5. All 33 screens now measure 4.5:1 or better. The sweep is kept as panel/audit.js and its clean output joins the evidence a restyle step must show. Account and roles. A user's e-mail is its own field (users.email in the 2.0 baseline), edited under Account and seen by the global user on the user form and list. The DMARC default report address is a user's setting, not the server's: it moves to Account with its authorization record, and a domain follows the default of a named user. Roles are labelled by reach - global, domain - and each assignment list gains All, a per-user flag that covers domains added later. Account gains Notifications for the new roadmap candidate panel-notifications; password-reset is added as a candidate too. Also: the inbound spam filter is visible where a domain user looks (a box on each inbound domain, a tag on the list, its socket on Health); the health row is compact again and splits 6/3/2; a read-only value is as tall as its Copy button; Preflight and Account lose boxes that only filled space, and the plan's layout rule and contract now forbid them. check.py gains the type floor, the table-container rule and the is-small rule, and no longer counts min-width: 0 as setting a width. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,7 @@ design itself is drawn and accepted:
|
||||
| [panel/components.html](../assets/panel-redesign/panel/components.html) | every component in every state — the only parts a page may be built from |
|
||||
| [panel/outlines.json](../assets/panel-redesign/panel/outlines.json) | the component skeleton of each screen, generated from the mockups |
|
||||
| [panel/check.py](../assets/panel-redesign/panel/check.py) | the contract as a program; its rules are ported to Go tests below |
|
||||
| [panel/audit.js](../assets/panel-redesign/panel/audit.js) | what only a browser can measure: text size, contrast, overflow, clipped values — pasted into the console, on the mockups or on the running panel |
|
||||
|
||||
When this file and a mockup disagree, the mockup wins and this file is fixed.
|
||||
When an implementation and a mockup disagree, the implementation is wrong.
|
||||
@@ -55,7 +56,7 @@ Navigation is grouped by mail direction. Page titles carry the group
|
||||
| Outbound | Domains · Log · Queue · DMARC reports | `/domains`, `/deliveries`, `/mail-queue`, `/dmarc` |
|
||||
| Inbound | Domains · Log · Queue | `/inbound`; Log and Queue are new |
|
||||
| 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` |
|
||||
| User menu | Account · Help · Sign out | Account is the user's half of `/settings`: credentials, e-mail, DMARC default |
|
||||
|
||||
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
|
||||
@@ -86,7 +87,7 @@ 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 |
|
||||
| | `global` | `domain` |
|
||||
|---|---|---|
|
||||
| Overview, Server | yes | no (404) |
|
||||
| Outbound: domain, applications, settings, log, DMARC | all domains | assigned outbound domains |
|
||||
@@ -100,13 +101,53 @@ only.
|
||||
- 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.
|
||||
- The user form has two lists (mockup `user.html`); the *domain* role needs at
|
||||
least one assignment on either.
|
||||
- **Each list has *All*, which includes domains added later** — a flag per
|
||||
user and direction (`users.all_domains`, `users.all_inbound_domains`), not a
|
||||
tick on every row: otherwise tomorrow's domain is silently unassigned. *All*
|
||||
widens the list, not the role: adding and deleting domains, the queues and
|
||||
Server stay with the global role. The form works without scripts — with
|
||||
*All* ticked the server ignores the rows below it.
|
||||
- **The role is a reach, not a rank.** Everyone in the panel is an
|
||||
administrator, so the label is `global` or `domain` — in the users list, the
|
||||
role select and the page kickers; never "admin".
|
||||
- A group with nothing assigned is absent from that user's menu.
|
||||
- **The spam filter is visible to whoever it affects.** Whether the instance
|
||||
filters inbound mail is an environment setting, so a domain administrator
|
||||
cannot look it up: every inbound domain page has a *Spam filter* box (on /
|
||||
off, engine, what happens to suspicious mail, who keeps the lists), the
|
||||
inbound domains list shows it in its head, and for the global administrator
|
||||
the filter's socket is one of the milter sockets on Health and Overview.
|
||||
- Authorization helpers come in pairs: `requireDomain(id)` and
|
||||
`requireInboundDomain(id)`; lists and logs are filtered by assignment in the
|
||||
query, not in the template.
|
||||
|
||||
## Account e-mail
|
||||
|
||||
**Decided 2026-09-21 (owner): a user's e-mail is theirs, not a DMARC field.**
|
||||
Today the column is `users.dmarc_report_email` and the only place it shows is
|
||||
the DMARC card, so it reads as if it existed for that alone.
|
||||
|
||||
- The 2.0 baseline has `users.email`. It is what the panel writes to: event
|
||||
notifications ([panel-notifications](../roadmap.md#panel-notifications)) and
|
||||
a reset link ([password-reset](../roadmap.md#password-reset)).
|
||||
- **Account** edits it; the global administrator sees and sets it on the user
|
||||
form and sees it in the users list.
|
||||
- DMARC only *may* use it. **The default report address is a user's setting,
|
||||
not the server's** (owner, 2026-09-21): it lives on **Account** with its
|
||||
report-authorization record, as a choice — SelfPost hosted, *my account
|
||||
e-mail*, another address, none. Historically it was the administrator's
|
||||
profile field, later mirrored into an instance setting only the global user
|
||||
could edit; Server › Settings keeps nothing of it.
|
||||
- A domain can have several users, so "the default" must be someone's. A
|
||||
domain's report address is *the default of a named user* (shown as
|
||||
`admin's default — mix@example.org`), the hosted address, a custom one or
|
||||
none. A new domain follows whoever created it; any user of the domain may
|
||||
switch it to their own default. If that user is deleted the domain falls
|
||||
to *none* and its DMARC check says so. Baseline: `users.dmarc_default_mode`,
|
||||
`users.dmarc_default_address`, `domains.dmarc_rua_user_id`.
|
||||
|
||||
## Routes
|
||||
|
||||
**Decided 2026-09-21 (owner): paths are renamed to follow the new names and
|
||||
@@ -135,8 +176,8 @@ list, an action under the thing it changes.
|
||||
| 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) |
|
||||
| settings | `GET, POST /server/settings` | the instance half of `/settings`: log retention, rate limits; plus the notifications switch |
|
||||
| account | `GET /account` · `POST /account/profile`, `/password`, `/notifications`, `/dmarc` | `/settings`: credentials and the DMARC default (today `/account` redirects there) |
|
||||
| help · components | `GET /help` · `GET /server/components` | `/help` · — |
|
||||
|
||||
Unchanged: `/healthz`, `/license`, `/static/`, `/setup/{token}`, `/login`,
|
||||
@@ -191,6 +232,8 @@ the first step of that feature's plan — not invented during implementation.
|
||||
| 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 + two tables, `status_tag`, filter form in `box_head`; the log links *Allow / Deny sender* to the lists | `in-log`, `in-filter-lists` |
|
||||
| inbound-quarantine (candidate) | new **Inbound › Quarantine**, list + detail with *Release* / *Discard* | list layout; detail like `out-message` with `facts`, `log_pane` for headers only, Release beside a `sp-danger-zone` Discard | `in-quarantine`, `in-quarantine-message` — the open questions of its plan (where mail lives, what release means, retention, RBAC) stay open; the mockup assumes SelfPost-held mail, release to upstream, 14 days, delegated like the rest of Inbound |
|
||||
| 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` and a *What to do* column, one form box for the test e-mail | `preflight` |
|
||||
| panel-notifications (candidate) | per-user event choice on **Account**; one switch under **Server › Settings** | checkboxes in a box, `help` | `account`, `settings` |
|
||||
| password-reset (candidate) | a link on sign-in, two signed-out screens (request, set new password) | the signed-out layout | **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 |
|
||||
@@ -226,8 +269,31 @@ migration to the 1.x chain as planned; the 2.0 baseline then folds it in.
|
||||
| `timeline`, `log_pane`, `empty_state`, `confirm_list`, `help_topic` | as on the components page |
|
||||
| form rows | `sp-row` (wide + short), `sp-pair` (two equal) |
|
||||
|
||||
The shell — brick navbar with hover menus, perforated edge, sibling-page
|
||||
strip, footer — belongs to `layout.html` alone.
|
||||
The shell belongs to `layout.html` alone: a brick navbar with hover menus
|
||||
that carries the **wordmark, not the stamp** (at nav size the stamp dissolves
|
||||
into the bar; it stays on sign-in, setup and the favicon); under it the
|
||||
stamp's paper margin with the perforation along its outer edge — round holes,
|
||||
flat teeth, the mark's ink hairline, one tile `perf-edge.svg`; then the
|
||||
sibling-page strip; and the footer.
|
||||
|
||||
## Type and tables
|
||||
|
||||
- **Three sizes.** Body 16px; `--sp-fs-s` 14px for supporting text (labels,
|
||||
`help`, notes, mono values, logs); `--sp-fs-xs` 13px for upper-case mono
|
||||
micro-labels and tags only. Nothing a person has to read is under 13px.
|
||||
Bulma's own small sizes are overridden (`help`, `tag`, `label`,
|
||||
`menu-label`, the navbar dropdown) or not used (`is-small` on a control,
|
||||
`is-size-7` — `sp-small` instead). `check.py` enforces the floor.
|
||||
- **Contrast.** Text is at least 4.5:1 against the ground it actually sits on
|
||||
(3:1 for large text). Two traps are closed in the theme: Bulma picks a solid
|
||||
button's text colour from the fill's lightness, and the brick and the red
|
||||
land on the wrong side — solid buttons are white on colour; and the muted
|
||||
grey is `#5c6774`, not the 4.1:1 `#6b7684` of the old panel.
|
||||
- **A table sits in a `table-container`**, so a wide one scrolls inside its
|
||||
box instead of being cut off. Addresses in table cells get a `<wbr>` after
|
||||
the `@` from one template function, so a column can shrink without breaking a
|
||||
name mid-word. Screens fit without scrolling at 1280px and up; below that a
|
||||
dense log scrolls inside its box.
|
||||
|
||||
## Layout rules
|
||||
|
||||
@@ -239,7 +305,11 @@ A page is exactly one of:
|
||||
- **signed out** — the split brand / form screen.
|
||||
|
||||
Width comes from the layout, never from the page. A narrow form does not get a
|
||||
`max-width`; it gets a column and a neighbour that explains it.
|
||||
`max-width`; it gets a column. **What stands beside it must be real** — the
|
||||
form's own result, the facts it changes, a second form. A box written to fill
|
||||
the space beside another is the same defect as a width tuned by hand, and the
|
||||
first draft of Preflight had one. If nothing real belongs there, the form
|
||||
shares its box with its result, or the page uses fewer columns.
|
||||
|
||||
## The contract
|
||||
|
||||
@@ -249,17 +319,20 @@ An implementer — human or model — **must not**:
|
||||
that the components page does not show.
|
||||
2. Add a component, a state or a layout. If a screen seems to need one, **stop
|
||||
and ask**; do not approximate it with ad-hoc markup.
|
||||
3. Put a selector in `panel.css` that names a page or an element id, uses
|
||||
`!important`, or sets a width outside the `WIDTH_OK` list.
|
||||
4. Write component markup by hand in a page template instead of calling the
|
||||
3. Add a box whose only job is to fill space, or text that restates the page.
|
||||
Every box answers a question the operator has on that page.
|
||||
4. Put a selector in `panel.css` that names a page or an element id, uses
|
||||
`!important`, sets a width outside the `WIDTH_OK` list, or a font size under
|
||||
the 13px floor.
|
||||
5. Write component markup by hand in a page template instead of calling the
|
||||
partial.
|
||||
5. Edit the vendored Bulma, or load a second CSS framework, icon set or any
|
||||
6. Edit the vendored Bulma, or load a second CSS framework, icon set or any
|
||||
JavaScript widget.
|
||||
6. Change a mockup, `outlines.json`, `check.py` or a guard test in the same
|
||||
7. Change a mockup, `outlines.json`, `check.py` or a guard test in the same
|
||||
commit as `internal/web`, or at all without the owner's explicit
|
||||
instruction. **The guards are not the implementer's to adjust.** A failing
|
||||
guard means the implementation is wrong, not the guard.
|
||||
7. Report a step as done on the strength of a description. Done means the
|
||||
8. Report a step as done on the strength of a description. Done means the
|
||||
evidence below exists.
|
||||
|
||||
**Changing the design** is allowed and has one route: mockup first. Edit the
|
||||
@@ -291,6 +364,8 @@ or the guard tests.
|
||||
Every checklist step that restyles pages ends with, in the commit:
|
||||
|
||||
- `go test ./internal/web/...` output in the commit message body (pass);
|
||||
- the output of `panel/audit.js` run against the restyled pages of the running
|
||||
container, with an empty `problems` list;
|
||||
- a screenshot of each restyled page at 1440×900, saved next to its mockup's
|
||||
name under `docs/assets/panel-redesign/evidence/`, rendered from the running
|
||||
container with the e2e fixtures — not from the mockup;
|
||||
|
||||
Reference in New Issue
Block a user