diff --git a/server/internal/adminui/render.go b/server/internal/adminui/render.go index 9b30a05..44b93b6 100644 --- a/server/internal/adminui/render.go +++ b/server/internal/adminui/render.go @@ -8,12 +8,40 @@ import ( "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) { @@ -33,119 +61,228 @@ func (s *Server) render(w http.ResponseWriter, r *http.Request, page string, dat _, _ = 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}} -{{if ne .Page "login"}} -
-

Echolot

- - {{.Session.Display}}{{if not .Session.Admin}} (your account){{end}} +
+

echolot

+ {{if ne .Page "login"}} + + {{.Session.Display}}{{if not .Session.Admin}} · your account{{end}}
+ {{end}}
-{{end}}
{{if eq .Page "login"}}

Sign in

- {{with .Error}}

{{.}}

{{end}} +

This server keeps the measurements your devices have uploaded.

{{if .OIDC}} -

-

or use the break-glass account:

+

Sign in with your identity provider

{{end}} {{if .LocalSet}} -
-

-

+ +

Break-glass account

+ + + +

{{else}} -

No break-glass admin is set. Run - echolot-server --set-admin-password on the host.

+

No break-glass account is set. Run + echolot-server --set-admin-password on the host to create one.

{{end}} {{else if eq .Page "dashboard"}} -
-
{{.Devices}}{{if .Admin}}devices{{else}}your devices{{end}}
- {{if .Admin}}
{{.Linked}}signed in
{{end}} -
{{.Runs}}{{if .Admin}}stored runs{{else}}your runs{{end}}
-
+

{{if .Admin}}This server{{else}}Your account{{end}}

+
    +
  • {{if .Admin}}devices enrolled{{else}}your devices{{end}} + {{.Devices}}
  • + {{if .Admin}} +
  • linked to an account + {{.Linked}}
  • + {{end}} +
  • {{if .Admin}}runs stored{{else}}your runs{{end}} + {{.Runs}}
  • + {{if .Admin}} +
  • server version + {{.Version}}
  • + {{end}} +
{{if not .Admin}} -
-

This is your account. You can see the devices you have signed in on, review everything - they have uploaded, and delete any of it.

-

Administering the server — enrolling devices, revoking them, and - seeing other people's uploads — needs an administrator account.

+
+

You can see every device you have signed in on, read everything they have uploaded, and + delete any of it.

+

Enrolling devices, revoking them, and reading other people's uploads need an + administrator account.

{{end}} - {{if .Admin}} -
-

Server

-

version {{.Version}}

- {{with .SelfTest}}
{{printf "%+v" .}}
{{end}} -
+ {{with .SelfTest}} +

Self-test

+

What this server can measure from where it stands. A capability missing here is + missing from every run it takes part in.

+
{{printf "%+v" .}}
{{end}} {{else if eq .Page "devices"}}

{{if .Admin}}Devices{{else}}Your devices{{end}}

{{with .Link}} -
-

Enrolment link — single use, valid 24 hours. Treat it like a password until spent.

+
+

Enrolment link

+

Single use, valid 24 hours. Treat it like a password until it is spent.

{{.}}

-

On a device with adb:
- adb shell am start -a android.intent.action.VIEW -d "{{.}}"

+

adb shell am start -a android.intent.action.VIEW -d "{{.}}"

{{end}} {{if .Admin}} @@ -154,48 +291,71 @@ const baseHTML = ` {{end}} -
- - {{range .Rows}} - - - - - - - - - {{else}} - - {{end}} -
DeviceNameAccountEnrolledRuns
{{.ID}}{{if .Name}}{{.Name}}{{else}}{{end}}{{if .LinkedToAccount}}{{.AccountName}}{{else}}not signed in{{end}}{{.Enrolled.Format "2006-01-02 15:04"}}{{.Runs}}{{if $.Admin}}
+ {{if .Rows}}
+ {{range .Rows}} +
+
+ {{if .Name}}{{.Name}}{{else}}{{.ID}}{{end}} + {{if .LinkedToAccount}}{{.AccountName}} + {{else}}no account{{end}} +
+
    +
  • device{{.ID}}
  • +
  • enrolled + {{.Enrolled.Format "2006-01-02 15:04"}}
  • +
  • runs{{.Runs}}
  • +
+ {{if $.Admin}} + - {{end}}
{{if $.Admin}}No devices enrolled.{{else}}You have not signed in on any device yet. Sign in from the Echolot app to link one.{{end}}
+ + + {{end}} +
+ {{end}} +
{{else}} +

{{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}}

+ {{end}} {{else if eq .Page "runs"}}

{{if .Admin}}Uploaded runs{{else}}Your uploaded runs{{end}}

-

Shown exactly as uploaded, at the privacy level the uploader chose. Nothing - here can un-redact a run.

-
- - {{range .Rows}} - - - - - - - - - - {{else}} - - {{end}} -
UploadedDeviceVerdictFindingsSizeLevel
{{.UploadedAt.Format "2006-01-02 15:04"}}{{.DeviceName}}{{if .Verdict}}{{.Verdict}}{{else}}{{end}}{{.FindingCount}}{{kb .SizeBytes}} kB{{.Anonymization}}open
Nothing uploaded yet.
+

Each run is shown exactly as it arrived, at the privacy level its uploader chose. + Nothing here can un-redact one.

+ {{if .Rows}} +
{{range .Rows}}{{end}}
+

oldest → newest + clean + worth a look + faults + inconclusive

+ {{end}} + {{if .Rows}}
+ {{range .Rows}} +
+
+ {{.UploadedAt.Format "2006-01-02 15:04"}} + {{verdictLabel .Verdict}} +
+
    +
  • device{{.DeviceName}}
  • +
  • findings{{.FindingCount}}
  • +
  • size{{kb .SizeBytes}} kB
  • +
  • privacy{{.Anonymization}}
  • +
+ +
+ {{end}} +
{{else}} +

Nothing uploaded yet. Take a measurement in the app and upload it; it will + appear here.

+ {{end}} {{else if eq .Page "run"}}

Run {{.ID}}

-
+

The document as stored, indented for reading. Nothing has been added or removed.

+