fix: address code review findings for Phase 7-8

Bugs fixed:
- Renderer.Error set WriteHeader before Content-Type, causing
  the header to be silently dropped. Now sets Content-Type first.
- truncate template function operated on bytes, not runes — could
  split multi-byte UTF-8 characters (Norwegian æøå). Now uses
  []rune for correct Unicode handling.

Performance:
- Skip session DB lookup (2 queries) on /static/ and /uploads/
  requests — these never use user context.

UX consistency:
- Replace all http.NotFound and http.Error("Forbidden") in
  handler layer with styled error pages via Renderer.Error.
- Add notFound/forbidden helper methods on Handler.

Deployment fixes:
- Remove false libc6/glibc deps from nfpm.yaml (binary is
  statically linked with CGO_ENABLED=0).
- Add CGO_ENABLED=0 to Makefile build target for consistency.
- Add .dockerignore to exclude .git, dist/, data/ from build
  context.
- Remove phantom 'lint' from Makefile .PHONY.
- Document ProtectSystem=strict constraint in systemd service.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2026-03-29 16:39:10 +02:00
commit aa5ab6b415
9 changed files with 73 additions and 32 deletions

View file

@ -154,14 +154,32 @@ func (r *Renderer) Page(w http.ResponseWriter, req *http.Request, name string, d
// Error renders an error page with the given HTTP status code.
func (r *Renderer) Error(w http.ResponseWriter, req *http.Request, code int, message string) {
w.WriteHeader(code)
r.Page(w, req, "error", PageData{
data := PageData{
Title: message,
Data: map[string]any{
"Code": code,
"Message": message,
},
})
}
// Populate common fields from context (same as Page does).
data.User = middleware.UserFromContext(req.Context())
data.CSRFToken = middleware.CSRFTokenFromContext(req.Context())
data.BasePath = r.cfg.BasePath
data.SiteName = r.cfg.SiteName
data.ExternalURL = r.cfg.ExternalURL
t, ok := r.templates["error"]
if !ok {
http.Error(w, message, code)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(code)
if err := t.ExecuteTemplate(w, "base.html", data); err != nil {
slog.Error("error template execute failed", "error", err)
}
}
// Partial renders a partial template (for HTMX responses).
@ -184,12 +202,14 @@ func (r *Renderer) templateFuncs() template.FuncMap {
"externalURL": func() string {
return r.cfg.ExternalURL
},
// truncate truncates a string to n characters.
// truncate truncates a string to n runes (not bytes) to avoid
// splitting multi-byte UTF-8 characters (Norwegian æøå etc.).
"truncate": func(n int, s string) string {
if len(s) <= n {
runes := []rune(s)
if len(runes) <= n {
return s
}
return s[:n] + "..."
return string(runes[:n]) + "..."
},
// join joins strings with a separator.
"join": strings.Join,