package webserver import ( "fmt" "html/template" "io/fs" "net/http" "strings" "time" "git.wntrmute.dev/kyle/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) }