Say on each held row how its model runs and where

Ready of wanted, how many are starting, and the nodes, from the fleet data
the card already reads, so the list answers it without a click.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ilker ManapandClaude Opus 5.5 committed 2026-09-30 10:38:49 +02:00
1 parent d7f1c1d545
commit a32ea0e5ab
5 files changed
+79 -2

No files matched your search

+4
View File
@@ -49,6 +49,10 @@ rather than accepted and left short. A ceiling above the room is taken, and
said. The same answer is at `GET /api/v1/models/{name}/headroom` on the admin
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.
## v0.10.4 — 2026-09-30
Published first as release candidates v0.10.4-rc.1 to rc.9.
+44 -2
View File
@@ -7,11 +7,13 @@ import (
"fmt"
"html/template"
"net/http"
"slices"
"sort"
"strconv"
"strings"
"time"
"gitea.mynodes.xyz/ilker/llmgw/internal/control/adminapi"
"gitea.mynodes.xyz/ilker/llmgw/internal/control/catalog"
"gitea.mynodes.xyz/ilker/llmgw/internal/gguf"
"gitea.mynodes.xyz/ilker/llmgw/internal/hf"
@@ -69,6 +71,16 @@ type modelUse struct {
Replicas int
// Min and Max are its scaling as the catalog holds it.
Min, Max int
// Ready, Wanted and Nodes are how it runs now, for the row itself:
// replicas ready of those it should have, and the nodes they are on.
// Fleet is false where nothing could be asked, so the row says nothing
// rather than "0 of 0".
Fleet bool
Ready, Wanted int
Starting int
Nodes []string
Parked bool
Unplaceable bool
// Declared models are read from server.yaml on every start, so a mode
// changed here would revert; the page says where to change it instead.
Declared bool
@@ -433,13 +445,26 @@ func (p *AccountPages) renderMirror(w http.ResponseWriter, r *http.Request,
}
d.Rows = append(d.Rows, row)
}
var views map[string]adminapi.ModelView
if p.fleet != nil && len(sources) > 0 {
views = map[string]adminapi.ModelView{}
for _, v := range p.fleet.Models() {
views[v.Name] = v
}
}
for i := range d.Rows {
for _, m := range sources[d.Rows[i].URL] {
d.Rows[i].UsedBy = append(d.Rows[i].UsedBy, modelUse{
u := modelUse{
Name: m.Name, Mode: modeOf(m.Scaling), Replicas: max(m.Scaling.Max, 1),
Min: m.Scaling.Min, Max: m.Scaling.Max,
Declared: p.catalog.Declared(m.Name),
})
}
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))
}
d.Rows[i].UsedBy = append(d.Rows[i].UsedBy, u)
}
}
@@ -761,3 +786,20 @@ func writeJSONStatus(w http.ResponseWriter, code int, v any) {
w.WriteHeader(code)
_ = json.NewEncoder(w).Encode(v)
}
// runningOn is the nodes a model's replicas are on, each once and in order,
// and how many of its replicas are not ready yet.
func runningOn(reps []adminapi.ReplicaView) (nodes []string, starting int) {
for _, r := range reps {
if r.State != "ready" {
starting++
}
for _, sh := range r.Shards {
if !slices.Contains(nodes, sh.Node) {
nodes = append(nodes, sh.Node)
}
}
}
slices.Sort(nodes)
return nodes, starting
}
+21
View File
@@ -143,3 +143,24 @@ func TestAModelSetInConfigurationIsShownButNotChanged(t *testing.T) {
t.Error("room that reached the limit asked should read as at least")
}
}
func TestAHeldRowSaysHowItsModelRunsWithoutOpening(t *testing.T) {
body := detailPage(t, &roomyFleet{more: 1}, coderCatalog())("/admin/mirror")
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>`) {
t.Errorf("the row does not say where its replicas run: %s", body)
}
}
func TestRunningOnNamesEachNodeOnceAndCountsWhatIsStarting(t *testing.T) {
nodes, starting := runningOn([]adminapi.ReplicaView{
{State: "ready", Shards: []adminapi.ShardView{{Node: "n2"}}},
{State: "starting", Shards: []adminapi.ShardView{{Node: "n1"}}},
{State: "ready", Shards: []adminapi.ShardView{{Node: "n2"}}},
})
if strings.Join(nodes, ",") != "n1,n2" || starting != 1 {
t.Errorf("nodes %v, starting %d", nodes, starting)
}
}
+4
View File
@@ -831,3 +831,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.short { color: var(--warn); }
.use .runs-now.bad { color: var(--bad); }
.use .runs-now .node { white-space: nowrap; }
+6
View File
@@ -67,6 +67,12 @@ 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>
{{else}}
{{/* Submitted on change: the choice is the action, and a second click