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:
@@ -22,17 +22,6 @@ func (h *Handlers) requireGlobal(w http.ResponseWriter, r *http.Request) (auth.P
|
||||
return p, true
|
||||
}
|
||||
|
||||
func (h *Handlers) pageBase(r *http.Request) map[string]any {
|
||||
p, _ := h.principal(r)
|
||||
return map[string]any{
|
||||
"User": auth.CurrentUser(r),
|
||||
"IsGlobal": p.IsGlobal(),
|
||||
// Which groups the menu shows this user (plan § Who sees what).
|
||||
"HasOutbound": p.HasOutbound(),
|
||||
"HasInbound": p.HasInbound(),
|
||||
}
|
||||
}
|
||||
|
||||
// shellMeta is what the shell of a page of the component kit needs to know
|
||||
// about the person looking at it: their name and which groups of the menu they
|
||||
// reach. The page's constructor adds its title and where it sits in the menu.
|
||||
|
||||
@@ -184,9 +184,6 @@ func TestAccountPageIsDrawnInTheShell(t *testing.T) {
|
||||
t.Errorf("account page missing %q", want)
|
||||
}
|
||||
}
|
||||
if strings.Contains(out, "/static/legacy.css") {
|
||||
t.Error("account page loads the old stylesheet")
|
||||
}
|
||||
}
|
||||
|
||||
// A refused form is shown again with the error above the boxes and what the
|
||||
|
||||
@@ -50,7 +50,4 @@ func TestComponentsPageRendersTheKitForTheGlobalRole(t *testing.T) {
|
||||
t.Errorf("the kit page is missing %q", want)
|
||||
}
|
||||
}
|
||||
if strings.Contains(out, "/static/legacy.css") {
|
||||
t.Error("the kit page loads the old stylesheet")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"github.com/mixeme/selfpost/internal/mailhdr"
|
||||
"github.com/mixeme/selfpost/internal/postfix"
|
||||
"github.com/mixeme/selfpost/internal/store"
|
||||
"github.com/mixeme/selfpost/internal/web/auth"
|
||||
"github.com/mixeme/selfpost/internal/web/view"
|
||||
)
|
||||
|
||||
@@ -333,26 +332,15 @@ func parsePage(v string) int {
|
||||
return n
|
||||
}
|
||||
|
||||
// HandleMailQueue renders the Mail queue page (architecture.md § Panel HTTP
|
||||
// HandleMailQueue renders the Outbound queue page (architecture.md § Panel HTTP
|
||||
// surface).
|
||||
func (h *Handlers) HandleMailQueue(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := h.requireGlobal(w, r); !ok {
|
||||
return
|
||||
}
|
||||
out, errText := readQueue()
|
||||
policy := h.cfg.RetryPolicy
|
||||
h.view.Render(w, http.StatusOK, "mail_queue", map[string]any{
|
||||
"Title": "SelfPost — mail queue",
|
||||
"User": auth.CurrentUser(r),
|
||||
"Active": "mail_queue",
|
||||
"IsGlobal": true,
|
||||
"Output": out,
|
||||
"Error": errText,
|
||||
"FirstRetry": policy.FirstRetry(),
|
||||
"BackoffCap": policy.BackoffCap(),
|
||||
"QueueLifetime": policy.QueueLifetime(),
|
||||
"RetryFromDefaults": policy.FromDefaults,
|
||||
})
|
||||
h.view.Render(w, http.StatusOK, "out-queue", h.outQueuePage(r).
|
||||
WithRetryPolicy(policy.FirstRetry(), policy.BackoffCap(), policy.QueueLifetime(), policy.FromDefaults))
|
||||
}
|
||||
|
||||
// HandleMailQueueBody serves the HTMX polling fragment for the queue view.
|
||||
@@ -360,11 +348,14 @@ func (h *Handlers) HandleMailQueueBody(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := h.requireGlobal(w, r); !ok {
|
||||
return
|
||||
}
|
||||
h.view.RenderFragment(w, http.StatusOK, "out_queue_body", h.outQueuePage(r))
|
||||
}
|
||||
|
||||
// outQueuePage reads the queue and gives it to the view; the page and its
|
||||
// fragment draw the same box from it.
|
||||
func (h *Handlers) outQueuePage(r *http.Request) *view.OutQueue {
|
||||
out, errText := readQueue()
|
||||
h.view.RenderFragment(w, http.StatusOK, "mail_queue_body", map[string]any{
|
||||
"Output": out,
|
||||
"Error": errText,
|
||||
})
|
||||
return view.NewOutQueue(h.shellMeta(r), out, errText)
|
||||
}
|
||||
|
||||
// readQueue runs postqueue -p, returning a friendly message instead of the
|
||||
|
||||
@@ -479,17 +479,17 @@ func fixtureRetryPolicy() postfix.RetryPolicy {
|
||||
}
|
||||
}
|
||||
|
||||
// The retry card sits on the page itself, outside the HTMX poll, and prints
|
||||
// The retry policy sits on the page itself, outside the HTMX poll, and prints
|
||||
// whatever policy was cached on Config — never a live postconf.
|
||||
func TestMailQueueShowsRetryPolicyCard(t *testing.T) {
|
||||
h := &Handlers{view: mustView(t), cfg: Config{Version: "test", RetryPolicy: fixtureRetryPolicy()}}
|
||||
|
||||
out := getBody(t, h.HandleMailQueue, "/outbound/queue")
|
||||
for _, want := range []string{
|
||||
"How delivery retries work",
|
||||
"id=\"retry-policy\"",
|
||||
"<h2>How retries work</h2>",
|
||||
"This Postfix's policy, read at panel start",
|
||||
">10 minutes<",
|
||||
"doubling, cap about 1 hour 7 minutes",
|
||||
"capped at about 1 hour 7 minutes",
|
||||
">2 days<",
|
||||
} {
|
||||
if !strings.Contains(out, want) {
|
||||
@@ -499,7 +499,7 @@ func TestMailQueueShowsRetryPolicyCard(t *testing.T) {
|
||||
if strings.Contains(out, ">5 minutes<") || strings.Contains(out, ">5 days<") {
|
||||
t.Errorf("mail queue shows stock defaults instead of the fixture:\n%s", out)
|
||||
}
|
||||
if strings.Contains(out, "compiled-in defaults") {
|
||||
if strings.Contains(out, "Compiled-in defaults") {
|
||||
t.Error("a fixture policy must not show the fallback note")
|
||||
}
|
||||
}
|
||||
@@ -508,7 +508,10 @@ func TestMailQueueBodyOmitsRetryPolicyCard(t *testing.T) {
|
||||
h := &Handlers{view: mustView(t), cfg: Config{RetryPolicy: fixtureRetryPolicy()}}
|
||||
|
||||
out := getBody(t, h.HandleMailQueueBody, "/outbound/queue/fragment")
|
||||
if strings.Contains(out, "How delivery retries work") || strings.Contains(out, "10 minutes") {
|
||||
if !strings.HasPrefix(out, `<div class="box" id="out-queue" data-poll`) || strings.Contains(out, "<html") {
|
||||
t.Errorf("the fragment is not the polled box alone:\n%.200s", out)
|
||||
}
|
||||
if strings.Contains(out, "How retries work") || strings.Contains(out, "10 minutes") {
|
||||
t.Errorf("HTMX fragment includes the retry card:\n%s", out)
|
||||
}
|
||||
}
|
||||
@@ -517,7 +520,7 @@ func TestMailQueueNotesCompiledInFallback(t *testing.T) {
|
||||
h := &Handlers{view: mustView(t), cfg: Config{RetryPolicy: postfix.DefaultRetryPolicy()}}
|
||||
|
||||
out := getBody(t, h.HandleMailQueue, "/outbound/queue")
|
||||
if !strings.Contains(out, "compiled-in defaults") {
|
||||
if !strings.Contains(out, "Compiled-in defaults") {
|
||||
t.Errorf("fallback note missing:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,8 @@ import (
|
||||
|
||||
// The Outbound log and the page of one message, rendered from the real journal
|
||||
// by the handlers. Their markup is held to the mockups by the guards in
|
||||
// internal/web/view; what is checked here is the behaviour the pages keep from
|
||||
// the legacy ones — the polled region, the paging, the scoping — on the new
|
||||
// markup.
|
||||
// internal/web/view; what is checked here is the behaviour of the pages — the
|
||||
// polled region, the paging, the scoping.
|
||||
|
||||
// regionOf cuts the polled region (the table and the foot under it) out of a
|
||||
// body: from the table's container to the end of the foot.
|
||||
|
||||
@@ -223,7 +223,7 @@ func TestUserFormOfTheOnlyGlobalUserSavesUntouched(t *testing.T) {
|
||||
pv := map[string]string{"uid": idStr(root.ID)}
|
||||
|
||||
get := send(h.HandleUserEdit, &root, "GET", path, pv, nil).Body.String()
|
||||
for _, want := range []string{`<input type="hidden" name="role" value="global">`, `<select id="role" name="role" disabled>`} {
|
||||
for _, want := range []string{`<input type="hidden" name="role" value="global">`, `<select id="role" name="role" data-global-role="global" disabled>`} {
|
||||
if !strings.Contains(get, want) {
|
||||
t.Errorf("the form of the only global user is missing %q", want)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user