From b8cf1b61279e9f5da3f32acf6335c798679499d7 Mon Sep 17 00:00:00 2001 From: Jake Turner Date: Fri, 20 Mar 2026 18:24:57 +0000 Subject: [PATCH] fix(disk): correct storage display by fixing device matching and dedup mount entries --- admin/app/services/system_service.ts | 13 ++- admin/app/utils/fs.ts | 7 +- admin/docs/release-notes.md | 1 + admin/inertia/hooks/useDiskDisplayData.ts | 82 +++++++++++++++++++ admin/inertia/pages/easy-setup/index.tsx | 42 +--------- admin/inertia/pages/settings/system.tsx | 38 +-------- .../collect-disk-info.sh | 4 + 7 files changed, 106 insertions(+), 81 deletions(-) create mode 100644 admin/inertia/hooks/useDiskDisplayData.ts diff --git a/admin/app/services/system_service.ts b/admin/app/services/system_service.ts index ab450b6..84157af 100644 --- a/admin/app/services/system_service.ts +++ b/admin/app/services/system_service.ts @@ -579,10 +579,21 @@ export class SystemService { return [] } + // Deduplicate: same device path mounted in multiple places (Docker bind-mounts) + // Keep the entry with the largest size — that's the real partition + const deduped = new Map() + for (const entry of fsSize) { + const existing = deduped.get(entry.fs) + if (!existing || entry.size > existing.size) { + deduped.set(entry.fs, entry) + } + } + const dedupedFsSize = Array.from(deduped.values()) + return diskLayout.blockdevices .filter((disk) => disk.type === 'disk') // Only physical disks .map((disk) => { - const filesystems = getAllFilesystems(disk, fsSize) + const filesystems = getAllFilesystems(disk, dedupedFsSize) // Across all partitions const totalUsed = filesystems.reduce((sum, p) => sum + (p.used || 0), 0) diff --git a/admin/app/utils/fs.ts b/admin/app/utils/fs.ts index 7cc3ba8..59bd5c5 100644 --- a/admin/app/utils/fs.ts +++ b/admin/app/utils/fs.ts @@ -138,14 +138,13 @@ export function matchesDevice(fsPath: string, deviceName: string): boolean { // Remove /dev/ and /dev/mapper/ prefixes const normalized = fsPath.replace('/dev/mapper/', '').replace('/dev/', '') - // Direct match + // Direct match (covers /dev/sda1 ↔ sda1, /dev/nvme0n1p1 ↔ nvme0n1p1) if (normalized === deviceName) { return true } - // LVM volumes use dashes instead of slashes - // e.g., ubuntu--vg-ubuntu--lv matches the device name - if (fsPath.includes(deviceName)) { + // LVM/device-mapper: e.g., /dev/mapper/ubuntu--vg-ubuntu--lv contains "ubuntu--lv" + if (fsPath.startsWith('/dev/mapper/') && fsPath.includes(deviceName)) { return true } diff --git a/admin/docs/release-notes.md b/admin/docs/release-notes.md index 7547595..6ea2f34 100644 --- a/admin/docs/release-notes.md +++ b/admin/docs/release-notes.md @@ -10,6 +10,7 @@ ### Bug Fixes - **Settings**: Storage usage display now prefers real block devices over tempfs. Thanks @Bortlesboat for the fix! +- **Settings**: Fixed an issue where device matching and mount entry deduplication logic could cause incorrect storage usage reporting and missing devices in storage displays. - **Maps**: The Maps page now respects the request protocol (http vs https) to ensure map tiles load correctly. Thanks @davidgross for the bug report! - **Knowledge Base**: Fixed an issue where file embedding jobs could cause a retry storm if the Ollama service was unavailable. Thanks @skyam25 for the bug report! - **Curated Collections**: Fixed some broken links in the curated collections definitions (maps and ZIM files) that were causing some resources to fail to download. diff --git a/admin/inertia/hooks/useDiskDisplayData.ts b/admin/inertia/hooks/useDiskDisplayData.ts new file mode 100644 index 0000000..1d8b15d --- /dev/null +++ b/admin/inertia/hooks/useDiskDisplayData.ts @@ -0,0 +1,82 @@ +import { NomadDiskInfo } from '../../types/system' +import { Systeminformation } from 'systeminformation' +import { formatBytes } from '~/lib/util' + +type DiskDisplayItem = { + label: string + value: number + total: string + used: string + subtext: string + totalBytes: number + usedBytes: number +} + +/** Get all valid disks formatted for display (settings/system page) */ +export function getAllDiskDisplayItems( + disks: NomadDiskInfo[] | undefined, + fsSize: Systeminformation.FsSizeData[] | undefined +): DiskDisplayItem[] { + const validDisks = disks?.filter((d) => d.totalSize > 0) || [] + + if (validDisks.length > 0) { + return validDisks.map((disk) => ({ + label: disk.name || 'Unknown', + value: disk.percentUsed || 0, + total: formatBytes(disk.totalSize), + used: formatBytes(disk.totalUsed), + subtext: `${formatBytes(disk.totalUsed || 0)} / ${formatBytes(disk.totalSize || 0)}`, + totalBytes: disk.totalSize, + usedBytes: disk.totalUsed, + })) + } + + if (fsSize && fsSize.length > 0) { + const seen = new Set() + const uniqueFs = fsSize.filter((fs) => { + if (fs.size <= 0 || seen.has(fs.size)) return false + seen.add(fs.size) + return true + }) + const realDevices = uniqueFs.filter((fs) => fs.fs.startsWith('/dev/')) + const displayFs = realDevices.length > 0 ? realDevices : uniqueFs + return displayFs.map((fs) => ({ + label: fs.fs || 'Unknown', + value: fs.use || 0, + total: formatBytes(fs.size), + used: formatBytes(fs.used), + subtext: `${formatBytes(fs.used)} / ${formatBytes(fs.size)}`, + totalBytes: fs.size, + usedBytes: fs.used, + })) + } + + return [] +} + +/** Get primary disk info for storage projection (easy-setup page) */ +export function getPrimaryDiskInfo( + disks: NomadDiskInfo[] | undefined, + fsSize: Systeminformation.FsSizeData[] | undefined +): { totalSize: number; totalUsed: number } | null { + const validDisks = disks?.filter((d) => d.totalSize > 0) || [] + if (validDisks.length > 0) { + const diskWithRoot = validDisks.find((d) => + d.filesystems?.some((fs) => fs.mount === '/' || fs.mount === '/storage') + ) + const primary = + diskWithRoot || validDisks.reduce((a, b) => (b.totalSize > a.totalSize ? b : a)) + return { totalSize: primary.totalSize, totalUsed: primary.totalUsed } + } + + if (fsSize && fsSize.length > 0) { + const realDevices = fsSize.filter((fs) => fs.fs.startsWith('/dev/')) + const primary = + realDevices.length > 0 + ? realDevices.reduce((a, b) => (b.size > a.size ? b : a)) + : fsSize[0] + return { totalSize: primary.size, totalUsed: primary.used } + } + + return null +} diff --git a/admin/inertia/pages/easy-setup/index.tsx b/admin/inertia/pages/easy-setup/index.tsx index 9c7ca9f..0229322 100644 --- a/admin/inertia/pages/easy-setup/index.tsx +++ b/admin/inertia/pages/easy-setup/index.tsx @@ -16,6 +16,7 @@ import StorageProjectionBar from '~/components/StorageProjectionBar' import { useNotifications } from '~/context/NotificationContext' import useInternetStatus from '~/hooks/useInternetStatus' import { useSystemInfo } from '~/hooks/useSystemInfo' +import { getPrimaryDiskInfo } from '~/hooks/useDiskDisplayData' import classNames from 'classnames' import type { CategoryWithStatus, SpecTier, SpecResource } from '../../../types/collections' import { resolveTierResources } from '~/lib/collections' @@ -296,46 +297,7 @@ export default function EasySetupWizard(props: { system: { services: ServiceSlim ]) // Get primary disk/filesystem info for storage projection - // Try disk array first (Linux/production), fall back to fsSize (Windows/dev) - // Filter out invalid disks (totalSize === 0) and prefer disk with root mount or largest valid disk - const getPrimaryDisk = () => { - if (!systemInfo?.disk || systemInfo.disk.length === 0) return null - - // Filter to only valid disks with actual storage - const validDisks = systemInfo.disk.filter((d) => d.totalSize > 0) - if (validDisks.length === 0) return null - - // Prefer disk containing root mount (/) or /storage mount - const diskWithRoot = validDisks.find((d) => - d.filesystems?.some((fs) => fs.mount === '/' || fs.mount === '/storage') - ) - if (diskWithRoot) return diskWithRoot - - // Fall back to largest valid disk - return validDisks.reduce((largest, current) => - current.totalSize > largest.totalSize ? current : largest - ) - } - - const primaryDisk = getPrimaryDisk() - // When falling back to fsSize (systeminformation), prefer real block devices - // over virtual filesystems like tmpfs which report misleading capacity. - const getPrimaryFs = () => { - if (!systemInfo?.fsSize || systemInfo.fsSize.length === 0) return null - const realDevices = systemInfo.fsSize.filter((fs) => fs.fs.startsWith('/dev/')) - if (realDevices.length > 0) { - return realDevices.reduce((largest, current) => - current.size > largest.size ? current : largest - ) - } - return systemInfo.fsSize[0] - } - const primaryFs = getPrimaryFs() - const storageInfo = primaryDisk - ? { totalSize: primaryDisk.totalSize, totalUsed: primaryDisk.totalUsed } - : primaryFs - ? { totalSize: primaryFs.size, totalUsed: primaryFs.used } - : null + const storageInfo = getPrimaryDiskInfo(systemInfo?.disk, systemInfo?.fsSize) const canProceedToNextStep = () => { if (!isOnline) return false // Must be online to proceed diff --git a/admin/inertia/pages/settings/system.tsx b/admin/inertia/pages/settings/system.tsx index 9f7f67b..7b40088 100644 --- a/admin/inertia/pages/settings/system.tsx +++ b/admin/inertia/pages/settings/system.tsx @@ -3,6 +3,7 @@ import { Head } from '@inertiajs/react' import SettingsLayout from '~/layouts/SettingsLayout' import { SystemInformationResponse } from '../../../types/system' import { formatBytes } from '~/lib/util' +import { getAllDiskDisplayItems } from '~/hooks/useDiskDisplayData' import CircularGauge from '~/components/systeminfo/CircularGauge' import HorizontalBarChart from '~/components/HorizontalBarChart' import InfoCard from '~/components/systeminfo/InfoCard' @@ -105,42 +106,7 @@ export default function SettingsPage(props: { : `${uptimeMinutes}m` // Build storage display items - fall back to fsSize when disk array is empty - // (Same approach as Easy Setup wizard fix from PR #90) - const validDisks = info?.disk?.filter((d) => d.totalSize > 0) || [] - let storageItems: { - label: string - value: number - total: string - used: string - subtext: string - }[] = [] - if (validDisks.length > 0) { - storageItems = validDisks.map((disk) => ({ - label: disk.name || 'Unknown', - value: disk.percentUsed || 0, - total: disk.totalSize ? formatBytes(disk.totalSize) : 'N/A', - used: disk.totalUsed ? formatBytes(disk.totalUsed) : 'N/A', - subtext: `${formatBytes(disk.totalUsed || 0)} / ${formatBytes(disk.totalSize || 0)}`, - })) - } else if (info?.fsSize && info.fsSize.length > 0) { - // Deduplicate by size (same physical disk mounted in multiple places shows identical sizes) - const seen = new Set() - const uniqueFs = info.fsSize.filter((fs) => { - if (fs.size <= 0 || seen.has(fs.size)) return false - seen.add(fs.size) - return true - }) - // Prefer real block devices (/dev/), exclude virtual filesystems (efivarfs, tmpfs, etc.) - const realDevices = uniqueFs.filter((fs) => fs.fs.startsWith('/dev/')) - const displayFs = realDevices.length > 0 ? realDevices : uniqueFs - storageItems = displayFs.map((fs) => ({ - label: fs.fs || 'Unknown', - value: fs.use || 0, - total: formatBytes(fs.size), - used: formatBytes(fs.used), - subtext: `${formatBytes(fs.used)} / ${formatBytes(fs.size)}`, - })) - } + const storageItems = getAllDiskDisplayItems(info?.disk, info?.fsSize) return ( diff --git a/install/sidecar-disk-collector/collect-disk-info.sh b/install/sidecar-disk-collector/collect-disk-info.sh index ec550c1..7705175 100755 --- a/install/sidecar-disk-collector/collect-disk-info.sh +++ b/install/sidecar-disk-collector/collect-disk-info.sh @@ -40,6 +40,10 @@ while true; do [[ "$fstype" =~ ^(tmpfs|devtmpfs|squashfs|sysfs|proc|devpts|cgroup|cgroup2|overlay|nsfs|autofs|hugetlbfs|mqueue|pstore|fusectl|binfmt_misc)$ ]] && continue [[ "$mountpoint" == "none" ]] && continue + # Skip Docker bind-mounts to individual files (e.g., /etc/resolv.conf, /etc/hostname, /etc/hosts) + # These are not real filesystem roots and report misleading sizes + [[ -f "/host${mountpoint}" ]] && continue + STATS=$(df -B1 "/host${mountpoint}" 2>/dev/null | awk 'NR==2{print $2,$3,$4,$5}') [[ -z "$STATS" ]] && continue