diff --git a/packages/widgets/src/filter-storage-volumes.spec.ts b/packages/widgets/src/filter-storage-volumes.spec.ts index 51d84169a..bcdfe09e4 100644 --- a/packages/widgets/src/filter-storage-volumes.spec.ts +++ b/packages/widgets/src/filter-storage-volumes.spec.ts @@ -1,10 +1,6 @@ import { describe, expect, test } from "vitest"; -import { - filterStorageVolumes, - normalizeStorageDeviceName, - toScopedStorageVolumeValue, -} from "./filter-storage-volumes"; +import { filterStorageVolumes, normalizeStorageDeviceName, toScopedStorageVolumeValue } from "./filter-storage-volumes"; import { matchFileSystemAndSmart } from "./health-monitoring/system-health"; describe("normalizeStorageDeviceName", () => { @@ -13,6 +9,12 @@ describe("normalizeStorageDeviceName", () => { expect(normalizeStorageDeviceName("/dev/sda")).toBe("/dev/sda"); }); + test("strips partition suffixes from unprefixed device names (OpenMediaVault)", () => { + expect(normalizeStorageDeviceName("sda1")).toBe("sda"); + expect(normalizeStorageDeviceName("sda")).toBe("sda"); + expect(normalizeStorageDeviceName("nvme0n1p1")).toBe("nvme0n1"); + }); + test("preserves Synology-style volume names", () => { expect(normalizeStorageDeviceName("volume_1")).toBe("volume_1"); expect(normalizeStorageDeviceName("volume_2")).toBe("volume_2"); @@ -143,4 +145,36 @@ describe("matchFileSystemAndSmart", () => { overallStatus: "", }); }); + + test("joins unprefixed OMV partitions with their base device SMART data", () => { + const result = matchFileSystemAndSmart( + [ + { deviceName: "sda1", used: "3.13 GiB", available: "26853056512", percentage: 12 }, + { deviceName: "sdb1", used: "396.24 GiB", available: "1542365618176", percentage: 22 }, + ], + [ + { deviceName: "sda", temperature: 0, overallStatus: "BAD_STATUS" }, + { deviceName: "sdb", temperature: 33, overallStatus: "GOOD" }, + ], + ); + + expect(result).toEqual([ + { + deviceName: "sda", + used: "3.13 GiB", + available: "26853056512", + percentage: 12, + temperature: 0, + overallStatus: "BAD_STATUS", + }, + { + deviceName: "sdb", + used: "396.24 GiB", + available: "1542365618176", + percentage: 22, + temperature: 33, + overallStatus: "GOOD", + }, + ]); + }); }); diff --git a/packages/widgets/src/filter-storage-volumes.ts b/packages/widgets/src/filter-storage-volumes.ts index 5e88584be..b1788b27f 100644 --- a/packages/widgets/src/filter-storage-volumes.ts +++ b/packages/widgets/src/filter-storage-volumes.ts @@ -5,10 +5,16 @@ interface StorageVolumeEntry { const partitionSuffixPatterns: ReadonlyArray<{ pattern: RegExp; baseGroupIndex: number }> = [ // SCSI/SATA/VirtIO: /dev/sda1 -> /dev/sda { pattern: /^(\/dev\/(?:sd|vd|hd|xvd)[a-z]+)[0-9]+$/, baseGroupIndex: 1 }, + // SCSI/SATA/VirtIO without device node prefix: sda1 -> sda (OMV reports unprefixed names) + { pattern: /^((?:sd|vd)[a-z]+)[0-9]+$/, baseGroupIndex: 1 }, // NVMe: /dev/nvme0n1p2 -> /dev/nvme0n1 { pattern: /^(\/dev\/nvme[0-9]+n[0-9]+)p[0-9]+$/, baseGroupIndex: 1 }, + // NVMe without device node prefix: nvme0n1p2 -> nvme0n1 + { pattern: /^(nvme[0-9]+n[0-9]+)p[0-9]+$/, baseGroupIndex: 1 }, // eMMC: /dev/mmcblk0p1 -> /dev/mmcblk0 { pattern: /^(\/dev\/mmcblk[0-9]+)p[0-9]+$/, baseGroupIndex: 1 }, + // eMMC without device node prefix: mmcblk0p1 -> mmcblk0 + { pattern: /^(mmcblk[0-9]+)p[0-9]+$/, baseGroupIndex: 1 }, ]; export const normalizeStorageDeviceName = (deviceName: string): string => { @@ -28,18 +34,14 @@ export const toScopedStorageVolumeValue = (integrationId: string, value: string) return `${integrationId}:${volumeName}`; }; -const storageDeviceNamesMatch = (leftDeviceName: string, rightDeviceName: string): boolean => { +export const storageDeviceNamesMatch = (leftDeviceName: string, rightDeviceName: string): boolean => { return ( leftDeviceName === rightDeviceName || normalizeStorageDeviceName(leftDeviceName) === normalizeStorageDeviceName(rightDeviceName) ); }; -const matchesVisibleStorageVolume = ( - visibleVolume: string, - integrationId: string, - deviceName: string, -): boolean => { +const matchesVisibleStorageVolume = (visibleVolume: string, integrationId: string, deviceName: string): boolean => { const separatorIndex = visibleVolume.indexOf(":"); if (separatorIndex === -1) { return storageDeviceNamesMatch(visibleVolume, deviceName); diff --git a/packages/widgets/src/health-monitoring/system-health.tsx b/packages/widgets/src/health-monitoring/system-health.tsx index 3614a570c..e83fef9eb 100644 --- a/packages/widgets/src/health-monitoring/system-health.tsx +++ b/packages/widgets/src/health-monitoring/system-health.tsx @@ -36,7 +36,7 @@ import { formatBytes } from "@homarr/common"; import type { TranslationFunction } from "@homarr/translation"; import { useI18n } from "@homarr/translation/client"; -import { filterStorageVolumes, normalizeStorageDeviceName } from "../filter-storage-volumes"; +import { filterStorageVolumes, storageDeviceNamesMatch } from "../filter-storage-volumes"; import { WidgetEmptyState } from "../common/empty-state"; import type { WidgetComponentProps } from "../definition"; import { CpuRing } from "./rings/cpu-ring"; @@ -296,12 +296,7 @@ interface SmartData { export const matchFileSystemAndSmart = (fileSystems: FileSystem[], smartData: SmartData[]) => { return fileSystems .map((fileSystem) => { - const normalizedFileSystemName = normalizeStorageDeviceName(fileSystem.deviceName); - const smartDisk = smartData.find( - (smart) => - smart.deviceName === fileSystem.deviceName || - normalizeStorageDeviceName(smart.deviceName) === normalizedFileSystemName, - ); + const smartDisk = smartData.find((smart) => storageDeviceNamesMatch(smart.deviceName, fileSystem.deviceName)); return { deviceName: smartDisk?.deviceName ?? fileSystem.deviceName, diff --git a/packages/widgets/src/system-disks/component.tsx b/packages/widgets/src/system-disks/component.tsx index 5f84dd0b4..5cd45cf0f 100644 --- a/packages/widgets/src/system-disks/component.tsx +++ b/packages/widgets/src/system-disks/component.tsx @@ -10,7 +10,7 @@ import { useI18n } from "@homarr/translation/client"; import { WidgetEmptyState } from "../common/empty-state"; import type { WidgetComponentProps } from "../definition"; -import { filterStorageVolumes } from "../filter-storage-volumes"; +import { filterStorageVolumes, storageDeviceNamesMatch } from "../filter-storage-volumes"; import { NoIntegrationDataError } from "../errors/no-data-integration"; type DisplayMode = WidgetComponentProps<"systemDisks">["options"]["displayMode"]; @@ -140,7 +140,7 @@ export default function SystemResources({ integrationIds, options }: WidgetCompo return ( {fileSystem.map((item) => { - const smartItem = smart.find((smart) => smart.deviceName === item.deviceName); + const smartItem = smart.find((smart) => storageDeviceNamesMatch(smart.deviceName, item.deviceName)); return (