{{define "dashboard"}} llmgw — {{t "dash.title"}}

{{t "dash.h1"}}

{{t "dash.sub" .NodeCount .ReadyReplicas .TotalReplicas .ModelCount}}

{{if .OnAdminListener}}{{else}}{{template "nav" .Menu}}{{end}}
{{/* The page refreshes this part in place rather than reloading: a reload every few seconds threw away the scroll position of anyone reading the bottom of it. Paused while the tab is hidden. Without script, the meta refresh in the head does the old thing. */}}
{{if .HasCharts}}
{{count .Requests24h}}
{{t "dash.tile.requests"}}
{{.RequestSpark}}
{{compact .Tokens24h}}
{{t "dash.tile.tokens"}}
{{.TokenSpark}}
{{count .Errors24h}}
{{t "dash.tile.errors"}}
{{units .Spend24h}}
{{t "dash.tile.spend"}}
{{end}}

{{t "dash.capacity"}}

{{if .Summary.GPUs}} {{range .Summary.GPUs}}{{end}}
{{t "dash.cap.class"}}{{t "dash.cap.in_use"}}{{t "dash.cap.free"}}{{t "dash.cap.faulty"}}{{t "dash.cap.usage"}}
{{.Class}} {{.Used}}/{{.Total}} {{.Free}} {{if .Unhealthy}}{{.Unhealthy}}{{else}}—{{end}} {{pct .Used .Total}}%
{{else if .CPU.Nodes}} {{/* No GPUs is not no capacity: a CPU fleet still has memory and disk, which is what decides what it can hold. */}}
{{.CPU.Ready}}/{{.CPU.Nodes}} {{t "dash.cpu.ready"}}
{{count .CPU.MemoryGB}} GB {{t "dash.cpu.memory"}}
{{if .CPU.DiskTotal}}
{{t "dash.cpu.disk_of" (count .CPU.DiskFreeGB) (count .CPU.DiskTotal)}} {{t "dash.cpu.disk_free"}}
{{end}}
{{else}}

{{t "dash.no_nodes"}}

{{end}} {{with .GPUMap}}

{{t "dash.gpumap.title"}} {{t "dash.gpumap.by"}} {{range .Modes}}{{if eq .Key $.GPUMap.By}}{{.Label}}{{else}}{{.Label}}{{end}} {{end}}

{{range .Rows}}
{{.Node}}{{range .Cells}}{{.Label}}{{end}}
{{end}}

{{.Legend}} {{t "dash.gpumap.note"}}

{{end}} {{with .Efficiency}}

{{t "dash.eff.title"}} {{t "dash.eff.window"}}

{{/* Per card, because a model on eight cards making twice the tokens of one on two is the worse of the two; per watt-hour, because that is what the room is paying. */}} {{range .}} {{end}}
{{t "dash.col.model"}}GPUs{{t "dash.eff.tps"}}{{t "dash.eff.per_gpu"}}{{t "dash.eff.busy"}}{{t "dash.eff.power_load"}}{{t "dash.eff.sm_active"}}{{t "dash.eff.draw"}}{{t "dash.eff.per_wh"}}
{{.Model}}{{num .GPUs 0}}{{num .TokensPerSec 1}} {{num .PerGPU 1}}{{pc .Util 0}} {{if ge .PowerLoad 0.0}}{{pc .PowerLoad 0}}{{else}}—{{end}}{{if ge .SMActive 0.0}}{{pc .SMActive 0}}{{else}}—{{end}} {{if .Watts}}{{num .Watts 0}} W{{else}}—{{end}}{{if .PerWh}}{{num .PerWh 0}}{{else}}—{{end}}

{{t "dash.eff.note"}}

{{end}} {{with .IdleGPUs}}

{{t "dash.idle.title"}}

{{/* The first place to find room: replicas whose every card reads under 5% busy now. A model kept warm on purpose is here too; whether it should be is the operator's call. */}} {{range .}} {{end}}
{{t "dash.col.model"}}{{t "dash.col.replica"}}{{t "dash.col.node"}}GPUs{{t "dash.idle.busiest"}}{{t "dash.idle.last_request"}}
{{.Model}}{{.Replica}}{{.Node}} {{len .GPUs}}{{pc .MaxUtil 0}}{{if .Idle}}{{t "dash.idle.ago" .Idle}}{{else}}{{t "dash.idle.unknown"}}{{end}}
{{end}}

{{t "dash.nodes"}}

{{with .Notice}}

{{.}}

{{end}}{{with .Problem}}

{{.}}

{{end}} {{if .Nodes}} {{if .AnyGPUs}}{{end}}{{if .AnyGPUs}}{{end}}{{if .AnyClasses}}{{end}}{{if .AnyFabrics}}{{end}} {{range .Nodes}} {{/* The state is a class as well as a word: the class stays, the word is the reader's. */}} {{/* A dash, not an empty bar, when the agent has not measured it lately: unknown is not idle. */}} {{if $.AnyGPUs}}{{end}} {{/* Free out of total, because the first is meaningless without the second, and an em dash where nothing was reported: an agent that was never asked about its disk must not read as a full one. */}} {{/* Which vLLM build the node installs: left to its hardware, or chosen. What is installed beside it, so a choice and its outcome are read together. Only a GPU node has one; changing it is for an administrator, on the page reached through a session. */}} {{if $.AnyGPUs}}{{end}} {{if $.AnyClasses}}{{end}} {{if $.AnyFabrics}}{{end}} {{/* What is unhealthy, what an install said and the node's warnings come from the control plane and the agent, in English. */}} {{if .Unhealthy}}{{end}} {{range .Provision}}{{if and (ne .State "ready") (ne .State "skipped")}}{{end}}{{end}} {{range .Warnings}}{{end}} {{end}}
{{t "dash.col.node"}}{{t "dash.col.state"}}CPU{{t "dash.nodes.ram"}}GPUs{{t "dash.nodes.disk"}}{{t "dash.nodes.engines"}}{{t "dash.nodes.build"}}{{t "dash.nodes.classes"}}{{t "dash.nodes.fabrics"}}{{t "dash.nodes.models"}}{{t "dash.nodes.replicas"}}{{t "dash.nodes.seen"}}
{{shortNode .ID $.NodePrefix}}{{if .MgmtAddr}} {{.MgmtAddr}}{{end}} {{t "dash.nodes.logs"}}{{t (print "dash.state." .State)}}{{with .Load}}{{template "minibar" pctbar .CPUPercent}}{{pc .CPUPercent 0}}{{if .CPUs}} {{t "dash.nodes.cpus_of" .CPUs}}{{end}}{{else}}—{{end}} {{with .Load}}{{template "minibar" pctbar .MemPercent}}{{bytes .MemUsed}} {{t "dash.nodes.mem_of" (bytes .MemTotal)}}{{else}}{{if .MemoryGB}}{{.MemoryGB}}G{{else}}—{{end}}{{end}}{{if .GPUTotal}}{{.GPUFree}}/{{.GPUTotal}}{{else}}—{{end}}{{if .DiskTotalGB}}{{.DiskFreeGB}}G/{{.DiskTotalGB}}G{{else}}—{{end}} {{if .Engines}}{{join .Engines}}{{else}}{{t "dash.nodes.no_engines"}}{{end}}{{if .GPUTotal}}{{$node := .}} {{range .Provision}}{{if eq .Engine "vllm"}}{{.Version}} {{with .Variant}}{{buildLabel .}} {{end}}{{t (print "dash.state." .State)}}{{end}}{{end}} {{if and $.Menu.CSRF .EngineBuilds}}
{{/* For a browser without script; the script below hides it. Not in a
{{join .Classes}}{{join .Fabrics}}{{if .Models}}{{len .Models}}{{else}}—{{end}} {{.Replicas}} {{or .LastSeenAgo "—"}}
{{join .Unhealthy}}
{{.Engine}} {{.Version}}{{if .Variant}} ({{.Variant}}){{end}} {{t (print "dash.state." .State)}}: {{.Detail}}
{{.}}
{{else}}

{{t "dash.no_agents"}}

{{end}}

{{t "dash.models"}}

{{if .Models}} {{if .AnyFallbacks}}{{end}} {{range .Models}}{{.Class}}{{if .Preempted}} −{{.Preempted}}{{end}} {{/* The warnings are the catalogue's own, in English. */}} {{if or .MemoryGB .CPUCores}}{{.MemoryGB}}G{{if .CPUCores}} · {{.CPUCores}}c{{end}}{{else}}—{{end}} {{- if .MeasuredMemoryGB}} {{t "dash.models.used" (num .MeasuredMemoryGB 1)}}{{end}}{{if .Warnings}} ⚠{{end}} {{if $.AnyFallbacks}}{{end}} {{/* Cut to the column, the whole sentence on hover: a row of its own for each description made the table twice as long as the catalog. */}} {{end}}
{{t "dash.col.model"}}{{t "dash.models.aliases"}}{{t "dash.models.ready"}}{{t "dash.models.wanted"}}{{t "dash.models.class"}}{{t "dash.models.reserves"}}{{t "dash.models.profiles"}}{{t "dash.models.fallbacks"}}{{t "dash.models.for"}}
{{.Name}} {{join .Aliases}} {{.Ready}} {{.Min}}–{{.Max}} {{if .External}}{{t "dash.models.external"}} {{.External}}{{else}}{{join .Profiles}}{{end}}{{join .Fallbacks}}{{or .Description "—"}}
{{else}}

{{t "dash.models.empty"}}

{{end}} {{with .Presets}}

{{t "dash.presets.title"}}

{{t "dash.presets.about"}}

{{range .Rows}} {{/* Why the leader does not add it itself is the control plane's, in English. */}} {{or .State "—"}}{{with .Blocked}} ({{.}}){{end}} {{end}}
{{t "dash.col.model"}}{{t "dash.presets.makes"}}{{t "dash.presets.card"}}{{t "dash.presets.licence"}}{{t "dash.presets.fits"}}{{t "dash.presets.state"}}
{{.Name}} {{.Makes}} {{.Card}} {{.Licence}} {{or .Fits (t "dash.presets.fits_none")}}{{if and $.Menu.CSRF (not .InCatalog)}}
{{if .AddsComfyUI}} {{t "dash.presets.adds_comfyui"}}{{end}}{{end}}

{{if .Auto}}{{t "dash.presets.auto_on"}}{{else}}{{t "dash.presets.auto_off"}}{{end}} {{if $.Menu.CSRF}}

{{end}}

{{end}} {{if .Unplaced}}

{{t "dash.unplaced"}}

{{/* The scheduler's reasons, as it gives them. */}} {{range .Unplaced}}

{{.Model}}

{{range .Rejections}}{{.Profile}} @ {{or .Pool "-"}}{{if .Node}}/{{.Node}}{{end}}: {{.Reason}}
{{end}}
{{end}} {{end}}

{{t "dash.replicas"}}

{{if .Replicas}} {{range .Replicas}}{{if .Err}}{{end}} {{end}}
{{t "dash.col.replica"}}{{t "dash.col.model"}}{{t "dash.col.state"}}{{t "dash.replicas.pool"}}{{t "dash.replicas.endpoint"}}{{t "dash.replicas.age"}}{{t "dash.nodes"}}
{{.ID}} {{.Model}} {{t (print "dash.state." .State)}} {{or .Pool "—"}} {{if .Endpoint}}{{.Endpoint}}{{else}}—{{end}} {{age .AgeSec}} {{range .Shards}}{{shortNode .Node $.NodePrefix}} {{end}}
{{.Err}}
{{else}}

{{t "dash.replicas.none"}}

{{end}} {{if .Summary.IdleReplicas}}

{{t "dash.holding"}}

{{range .Summary.IdleReplicas}}{{end}}
{{t "dash.col.replica"}}{{t "dash.col.model"}}GPUs{{t "dash.holding.unused_for"}}
{{.ID}} {{.Model}} {{.GPUs}} {{age .IdleSec}}
{{end}} {{if or .Summary.Fabrics (and .Fabrics (or .Fabrics.Physical .Fabrics.Problems))}}

{{t "dash.nodes.fabrics"}}

{{with .Fabrics}}{{if .Problems}} {{end}}{{end}} {{if .Summary.Fabrics}} {{range .Summary.Fabrics}}{{$name := .Fabric}}{{end}}
{{t "dash.fabrics.fabric"}}{{t "dash.fabrics.physically"}}{{t "dash.nodes"}}{{t "dash.fabrics.fully_free"}}{{t "dash.fabrics.weights"}}
{{.Fabric}} {{with $.Fabrics}}{{range .Physical}}{{if has .Declared $name}}
{{.Label}}{{if .RateGbps}} · {{t "fabric.gbps" .RateGbps}}{{end}}
{{end}}{{end}}{{end}}
{{.Nodes}} {{.LargestFreeBlock}} {{if .NodesWithWeights}}{{t "dash.fabrics.weights_on" .Models .NodesWithWeights .Nodes}}{{else}}{{t "dash.fabrics.not_reported"}}{{end}}
{{end}} {{with .Fabrics}}{{if .Physical}}

{{t "dash.fabrics.see_map"}}

{{t "dash.fabrics.sms"}}

{{range .Physical}}{{end}}
{{t "dash.fabrics.fabric"}}{{t "dash.fabrics.physically"}}{{t "dash.fabrics.master"}}{{t "dash.fabrics.standby"}}{{t "dash.nodes"}}{{t "dash.fabrics.last_change"}}
{{if .Declared}}{{range $i, $d := .Declared}}{{if $i}}, {{end}}{{$d}}{{end}}{{else}}{{t "fabric.undeclared"}}{{end}} {{.Label}}{{if .RateGbps}} · {{t "fabric.gbps" .RateGbps}}{{end}} {{if eq .Kind "roce"}}{{t "dash.fabrics.roce"}}{{else}}{{with .SM}}{{.GUID}}{{if .LID}} {{t "dash.fabrics.lid_priority" .LID .Priority}}{{end}}{{else}}—{{end}}{{end}} {{if eq .Kind "roce"}}—{{else if .SMs}}{{if .Standbys}}{{.Standbys}}{{else}}{{t "dash.fabrics.none"}}{{end}}{{else}}{{t "dash.fabrics.not_listed"}}{{end}} {{len .Members}} {{with .Changes}}{{with index . 0}}{{ago .At}}: {{.From}} → {{.To}}{{end}}{{if gt (len .) 1}} {{t "dash.fabrics.in_a_day" (len .)}}{{end}}{{else}}{{t "dash.fabrics.none_in_a_day"}}{{end}}
{{end}}{{end}}

{{t "dash.fabrics.note"}}

{{end}} {{with .NVLink}}

{{t "dash.nvlink.title"}}

{{tn "dash.nvlink.domains" (len .Domains)}}{{range .Domains}} · {{shortDomain .Key}} {{t "dash.nvlink.trays" .Usable (len .Members)}}{{end}} {{if .Problems}} · {{tn "dash.nvlink.problems" (len .Problems)}}{{end}} · {{t "dash.nvlink.see_racks"}}

{{end}} {{with .Fleet}}

{{t "dash.fleet.title"}} {{t "dash.fleet.last" .Window}}

{{if .Err}} {{/* Said outright. An empty panel reads as a healthy cluster with nothing happening, which is the opposite of not knowing. */}}

{{t "dash.no_history" .Err}}

{{else}}
{{template "tschart" dict "t" .Saturation "title" (t "dash.fleet.saturation") "note" (t "dash.fleet.saturation_note")}} {{template "tschart" dict "t" .Ready "title" (t "dash.fleet.ready") "note" (t "dash.fleet.ready_note")}}
{{if $.AnyGPUs}}
{{template "tschart" dict "t" .GPUBusy "title" (t "dash.fleet.gpu_busy") "note" (t "dash.fleet.gpu_busy_note")}} {{template "tschart" dict "t" .GPUMemory "title" (t "dash.fleet.gpu_memory") "note" (t "dash.fleet.gpu_memory_note")}}
{{template "tschart" dict "t" .GPUPowerLoad "title" (t "dash.fleet.gpu_power_load") "note" (t "dash.fleet.gpu_power_load_note")}} {{if not .GPUSMActive.Empty}}{{template "tschart" dict "t" .GPUSMActive "title" (t "dash.fleet.gpu_sm_active") "note" (t "dash.fleet.gpu_sm_active_note")}}{{end}}
{{end}}
{{template "tschart" dict "t" .Queue "title" (t "dash.fleet.queue") "note" (t "dash.fleet.queue_note")}} {{template "tschart" dict "t" .KV "title" (t "dash.fleet.kv") "note" (t "dash.fleet.kv_note")}}
{{end}} {{end}}

{{t "dash.traffic"}} {{t "dash.traffic.window"}}

{{if .HasCharts}}

{{t "dash.traffic.requests"}}

{{.RequestChart}}

{{t "dash.traffic.tokens"}}

{{.TokenChart}}

{{t "dash.traffic.latency"}}

{{.LatencyChart}}
{{else if .AuthEnabled}}

{{t "dash.traffic.none"}}

{{end}} {{if .HasPeople}}

{{t "dash.people"}}

{{range .Users}}{{end}}
{{t "dash.col.person"}}{{t "dash.people.keys"}}{{t "dash.col.requests"}}{{t "dash.col.tokens"}}{{t "dash.col.errors"}}{{t "dash.col.spend"}}{{t "dash.people.last_seen"}}
{{if .Email}}{{.Email}}{{else}}{{t "dash.people.operator_keys"}}{{end}} {{count .Keys}} {{count .Requests}} {{compact .Tokens}} {{if .Errors}}{{count .Errors}}{{else}}—{{end}} {{units .CostMicros}} {{ago .LastSeen}}
{{end}} {{if .Models2}}

{{t "dash.running"}}

{{range .Models2}}{{end}}
{{t "dash.col.model"}}{{t "dash.col.requests"}}{{t "dash.col.tokens"}}{{t "dash.col.errors"}}{{t "dash.traffic.latency"}}
{{.Model}} {{count .Requests}} {{compact .Tokens}} {{if .Errors}}{{count .Errors}}{{else}}—{{end}} {{latency .AvgLatency}}

{{t "dash.running.note"}}

{{end}}

{{t "dash.usage"}} {{t "dash.usage.window"}}

{{if .AuthEnabled}} {{if .Usage}} {{range .Usage}}{{end}}
{{t "dash.col.person"}}{{t "dash.usage.through"}}{{t "dash.usage.against"}}{{t "dash.col.requests"}}{{t "dash.col.errors"}}{{t "dash.usage.prompt"}}{{t "dash.usage.completion"}}{{t "dash.col.spend"}}{{t "dash.usage.last"}}
{{or .Email "—"}} {{if .Chat}}{{t "dash.usage.chat_page"}}{{else if .KeyName}}{{.KeyName}} {{.KeyPrefix}}{{else}}{{.KeyID}}{{end}} {{if .ProjectPath}}{{.ProjectPath}}{{else if .ProjectID}}{{.ProjectID}}{{else}}{{t "dash.usage.personal"}}{{end}} {{count .Requests}} {{if .Errors}}{{count .Errors}}{{else}}0{{end}} {{compact .Prompt}} {{compact .Completion}} {{units .CostMicros}} {{ago .LastAt}}

{{t "dash.usage.note"}}

{{else}}

{{t "dash.usage.none"}}

{{end}} {{else}}

{{t "dash.usage.auth_off"}}

{{end}} {{if not .OnAdminListener}}

{{t "dash.admin_listener"}} ssh -L 9090:127.0.0.1:9090 <control-plane>

{{end}}
{{end}} {{/* One chart: the SVG, its legend, and the sentence that says how to read it. A legend is always present for two lines or more — colour alone is not an encoding a reader can rely on — and a single line needs none, because the title already names it. */}} {{define "tschart"}}

{{.title}}

{{if .t.Empty}}

{{t "chart.nothing_yet"}}

{{else}} {{.t.SVG}} {{t "chart.peak" .t.Peak}} {{if gt (len .t.Legend) 1}}
{{range .t.Legend}}{{.Name}} {{.Last}}{{end}}
{{end}} {{end}}

{{.note}}

{{end}} {{/* A small horizontal bar for a percentage, coloured as it fills. */}} {{define "minibar"}}{{end}}