mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-03-28 03:29:25 +01:00
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:
parent
569dae057d
commit
b0be99700d
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user