d49351c022
Codeberg is being retired as the project's public site, so every reference now points at GitHub. That includes the Go module path (codeberg.org/mix/selfpost → github.com/mixeme/selfpost): leaving an import path on a host that is going away would break `go get` and `go install`, so this is not only a docs change. Touches go.mod, test/e2e/go.mod, all imports, Makefile MODULE, the -ldflags version stamp in build/Dockerfile and docs/development.md, the licence headers in the SVG/HTML assets, and README (no more primary/mirror pair). Comments no longer cite the archived specification. "spec 7.6.1", "spec 5.1" and friends pointed into docs/archive/specification-v1.0.md, which is marked as not a source of truth; each is now a reference to the live document that owns the subject — architecture.md (with section), product.md, security.md or the README. The review only asked for the 7.x refs (code-review.md § 4), but 4/5/6/ 8/9 had the same defect, so they went too. Comments only, no behaviour change. Also closes the remaining review items: architecture.md gained a Code layers section with the layer diagram (A2), and TestParseDelivery gained the exotic mail.log cases (§ 3). Fixes a bug that last test found: the delivery-line pattern matched status= greedily, taking the *last* occurrence on the line. Postfix appends the remote server's reply verbatim, so a rejection whose reply quoted "status=sent" was filed as a delivered message in the send log. It now takes the first status= after the recipient, which is the real field. R7 (CONTRIBUTING.md) moved to roadmap 2.x — one developer, no external PR flow, so the file would have no audience yet. R1 (compose image tag) and the git tag stay in roadmap § v1.x as the release-commit steps. gofmt/go vet clean on both modules; go test ./... green except the three known Windows-only failures (file perms, backslash paths, renaming an open file). Not exercised on the dev server — no Docker locally. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
124 lines
4.8 KiB
Go
124 lines
4.8 KiB
Go
package web
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"html/template"
|
|
"net/http"
|
|
)
|
|
|
|
// templates holds the parsed page and fragment templates. Each page is parsed
|
|
// together with the shared base layout so {{ template "base" . }} works.
|
|
// Fragments (HTMX polling targets, architecture.md § Panel HTTP surface) are
|
|
// parsed standalone, without the layout, so they can be swapped into an
|
|
// existing page as an HTML snippet rather than a full document. Rendering
|
|
// always goes through html/template, which auto-escapes all interpolated data
|
|
// regardless (security.md).
|
|
type templates struct {
|
|
pages map[string]*template.Template
|
|
fragments map[string]*template.Template
|
|
}
|
|
|
|
// pageFiles maps a logical page name to its template files. Every page
|
|
// composes with layout.html; pages that embed a polling fragment
|
|
// (architecture.md § Panel HTTP surface) list that fragment's file too, so the
|
|
// same {{define}} block renders both the initial page and the fragment's own
|
|
// refresh responses identically. Pages sharing a block of markup (the
|
|
// encryption fields on the two secret downloads) list that partial the same
|
|
// way.
|
|
var pageFiles = map[string][]string{
|
|
"setup": {"templates/setup.html"},
|
|
"login": {"templates/login.html"},
|
|
"dashboard": {"templates/dashboard.html"},
|
|
"account": {"templates/account.html"},
|
|
"backup": {"templates/backup.html", "templates/encrypt_fields.html"},
|
|
"domain_detail": {"templates/domain_detail.html", "templates/encrypt_fields.html"},
|
|
"domain_delete": {"templates/domain_delete.html"},
|
|
"deliveries": {"templates/deliveries.html", "templates/deliveries_rows.html"},
|
|
"mail_queue": {"templates/mail_queue.html", "templates/mail_queue_body.html"},
|
|
"system_log": {"templates/system_log.html", "templates/system_log_body.html"},
|
|
"status": {"templates/status.html", "templates/status_body.html"},
|
|
}
|
|
|
|
// fragmentFiles maps a fragment name (also its {{define}} block name) to its
|
|
// template file, for standalone rendering by the HTMX polling endpoints.
|
|
var fragmentFiles = map[string]string{
|
|
"deliveries_rows": "templates/deliveries_rows.html",
|
|
"mail_queue_body": "templates/mail_queue_body.html",
|
|
"system_log_body": "templates/system_log_body.html",
|
|
"status_body": "templates/status_body.html",
|
|
}
|
|
|
|
func loadTemplates() (*templates, error) {
|
|
t := &templates{
|
|
pages: make(map[string]*template.Template),
|
|
fragments: make(map[string]*template.Template),
|
|
}
|
|
for name, files := range pageFiles {
|
|
patterns := append([]string{"templates/layout.html"}, files...)
|
|
tmpl, err := template.New("layout.html").ParseFS(assetsFS, patterns...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse template %s: %w", name, err)
|
|
}
|
|
t.pages[name] = tmpl
|
|
}
|
|
for name, file := range fragmentFiles {
|
|
tmpl, err := template.ParseFS(assetsFS, file)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse fragment %s: %w", name, err)
|
|
}
|
|
t.fragments[name] = tmpl
|
|
}
|
|
return t, nil
|
|
}
|
|
|
|
// render writes a page using the base layout. Rendering to a buffer first means
|
|
// a template error yields a clean 500 instead of a half-written page.
|
|
func (s *Server) render(w http.ResponseWriter, status int, page string, data any) {
|
|
tmpl, ok := s.tmpl.pages[page]
|
|
if !ok {
|
|
http.Error(w, "template not found", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
// The layout's navigation compares .Active against each item, so the key
|
|
// must exist on every authenticated page. Defaulting it here keeps a page
|
|
// 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 _, has := m["Active"]; !has {
|
|
m["Active"] = ""
|
|
}
|
|
m["Version"] = s.cfg.Version
|
|
}
|
|
var buf bytes.Buffer
|
|
if err := tmpl.ExecuteTemplate(&buf, "layout.html", data); err != nil {
|
|
logf("panel: render %s: %v", page, err)
|
|
http.Error(w, "internal error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
w.WriteHeader(status)
|
|
_, _ = buf.WriteTo(w)
|
|
}
|
|
|
|
// renderFragment writes an HTMX polling fragment as a bare HTML snippet, with
|
|
// no surrounding layout (architecture.md § Panel HTTP surface: fragment
|
|
// endpoints return HTML, not JSON).
|
|
func (s *Server) renderFragment(w http.ResponseWriter, status int, name string, data any) {
|
|
tmpl, ok := s.tmpl.fragments[name]
|
|
if !ok {
|
|
http.Error(w, "template not found", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
var buf bytes.Buffer
|
|
if err := tmpl.ExecuteTemplate(&buf, name, data); err != nil {
|
|
logf("panel: render fragment %s: %v", name, err)
|
|
http.Error(w, "internal error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
w.WriteHeader(status)
|
|
_, _ = buf.WriteTo(w)
|
|
}
|