panel: align page URLs, titles, and headings
test / test (push) Has been cancelled

Settings moves to /settings with a 308 redirect from /account; domains, Status, Users, and user forms get matching browser titles and h1 text; backup page title reflects domain import.

Co-Authored-By: Composer <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-12 15:24:14 +03:00
parent b87baa6dd6
commit 1d30605533
15 changed files with 41 additions and 17 deletions
+9
View File
@@ -5,6 +5,15 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
## [Unreleased] ## [Unreleased]
### Changed
- panel: page URLs, browser titles, and headings are aligned — **Settings** is
now `/settings` (legacy `/account` redirects with 308); the domains list title
is `SelfPost — domains`; Status, Users, and user create/edit titles match their
nav labels and `<h1>` text; the backup page title is `SelfPost — backup &
migration` to reflect domain import as well as full backup. Operator guide and
architecture route tables updated.
### Fixed ### Fixed
- image: `mail.log` rotation no longer silently stops when the build context - image: `mail.log` rotation no longer silently stops when the build context
+2 -1
View File
@@ -170,7 +170,8 @@ unless noted. The table below is a summary — HTMX fragment endpoints
| `/system-log` | `mail.log` tail | | `/system-log` | `mail.log` tail |
| `/reload` | Reload OpenDKIM + Postfix maps | | `/reload` | Reload OpenDKIM + Postfix maps |
| `/backup` | Full backup download (page also hosts the import form) | | `/backup` | Full backup download (page also hosts the import form) |
| `/account` | Admin username/password | | `/settings` | Admin username/password and DMARC report address |
| `/users`, `/users/*` | Panel user CRUD (global admin only) |
HTMX polling refreshes monitoring fragments (5 s while the operator is active on HTMX polling refreshes monitoring fragments (5 s while the operator is active on
the page, 30 s when the tab is visible but idle, none when hidden — scheduled in the page, 30 s when the tab is visible but idle, none when hidden — scheduled in
+1 -1
View File
@@ -229,7 +229,7 @@ service healthy and will mail be accepted?"
- **Backup** (`/backup`) — download a full-server backup; the same page hosts - **Backup** (`/backup`) — download a full-server backup; the same page hosts
the domain-import form (`POST /domains/import`). See the domain-import form (`POST /domains/import`). See
[Backup, restore, and moving a single domain](#backup-restore-and-moving-a-single-domain). [Backup, restore, and moving a single domain](#backup-restore-and-moving-a-single-domain).
- **Settings** (`/account`) — change the administrator username and/or password. - **Settings** (`/settings`) — change the administrator username and/or password.
Application SASL logins are separate and are not changed here. Application SASL logins are separate and are not changed here.
**Sessions.** A login survives a container restart: sessions live in SQLite, not **Sessions.** A login survives a container restart: sessions live in SQLite, not
+3 -3
View File
@@ -49,7 +49,7 @@ func (h *Handlers) renderAccount(w http.ResponseWriter, r *http.Request, status
} }
data := h.pageBase(r) data := h.pageBase(r)
data["Title"] = "SelfPost — settings" data["Title"] = "SelfPost — settings"
data["Active"] = "account" data["Active"] = "settings"
data["FormUsername"] = formUsername data["FormUsername"] = formUsername
data["FormDMARCEmail"] = formDMARCEmail data["FormDMARCEmail"] = formDMARCEmail
data["ShowDMARC"] = showDMARC data["ShowDMARC"] = showDMARC
@@ -59,7 +59,7 @@ func (h *Handlers) renderAccount(w http.ResponseWriter, r *http.Request, status
data["ReportAuthHub"] = dnscheck.EmailDomain(formDMARCEmail) data["ReportAuthHub"] = dnscheck.EmailDomain(formDMARCEmail)
data["Error"] = formErr data["Error"] = formErr
data["Flash"] = accountFlash(r) data["Flash"] = accountFlash(r)
h.view.Render(w, status, "account", data) h.view.Render(w, status, "settings", data)
} }
func accountFlash(r *http.Request) string { func accountFlash(r *http.Request) string {
@@ -196,7 +196,7 @@ func (h *Handlers) submitAccount(w http.ResponseWriter, r *http.Request) {
} }
logf("panel: user %d account updated (username: %t, password: %t, dmarc email: %t)", user.ID, renaming, repassword, emailChanging) logf("panel: user %d account updated (username: %t, password: %t, dmarc email: %t)", user.ID, renaming, repassword, emailChanging)
http.Redirect(w, r, "/account?updated="+updatedFlag(renaming, repassword, emailChanging), http.StatusSeeOther) http.Redirect(w, r, "/settings?updated="+updatedFlag(renaming, repassword, emailChanging), http.StatusSeeOther)
} }
func updatedFlag(renamed, repassword, emailChanged bool) string { func updatedFlag(renamed, repassword, emailChanged bool) string {
+1 -1
View File
@@ -45,7 +45,7 @@ func (h *Handlers) renderBackupPage(w http.ResponseWriter, r *http.Request, stat
// wrong form. // wrong form.
func (h *Handlers) renderBackupPageWith(w http.ResponseWriter, r *http.Request, status int, importErr, backupErr string) { func (h *Handlers) renderBackupPageWith(w http.ResponseWriter, r *http.Request, status int, importErr, backupErr string) {
data := h.pageBase(r) data := h.pageBase(r)
data["Title"] = "SelfPost — backup" data["Title"] = "SelfPost — backup & migration"
data["Active"] = "backup" data["Active"] = "backup"
data["ImportErr"] = importErr data["ImportErr"] = importErr
data["BackupErr"] = backupErr data["BackupErr"] = backupErr
+1 -1
View File
@@ -41,7 +41,7 @@ func (h *Handlers) renderDashboard(w http.ResponseWriter, r *http.Request, statu
return return
} }
data := h.pageBase(r) data := h.pageBase(r)
data["Title"] = "SelfPost" data["Title"] = "SelfPost — domains"
data["Active"] = "domains" data["Active"] = "domains"
data["Domains"] = h.domainRows(domains) data["Domains"] = h.domainRows(domains)
data["Error"] = formErr data["Error"] = formErr
+5 -1
View File
@@ -121,7 +121,11 @@ func (h *Handlers) renderUserForm(w http.ResponseWriter, r *http.Request, status
return return
} }
data := h.pageBase(r) data := h.pageBase(r)
data["Title"] = "SelfPost — user" if userID != 0 {
data["Title"] = "SelfPost — edit user"
} else {
data["Title"] = "SelfPost — create user"
}
data["Active"] = "users" data["Active"] = "users"
data["UserID"] = userID data["UserID"] = userID
data["Domains"] = domains data["Domains"] = domains
@@ -53,7 +53,7 @@
they are policy, and the domain may already publish an SPF record for other they are policy, and the domain may already publish an SPF record for other
senders. SelfPost is a send-only relay — most operators have no inbox on the senders. SelfPost is a send-only relay — most operators have no inbox on the
sending domain, so the suggested DMARC record omits <code>rua=</code> unless a sending domain, so the suggested DMARC record omits <code>rua=</code> unless a
report address is configured below or in <a href="/account">Settings</a>. report address is configured below or in <a href="/settings">Settings</a>.
Publish both as TXT records.</p> Publish both as TXT records.</p>
<label>SPF — host / name</label> <label>SPF — host / name</label>
+1 -1
View File
@@ -81,7 +81,7 @@
{{template "sections" .}} {{template "sections" .}}
<div class="session"> <div class="session">
<span class="session-user muted">{{template "icon-account"}}User: {{.User}}</span> <span class="session-user muted">{{template "icon-account"}}User: {{.User}}</span>
{{if eq .Active "account"}}<span aria-current="page">{{template "icon-settings"}}Settings</span>{{else}}<a href="/account">{{template "icon-settings"}}Settings</a>{{end}} {{if eq .Active "settings"}}<span aria-current="page">{{template "icon-settings"}}Settings</span>{{else}}<a href="/settings">{{template "icon-settings"}}Settings</a>{{end}}
<form class="inline" method="post" action="/logout"> <form class="inline" method="post" action="/logout">
<button type="submit" class="danger">{{template "icon-sign-out"}}Sign out</button> <button type="submit" class="danger">{{template "icon-sign-out"}}Sign out</button>
</form> </form>
@@ -9,7 +9,7 @@
{{if .Error}}<p class="error">{{.Error}}</p>{{end}} {{if .Error}}<p class="error">{{.Error}}</p>{{end}}
{{if .ShowDMARC}} {{if .ShowDMARC}}
<form method="post" action="/account"> <form method="post" action="/settings">
<div class="split"> <div class="split">
<div class="card"> <div class="card">
<h2>Panel credentials</h2> <h2>Panel credentials</h2>
@@ -80,7 +80,7 @@ this one stays signed in.</p>
<h2>Panel credentials</h2> <h2>Panel credentials</h2>
<p class="muted">These are the credentials for this control panel only. <p class="muted">These are the credentials for this control panel only.
Applications keep their own logins and passwords, which are not affected.</p> Applications keep their own logins and passwords, which are not affected.</p>
<form method="post" action="/account"> <form method="post" action="/settings">
<label for="username">Username</label> <label for="username">Username</label>
<input id="username" name="username" autocomplete="username" <input id="username" name="username" autocomplete="username"
autocapitalize="none" spellcheck="false" value="{{.FormUsername}}" required> autocapitalize="none" spellcheck="false" value="{{.FormUsername}}" required>
+1 -1
View File
@@ -3,7 +3,7 @@
{{define "wide"}}wide{{end}} {{define "wide"}}wide{{end}}
{{define "content"}} {{define "content"}}
<h1>Server status</h1> <h1>Status</h1>
{{if .Flash}}<div class="flash">{{.Flash}}</div>{{end}} {{if .Flash}}<div class="flash">{{.Flash}}</div>{{end}}
+1 -1
View File
@@ -1,5 +1,5 @@
{{define "content"}} {{define "content"}}
<h1>Panel users</h1> <h1>Users</h1>
{{if .Flash}}<div class="flash">{{.Flash}}</div>{{end}} {{if .Flash}}<div class="flash">{{.Flash}}</div>{{end}}
+1 -1
View File
@@ -264,7 +264,7 @@ func TestOnlyThePagesMadeOfDataDeclareThemselvesWide(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("New: %v", err) t.Fatalf("New: %v", err)
} }
wide := map[string]bool{"account": true, "deliveries": true, "delivery": true, "mail_queue": true, "status": true, "system_log": true} wide := map[string]bool{"settings": true, "deliveries": true, "delivery": true, "mail_queue": true, "status": true, "system_log": true}
for name, page := range engine.Pages() { for name, page := range engine.Pages() {
var buf bytes.Buffer var buf bytes.Buffer
if err := page.ExecuteTemplate(&buf, "wide", nil); err != nil { if err := page.ExecuteTemplate(&buf, "wide", nil); err != nil {
+1 -1
View File
@@ -34,7 +34,7 @@ var pageFiles = map[string][]string{
"setup": {"templates/setup.html"}, "setup": {"templates/setup.html"},
"login": {"templates/login.html"}, "login": {"templates/login.html"},
"dashboard": {"templates/dashboard.html"}, "dashboard": {"templates/dashboard.html"},
"account": {"templates/account.html"}, "settings": {"templates/settings.html"},
"users": {"templates/users.html"}, "users": {"templates/users.html"},
"user_form": {"templates/user_form.html"}, "user_form": {"templates/user_form.html"},
"backup": {"templates/backup.html", "templates/encrypt_fields.html"}, "backup": {"templates/backup.html", "templates/encrypt_fields.html"},
+11 -1
View File
@@ -151,7 +151,8 @@ func (s *Server) Handler() http.Handler {
authed.HandleFunc("POST /applications/{aid}/delete", h.HandleDeleteApplication) authed.HandleFunc("POST /applications/{aid}/delete", h.HandleDeleteApplication)
authed.HandleFunc("POST /reload", h.HandleReload) authed.HandleFunc("POST /reload", h.HandleReload)
authed.HandleFunc("/account", h.HandleAccount) authed.HandleFunc("/settings", h.HandleAccount)
authed.HandleFunc("/account", redirectSettings)
authed.HandleFunc("GET /users", h.HandleUsers) authed.HandleFunc("GET /users", h.HandleUsers)
authed.HandleFunc("GET /users/new", h.HandleUserNew) authed.HandleFunc("GET /users/new", h.HandleUserNew)
@@ -174,6 +175,15 @@ func (s *Server) Handler() http.Handler {
return s.secure(mux) return s.secure(mux)
} }
// redirectSettings sends legacy /account bookmarks to /settings (308 preserves POST).
func redirectSettings(w http.ResponseWriter, r *http.Request) {
target := "/settings"
if r.URL.RawQuery != "" {
target += "?" + r.URL.RawQuery
}
http.Redirect(w, r, target, http.StatusPermanentRedirect)
}
func redirectHome(w http.ResponseWriter, r *http.Request) { func redirectHome(w http.ResponseWriter, r *http.Request) {
p, ok := auth.PrincipalFromRequest(r) p, ok := auth.PrincipalFromRequest(r)
if ok && !p.IsGlobal() { if ok && !p.IsGlobal() {