// SPDX-FileCopyrightText: 2026 Echolot contributors
// SPDX-License-Identifier: GPL-3.0-or-later
package adminui
import (
"bytes"
"html/template"
"log/slog"
"net/http"
"strings"
)
// Templates are parsed once at start. html/template escapes by context, which is what makes it
// safe to render device names and finding text that ultimately arrived over a network.
var tpl = template.Must(template.New("base").Funcs(template.FuncMap{
"kb": func(n int64) int64 { return n / 1024 },
// verdictClass keeps an uploaded string out of the class attribute. The verdict arrives inside
// a document a device sent us, so interpolating it into markup would be trusting a stranger's
// text with a place in the stylesheet; mapping through a fixed set costs nothing and closes it.
"verdictClass": func(v string) string {
switch strings.ToLower(v) {
case "green", "yellow", "red", "inconclusive":
return "v-" + strings.ToLower(v)
default:
return "v-unknown"
}
},
// verdictLabel says what the light means rather than what it is called. "yellow" is a colour;
// "worth a look" is a finding, and the reader is here to act on it.
"verdictLabel": func(v string) string {
switch strings.ToLower(v) {
case "green":
return "clean"
case "yellow":
return "worth a look"
case "red":
return "faults found"
case "inconclusive":
return "inconclusive"
default:
return "not recorded"
}
},
}).Parse(baseHTML))
func (s *Server) render(w http.ResponseWriter, r *http.Request, page string, data map[string]any) {
data["Page"] = page
var buf bytes.Buffer
if err := tpl.Execute(&buf, data); err != nil {
slog.Error("admin template", "page", page, "err", err)
http.Error(w, "template error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
// There is no script here and nothing loaded from anywhere else, so a strict policy costs
// nothing and closes injected-script attacks even if an escaping bug ever slips through.
w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'")
w.Header().Set("Referrer-Policy", "no-referrer")
w.Header().Set("X-Content-Type-Options", "nosniff")
_, _ = buf.WriteTo(w)
}
// The visual language is an echo sounder's, which is what the name means: an instrument that emits
// a ping and reads what comes back. That gives the palette (the colours of a water column rather
// than a neutral near-black), the type (machine-set, because an instrument's readings are), and
// the one piece of real ornament — a trace of returns across time on the runs page.
//
// No web fonts: the CSP forbids loading anything, and shipping font files with a single Go binary
// would trade the property that makes this server pleasant to run for a typeface. So the character
// has to come from treatment — tracking, case, scale, rules — rather than from novel letterforms.
//
// Tables become stacked records below 46rem rather than scrolling sideways. That is not a fallback:
// a sounding log prints as label-and-value pairs, and on a phone that form is easier to read than
// any table, so the mobile layout is the more faithful one of the two.
const baseHTML = `
Echolot — {{.Page}}
echolot
{{if ne .Page "login"}}
{{.Session.Display}}{{if not .Session.Admin}} · your account{{end}}
{{end}}
{{if eq .Page "login"}}
Sign in
This server keeps the measurements your devices have uploaded.
{{if .Admin}}No devices yet. Create an enrolment link and open it on the phone
you want to measure from.{{else}}No devices yet. Sign in from the Echolot app on your phone to
link one to this account.{{end}}