diff --git a/CHANGELOG.md b/CHANGELOG.md index e618caa..50b2eb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/internal/web/admin_mirror.go b/internal/web/admin_mirror.go index df7ace0..f683585 100644 --- a/internal/web/admin_mirror.go +++ b/internal/web/admin_mirror.go @@ -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 "" +} diff --git a/internal/web/admin_mirror_test.go b/internal/web/admin_mirror_test.go index fcdc35d..c32c63e 100644 --- a/internal/web/admin_mirror_test.go +++ b/internal/web/admin_mirror_test.go @@ -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
set in server.yaml") { + if !strings.Contains(body, `title="set in server.yaml, which is read again on every start">on demand · yaml`) { t.Error("a declared model should say where its mode is set") } if strings.Count(body, `name="mode" class="mode"`) != 1 { diff --git a/internal/web/mirror_detail_test.go b/internal/web/mirror_detail_test.go index 879c5d6..988d44b 100644 --- a/internal/web/mirror_detail_test.go +++ b/internal/web/mirror_detail_test.go @@ -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, `1/1 ready · n2`) { + if !strings.Contains(body, `title="replicas ready / wanted">1/1 · n2`) { 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) + } + } +} diff --git a/internal/web/static/base.css b/internal/web/static/base.css index 34d77b7..e7d23e8 100644 --- a/internal/web/static/base.css +++ b/internal/web/static/base.css @@ -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; } diff --git a/internal/web/templates/admin_mirror.html b/internal/web/templates/admin_mirror.html index a6ca040..7574a7e 100644 --- a/internal/web/templates/admin_mirror.html +++ b/internal/web/templates/admin_mirror.html @@ -67,13 +67,8 @@ reachable from this machine.

{{/* 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}}as {{.Name}}{{end}} - {{if .Fleet}} - {{- 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}}{{$n}}{{end}}{{end}}{{end}} - {{if .Declared}}{{if eq .Mode "always"}}always on{{else if eq .Mode "ondemand"}}on demand{{else}}stopped{{end}}
set in server.yaml
+ {{if .Declared}} + {{- if eq .Mode "always"}}always on{{else if eq .Mode "ondemand"}}on demand{{else}}stopped{{end}} · yaml {{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.

{{end}} + {{if .Fleet}} + {{- 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}}{{$n.Short}}{{end}}{{end}}{{end}} {{end}} {{else}}not served{{end}}