panel-redesign: the queue in the new shell; the old design is removed

Stage 2, steps 7 and 8 in one commit: the static guard fails as soon as
the ratchet holds no page while legacy.css still exists, and the tests
of the old layout need it while that layout exists - the two steps do
not split into states that pass on their own. Written by Sonnet,
reviewed by Opus.

The queue
  - out-queue, named after its mockup: today's content - the listing of
    postqueue -p in a log pane, polled as before (GET
    /outbound/queue/fragment) - and the "How retries work" facts. The
    mockup's table of messages is the roadmap's queue-as-a-table; the
    outline guard exempts this page for exactly that reason. Routes and
    the global-only check are unchanged; the page has no actions and
    never had.
  - Inbound has no Queue entry and no route serves one.

The old design is gone
  - Deleted: layout_legacy.html, legacy.css, logo-compact.svg,
    mail_queue.html, mail_queue_body.html.
  - view.go has one path: every page is layout.html + components.html +
    its own files; kitPages, kitFragments, the map-data branch of Render,
    the dict and back template functions and metaOf's map branch are
    removed. handlers: pageBase removed.
  - legacy_pages.txt holds its header and no entry.
  - panel.js: the old copy handler (button.copy in .code-row) is
    removed. A show-when rule now applies to every element it matches,
    and hiding adds Bulma's is-hidden beside the hidden attribute - a
    .box or a grid row has a display of its own and stayed visible.
  - The user form hides both domain lists again when the role is Global
    (Box.DomainPick, an attribute; data-global-role on the select).
  - Tests of the old layout are deleted; what they asserted about the
    shell (version only signed in, the menu follows the user's reach,
    Inbound only with the feature, Help present, the current entry
    marked) is asserted on the kit pages, two tests added for it.

For the owner: is-hidden is a Bulma class outside the subset of
check.py. No template writes it - the script sets it at run time to
hide a block - so no guard sees it; say if it should be on the list.

Evidence
  - docs/assets/panel-redesign/evidence/out-queue.png: 1440 px wide,
    headless Edge, the real template served locally with its fixture -
    NOT from the container (no Docker on this machine).
  - panel/audit.js on out-queue, user and backup at 1440 and 1280:
    366 texts checked, problems: [].
  - In a browser against the local fixture server: the encryption
    fields of Backup, the manual / auto limit fields and the custom
    report address of Domain settings show and hide with their control;
    all 28 fixture pages answer 200 with panel.css, /static/legacy.css
    answers 404.
  - Not exercised: polling against a real postqueue (none on Windows).
  - go test -count=1 ./internal/web/... ./cmd/panel
      ok  internal/web
      ok  internal/web/auth
      ok  internal/web/handlers
      ok  internal/web/validate
      ok  internal/web/view
      ok  cmd/panel

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mix
2026-10-10 02:36:04 +03:00
co-authored by Claude Opus 5.5
parent fa696efa8b
commit 32c6ae98a1
32 changed files with 329 additions and 1712 deletions
+81
View File
@@ -0,0 +1,81 @@
package view
// The data of the Outbound queue (templates/out_queue.html, mockup
// out-queue.html): what Postfix is still holding, and the retry policy that
// decides how long it holds it. The queue box polls; the page and its fragment,
// GET /outbound/queue/fragment, render the same box
// (templates/out_queue_body.html).
//
// Until the queue is a table (docs/roadmap.md, parse postqueue -j) the box
// shows postqueue's own listing in a log pane, as the system log shows
// mail.log: the mockup's table is not drawn from data the panel has.
//
// A page is a typed struct that embeds Meta, built by a constructor and filled
// by With… methods; the handler reads Postfix, the view decides what is said and
// how it is worded.
import "strings"
const outboundQueue = "/outbound/queue"
// OutQueue is the data of the Outbound queue. It is for the global role.
type OutQueue struct {
Meta
Head Head
Queue Box
Lines []LogLine
// Empty is shown in place of the listing when there is none: the queue holds
// nothing, or it could not be read.
Empty EmptyState
Retries Box
Policy []Fact
}
// NewOutQueue builds the page. listing is what postqueue printed, as it was
// read, and errText is why it could not be read, empty when it could.
func NewOutQueue(m Meta, listing, errText string) *OutQueue {
m.Title, m.Section, m.Page = "Outbound queue", "outbound", "queue"
p := &OutQueue{
Meta: m,
Head: Head{
Kicker: "Outbound",
Title: "Outbound queue",
Lead: Rich("What applications handed over and the receiving server has not yet taken, as ", Code("postqueue -p"), " lists it."),
},
Queue: Box{No: "01", Title: "Waiting", ID: "out-queue", Poll: outboundQueue + "/fragment",
End: Plain("Refreshes on its own")},
Empty: EmptyState{Icon: "ti-stack-2", Text: Plain("Queue is empty.")},
Retries: Box{No: "02", Title: "How retries work"},
}
if errText != "" {
p.Empty = EmptyState{Icon: "ti-alert-circle", Text: Plain(errText)}
return p
}
if strings.TrimSpace(listing) == "" {
return p
}
for _, line := range strings.Split(strings.TrimRight(listing, "\r\n"), "\n") {
p.Lines = append(p.Lines, LogLine{Text: strings.TrimRight(line, "\r")})
}
return p
}
// WithRetryPolicy fills the second box: the first retry, the cap the later ones
// double up to and how long a message is kept, each as the handler words it.
// fromDefaults says the Postfix configuration could not be read and these are
// the compiled-in values.
func (p *OutQueue) WithRetryPolicy(firstRetry, backoffCap, lifetime string, fromDefaults bool) *OutQueue {
p.Retries.End = Plain("This Postfix's policy, read at panel start")
if fromDefaults {
p.Retries.End = Plain("Compiled-in defaults: the Postfix configuration could not be read")
}
p.Policy = []Fact{
{Label: "First retry", Value: Plain(firstRetry), Big: true},
{Label: "Later retries", Value: Plain("doubling"), Big: true, Note: Plain("capped at " + backoffCap)},
{Label: "Kept in queue", Value: Plain(lifetime), Big: true},
{Label: "Then", Value: Plain("bounced"), Big: true, Note: Plain("no attempt counter, only time")},
}
return p
}