Files
Ilker ManapandClaude Opus 5.5 11231d8c2b The portal in English and Turkish: a catalogue per language, a switcher at the foot of every page, landing.language, Turkish numbers and dates
Every template and handler message in internal/web goes through a JSON
catalogue under internal/web/locales/<code>/, with English as the fallback.
The language is the person's choice (a cookie set by /_lang/{code}), else
Accept-Language, else landing.language. Tests refuse a key some language
lacks, a translation with other arguments than English, and template text
left in English.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 00:12:53 +02:00

363 lines
22 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{define "dashboard"}}<!doctype html>
<html lang="{{lang}}"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<noscript>{{.RefreshMeta}}</noscript>
<meta name="htmx-config" content='{"allowEval":false}'>
<script src="/_static/htmx-2.0.11.min.js"></script>
<script defer src="/_static/pages.js"></script>
<title>llmgw — {{t "dash.title"}}</title><link rel="stylesheet" href="{{stylesheet}}">
</head><body><div class="wrap">
<header>
<h1>{{t "dash.h1"}}</h1>
<p class="sub">{{t "dash.sub" .NodeCount .ReadyReplicas .TotalReplicas .ModelCount}}</p>
{{if .OnAdminListener}}<nav>
<a href="/metrics">{{t "dash.nav.metrics"}}</a>
<a href="/debug/pprof/" translate="no">pprof</a>
<a href="/api/v1/nodes" translate="no">api</a>
</nav>{{else}}{{template "nav" .Menu}}{{end}}
</header>
{{/* 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. */}}
<div id="live" hx-get="{{if .OnAdminListener}}/{{else}}/admin{{end}}" hx-trigger="every {{.RefreshSec}}s" data-visible-only hx-select="#live" hx-swap="outerHTML">
{{if .HasCharts}}
<div class="tiles">
<div class="tile"><div class="v">{{count .Requests24h}}</div><div class="k">{{t "dash.tile.requests"}}</div>{{.RequestSpark}}</div>
<div class="tile"><div class="v">{{compact .Tokens24h}}</div><div class="k">{{t "dash.tile.tokens"}}</div>{{.TokenSpark}}</div>
<div class="tile"><div class="v">{{count .Errors24h}}</div><div class="k">{{t "dash.tile.errors"}}</div></div>
<div class="tile"><div class="v">{{units .Spend24h}}</div><div class="k">{{t "dash.tile.spend"}}</div></div>
</div>
{{end}}
<h2>{{t "dash.capacity"}}</h2>
{{if .Summary.GPUs}}
<table><tr><th>{{t "dash.cap.class"}}</th><th class="num">{{t "dash.cap.in_use"}}</th><th class="num">{{t "dash.cap.free"}}</th><th class="num">{{t "dash.cap.faulty"}}</th><th>{{t "dash.cap.usage"}}</th></tr>
{{range .Summary.GPUs}}<tr>
<td><code>{{.Class}}</code></td>
<td class="num">{{.Used}}/{{.Total}}</td>
<td class="num">{{.Free}}</td>
<td class="num">{{if .Unhealthy}}<span class="bad">{{.Unhealthy}}</span>{{else}}—{{end}}</td>
<td class="meter"><span style="width:{{pct .Used .Total}}%"></span><em>{{pct .Used .Total}}%</em></td>
</tr>{{end}}</table>
{{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. */}}
<div class="capacity-cpu">
<div><b>{{.CPU.Ready}}/{{.CPU.Nodes}}</b> <span>{{t "dash.cpu.ready"}}</span></div>
<div><b>{{count .CPU.MemoryGB}} GB</b> <span>{{t "dash.cpu.memory"}}</span></div>
{{if .CPU.DiskTotal}}<div><b>{{t "dash.cpu.disk_of" (count .CPU.DiskFreeGB) (count .CPU.DiskTotal)}}</b> <span>{{t "dash.cpu.disk_free"}}</span></div>{{end}}
</div>
{{else}}<p class="empty">{{t "dash.no_nodes"}}</p>{{end}}
{{with .GPUMap}}
<h2>{{t "dash.gpumap.title"}} <span class="opt">{{t "dash.gpumap.by"}}
{{range .Modes}}{{if eq .Key $.GPUMap.By}}<strong>{{.Label}}</strong>{{else}}<a href="?gpumap={{.Key}}">{{.Label}}</a>{{end}} {{end}}</span></h2>
<div class="gpu-map">
{{range .Rows}}<div class="gpu-row"><span class="gpu-node">{{.Node}}</span>{{range .Cells}}<span class="gpu-cell" style="{{.Style}}" title="{{.Title}}">{{.Label}}</span>{{end}}</div>
{{end}}</div>
<p class="note">{{.Legend}} {{t "dash.gpumap.note"}}</p>
{{end}}
{{with .Efficiency}}
<h3>{{t "dash.eff.title"}} <span class="opt">{{t "dash.eff.window"}}</span></h3>
{{/* 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. */}}
<table class="rows"><tr><th>{{t "dash.col.model"}}</th><th class="num">GPUs</th><th class="num">{{t "dash.eff.tps"}}</th><th class="num">{{t "dash.eff.per_gpu"}}</th><th class="num">{{t "dash.eff.busy"}}</th><th class="num">{{t "dash.eff.draw"}}</th><th class="num">{{t "dash.eff.per_wh"}}</th></tr>
{{range .}}<tr><td>{{.Model}}</td><td class="num">{{num .GPUs 0}}</td><td class="num">{{num .TokensPerSec 1}}</td>
<td class="num">{{num .PerGPU 1}}</td><td class="num">{{num .Util 0}}%</td>
<td class="num">{{if .Watts}}{{num .Watts 0}} W{{else}}—{{end}}</td><td class="num">{{if .PerWh}}{{num .PerWh 0}}{{else}}—{{end}}</td></tr>
{{end}}</table>
{{end}}
{{with .IdleGPUs}}
<h3>{{t "dash.idle.title"}}</h3>
{{/* 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. */}}
<table class="rows"><tr><th>{{t "dash.col.model"}}</th><th>{{t "dash.col.replica"}}</th><th>{{t "dash.col.node"}}</th><th class="num">GPUs</th><th class="num">{{t "dash.idle.busiest"}}</th><th>{{t "dash.idle.last_request"}}</th></tr>
{{range .}}<tr><td>{{.Model}}</td><td class="clip"><code>{{.Replica}}</code></td><td><code>{{.Node}}</code></td>
<td class="num">{{len .GPUs}}</td><td class="num">{{num .MaxUtil 0}}%</td><td>{{if .Idle}}{{t "dash.idle.ago" .Idle}}{{else}}<span class="note">{{t "dash.idle.unknown"}}</span>{{end}}</td></tr>
{{end}}</table>
{{end}}
<h2>{{t "dash.nodes"}}</h2>
{{with .Notice}}<p class="notice">{{.}}</p>{{end}}{{with .Problem}}<p class="error">{{.}}</p>{{end}}
{{if .Nodes}}
<table class="nodes rows"><tr><th>{{t "dash.col.node"}}</th><th>{{t "dash.col.state"}}</th><th>CPU</th><th>{{t "dash.nodes.ram"}}</th>{{if .AnyGPUs}}<th class="num">GPUs</th>{{end}}<th class="num">{{t "dash.nodes.disk"}}</th><th>{{t "dash.nodes.engines"}}</th>{{if .AnyGPUs}}<th>{{t "dash.nodes.build"}}</th>{{end}}{{if .AnyClasses}}<th>{{t "dash.nodes.classes"}}</th>{{end}}{{if .AnyFabrics}}<th>{{t "dash.nodes.fabrics"}}</th>{{end}}<th class="num">{{t "dash.nodes.models"}}</th><th class="num">{{t "dash.nodes.replicas"}}</th><th>{{t "dash.nodes.seen"}}</th></tr>
{{range .Nodes}}<tr>
<td><a href="{{if $.OnAdminListener}}/nodes/{{.ID}}{{else}}/admin/nodes/{{.ID}}{{end}}" title="{{.ID}}"><code>{{shortNode .ID $.NodePrefix}}</code></a>{{if .MgmtAddr}} <span class="note">{{.MgmtAddr}}</span>{{end}}
<a class="note" href="{{if $.OnAdminListener}}/nodes/{{.ID}}/logs{{else}}/admin/nodes/{{.ID}}/logs{{end}}">{{t "dash.nodes.logs"}}</a></td>
{{/* The state is a class as well as a word: the class stays, the word
is the reader's. */}}
<td class="state state-{{.State}}">{{t (print "dash.state." .State)}}</td>
{{/* A dash, not an empty bar, when the agent has not measured it lately:
unknown is not idle. */}}
<td class="load">{{with .Load}}{{template "minibar" pctbar .CPUPercent}}<span class="load-v">{{num .CPUPercent 0}}%{{if .CPUs}} <span class="note">{{t "dash.nodes.cpus_of" .CPUs}}</span>{{end}}</span>{{else}}—{{end}}</td>
<td class="load">{{with .Load}}{{template "minibar" pctbar .MemPercent}}<span class="load-v">{{bytes .MemUsed}} <span class="note">{{t "dash.nodes.mem_of" (bytes .MemTotal)}}</span></span>{{else}}{{if .MemoryGB}}{{.MemoryGB}}G{{else}}—{{end}}{{end}}</td>
{{if $.AnyGPUs}}<td class="num">{{if .GPUTotal}}{{.GPUFree}}/{{.GPUTotal}}{{else}}—{{end}}</td>{{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. */}}
<td class="num">{{if .DiskTotalGB}}{{.DiskFreeGB}}G/{{.DiskTotalGB}}G{{else}}—{{end}}</td>
<td>{{if .Engines}}{{join .Engines}}{{else}}<span class="note">{{t "dash.nodes.no_engines"}}</span>{{end}}</td>
{{/* 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}}<td class="clip">{{if .GPUTotal}}{{$node := .}}
{{range .Provision}}{{if eq .Engine "vllm"}}<span class="note" title="{{.Detail}}">{{.Version}} {{with .Variant}}{{buildLabel .}} {{end}}{{t (print "dash.state." .State)}}</span>{{end}}{{end}}
{{if and $.Menu.CSRF .EngineBuilds}}
<form method="post" action="/admin/nodes/{{.ID}}/engines" class="inline build-choice">
<input type="hidden" name="csrf" value="{{$.Menu.CSRF}}">
<input type="hidden" name="engine" value="vllm">
<select name="build" title="{{t "dash.nodes.build_hint"}}">
{{range engineBuilds "vllm"}}<option value="{{.}}"{{if eq . (index $node.EngineBuilds "vllm")}} selected{{end}}>{{buildLabel .}}</option>{{end}}
</select>
{{/* For a browser without script; the script below hides it. Not in a
<noscript>, whose inside is text to the catalogue check. */}}
<button type="submit">{{t "common.save"}}</button>
</form>
{{end}}
{{else}}—{{end}}</td>{{end}}
{{if $.AnyClasses}}<td>{{join .Classes}}</td>{{end}}
{{if $.AnyFabrics}}<td>{{join .Fabrics}}</td>{{end}}
<td class="num">{{if .Models}}{{len .Models}}{{else}}—{{end}}</td>
<td class="num">{{.Replicas}}</td>
<td>{{or .LastSeenAgo "—"}}</td>
{{/* What is unhealthy, what an install said and the node's warnings come
from the control plane and the agent, in English. */}}
</tr>{{if .Unhealthy}}<tr><td colspan="12" class="note bad">{{join .Unhealthy}}</td></tr>{{end}}
{{range .Provision}}{{if and (ne .State "ready") (ne .State "skipped")}}<tr><td colspan="12" class="note{{if or (eq .State "failed") (eq .State "unsupported")}} bad{{end}}">{{.Engine}} {{.Version}}{{if .Variant}} ({{.Variant}}){{end}} {{t (print "dash.state." .State)}}: {{.Detail}}</td></tr>{{end}}{{end}}
{{range .Warnings}}<tr><td colspan="12" class="note bad">{{.}}</td></tr>{{end}}
{{end}}</table>
<script>
// A build chosen is saved as it is chosen, with the page reloaded on the
// answer: one select, no separate button to forget.
document.querySelectorAll("form.build-choice select").forEach(function (s) {
s.form.querySelectorAll("button").forEach(function (b) { b.hidden = true; });
s.addEventListener("change", function () { s.form.submit(); });
});
</script>
{{else}}<p class="empty">{{t "dash.no_agents"}}</p>{{end}}
<h2>{{t "dash.models"}}</h2>
{{if .Models}}
<table class="rows"><tr><th>{{t "dash.col.model"}}</th><th>{{t "dash.models.aliases"}}</th><th class="num">{{t "dash.models.ready"}}</th><th class="num">{{t "dash.models.wanted"}}</th><th>{{t "dash.models.class"}}</th><th>{{t "dash.models.reserves"}}</th><th>{{t "dash.models.profiles"}}</th>{{if .AnyFallbacks}}<th>{{t "dash.models.fallbacks"}}</th>{{end}}<th>{{t "dash.models.for"}}</th></tr>
{{range .Models}}<tr>
<td><code>{{.Name}}</code></td>
<td>{{join .Aliases}}</td>
<td class="num">{{.Ready}}</td>
<td class="num">{{.Min}}–{{.Max}}</td>
<td{{if .HeldFor}} class="warn" title="{{t "dash.models.held_for" .HeldFor}}"{{end}}>{{.Class}}{{if .Preempted}} <span class="note" title="{{t "dash.models.preempted"}}">−{{.Preempted}}</span>{{end}}</td>
{{/* The warnings are the catalogue's own, in English. */}}
<td{{with .Warnings}} class="warn" title="{{join .}}"{{end}}>{{if or .MemoryGB .CPUCores}}{{.MemoryGB}}G{{if .CPUCores}} · {{.CPUCores}}c{{end}}{{else}}—{{end}}
{{- if .MeasuredMemoryGB}} <span class="note">{{t "dash.models.used" (num .MeasuredMemoryGB 1)}}</span>{{end}}{{if .Warnings}} ⚠{{end}}</td>
<td>{{if .External}}<span class="tag" title="{{t "dash.models.external_hint"}}">{{t "dash.models.external"}}</span> {{.External}}{{else}}{{join .Profiles}}{{end}}</td>
{{if $.AnyFallbacks}}<td>{{join .Fallbacks}}</td>{{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. */}}
<td class="clip"{{with .Description}} title="{{.}}"{{end}}>{{or .Description "—"}}</td>
</tr>
{{end}}</table>
{{else}}<p class="empty">{{t "dash.models.empty"}}</p>{{end}}
{{if .Unplaced}}
<h2>{{t "dash.unplaced"}}</h2>
{{/* The scheduler's reasons, as it gives them. */}}
{{range .Unplaced}}
<p><code>{{.Model}}</code></p>
<pre>{{range .Rejections}}{{.Profile}} @ {{or .Pool "-"}}{{if .Node}}/{{.Node}}{{end}}: {{.Reason}}
{{end}}</pre>
{{end}}
{{end}}
<h2>{{t "dash.replicas"}}</h2>
{{if .Replicas}}
<table class="rows"><tr><th>{{t "dash.col.replica"}}</th><th>{{t "dash.col.model"}}</th><th>{{t "dash.col.state"}}</th><th>{{t "dash.replicas.pool"}}</th><th>{{t "dash.replicas.endpoint"}}</th><th class="num">{{t "dash.replicas.age"}}</th><th>{{t "dash.nodes"}}</th></tr>
{{range .Replicas}}<tr>
<td><code>{{.ID}}</code></td>
<td>{{.Model}}</td>
<td class="state state-{{.State}}">{{t (print "dash.state." .State)}}</td>
<td>{{or .Pool "—"}}</td>
<td>{{if .Endpoint}}<code>{{.Endpoint}}</code>{{else}}—{{end}}</td>
<td class="num">{{age .AgeSec}}</td>
<td>{{range .Shards}}<code title="{{.Node}}">{{shortNode .Node $.NodePrefix}}</code> {{end}}</td>
</tr>{{if .Err}}<tr><td colspan="7" class="note">{{.Err}}</td></tr>{{end}}
{{end}}</table>
{{else}}<p class="empty">{{t "dash.replicas.none"}}</p>{{end}}
{{if .Summary.IdleReplicas}}
<h2>{{t "dash.holding"}}</h2>
<table><tr><th>{{t "dash.col.replica"}}</th><th>{{t "dash.col.model"}}</th><th class="num">GPUs</th><th class="num">{{t "dash.holding.unused_for"}}</th></tr>
{{range .Summary.IdleReplicas}}<tr>
<td><code>{{.ID}}</code></td>
<td>{{.Model}}</td>
<td class="num">{{.GPUs}}</td>
<td class="num">{{age .IdleSec}}</td>
</tr>{{end}}</table>
{{end}}
{{if or .Summary.Fabrics (and .Fabrics (or .Fabrics.Physical .Fabrics.Problems))}}
<h2>{{t "dash.nodes.fabrics"}}</h2>
{{with .Fabrics}}{{if .Problems}}
<ul class="fabric-problems">{{range .Problems}}
<li class="{{.Level}}"><span class="tag{{if eq (printf "%s" .Level) "fail"}} bad{{end}}">{{t (print "fabric.level." .Level)}}</span> {{.Message}}</li>{{end}}
</ul>{{end}}{{end}}
{{if .Summary.Fabrics}}
<table><tr><th>{{t "dash.fabrics.fabric"}}</th><th>{{t "dash.fabrics.physically"}}</th><th class="num">{{t "dash.nodes"}}</th><th class="num">{{t "dash.fabrics.fully_free"}}</th><th>{{t "dash.fabrics.weights"}}</th></tr>
{{range .Summary.Fabrics}}{{$name := .Fabric}}<tr>
<td><code>{{.Fabric}}</code></td>
<td>{{with $.Fabrics}}{{range .Physical}}{{if has .Declared $name}}<div>{{.Label}}{{if .RateGbps}} <span class="note">· {{t "fabric.gbps" .RateGbps}}</span>{{end}}</div>{{end}}{{end}}{{end}}</td>
<td class="num">{{.Nodes}}</td>
<td class="num">{{.LargestFreeBlock}}</td>
<td>{{if .NodesWithWeights}}{{t "dash.fabrics.weights_on" .Models .NodesWithWeights .Nodes}}{{else}}<span class="note">{{t "dash.fabrics.not_reported"}}</span>{{end}}</td>
</tr>{{end}}</table>
{{end}}
{{with .Fabrics}}{{if .Physical}}
<p><a href="{{if $.OnAdminListener}}/fabrics{{else}}/admin/fabrics{{end}}">{{t "dash.fabrics.see_map"}}</a></p>
<h3>{{t "dash.fabrics.sms"}}</h3>
<table><tr><th>{{t "dash.fabrics.fabric"}}</th><th>{{t "dash.fabrics.physically"}}</th><th>{{t "dash.fabrics.master"}}</th><th>{{t "dash.fabrics.standby"}}</th><th class="num">{{t "dash.nodes"}}</th><th>{{t "dash.fabrics.last_change"}}</th></tr>
{{range .Physical}}<tr>
<td>{{if .Declared}}{{range $i, $d := .Declared}}{{if $i}}, {{end}}<code>{{$d}}</code>{{end}}{{else}}<span class="note">{{t "fabric.undeclared"}}</span>{{end}}</td>
<td>{{.Label}}{{if .RateGbps}} <span class="note">· {{t "fabric.gbps" .RateGbps}}</span>{{end}}</td>
<td>{{if eq .Kind "roce"}}<span class="note">{{t "dash.fabrics.roce"}}</span>{{else}}{{with .SM}}<code>{{.GUID}}</code>{{if .LID}} <span class="note">{{t "dash.fabrics.lid_priority" .LID .Priority}}</span>{{end}}{{else}}—{{end}}{{end}}</td>
<td>{{if eq .Kind "roce"}}—{{else if .SMs}}{{if .Standbys}}{{.Standbys}}{{else}}<span class="bad">{{t "dash.fabrics.none"}}</span>{{end}}{{else}}<span class="note">{{t "dash.fabrics.not_listed"}}</span>{{end}}</td>
<td class="num">{{len .Members}}</td>
<td>{{with .Changes}}{{with index . 0}}{{ago .At}}: <code>{{.From}}</code> → <code>{{.To}}</code>{{end}}{{if gt (len .) 1}} <span class="note">{{t "dash.fabrics.in_a_day" (len .)}}</span>{{end}}{{else}}<span class="note">{{t "dash.fabrics.none_in_a_day"}}</span>{{end}}</td>
</tr>{{end}}</table>
{{end}}{{end}}
<p class="note">{{t "dash.fabrics.note"}}</p>
{{end}}
{{with .NVLink}}
<h2>{{t "dash.nvlink.title"}}</h2>
<p>{{tn "dash.nvlink.domains" (len .Domains)}}{{range .Domains}} · <code>{{shortDomain .Key}}</code> {{t "dash.nvlink.trays" .Usable (len .Members)}}{{end}}
{{if .Problems}} · <span class="bad">{{tn "dash.nvlink.problems" (len .Problems)}}</span>{{end}}
· <a href="{{if $.OnAdminListener}}/nvlink{{else}}/admin/nvlink{{end}}">{{t "dash.nvlink.see_racks"}}</a></p>
{{end}}
{{with .Fleet}}
<h2>{{t "dash.fleet.title"}} <span class="opt">{{t "dash.fleet.last" .Window}}</span></h2>
{{if .Err}}
{{/* Said outright. An empty panel reads as a healthy cluster with nothing
happening, which is the opposite of not knowing. */}}
<p class="note error-row">{{t "dash.no_history" .Err}}</p>
{{else}}
<div class="chart-row">
{{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")}}
</div>
{{if $.AnyGPUs}}
<div class="chart-row">
{{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")}}
</div>
{{end}}
<div class="chart-row">
{{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")}}
</div>
{{end}}
{{end}}
<hr class="part">
<h2>{{t "dash.traffic"}} <span class="note">{{t "dash.traffic.window"}}</span></h2>
{{if .HasCharts}}
<p class="note">{{t "dash.traffic.requests"}}</p>{{.RequestChart}}
<div class="chart-row">
<div><p class="note">{{t "dash.traffic.tokens"}}</p>{{.TokenChart}}</div>
<div><p class="note">{{t "dash.traffic.latency"}}</p>{{.LatencyChart}}</div>
</div>
{{else if .AuthEnabled}}
<p class="empty">{{t "dash.traffic.none"}}</p>
{{end}}
{{if .HasPeople}}
<h2>{{t "dash.people"}}</h2>
<table><tr><th>{{t "dash.col.person"}}</th><th class="num">{{t "dash.people.keys"}}</th><th class="num">{{t "dash.col.requests"}}</th><th class="num">{{t "dash.col.tokens"}}</th><th class="num">{{t "dash.col.errors"}}</th><th class="num">{{t "dash.col.spend"}}</th><th>{{t "dash.people.last_seen"}}</th></tr>
{{range .Users}}<tr>
<td>{{if .Email}}{{.Email}}{{else}}<span class="note">{{t "dash.people.operator_keys"}}</span>{{end}}</td>
<td class="num">{{count .Keys}}</td>
<td class="num">{{count .Requests}}</td>
<td class="num">{{compact .Tokens}}</td>
<td class="num">{{if .Errors}}<span class="bad">{{count .Errors}}</span>{{else}}—{{end}}</td>
<td class="num">{{units .CostMicros}}</td>
<td>{{ago .LastSeen}}</td>
</tr>{{end}}</table>
{{end}}
{{if .Models2}}
<h2>{{t "dash.running"}}</h2>
<table><tr><th>{{t "dash.col.model"}}</th><th class="num">{{t "dash.col.requests"}}</th><th class="num">{{t "dash.col.tokens"}}</th><th class="num">{{t "dash.col.errors"}}</th><th class="num">{{t "dash.traffic.latency"}}</th></tr>
{{range .Models2}}<tr>
<td><code>{{.Model}}</code></td>
<td class="num">{{count .Requests}}</td>
<td class="num">{{compact .Tokens}}</td>
<td class="num">{{if .Errors}}<span class="bad">{{count .Errors}}</span>{{else}}—{{end}}</td>
<td class="num">{{latency .AvgLatency}}</td>
</tr>{{end}}</table>
<p class="note">{{t "dash.running.note"}}</p>
{{end}}
<h2>{{t "dash.usage"}} <span class="note">{{t "dash.usage.window"}}</span></h2>
{{if .AuthEnabled}}
{{if .Usage}}
<table><tr><th>{{t "dash.col.person"}}</th><th>{{t "dash.usage.through"}}</th><th>{{t "dash.usage.against"}}</th><th class="num">{{t "dash.col.requests"}}</th><th class="num">{{t "dash.col.errors"}}</th><th class="num">{{t "dash.usage.prompt"}}</th><th class="num">{{t "dash.usage.completion"}}</th><th class="num">{{t "dash.col.spend"}}</th><th>{{t "dash.usage.last"}}</th></tr>
{{range .Usage}}<tr>
<td>{{or .Email "—"}}</td>
<td>{{if .Chat}}{{t "dash.usage.chat_page"}}{{else if .KeyName}}{{.KeyName}} <code class="note">{{.KeyPrefix}}</code>{{else}}<code title="{{.KeyID}}">{{.KeyID}}</code>{{end}}</td>
<td>{{if .ProjectPath}}{{.ProjectPath}}{{else if .ProjectID}}<code>{{.ProjectID}}</code>{{else}}{{t "dash.usage.personal"}}{{end}}</td>
<td class="num">{{count .Requests}}</td>
<td class="num">{{if .Errors}}<span class="state-failed">{{count .Errors}}</span>{{else}}0{{end}}</td>
<td class="num">{{compact .Prompt}}</td>
<td class="num">{{compact .Completion}}</td>
<td class="num">{{units .CostMicros}}</td>
<td><span class="note">{{ago .LastAt}}</span></td>
</tr>{{end}}</table>
<p class="note">{{t "dash.usage.note"}}</p>
{{else}}<p class="empty">{{t "dash.usage.none"}}</p>{{end}}
{{else}}
<p class="empty">{{t "dash.usage.auth_off"}}</p>
{{end}}
{{if not .OnAdminListener}}
<p class="note">{{t "dash.admin_listener"}}
<code>ssh -L 9090:127.0.0.1:9090 &lt;control-plane&gt;</code></p>
{{end}}
</div>
<footer>
<span>llmgw {{.Version}} · {{t "common.refreshes_every" .RefreshSec}}</span>
<span>{{.Now}}</span>
{{template "langs"}}</footer>
</div></body></html>{{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"}}
<div class="chart-wrap">
<h3 class="chart-title">{{.title}}</h3>
{{if .t.Empty}}
<p class="empty">{{t "chart.nothing_yet"}}</p>
{{else}}
{{.t.SVG}}
<span class="chart-peak">{{t "chart.peak" .t.Peak}}</span>
{{if gt (len .t.Legend) 1}}
<div class="legend">
{{range .t.Legend}}<span><i style="background:var({{cssvar .Colour}})"></i>{{.Name}} <b>{{.Last}}</b></span>{{end}}
</div>
{{end}}
{{end}}
<p class="note">{{.note}}</p>
</div>
{{end}}
{{/* A small horizontal bar for a percentage, coloured as it fills. */}}
{{define "minibar"}}<span class="minibar{{if ge .P 90}} hot{{else if ge .P 75}} warm{{end}}"><span style="width: {{.P}}%"></span></span>{{end}}