Files
selfpost/docs/plans/panel-redesign.md
T
mixandClaude Fable 5.1 1bcfca1586 docs: turn the accepted panel design into an enforceable contract
panel-redesign.md is rewritten for the accepted direction: three layers
(Bulma as shipped, adapters, own sp- components), the page layouts, what
an implementer must not do, and how that is enforced — guard tests that
land before any template, a design-first CI step, and acceptance by
evidence (test output plus a screenshot of every page beside its mockup,
reviewed by a model that did not write the step).

A components page shows every building block in every state; a screen
may be built from nothing else. panel/check.py lints the mockups against
the contract (class vocabulary, CSS bans on !important, id and page
selectors and stray widths, page structure) and writes
panel/outlines.json, the per-screen component skeleton an implementation
is compared with, so a different design cannot pass as this one.

Own modifier classes now carry the sp- prefix (sp-warn, sp-fail, sp-big,
sp-danger-zone, ...) so the vocabulary is machine-checkable; the lint
also caught a width set outside the layout in the mockup's own CSS.

Agent rule .cursor/rules/panel-design.mdc; roadmap row and section
updated (0/15, still candidate).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-21 20:55:18 +03:00

200 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Plan: panel-redesign
**Status:** candidate
**Date:** 2026-09-21 (replaces the 2026-09-08 givens)
**Version:** `1.x` MINOR once agreed.
---
## What this file is
The design contract for the control panel and the plan to implement it. The
design itself is drawn and accepted:
| Artefact | Role |
|---|---|
| [docs/assets/panel-redesign/panel/](../assets/panel-redesign/index.html) | 29 screens — **the** reference for what each page looks like |
| [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 |
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.
The 2026-09-08 givens ("no component library, Web Awesome layout CSS only") are
withdrawn. `docs/assets/panel-ui/` and the stage-1 directions `a/`, `b/`, `c/`
are history, not a brief.
## Goal
A panel an operator opens a few times a year and still finds their way in: the
important things in view, details one or two clicks away, one visual language
on every page, and no page that tunes its own width.
## Delivery constraints (unchanged)
| Constraint | Where |
|---|---|
| Go `html/template`, server-rendered; htmx 2.x for polled fragments | [internal/web/view/](../../internal/web/view/) |
| Assets embedded (`//go:embed`), no CDN at runtime | [view.go](../../internal/web/view/view.go) |
| CSP `default-src 'self'` — no inline script, no `style=""` | [security.go](../../internal/web/security.go) |
| State changes are native HTML POST; no script is the only way to submit | templates |
| New front-end is MIT-or-better and listed in NOTICE | [NOTICE](../../NOTICE) |
Bulma was chosen partly because it needs nothing from this table relaxed: no
JavaScript, no `data:` URIs.
## Information architecture
Navigation is grouped by mail direction. Page titles carry the group
(**Outbound domains**, **Inbound log**); menu entries do not repeat it.
| Group | Pages | Today |
|---|---|---|
| Overview | Overview | `/status` (verdicts only) |
| 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` |
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.
**Needs its own roadmap decision before it is built** (the mockups show the
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.
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.
## Three layers
1. **Bulma, as shipped.** Vendored `bulma.min.css`, never edited. Only the
classes listed in `check.py` (`BULMA`) are used; reaching for another is a
design change.
2. **Adapters** — where display logic lives once:
`status_tag` (status → `tag is-… is-light`) as a template function, and
`copy_field` (the read-only value with its Copy button).
3. **Own components** — everything with an `sp-` class, styled in one
`panel.css`, each rendered by one partial in `components.html`:
| Partial | What it is |
|---|---|
| `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` |
| `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 |
| `side_menu` | Bulma `menu` with icons and count tags; sticky |
| `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.
## Layout rules
A page is exactly one of:
- **list** — `page_head`, then full-width boxes: the add form, then the table.
- **detail** — `page_head`, then `columns`: `is-3` `side_menu` + `is-9` boxes.
- **form** — `page_head`, then boxes in equal `columns`, one submit row below.
- **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.
## The contract
An implementer — human or model — **must not**:
1. Write an `sp-` class, a Bulma class outside the subset, or any other class
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
partial.
5. 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
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
evidence below exists.
**Changing the design** is allowed and has one route: mockup first. Edit the
fragment in `panel/src/`, the components page if a part changed, run
`build.py` and `check.py --write`, commit as `design: …` with the reason, get
it accepted — and only then touch `internal/web`.
## Enforcement
Guard tests live in `internal/web/view` and run in `go test ./...` and CI.
They land **before** any template is restyled (checklist step 2), written and
reviewed by models other than the one that implements the templates.
| Test | Fails when |
|---|---|
| `TestPanelClassVocabulary` | a template uses a class outside the Bulma subset + the `sp-` classes of `panel.css`; an `sp-` class is defined but unused, or used but absent from `components.html` |
| `TestPanelCSSContract` | `panel.css` has `!important`, an id or page-name selector, or a width outside `WIDTH_OK` |
| `TestTemplatesUseComponents` | a page template contains `sp-box-head`, `sp-postmark`, `sp-record`, `sp-facts`, … literally instead of `{{template "…"}}` |
| `TestPanelOutlinesMatchMockups` | a page rendered with fixtures has a different component skeleton than `outlines.json` — a different design cannot pass as this one |
| `TestVendoredBulmaChecksum` | `bulma.min.css` differs from the pinned SHA-256 |
| `TestNoTemplateUsesInlineScriptOrStyle` | (exists) inline `style` / script |
CI adds one step, `design-first`: a commit that touches
`internal/web/view/**` must not touch `docs/assets/panel-redesign/panel/**`
or the guard tests.
### Evidence for "done"
Every checklist step that restyles pages ends with, in the commit:
- `go test ./internal/web/...` output in the commit message body (pass);
- 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;
- a review by a model that did not write the step
([development.md](../development.md) § Model routing): mockup and screenshot
side by side, every difference either fixed or listed for the owner.
The owner accepts a step by looking at the pairs. A step without them is not
reviewed.
## Out of scope
- New features beyond the four rows listed under Information architecture.
- Dark scheme and the narrow-screen shell: Bulma ships both; they are drawn
and agreed as a `design:` change before anyone implements them.
- Weakening CSP; a JS bundler; a front-end framework.
## Implementation checklist
No code until the roadmap status is **agreed**.
- [ ] 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**
- [ ] `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: 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**
- [ ] `go vet`, `go test ./...`, e2e suite — **Haiku**