Files
llmgw/internal/web/templates/org.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

183 lines
9.5 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.
{{define "org"}}<!doctype html>
<html lang="{{lang}}"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>{{.Org.Name}} · llmgw</title><link rel="stylesheet" href="{{stylesheet}}">
{{if .Edit}}<script defer src="{{asset "pages.js"}}"></script>{{end}}
</head><body><div class="wrap">
<header>
<h1>{{.Org.Name}}</h1>
<p class="sub">{{tn "org.sub" .People}}</p>
{{template "nav" .Nav}}
</header>
{{if gt (len .Orgs) 1}}
<div class="filters">
<span class="filter-label">{{t "org.filter"}}</span>
{{range .Orgs}}<a class="chip{{if eq .ID $.Org.ID}} on{{end}}" href="/org/{{.ID}}">{{.Name}}</a>{{end}}
</div>
{{end}}
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
{{if .Message}}<p class="ok">{{.Message}}</p>{{end}}
<h2>{{t "org.use"}}</h2>
<div class="filters">
<span class="filter-label">{{t "usage.group_by"}}</span>
{{range .Groups}}<a class="chip{{if .Selected}} on{{end}}"
href="/org/{{$.Org.ID}}?by={{.Key}}&amp;window={{$.Window}}">{{.Label}}</a>{{end}}
<span class="filter-label">{{t "usage.window"}}</span>
{{range .Windows}}<a class="chip{{if .Selected}} on{{end}}"
href="/org/{{$.Org.ID}}?by={{$.By}}&amp;window={{.Key}}">{{.Label}}</a>{{end}}
</div>
<div class="chart-row">
{{template "tschart" dict "t" .Requests "title" (t "usage.requests")
"note" (t "org.requests_note")}}
{{template "tschart" dict "t" .Tokens "title" (t "usage.tokens")
"note" (t "org.tokens_note")}}
</div>
{{if .Priced}}
<div class="chart-row">
{{template "tschart" dict "t" .Spend "title" (t "usage.spend") "note" (t "org.spend_note")}}
</div>
{{end}}
{{if .Rows}}
<table class="rows compact">
<tr><th>{{if eq .By "user"}}{{t "org.col.person"}}{{else if eq .By "project"}}{{t "org.col.project"}}{{else}}{{t "org.col.team"}}{{end}}</th>
<th class="num">{{t "usage.requests"}}</th><th class="num">{{t "usage.tokens"}}</th><th class="num">{{t "usage.spend"}}</th><th class="num">{{t "usage.errors"}}</th></tr>
{{range .Rows}}<tr>
<td>{{.Name}}</td><td class="num">{{compact .Requests}}</td><td class="num">{{compact .Tokens}}</td>
<td class="num">{{.Spend}}</td><td class="num">{{if .Errors}}{{count .Errors}}{{else}}—{{end}}</td>
</tr>{{end}}
</table>
{{else}}<p class="empty">{{t "org.use_empty"}}</p>{{end}}
<p class="note">{{t "org.use_scope"}}</p>
<h2>{{t "org.limits.title"}}</h2>
{{if .Quotas}}
<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 "org.col.spent_30d"}}</th></tr>
{{range .Quotas}}
<tr class="q-depth-{{.Indent}}">
<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>{{end}}
{{if .Shadowed}}<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}}"><i style="width:{{pct .Percent 100}}%"></i></span>
<span class="q-pct">{{t "quotas.pct" .Percent}}</span>{{end}}</td>
</tr>
{{end}}
</table>
<p class="note">{{t "org.limits.note"}}</p>
{{end}}
<h2>{{t "org.members.title"}}</h2>
{{if .Members}}
<table class="rows compact">
<tr><th>{{t "org.col.person"}}</th><th>{{t "org.col.member_of"}}</th>{{if .Edit}}<th>{{t "org.col.role"}}</th>{{end}}<th>{{t "org.col.state"}}</th></tr>
{{range $m := .Members}}<tr>
<td>{{$m.Email}}</td>
<td>{{range $m.Places}}<span class="chip-m">{{.Where}}{{if and (ne .Role "member") (not $.Edit)}} <b class="chip-role">{{t (print "users.role." .Role)}}</b>{{end}}
{{if $.Edit}}<form method="post" action="/org/{{$.Org.ID}}/members/{{$m.ID}}/leave" class="inline"
data-confirm="{{t "users.take_out_confirm" $m.Email .Where}}">
<input type="hidden" name="csrf" value="{{$.CSRF}}"><input type="hidden" name="where" value="{{.Value}}">
<button type="submit" class="chip-x" title="{{t "users.take_out_of" .Where}}" aria-label="{{t "users.take_out_of" .Where}}">×</button>
</form>{{end}}</span>{{end}}</td>
{{if $.Edit}}<td>{{if and $m.OrgRole (eq $m.ID $.Me)}}<span class="chip-role">{{t (print "users.role." $m.OrgRole)}}</span>{{else if $m.OrgRole}}
<form method="post" action="/org/{{$.Org.ID}}/members/{{$m.ID}}/role" class="inline">
<input type="hidden" name="csrf" value="{{$.CSRF}}">
<select name="role" class="chip-sel" data-autosubmit aria-label="{{t "users.role_in" $.Org.Name}}" title="{{t "users.role_note"}}">
{{$r := $m.OrgRole}}{{range $role := roles}}<option value="{{$role}}"{{if eq $role $r}} selected{{end}}>{{t (print "users.role." $role)}}</option>{{end}}
</select>
</form>{{else}}<span class="opt">—</span>{{end}}</td>{{end}}
<td class="state state-{{$m.State}}">{{with $m.State}}{{t (print "org.state." .)}}{{end}}</td>
</tr>{{end}}
</table>
{{else}}<p class="empty">{{t "org.members.empty"}}</p>{{end}}
{{if .Edit}}
<form method="post" action="/org/{{.Org.ID}}/members" class="row">
<input type="hidden" name="csrf" value="{{.CSRF}}">
<input type="email" name="email" required placeholder="{{t "org.add.email"}}" aria-label="{{t "org.add.email"}}">
<select name="where" required aria-label="{{t "org.add.where"}}">
{{range .Places}}<option value="{{.Value}}">{{.Label}}</option>{{end}}
</select>
<button type="submit">{{t "org.add.button"}}</button>
</form>
<p class="note">{{t "org.add.note"}}</p>
<h2>{{t "org.tree.title"}}</h2>
{{if .Teams}}
<table class="rows compact">
<tr><th>{{t "org.col.team"}}</th><th>{{t "org.col.projects"}}</th><th></th></tr>
{{range $tm := .Teams}}<tr>
<td><details class="edit"><summary>{{$tm.Name}}</summary>
<form method="post" action="/org/{{$.Org.ID}}/team/{{$tm.ID}}/rename" class="row">
<input type="hidden" name="csrf" value="{{$.CSRF}}">
<input type="text" name="name" value="{{$tm.Name}}" required aria-label="{{t "org.tree.rename"}}">
<button type="submit" class="small">{{t "org.tree.rename"}}</button>
</form></details></td>
<td>{{range $tm.Projects}}<span class="chip-m">{{.Name}}
<form method="post" action="/org/{{$.Org.ID}}/project/{{.ID}}/delete" class="inline" data-confirm="{{t "org.tree.remove_confirm" .Name}}">
<input type="hidden" name="csrf" value="{{$.CSRF}}">
<button type="submit" class="chip-x" title="{{t "org.tree.remove" .Name}}" aria-label="{{t "org.tree.remove" .Name}}">×</button>
</form></span>{{end}}
<form method="post" action="/org/{{$.Org.ID}}/projects" class="inline add-to">
<input type="hidden" name="csrf" value="{{$.CSRF}}"><input type="hidden" name="team" value="{{$tm.ID}}">
<input type="text" name="name" required placeholder="{{t "org.tree.new_project"}}" aria-label="{{t "org.tree.new_project_in" $tm.Name}}">
<button type="submit" class="small">{{t "common.add"}}</button>
</form></td>
<td class="num"><form method="post" action="/org/{{$.Org.ID}}/team/{{$tm.ID}}/delete" class="inline" data-confirm="{{t "org.tree.remove_confirm" $tm.Name}}">
<input type="hidden" name="csrf" value="{{$.CSRF}}">
<button type="submit" class="link danger">{{t "org.tree.remove_team"}}</button>
</form></td>
</tr>{{end}}
</table>
{{else}}<p class="empty">{{t "org.tree.empty"}}</p>{{end}}
<form method="post" action="/org/{{.Org.ID}}/teams" class="row">
<input type="hidden" name="csrf" value="{{.CSRF}}">
<input type="text" name="name" required placeholder="{{t "org.tree.new_team"}}" aria-label="{{t "org.tree.new_team"}}">
<button type="submit">{{t "org.tree.add_team"}}</button>
</form>
<p class="note">{{t "org.tree.note"}}</p>
<h2>{{t "org.keys.title"}}</h2>
{{if .Keys}}
<table class="rows compact">
<tr><th>{{t "org.keys.col.key"}}</th><th>{{t "org.col.project"}}</th><th>{{t "org.keys.col.owner"}}</th><th>{{t "org.keys.col.used"}}</th><th></th></tr>
{{range .Keys}}<tr>
<td><code>{{.Prefix}}…</code> {{.Name}}</td><td>{{.ProjectPath}}</td><td>{{or .OwnerEmail "—"}}</td>
<td>{{if .LastUsedAt.IsZero}}—{{else}}{{ago .LastUsedAt}}{{end}}</td>
<td class="num"><form method="post" action="/org/{{$.Org.ID}}/keys/{{.ID}}/revoke" class="inline" data-confirm="{{t "org.keys.revoke_confirm" .Prefix}}">
<input type="hidden" name="csrf" value="{{$.CSRF}}">
<button type="submit" class="link danger">{{t "org.keys.revoke"}}</button>
</form></td>
</tr>{{end}}
</table>
{{else}}<p class="empty">{{t "org.keys.empty"}}</p>{{end}}
{{end}}
<footer><span>llmgw {{.Version}}</span><span>{{stamp .Now}}</span>{{template "langs"}}</footer>
</div></body></html>{{end}}
{{define "org-none"}}<!doctype html>
<html lang="{{lang}}"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>{{t "org.none.title"}} · llmgw</title><link rel="stylesheet" href="{{stylesheet}}">
</head><body><div class="wrap">
<header>
<h1>{{t "org.none.title"}}</h1>
<p class="sub">{{t "org.none.sub"}}</p>
{{template "nav" .Nav}}
</header>
<p>{{t "org.none.about"}}</p>
<p>{{th "org.none.how"}}</p>
<footer><span>llmgw {{.Version}}</span><span>{{stamp .Now}}</span>{{template "langs"}}</footer>
</div></body></html>{{end}}