/admin/mirror, behind the same admin session as the other admin pages. What is held, what is arriving with its byte count, what failed and why; start a fetch, retry a failed one, remove a version. Not mounted at all without a mirror configured. The right page for a deployment with no mirror is no page, rather than an empty one pointing at nothing. It reloads itself only while something is downloading. A reload at any other moment takes with it whatever is half-typed into the fetch form, and there is nothing to watch when nothing is moving. A failed row offers Retry, not Remove. A fetch that failed left nothing on disk, so the remove could only answer "the mirror holds nothing for this" — a button whose single outcome is a confusing error. Starting the same download again is what somebody looking at that row wants, and the mirror allows it: only a download still running is refused. An unreachable mirror is said outright instead of rendering as an empty table. The two look nearly identical and mean opposite things: one is a mirror waiting to be filled, the other is a machine to go and look at. Dashboard takes NavExtras rather than a second bool. Two adjacent booleans at a call site get swapped silently and are discovered by somebody clicking the wrong link. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
71 lines
2.9 KiB
HTML
71 lines
2.9 KiB
HTML
{{define "admin_users"}}<!doctype html>
|
|
<html lang="en"><head>
|
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>Accounts · llmgw</title><link rel="stylesheet" href="/_static/base.css">
|
|
</head><body><div class="wrap">
|
|
<header>
|
|
<h1>Accounts</h1>
|
|
<p class="sub">{{len .Pending}} waiting for approval · {{len .Active}} active</p>
|
|
<nav><a href="/admin">cluster</a>
|
|
{{if .HasMirror}} <a href="/admin/mirror">mirror</a>
|
|
{{end}} <a href="/profile">your keys</a></nav>
|
|
</header>
|
|
|
|
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
|
|
{{if .Message}}<p class="ok">{{.Message}}</p>{{end}}
|
|
|
|
<h2>Waiting for approval</h2>
|
|
{{if .Pending}}
|
|
<table><tr><th>Email</th><th>Name</th><th>Signed up</th><th>Project</th><th></th></tr>
|
|
{{range .Pending}}<tr>
|
|
<td>{{.Email}}</td><td>{{.Name}}</td><td>{{ago .CreatedAt}}</td>
|
|
<td colspan="2">
|
|
<form method="post" action="/admin/users/{{.ID}}/approve" class="inline">
|
|
<input type="hidden" name="csrf" value="{{$.CSRF}}">
|
|
<select name="project">
|
|
{{range $.Projects}}<option value="{{.ID}}">{{.Path}}</option>{{end}}
|
|
</select>
|
|
<button type="submit">Approve</button>
|
|
</form>
|
|
<form method="post" action="/admin/users/{{.ID}}/suspend" class="inline">
|
|
<input type="hidden" name="csrf" value="{{$.CSRF}}">
|
|
<button type="submit" class="link danger">Reject</button>
|
|
</form>
|
|
</td>
|
|
</tr>{{end}}</table>
|
|
{{else}}<p class="empty">Nobody is waiting.</p>{{end}}
|
|
|
|
<h2>Accounts</h2>
|
|
{{if .Active}}
|
|
<table><tr><th>Email</th><th>State</th><th>Admin</th><th>Project</th><th>Last login</th><th></th></tr>
|
|
{{range .Active}}<tr>
|
|
<td>{{.Email}}</td>
|
|
<td class="state state-{{.State}}">{{.State}}</td>
|
|
<td>{{if .IsAdmin}}yes{{else}}—{{end}}</td>
|
|
<td>{{or .ProjectID "—"}}</td>
|
|
<td>{{ago .LastLoginAt}}</td>
|
|
<td>
|
|
{{if eq (printf "%s" .State) "active"}}
|
|
<form method="post" action="/admin/users/{{.ID}}/suspend" class="inline">
|
|
<input type="hidden" name="csrf" value="{{$.CSRF}}">
|
|
<button type="submit" class="link danger">Suspend</button>
|
|
</form>
|
|
{{else}}
|
|
<form method="post" action="/admin/users/{{.ID}}/reinstate" class="inline">
|
|
<input type="hidden" name="csrf" value="{{$.CSRF}}">
|
|
<button type="submit" class="link">Reinstate</button>
|
|
</form>
|
|
{{end}}
|
|
</td>
|
|
</tr>{{end}}</table>
|
|
{{else}}<p class="empty">No accounts yet.</p>{{end}}
|
|
|
|
<p class="note">Three things are deliberately not here. Making someone an admin,
|
|
resetting a password and issuing keys on another person's behalf all need a
|
|
shell on the control plane — <code>llmgw users promote</code>,
|
|
<code>reset-password</code>, <code>llmgw keys</code>. A stolen admin session can
|
|
approve and suspend; it cannot hand itself more access or take over an account.</p>
|
|
|
|
<footer><span>llmgw {{.Version}}</span><span>{{.Now}}</span></footer>
|
|
</div></body></html>{{end}}
|