Files
llmgw/internal/web/templates/admin_people.html
T
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

156 lines
8.8 KiB
HTML
Raw 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.
{{/* People, their limits and their use: one page in three tabs, because all
three are about the same tree. Each tab keeps its old address. Forms and
links work as plain navigation; htmx, when it loads, swaps only #people,
so a change does not jump the page. */}}
{{define "admin_people"}}<!doctype html>
<html lang="{{lang}}"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>{{t "people.title"}} · llmgw</title><link rel="stylesheet" href="{{stylesheet}}">
<meta name="htmx-config" content='{"scrollIntoViewOnBoost":false,"defaultFocusScroll":false,"allowEval":false}'>
<script src="/_static/htmx-2.0.11.min.js" defer></script>
</head><body><div class="wrap">
<header>
<h1>{{t "people.title"}}</h1>
<p class="sub">{{t "people.sub"}}</p>
{{template "nav" .Nav}}
</header>
<div id="people" hx-boost="true" hx-target="#people" hx-select="#people"
hx-swap="outerHTML" hx-push-url="false">
<nav class="page-tabs" aria-label="{{t "people.title"}}">
{{if .HasPeople}}<a href="/admin/users" hx-push-url="true"{{if eq .Tab "people"}} aria-current="page"{{end}}>{{t "people.tab.people"}}{{if .Pending}} <span class="tag">{{t "people.waiting" .Pending}}</span>{{end}}</a>{{end}}
{{if .HasLimits}}<a href="/admin/quotas" hx-push-url="true"{{if eq .Tab "limits"}} aria-current="page"{{end}}>{{t "people.tab.limits"}}</a>{{end}}
{{if .HasUsage}}<a href="/admin/usage" hx-push-url="true"{{if eq .Tab "usage"}} aria-current="page"{{end}}>{{t "people.tab.usage"}}</a>{{end}}
</nav>
{{if .Users}}
<div class="people-grid{{if .Detail}} with-detail{{end}}">
{{with .Detail}}<aside class="people-detail" id="detail" aria-label="{{.Name}}">{{template "people-detail" .}}</aside>{{end}}
<div class="people-main">{{template "people_body" .Users}}</div>
</div>
{{else if .Quotas}}{{template "limits_body" .Quotas}}
{{else if .Usage}}{{template "usage_body" .Usage}}
{{end}}
</div>
<footer><span>llmgw {{.Version}}</span><span>{{.Now}}</span>{{template "langs"}}</footer>
</div>
<script>
// Search the account list. Outside #people so what was typed survives a swap,
// and applied again after every swap.
(function () {
var box = document.createElement('input');
box.type = 'search'; box.placeholder = {{t "people.search.placeholder"}};
box.className = 'search'; box.setAttribute('aria-label', {{t "people.search.label"}});
function apply() {
var q = box.value.trim().toLowerCase();
document.querySelectorAll('#account-list tr[data-q]').forEach(function (tr) {
tr.hidden = q !== '' && tr.dataset.q.indexOf(q) < 0;
});
}
box.addEventListener('input', apply);
function place() {
var h = document.getElementById('accounts');
if (h && box.parentNode !== h.parentNode) h.after(box);
apply();
}
place();
// Projects the reader had open stay open across a change.
var open = [];
document.addEventListener('htmx:beforeSwap', function () {
open = Array.prototype.map.call(document.querySelectorAll('details.project[open]'), function (d) { return d.id; });
});
document.addEventListener('htmx:afterSettle', function () {
open.forEach(function (id) { var d = document.getElementById(id); if (d) d.open = true; });
place();
});
})();
</script>
</body></html>{{end}}
{{/* One branch or person: its limits, its capacity and its use, together. */}}
{{define "people-detail"}}{{$d := .}}{{$sel := printf "%s:%s" .Kind .ID}}
<div class="pd-head">
<span class="q-kind">{{t (print "people.kind." .Kind)}}</span>
<h2>{{.Name}}</h2>
{{if and .Path (ne .Path .Name)}}<p class="pd-path">{{.Path}}</p>{{end}}
<a href="/admin/users" class="pd-close" aria-label="{{t "people.close"}}">×</a>
</div>
{{with .Memberships}}<p class="pd-sub">{{t "people.in"}} {{range $i, $m := .}}{{if $i}}, {{end}}<a href="/admin/users?sel={{$m.Scope}}:{{$m.ScopeID}}">{{$m.Path}}</a>{{end}}</p>
{{else}}{{if eq (printf "%s" $d.Kind) "user"}}<p class="pd-sub">{{t "people.on_own"}}</p>{{end}}{{end}}
{{if or .Limits .Person}}
<h3>{{t "people.limits"}}</h3>
{{if not .BudgetsBind}}<p class="note">{{t "people.budgets_unpriced"}}</p>{{end}}
{{with .Limits}}
<dl class="pd-limits">
<dt>{{t "people.rpm"}}</dt><dd>{{template "quota-value" dict "Own" .Shown.RPM "Up" .RPMUp "From" .RPMFrom}}</dd>
<dt>{{t "people.tpm"}}</dt><dd>{{template "quota-value" dict "Own" .Shown.TPM "Up" .TPMUp "From" .TPMFrom}}</dd>
<dt>{{t "people.budget"}}</dt><dd>{{if .Budget}}{{.Shown.Budget}}{{if .Period}}<span class="q-per">/{{.Shown.Period}}</span>{{end}}{{else}}{{template "quota-value" dict "Up" .BudgetUp "From" .BudgetFrom}}{{end}}</dd>
<dt>{{t "people.spent" $d.QuotaWindow}}</dt><dd><span class="q-spend">{{.Spend}}</span>{{if .HasPct}} <span class="q-meter{{if ge .Percent 90}} hot{{end}}"><i style="width:{{pct .Percent 100}}%"></i></span> <span class="q-pct">{{t "quotas.pct" .Percent}}</span>{{end}}</dd>
<dt>{{t "people.models"}}</dt><dd>{{if .Models}}{{th "people.models_only" .Models}}{{else if .ModelsFrom}}{{t "people.models_limited_by" .ModelsFrom}}{{else}}{{t "people.models_every"}}{{end}}</dd>
{{if .Member}}<dt>{{t "people.each_member"}}</dt><dd>{{.Member}}</dd>{{end}}
</dl>
{{if .Shadowed}}<p class="q-sub q-warn">{{.Shadowed}}</p>{{end}}
<details class="edit"><summary>{{if .HasAny}}{{t "people.change_limits"}}{{else}}{{t "people.set_limits"}}{{end}}</summary>
{{template "quota-form" (dict "Scope" (printf "%s" .Scope) "ID" .ID "CSRF" $d.CSRF "Sel" $sel
"F" (dict "RPM" .RPM "TPM" .TPM "Budget" .Budget "Period" .Period "Models" .Models "HasAny" .HasAny)
"Title" (t "people.quota_for" .Crumb)
"Note" (t "people.note.shared"))}}
</details>
<details class="edit"><summary>{{t "people.limit_each_member"}}</summary>
{{template "quota-form" (dict "Scope" (printf "%s-member" .Scope) "ID" .ID "F" .MemberRaw "CSRF" $d.CSRF "Sel" $sel
"Title" (t "people.each_member_of" .Crumb)
"Note" (t "people.note.each_member"))}}
</details>
{{end}}
{{with .Person}}
<dl class="pd-limits">
<dt>{{t "people.rpm"}}</dt><dd>{{or .Shown.RPM "—"}}</dd>
<dt>{{t "people.tpm"}}</dt><dd>{{or .Shown.TPM "—"}}</dd>
<dt>{{t "people.budget"}}</dt><dd>{{if .Budget}}{{.Shown.Budget}}{{if .Period}}<span class="q-per">/{{.Shown.Period}}</span>{{end}}{{else}}—{{end}}</dd>
<dt>{{t "people.spent" $d.QuotaWindow}}</dt><dd><span class="q-spend">{{.Spend}}</span>{{if .HasPct}} <span class="q-meter{{if ge .Percent 90}} hot{{end}}"><i style="width:{{pct .Percent 100}}%"></i></span> <span class="q-pct">{{t "quotas.pct" .Percent}}</span>{{end}}</dd>
<dt>{{t "people.models"}}</dt><dd>{{if .Models}}{{th "people.models_only" .Models}}{{else}}{{t "people.models_every"}}{{end}}</dd>
</dl>
<details class="edit"><summary>{{if .HasAny}}{{t "people.change_limits"}}{{else}}{{t "people.set_limits"}}{{end}}</summary>
{{template "quota-form" (dict "Scope" "user" "ID" .ID "CSRF" $d.CSRF "Sel" $sel
"F" (dict "RPM" .RPM "TPM" .TPM "Budget" .Budget "Period" .Period "Models" .Models "HasAny" .HasAny)
"Title" (t "people.everywhere" .Email)
"Note" (t "people.note.person"))}}
</details>
{{end}}
{{end}}
{{if .HasCapacity}}
<h3>{{t "people.capacity"}}</h3>
{{with .Capacity}}
<dl class="pd-limits">
<dt>GPUs</dt><dd>{{or .GPUs "—"}}</dd>
<dt>{{t "people.cores"}}</dt><dd>{{or .Cores "—"}}</dd>
<dt>{{t "people.memory_gb"}}</dt><dd>{{or .Memory "—"}}</dd>
</dl>
<p class="note">{{t "people.capacity_note"}} <a href="/admin/quotas?cap={{$sel}}#capacity">{{t "people.change"}}</a></p>
{{else}}<p class="note">{{t "people.no_capacity"}}
<a href="/admin/quotas?cap={{$sel}}#capacity">{{t "people.set_one"}}</a></p>{{end}}
{{end}}
{{if .HasUsage}}
<h3>{{t "people.use" .UsageWindow}}</h3>
{{if .Totals.Name}}
<p class="pd-sub">{{t "people.n_requests" (compact .Totals.Requests)}} · {{t "people.n_tokens" (compact .Totals.Tokens)}}{{if .Priced}} · {{t "people.spent_amount" .Totals.Spend}}{{end}}{{if .Totals.Errors}} · {{t "people.n_errors" (count .Totals.Errors)}}{{end}}</p>
{{if and .HasStats .Stats.Requests}}{{with .Stats}}
<dl class="pd-limits">
<dt>{{t "people.error_rate"}}</dt><dd{{if ge .ErrorRate 5.0}} class="q-warn"{{end}}>{{pct1 .ErrorRate}}%</dd>
<dt>{{t "people.refused"}}</dt><dd{{if .Limited}} class="q-warn"{{end}}>{{if .Limited}}{{compact .Limited}}{{else}}{{t "common.none"}}{{end}}</dd>
<dt>{{t "people.first_token"}}</dt><dd>p50 {{pctile .TTFT 50}} · p95 {{pctile .TTFT 95}} · p99 {{pctile .TTFT 99}}</dd>
<dt>{{t "people.latency"}}</dt><dd>p50 {{pctile .Latency 50}} · p95 {{pctile .Latency 95}} · p99 {{pctile .Latency 99}}</dd>
</dl>{{end}}{{end}}
{{template "tschart" dict "t" .Requests "title" (t "people.chart.requests") "note" ""}}
{{template "tschart" dict "t" .Tokens "title" (t "people.chart.tokens") "note" ""}}
{{if .Priced}}{{template "tschart" dict "t" .Spend "title" (t "people.chart.spend") "note" ""}}{{end}}
{{else}}<p class="empty">{{t "people.nothing_used"}}</p>{{end}}
{{end}}
{{end}}