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:
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
+2
-2
@@ -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>
|
||||||
@@ -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,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}}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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() {
|
||||||
|
|||||||
Reference in New Issue
Block a user