panel: show the running version in the layout footer
Nothing in the UI said which build was running, though it is the value a backup manifest is compared against on restore and the first thing worth knowing when the panel misbehaves — it was only in the startup log line and `panel -version`. Add it as a small footer in the shared layout, supplied from render() alongside .Active so no handler has to pass it, and gated on .User: the login and setup pages face the internet and should not advertise a version. Tests cover both the footer and render() supplying the key, since neither is visible from any single handler. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,11 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
- panel: every authenticated page now ends with the running version
|
||||||
|
(`SelfPost 1.1.0`) in a small footer. It is the value a backup manifest is
|
||||||
|
checked against on restore, and the first thing to establish when the panel
|
||||||
|
behaves unexpectedly. The login and setup pages deliberately do not show it.
|
||||||
|
|
||||||
- panel: the domain page now shows the **SPF and DMARC records it expects**,
|
- panel: the domain page now shows the **SPF and DMARC records it expects**,
|
||||||
with host, value and a Copy button, next to the DKIM record it already
|
with host, value and a Copy button, next to the DKIM record it already
|
||||||
showed — previously it only said "also configure SPF and DMARC (see the
|
showed — previously it only said "also configure SPF and DMARC (see the
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ td.actions { text-align: right; }
|
|||||||
@media (prefers-color-scheme: dark) { .code { background: #14171a !important; border-color: #2b3138 !important; } }
|
@media (prefers-color-scheme: dark) { .code { background: #14171a !important; border-color: #2b3138 !important; } }
|
||||||
h2 { font-size: 1.05rem; margin: 0 0 0.4rem; }
|
h2 { font-size: 1.05rem; margin: 0 0 0.4rem; }
|
||||||
.back { display: inline-block; margin-bottom: 1rem; }
|
.back { display: inline-block; margin-bottom: 1rem; }
|
||||||
|
/* Build version, closing every authenticated page. Quiet on purpose: it is
|
||||||
|
reference material, not something to read on the way past. */
|
||||||
|
.version { margin-top: 1.6rem; text-align: right; font-size: 0.8rem; color: #6b7280; }
|
||||||
select, textarea {
|
select, textarea {
|
||||||
width: 100%; padding: 0.55rem 0.7rem; font-size: 1rem;
|
width: 100%; padding: 0.55rem 0.7rem; font-size: 1rem;
|
||||||
border: 1px solid #cfd4da; border-radius: 6px; background: #fff; color: inherit;
|
border: 1px solid #cfd4da; border-radius: 6px; background: #fff; color: inherit;
|
||||||
|
|||||||
@@ -79,10 +79,13 @@ func (s *Server) render(w http.ResponseWriter, status int, page string, data any
|
|||||||
// The layout's navigation compares .Active against each item, so the key
|
// The layout's navigation compares .Active against each item, so the key
|
||||||
// must exist on every authenticated page. Defaulting it here keeps a page
|
// must exist on every authenticated page. Defaulting it here keeps a page
|
||||||
// that forgets it from failing to render — it simply highlights nothing.
|
// that forgets it from failing to render — it simply highlights nothing.
|
||||||
|
// .Version, shown in the layout's footer, is supplied the same way: it is
|
||||||
|
// the same value on every page, so no handler should have to pass it.
|
||||||
if m, ok := data.(map[string]any); ok {
|
if m, ok := data.(map[string]any); ok {
|
||||||
if _, has := m["Active"]; !has {
|
if _, has := m["Active"]; !has {
|
||||||
m["Active"] = ""
|
m["Active"] = ""
|
||||||
}
|
}
|
||||||
|
m["Version"] = s.cfg.Version
|
||||||
}
|
}
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
if err := tmpl.ExecuteTemplate(&buf, "layout.html", data); err != nil {
|
if err := tmpl.ExecuteTemplate(&buf, "layout.html", data); err != nil {
|
||||||
|
|||||||
@@ -19,6 +19,11 @@
|
|||||||
<main>
|
<main>
|
||||||
{{if .User}}{{template "nav" .}}{{end}}
|
{{if .User}}{{template "nav" .}}{{end}}
|
||||||
{{template "content" .}}
|
{{template "content" .}}
|
||||||
|
{{/* The running version, on every authenticated page: it is what a backup
|
||||||
|
manifest is checked against on restore and the first thing to establish
|
||||||
|
when something behaves unexpectedly. Only for signed-in administrators —
|
||||||
|
the login and setup pages must not advertise it to the internet. */}}
|
||||||
|
{{if .User}}<footer class="version">SelfPost {{.Version}}</footer>{{end}}
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>{{end}}
|
</html>{{end}}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package web
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"path"
|
"path"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -27,6 +29,68 @@ func TestEveryPageResolvesNav(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The version comes from render(), not from each handler's data map, so the
|
||||||
|
// footer is only correct as long as every page composes with the layout and
|
||||||
|
// render keeps supplying the key. Both are asserted here rather than trusted.
|
||||||
|
func TestLayoutShowsTheVersionOnlyWhenSignedIn(t *testing.T) {
|
||||||
|
tmpl, err := loadTemplates()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("loadTemplates: %v", err)
|
||||||
|
}
|
||||||
|
rendered := 0
|
||||||
|
for name := range tmpl.pages {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err := tmpl.pages[name].ExecuteTemplate(&buf, "layout.html", map[string]any{
|
||||||
|
"Title": "t", "User": "admin", "Active": "", "Version": "9.9.9-test",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
// Pages whose content block needs more data than this cannot be
|
||||||
|
// rendered here; the footer is in the shared layout, so one page
|
||||||
|
// that does render proves it for all of them.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rendered++
|
||||||
|
if !strings.Contains(buf.String(), "SelfPost 9.9.9-test") {
|
||||||
|
t.Errorf("page %q does not show the version in the layout footer", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if rendered == 0 {
|
||||||
|
t.Fatal("no page rendered, so the footer was never actually checked")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signed out (login, setup) the version must not be advertised.
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := tmpl.pages["login"].ExecuteTemplate(&buf, "layout.html", map[string]any{
|
||||||
|
"Title": "t", "Active": "", "Version": "9.9.9-test",
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("execute login: %v", err)
|
||||||
|
}
|
||||||
|
if strings.Contains(buf.String(), "9.9.9-test") {
|
||||||
|
t.Errorf("the login page shows the version to unauthenticated visitors:\n%s", buf.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenderSuppliesTheVersion(t *testing.T) {
|
||||||
|
tmpl, err := loadTemplates()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("loadTemplates: %v", err)
|
||||||
|
}
|
||||||
|
s := &Server{tmpl: tmpl, cfg: Config{Version: "9.9.9-test"}}
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
data := map[string]any{"Title": "t", "User": "admin"}
|
||||||
|
s.render(rec, http.StatusOK, "backup", data)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want 200", rec.Code)
|
||||||
|
}
|
||||||
|
if got := data["Version"]; got != "9.9.9-test" {
|
||||||
|
t.Errorf("render did not supply Version (got %v)", got)
|
||||||
|
}
|
||||||
|
if !strings.Contains(rec.Body.String(), "SelfPost 9.9.9-test") {
|
||||||
|
t.Errorf("rendered page does not show the version:\n%s", rec.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNavMarksActivePage(t *testing.T) {
|
func TestNavMarksActivePage(t *testing.T) {
|
||||||
tmpl, err := loadTemplates()
|
tmpl, err := loadTemplates()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user