fix(unraid): use metrics.memory utilization for RAM widget values

The health monitoring widget computed total memory from info.memory.layout[].size
and derived used memory from percentTotal. On some Unraid versions the layout size
is reported in KiB, causing the RAM value to be ~1024x too small (0.01 GiB instead
of ~12 GiB) while the percentage ring stayed correct.

Use metrics.memory.total and metrics.memory.available (documented as bytes) with
used = total - available, matching Unraid's own percentTotal calculation. Removes
the now-unused info.memory.layout query and schema field and adds regression tests.
This commit is contained in:
ajnart committed 2026-08-31 07:24:38 +02:00
1 parent b6dfcd69ba
commit 91e9d77f46
3 files changed
+156 -14

No files matched your search

@@ -0,0 +1,150 @@
// @vitest-environment node
import { Response } from "undici";
import { beforeEach, describe, expect, test, vi } from "vitest";
vi.hoisted(() => {
process.env.CI = "true";
process.env.NODE_ENV = "test";
process.env.SECRET_ENCRYPTION_KEY = "0".repeat(64);
});
vi.mock("@homarr/core/infrastructure/http", () => ({
fetchWithTrustedCertificatesAsync: vi.fn(),
}));
vi.mock("@homarr/core/infrastructure/logs", () => ({
createLogger: () => ({
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
}),
}));
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import type { IntegrationSecret } from "../../base/types";
import { UnraidIntegration } from "../unraid-integration";
const TEST_URL = "https://unraid.example.com";
const mockFetch = vi.mocked(fetchWithTrustedCertificatesAsync);
const createIntegration = (secrets: IntegrationSecret[] = []) =>
new UnraidIntegration({
id: "test-unraid",
name: "Test Unraid",
url: TEST_URL,
externalUrl: null,
decryptedSecrets: secrets.length ? secrets : [{ kind: "apiKey", value: "secret-api-key" }],
});
const graphqlResponse = (data: unknown) => ({
data,
});
const mockGraphQL = (data: unknown) => {
mockFetch.mockResolvedValue(
new Response(JSON.stringify(graphqlResponse(data)), {
status: 200,
headers: { "content-type": "application/json" },
}),
);
};
const GiB = 1024 * 1024 * 1024;
const systemInfoResponse = {
metrics: {
cpu: {
percentTotal: 10,
cpus: [{ percentTotal: 10 }, { percentTotal: 10 }],
},
memory: {
available: 20 * GiB,
used: 12 * GiB,
free: 10 * GiB,
total: 32 * GiB,
percentTotal: 37.5,
},
},
array: {
state: "STARTED",
capacity: {
disks: {
free: 100,
total: 200,
used: 100,
},
},
disks: [
{
name: "disk1",
size: 200 * 1024 * 1024,
fsFree: 100 * 1024 * 1024,
fsUsed: 100 * 1024 * 1024,
status: "DISK_OK",
temp: 38,
},
],
},
info: {
devices: {
network: [
{
speed: 1000,
dhcp: true,
model: "eth0",
},
],
},
os: {
platform: "linux",
distro: "Unraid",
release: "7.3.2",
uptime: new Date().toISOString(),
},
cpu: {
manufacturer: "Intel",
brand: "Intel Core i5",
cores: 4,
threads: 8,
},
},
};
describe("UnraidIntegration", () => {
beforeEach(() => {
vi.clearAllMocks();
});
test("computes used memory from metrics.memory.total - available instead of layout sizes", async () => {
mockGraphQL(systemInfoResponse);
const integration = createIntegration();
const result = await integration.getSystemInfoAsync();
expect(result.memUsedInBytes).toBe(12 * GiB);
expect(result.memAvailableInBytes).toBe(20 * GiB);
});
test("never reports negative used memory when available exceeds total", async () => {
mockGraphQL({
...systemInfoResponse,
metrics: {
...systemInfoResponse.metrics,
memory: {
...systemInfoResponse.metrics.memory,
available: 40 * GiB,
total: 32 * GiB,
},
},
});
const integration = createIntegration();
const result = await integration.getSystemInfoAsync();
expect(result.memUsedInBytes).toBe(0);
expect(result.memAvailableInBytes).toBe(32 * GiB);
});
});
@@ -47,8 +47,12 @@ export class UnraidIntegration extends Integration implements ISystemHealthMonit
cpuUtilizationNormalized = cpuUtilization / cpuCount;
}
const totalMemory = systemInfo.info.memory.layout.reduce((acc, layout) => layout.size + acc, 0);
const usedMemory = totalMemory * (systemInfo.metrics.memory.percentTotal / 100);
// metrics.memory.* is documented as bytes (see MemoryUtilization schema), unlike
// info.memory.layout[].size, which can be reported in KiB on some Unraid versions and caused a
// 1024x under-reporting of used RAM. total - available matches Unraid's percentTotal calculation.
const totalMemory = systemInfo.metrics.memory.total;
const usedMemory = Math.max(totalMemory - systemInfo.metrics.memory.available, 0);
const uptime = dayjs(systemInfo.info.os.uptime);
return {
@@ -143,11 +147,6 @@ export class UnraidIntegration extends Integration implements ISystemHealthMonit
brand,
cores,
threads
},
memory {
layout {
size
}
}
}
}
@@ -60,13 +60,6 @@ export const unraidSystemInfoSchema = z.object({
cores: z.number(),
threads: z.number(),
}),
memory: z.object({
layout: z.array(
z.object({
size: z.number(),
}),
),
}),
}),
});