All import paths updated to git.wntrmute.dev/mc/. Bumps mcdsl to v1.2.0. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
137 lines
3.2 KiB
Go
137 lines
3.2 KiB
Go
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)
|
|
}
|