Merge branch 'feat/admin-console': Administration page (Users + Server tabs)

This commit is contained in:
2026-10-15 16:44:09 +07:00
20 changed files with 1333 additions and 22 deletions
+15 -11
View File
@@ -1,6 +1,7 @@
/**
* @file Users.tsx
* @description The Users page (`/users`, a sidebar item), server mode only and
* @description The Users tab of the Administration page (`/admin`, which
* `/users` redirects to — `Administration.tsx`), server mode only and
* only for an admin or analyst (docs/SERVER-MODE.md): every account with its
* role, state, workspaces, sessions, the tokens/$ it used in the last 7 and
* 30 days (`GET /api/users`) and how much disk its home folder takes
@@ -9,9 +10,9 @@
* an admin — add a user, and ONE Edit per row (`UserEditDialog`: profile,
* role, active/disabled, password, owned workspaces, sign out everywhere, sent
* as one change), and the Claude/Codex account pool every user's sessions run
* on (`AccountPoolCard`), and (one Linux user per account) each member's own
* tmux server beside the installed version, restartable by an admin
* (`MemberTmuxCard`). Every change asks for the admin's OWN password again (the
* on (`AccountPoolCard`). (Each member's own tmux server — one Linux user per
* account — moved to the Server tab, `MemberTmuxCard` in `ServerPanel`.)
* Every change asks for the admin's OWN password again (the
* server refuses without it); an analyst only reads. A row shows the display
* name and email under the username when set; right-clicking a row offers
* Edit, Open home (in /files), and copying the username or email.
@@ -51,7 +52,6 @@ import { ConfirmModal } from "../components/ConfirmModal";
import { StatCard } from "../components/StatCard";
import { ROLE_BADGE, UserEditDialog } from "../components/users/UserEditDialog";
import { AccountPoolCard } from "../components/users/AccountPoolCard";
import { MemberTmuxCard } from "../components/users/MemberTmuxCard";
import { TableRowSkeleton } from "../components/Skeleton";
import { ContextMenu, MenuItem, MenuSeparator, useContextMenu } from "../components/ContextMenu";
import { copyText } from "../lib/clipboard";
@@ -59,7 +59,7 @@ import { copyText } from "../lib/clipboard";
type Role = AuthUser["role"];
const ROLES: Role[] = ["admin", "analyst", "developer"];
export function Users() {
export function Users({ embedded = false }: { embedded?: boolean } = {}) {
const { t } = useTranslation("users");
const auth = useAuth();
const me = auth.state.mode === "server" ? auth.state.user : null;
@@ -132,11 +132,16 @@ export function Users() {
<div className="animate-fade-in space-y-6">
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-xl bg-accent/15 flex items-center justify-center">
<UsersIcon className="w-4.5 h-4.5 text-accent" />
</div>
{!embedded && (
<div className="w-9 h-9 rounded-xl bg-accent/15 flex items-center justify-center">
<UsersIcon className="w-4.5 h-4.5 text-accent" />
</div>
)}
<div>
<h1 className="text-lg font-semibold text-fg-primary">{t("title")}</h1>
{/* Under Administration's tab bar the tab already names the page. */}
<h1 className={embedded ? "sr-only" : "text-lg font-semibold text-fg-primary"}>
{t("title")}
</h1>
<p className="text-xs text-fg-muted">{t("description")}</p>
</div>
</div>
@@ -346,7 +351,6 @@ export function Users() {
})()}
<AccountPoolCard isAdmin={isAdmin} />
<MemberTmuxCard isAdmin={isAdmin} />
<section className="card">
<div className="flex items-center gap-2 px-5 py-4 border-b border-border">