internal/web/web.go
96 lines · 2336 bytes
1// Package web holds the server-rendered templates and static assets for the
2// read-only UI. No JavaScript, no build step.
3package web
4
5import (
6 "embed"
7 "html/template"
8 "io"
9 "runtime/debug"
10 "strings"
11 "sync"
12 "time"
13)
14
15//go:embed templates/*.html
16var templateFS embed.FS
17
18//go:embed static/style.css
19var StyleCSS []byte
20
21//go:embed static/favicon.svg
22var FaviconSVG []byte
23
24// version returns the short VCS revision baked into the binary, or "" when
25// built outside a checkout. Used by the layout footer.
26var version = sync.OnceValue(func() string {
27 info, ok := debug.ReadBuildInfo()
28 if !ok {
29 return ""
30 }
31 for _, s := range info.Settings {
32 if s.Key == "vcs.revision" && len(s.Value) >= 10 {
33 return s.Value[:10]
34 }
35 }
36 return ""
37})
38
39// fullVersion is the complete VCS revision, for linking the footer hash
40// to the upstream commit page.
41var fullVersion = sync.OnceValue(func() string {
42 info, ok := debug.ReadBuildInfo()
43 if !ok {
44 return ""
45 }
46 for _, s := range info.Settings {
47 if s.Key == "vcs.revision" {
48 return s.Value
49 }
50 }
51 return ""
52})
53
54var funcs = template.FuncMap{
55 "gitbayVersion": func() string { return version() },
56 "gitbayCommit": func() string { return fullVersion() },
57 // paragraphs splits plain text on blank lines for safe rich display.
58 "paragraphs": func(s string) []string {
59 var out []string
60 for _, p := range strings.Split(s, "\n\n") {
61 if p = strings.TrimSpace(p); p != "" {
62 out = append(out, p)
63 }
64 }
65 return out
66 },
67 // short abbreviates a commit SHA for display.
68 "short": func(s string) string {
69 if len(s) > 10 {
70 return s[:10]
71 }
72 return s
73 },
74 "add": func(a, b int) int { return a + b },
75 "sub": func(a, b int) int { return a - b },
76 // when formats a stored RFC3339 timestamp for display; unparseable
77 // values pass through unchanged.
78 "when": func(s string) string {
79 t, err := time.Parse(time.RFC3339Nano, s)
80 if err != nil {
81 return s
82 }
83 return t.UTC().Format("2006-01-02 15:04")
84 },
85}
86
87// Render executes the named page template with the shared layout.
88func Render(w io.Writer, page string, data any) error {
89 t, err := template.Must(
90 template.New("layout.html").Funcs(funcs).ParseFS(templateFS, "templates/layout.html"),
91 ).ParseFS(templateFS, "templates/"+page)
92 if err != nil {
93 return err
94 }
95 return t.ExecuteTemplate(w, "layout", data)
96}