From c1926be869286a43cbfe9c03659f0de8c7118dfb Mon Sep 17 00:00:00 2001
From: Ilker Manap
Date: Mon, 28 Sep 2026 18:08:16 +0200
Subject: [PATCH] Tidy the dashboard and the forms around it
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 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)
---
internal/web/charts2.go | 52 ++++++++----------------
internal/web/charts2_test.go | 45 ++++++++++----------
internal/web/dashboard.go | 25 ++++++++++++
internal/web/static/base.css | 26 +++++++++++-
internal/web/templates/account.html | 4 +-
internal/web/templates/admin_quotas.html | 2 +-
internal/web/templates/dashboard.html | 17 ++++++--
internal/web/web.go | 13 ++++++
8 files changed, 118 insertions(+), 66 deletions(-)
diff --git a/internal/web/charts2.go b/internal/web/charts2.go
index 41e2bf3..3f91820 100644
--- a/internal/web/charts2.go
+++ b/internal/web/charts2.go
@@ -33,15 +33,6 @@ func HBarChart(rows []Row, colour, unit string) template.HTML {
return template.HTML(`
Nothing recorded yet.
`) //nolint:gosec // constant
}
- const (
- rowH = 26.0 // the band
- barH = 14.0 // the mark, capped well under the band so the rest is air
- labelW = 190.0
- valueW = 96.0
- width = 720.0
- )
- height := float64(len(rows))*rowH + 8
-
maxV := 0.0
for _, r := range rows {
if r.Value > maxV {
@@ -51,41 +42,32 @@ func HBarChart(rows []Row, colour, unit string) template.HTML {
if maxV <= 0 {
maxV = 1
}
- plotW := width - labelW - valueW
+ // 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
- fmt.Fprintf(&b, ``)
return template.HTML(b.String()) //nolint:gosec // every interpolation is escaped above
}
diff --git a/internal/web/charts2_test.go b/internal/web/charts2_test.go
index fabb221..00d5157 100644
--- a/internal/web/charts2_test.go
+++ b/internal/web/charts2_test.go
@@ -28,10 +28,13 @@ func TestASmallValueStillDrawsAMark(t *testing.T) {
{Label: "huge", Value: 1_000_000},
{Label: "tiny", Value: 1},
}, "--viz-1", ""))
- // Two paths, one per row: the tiny one is still drawn.
- if n := strings.Count(out, ""); n != 2 {
+ if n := strings.Count(out, `title="`); n != 2 {
t.Errorf("%d hover titles for two bars", n)
}
}
@@ -72,17 +70,17 @@ func TestEveryBarCarriesItsOwnHoverTitle(t *testing.T) {
func TestALongLabelIsTrimmedRatherThanOverflowing(t *testing.T) {
long := strings.Repeat("a", 80) + "@example.com"
out := string(HBarChart([]Row{{Label: long, Value: 1}}, "--viz-1", ""))
- // Trimmed where it is drawn, in full where it is hovered: the plot keeps
- // its geometry and the reader can still find out whose row it is.
- drawn := out[strings.Index(out, "")]
- if strings.Contains(drawn, long) {
- t.Error("a very long label was drawn in full and would overrun the plot")
+ // Cut where it is drawn — the label cell ellipsises in CSS — and in full
+ // where it is hovered, so the reader can still find out whose row it is.
+ if !strings.Contains(out, `title="`+long+`: `) {
+ t.Error("the full label is not available on hover")
}
- if !strings.Contains(out, long) {
- t.Error("the full label is not available anywhere, not even on hover")
+ css, err := staticFiles.ReadFile("static/base.css")
+ if err != nil {
+ t.Fatal(err)
}
- if !strings.Contains(out, "…") {
- t.Error("the label was cut without saying it had been")
+ if !strings.Contains(string(css), ".ranked .rk-label { font-size: .82rem; overflow: hidden; text-overflow: ellipsis;") {
+ t.Error("the label cell no longer cuts a long label with an ellipsis")
}
}
@@ -108,11 +106,12 @@ func TestTheChartScalesToItsContainer(t *testing.T) {
// The dashboard is read on laptops and phones. A fixed pixel width is how a
// table ends up scrolling sideways.
out := string(HBarChart([]Row{{Label: "a", Value: 1}}, "", ""))
- if !strings.Contains(out, "viewBox") {
- t.Error("no viewBox, so the chart cannot scale")
+ // Bars are a share of whatever width the grid gives them, never pixels.
+ if !strings.Contains(out, "width:100.0%") {
+ t.Errorf("the bar is not sized as a share of its track:\n%s", out)
}
- if strings.Contains(out, `width="720"`) {
- t.Error("a fixed pixel width was set on the chart")
+ if strings.Contains(out, "px") {
+ t.Error("a fixed pixel size was set on the chart")
}
}
diff --git a/internal/web/dashboard.go b/internal/web/dashboard.go
index 110619c..055899b 100644
--- a/internal/web/dashboard.go
+++ b/internal/web/dashboard.go
@@ -14,6 +14,9 @@ import (
// DashboardData is the whole page.
type DashboardData struct {
+ // CPU is the fleet's capacity when it has no GPUs, which is shown instead
+ // of a GPU table that would only say there are none.
+ CPU cpuCapacity
// Menu is the shared top menu; Nav below is what the cluster page adds.
Menu Nav
Nodes []adminapi.NodeView
@@ -152,6 +155,7 @@ func Dashboard(src Source, usage UsageSource, version string, refresh time.Durat
}
if src != nil {
d.Nodes = src.Nodes()
+ d.CPU = cpuCapacityOf(d.Nodes)
d.Models = src.Models()
d.Replicas = src.Replicas("")
d.NodeCount = len(d.Nodes)
@@ -321,3 +325,24 @@ func unplaced(src Source, models []adminapi.ModelView) []adminapi.ExplainView {
}
return out
}
+
+// cpuCapacity is what a fleet without GPUs has to offer.
+type cpuCapacity struct {
+ Nodes, Ready int
+ MemoryGB int
+ DiskFreeGB, DiskTotal int
+}
+
+func cpuCapacityOf(nodes []adminapi.NodeView) cpuCapacity {
+ var c cpuCapacity
+ for _, n := range nodes {
+ c.Nodes++
+ if n.State == "ready" {
+ c.Ready++
+ }
+ c.MemoryGB += n.MemoryGB
+ c.DiskFreeGB += n.DiskFreeGB
+ c.DiskTotal += n.DiskTotalGB
+ }
+ return c
+}
diff --git a/internal/web/static/base.css b/internal/web/static/base.css
index 0ea359e..02f45ef 100644
--- a/internal/web/static/base.css
+++ b/internal/web/static/base.css
@@ -37,7 +37,7 @@ th, td { text-align: left; padding: .5rem .7rem; border-bottom: 1px solid var(--
th { font-weight: 600; color: var(--muted); font-size: .78rem; text-transform: uppercase;
letter-spacing: .04em; }
tr:last-child td { border-bottom: none; }
-td.num { text-align: right; font-variant-numeric: tabular-nums; }
+td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
code, pre { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .85em; }
pre { background: var(--card); border: 1px solid var(--line); border-radius: 6px;
padding: .9rem 1rem; overflow-x: auto; }
@@ -372,3 +372,27 @@ nav.menu a[aria-current="page"] { color: var(--fg); font-weight: 600;
nav.menu .menu-gap { flex: 1 1 auto; min-width: .5rem; }
nav.menu form.inline button.link { font-size: .9rem; text-decoration: none; color: var(--muted); }
nav.menu form.inline button.link:hover { color: var(--fg); text-decoration: underline; }
+
+/* Ranked bars: label, bar, value, each taking the room it needs. */
+.ranked { display: grid; grid-template-columns: minmax(7rem, max-content) minmax(6rem, 1fr) max-content;
+ gap: .35rem .8rem; align-items: center; max-width: 100%; }
+.ranked .rk { display: contents; }
+.ranked .rk-label { font-size: .82rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
+ max-width: 18rem; }
+.ranked .rk-track { height: .7rem; }
+.ranked .rk-bar { display: block; height: 100%; border-radius: 0 4px 4px 0; min-width: 2px; }
+.ranked .rk-value { font-size: .8rem; color: var(--muted); font-variant-numeric: tabular-nums;
+ white-space: nowrap; }
+
+/* A warning about the page as a whole, set apart from the notes around it. */
+.banner-warn { border: 1px solid var(--warn); border-left-width: 4px; border-radius: 6px;
+ background: var(--card); padding: .6rem .8rem; margin: 0 0 1rem;
+ font-size: .88rem; color: var(--fg); }
+
+/* Single-column forms read as a form, not as a row of the page. */
+form.narrow, .narrow-form form { max-width: 30rem; }
+
+/* CPU-only capacity: one line of figures in place of a GPU table. */
+.capacity-cpu { display: flex; flex-wrap: wrap; gap: .4rem 1.6rem; margin: .2rem 0 1rem; }
+.capacity-cpu b { font-size: 1.15rem; font-variant-numeric: tabular-nums; }
+.capacity-cpu span { color: var(--muted); font-size: .85rem; }
diff --git a/internal/web/templates/account.html b/internal/web/templates/account.html
index 3e057bc..2c1df48 100644
--- a/internal/web/templates/account.html
+++ b/internal/web/templates/account.html
@@ -69,7 +69,7 @@ Tell whoever you asked for access that you have signed up.