package admin
import (
"embed"
"fmt"
"html/template"
"io"
"net/http"
"time"
)
//go:embed templates/*.html static/*
var assetsFS embed.FS
// pageData is the common envelope passed to every rendered page.
type pageData struct {
Username string
CSRF string
Active string // nav key for highlight
Flash string
Data any
}
// renderer holds the parsed template set.
type renderer struct {
pages map[string]*template.Template
}
var tmplFuncs = template.FuncMap{
"fmtTime": func(t time.Time) string {
if t.IsZero() {
return "-"
}
return t.UTC().Format("2006-01-02 15:04:05Z")
},
}
// newRenderer parses base.html with each page template into its own set.
func newRenderer() (*renderer, error) {
pages := map[string][]string{
"login": {"templates/login.html"},
"dashboard": {"templates/base.html", "templates/dashboard.html"},
"codes": {"templates/base.html", "templates/codes.html"},
"nodes": {"templates/base.html", "templates/nodes.html"},
"audit": {"templates/base.html", "templates/audit.html"},
}
r := &renderer{pages: make(map[string]*template.Template, len(pages))}
for name, files := range pages {
t, err := template.New("").Funcs(tmplFuncs).ParseFS(assetsFS, files...)
if err != nil {
return nil, fmt.Errorf("admin.newRenderer parse %s: %w", name, err)
}
r.pages[name] = t
}
return r, nil
}
// render writes the named page. The login page renders itself; all others
// render through the "base" layout.
func (r *renderer) render(w http.ResponseWriter, name string, data pageData) {
t, ok := r.pages[name]
if !ok {
http.Error(w, "template not found", http.StatusInternalServerError)
return
}
root := "base"
if name == "login" {
root = "login"
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := t.ExecuteTemplate(w, root, data); err != nil {
// Header may already be written; log via stderr only.
fmt.Fprintf(io.Discard, "render error: %v", err)
}
}