// Package web serves the two HTML surfaces: a landing page on the // client-facing port and a read-only dashboard on the admin one. // // Server-rendered templates with one stylesheet, embedded in the binary. The // single-binary goal rules out a JavaScript toolchain — a build step in CI, a // node runtime on the runner, and megabytes of bundle — and nothing on either // page needs one. The dashboard refreshes by reloading. // // The split between the two pages is a security boundary, not a layout choice. // The landing page is served to anyone who can reach the address, so it // contains no data at all: it says how to connect, not what is here. The // dashboard shows the cluster and belongs on the admin listener, which is bound // to loopback — see Dashboard. package web import ( "embed" "fmt" "html/template" "net/http" "strings" "time" ) //go:embed templates var files embed.FS var funcs = template.FuncMap{ "join": func(v []string) string { if len(v) == 0 { return "—" } return strings.Join(v, ", ") }, "duration": func(sec int64) string { return time.Duration(sec * int64(time.Second)).Round(time.Second).String() }, "ago": func(t time.Time) string { if t.IsZero() { return "never" } return time.Since(t).Round(time.Second).String() + " ago" }, // Spend is stored in micro-units so accumulation is exact; it is shown in // whole units because that is the number an operator set a budget in. "units": func(micros int64) string { return fmt.Sprintf("%.2f", float64(micros)/1e6) }, } var tmpl = template.Must(template.New("").Funcs(funcs).ParseFS(files, "templates/*.html")) // Static serves the stylesheet, mounted on both listeners because both pages // use it and it contains nothing. // // One file rather than a directory server: exposing a file server over the // embedded templates would also serve the templates themselves, which is // harmless today and the kind of thing that stops being harmless later. func Static() http.Handler { css, err := files.ReadFile("templates/base.css") if err != nil { panic("web: the stylesheet is missing from the binary: " + err.Error()) } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/_static/base.css" { http.NotFound(w, r) return } w.Header().Set("Content-Type", "text/css; charset=utf-8") w.Header().Set("Cache-Control", "public, max-age=3600") _, _ = w.Write(css) }) } func render(w http.ResponseWriter, name string, data any) { w.Header().Set("Content-Type", "text/html; charset=utf-8") if err := tmpl.ExecuteTemplate(w, name, data); err != nil { // The status is already written by then, so there is nothing honest to // send; the page simply ends short. http.Error(w, "template error", http.StatusInternalServerError) } }