Files
llmgw/internal/web/templates/admin_quotas.html
T
Ilker ManapandClaude Opus 5.5 102ca9556f An organization's admin, and forms only for those who may send them
C1. A membership of an organization may be admin as well as member and
manager. An admin sees /org as a manager does and changes the
organization from it: its teams and projects, who of the approved
accounts is in them, its members' roles, and the keys spent in its
projects. Every id a form names is checked against the organization;
limits, accounts, the organization's name and the cluster stay with the
cluster's staff, and an admin cannot take their own role away.

A group from OIDC, SCIM or a directory no longer rewrites a membership
it names as a plain member at every sign-in, which took a manager's role
away (EnsureMembership).

Pages draw a form only for someone who may send it: Nav.May asks the
staff rule staffMay keeps, and a page whose forms a person may not send
gets no form token. A viewer sees the people, limits and mirror pages
without forms; an operator the mirror's.

Forms are compact: labels close to their fields, fields as wide as what
goes in them, smaller buttons, and the short forms on one line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 11:38:06 +01:00

218 lines
12 KiB
HTML

{{/* The limits tab of the people page. */}}
{{define "limits_body"}}
{{if not .BudgetsBind}}
{{/* A budget is spend per period and spend comes from a price table. With no
prices every request costs nothing, so these numbers read as limits and
bind on nothing. Self-hosted models having no per-token price is ordinary;
showing somebody a budget without saying so is not. */}}
<p class="banner-warn" role="note">{{th "quotas.unpriced"}}</p>
{{end}}
{{if .Error}}<p class="error" role="alert">{{.Error}}</p>{{end}}
{{if .Message}}<p class="ok" role="status">{{.Message}}</p>{{end}}
{{if .Rows}}
{{/* The tree is the interface. A quota belongs to a branch, so the form to set
one opens on that branch rather than asking somebody to name it again in a
box at the bottom. */}}
<table class="rows quota-tree">
<tr><th>{{t "quotas.col.where"}}</th><th class="num">{{t "quotas.col.rpm"}}</th><th class="num">{{t "quotas.col.tpm"}}</th>
<th class="num">{{t "quotas.col.budget"}}</th><th>{{t "quotas.col.spent"}}</th><th></th></tr>
{{range .Rows}}
<tr class="q-depth-{{.Indent}}{{if .Editing}} editing{{end}}">
<td class="q-where">
<span class="q-name">{{.Path}}</span> <span class="q-kind">{{t (print "quotas.kind." .Scope)}}</span>
{{if .Models}}<div class="q-sub">{{th "quotas.only_models" .Models}}</div>
{{else if .ModelsFrom}}<div class="q-sub">{{t "quotas.models_from" .ModelsFrom}}</div>{{end}}
{{if .Member}}<div class="q-sub">{{t "quotas.each_member" .Member}}</div>{{end}}
{{if .Shadowed}}
{{/* A value that is written here and never reached. Resolution takes the
most restrictive along the chain, so without saying so the page would
show a number as though it applied. */}}
<div class="q-sub q-warn">{{.Shadowed}}</div>{{end}}
</td>
<td class="num">{{template "quota-value" dict "Own" .Shown.RPM "Up" .RPMUp "From" .RPMFrom}}</td>
<td class="num">{{template "quota-value" dict "Own" .Shown.TPM "Up" .TPMUp "From" .TPMFrom}}</td>
<td class="num">{{if .Budget}}{{.Shown.Budget}}{{if .Period}}<span class="q-per">/{{.Shown.Period}}</span>{{end}}
{{else}}{{template "quota-value" dict "Up" .BudgetUp "From" .BudgetFrom}}{{end}}</td>
<td class="q-spent">
<span class="q-spend">{{.Spend}}</span>
{{if .HasPct}}<span class="q-meter{{if ge .Percent 90}} hot{{end}}" title="{{t "quotas.pct_of_budget" .Percent}}"><i style="width:{{pct .Percent 100}}%"></i></span>
<span class="q-pct">{{t "quotas.pct" .Percent}}</span>{{end}}
</td>
<td class="q-act">{{if $.CSRF}}
{{if .Editing}}<a href="/admin/quotas">{{t "quotas.close"}}</a>
{{else}}<a href="/admin/quotas?edit={{.Scope}}:{{.ID}}">{{if .HasAny}}{{t "quotas.change"}}{{else}}{{t "quotas.set"}}{{end}}</a>{{end}}
{{if .MemberEditing}}<a href="/admin/quotas">{{t "quotas.close"}}</a>
{{else}}<a href="/admin/quotas?edit={{.Scope}}-member:{{.ID}}" title="{{t "quotas.per_member_title"}}">{{t "quotas.per_member"}}</a>{{end}}
{{end}}</td>
</tr>
{{if .MemberEditing}}
<tr class="editing"><td colspan="6">
{{template "quota-form" (dict "Scope" (printf "%s-member" .Scope) "ID" .ID "F" .MemberRaw "CSRF" $.CSRF
"Title" (t "quotas.member_form.title" .Crumb)
"Note" (t "quotas.member_form.note"))}}
</td></tr>
{{end}}
{{if .Editing}}
<tr class="editing"><td colspan="6">
{{if $.CSRF}}<form method="post" action="/admin/quotas" class="q-form">
<input type="hidden" name="csrf" value="{{$.CSRF}}">
<input type="hidden" name="scope" value="{{.Scope}}">
<input type="hidden" name="id" value="{{.ID}}">
<p class="q-form-title">{{th "quotas.form.title" .Crumb}}</p>
<div class="field-row">
<label>{{t "quotas.form.rpm"}}
<input type="text" name="rpm" value="{{.RPM}}" placeholder="{{if .RPMUp}}{{t "quotas.form.up_from" .RPMUp .RPMFrom}}{{else}}{{t "common.none"}}{{end}}"></label>
<label>{{t "quotas.form.tpm"}}
<input type="text" name="tpm" value="{{.TPM}}" placeholder="{{if .TPMUp}}{{t "quotas.form.up_from" .TPMUp .TPMFrom}}{{else}}{{t "common.none"}}{{end}}"></label>
<label>{{t "quotas.col.budget"}}
<input type="text" name="budget" value="{{.Budget}}" placeholder="{{if .BudgetUp}}{{t "quotas.form.up_from" .BudgetUp .BudgetFrom}}{{else}}{{t "common.none"}}{{end}}"></label>
<label class="q-period">{{t "quotas.form.per"}}
<select name="period">
<option value="month"{{if eq .Period "month"}} selected{{end}}>{{t "quotas.per.month"}}</option>
<option value="day"{{if eq .Period "day"}} selected{{end}}>{{t "quotas.per.day"}}</option>
</select></label>
</div>
<label>{{t "quotas.form.models"}} <span class="opt">{{t "quotas.form.models_hint"}}</span>
<input type="text" name="models" value="{{.Models}}" placeholder="{{t "quotas.form.every_model"}}"></label>
<div class="q-form-actions">
<button type="submit">{{t "common.save"}}</button>
<a href="/admin/quotas">{{t "common.cancel"}}</a>
<button type="submit" name="clear" value="1" class="link danger"
{{if not .HasAny}}disabled{{end}}>{{t "quotas.form.remove_all"}}</button>
</div>
</form>{{end}}
</td></tr>
{{end}}
{{end}}
</table>
<p class="q-legend">{{th "quotas.legend"}}</p>
{{else}}
<p class="empty">{{th "quotas.empty_tree"}}</p>
{{end}}
<h2 id="people-limits">{{t "quotas.people.title"}}</h2>
<p class="note">{{t "quotas.people.note"}} {{.Personal}}</p>
{{if .People}}
<table class="rows quota-tree people-limits">
<tr><th>{{t "quotas.col.person"}}</th><th class="num">{{t "quotas.col.rpm"}}</th><th class="num">{{t "quotas.col.tpm"}}</th>
<th class="num">{{t "quotas.col.budget"}}</th><th>{{t "quotas.col.spent"}}</th><th></th></tr>
{{range .People}}
<tr{{if .Editing}} class="editing"{{end}}>
<td class="q-where"><span class="q-name">{{.Email}}</span>
{{if .Models}}<div class="q-sub">{{th "quotas.only_models" .Models}}</div>{{end}}</td>
<td class="num">{{or .Shown.RPM "—"}}</td>
<td class="num">{{or .Shown.TPM "—"}}</td>
<td class="num">{{if .Budget}}{{.Shown.Budget}}{{if .Period}}<span class="q-per">/{{.Shown.Period}}</span>{{end}}{{else}}—{{end}}</td>
<td class="q-spent"><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}}</td>
<td class="q-act">{{if $.CSRF}}{{if .Editing}}<a href="/admin/quotas">{{t "quotas.close"}}</a>
{{else}}<a href="/admin/quotas?edit=user:{{.ID}}">{{if .HasAny}}{{t "quotas.change"}}{{else}}{{t "quotas.set"}}{{end}}</a>{{end}}{{end}}</td>
</tr>
{{if .Editing}}
<tr class="editing"><td colspan="6">
{{template "quota-form" (dict "Scope" "user" "ID" .ID "CSRF" $.CSRF
"F" (dict "RPM" .RPM "TPM" .TPM "Budget" .Budget "Period" .Period "Models" .Models "HasAny" .HasAny)
"Title" (t "quotas.person_form.title" .Email)
"Note" (t "quotas.person_form.note"))}}
</td></tr>
{{end}}
{{end}}
</table>
{{else}}<p class="empty">{{t "quotas.people.empty"}}</p>{{end}}
{{if .HasCapacity}}
<h2 id="capacity">{{t "capacity.title"}}</h2>
<p class="note">{{t "capacity.note"}}</p>
{{if .Capacity}}
<table class="rows quota-tree">
<tr><th>{{t "quotas.col.where"}}</th><th class="num">GPUs</th><th class="num">{{t "capacity.col.cores"}}</th><th class="num">{{t "capacity.col.memory"}}</th><th>{{t "capacity.col.set_by"}}</th><th></th></tr>
{{range .Capacity}}<tr>
<td class="q-where"><span class="q-name">{{or .Name .Scope}}</span></td>
<td class="num">{{or .GPUs "—"}}</td>
<td class="num">{{or .Cores "—"}}</td>
<td class="num">{{or .Memory "—"}}</td>
<td class="note">{{.UpdatedBy}}</td>
<td class="q-act"><a href="/admin/quotas?cap={{.Scope}}#capacity">{{t "quotas.change"}}</a>
{{if $.CSRF}}<form method="post" action="/admin/quotas/capacity" class="inline">
<input type="hidden" name="csrf" value="{{$.CSRF}}">
<input type="hidden" name="scope" value="{{.Scope}}">
<button type="submit" name="clear" value="1" class="link danger">{{t "capacity.remove"}}</button>
</form>{{end}}</td>
</tr>{{end}}
</table>
<p class="q-legend">{{t "capacity.legend"}}</p>
{{else}}<p class="empty">{{t "capacity.empty"}}</p>{{end}}
{{if .Scopes}}
{{if .CSRF}}<form method="post" action="/admin/quotas/capacity" class="q-form capacity-form">
<input type="hidden" name="csrf" value="{{.CSRF}}">
<p class="q-form-title">{{if .CapForm.Scope}}{{th "capacity.form.title_change"}}{{else}}{{th "capacity.form.title_set"}}{{end}}</p>
<div class="field-row">
<label>{{t "quotas.col.where"}}
<select name="scope" required>
<option value="">{{t "capacity.form.choose"}}</option>
{{range .Scopes}}<option value="{{.Key}}"{{if eq .Key $.CapForm.Scope}} selected{{end}}>{{.Name}}</option>{{end}}
</select></label>
</div>
<div class="field-row">
<label class="short">{{t "capacity.form.g_gpus"}} <input type="text" name="gpus" value="{{.CapForm.GPUs}}" placeholder="—"></label>
<label class="short">{{t "capacity.col.cores"}} <input type="text" name="cores" value="{{.CapForm.Cores}}" placeholder="—"></label>
<label class="short">{{t "capacity.col.memory"}} <input type="text" name="memory" value="{{.CapForm.Memory}}" placeholder="—"></label>
<span class="field-gap"></span>
<label class="short">{{t "capacity.form.l_gpus"}} <input type="text" name="limit_gpus" value="{{.CapForm.LGPUs}}" placeholder="—"></label>
<label class="short">{{t "capacity.col.cores"}} <input type="text" name="limit_cores" value="{{.CapForm.LCores}}" placeholder="—"></label>
<label class="short">{{t "capacity.col.memory"}} <input type="text" name="limit_memory" value="{{.CapForm.LMem}}" placeholder="—"></label>
</div>
<div class="q-form-actions"><button type="submit">{{t "common.save"}}</button>
{{if .CapForm.Scope}}<a href="/admin/quotas#capacity">{{t "common.cancel"}}</a>{{end}}</div>
</form>{{end}}
{{else}}<p class="empty">{{th "capacity.empty_tree"}}</p>{{end}}
{{end}}
<details class="explain">
<summary>{{t "quotas.explain.summary"}}</summary>
<p>{{t "quotas.explain.chain"}}</p>
<p>{{th "quotas.explain.keys"}}</p>
</details>
{{end}}
{{/* A limit as the table shows it: the row's own value, else the one it is
held to from above, marked as inherited and saying from where. */}}
{{/* The form for a limit that is not a branch's own: per member, or a person. */}}
{{define "quota-form"}}
{{if .CSRF}}<form method="post" action="/admin/quotas" class="q-form">
<input type="hidden" name="csrf" value="{{.CSRF}}">
<input type="hidden" name="scope" value="{{.Scope}}">
<input type="hidden" name="id" value="{{.ID}}">
{{/* From a panel on the people tab: the answer comes back to that panel. */}}
{{with .Sel}}<input type="hidden" name="tab" value="people"><input type="hidden" name="sel" value="{{.}}">{{end}}
<p class="q-form-title">{{th "quotas.qform.title" .Title .Note}}</p>
<div class="field-row">
<label class="short">{{t "quotas.form.rpm"}} <input type="text" name="rpm" value="{{.F.RPM}}" placeholder="{{t "common.none"}}"></label>
<label class="short">{{t "quotas.form.tpm"}} <input type="text" name="tpm" value="{{.F.TPM}}" placeholder="{{t "common.none"}}"></label>
<label class="short">{{t "quotas.col.budget"}} <input type="text" name="budget" value="{{.F.Budget}}" placeholder="{{t "common.none"}}"></label>
<label class="q-period">{{t "quotas.form.per"}}
<select name="period">
<option value="month"{{if eq .F.Period "month"}} selected{{end}}>{{t "quotas.per.month"}}</option>
<option value="day"{{if eq .F.Period "day"}} selected{{end}}>{{t "quotas.per.day"}}</option>
</select></label>
<label class="grow">{{t "quotas.form.models"}} <span class="opt">{{t "quotas.form.models_hint"}}</span>
<input type="text" name="models" value="{{.F.Models}}" placeholder="{{t "quotas.form.every_model"}}"></label>
</div>
<div class="q-form-actions">
<button type="submit">{{t "common.save"}}</button>
{{if not .Sel}}<a href="/admin/quotas">{{t "common.cancel"}}</a>{{end}}
<button type="submit" name="clear" value="1" class="link danger"
{{if not .F.HasAny}}disabled{{end}}>{{t "quotas.qform.remove"}}</button>
</div>
</form>{{end}}
{{end}}
{{define "quota-value"}}{{if .Own}}{{.Own}}{{else if .Up}}<span class="q-up" title="{{t "quotas.set_on" .From}}">{{.Up}} ↑</span>{{else}}<span class="q-none">—</span>{{end}}{{end}}