From f1a90e120efe365eb024010f582cccfdedf341d9 Mon Sep 17 00:00:00 2001 From: nntrivi2001 Date: Thu, 29 Oct 2026 13:40:28 +0700 Subject: [PATCH] fix(users): every user row shows its avatar, not only its initials System Health's per-user CPU/RAM card asked UserAvatar for the viewer's own picture with no user id, and the host stats never carried avatarAt, so every card fell back to letters. The groups now carry avatarAt and the card passes userId. The user editor's header, the pool board's member chips and the pool member picker drew hand-made initials; they use UserAvatar too. --- client/src/components/health/HostHealth.tsx | 7 ++++++- .../health/__tests__/HostHealth.test.tsx | 1 + client/src/components/users/MemberPicker.tsx | 10 +++++++++- client/src/components/users/PoolGroups.tsx | 14 ++++++++------ client/src/components/users/UserAvatar.tsx | 4 +++- client/src/components/users/UserEditDialog.tsx | 13 +++++++------ .../users/__tests__/AccountPoolCard.test.tsx | 2 +- .../users/__tests__/UserEditDialog.test.tsx | 7 ++++++- .../components/users/__tests__/UserQuota.test.tsx | 1 + client/src/lib/api.ts | 2 ++ server/lib/host-stats.js | 3 ++- 11 files changed, 46 insertions(+), 18 deletions(-) diff --git a/client/src/components/health/HostHealth.tsx b/client/src/components/health/HostHealth.tsx index eb3f8726..56c9e57a 100644 --- a/client/src/components/health/HostHealth.tsx +++ b/client/src/components/health/HostHealth.tsx @@ -683,7 +683,12 @@ export function HostSections({ host, heap }: { host: HostState; heap?: HeapInfo aria-expanded={isOpen} className="flex w-full items-center gap-2 text-left disabled:cursor-default" > - + {name} diff --git a/client/src/components/health/__tests__/HostHealth.test.tsx b/client/src/components/health/__tests__/HostHealth.test.tsx index 55f7a769..35c714ce 100644 --- a/client/src/components/health/__tests__/HostHealth.test.tsx +++ b/client/src/components/health/__tests__/HostHealth.test.tsx @@ -69,6 +69,7 @@ const stats: HostStats = { const api = vi.hoisted(() => ({ host: { stats: vi.fn(), history: vi.fn() }, me: { avatarUrl: () => null }, + users: { avatarUrl: () => null }, })); vi.mock("../../../lib/api", async (importOriginal) => { const actual = await importOriginal>(); diff --git a/client/src/components/users/MemberPicker.tsx b/client/src/components/users/MemberPicker.tsx index 8869ab78..2c6c270b 100644 --- a/client/src/components/users/MemberPicker.tsx +++ b/client/src/components/users/MemberPicker.tsx @@ -13,6 +13,7 @@ import { useTranslation } from "react-i18next"; import { Search } from "lucide-react"; import type { AuthUser, ServerUserUsage } from "../../lib/api"; import { Checkbox } from "../Checkbox"; +import { UserAvatar } from "./UserAvatar"; export const ROLES: AuthUser["role"][] = ["admin", "developer", "analyst"]; /** A role, every user, or the users in no pool group. */ @@ -168,7 +169,14 @@ export function MemberPicker({ checked={chosen.has(u.id)} onChange={(on) => setMany([u.id], on)} label={ - + + {u.username} {u.disabled && ({t("disabled")})} {note?.(u)} diff --git a/client/src/components/users/PoolGroups.tsx b/client/src/components/users/PoolGroups.tsx index 669ebcc2..ee2597d3 100644 --- a/client/src/components/users/PoolGroups.tsx +++ b/client/src/components/users/PoolGroups.tsx @@ -36,6 +36,7 @@ import { toast } from "../../lib/toast"; import { ConfirmModal } from "../ConfirmModal"; import { AiToolIcon } from "../AiToolIcon"; import { MemberPicker } from "./MemberPicker"; +import { UserAvatar } from "./UserAvatar"; /** Pool tools with their labels, in display order. */ export const POOL_TOOL_LABELS: { id: PoolTool; label: string }[] = [ @@ -363,12 +364,13 @@ export function PoolGroups({ } ${isDragged(drag) ? "opacity-40" : ""}`} data-testid="pool-member" > - - {name.charAt(0)} - + {name} {u && {t(`roles.${u.role}`)}} {isAdmin && from && ( diff --git a/client/src/components/users/UserAvatar.tsx b/client/src/components/users/UserAvatar.tsx index b1fe555b..415f0095 100644 --- a/client/src/components/users/UserAvatar.tsx +++ b/client/src/components/users/UserAvatar.tsx @@ -4,7 +4,9 @@ * (`GET /api/me/avatar`) or, with `userId`, another's (`GET * /api/users/:id/avatar`, staff only), versioned by `avatarAt`; without one — * or when the image fails to load — the first letters of its name. Used by the - * sidebar's account row, its workspace owner groups and Settings → Account. + * sidebar's account row, its workspace owner groups, Settings → Account, the + * Users page and its editor, the pool board and member picker, and System + * Health's per-user CPU/RAM card. * @author Nguyễn Ngọc Trí Vĩ */ diff --git a/client/src/components/users/UserEditDialog.tsx b/client/src/components/users/UserEditDialog.tsx index bf95e0e3..bdcd47d0 100644 --- a/client/src/components/users/UserEditDialog.tsx +++ b/client/src/components/users/UserEditDialog.tsx @@ -33,6 +33,7 @@ import { copyText } from "../../lib/clipboard"; import { isSubmitEnter } from "../../lib/keys"; import { passwordProblems } from "../../lib/passwordRules"; import { PasswordRules } from "./PasswordRules"; +import { UserAvatar } from "./UserAvatar"; import { useDialog } from "../../hooks/useDialog"; import { Switch } from "../Switch"; import { Checkbox } from "../Checkbox"; @@ -243,12 +244,12 @@ export function UserEditDialog({ className="flex max-h-[90vh] w-full max-w-2xl flex-col overflow-hidden rounded-xl border border-border bg-surface-1 shadow-xl" >
- - {shown.slice(0, 2)} - +

{t("edit.title", { name: user.displayName || user.username })} diff --git a/client/src/components/users/__tests__/AccountPoolCard.test.tsx b/client/src/components/users/__tests__/AccountPoolCard.test.tsx index f658f106..8b95cf3f 100644 --- a/client/src/components/users/__tests__/AccountPoolCard.test.tsx +++ b/client/src/components/users/__tests__/AccountPoolCard.test.tsx @@ -33,7 +33,7 @@ const accountPool = vi.hoisted(() => ({ updateGroup: vi.fn(), deleteGroup: vi.fn(), })); -const users = vi.hoisted(() => ({ list: vi.fn() })); +const users = vi.hoisted(() => ({ list: vi.fn(), avatarUrl: () => null })); const aiTools = vi.hoisted(() => ({ list: vi.fn(), ompSharing: vi.fn(), diff --git a/client/src/components/users/__tests__/UserEditDialog.test.tsx b/client/src/components/users/__tests__/UserEditDialog.test.tsx index 3cdf23b7..d82cda1f 100644 --- a/client/src/components/users/__tests__/UserEditDialog.test.tsx +++ b/client/src/components/users/__tests__/UserEditDialog.test.tsx @@ -13,7 +13,12 @@ import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import type { ServerUserUsage } from "../../../lib/api"; -const users = vi.hoisted(() => ({ get: vi.fn(), update: vi.fn(), remove: vi.fn() })); +const users = vi.hoisted(() => ({ + get: vi.fn(), + update: vi.fn(), + remove: vi.fn(), + avatarUrl: () => null, +})); const confirmDialog = vi.hoisted(() => vi.fn()); vi.mock("../../../lib/confirm", () => ({ confirmDialog })); const lanes = vi.hoisted(() => ({ list: vi.fn() })); diff --git a/client/src/components/users/__tests__/UserQuota.test.tsx b/client/src/components/users/__tests__/UserQuota.test.tsx index 5c05af43..4d6d50a1 100644 --- a/client/src/components/users/__tests__/UserQuota.test.tsx +++ b/client/src/components/users/__tests__/UserQuota.test.tsx @@ -16,6 +16,7 @@ const users = vi.hoisted(() => ({ setQuotaDefault: vi.fn(), update: vi.fn(), get: vi.fn(), + avatarUrl: () => null, })); const lanes = vi.hoisted(() => ({ list: vi.fn() })); vi.mock("../../../lib/api", async (importOriginal) => { diff --git a/client/src/lib/api.ts b/client/src/lib/api.ts index 7a0a79b1..b99771e4 100644 --- a/client/src/lib/api.ts +++ b/client/src/lib/api.ts @@ -684,6 +684,8 @@ export interface HostUsageGroup { username?: string; /** The user's display name, null when none is set (absent from an older server). */ displayName?: string | null; + /** When the user's avatar was last set (null = none; absent from an older server). */ + avatarAt?: string | null; /** % of the whole machine's CPU (100 = every core busy) over the sample. */ cpu: number; /** Resident memory, bytes (shared pages count in every process). */ diff --git a/server/lib/host-stats.js b/server/lib/host-stats.js index b2180bcf..84f13b20 100644 --- a/server/lib/host-stats.js +++ b/server/lib/host-stats.js @@ -555,12 +555,13 @@ async function usage(t0, t1, p0, p1, mem) { const round = (n) => Math.round(n * 10) / 10; -/** A user's group fields: id, username and display name (every viewer reads them). */ +/** A user's group fields: id, username, display name and avatar version (every viewer reads them). */ const userExtra = (u) => ({ kind: "user", userId: u.id, username: u.username, displayName: u.displayName ?? null, + avatarAt: u.avatarAt ?? null, }); async function collect() {