project-nomad/admin/inertia/pages/settings/system.tsx
Chris Sherwood f02c5e5cd0 fix(System): use available memory for usage calculation
mem.used on Linux includes reclaimable buff/cache, which caused the
System Information page to show 97% memory usage on a 64GB machine
that actually had 53GB available. The warning banner fired at >90%
creating a false alarm.

Now uses (total - available) for the gauge, percentage, and displayed
values. Also renames "Free RAM" to "Available RAM" using mem.available
instead of mem.free, since free is misleadingly small on Linux (it
excludes reclaimable cache).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 10:17:55 -08:00

290 lines
12 KiB
TypeScript

import { Head } from '@inertiajs/react'
import SettingsLayout from '~/layouts/SettingsLayout'
import { SystemInformationResponse } from '../../../types/system'
import { formatBytes } from '~/lib/util'
import CircularGauge from '~/components/systeminfo/CircularGauge'
import HorizontalBarChart from '~/components/HorizontalBarChart'
import InfoCard from '~/components/systeminfo/InfoCard'
import Alert from '~/components/Alert'
import { useSystemInfo } from '~/hooks/useSystemInfo'
import StatusCard from '~/components/systeminfo/StatusCard'
import { IconCpu, IconDatabase, IconServer, IconDeviceDesktop, IconComponents } from '@tabler/icons-react'
export default function SettingsPage(props: {
system: { info: SystemInformationResponse | undefined }
}) {
const { data: info } = useSystemInfo({
initialData: props.system.info,
})
// Use (total - available) to reflect actual memory pressure.
// mem.used includes reclaimable buff/cache on Linux, which inflates the number.
const memoryUsed = info?.mem.total && info?.mem.available != null
? info.mem.total - info.mem.available
: info?.mem.used || 0
const memoryUsagePercent = info?.mem.total
? ((memoryUsed / info.mem.total) * 100).toFixed(1)
: 0
const swapUsagePercent = info?.mem.swaptotal
? ((info.mem.swapused / info.mem.swaptotal) * 100).toFixed(1)
: 0
const uptimeSeconds = info?.uptime.uptime || 0
const uptimeDays = Math.floor(uptimeSeconds / 86400)
const uptimeHours = Math.floor((uptimeSeconds % 86400) / 3600)
const uptimeMinutes = Math.floor((uptimeSeconds % 3600) / 60)
const uptimeDisplay = uptimeDays > 0
? `${uptimeDays}d ${uptimeHours}h ${uptimeMinutes}m`
: uptimeHours > 0
? `${uptimeHours}h ${uptimeMinutes}m`
: `${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)}`,
}))
}
return (
<SettingsLayout>
<Head title="System Information" />
<div className="xl:pl-72 w-full">
<main className="px-6 lg:px-12 py-6 lg:py-8">
<div className="mb-8">
<h1 className="text-4xl font-bold text-desert-green mb-2">System Information</h1>
<p className="text-desert-stone-dark">
Real-time monitoring and diagnostics Last updated: {new Date().toLocaleString()}
Refreshing data every 30 seconds
</p>
</div>
{Number(memoryUsagePercent) > 90 && (
<div className="mb-6">
<Alert
type="error"
title="Very High Memory Usage Detected"
message="System memory usage exceeds 90%. Performance degradation may occur."
variant="bordered"
/>
</div>
)}
<section className="mb-12">
<h2 className="text-2xl font-bold text-desert-green mb-6 flex items-center gap-2">
<div className="w-1 h-6 bg-desert-green" />
Resource Usage
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<div className="bg-desert-white rounded-lg p-6 border border-desert-stone-light shadow-sm hover:shadow-lg transition-shadow">
<CircularGauge
value={info?.currentLoad.currentLoad || 0}
label="CPU Usage"
size="lg"
variant="cpu"
subtext={`${info?.cpu.cores || 0} cores`}
icon={<IconCpu className="w-8 h-8" />}
/>
</div>
<div className="bg-desert-white rounded-lg p-6 border border-desert-stone-light shadow-sm hover:shadow-lg transition-shadow">
<CircularGauge
value={Number(memoryUsagePercent)}
label="Memory Usage"
size="lg"
variant="memory"
subtext={`${formatBytes(memoryUsed)} / ${formatBytes(info?.mem.total || 0)}`}
icon={<IconDatabase className="w-8 h-8" />}
/>
</div>
<div className="bg-desert-white rounded-lg p-6 border border-desert-stone-light shadow-sm hover:shadow-lg transition-shadow">
<CircularGauge
value={Number(swapUsagePercent)}
label="Swap Usage"
size="lg"
variant="disk"
subtext={`${formatBytes(info?.mem.swapused || 0)} / ${formatBytes(info?.mem.swaptotal || 0)}`}
icon={<IconServer className="w-8 h-8" />}
/>
</div>
</div>
</section>
<section className="mb-12">
<h2 className="text-2xl font-bold text-desert-green mb-6 flex items-center gap-2">
<div className="w-1 h-6 bg-desert-green" />
System Details
</h2>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<InfoCard
title="Operating System"
icon={<IconDeviceDesktop className="w-6 h-6" />}
variant="elevated"
data={[
{ label: 'Distribution', value: info?.os.distro },
{ label: 'Kernel Version', value: info?.os.kernel },
{ label: 'Architecture', value: info?.os.arch },
{ label: 'Hostname', value: info?.os.hostname },
{ label: 'Platform', value: info?.os.platform },
]}
/>
<InfoCard
title="Processor"
icon={<IconCpu className="w-6 h-6" />}
variant="elevated"
data={[
{ label: 'Manufacturer', value: info?.cpu.manufacturer },
{ label: 'Brand', value: info?.cpu.brand },
{ label: 'Cores', value: info?.cpu.cores },
{ label: 'Physical Cores', value: info?.cpu.physicalCores },
{
label: 'Virtualization',
value: info?.cpu.virtualization ? 'Enabled' : 'Disabled',
},
]}
/>
{info?.graphics?.controllers && info.graphics.controllers.length > 0 && (
<InfoCard
title="Graphics"
icon={<IconComponents className="w-6 h-6" />}
variant="elevated"
data={info.graphics.controllers.map((gpu, i) => {
const prefix = info.graphics.controllers.length > 1 ? `GPU ${i + 1} ` : ''
return [
{ label: `${prefix}Model`, value: gpu.model },
{ label: `${prefix}Vendor`, value: gpu.vendor },
{ label: `${prefix}VRAM`, value: gpu.vram ? `${gpu.vram} MB` : 'N/A' },
]
}).flat()}
/>
)}
</div>
</section>
<section className="mb-12">
<h2 className="text-2xl font-bold text-desert-green mb-6 flex items-center gap-2">
<div className="w-1 h-6 bg-desert-green" />
Memory Allocation
</h2>
<div className="bg-desert-white rounded-lg p-8 border border-desert-stone-light shadow-sm hover:shadow-lg transition-shadow">
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 mb-8">
<div className="text-center">
<div className="text-3xl font-bold text-desert-green mb-1">
{formatBytes(info?.mem.total || 0)}
</div>
<div className="text-sm text-desert-stone-dark uppercase tracking-wide">
Total RAM
</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-desert-green mb-1">
{formatBytes(memoryUsed)}
</div>
<div className="text-sm text-desert-stone-dark uppercase tracking-wide">
Used RAM
</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-desert-green mb-1">
{formatBytes(info?.mem.available || 0)}
</div>
<div className="text-sm text-desert-stone-dark uppercase tracking-wide">
Available RAM
</div>
</div>
</div>
<div className="relative h-12 bg-desert-stone-lighter rounded-lg overflow-hidden border border-desert-stone-light">
<div
className="absolute left-0 top-0 h-full bg-desert-orange transition-all duration-1000"
style={{ width: `${memoryUsagePercent}%` }}
></div>
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-sm font-bold text-desert-white drop-shadow-md z-10">
{memoryUsagePercent}% Utilized
</span>
</div>
</div>
</div>
</section>
<section className="mb-12">
<h2 className="text-2xl font-bold text-desert-green mb-6 flex items-center gap-2">
<div className="w-1 h-6 bg-desert-green" />
Storage Devices
</h2>
<div className="bg-desert-white rounded-lg p-8 border border-desert-stone-light shadow-sm hover:shadow-lg transition-shadow">
{storageItems.length > 0 ? (
<HorizontalBarChart
items={storageItems}
progressiveBarColor={true}
statuses={[
{
label: 'Normal',
min_threshold: 0,
color_class: 'bg-desert-olive',
},
{
label: 'Warning - Usage High',
min_threshold: 75,
color_class: 'bg-desert-orange',
},
{
label: 'Critical - Disk Almost Full',
min_threshold: 90,
color_class: 'bg-desert-red',
},
]}
/>
) : (
<div className="text-center text-desert-stone-dark py-8">
No storage devices detected
</div>
)}
</div>
</section>
<section>
<h2 className="text-2xl font-bold text-desert-green mb-6 flex items-center gap-2">
<div className="w-1 h-6 bg-desert-green" />
System Status
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<StatusCard title="System Uptime" value={uptimeDisplay} />
<StatusCard title="CPU Cores" value={info?.cpu.cores || 0} />
<StatusCard title="Storage Devices" value={storageItems.length} />
</div>
</section>
</main>
</div>
</SettingsLayout>
)
}