Files
llmgw/internal/web/templates/admin_quotas.html
T
Ilker ManapandClaude Opus 5 5726252825
CI / test (push) Successful in 1m44s
Open the quota form on the branch, instead of asking which branch again
The tree was rendered and then, underneath it, a form with a dropdown asking
which of those rows you meant. The tree already said. Picking a branch, reading
its current numbers, scrolling past the tree and selecting the same branch from
a list is three steps to do one thing, and two of them are a chance to pick the
wrong row.

A quota belongs to a branch, so the form opens on it: each row links to
?edit=scope:id, that row renders its form inline, pre-filled with what is set
there now and naming the branch in full.

By link rather than by disclosure on every row: a collapsed form per branch
would put a form in the markup for every group on a page whose job is to be
read as a tree. One is open at a time, and the rest keep their link.

Clearing moved into that form as a second submit, so a row that is not being
edited has one control and not two.

Saving closes it. Re-rendering the form over the row it just changed hides the
result and invites submitting the same thing twice.

The note about a quota being shared came out of the footer: the open form says
it, at the point where somebody is about to do it.

The dropdown's parsing went with it rather than being left unreachable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-27 14:54:02 +02:00

96 lines
4.5 KiB
HTML

{{define "admin_quotas"}}<!doctype html>
<html lang="en"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Quotas · llmgw</title><link rel="stylesheet" href="/_static/base.css">
</head><body><div class="wrap">
<header>
<h1>Quotas</h1>
<p class="sub">What each organization, team and project may use · spend over the last {{.Since}}</p>
<nav><a href="/admin">cluster</a> <a href="/admin/users">accounts</a>
<a href="/profile">your keys</a></nav>
</header>
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
{{if .Message}}<p class="ok">{{.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>
<tr><th>Where</th><th>RPM</th><th>TPM</th><th>Budget</th><th>Spent</th><th>Used</th><th></th></tr>
{{range .Rows}}
<tr{{if .Editing}} class="editing"{{end}}>
<td style="padding-left:{{.Indent}}.6rem">
{{if eq .Indent 0}}<strong>{{.Path}}</strong>{{else}}{{.Path}}{{end}}
<span class="opt">{{.Scope}}</span>
</td>
<td class="num">{{if .RPM}}{{.RPM}}{{else}}—{{end}}</td>
<td class="num">{{if .TPM}}{{.TPM}}{{else}}—{{end}}</td>
<td class="num">{{if .Budget}}{{.Budget}}{{if .Period}}/{{.Period}}{{end}}{{else}}—{{end}}</td>
<td class="num">{{.Spend}}</td>
<td class="meter">{{if .HasPct}}<span style="width:{{pct .Percent 100}}%"></span><em>{{.Percent}}%</em>{{else}}—{{end}}</td>
<td>
{{if .Editing}}<a href="/admin/quotas">close</a>
{{else}}<a href="/admin/quotas?edit={{.Scope}}:{{.ID}}">{{if .HasAny}}change{{else}}set quota{{end}}</a>{{end}}
</td>
</tr>
{{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 and the first sign would be a refusal that
disagrees with the screen. */}}
<tr><td colspan="7" class="error-row">written here, but {{.Shadowed}}</td></tr>
{{end}}
{{if .Models}}<tr><td colspan="7" class="url-row">only: <code>{{.Models}}</code></td></tr>{{end}}
{{if .Editing}}
<tr class="editing"><td colspan="7">
<form method="post" action="/admin/quotas">
<input type="hidden" name="csrf" value="{{$.CSRF}}">
<input type="hidden" name="scope" value="{{.Scope}}">
<input type="hidden" name="id" value="{{.ID}}">
<p class="sub">Quota for <strong>{{.Crumb}}</strong> — shared by everything
beneath it. Whatever is set here replaces what was there; a field left blank
is no limit of that kind, not a limit of zero.</p>
<div class="field-row">
<label>Requests / minute
<input type="text" name="rpm" value="{{.RPM}}" placeholder="none"></label>
<label>Tokens / minute
<input type="text" name="tpm" value="{{.TPM}}" placeholder="none"></label>
<label>Budget
<input type="text" name="budget" value="{{.Budget}}" placeholder="none"></label>
<label>per
<select name="period">
<option value="month"{{if eq .Period "month"}} selected{{end}}>month</option>
<option value="day"{{if eq .Period "day"}} selected{{end}}>day</option>
</select></label>
</div>
<label>Only these models <span class="opt">comma separated; blank allows all</span>
<input type="text" name="models" value="{{.Models}}" placeholder="every model"></label>
<button type="submit">Save</button>
<button type="submit" name="clear" value="1" class="link danger"
{{if not .HasAny}}disabled{{end}}>Remove every limit here</button>
</form>
</td></tr>
{{end}}
{{end}}
</table>
{{else}}
<p class="empty">Nothing to give a quota to yet. Make an organization on the
<a href="/admin/users">accounts page</a> first.</p>
{{end}}
<p class="note">Limits compose downward by taking the most restrictive value, so
a team cannot raise what its organization set and a project cannot raise its
team's. Only an organization's own limit is bounded by nothing. Model lists
intersect rather than add: a project cannot allow a model its team withheld.</p>
<p class="note">Still not here, deliberately: per-key limits. A key is issued by
the person who owns it and bounded by the project it belongs to, and letting an
admin session reach individual keys would put every caller's throughput one
click away — <code>llmgw keys limits --scope key</code> for that.</p>
<footer><span>llmgw {{.Version}}</span><span>{{.Now}}</span></footer>
</div></body></html>{{end}}