feat(admin): Administration page — Users tab + Server tab (hardware, per-user CPU/memory/home share)
/users becomes /admin (redirected), a tab shell over the existing Users view and a new Server tab backed by GET /api/host (server/lib/host-stats.js): hostname/OS/uptime, CPU and load, memory and swap, every real disk with HDD/SSD, NVIDIA GPUs, and each user's live share from two /proc samples — attributed by the member's Linux uid (S3) or, for host processes, by the inherited AGENTHUB_LANE_ID's workspace owner. Read-only, staff-only (/host in STAFF_ONLY_PREFIXES), comm names only. Members' tmux card moves to the Server tab.
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* @file host-stats.test.js
|
||||
* @description Tests for server/lib/host-stats.js — the Server tab's hardware
|
||||
* snapshot and per-user CPU/memory attribution (`GET /api/host`).
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
const { test } = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const { hostStats, mountOf, __setDeps } = require("../lib/host-stats");
|
||||
|
||||
const linux = process.platform === "linux";
|
||||
|
||||
test("mountOf picks the longest mount point holding the path", () => {
|
||||
const list = [{ mount: "/" }, { mount: "/srv" }, { mount: "/srv/users" }];
|
||||
assert.equal(mountOf(list, "/srv/users/alice"), "/srv/users");
|
||||
assert.equal(mountOf(list, "/srv/usersx"), "/srv");
|
||||
assert.equal(mountOf(list, "/home/x"), "/");
|
||||
assert.equal(mountOf(list, null), null);
|
||||
});
|
||||
|
||||
test("hostStats reports hardware and attributes processes by uid", { skip: !linux }, async () => {
|
||||
const me = process.getuid();
|
||||
__setDeps({
|
||||
users: () => [
|
||||
{ id: 7, username: "zed" },
|
||||
{ id: 8, username: "idle" },
|
||||
],
|
||||
memberNameForUid: (uid) => (uid === me ? "zed" : null),
|
||||
laneOwner: () => null,
|
||||
homesRoot: () => "/",
|
||||
wait: () => new Promise((r) => setTimeout(r, 20)),
|
||||
});
|
||||
const r = await hostStats();
|
||||
assert.ok(r.cpu.cores > 0);
|
||||
assert.ok(r.memory.total > 0 && r.memory.used >= 0);
|
||||
assert.ok(Array.isArray(r.disks));
|
||||
const zed = r.usage.groups.find((g) => g.key === "user:7");
|
||||
assert.ok(zed.processes > 0, "this test's own process belongs to zed");
|
||||
assert.ok(zed.memory > 0);
|
||||
assert.ok(zed.top.every((t) => typeof t.name === "string" && !("argv" in t)));
|
||||
const idle = r.usage.groups.find((g) => g.key === "user:8");
|
||||
assert.deepEqual([idle.cpu, idle.memory, idle.processes], [0, 0, 0]);
|
||||
assert.equal(
|
||||
r.usage.groups.find((g) => g.key === "host"),
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
"a host process with a workspace's AGENTHUB_LANE_ID counts for its owner",
|
||||
{ skip: !linux },
|
||||
async () => {
|
||||
const { spawn } = require("node:child_process");
|
||||
const child = spawn("sleep", ["5"], { env: { ...process.env, AGENTHUB_LANE_ID: "4242" } });
|
||||
try {
|
||||
__setDeps({
|
||||
users: () => [{ id: 3, username: "own" }],
|
||||
memberNameForUid: () => null,
|
||||
laneOwner: (id) => (id === 4242 ? 3 : null),
|
||||
wait: () => new Promise((r) => setTimeout(r, 20)),
|
||||
});
|
||||
const r = await hostStats();
|
||||
const own = r.usage.groups.find((g) => g.key === "user:3");
|
||||
assert.ok(own.top.some((t) => t.pid === child.pid));
|
||||
} finally {
|
||||
child.kill();
|
||||
}
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user