feat: add packaging, deployment, error pages, and project docs

Phase 7 — Polish:
- Error page template with styled 404/403/500 pages
- Error rendering helper on Renderer

Phase 8 — Packaging & Deployment:
- Containerfile: multi-stage build, non-root user, health check,
  OCI labels with build date and git revision
- Makefile: build, test, cross-compile, deb, rpm, container,
  tarballs, checksums targets
- nfpm.yaml: .deb and .rpm package config
- systemd service: hardened with NoNewPrivileges, ProtectSystem,
  ProtectHome, PrivateTmp, RestrictSUIDSGID
- Default environment file with commented examples
- postinstall/preremove scripts (shellcheck validated)
- compose.yaml: example Podman/Docker Compose
- Caddyfile.example: subdomain, subpath, and remote proxy configs
- CHANGELOG.md for release notes
- CLAUDE.md with architecture, conventions, and quick reference

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

View file

@ -143,3 +143,8 @@ func (h *Handler) Routes() *http.ServeMux {
return mux
}
// handleNotFound renders a styled 404 page.
func (h *Handler) handleNotFound(w http.ResponseWriter, r *http.Request) {
h.deps.Renderer.Error(w, r, http.StatusNotFound, "Ikke funnet")
}

View file

@ -53,7 +53,8 @@ func RequireAdmin(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user := UserFromContext(r.Context())
if user == nil || !user.IsAdmin() {
http.Error(w, "Forbidden", http.StatusForbidden)
w.WriteHeader(http.StatusForbidden)
w.Write([]byte("Forbidden"))
return
}
next.ServeHTTP(w, r)

View file

@ -152,6 +152,18 @@ 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{
Title: message,
Data: map[string]any{
"Code": code,
"Message": message,
},
})
}
// Partial renders a partial template (for HTMX responses).
func (r *Renderer) Partial(w io.Writer, name string, data any) error {
key := "partial:" + name