package webserver
import (
"fmt"
"html/template"
"io/fs"
"net/http"
"strings"
"time"
"git.wntrmute.dev/mc/mcr/web"
)
// templateSet wraps parsed templates and provides a render method.
type templateSet struct {
templates map[string]*template.Template
}
// templateFuncs returns the function map used across all templates.
func templateFuncs() template.FuncMap {
return template.FuncMap{
"formatSize": formatSize,
"formatTime": formatTime,
"truncate": truncate,
"joinStrings": joinStrings,
}
}
// loadTemplates parses all page templates with the layout template.
func loadTemplates() (*templateSet, error) {
// Read layout template.
layoutBytes, err := fs.ReadFile(web.Content, "templates/layout.html")
if err != nil {
return nil, fmt.Errorf("webserver: read layout template: %w", err)
}
layoutStr := string(layoutBytes)
pages := []string{
"login",
"dashboard",
"repositories",
"repository_detail",
"manifest_detail",
"policies",
"audit",
}
ts := &templateSet{
templates: make(map[string]*template.Template, len(pages)),
}
for _, page := range pages {
pageBytes, readErr := fs.ReadFile(web.Content, "templates/"+page+".html")
if readErr != nil {
return nil, fmt.Errorf("webserver: read template %s: %w", page, readErr)
}
t, parseErr := template.New("layout").Funcs(templateFuncs()).Parse(layoutStr)
if parseErr != nil {
return nil, fmt.Errorf("webserver: parse layout for %s: %w", page, parseErr)
}
_, parseErr = t.Parse(string(pageBytes))
if parseErr != nil {
return nil, fmt.Errorf("webserver: parse template %s: %w", page, parseErr)
}
ts.templates[page] = t
}
return ts, nil
}
// render executes a named template and writes the result to w.
func (ts *templateSet) render(w http.ResponseWriter, name string, data any) {
t, ok := ts.templates[name]
if !ok {
http.Error(w, "template not found", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := t.Execute(w, data); err != nil {
// Template already started writing; log but don't send another error.
_ = err // best-effort; headers may already be sent
}
}
// formatSize converts bytes to a human-readable string.
func formatSize(b int64) string {
const (
kib = 1024
mib = 1024 * kib
gib = 1024 * mib
tib = 1024 * gib
)
switch {
case b >= tib:
return fmt.Sprintf("%.1f TiB", float64(b)/float64(tib))
case b >= gib:
return fmt.Sprintf("%.1f GiB", float64(b)/float64(gib))
case b >= mib:
return fmt.Sprintf("%.1f MiB", float64(b)/float64(mib))
case b >= kib:
return fmt.Sprintf("%.1f KiB", float64(b)/float64(kib))
default:
return fmt.Sprintf("%d B", b)
}
}
// formatTime converts an RFC3339 string to a more readable format.
func formatTime(s string) string {
t, err := time.Parse(time.RFC3339, s)
if err != nil {
// Try RFC3339Nano.
t, err = time.Parse(time.RFC3339Nano, s)
if err != nil {
return s
}
}
return t.Format("2006-01-02 15:04:05")
}
// truncate returns the first n characters of s, appending "..." if truncated.
func truncate(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n] + "..."
}
// joinStrings joins string slices for template rendering.
func joinStrings(ss []string, sep string) string {
return strings.Join(ss, sep)
}