CI / test (push) Successful in 1m28s
The dashboard could say what the cluster was made of and not who was spending it. Usage was per key, which is the right unit for revoking something and the wrong one for the question actually being asked: one person holds a laptop key, a CI key and a notebook key, and adding three rows up by eye is a chore that gets skipped. Per person now, joined through the keys they own, and per model — counted by the model that answered rather than the one requested, since those differ whenever a fallback serves and what actually ran is the question. Headline figures are stat tiles with a sparkline: a single current value is a number, and plotting one is how a chart ends up with a single bar in it. The sparkline says only rising, falling or flat, which is the one thing the number cannot. Ranked horizontal bars, because the labels are email addresses and model names and the label is the point — the question is which person, not what shape the distribution has. Rotated or truncated axis labels say the chart matters more than reading it. Trimmed where drawn and complete on hover, so the plot keeps its geometry and a reader can still find out whose row it is. The palette is validated rather than chosen: every adjacent pair clears the colour-vision separation threshold, and the dark values are the same hues re-stepped for the dark surface instead of an automatic flip. Run through a validator, because that is the only way anyone can honestly claim a palette is colourblind-safe. Which turned up a bug in the existing chart code. cssVar is an allowlist of four names that silently returns --accent for anything else, and callers wrap it in var() themselves — so every new series would have rendered in one colour with accent-coloured text. Caught by a test written from the rule that chart text wears text tokens, never the series colour. A key with no owner is labelled "operator-issued keys" rather than unknown: it predates accounts and is doing its job, and "unknown" would imply something is wrong. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
132 lines
3.9 KiB
Go
132 lines
3.9 KiB
Go
package web
|
|
|
|
import (
|
|
"fmt"
|
|
"html/template"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Inline SVG, generated here.
|
|
//
|
|
// A chart is a shape, and a column of numbers makes the reader compute the
|
|
// shape themselves. Drawing it costs about a hundred lines and no dependency:
|
|
// a charting library would mean a JavaScript toolchain in CI, a build step, and
|
|
// a megabyte of bundle for what is fundamentally a list of rectangles.
|
|
//
|
|
// The output is marked template.HTML, so everything interpolated into it must
|
|
// be a number this file produced. No caller-supplied string is ever written
|
|
// into the markup except through escapeText.
|
|
|
|
// Series is one set of bars.
|
|
type Series struct {
|
|
Values []float64
|
|
Labels []string
|
|
// Colour is a CSS custom property name, so the chart follows the page's
|
|
// light and dark palettes instead of pinning its own.
|
|
Colour string
|
|
}
|
|
|
|
const (
|
|
chartW = 720.0
|
|
chartH = 120.0
|
|
chartPad = 18.0
|
|
)
|
|
|
|
// BarChart renders a series as bars with a baseline and a peak label.
|
|
//
|
|
// Empty and all-zero series render as an empty frame rather than nothing: a
|
|
// missing chart reads as a broken page, and a flat one reads as no traffic,
|
|
// which is the truth.
|
|
func BarChart(s Series, unit string) template.HTML {
|
|
if s.Colour == "" {
|
|
s.Colour = "--accent"
|
|
}
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, `<svg class="chart" viewBox="0 0 %.0f %.0f" preserveAspectRatio="none" role="img">`,
|
|
chartW, chartH)
|
|
|
|
maxV := 0.0
|
|
for _, v := range s.Values {
|
|
if v > maxV {
|
|
maxV = v
|
|
}
|
|
}
|
|
// Baseline first so bars sit on it.
|
|
fmt.Fprintf(&b, `<line x1="0" y1="%.1f" x2="%.0f" y2="%.1f" class="axis"/>`,
|
|
chartH-chartPad, chartW, chartH-chartPad)
|
|
|
|
if n := len(s.Values); n > 0 && maxV > 0 {
|
|
slot := chartW / float64(n)
|
|
width := slot * 0.72
|
|
for i, v := range s.Values {
|
|
h := (v / maxV) * (chartH - chartPad*2)
|
|
if h < 1 && v > 0 {
|
|
// A bar for a real value must be visible, or an hour with
|
|
// traffic looks identical to one without.
|
|
h = 1
|
|
}
|
|
x := float64(i)*slot + (slot-width)/2
|
|
y := chartH - chartPad - h
|
|
title := ""
|
|
if i < len(s.Labels) {
|
|
title = fmt.Sprintf("<title>%s</title>", escapeText(s.Labels[i]))
|
|
}
|
|
fmt.Fprintf(&b,
|
|
`<rect x="%.2f" y="%.2f" width="%.2f" height="%.2f" style="fill:var(%s)">%s</rect>`,
|
|
x, y, width, h, cssVar(s.Colour), title)
|
|
}
|
|
}
|
|
b.WriteString(`</svg>`)
|
|
|
|
peak := fmt.Sprintf("peak %s %s", compact(maxV), unit)
|
|
if maxV == 0 {
|
|
peak = "nothing recorded"
|
|
}
|
|
return template.HTML(`<div class="chart-wrap">` + b.String() + //nolint:gosec // built above
|
|
`<span class="chart-peak">` + string(escapeText(peak)) + `</span></div>`)
|
|
}
|
|
|
|
// cssVar restricts what can reach a style attribute to names this package
|
|
// defines, so a colour can never become a way to inject CSS.
|
|
func cssVar(name string) string {
|
|
switch name {
|
|
case "--accent", "--ok", "--warn", "--bad",
|
|
// The chart palette, and the text tokens charts label themselves with.
|
|
// An allowlist rather than a check, because this value is interpolated
|
|
// into a style attribute and the set of things that belong there is
|
|
// small, known and worth writing down.
|
|
"--viz-1", "--viz-2", "--viz-3", "--viz-4", "--viz-grid",
|
|
"--fg", "--muted":
|
|
return name
|
|
default:
|
|
return "--accent"
|
|
}
|
|
}
|
|
|
|
// escapeText is the only path by which non-numeric content enters the markup.
|
|
func escapeText(s string) template.HTML {
|
|
return template.HTML(template.HTMLEscapeString(s)) //nolint:gosec // escaped on the line above
|
|
}
|
|
|
|
// compact renders a number the way a person reads one at a glance.
|
|
func compact(v float64) string {
|
|
switch {
|
|
case v >= 1e9:
|
|
return fmt.Sprintf("%.1fB", v/1e9)
|
|
case v >= 1e6:
|
|
return fmt.Sprintf("%.1fM", v/1e6)
|
|
case v >= 1e3:
|
|
return fmt.Sprintf("%.1fk", v/1e3)
|
|
case v == float64(int64(v)):
|
|
return fmt.Sprintf("%d", int64(v))
|
|
default:
|
|
return fmt.Sprintf("%.1f", v)
|
|
}
|
|
}
|
|
|
|
// hourLabel is what a bar's tooltip says.
|
|
func hourLabel(t time.Time, v float64, unit string) string {
|
|
return fmt.Sprintf("%s — %s %s", t.Local().Format("Mon 15:04"), compact(v), unit)
|
|
}
|