feat(debug-info): add storage, docker, GPU health, and auto-update diagnostics

The Debug Info bundle carried no storage-path info, which stalled diagnosis of
relocation issues like #1050 (moved data not seen by the admin). Add the fields
support actually asks for, all best-effort so one failure never blanks the bundle:

- Storage: resolved host storage root (#938), container path, whether
  NOMAD_STORAGE_PATH is set, and the Kiwix library book count (0 books is the
  tell for an empty/wrong-path library).
- Docker Engine version (reporters currently paste it by hand; needed for
  container/updater issues).
- GPU passthrough health (gpuHealth.status + detected gpu.type) for the
  passthrough-lost-after-update class (#755/#878).
- Auto-update status for core/apps/content plus any auto-disabled reason,
  since the auto-update trilogy shipped.

Adds a public DockerService.getHostStorageRoot() wrapper over the existing
#938 resolver.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Sherwood 2026-07-14 09:50:53 -07:00
parent 6a4f02dd46
commit 9b7fe521d1
2 changed files with 61 additions and 1 deletions

View File

@ -512,6 +512,15 @@ export class DockerService {
* Falls back to NOMAD_STORAGE_PATH / the production default if the admin
* container or its storage mount can't be inspected.
*/
/**
* Public accessor for the resolved host path backing `/app/storage`. Used by the
* Debug Info bundle so support can see where content actually lives on the host
* (the #1050 class of "moved my data, admin doesn't see it" reports).
*/
async getHostStorageRoot(): Promise<string> {
return this._resolveHostStorageRoot()
}
private async _resolveHostStorageRoot(): Promise<string> {
if (this._hostStorageRoot) return this._hostStorageRoot
const fallback = env.get('NOMAD_STORAGE_PATH', DockerService.DEFAULT_HOST_STORAGE_ROOT)

View File

@ -22,6 +22,7 @@ import KVStore from '#models/kv_store'
import { KV_STORE_SCHEMA, KVStoreKey } from '../../types/kv_store.js'
import { isNewerVersion } from '../utils/version.js'
import { invalidateAssistantNameCache } from '../../config/inertia.js'
import { KiwixLibraryService } from '#services/kiwix_library_service'
@inject()
export class SystemService {
@ -757,6 +758,28 @@ export class SystemService {
this.checkLatestVersion().catch(() => null),
])
// Diagnostics tied to common support cases: storage relocation (#1050),
// container/updater issues (#858), GPU passthrough (#755/#878), and the
// auto-update trilogy. All best-effort so a single failure never blanks the
// whole bundle.
const [dockerVersion, hostStorageRoot, kiwixBookCount, gpuType] = await Promise.all([
this.dockerService.docker
.version()
.then((v: any) => v?.Version ?? null)
.catch(() => null),
this.dockerService.getHostStorageRoot().catch(() => null),
new KiwixLibraryService().getBookCount().catch(() => null),
KVStore.getValue('gpu.type').catch(() => null),
])
const [autoUpdateCore, autoUpdateApps, autoUpdateContent, autoDisabledReason] =
await Promise.all([
KVStore.getValue('autoUpdate.enabled').catch(() => null),
KVStore.getValue('appAutoUpdate.enabled').catch(() => null),
KVStore.getValue('contentAutoUpdate.enabled').catch(() => null),
KVStore.getValue('autoUpdate.autoDisabledReason').catch(() => null),
])
const isEnabled = (v: any) => v === true || v === 'true'
const lines: string[] = [
'Project NOMAD Debug Info',
'========================',
@ -765,7 +788,7 @@ export class SystemService {
]
if (systemInfo) {
const { cpu, mem, os, disk, fsSize, uptime, graphics } = systemInfo
const { cpu, mem, os, disk, fsSize, uptime, graphics, gpuHealth } = systemInfo
lines.push('')
lines.push('System:')
@ -773,6 +796,7 @@ export class SystemService {
if (os.hostname) lines.push(` Hostname: ${os.hostname}`)
if (os.kernel) lines.push(` Kernel: ${os.kernel}`)
if (os.arch) lines.push(` Architecture: ${os.arch}`)
if (dockerVersion) lines.push(` Docker Engine: ${dockerVersion}`)
if (uptime?.uptime) lines.push(` Uptime: ${this._formatUptime(uptime.uptime)}`)
lines.push('')
@ -794,6 +818,10 @@ export class SystemService {
} else {
lines.push(' GPU: None detected')
}
if (gpuHealth?.status) {
const vendor = gpuType || gpuHealth.gpuVendor
lines.push(` GPU Passthrough: ${gpuHealth.status}${vendor ? ` (${vendor})` : ''}`)
}
// Disk info — try disk array first, fall back to fsSize
const diskEntries = disk.filter((d) => d.totalSize > 0)
@ -814,6 +842,20 @@ export class SystemService {
}
}
lines.push('')
lines.push('Storage:')
lines.push(` Host storage root: ${hostStorageRoot ?? 'unknown'}`)
lines.push(` Container path: ${DockerService.ADMIN_STORAGE_DEST}`)
const storageEnv = process.env.NOMAD_STORAGE_PATH
lines.push(
` NOMAD_STORAGE_PATH: ${storageEnv ? storageEnv : 'not set (auto-detected from admin mount)'}`
)
if (kiwixBookCount !== null) {
lines.push(
` Kiwix library: ${kiwixBookCount === 0 ? 'empty (0 books)' : `${kiwixBookCount} book(s)`}`
)
}
const installed = services.filter((s) => s.installed)
lines.push('')
if (installed.length > 0) {
@ -837,6 +879,15 @@ export class SystemService {
lines.push(`Update Available: ${updateMsg}`)
}
lines.push('')
lines.push('Auto-Update:')
lines.push(` Core: ${isEnabled(autoUpdateCore) ? 'Enabled' : 'Disabled'}`)
lines.push(` Apps: ${isEnabled(autoUpdateApps) ? 'Enabled' : 'Disabled'}`)
lines.push(` Content: ${isEnabled(autoUpdateContent) ? 'Enabled' : 'Disabled'}`)
if (autoDisabledReason) {
lines.push(` Auto-disabled reason: ${autoDisabledReason}`)
}
return lines.join('\n')
}