project-nomad/admin/inertia/pages/settings/system.tsx
Chris Sherwood b0be99700d fix(System): show host OS, hostname, GPU instead of container info
Inside Docker, systeminformation reports the container's Alpine Linux
distro, container ID as hostname, and no GPU. This enriches the System
Information page with actual host details via the Docker API:

- Distribution and kernel version from docker.info()
- Real hostname from docker.info().Name
- GPU model and VRAM via nvidia-smi inside the Ollama container
- Graphics card in System Details (Model, Vendor, VRAM)
- Friendly uptime display (days/hours/minutes instead of minutes only)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 13:23:39 -08:00

285 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,
})
const memoryUsagePercent = info?.mem.total
? (((info.mem.total - info.mem.available) / 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(info?.mem.used || 0)} / ${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(info?.mem.used || 0)}
</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.free || 0)}
</div>
<div className="text-sm text-desert-stone-dark uppercase tracking-wide">
Free 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>
)
}