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>
This commit is contained in:
Chris Sherwood 2026-02-07 09:52:27 -08:00 committed by Jake Turner
parent 569dae057d
commit b0be99700d
3 changed files with 100 additions and 4 deletions

View File

@ -5,6 +5,7 @@ import { ServiceSlim } from '../../types/services.js'
import logger from '@adonisjs/core/services/logger'
import si from 'systeminformation'
import { NomadDiskInfo, NomadDiskInfoRaw, SystemInformationResponse } from '../../types/system.js'
import { SERVICE_NAMES } from '../../constants/service_names.js'
import { readFileSync } from 'fs'
import path, { join } from 'path'
import { getAllFilesystems, getFile } from '../utils/fs.js'
@ -144,13 +145,14 @@ export class SystemService {
async getSystemInfo(): Promise<SystemInformationResponse | undefined> {
try {
const [cpu, mem, os, currentLoad, fsSize, uptime] = await Promise.all([
const [cpu, mem, os, currentLoad, fsSize, uptime, graphics] = await Promise.all([
si.cpu(),
si.mem(),
si.osInfo(),
si.currentLoad(),
si.fsSize(),
si.time(),
si.graphics(),
])
let diskInfo: NomadDiskInfoRaw | undefined
@ -173,6 +175,75 @@ export class SystemService {
logger.error('Error reading disk info file:', error)
}
// Query Docker API for host-level info (hostname, OS, GPU runtime)
// si.osInfo() returns the container's info inside Docker, not the host's
try {
const dockerInfo = await this.dockerService.docker.info()
if (dockerInfo.Name) {
os.hostname = dockerInfo.Name
}
if (dockerInfo.OperatingSystem) {
os.distro = dockerInfo.OperatingSystem
}
if (dockerInfo.KernelVersion) {
os.kernel = dockerInfo.KernelVersion
}
// If si.graphics() returned no controllers (common inside Docker),
// fall back to nvidia runtime + nvidia-smi detection
if (!graphics.controllers || graphics.controllers.length === 0) {
const runtimes = dockerInfo.Runtimes || {}
if ('nvidia' in runtimes) {
let gpuName = 'NVIDIA GPU'
try {
const containers = await this.dockerService.docker.listContainers({ all: false })
const ollamaContainer = containers.find((c) =>
c.Names.includes(`/${SERVICE_NAMES.OLLAMA}`)
)
if (ollamaContainer) {
const container = this.dockerService.docker.getContainer(ollamaContainer.Id)
const exec = await container.exec({
Cmd: ['nvidia-smi', '--query-gpu=name,memory.total', '--format=csv,noheader,nounits'],
AttachStdout: true,
AttachStderr: true,
Tty: true,
})
const stream = await exec.start({ Tty: true })
const output = await new Promise<string>((resolve) => {
let data = ''
const timeout = setTimeout(() => resolve(data), 5000)
stream.on('data', (chunk: Buffer) => { data += chunk.toString() })
stream.on('end', () => { clearTimeout(timeout); resolve(data) })
})
const cleaned = output.replace(/[\x00-\x08]/g, '').trim()
if (cleaned && !cleaned.toLowerCase().includes('error')) {
const parts = cleaned.split(',').map((s) => s.trim())
gpuName = parts[0] || gpuName
const vramMB = parts[1] ? parseInt(parts[1], 10) : 0
graphics.controllers = [{
vendor: 'NVIDIA',
model: gpuName,
vram: vramMB || null,
} as any]
}
}
} catch {
// nvidia-smi failed, use generic entry
}
if (graphics.controllers.length === 0) {
graphics.controllers = [{
vendor: 'NVIDIA',
model: gpuName,
vram: null,
} as any]
}
}
}
} catch {
// Docker info query failed, skip host-level enrichment
}
return {
cpu,
mem,
@ -181,6 +252,7 @@ export class SystemService {
currentLoad,
fsSize,
uptime,
graphics,
}
} catch (error) {
logger.error('Error getting system info:', error)

View File

@ -8,7 +8,7 @@ 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 } from '@tabler/icons-react'
import { IconCpu, IconDatabase, IconServer, IconDeviceDesktop, IconComponents } from '@tabler/icons-react'
export default function SettingsPage(props: {
system: { info: SystemInformationResponse | undefined }
@ -25,7 +25,15 @@ export default function SettingsPage(props: {
? ((info.mem.swapused / info.mem.swaptotal) * 100).toFixed(1)
: 0
const uptimeMinutes = info?.uptime.uptime ? Math.floor(info.uptime.uptime / 60) : 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)
@ -160,6 +168,21 @@ export default function SettingsPage(props: {
},
]}
/>
{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">
@ -249,7 +272,7 @@ export default function SettingsPage(props: {
System Status
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<StatusCard title="System Uptime" value={`${uptimeMinutes}m`} />
<StatusCard title="System Uptime" value={uptimeDisplay} />
<StatusCard title="CPU Cores" value={info?.cpu.cores || 0} />
<StatusCard title="Storage Devices" value={storageItems.length} />
</div>

View File

@ -8,6 +8,7 @@ export type SystemInformationResponse = {
currentLoad: Systeminformation.CurrentLoadData
fsSize: Systeminformation.FsSizeData[]
uptime: Systeminformation.TimeData
graphics: Systeminformation.GraphicsData
}
// Type inferrence is not working properly with usePage and shared props, so we define this type manually