diff --git a/CHANGELOG.md b/CHANGELOG.md index 580cc3e..bb96c0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -113,6 +113,11 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version the help drawer is gone. The user form sets the user's e-mail. The backup download is encrypted unless the box is unticked. Only the queue page is left on the ratchet. +- panel-redesign (stage 2): the **Outbound queue** page is in the new shell + with its content unchanged - the listing of `postqueue -p` and how + retries work. With it every page is built from the component kit: the old + stylesheet, the old layout and the engine's second rendering path are + deleted, and the ratchet is empty. ### Changed diff --git a/docs/assets/panel-redesign/evidence/out-queue.png b/docs/assets/panel-redesign/evidence/out-queue.png new file mode 100644 index 0000000..d574963 Binary files /dev/null and b/docs/assets/panel-redesign/evidence/out-queue.png differ diff --git a/docs/plans/panel-redesign.md b/docs/plans/panel-redesign.md index 3118c8c..6098cbd 100644 --- a/docs/plans/panel-redesign.md +++ b/docs/plans/panel-redesign.md @@ -519,8 +519,8 @@ only when both are done. Every stage-2 step ends with the evidence of - [x] Outbound: log, message, DMARC hub / domain / report — **Sonnet** - [x] Inbound: domains, domain with the *Spam filter* box, delete — **Sonnet** - [x] Server: system log, backup, users, user form, delete; Help as `help_topic` partials, the drawer removed — **Sonnet** -- [ ] Queues: old content inside the new shell (outbound), Inbound › Queue hidden — **Sonnet** -- [ ] Remove the old `panel.css`, the drawer code in `panel.js` and every class nothing references; `legacyPages` empty, vocabulary test green with no allow-list — **Sonnet** +- [x] Queues: old content inside the new shell (outbound), Inbound › Queue hidden — **Sonnet** +- [x] Remove the old `panel.css`, the drawer code in `panel.js` and every class nothing references; `legacyPages` empty, vocabulary test green with no allow-list — **Sonnet** **Stage 3 — reviews, docs, 2.0.0** diff --git a/docs/roadmap.md b/docs/roadmap.md index 02ea499..d646782 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -39,7 +39,7 @@ in `git log` and [CHANGELOG.md](../CHANGELOG.md). | panel-notifications | E-mail notifications about important events | candidate | — | — | | delivery-log-storage | Keep a message's `mail.log` lines with its send-log row | candidate | — | — | | password-reset | Password reset by e-mail | candidate | — | — | -| panel-redesign | Panel redesign (accepted mockups, design contract) | agreed | 21/27 | [plans/panel-redesign.md](plans/panel-redesign.md) | +| panel-redesign | Panel redesign (accepted mockups, design contract) | agreed | 23/27 | [plans/panel-redesign.md](plans/panel-redesign.md) | | schema-squash | Squash SQLite migrations into a 2.x baseline | built in panel-redesign (stage 1) | — | [plans/panel-redesign.md](plans/panel-redesign.md) | **Recommended order** (not binding; owner, 2026-10-09: feature order follows diff --git a/internal/web/handlers/authz.go b/internal/web/handlers/authz.go index a2d088e..4d3c337 100644 --- a/internal/web/handlers/authz.go +++ b/internal/web/handlers/authz.go @@ -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. diff --git a/internal/web/handlers/handlers_account_test.go b/internal/web/handlers/handlers_account_test.go index 1f2ba28..f266f02 100644 --- a/internal/web/handlers/handlers_account_test.go +++ b/internal/web/handlers/handlers_account_test.go @@ -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 diff --git a/internal/web/handlers/handlers_components_test.go b/internal/web/handlers/handlers_components_test.go index 8dadd3e..6ac9c94 100644 --- a/internal/web/handlers/handlers_components_test.go +++ b/internal/web/handlers/handlers_components_test.go @@ -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") - } } diff --git a/internal/web/handlers/handlers_monitor.go b/internal/web/handlers/handlers_monitor.go index 6deebfc..5d68b66 100644 --- a/internal/web/handlers/handlers_monitor.go +++ b/internal/web/handlers/handlers_monitor.go @@ -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 diff --git a/internal/web/handlers/handlers_monitor_test.go b/internal/web/handlers/handlers_monitor_test.go index d000320..0a390c6 100644 --- a/internal/web/handlers/handlers_monitor_test.go +++ b/internal/web/handlers/handlers_monitor_test.go @@ -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\"", + "

How retries work

", + "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, `
`, ``, ``, `name="password" type="password" autocomplete="new-password">`, `Leave empty to keep the current one.`, - ``, `