Files
llmgw/internal/web/templates/account.html
T
Ilker ManapandClaude Opus 5.5 16a1e8b4d8 Show a person their own use and what is left of their budgets
The account page (was "Your keys") shows the last thirty days of the
person's own use - requests, tokens, spend and tokens per day - and, for
each place they can spend, what they used there and a bar for every
budget that applies: their own, a per-member one, or a project's, team's
or organization's shared with others, named as such. Bars turn amber past
four-fifths and red when spent. It sits under the keys now, above the
password form.

Spend is read from the usage table for the pool each limit counts
against, from the start of its period.

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

156 lines
7.2 KiB
HTML

{{define "signup"}}<!doctype html>
<html lang="en"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Sign up · llmgw</title><link rel="stylesheet" href="{{stylesheet}}">
</head><body><div class="wrap narrow">
<header><h1>Create an account</h1>
<p class="sub">An operator approves it before it can be used.</p></header>
{{if .Done}}
<p>Thank you. Your account is waiting for approval.</p>
<p class="note">You will not get an email from this gateway — it does not send any.
Tell whoever you asked for access that you have signed up.</p>
<p><a href="/login">Sign in</a> once you have been approved.</p>
{{else}}
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
<form method="post" autocomplete="on">
<label>Email<input type="email" name="email" value="{{.Email}}" required autocomplete="email"></label>
<label>Name<input type="text" name="name" value="{{.Name}}" autocomplete="name"></label>
<label>Password<input type="password" name="password" required minlength="{{.MinPassword}}"
autocomplete="new-password"></label>
<p class="note">At least {{.MinPassword}} characters. Length is the only rule —
a long phrase beats a short one with punctuation in it.</p>
<button type="submit">Sign up</button>
</form>
<p class="note">Already have an account? <a href="/login">Sign in</a>.</p>
{{end}}
<footer><span>llmgw {{.Version}}</span><span><a href="/">about this gateway</a></span></footer>
</div></body></html>{{end}}
{{define "login"}}<!doctype html>
<html lang="en"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Sign in · llmgw</title><link rel="stylesheet" href="{{stylesheet}}">
</head><body><div class="wrap narrow">
<header><h1>Sign in</h1></header>
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
<form method="post" autocomplete="on">
<label>Email<input type="email" name="email" value="{{.Email}}" required autocomplete="email"></label>
<label>Password<input type="password" name="password" required autocomplete="current-password"></label>
<button type="submit">Sign in</button>
</form>
{{if .SignupOpen}}<p class="note">No account? <a href="/signup">Sign up</a>.</p>{{end}}
<footer><span>llmgw {{.Version}}</span><span><a href="/">about this gateway</a></span></footer>
</div></body></html>{{end}}
{{define "profile"}}<!doctype html>
<html lang="en"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Account · llmgw</title><link rel="stylesheet" href="{{stylesheet}}">
</head><body><div class="wrap">
<header>
<h1>Your account</h1>
<p class="sub">{{.User.Email}}{{if ne .User.Name .User.Email}} ({{.User.Name}}){{end}}</p>
{{template "nav" .Nav}}
</header>
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
{{if .NewKey}}
<h2>Your new key</h2>
<p>This is shown once. Nothing can produce it again — only its hash is kept.</p>
<pre>{{.NewKey}}</pre>
<p>Put it in a password manager now.</p>
<pre>curl {{.BaseURL}}/v1/chat/completions \
-H "Authorization: Bearer {{.NewKey}}" \
-H "Content-Type: application/json" \
-d '{"model": "{{.ExampleModel}}", "messages": [{"role":"user","content":"hello"}]}'</pre>
{{end}}
<h2>Create a key</h2>
<form method="post" action="/profile/keys" class="narrow">
<input type="hidden" name="csrf" value="{{.CSRF}}">
<label>What is it for?<input type="text" name="name" placeholder="laptop, ci, notebook" required></label>
{{if .Contexts}}
<label>Spends against
<select name="context">
{{range .Contexts}}<option value="{{.Value}}">{{.Label}}</option>{{end}}
</select></label>
<button type="submit">Create</button>
{{else}}
<p class="note">You are in no project yet, and personal keys are off here.
Ask an administrator to add you to one.</p>
{{end}}
</form>
<h2>Keys</h2>
{{if .Keys}}
<table><tr><th>Name</th><th>Key</th><th>Spends against</th><th>Created</th><th>Last used</th><th>State</th><th></th></tr>
{{range .Keys}}<tr>
<td>{{.Name}}</td>
<td><code>{{.Prefix}}</code></td>
<td>{{if .ProjectID}}{{or (index $.Where .ProjectID) "a project you have left"}}{{else}}Personal{{end}}</td>
<td>{{date .CreatedAt}}</td>
<td>{{ago .LastUsedAt}}</td>
<td class="state state-{{state .}}">{{state .}}</td>
<td>{{if eq (state .) "active"}}
<form method="post" action="/profile/keys/{{.ID}}/revoke">
<input type="hidden" name="csrf" value="{{$.CSRF}}">
<button type="submit" class="link danger">Revoke</button>
</form>
{{end}}</td>
</tr>{{end}}</table>
<p class="note">Revoking takes effect within the gateway's cache window, up to a
minute. To rotate: create the new key, switch over, wait for "last used" on the
old one to stop moving, then revoke it.</p>
{{else}}<p class="empty">You have no keys yet.</p>{{end}}
<h2>Where you are</h2>
{{if .Memberships}}
<p>{{range $i, $m := .Memberships}}{{if $i}} · {{end}}{{$m.Path}} <span class="opt">{{$m.Scope}}</span>{{end}}</p>
{{else}}<p class="empty">You are not a member of any organization, team or project.</p>{{end}}
<h2>Your use and limits</h2>
{{with .Own}}
<div class="tiles own-use">
<div class="tile"><div class="v">{{compact .Requests}}</div><div class="k">requests · 30 days</div></div>
<div class="tile"><div class="v">{{compact .Tokens}}</div><div class="k">tokens · 30 days</div></div>
<div class="tile"><div class="v">{{.Spend}}</div><div class="k">spend · 30 days</div></div>
</div>
{{with .Chart}}<div class="chart-wrap"><h3 class="chart-title">Tokens per day</h3>{{.}}</div>{{end}}
{{end}}
{{range .LimitsBy}}
<h3 class="limits-for">{{.Label}}{{with .Use}} <span class="opt">— {{compact .Requests}} requests, {{compact .Tokens}} tokens in 30 days</span>{{end}}</h3>
{{range .Budgets}}
<div class="budget{{if .Over}} over{{else if .Near}} near{{end}}">
<div class="budget-head"><span>{{.Label}}</span><span><b>{{.Used}}</b> of {{.Budget}} {{.Period}}</span></div>
<div class="budget-bar"><span style="width: {{.Percent}}%"></span></div>
</div>
{{end}}
{{if .Rows}}
<table class="compact narrow-table"><tr><th>Limit</th><th class="num">Value</th></tr>
{{range .Rows}}<tr><td>{{.Name}}</td><td class="num">{{.Value}}</td></tr>{{end}}</table>
{{else}}<p class="empty">No limits here.</p>{{end}}
{{else}}<p class="empty">Nowhere to spend yet.</p>{{end}}
<p class="note">Everything that applies, from the organization down to you: the
strictest value wins.</p>
<h2>Password</h2>
{{if .PasswordChanged}}<p>Your password was changed. Any other browser you were
signed in on has been signed out.</p>{{end}}
{{if .PasswordError}}<p class="error">{{.PasswordError}}</p>{{end}}
<form method="post" action="/profile/password" autocomplete="on" class="narrow">
<input type="hidden" name="csrf" value="{{.CSRF}}">
<label>Current password<input type="password" name="current" required
autocomplete="current-password"></label>
<label>New password<input type="password" name="new" required
minlength="{{.MinPassword}}" autocomplete="new-password"></label>
<button type="submit">Change password</button>
</form>
<p class="note">Changing it signs out every other browser, which is the point if
you are changing it because something went wrong.</p>
<footer><span>llmgw {{.Version}}</span><span>{{.Now}}</span></footer>
</div></body></html>{{end}}