mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-03-31 05:59:26 +02:00
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>
290 lines
12 KiB
TypeScript
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>
|
|
)
|
|
}
|