Keep each held row on one line
The replica count, its nodes and the mode selector sit side by side instead of stacking in a fixed-width cell, and node names drop the prefix every node shares, with the full name on hover. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
a32ea0e5ab
commit
cbf95c034e
6 files changed
+75
-20
No files matched your search
+3
-1
@@ -51,7 +51,9 @@ API.
|
||||
|
||||
Each row also says how its model runs without being opened: replicas ready of
|
||||
those it should have, how many are still starting, and the nodes they are on —
|
||||
in amber when short, in red when parked or when it does not fit.
|
||||
in amber when short, in red when parked or when it does not fit. It is one line
|
||||
per model beside its mode, and nodes drop the part of the name every node
|
||||
shares (node05 for llmgw-node05, the full name on hover).
|
||||
|
||||
## v0.10.4 — 2026-09-30
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ type modelUse struct {
|
||||
Fleet bool
|
||||
Ready, Wanted int
|
||||
Starting int
|
||||
Nodes []string
|
||||
Nodes []nodeName
|
||||
Parked bool
|
||||
Unplaceable bool
|
||||
// Declared models are read from server.yaml on every start, so a mode
|
||||
@@ -446,11 +446,17 @@ func (p *AccountPages) renderMirror(w http.ResponseWriter, r *http.Request,
|
||||
d.Rows = append(d.Rows, row)
|
||||
}
|
||||
var views map[string]adminapi.ModelView
|
||||
prefix := ""
|
||||
if p.fleet != nil && len(sources) > 0 {
|
||||
views = map[string]adminapi.ModelView{}
|
||||
for _, v := range p.fleet.Models() {
|
||||
views[v.Name] = v
|
||||
}
|
||||
var ids []string
|
||||
for _, n := range p.fleet.Nodes() {
|
||||
ids = append(ids, n.ID)
|
||||
}
|
||||
prefix = sharedPrefix(ids)
|
||||
}
|
||||
for i := range d.Rows {
|
||||
for _, m := range sources[d.Rows[i].URL] {
|
||||
@@ -462,7 +468,11 @@ func (p *AccountPages) renderMirror(w http.ResponseWriter, r *http.Request,
|
||||
if v, ok := views[m.Name]; ok {
|
||||
u.Fleet, u.Ready, u.Wanted = true, v.Ready, v.Desired
|
||||
u.Parked, u.Unplaceable = v.Parked, v.Unplaceable
|
||||
u.Nodes, u.Starting = runningOn(p.fleet.Replicas(m.Name))
|
||||
var nodes []string
|
||||
nodes, u.Starting = runningOn(p.fleet.Replicas(m.Name))
|
||||
for _, n := range nodes {
|
||||
u.Nodes = append(u.Nodes, nodeName{Full: n, Short: strings.TrimPrefix(n, prefix)})
|
||||
}
|
||||
}
|
||||
d.Rows[i].UsedBy = append(d.Rows[i].UsedBy, u)
|
||||
}
|
||||
@@ -803,3 +813,26 @@ func runningOn(reps []adminapi.ReplicaView) (nodes []string, starting int) {
|
||||
slices.Sort(nodes)
|
||||
return nodes, starting
|
||||
}
|
||||
|
||||
// nodeName is a node as a row names it: short, with the full name beside.
|
||||
type nodeName struct{ Short, Full string }
|
||||
|
||||
// sharedPrefix is what every node's name starts with, up to and including a
|
||||
// separator — "llmgw-" of llmgw-node01 and llmgw-node02 — so a row can say
|
||||
// node01 and fit on a line. Nothing when the names share no such part, or
|
||||
// when there is one node and dropping it would leave only a number.
|
||||
func sharedPrefix(ids []string) string {
|
||||
if len(ids) < 2 {
|
||||
return ""
|
||||
}
|
||||
p := ids[0]
|
||||
for _, id := range ids[1:] {
|
||||
for !strings.HasPrefix(id, p) {
|
||||
p = p[:len(p)-1]
|
||||
}
|
||||
}
|
||||
if i := strings.LastIndexAny(p, "-._"); i >= 0 {
|
||||
return p[:i+1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -592,7 +592,7 @@ func TestHowAModelRunsIsChangedWhereItsFileIsShown(t *testing.T) {
|
||||
t.Error("the running model's mode is not shown as chosen")
|
||||
}
|
||||
// Declared in server.yaml: shown, not offered, since it would revert.
|
||||
if !strings.Contains(body, "on demand<br>set in server.yaml") {
|
||||
if !strings.Contains(body, `title="set in server.yaml, which is read again on every start">on demand · yaml</span>`) {
|
||||
t.Error("a declared model should say where its mode is set")
|
||||
}
|
||||
if strings.Count(body, `name="mode" class="mode"`) != 1 {
|
||||
|
||||
@@ -149,7 +149,7 @@ func TestAHeldRowSaysHowItsModelRunsWithoutOpening(t *testing.T) {
|
||||
if strings.Contains(body, "held-card") {
|
||||
t.Fatal("the card opened by itself")
|
||||
}
|
||||
if !strings.Contains(body, `<span class="runs-now">1/1 ready · <span class="node">n2</span></span>`) {
|
||||
if !strings.Contains(body, `title="replicas ready / wanted">1/1 · <span class="node" title="n2">n2</span></span>`) {
|
||||
t.Errorf("the row does not say where its replicas run: %s", body)
|
||||
}
|
||||
}
|
||||
@@ -164,3 +164,20 @@ func TestRunningOnNamesEachNodeOnceAndCountsWhatIsStarting(t *testing.T) {
|
||||
t.Errorf("nodes %v, starting %d", nodes, starting)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeNamesLoseOnlyWhatEveryNodeShares(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
ids []string
|
||||
want string
|
||||
}{
|
||||
{[]string{"llmgw-node01", "llmgw-node02", "llmgw-node06"}, "llmgw-"},
|
||||
{[]string{"gpu-a.dc1", "gpu-b.dc1"}, "gpu-"},
|
||||
{[]string{"n1", "n2"}, ""},
|
||||
{[]string{"llmgw-node01"}, ""},
|
||||
{[]string{"alpha", "beta"}, ""},
|
||||
} {
|
||||
if got := sharedPrefix(c.ids); got != c.want {
|
||||
t.Errorf("%v: %q, expected %q", c.ids, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -444,14 +444,16 @@ h3.limits-for { font-size: .92rem; margin: 1rem 0 .4rem; }
|
||||
table.narrow-table { max-width: 30rem; }
|
||||
.q-act a + a { margin-left: .8rem; }
|
||||
.chip-role { font-size: .68rem; font-weight: 600; color: var(--accent); text-transform: uppercase; letter-spacing: .04em; }
|
||||
/* Runs: one fixed-width selector per model, so they line up down the column
|
||||
whatever the names; a catalog name differing from the mirror's sits above. */
|
||||
table.held .use { width: 9.5rem; margin: .1rem 0; }
|
||||
table.held .use .as { display: block; font-size: .72rem; color: var(--muted);
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
table.held .use .opt { font-size: .75rem; line-height: 1.3; }
|
||||
/* Runs: one line per model — a catalog name that differs from the mirror's,
|
||||
how it runs, and where — with the selector at a fixed width so the
|
||||
replica counts line up down the column. */
|
||||
table.held .use { display: flex; align-items: center; gap: .5rem; margin: .1rem 0; white-space: nowrap; }
|
||||
table.held .use .as { font-size: .72rem; color: var(--muted); max-width: 10rem;
|
||||
overflow: hidden; text-overflow: ellipsis; }
|
||||
table.held .use form.inline { margin: 0; }
|
||||
table.held .use .mode-fixed { width: 7.5rem; font-size: .8rem; color: var(--muted); }
|
||||
table.held td.num { white-space: nowrap; }
|
||||
table.held select.mode { width: 100%; padding: .15rem .3rem; font-size: .8rem; }
|
||||
table.held select.mode { width: 7.5rem; margin: 0; padding: .1rem .3rem; font-size: .8rem; }
|
||||
|
||||
/* Link pills: the same look as .tabs, for choices that are addresses rather
|
||||
than panels on one page — a filtered listing should survive a reload and be
|
||||
@@ -831,7 +833,7 @@ a.open-held { text-decoration: none; color: inherit; }
|
||||
a.open-held:hover strong { text-decoration: underline; }
|
||||
a.open-held[aria-expanded=true]::before { content: "▾ "; color: var(--muted); }
|
||||
a.open-held[aria-expanded=false]::before { content: "▸ "; color: var(--muted); }
|
||||
.use .runs-now { display: block; font-size: .78rem; color: var(--muted); margin: .1rem 0 .25rem; }
|
||||
.use .runs-now { font-size: .78rem; color: var(--muted); }
|
||||
.use .runs-now.short { color: var(--warn); }
|
||||
.use .runs-now.bad { color: var(--bad); }
|
||||
.use .runs-now .node { white-space: nowrap; }
|
||||
@@ -67,13 +67,8 @@ reachable from this machine.</p>
|
||||
{{/* Named only when the catalog calls it something other than the
|
||||
mirror does; otherwise it is the model column again. */}}
|
||||
{{if ne .Name $row.Model}}<span class="as" title="{{.Name}}">as {{.Name}}</span>{{end}}
|
||||
{{if .Fleet}}<span class="runs-now{{if or .Parked .Unplaceable}} bad{{else if lt .Ready .Wanted}} short{{end}}">
|
||||
{{- if .Parked}}parked
|
||||
{{- else if and (eq .Wanted 0) (eq .Ready 0)}}no replicas
|
||||
{{- else}}{{.Ready}}/{{.Wanted}} ready{{if .Starting}}, {{.Starting}} starting{{end}}{{end}}
|
||||
{{- if .Unplaceable}} · does not fit{{end}}
|
||||
{{- with .Nodes}} · {{range $i, $n := .}}{{if $i}}, {{end}}<span class="node">{{$n}}</span>{{end}}{{end}}</span>{{end}}
|
||||
{{if .Declared}}<span class="opt">{{if eq .Mode "always"}}always on{{else if eq .Mode "ondemand"}}on demand{{else}}stopped{{end}}<br>set in server.yaml</span>
|
||||
{{if .Declared}}<span class="mode-fixed" title="set in server.yaml, which is read again on every start">
|
||||
{{- if eq .Mode "always"}}always on{{else if eq .Mode "ondemand"}}on demand{{else}}stopped{{end}} · yaml</span>
|
||||
{{else}}
|
||||
{{/* Submitted on change: the choice is the action, and a second click
|
||||
to confirm it would only be a way to forget to. */}}
|
||||
@@ -89,6 +84,12 @@ reachable from this machine.</p>
|
||||
<noscript><button type="submit" class="link">set</button></noscript>
|
||||
</form>
|
||||
{{end}}
|
||||
{{if .Fleet}}<span class="runs-now{{if or .Parked .Unplaceable}} bad{{else if lt .Ready .Wanted}} short{{end}}" title="replicas ready / wanted{{if .Starting}}, + starting{{end}}">
|
||||
{{- if .Parked}}parked
|
||||
{{- else if and (eq .Wanted 0) (eq .Ready 0)}}none running
|
||||
{{- else}}{{.Ready}}/{{.Wanted}}{{if .Starting}} +{{.Starting}}{{end}}{{end}}
|
||||
{{- if .Unplaceable}} · no room{{end}}
|
||||
{{- with .Nodes}} · {{range $i, $n := .}}{{if $i}}, {{end}}<span class="node" title="{{$n.Full}}">{{$n.Short}}</span>{{end}}{{end}}</span>{{end}}
|
||||
</div>{{end}}
|
||||
{{else}}<span class="opt">not served</span>{{end}}
|
||||
</td>
|
||||
|
||||
Reference in new issue
Block a user