Compare commits

..
9 changed files with 459 additions and 601 deletions

No files matched your search

@@ -1,150 +0,0 @@
// @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,12 +47,8 @@ export class UnraidIntegration extends Integration implements ISystemHealthMonit
cpuUtilizationNormalized = cpuUtilization / cpuCount;
}
// 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 totalMemory = systemInfo.info.memory.layout.reduce((acc, layout) => layout.size + acc, 0);
const usedMemory = totalMemory * (systemInfo.metrics.memory.percentTotal / 100);
const uptime = dayjs(systemInfo.info.os.uptime);
return {
@@ -147,6 +143,11 @@ export class UnraidIntegration extends Integration implements ISystemHealthMonit
brand,
cores,
threads
},
memory {
layout {
size
}
}
}
}
@@ -60,6 +60,13 @@ export const unraidSystemInfoSchema = z.object({
cores: z.number(),
threads: z.number(),
}),
memory: z.object({
layout: z.array(
z.object({
size: z.number(),
}),
),
}),
}),
});
+10 -10
View File
@@ -3163,8 +3163,8 @@
"noColumns": "Spalten in Elementen auswählen",
"noCommunications": "Kann keine Daten von der Integration laden",
"noItems": "",
"actionFailed": "",
"actionFailedMessage": ""
"actionFailed": "Aktion fehlgeschlagen",
"actionFailedMessage": "Diese Aktion konnte nicht abgeschlossen werden. Bitte versuche es erneut."
},
"items": {
"actions": {
@@ -3452,7 +3452,7 @@
"queued": ""
},
"type": {
"movie": "",
"movie": "Film",
"episode": ""
},
"empty": {
@@ -3812,7 +3812,7 @@
},
"resource": {
"routers": "",
"services": "",
"services": "Dienste",
"middlewares": ""
},
"status": {
@@ -4098,7 +4098,7 @@
"temp": "",
"battery": "",
"services": "Dienste",
"uptime": "",
"uptime": "Verfügbarkeit",
"agent": "Agent"
}
},
@@ -4173,12 +4173,12 @@
"chart": {
"cpu": {
"title": "CPU Auslastung",
"subtitle": "",
"subtitle": "Durchschnittliche Systemweite CPU-Auslastung",
"series": "CPU"
},
"memory": {
"title": "",
"subtitle": "",
"title": "Speicherverbrauch",
"subtitle": "Genaue Auslastung zum erfassten Zeitpunkt",
"series": "Belegt",
"cache": ""
},
@@ -4189,7 +4189,7 @@
},
"diskIO": {
"title": "Festplatten-E/A",
"subtitle": "",
"subtitle": "Durchsatz des Root-Dateisystems",
"read": "Lesen",
"write": "Schreiben"
},
@@ -6096,7 +6096,7 @@
}
},
"availability": {
"available": "",
"available": "Verfügbar",
"partiallyAvailable": "",
"processing": "",
"requested": "",
+21 -21
View File
@@ -3179,7 +3179,7 @@
"detailsTitle": ""
},
"downSpeed": {
"columnTitle": "",
"columnTitle": "Down",
"detailsTitle": ""
},
"index": {
@@ -3194,10 +3194,10 @@
"columnTitle": ""
},
"name": {
"columnTitle": ""
"columnTitle": "Job name"
},
"progress": {
"columnTitle": "",
"columnTitle": "Progress",
"detailsTitle": ""
},
"ratio": {
@@ -3217,7 +3217,7 @@
"detailsTitle": ""
},
"state": {
"columnTitle": "",
"columnTitle": "State",
"detailsTitle": ""
},
"time": {
@@ -3315,7 +3315,7 @@
}
},
"mediaRequests-requestList": {
"name": "",
"name": "Media Requests List",
"description": "",
"option": {
"linksTargetNewTab": {
@@ -3337,15 +3337,15 @@
"unknown": "",
"pending": "",
"processing": "",
"requested": "",
"requested": "Requested",
"partiallyAvailable": "",
"available": "",
"blacklisted": "",
"blacklisted": "Blacklisted",
"deleted": ""
},
"status": {
"pending": "",
"approved": "",
"approved": "Approved",
"declined": "",
"failed": "",
"completed": ""
@@ -3358,7 +3358,7 @@
"option": {},
"titles": {
"stats": {
"main": "",
"main": "Media Stats",
"approved": "",
"pending": "",
"processing": "",
@@ -3402,15 +3402,15 @@
"panel": {
"statistics": {
"empty": "",
"transcodes": "",
"transcodes": "Transcodes",
"transcodesCount": "",
"healthChecksCount": "",
"filesCount": "",
"savedSpace": "",
"healthChecks": "",
"videoCodecs": "",
"videoContainers": "",
"videoResolutions": ""
"healthChecks": "Health checks",
"videoCodecs": "Codecs",
"videoContainers": "Containers",
"videoResolutions": "Resolutions"
},
"workers": {
"empty": "",
@@ -3620,7 +3620,7 @@
"option": {},
"card": {
"vpn": {
"countConnected": ""
"countConnected": "{count} connected"
}
},
"error": {
@@ -3637,14 +3637,14 @@
"name": ""
},
"wifi": {
"name": ""
"name": "Wi-Fi"
}
},
"users": {
"label": ""
},
"guests": {
"label": ""
"label": "Guests"
}
},
"option": {
@@ -3890,7 +3890,7 @@
"option": {
"cpu": "",
"memory": "",
"gpu": "",
"gpu": "GPU",
"network": ""
}
},
@@ -3908,7 +3908,7 @@
"cpu": "",
"memory": "",
"gpu": "",
"network": "",
"network": "NET",
"up": "",
"down": ""
}
@@ -4956,7 +4956,7 @@
"title": "",
"status": {
"idle": "",
"running": "",
"running": "Running",
"error": "",
"disabled": ""
},
@@ -5858,7 +5858,7 @@
}
},
"search": {
"placeholder": "",
"placeholder": "Search for anything",
"nothingFound": "",
"error": {
"fetch": ""
+7 -7
View File
@@ -604,7 +604,7 @@
}
},
"app": {
"search": "Trouver une application",
"search": "Trouver une app",
"page": {
"list": {
"title": "Applications",
@@ -3358,7 +3358,7 @@
"option": {},
"titles": {
"stats": {
"main": "Statistiques des médias",
"main": "Stats des médias",
"approved": "Déjà approuvé",
"pending": "En attente de validation",
"processing": "En cours de traitement",
@@ -4118,7 +4118,7 @@
"internalServerError": ""
},
"history": "Historique",
"resolved": "Résolut",
"resolved": "Résolu",
"status": {
"triggered": "",
"ok": ""
@@ -4173,12 +4173,12 @@
"chart": {
"cpu": {
"title": "",
"subtitle": "",
"subtitle": "Utilisation moyenne du CPU à l'échelle du système",
"series": "CPU"
},
"memory": {
"title": "Utilisation Mémoire",
"subtitle": "",
"subtitle": "Utilisation précise au moment enregistré",
"series": "",
"cache": ""
},
@@ -4189,7 +4189,7 @@
},
"diskIO": {
"title": "",
"subtitle": "",
"subtitle": "Débit du système de fichiers root",
"read": "",
"write": ""
},
@@ -5766,7 +5766,7 @@
"label": "Médias"
},
"apps": {
"label": "Applications",
"label": "Apps",
"new": {
"label": "Nouveau"
},
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -3452,7 +3452,7 @@
"queued": ""
},
"type": {
"movie": "",
"movie": "Film",
"episode": ""
},
"empty": {
+2 -2
View File
@@ -4887,11 +4887,11 @@
"text": "Не індексувати сайт у пошукових системах і не показувати його в жодному результаті пошуку"
},
"noFollow": {
"title": "Не переходити (No follow)",
"title": "Не переходити",
"text": "Не переходьте за посиланнями під час індексації. Якщо вимкнути це, то сканери намагатимуться переходити за всіма посиланнями на Homarr."
},
"noTranslate": {
"title": "Не перекладати (No translate)",
"title": "Не перекладати",
"text": "Якщо мова сайту не є тією, яку користувач, імовірно, захоче прочитати, Google покаже посилання на переклад у результатах пошуку"
},
"noSiteLinksSearchBox": {