Files
llmgw/internal/web/templates/admin_users.html
T
Ilker ManapandClaude Opus 5.5 11231d8c2b The portal in English and Turkish: a catalogue per language, a switcher at the foot of every page, landing.language, Turkish numbers and dates
Every template and handler message in internal/web goes through a JSON
catalogue under internal/web/locales/<code>/, with English as the fallback.
The language is the person's choice (a cookie set by /_lang/{code}), else
Accept-Language, else landing.language. Tests refuse a key some language
lacks, a translation with other arguments than English, and template text
left in English.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 00:12:53 +02:00

322 lines
17 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.
{{/* The first tab of the people page: the tree, and everyone in it. */}}
{{define "people_body"}}
{{if .Error}}<p class="error" role="alert">{{.Error}}</p>{{end}}
{{if .Message}}<p class="ok" role="status">{{.Message}}</p>{{end}}
{{if .Pending}}
<h2>{{t "users.pending.title"}}</h2>
<table class="rows"><tr><th>{{t "users.col.email"}}</th><th>{{t "users.col.name"}}</th><th>{{t "users.col.signed_up"}}</th><th>{{t "users.col.approve_into"}}</th><th></th></tr>
{{range .Pending}}<tr>
<td>{{.Email}}</td><td>{{.Name}}</td><td>{{ago .CreatedAt}}</td>
<td>
<form method="post" action="/admin/users/{{.ID}}/approve" class="inline">
<input type="hidden" name="csrf" value="{{$.CSRF}}">
<select name="project" aria-label="{{t "users.project_for" .Email}}">
<option value="">{{t "users.no_project"}}</option>
{{range $.Projects}}<option value="{{.ID}}">{{.Path}}</option>{{end}}
</select>
<button type="submit">{{t "users.approve"}}</button>
</form>
</td>
<td>
<form method="post" action="/admin/users/{{.ID}}/delete" class="inline"
hx-confirm="{{t "users.reject_confirm" .Email}}">
<input type="hidden" name="csrf" value="{{$.CSRF}}">
<button type="submit" class="link danger">{{t "users.reject"}}</button>
</form>
</td>
</tr>{{end}}</table>
{{end}}
<h2>{{t "users.tree.title"}}</h2>
<p class="note">{{if .HasQuotas}}{{th "users.tree.note_quotas"}}{{else}}{{t "users.tree.note"}}{{end}}</p>
{{if not .Tree}}
<p class="empty">{{t "users.empty"}}</p>
{{end}}
{{range $org := .Tree}}
<section class="org">
<div class="node-head">
<strong class="node-name"><a href="/admin/users?sel=org:{{$org.ID}}" class="sel-link{{if eq $.Sel (printf "org:%s" $org.ID)}} on{{end}}">{{$org.Name}}</a></strong>
<span class="node-meta">{{tn "users.n_teams" (len $org.Teams)}}</span>
{{template "scope-members" (dict "Kind" "org" "ID" $org.ID "Name" $org.Name "D" $)}}
<details class="edit"><summary>{{t "users.rename"}}</summary>
<form method="post" action="/admin/tenancy/org/{{$org.ID}}/rename">
<input type="hidden" name="csrf" value="{{$.CSRF}}">
<input type="text" name="name" value="{{$org.Name}}" required aria-label="{{t "users.new_name"}}">
<button type="submit">{{t "common.save"}}</button>
</form>
</details>
{{if not $org.Teams}}
<form method="post" action="/admin/tenancy/org/{{$org.ID}}/delete" class="inline"
hx-confirm="{{t "users.remove_org_confirm" $org.Name}}">
<input type="hidden" name="csrf" value="{{$.CSRF}}">
<button type="submit" class="link danger">{{t "users.remove"}}</button>
</form>
{{end}}
</div>
{{range $team := $org.Teams}}
<div class="team">
<div class="node-head">
<span class="node-name"><a href="/admin/users?sel=team:{{$team.ID}}" class="sel-link{{if eq $.Sel (printf "team:%s" $team.ID)}} on{{end}}">{{$team.Name}}</a></span>
<span class="node-meta">{{tn "users.n_projects" (len $team.Projects)}}</span>
{{template "scope-members" (dict "Kind" "team" "ID" $team.ID "Name" $team.Name "D" $)}}
<details class="edit"><summary>{{t "users.rename"}}</summary>
<form method="post" action="/admin/tenancy/team/{{$team.ID}}/rename">
<input type="hidden" name="csrf" value="{{$.CSRF}}">
<input type="text" name="name" value="{{$team.Name}}" required aria-label="{{t "users.new_name"}}">
<button type="submit">{{t "common.save"}}</button>
</form>
</details>
{{if gt (len $.Orgs) 1}}
<details class="edit"><summary>{{t "users.move"}}</summary>
<form method="post" action="/admin/tenancy/team/{{$team.ID}}/move">
<input type="hidden" name="csrf" value="{{$.CSRF}}">
<select name="to" aria-label="{{t "users.move_team_to" $team.Name}}">
{{range $.Orgs}}{{if ne .ID $org.ID}}<option value="{{.ID}}">{{.Name}}</option>{{end}}{{end}}
</select>
<button type="submit">{{t "users.move_button"}}</button>
</form>
</details>
{{end}}
{{if not $team.Projects}}
<form method="post" action="/admin/tenancy/team/{{$team.ID}}/delete" class="inline"
hx-confirm="{{t "users.remove_team_confirm" $team.Name}}">
<input type="hidden" name="csrf" value="{{$.CSRF}}">
<button type="submit" class="link danger">{{t "users.remove"}}</button>
</form>
{{end}}
</div>
{{range $p := $team.Projects}}
<details class="project" id="p-{{$p.ID}}"{{if eq $.Open $p.ID}} open{{end}}>
<summary>
<span class="node-name">{{$p.Name}}</span>
<a href="/admin/users?sel=project:{{$p.ID}}&amp;open={{$p.ID}}" class="sel-link small{{if eq $.Sel (printf "project:%s" $p.ID)}} on{{end}}" title="{{t "users.limits_of" $p.Name}}">{{t "users.limits_and_use"}}</a>
<span class="node-meta">{{tn "users.n_members" $p.Users}}
· {{tn "users.n_keys" $p.ActiveKeys}}{{if gt $p.Keys $p.ActiveKeys}} {{t "users.n_revoked" (sub $p.Keys $p.ActiveKeys)}}{{end}}</span>
</summary>
<div class="project-body">
<div class="node-head">
<details class="edit"><summary>{{t "users.rename"}}</summary>
<form method="post" action="/admin/tenancy/project/{{$p.ID}}/rename">
<input type="hidden" name="csrf" value="{{$.CSRF}}">
<input type="hidden" name="open" value="{{$p.ID}}">
<input type="text" name="name" value="{{$p.Name}}" required aria-label="{{t "users.new_name"}}">
<button type="submit">{{t "common.save"}}</button>
</form>
</details>
{{if gt (len $.Teams) 1}}
<details class="edit"><summary>{{t "users.move_project"}}</summary>
<form method="post" action="/admin/tenancy/project/{{$p.ID}}/move">
<input type="hidden" name="csrf" value="{{$.CSRF}}">
<input type="hidden" name="open" value="{{$p.ID}}">
<select name="to" aria-label="{{t "users.move_project_to" $p.Name}}">
{{range $.Teams}}{{if ne .ID $team.ID}}<option value="{{.ID}}">{{.OrgName}} / {{.Name}}</option>{{end}}{{end}}
</select>
<button type="submit">{{t "users.move_button"}}</button>
</form>
</details>
{{end}}
{{if and (eq $p.Users 0) (eq $p.Keys 0)}}
<form method="post" action="/admin/tenancy/project/{{$p.ID}}/delete" class="inline"
hx-confirm="{{t "users.remove_project_confirm" $p.Name}}">
<input type="hidden" name="csrf" value="{{$.CSRF}}">
<button type="submit" class="link danger">{{t "users.remove"}}</button>
</form>
{{else}}<span class="note">{{t "users.not_removable"}}</span>{{end}}
</div>
{{with index $.Members (printf "project:%s" $p.ID)}}
<table class="rows compact"><tr><th>{{t "users.col.member"}}</th><th>{{t "users.col.state"}}</th><th>{{t "users.col.last_sign_in"}}</th><th></th></tr>
{{range .}}{{template "member-row" (dict "U" . "D" $ "Scope" (printf "project:%s" $p.ID) "Open" $p.ID)}}{{end}}
</table>
{{else}}<p class="empty">{{t "users.no_members"}}</p>{{end}}
{{template "add-member" (dict "Kind" "project" "ID" $p.ID "D" $ "Open" $p.ID)}}
{{with index $.Keys $p.ID}}
<table class="rows compact"><tr><th>{{t "users.col.key"}}</th><th>{{t "users.col.owner"}}</th><th>{{t "users.col.state"}}</th><th>{{t "users.col.last_used"}}</th><th></th></tr>
{{range .}}<tr>
<td>{{.Name}} <code>{{.Prefix}}</code></td>
<td>{{or .OwnerEmail (t "users.issued_by_operator")}}</td>
<td class="state state-{{.State}}">{{t (print "users.key_state." .State)}}</td>
<td>{{ago .LastUsedAt}}</td>
<td>{{if eq .State "active"}}
<form method="post" action="/admin/keys/{{.ID}}/revoke" class="inline"
hx-confirm="{{t "users.revoke_confirm" .Name}}">
<input type="hidden" name="csrf" value="{{$.CSRF}}">
<input type="hidden" name="open" value="{{$p.ID}}">
<button type="submit" class="link danger">{{t "users.revoke"}}</button>
</form>{{end}}</td>
</tr>{{end}}
</table>
{{end}}
</div>
</details>
{{end}}
{{/* Folded until wanted: a box open under every team was most of the
tree's height and read as something to fill in. */}}
<details class="add-inline">
<summary>{{t "users.add_project"}}</summary>
<form method="post" action="/admin/tenancy/project" class="add">
<input type="hidden" name="csrf" value="{{$.CSRF}}">
<input type="hidden" name="team" value="{{$team.ID}}">
<input type="text" name="name" required placeholder="{{t "users.new_project_in" $team.Name}}" aria-label="{{t "users.new_project_in_label" $team.Name}}">
<button type="submit">{{t "common.add"}}</button>
</form>
</details>
</div>
{{end}}
<details class="add-inline team-add">
<summary>{{t "users.add_team"}}</summary>
<form method="post" action="/admin/tenancy/team" class="add">
<input type="hidden" name="csrf" value="{{$.CSRF}}">
<input type="hidden" name="org" value="{{$org.ID}}">
<input type="text" name="name" required placeholder="{{t "users.new_team_in" $org.Name}}" aria-label="{{t "users.new_team_in_label" $org.Name}}">
<button type="submit">{{t "common.add"}}</button>
</form>
</details>
</section>
{{end}}
<form method="post" action="/admin/tenancy/org" class="add org-add">
<input type="hidden" name="csrf" value="{{.CSRF}}">
<input type="text" name="name" required placeholder="{{t "users.new_org"}}" aria-label="{{t "users.new_org_label"}}">
<button type="submit">{{t "users.add_org"}}</button>
</form>
<h2 id="accounts">{{t "users.accounts"}}</h2>
{{if .Accounts}}
<table class="rows" id="account-list"><tr><th>{{t "users.col.person"}}</th><th>{{t "users.col.state"}}</th><th>{{t "users.col.member_of"}}</th><th>{{t "users.col.last_sign_in"}}</th><th>{{t "users.col.add_to"}}</th><th></th></tr>
{{range .Accounts}}{{template "account-row" (dict "U" . "D" $)}}{{end}}
</table>
<p class="note">{{t "users.alone_note"}}</p>
{{else}}<p class="empty">{{t "users.no_accounts"}}</p>{{end}}
<p class="note">{{th "users.shell_note"}}</p>
{{end}}
{{/* The actions every account row carries: suspend or reinstate, remove. */}}
{{define "state-actions"}}{{$u := .U}}{{$d := .D}}
{{if ne $u.ID $d.AdminID}}
{{/* In a menu: two red links on every row made the list look alarmed,
and they are the two things done least often. */}}
<details class="row-menu">
<summary aria-label="{{t "users.more_for" $u.Email}}" title="{{t "users.more"}}">⋯</summary>
<div class="menu">
{{if eq (printf "%s" $u.State) "active"}}
<form method="post" action="/admin/users/{{$u.ID}}/suspend" class="inline">
<input type="hidden" name="csrf" value="{{$d.CSRF}}">
<input type="hidden" name="open" value="{{.Open}}">
<button type="submit" class="link danger">{{t "users.suspend"}}</button>
</form>
{{else}}
<form method="post" action="/admin/users/{{$u.ID}}/reinstate" class="inline">
<input type="hidden" name="csrf" value="{{$d.CSRF}}">
<input type="hidden" name="open" value="{{.Open}}">
<button type="submit" class="link">{{t "users.reinstate"}}</button>
</form>
{{end}}
<form method="post" action="/admin/users/{{$u.ID}}/delete" class="inline"
hx-confirm="{{t "users.remove_confirm" $u.Email}}">
<input type="hidden" name="csrf" value="{{$d.CSRF}}">
<input type="hidden" name="open" value="{{.Open}}">
<button type="submit" class="link danger">{{t "users.remove"}}</button>
</form>
</div>
</details>
{{else}}<span class="note">{{t "users.you"}}</span>{{end}}
{{end}}
{{/* A person in the list of every account: where they are, and where else
they could be. */}}
{{define "account-row"}}{{$u := .U}}{{$d := .D}}
<tr data-q="{{lower $u.Email}} {{lower $u.Name}}{{range $u.Memberships}} {{lower .Path}}{{end}}">
<td><a href="/admin/users?sel=user:{{$u.ID}}" class="sel-link{{if eq $d.Sel (printf "user:%s" $u.ID)}} on{{end}}">{{$u.Email}}</a>{{if $u.IsAdmin}} <span class="tag">{{t "users.tag.admin"}}</span>{{end}}</td>
<td class="state state-{{$u.State}}">{{t (print "users.state." $u.State)}}</td>
<td class="where">
{{range $i, $m := $u.Memberships}}{{if lt $i 2}}{{with $m}}<span class="chip-m">{{.Path}}{{if eq .Role "manager"}} <b class="chip-role">{{t "users.role.manager"}}</b>{{end}}
{{if eq (printf "%s" .Scope) "org"}}
<form method="post" action="/admin/users/{{$u.ID}}/role" class="inline">
<input type="hidden" name="csrf" value="{{$d.CSRF}}">
<input type="hidden" name="scope" value="org:{{.ScopeID}}">
<input type="hidden" name="role" value="{{if eq .Role "manager"}}member{{else}}manager{{end}}">
<button type="submit" class="chip-x" title="{{if eq .Role "manager"}}{{t "users.stop_managing_of" .Path}}{{else}}{{t "users.make_manager_of" .Path}}{{end}}"
aria-label="{{if eq .Role "manager"}}{{t "users.stop_managing"}}{{else}}{{t "users.make_manager"}}{{end}}">{{if eq .Role "manager"}}☆{{else}}★{{end}}</button>
</form>
{{end}}
<form method="post" action="/admin/users/{{$u.ID}}/leave" class="inline"
hx-confirm="{{t "users.take_out_confirm" $u.Email .Path}}">
<input type="hidden" name="csrf" value="{{$d.CSRF}}">
<input type="hidden" name="scope" value="{{.Scope}}:{{.ScopeID}}">
<button type="submit" class="chip-x" aria-label="{{t "users.take_out_of" .Path}}" title="{{t "users.take_out_of" .Path}}">×</button>
</form></span>{{end}}{{end}}
{{else}}<span class="opt">{{t "users.on_own"}}</span>{{end}}
{{/* Two, then a count: a person in six teams was six chips wrapping the
row into a paragraph. The rest are named on hover and in their own
scopes' member lists, where taking them out is done. */}}
{{if gt (len $u.Memberships) 2}}<span class="opt more" title="{{range $i, $m := $u.Memberships}}{{if ge $i 2}}{{$m.Path}}&#10;{{end}}{{end}}">+{{sub (len $u.Memberships) 2}}</span>{{end}}
</td>
<td>{{ago $u.LastLoginAt}}</td>
<td>
<form method="post" action="/admin/users/{{$u.ID}}/join" class="inline add-to">
<input type="hidden" name="csrf" value="{{$d.CSRF}}">
<select name="scope" aria-label="{{t "users.add_to_label" $u.Email}}" required>
<option value="">{{t "users.choose"}}</option>
{{range $d.Scopes}}<option value="{{.Value}}">{{.Label}}</option>{{end}}
</select>
<button type="submit" class="small">{{t "common.add"}}</button>
</form>
</td>
<td class="actions">{{template "state-actions" (dict "U" $u "D" $d "Open" "")}}</td>
</tr>{{end}}
{{/* A member in a scope's own list: taking them out of here is the action. */}}
{{define "member-row"}}{{$u := .U}}{{$d := .D}}
<tr>
<td>{{$u.Email}}{{if $u.IsAdmin}} <span class="tag">{{t "users.tag.admin"}}</span>{{end}}
{{if gt (len $u.Memberships) 1}}<span class="opt">{{t "users.also_in" (sub (len $u.Memberships) 1)}}</span>{{end}}</td>
<td class="state state-{{$u.State}}">{{t (print "users.state." $u.State)}}</td>
<td>{{ago $u.LastLoginAt}}</td>
<td class="actions">
<form method="post" action="/admin/users/{{$u.ID}}/leave" class="inline"
hx-confirm="{{t "users.take_out_here_confirm" $u.Email}}">
<input type="hidden" name="csrf" value="{{$d.CSRF}}">
<input type="hidden" name="scope" value="{{.Scope}}">
<input type="hidden" name="open" value="{{.Open}}">
<button type="submit" class="link danger">{{t "users.take_out"}}</button>
</form>
</td>
</tr>{{end}}
{{/* Adding a person to a scope, from the scope. */}}
{{define "add-member"}}{{$d := .D}}
<form method="post" action="/admin/tenancy/{{.Kind}}/{{.ID}}/add" class="add">
<input type="hidden" name="csrf" value="{{$d.CSRF}}">
<input type="hidden" name="open" value="{{.Open}}">
<select name="user" aria-label="{{t "users.person_to_add"}}" required>
<option value="">{{t "users.choose_person"}}</option>
{{range $d.Accounts}}<option value="{{.ID}}">{{.Email}}</option>{{end}}
</select>
<button type="submit">{{t "users.add_member"}}</button>
</form>{{end}}
{{/* An org's or team's own members, folded away until asked for. */}}
{{define "scope-members"}}{{$d := .D}}{{$k := printf "%s:%s" .Kind .ID}}
<details class="edit"><summary>{{tn "users.n_members" (len (index $d.Members $k))}}</summary>
<div class="scope-members">
{{with index $d.Members $k}}
<table class="rows compact">
{{range .}}{{template "member-row" (dict "U" . "D" $d "Scope" $k "Open" "")}}{{end}}
</table>
{{else}}<p class="note">{{t "users.nobody_in" .Name}}</p>{{end}}
{{template "add-member" (dict "Kind" .Kind "ID" .ID "D" $d "Open" "")}}
</div>
</details>{{end}}