From 9b7fe521d194ff4eed27f13328b37e9c2a223fd3 Mon Sep 17 00:00:00 2001 From: Chris Sherwood Date: Tue, 14 Jul 2026 09:50:53 -0700 Subject: [PATCH] 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) --- admin/app/services/docker_service.ts | 9 +++++ admin/app/services/system_service.ts | 53 +++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/admin/app/services/docker_service.ts b/admin/app/services/docker_service.ts index 68a631a..430a4aa 100644 --- a/admin/app/services/docker_service.ts +++ b/admin/app/services/docker_service.ts @@ -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 { + return this._resolveHostStorageRoot() + } + private async _resolveHostStorageRoot(): Promise { if (this._hostStorageRoot) return this._hostStorageRoot const fallback = env.get('NOMAD_STORAGE_PATH', DockerService.DEFAULT_HOST_STORAGE_ROOT) diff --git a/admin/app/services/system_service.ts b/admin/app/services/system_service.ts index 6f9d041..3b6fff1 100644 --- a/admin/app/services/system_service.ts +++ b/admin/app/services/system_service.ts @@ -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') }