Files
llmgw/internal/web/charts2.go
T
Ilker ManapandClaude Opus 5.5 c1926be869 Tidy the dashboard and the forms around it
- Numeric column headers are right-aligned like their values; the
  stylesheet only aligned the cells, so "REQUESTS" sat nowhere near its
  numbers in every table that has them.
- Ranked bars are an HTML grid instead of a fixed-width SVG: the value
  had 96 units and "34.2k tok  114 req" did not fit, so the figure a
  reader came for was the one cut off. Labels ellipsise in CSS and give
  their full text on hover.
- Mean latency reads as 5 ms, 8.8 s or 1m 51s rather than 110695 ms.
- The "What the cluster is doing" charts sat between the Models heading
  and the models table; they now come before it.
- A fleet without GPUs shows its CPU capacity — nodes ready, memory,
  disk free for weights — instead of "No GPUs have been reported", and
  a node with no GPUs shows "—" rather than 0/0.
- The profile's forms are a form's width rather than the page's, and
  the quotas page's price warning is a boxed banner rather than a line
  of red text.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 18:08:16 +02:00

137 lines
4.0 KiB
Go

package web
import (
"fmt"
"html/template"
"strings"
)
// Row is one labelled value in a ranked chart.
type Row struct {
Label string
Value float64
// Note is shown beside the value: a second figure worth carrying, such as
// how many requests made up a token total.
Note string
}
// HBarChart ranks labelled values as horizontal bars.
//
// Horizontal because the labels are email addresses and model names. Rotated or
// truncated labels on a column chart are a way of saying the chart is more
// important than being able to read it, and here the label is the point: the
// question is which person or which model, not what shape the distribution is.
//
// One series, so no legend — the heading names it. The value is written at the
// end of each bar rather than on an axis, because a reader wants the number for
// the row they are looking at, not a scale to estimate it against.
func HBarChart(rows []Row, colour, unit string) template.HTML {
if colour == "" {
colour = "--viz-1"
}
if len(rows) == 0 {
return template.HTML(`<p class="empty">Nothing recorded yet.</p>`) //nolint:gosec // constant
}
maxV := 0.0
for _, r := range rows {
if r.Value > maxV {
maxV = r.Value
}
}
if maxV <= 0 {
maxV = 1
}
// HTML rather than an SVG of fixed width. The SVG gave the value 96 units
// and "34.2k tok 114 req" did not fit, so the one figure a reader came for
// was the one cut off. A grid lets the label and the value take the room
// they need and gives the bar whatever is left.
var b strings.Builder
b.WriteString(`<div class="ranked" role="list">`)
for _, r := range rows {
pct := r.Value / maxV * 100
if pct < 0.5 && r.Value > 0 {
// A value that rounds to nothing still happened, and a bar of zero
// width reads as no data rather than as a small number.
pct = 0.5
}
value := compact(r.Value) + unitSuffix(unit)
title := r.Label + ": " + value
if r.Note != "" {
value += " · " + r.Note
}
fmt.Fprintf(&b, `<div class="rk" role="listitem" title="%s">`+
`<span class="rk-label">%s</span>`+
`<span class="rk-track"><i class="rk-bar" style="width:%.1f%%;background:var(%s)"></i></span>`+
`<span class="rk-value">%s</span></div>`,
escapeText(title), escapeText(r.Label), pct, cssVar(colour), escapeText(value))
}
b.WriteString(`</div>`)
return template.HTML(b.String()) //nolint:gosec // every interpolation is escaped above
}
// Sparkline draws a value's recent shape beside its number.
//
// No axes, no labels, no gridlines. It answers "rising, falling or flat" and
// nothing else; a reader who wants a number reads the number it sits under.
func Sparkline(values []float64, colour string) template.HTML {
if colour == "" {
colour = "--viz-1"
}
if len(values) < 2 {
return ""
}
const w, h = 120.0, 24.0
maxV := 0.0
for _, v := range values {
if v > maxV {
maxV = v
}
}
if maxV <= 0 {
// Flat at zero is a real answer — no traffic — and drawing it along the
// bottom says so, where an empty box would read as a broken chart.
maxV = 1
}
var b strings.Builder
fmt.Fprintf(&b, `<svg viewBox="0 0 %.0f %.0f" width="%.0f" height="%.0f" `+
`role="img" aria-label="recent trend">`, w, h, w, h)
b.WriteString(`<polyline fill="none" stroke="var(` + cssVar(colour) + `)` +
`" stroke-width="2" stroke-linejoin="round" stroke-linecap="round" points="`)
step := w / float64(len(values)-1)
for i, v := range values {
fmt.Fprintf(&b, "%.1f,%.1f ", float64(i)*step, h-(v/maxV*(h-4))-2)
}
b.WriteString(`"/></svg>`)
return template.HTML(b.String()) //nolint:gosec // numbers and a CSS var only
}
// trimLabel keeps a long label readable without letting it push the plot off
// the side. Long email addresses are the common case.
func trimLabel(s string, n int) string {
if len(s) <= n {
return s
}
if n <= 1 {
return s[:n]
}
return s[:n-1] + "…"
}
func unitSuffix(unit string) string {
if unit == "" {
return ""
}
return " " + unit
}
func maxFloat(a, b float64) float64 {
if a > b {
return a
}
return b
}