From b0be99700df7be439f9812c602aaf9d703f435d7 Mon Sep 17 00:00:00 2001 From: Chris Sherwood Date: Sat, 7 Feb 2026 09:52:27 -0800 Subject: [PATCH] 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 --- admin/app/services/system_service.ts | 74 ++++++++++++++++++++++++- admin/inertia/pages/settings/system.tsx | 29 +++++++++- admin/types/system.ts | 1 + 3 files changed, 100 insertions(+), 4 deletions(-) diff --git a/admin/app/services/system_service.ts b/admin/app/services/system_service.ts index 1729f12..a6e0f14 100644 --- a/admin/app/services/system_service.ts +++ b/admin/app/services/system_service.ts @@ -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 { 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((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) diff --git a/admin/inertia/pages/settings/system.tsx b/admin/inertia/pages/settings/system.tsx index 1a0b968..950f2fd 100644 --- a/admin/inertia/pages/settings/system.tsx +++ b/admin/inertia/pages/settings/system.tsx @@ -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 && ( + } + 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()} + /> + )}
@@ -249,7 +272,7 @@ export default function SettingsPage(props: { System Status
- +
diff --git a/admin/types/system.ts b/admin/types/system.ts index dd77a49..ceb7456 100644 --- a/admin/types/system.ts +++ b/admin/types/system.ts @@ -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