Files
llmgw/internal/web/templates/admin_usage.html
T
Ilker ManapandClaude Opus 5.5 c9cd25efc2 Draw one menu on every page
Each page wrote its own nav: the same links in a different order, some
missing, the current page sometimes listed and sometimes not. A shared
"nav" template now draws it everywhere — Cluster, People, Quotas, Usage,
Mirror for an administrator, then Chat, Your keys and Sign out — with
the current page marked by aria-current. Pages rendered outside the
account pages (the dashboard, node logs) get it through the request
context their admin wrapper sets. The admin listener's own page keeps
its metrics/pprof/api links.

The chat and key pages said "project proj-3kq7…"; they now show the
project's path, and the key page no longer repeats the email.

A test renders several admin pages and requires the same menu on each.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 17:55:36 +02:00

80 lines
3.2 KiB
HTML

{{define "admin_usage"}}<!doctype html>
<html lang="en"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Usage · llmgw</title><link rel="stylesheet" href="/_static/base.css">
</head><body><div class="wrap">
<header>
<h1>Usage over time</h1>
<p class="sub">Who is using the cluster, and when</p>
{{template "nav" .Nav}}
</header>
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
{{/* One row of controls above the charts, which is where a reader looks for
them. Links rather than a script: the choice is in the URL, so a view worth
coming back to can be bookmarked or pasted to somebody else. */}}
<div class="filters">
<span class="filter-label">Group by</span>
{{range .Groups}}<a class="chip{{if .Selected}} on{{end}}"
href="/admin/usage?by={{.Key}}&amp;window={{$.Window}}">{{.Label}}</a>{{end}}
<span class="filter-label">Window</span>
{{range .Windows}}<a class="chip{{if .Selected}} on{{end}}"
href="/admin/usage?by={{$.By}}&amp;window={{.Key}}">{{.Label}}</a>{{end}}
</div>
<div class="chart-row">
{{template "tschart" dict "t" .Requests "title" "Requests"
"note" "Requests served in each slice, counted where the key belongs now rather than where it was at the time."}}
{{template "tschart" dict "t" .Tokens "title" "Tokens"
"note" "Prompt and completion together, which is the figure a person recognises as what they consumed."}}
</div>
<div class="chart-row">
{{if .Priced}}
{{template "tschart" dict "t" .Spend "title" "Spend"
"note" "In whole units, the same ones a budget is written in."}}
{{else}}
{{/* Said rather than drawn. A spend chart flat along the bottom reads as a
quiet month, and this is not that. */}}
<div class="chart-wrap">
<h3 class="chart-title">Spend</h3>
<p class="empty">No model has a price, so every request costs zero and this
chart would be a flat line however busy the cluster is. Set prices in the
<code>pricing</code> block of the server configuration.</p>
</div>
{{end}}
</div>
{{if .Rows}}
<table>
<tr><th>Group</th><th>Requests</th><th>Tokens</th><th>Spend</th><th>Errors</th></tr>
{{range .Rows}}
<tr>
<td>{{.Name}}</td>
<td>{{compact .Requests}}</td>
<td>{{compact .Tokens}}</td>
<td>{{.Spend}}</td>
<td>{{if .Errors}}{{.Errors}}{{else}}—{{end}}</td>
</tr>
{{end}}
</table>
{{else}}
<p class="empty">Nothing recorded in this window.</p>
{{end}}
{{/* The charts fold their tail; the table does not. Both are here because they
answer different questions, and the one that is easy to get wrong is
thinking the four coloured lines are all of it. */}}
<p class="note">The charts draw the four busiest groups by total and sum the
rest into “other”, so they always add up to the whole cluster. The table below
is every group, in the same order.</p>
<p class="note">A person is not a level of the quota tree — one person holds
keys in several projects — so grouping by person crosses the tree rather than
naming a level of it. Quotas are written on the other three, on the
<a href="/admin/quotas">quotas page</a>.</p>
<footer><span>llmgw {{.Version}}</span><span>{{.Now}}</span></footer>
</div></body></html>{{end}}