mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-03-27 19:19:25 +01:00
fix(disk): correct storage display by fixing device matching and dedup mount entries
This commit is contained in:
parent
f5a181b09f
commit
b8cf1b6127
|
|
@ -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<string, NomadDiskInfoRaw['fsSize'][0]>()
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
82
admin/inertia/hooks/useDiskDisplayData.ts
Normal file
82
admin/inertia/hooks/useDiskDisplayData.ts
Normal file
|
|
@ -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<number>()
|
||||
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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<number>()
|
||||
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 (
|
||||
<SettingsLayout>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user