From b777e08371030abdb325a6600a886a0e73885dc1 Mon Sep 17 00:00:00 2001 From: nntrivi2001 Date: Tue, 3 Nov 2026 08:50:26 +0700 Subject: [PATCH] fix(health): the user hover card leaves out the login name It now opens only once the users list is read (admin/analyst): role, today's spend, workspaces, sessions and last sign-in. A developer gets no card. --- client/src/components/health/HostHealth.tsx | 13 ++++++----- .../health/__tests__/HostHealth.test.tsx | 22 ++++++++++--------- client/src/i18n/locales/en/dashboard.json | 1 - client/src/i18n/locales/vi/dashboard.json | 1 - docs/SERVER-MODE.md | 2 +- 5 files changed, 20 insertions(+), 19 deletions(-) diff --git a/client/src/components/health/HostHealth.tsx b/client/src/components/health/HostHealth.tsx index cc9643aa..e4eeb51f 100644 --- a/client/src/components/health/HostHealth.tsx +++ b/client/src/components/health/HostHealth.tsx @@ -12,8 +12,8 @@ * busiest processes with who each counts for (history, CPU threads and * processes are Linux-only reads, so off Linux those cards are left out); and — server mode — each user's * live CPU / RAM share (a tile per user; click for their busiest processes; hover - * for their login name and — admin/analyst, from `GET /api/users` — role, $ spent - * today, workspaces, sessions and last sign-in). Also exports `useHostStats` (a poll every `pollMs` while + * for — admin/analyst, from `GET /api/users` — their role, $ spent today, + * workspaces, sessions and last sign-in). Also exports `useHostStats` (a poll every `pollMs` while * the tab is visible; 0 = read once), `Meter`, `pct` and `fmtPct`, which the * Administration users table shares for each home's share of the homes disk. * Read-only. @@ -330,7 +330,7 @@ export function HostSections({ host, heap }: { host: HostState; heap?: HeapInfo // The user tile whose busiest processes are shown. const [open, setOpen] = useState(null); // Each account's role and spend for the tiles' hover card. `GET /api/users` - // is admin/analyst only, so a developer's card shows the login name alone. + // is admin/analyst only, so a developer gets no card. const { seesAll, local } = usePermissions(); const { t: tu } = useTranslation("users"); const [accounts, setAccounts] = useState>(new Map()); @@ -390,10 +390,11 @@ export function HostSections({ host, heap }: { host: HostState; heap?: HeapInfo return key; }; const userTip = (g: HostUsageGroup) => { - const lines = [groupName(g)]; - if (g.username) lines.push(t("host.userTipLogin", { v: g.username })); const u = g.userId != null ? accounts.get(g.userId) : undefined; - if (u) { + // No account row (a developer, or not read yet): no card at all. + if (!u) return ""; + const lines = [groupName(g)]; + { lines.push( t("host.userTipRole", { v: tu(`roles.${u.role}`) }) + (u.disabled ? ` · ${t("host.userTipDisabled")}` : "") diff --git a/client/src/components/health/__tests__/HostHealth.test.tsx b/client/src/components/health/__tests__/HostHealth.test.tsx index a7507568..172577db 100644 --- a/client/src/components/health/__tests__/HostHealth.test.tsx +++ b/client/src/components/health/__tests__/HostHealth.test.tsx @@ -8,7 +8,7 @@ * switch (`GET /api/host/history`), and the busiest processes with who each * counts for, the processes of no user in a short list of their own, and * each user's live CPU / RAM as a tile (their busiest processes on click; on - * hover their login name, and for staff their role and $ spent today); + * hover, for staff, their role and $ spent today); * off Linux the cards it can never fill are left out. * @author Nguyễn Ngọc Trí Vĩ */ @@ -186,7 +186,7 @@ describe("HostHealth", () => { expect(within(tile).getByText("claude")).toBeTruthy(); }); - it("names the login, role and today's spend when a user tile is hovered", async () => { + it("names the role and today's spend when a user tile is hovered", async () => { Object.assign(perms, { local: false }); api.users.list.mockResolvedValue({ users: [ @@ -206,25 +206,27 @@ describe("HostHealth", () => { }); render(); const tile = await screen.findByTestId("server-user"); - await vi.waitFor(() => expect(api.users.list).toHaveBeenCalled()); - fireEvent.mouseEnter(within(tile).getByRole("button").parentElement!); - const tip = await screen.findByText(/Login: mia/); - expect(tip.textContent).toContain("Role: Developer"); + // The card exists only once the users list has arrived. + const nameRow = () => within(tile).getByRole("button").parentElement!; + const tip = await vi.waitFor(() => { + fireEvent.mouseEnter(nameRow()); + return screen.getByText(/Role: Developer/); + }); + expect(tip.textContent).not.toContain("Login"); expect(tip.textContent).toMatch(/Today: \$1\.5/); expect(tip.textContent).toContain("2 workspaces · 5 sessions"); // Only the name row carries it: the CPU bar keeps its own help tip alone. fireEvent.mouseLeave(within(tile).getByRole("button").parentElement!); fireEvent.mouseEnter(within(tile).getByRole("meter", { name: "CPU" })); - expect(screen.queryByText(/Login: mia/)).toBeNull(); + expect(screen.queryByText(/Role: Developer/)).toBeNull(); }); - it("shows a developer only the login name, without reading the users list", async () => { + it("gives a developer no hover card, without reading the users list", async () => { Object.assign(perms, { seesAll: false, local: false, isAdmin: false }); render(); const tile = await screen.findByTestId("server-user"); fireEvent.mouseEnter(within(tile).getByRole("button").parentElement!); - const tip = await screen.findByText(/Login: mia/); - expect(tip.textContent).not.toContain("Today"); + expect(screen.queryByText(/Today:/)).toBeNull(); expect(api.users.list).not.toHaveBeenCalled(); }); diff --git a/client/src/i18n/locales/en/dashboard.json b/client/src/i18n/locales/en/dashboard.json index ada89554..46179cfe 100644 --- a/client/src/i18n/locales/en/dashboard.json +++ b/client/src/i18n/locales/en/dashboard.json @@ -183,7 +183,6 @@ "agenthub": "AgentHub", "usersTitle": "Users · CPU / RAM now", "usersCount": "{{n}} users", - "userTipLogin": "Login: {{v}}", "userTipRole": "Role: {{v}}", "userTipToday": "Today: {{cost}} · {{tokens}} tokens", "userTipWork": "{{w}} workspaces · {{s}} sessions", diff --git a/client/src/i18n/locales/vi/dashboard.json b/client/src/i18n/locales/vi/dashboard.json index 32b884eb..e02e5e2a 100644 --- a/client/src/i18n/locales/vi/dashboard.json +++ b/client/src/i18n/locales/vi/dashboard.json @@ -183,7 +183,6 @@ "agenthub": "AgentHub", "usersTitle": "Người dùng · CPU / RAM hiện tại", "usersCount": "{{n}} người dùng", - "userTipLogin": "Tên đăng nhập: {{v}}", "userTipRole": "Vai trò: {{v}}", "userTipToday": "Hôm nay: {{cost}} · {{tokens}} token", "userTipWork": "{{w}} workspace · {{s}} phiên", diff --git a/docs/SERVER-MODE.md b/docs/SERVER-MODE.md index bdd5e87b..db9c9a64 100644 --- a/docs/SERVER-MODE.md +++ b/docs/SERVER-MODE.md @@ -188,7 +188,7 @@ Workspaces made before server mode, or by the host account, have none. load, memory and swap, every real disk (HDD or SSD, how full; the one holding the users' homes is marked), any NVIDIA GPU. Each user's live share of it is a **Users · CPU / RAM now** card on the same tab (a tile per - user, their busiest processes on click; on hover their login name, and for an + user, their busiest processes on click; on hover, for an admin or analyst — from `GET /api/users` — their role, $ spent today, workspaces, sessions and last sign-in; their home share stays in Administration's users table), and the processes of no user are listed apart — CPU % of the whole machine (100 % = every core busy) and