mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-03-28 03:29: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 []
|
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
|
return diskLayout.blockdevices
|
||||||
.filter((disk) => disk.type === 'disk') // Only physical disks
|
.filter((disk) => disk.type === 'disk') // Only physical disks
|
||||||
.map((disk) => {
|
.map((disk) => {
|
||||||
const filesystems = getAllFilesystems(disk, fsSize)
|
const filesystems = getAllFilesystems(disk, dedupedFsSize)
|
||||||
|
|
||||||
// Across all partitions
|
// Across all partitions
|
||||||
const totalUsed = filesystems.reduce((sum, p) => sum + (p.used || 0), 0)
|
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
|
// Remove /dev/ and /dev/mapper/ prefixes
|
||||||
const normalized = fsPath.replace('/dev/mapper/', '').replace('/dev/', '')
|
const normalized = fsPath.replace('/dev/mapper/', '').replace('/dev/', '')
|
||||||
|
|
||||||
// Direct match
|
// Direct match (covers /dev/sda1 ↔ sda1, /dev/nvme0n1p1 ↔ nvme0n1p1)
|
||||||
if (normalized === deviceName) {
|
if (normalized === deviceName) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// LVM volumes use dashes instead of slashes
|
// LVM/device-mapper: e.g., /dev/mapper/ubuntu--vg-ubuntu--lv contains "ubuntu--lv"
|
||||||
// e.g., ubuntu--vg-ubuntu--lv matches the device name
|
if (fsPath.startsWith('/dev/mapper/') && fsPath.includes(deviceName)) {
|
||||||
if (fsPath.includes(deviceName)) {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
- **Settings**: Storage usage display now prefers real block devices over tempfs. Thanks @Bortlesboat for the fix!
|
- **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!
|
- **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!
|
- **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.
|
- **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 { useNotifications } from '~/context/NotificationContext'
|
||||||
import useInternetStatus from '~/hooks/useInternetStatus'
|
import useInternetStatus from '~/hooks/useInternetStatus'
|
||||||
import { useSystemInfo } from '~/hooks/useSystemInfo'
|
import { useSystemInfo } from '~/hooks/useSystemInfo'
|
||||||
|
import { getPrimaryDiskInfo } from '~/hooks/useDiskDisplayData'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import type { CategoryWithStatus, SpecTier, SpecResource } from '../../../types/collections'
|
import type { CategoryWithStatus, SpecTier, SpecResource } from '../../../types/collections'
|
||||||
import { resolveTierResources } from '~/lib/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
|
// Get primary disk/filesystem info for storage projection
|
||||||
// Try disk array first (Linux/production), fall back to fsSize (Windows/dev)
|
const storageInfo = getPrimaryDiskInfo(systemInfo?.disk, systemInfo?.fsSize)
|
||||||
// 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 canProceedToNextStep = () => {
|
const canProceedToNextStep = () => {
|
||||||
if (!isOnline) return false // Must be online to proceed
|
if (!isOnline) return false // Must be online to proceed
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { Head } from '@inertiajs/react'
|
||||||
import SettingsLayout from '~/layouts/SettingsLayout'
|
import SettingsLayout from '~/layouts/SettingsLayout'
|
||||||
import { SystemInformationResponse } from '../../../types/system'
|
import { SystemInformationResponse } from '../../../types/system'
|
||||||
import { formatBytes } from '~/lib/util'
|
import { formatBytes } from '~/lib/util'
|
||||||
|
import { getAllDiskDisplayItems } from '~/hooks/useDiskDisplayData'
|
||||||
import CircularGauge from '~/components/systeminfo/CircularGauge'
|
import CircularGauge from '~/components/systeminfo/CircularGauge'
|
||||||
import HorizontalBarChart from '~/components/HorizontalBarChart'
|
import HorizontalBarChart from '~/components/HorizontalBarChart'
|
||||||
import InfoCard from '~/components/systeminfo/InfoCard'
|
import InfoCard from '~/components/systeminfo/InfoCard'
|
||||||
|
|
@ -105,42 +106,7 @@ export default function SettingsPage(props: {
|
||||||
: `${uptimeMinutes}m`
|
: `${uptimeMinutes}m`
|
||||||
|
|
||||||
// Build storage display items - fall back to fsSize when disk array is empty
|
// Build storage display items - fall back to fsSize when disk array is empty
|
||||||
// (Same approach as Easy Setup wizard fix from PR #90)
|
const storageItems = getAllDiskDisplayItems(info?.disk, info?.fsSize)
|
||||||
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)}`,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsLayout>
|
<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
|
[[ "$fstype" =~ ^(tmpfs|devtmpfs|squashfs|sysfs|proc|devpts|cgroup|cgroup2|overlay|nsfs|autofs|hugetlbfs|mqueue|pstore|fusectl|binfmt_misc)$ ]] && continue
|
||||||
[[ "$mountpoint" == "none" ]] && 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}')
|
STATS=$(df -B1 "/host${mountpoint}" 2>/dev/null | awk 'NR==2{print $2,$3,$4,$5}')
|
||||||
[[ -z "$STATS" ]] && continue
|
[[ -z "$STATS" ]] && continue
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user