CI / test (push) Successful in 1m44s
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>
96 lines
4.5 KiB
HTML
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}}
|