1c6dfea5b5
Dashboard → System Health is grouped into AgentHub, Machine and Agent
activity. The machine (GET /api/host, every signed-in user) is drawn by
components/health/HostHealth.tsx: host card beside the CPU/memory history,
then CPU (threads as compact cells + temperatures/GPUs), disks & network,
busiest processes, and per-user tiles. Concurrency bars each show their own
agent type on hover.
The Health Score (client/src/lib/healthScore.ts) is now 100 minus what each
check costs, between an amber and a red threshold: event-loop p99
(server/lib/loop-delay.js, shared with /api/metrics), Linux PSI pressure for
CPU, memory and disk I/O (/api/host `pressure`; load per core without PSI),
RAM, the database's disk, V8 heap, hooks silent while agents work, and agent
errors in the last 24 h. All-time success rate, API errors and the transcript
cache hit rate no longer count. /api/settings/info adds server.event_loop and
activity {last_event_at, working_agents, agents_24h}.
Administration: a user row opens its detail in place (GET
/api/users/:id/details — spend, account pool share, live panes, workspaces,
newest sessions); the server panel is gone, member tmux folded into the table;
the Sessions list shows each row's owner in server mode.
183 lines
6.0 KiB
TypeScript
183 lines
6.0 KiB
TypeScript
/**
|
|
* @file HostHealth.test.tsx
|
|
* @description The machine on the Dashboard's System Health tab
|
|
* (`HostHealth.tsx`, `GET /api/host`), its whole machine group: the first card names the host, its OS,
|
|
* load and the AgentHub process with bars for CPU, memory, swap and heap; the
|
|
* rows under it show every CPU thread, temperatures, each disk with its I/O
|
|
* rate and each network interface, the CPU / memory history with its range
|
|
* 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).
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { fireEvent, render, screen, within } from "@testing-library/react";
|
|
import type { HostStats } from "../../../lib/api";
|
|
|
|
const GB = 1024 ** 3;
|
|
const stats: HostStats = {
|
|
at: "2026-10-15T00:00:00Z",
|
|
hostname: "box",
|
|
os: { platform: "linux", release: "7.0.0", distro: "Ubuntu 26.04", arch: "x64" },
|
|
uptime: 90000,
|
|
cpu: { model: "i5-7500", cores: 4, load: [1, 0.5, 0.25] },
|
|
memory: { total: 16 * GB, available: 8 * GB, used: 8 * GB, swapTotal: 4 * GB, swapUsed: 0 },
|
|
disks: [
|
|
{
|
|
device: "/dev/sdb2",
|
|
mount: "/",
|
|
type: "ext4",
|
|
kind: "SSD",
|
|
total: 200 * GB,
|
|
used: 50 * GB,
|
|
free: 150 * GB,
|
|
},
|
|
{
|
|
device: "/dev/sda1",
|
|
mount: "/data",
|
|
type: "ext4",
|
|
kind: "HDD",
|
|
total: 500 * GB,
|
|
used: 0,
|
|
free: 500 * GB,
|
|
},
|
|
],
|
|
homesMount: "/",
|
|
gpus: [],
|
|
usage: {
|
|
cpu: 40,
|
|
sampleMs: 1000,
|
|
groups: [
|
|
{
|
|
key: "user:2",
|
|
kind: "user",
|
|
userId: 2,
|
|
username: "mia",
|
|
cpu: 25,
|
|
memory: 2 * GB,
|
|
memoryPercent: 12.5,
|
|
processes: 3,
|
|
top: [{ pid: 77, name: "claude", cpu: 20, memory: GB }],
|
|
},
|
|
{ key: "host", kind: "host", cpu: 5, memory: GB, memoryPercent: 6.3, processes: 90, top: [] },
|
|
],
|
|
},
|
|
};
|
|
|
|
const api = vi.hoisted(() => ({
|
|
host: { stats: vi.fn(), history: vi.fn() },
|
|
me: { avatarUrl: () => null },
|
|
}));
|
|
vi.mock("../../../lib/api", async (importOriginal) => {
|
|
const actual = await importOriginal<Record<string, unknown>>();
|
|
return { ...actual, api };
|
|
});
|
|
|
|
import { HostSections, useHostStats } from "../HostHealth";
|
|
|
|
function Tab() {
|
|
const host = useHostStats();
|
|
return (
|
|
<HostSections
|
|
host={host}
|
|
heap={{ heapUsed: GB / 4, heapTotal: GB / 2, heapLimit: GB, external: 0 }}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const full: HostStats = {
|
|
...stats,
|
|
usage: {
|
|
...stats.usage!,
|
|
groups: stats.usage!.groups.map((g) => (g.kind === "user" ? { ...g, displayName: "Mia" } : g)),
|
|
},
|
|
bootTime: "2026-10-14T00:00:00Z",
|
|
cpu: { ...stats.cpu, physicalCores: 4, perCore: [10, 20, 30, 40] },
|
|
network: [{ name: "enp4s0", rx: 2048, tx: 1024 }],
|
|
diskIo: [{ name: "sda", read: 4096, write: 0 }],
|
|
temperatures: [{ chip: "coretemp", label: "Package id 0", celsius: 55 }],
|
|
topProcesses: [{ pid: 77, name: "claude", cpu: 20, memory: GB, group: "user:2" }],
|
|
process: { pid: 9, node: "v24.0.0", uptime: 3600, rss: GB, heapUsed: GB / 2 },
|
|
};
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
api.host.stats.mockResolvedValue(full);
|
|
api.host.history.mockResolvedValue({
|
|
range: "1h",
|
|
intervalMs: 30000,
|
|
running: true,
|
|
startedAt: "2026-10-15T00:00:00Z",
|
|
points: [
|
|
{
|
|
at: "2026-10-15T00:00:00Z",
|
|
cpu: 30,
|
|
memory: 50,
|
|
memoryUsed: 0,
|
|
swapUsed: 0,
|
|
load1: 1,
|
|
users: {},
|
|
},
|
|
{
|
|
at: "2026-10-15T00:00:30Z",
|
|
cpu: 40,
|
|
memory: 52,
|
|
memoryUsed: 0,
|
|
swapUsed: 0,
|
|
load1: 1,
|
|
users: {},
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
describe("HostHealth", () => {
|
|
it("names the machine and the AgentHub process, with bars", async () => {
|
|
render(<Tab />);
|
|
const card = await screen.findByTestId("server-facts");
|
|
expect(await within(card).findByText("box")).toBeTruthy();
|
|
expect(card.textContent).toContain("Ubuntu 26.04");
|
|
expect(card.textContent).toContain("1.0 GB");
|
|
expect(within(card).getByRole("meter", { name: "CPU" }).getAttribute("aria-valuenow")).toBe(
|
|
"40"
|
|
);
|
|
expect(within(card).getByRole("meter", { name: "Memory" }).getAttribute("aria-valuenow")).toBe(
|
|
"50"
|
|
);
|
|
expect(within(card).getByRole("meter", { name: "V8 Heap" })).toBeTruthy();
|
|
});
|
|
|
|
it("shows every thread, sensors, disks with I/O, network, history and processes", async () => {
|
|
render(<Tab />);
|
|
expect(await screen.findByTestId("server-history")).toBeTruthy();
|
|
expect(await screen.findByRole("img", { name: /CPU/ })).toBeTruthy();
|
|
expect(screen.getByTestId("server-cores").textContent).toContain("40%");
|
|
expect(screen.getByTestId("server-temperatures").textContent).toContain("55 °C");
|
|
const disks = screen.getByTestId("server-disk-io").textContent;
|
|
expect(disks).toContain("/data · HDD");
|
|
expect(disks).toContain("4.0 KB/s");
|
|
expect(screen.getByTestId("server-network").textContent).toContain("enp4s0");
|
|
const top = screen.getByTestId("server-top-processes");
|
|
expect(await within(top).findByText("Mia")).toBeTruthy();
|
|
const other = screen.getByTestId("server-usage-table");
|
|
expect(within(other).queryByText("Mia")).toBeNull();
|
|
expect(within(other).getByText(/^Host account/)).toBeTruthy();
|
|
fireEvent.click(screen.getByRole("button", { name: "24 h" }));
|
|
expect(api.host.history).toHaveBeenLastCalledWith("24h");
|
|
});
|
|
|
|
it("shows each user's live CPU and RAM, their processes on click", async () => {
|
|
render(<Tab />);
|
|
const tile = await screen.findByTestId("server-user");
|
|
expect(tile.textContent).toContain("Mia");
|
|
expect(within(tile).getByRole("meter", { name: "CPU" }).getAttribute("aria-valuenow")).toBe(
|
|
"25"
|
|
);
|
|
expect(tile.textContent).toContain("2.0 GB");
|
|
expect(within(tile).queryByText("claude")).toBeNull();
|
|
fireEvent.click(within(tile).getByRole("button", { expanded: false }));
|
|
expect(within(tile).getByText("claude")).toBeTruthy();
|
|
});
|
|
});
|