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>
10 KiB
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/ | 29 screens — the reference for what each page looks like |
| panel/components.html | every component in every state — the only parts a page may be built from |
| panel/outlines.json | the component skeleton of each screen, generated from the mockups |
| 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/ |
Assets embedded (//go:embed), no CDN at runtime |
view.go |
CSP default-src 'self' — no inline script, no style="" |
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 |
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: 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 -jinstead of printingpostqueue -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
- Bulma, as shipped. Vendored
bulma.min.css, never edited. Only the classes listed incheck.py(BULMA) are used; reaching for another is a design change. - Adapters — where display logic lives once:
status_tag(status →tag is-… is-light) as a template function, andcopy_field(the read-only value with its Copy button). - Own components — everything with an
sp-class, styled in onepanel.css, each rendered by one partial incomponents.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, thencolumns:is-3side_menu+is-9boxes. - form —
page_head, then boxes in equalcolumns, 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:
- Write an
sp-class, a Bulma class outside the subset, or any other class that the components page does not show. - 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.
- Put a selector in
panel.cssthat names a page or an element id, uses!important, or sets a width outside theWIDTH_OKlist. - Write component markup by hand in a page template instead of calling the partial.
- Edit the vendored Bulma, or load a second CSS framework, icon set or any JavaScript widget.
- Change a mockup,
outlines.json,check.pyor a guard test in the same commit asinternal/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. - 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 § 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-firstCI step — Opus - Owner decision recorded here: keep today's paths or rename with redirects — owner
panel.cssfrompanel/theme.css;components.htmlpartials;status_tag; a/componentspage behind the global role rendering every partial — Sonnetlayout.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.cssrules 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