# 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, `

`, 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**