Fix GPU inventory typing and user role mapping

This commit is contained in:
Esdras Renan 2025-10-13 13:59:48 -03:00
parent 42611df0f5
commit 4f812a2e4c
3 changed files with 183 additions and 84 deletions

View file

@ -10,8 +10,10 @@ import { Skeleton } from "@/components/ui/skeleton"
export function AdminMachineDetailsClient({ tenantId, machineId }: { tenantId: string; machineId: string }) {
const queryResult = useQuery(api.machines.listByTenant, { tenantId, includeMetadata: true }) as MachinesQueryItem[] | undefined
const isLoading = queryResult === undefined
const machines = queryResult ?? []
const machine = useMemo(() => machines.find((m) => m.id === machineId) ?? null, [machines, machineId])
const machine = useMemo(() => {
if (!queryResult) return null
return queryResult.find((m) => m.id === machineId) ?? null
}, [queryResult, machineId])
if (isLoading) {
return (

View file

@ -199,22 +199,97 @@ function readNumber(record: Record<string, unknown>, ...keys: string[]): number
return raw
}
if (typeof raw === "string") {
const parsed = Number(raw)
if (!Number.isNaN(parsed)) {
return parsed
const trimmed = raw.trim()
if (!trimmed) continue
const parsed = Number(trimmed)
if (!Number.isNaN(parsed)) return parsed
const digits = trimmed.replace(/[^0-9.]/g, "")
if (digits) {
const fallback = Number(digits)
if (!Number.isNaN(fallback)) return fallback
}
}
}
return undefined
}
function parseBytesLike(value: unknown): number | undefined {
if (typeof value === "number" && Number.isFinite(value)) return value
if (typeof value === "string") {
const trimmed = value.trim()
if (!trimmed) return undefined
const normalized = trimmed.replace(",", ".")
const match = normalized.match(/^([\d.]+)\s*(ti|tb|tib|gb|gib|mb|mib|kb|kib|b)?$/i)
if (match) {
const amount = Number(match[1])
if (Number.isNaN(amount)) return undefined
const unit = match[2]?.toLowerCase()
const base = 1024
const unitMap: Record<string, number> = {
b: 1,
kb: base,
kib: base,
mb: base ** 2,
mib: base ** 2,
gb: base ** 3,
gib: base ** 3,
tb: base ** 4,
tib: base ** 4,
ti: base ** 4,
}
if (unit) {
const multiplier = unitMap[unit]
if (multiplier) {
return amount * multiplier
}
}
return amount
}
const digits = normalized.replace(/[^0-9.]/g, "")
if (digits) {
const fallback = Number(digits)
if (!Number.isNaN(fallback)) return fallback
}
}
return undefined
}
function deriveVendor(record: Record<string, unknown>): string | undefined {
const direct = readString(record, "vendor", "Vendor", "AdapterCompatibility")
if (direct) return direct
const pnp = readString(record, "PNPDeviceID")
if (!pnp) return undefined
const match = pnp.match(/VEN_([0-9A-F]{4})/i)
if (match) {
const vendorCode = match[1].toUpperCase()
const vendorMap: Record<string, string> = {
"10DE": "NVIDIA",
"1002": "AMD",
"1022": "AMD",
"8086": "Intel",
"8087": "Intel",
"1AF4": "Red Hat",
}
return vendorMap[vendorCode] ?? `VEN_${vendorCode}`
}
const segments = pnp.split("\\")
const last = segments.pop()
return last && last.trim().length > 0 ? last : pnp
}
function normalizeGpuSource(value: unknown): GpuAdapter | null {
if (typeof value === "string") {
const name = value.trim()
return name ? { name } : null
}
const record = toRecord(value)
if (!record) return null
const name = readString(record, "name", "Name")
const vendor = readString(record, "vendor", "Vendor", "PNPDeviceID")
const driver = readString(record, "driver", "DriverVersion")
const memoryBytes = readNumber(record, "memoryBytes", "MemoryBytes", "AdapterRAM")
const name = readString(record, "name", "Name", "_name", "AdapterCompatibility")
const vendor = deriveVendor(record)
const driver = readString(record, "driver", "DriverVersion", "driverVersion")
const memoryBytes =
readNumber(record, "memoryBytes", "MemoryBytes", "AdapterRAM", "VRAM", "vramBytes") ??
parseBytesLike(record["AdapterRAM"] ?? record["VRAM"] ?? record["vram"])
if (!name && !vendor && !driver && memoryBytes === undefined) {
return null
}
@ -399,7 +474,7 @@ export function AdminMachinesOverview({ tenantId }: { tenantId: string }) {
.toLowerCase()
return hay.includes(text)
})
}, [machines, q, statusFilter, osFilter, companyQuery, onlyAlerts])
}, [machines, q, statusFilter, osFilter, companyQuery, onlyAlerts, companyNameBySlug])
return (
<div className="grid gap-6">
@ -556,6 +631,9 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
const linuxExt = extended?.linux ?? null
const windowsExt = extended?.windows ?? null
const macosExt = extended?.macos ?? null
const windowsMemoryModules = Array.isArray(windowsExt?.memoryModules) ? windowsExt.memoryModules : []
const windowsVideoControllers = Array.isArray(windowsExt?.videoControllers) ? windowsExt.videoControllers : []
const windowsDiskEntries = Array.isArray(windowsExt?.disks) ? windowsExt.disks : []
const linuxLsblk = linuxExt?.lsblk ?? []
const linuxSmartEntries = linuxExt?.smart ?? []
const normalizedHardwareGpus = Array.isArray(hardware?.gpus)
@ -563,24 +641,11 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
: []
const hardwarePrimaryGpu = hardware?.primaryGpu ? normalizeGpuSource(hardware.primaryGpu) : null
type WinCpuInfo = {
Name?: string
Manufacturer?: string
SocketDesignation?: string
NumberOfCores?: number
NumberOfLogicalProcessors?: number
L2CacheSize?: number
L3CacheSize?: number
MaxClockSpeed?: number
}
const winCpu = (() => {
const cpuInfo = windowsExt?.cpu
if (!cpuInfo) return null
return Array.isArray(cpuInfo) ? cpuInfo[0] ?? null : cpuInfo
})()
const winMemModules = windowsExt?.memoryModules ?? []
const winMemTotal = winMemModules.reduce((acc, module) => acc + Number(module?.Capacity ?? 0), 0)
const windowsVideoControllers = windowsExt?.videoControllers ?? []
const windowsCpuRaw = windowsExt?.cpu
const winCpu = windowsCpuRaw
? (Array.isArray(windowsCpuRaw) ? windowsCpuRaw[0] ?? null : windowsCpuRaw)
: null
const winMemTotal = windowsMemoryModules.reduce((acc, module) => acc + (parseBytesLike(module?.Capacity) ?? 0), 0)
const normalizedWindowsGpus = windowsVideoControllers
.map((controller) => normalizeGpuSource(controller))
.filter((gpu): gpu is GpuAdapter => Boolean(gpu))
@ -592,21 +657,24 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
],
(gpu) => `${gpu.name ?? ""}|${gpu.vendor ?? ""}|${gpu.driver ?? ""}`
)
const primaryGpu = combinedGpus[0] ?? null
const displayGpus = combinedGpus
const windowsPrimaryGpu = normalizedWindowsGpus[0] ?? null
const windowsCpuDetails = windowsExt?.cpu
? Array.isArray(windowsExt.cpu)
? windowsExt.cpu
: [windowsExt.cpu]
const displayGpus = [...combinedGpus].sort(
(a, b) => (b.memoryBytes ?? 0) - (a.memoryBytes ?? 0)
)
const primaryGpu = hardwarePrimaryGpu ?? displayGpus[0] ?? null
const windowsPrimaryGpu = [...normalizedWindowsGpus].sort(
(a, b) => (b.memoryBytes ?? 0) - (a.memoryBytes ?? 0)
)[0] ?? null
const windowsCpuDetails = windowsCpuRaw
? Array.isArray(windowsCpuRaw)
? windowsCpuRaw
: [windowsCpuRaw]
: []
const windowsServices = windowsExt?.services ?? []
const windowsSoftware = windowsExt?.software ?? []
const windowsDisks = windowsExt?.disks ?? []
const winDiskStats = windowsDisks.length > 0
const winDiskStats = windowsDiskEntries.length > 0
? {
count: windowsDisks.length,
total: windowsDisks.reduce((acc, d) => acc + Number(d?.Size ?? 0), 0),
count: windowsDiskEntries.length,
total: windowsDiskEntries.reduce((acc, disk) => acc + (parseBytesLike(disk?.Size) ?? 0), 0),
}
: { count: 0, total: 0 }
@ -1295,7 +1363,7 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
</div>
) : null}
{Array.isArray(windowsExt.memoryModules) && windowsExt.memoryModules.length > 0 ? (
{windowsMemoryModules.length > 0 ? (
<div className="rounded-md border border-slate-200 bg-slate-50/60 p-3">
<p className="text-xs font-semibold uppercase text-slate-500">Módulos de memória</p>
<div className="mt-2 overflow-hidden rounded-md border border-slate-200">
@ -1310,34 +1378,64 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
</TableRow>
</TableHeader>
<TableBody>
{(windowsExt.memoryModules as Array<any>).map((m, idx) => (
<TableRow key={`mem-${idx}`} className="border-slate-100">
<TableCell className="text-sm">{m?.BankLabel ?? "—"}</TableCell>
<TableCell className="text-sm text-muted-foreground">{formatBytes(Number(m?.Capacity ?? 0))}</TableCell>
<TableCell className="text-sm text-muted-foreground">{m?.Manufacturer ?? "—"}</TableCell>
<TableCell className="text-sm text-muted-foreground">{m?.PartNumber ?? "—"}</TableCell>
<TableCell className="text-sm text-muted-foreground">{m?.ConfiguredClockSpeed ?? m?.Speed ? `${m?.ConfiguredClockSpeed ?? m?.Speed} MHz` : "—"}</TableCell>
</TableRow>
))}
{windowsMemoryModules.map((module, idx) => {
const record = toRecord(module) ?? {}
const bank = readString(record, "BankLabel", "bankLabel") ?? "—"
const capacityBytes =
parseBytesLike(record["Capacity"]) ?? parseBytesLike(record["capacity"]) ?? 0
const manufacturer = readString(record, "Manufacturer", "manufacturer") ?? "—"
const partNumber = readString(record, "PartNumber", "partNumber") ?? "—"
const clockValue = readNumber(
record,
"ConfiguredClockSpeed",
"configuredClockSpeed",
"Speed",
"speed"
)
const clockLabel =
typeof clockValue === "number" && Number.isFinite(clockValue)
? `${clockValue} MHz`
: "—"
return (
<TableRow key={`mem-${idx}`} className="border-slate-100">
<TableCell className="text-sm">{bank}</TableCell>
<TableCell className="text-sm text-muted-foreground">
{capacityBytes > 0 ? formatBytes(capacityBytes) : "—"}
</TableCell>
<TableCell className="text-sm text-muted-foreground">{manufacturer}</TableCell>
<TableCell className="text-sm text-muted-foreground">{partNumber}</TableCell>
<TableCell className="text-sm text-muted-foreground">{clockLabel}</TableCell>
</TableRow>
)
})}
</TableBody>
</Table>
</div>
</div>
) : null}
{Array.isArray(windowsExt.videoControllers) && windowsExt.videoControllers.length > 0 ? (
{windowsVideoControllers.length > 0 ? (
<div className="rounded-md border border-slate-200 bg-slate-50/60 p-3">
<p className="text-xs font-semibold uppercase text-slate-500">Adaptadores de vídeo</p>
<ul className="mt-2 grid gap-1 text-xs text-muted-foreground">
{(windowsExt.videoControllers as Array<unknown>).map((vRaw, idx) => {
const v = (vRaw && typeof vRaw === "object") ? (vRaw as Record<string, unknown>) : undefined
const name = typeof v?.["Name"] === "string" ? (v["Name"] as string) : "—"
const ram = typeof v?.["AdapterRAM"] === "number" ? (v["AdapterRAM"] as number) : undefined
const driver = typeof v?.["DriverVersion"] === "string" ? (v["DriverVersion"] as string) : undefined
{windowsVideoControllers.map((controller, idx) => {
const record = toRecord(controller) ?? {}
const normalized = normalizeGpuSource(record)
const name =
normalized?.name ??
readString(record, "Name", "name") ??
"—"
const ram =
normalized?.memoryBytes ??
parseBytesLike(record["AdapterRAM"]) ??
undefined
const driver = normalized?.driver ?? readString(record, "DriverVersion", "driverVersion")
const vendor = normalized?.vendor ?? readString(record, "AdapterCompatibility")
return (
<li key={`vid-${idx}`}>
<span className="font-medium text-foreground">{name}</span>
{typeof ram === "number" ? <span className="ml-1">{formatBytes(ram)}</span> : null}
{typeof ram === "number" && ram > 0 ? <span className="ml-1">{formatBytes(ram)}</span> : null}
{vendor ? <span className="ml-1 text-muted-foreground">· {vendor}</span> : null}
{driver ? <span className="ml-1">· Driver {driver}</span> : null}
</li>
)
@ -1346,7 +1444,7 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
</div>
) : null}
{Array.isArray(windowsExt.disks) && windowsExt.disks.length > 0 ? (
{windowsDiskEntries.length > 0 ? (
<div className="rounded-md border border-slate-200 bg-slate-50/60 p-3">
<p className="text-xs font-semibold uppercase text-slate-500">Discos físicos</p>
<div className="mt-2 overflow-hidden rounded-md border border-slate-200">
@ -1361,15 +1459,25 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
</TableRow>
</TableHeader>
<TableBody>
{(windowsExt.disks as Array<any>).map((d, idx) => (
<TableRow key={`diskp-${idx}`} className="border-slate-100">
<TableCell className="text-sm">{d?.Model ?? "—"}</TableCell>
<TableCell className="text-sm text-muted-foreground">{formatBytes(Number(d?.Size ?? 0))}</TableCell>
<TableCell className="text-sm text-muted-foreground">{d?.InterfaceType ?? "—"}</TableCell>
<TableCell className="text-sm text-muted-foreground">{d?.MediaType ?? "—"}</TableCell>
<TableCell className="text-sm text-muted-foreground">{d?.SerialNumber ?? "—"}</TableCell>
</TableRow>
))}
{windowsDiskEntries.map((disk, idx) => {
const record = toRecord(disk) ?? {}
const model = readString(record, "Model", "model")
const serial = readString(record, "SerialNumber", "serialNumber")
const size = parseBytesLike(record["Size"])
const iface = readString(record, "InterfaceType", "interfaceType")
const media = readString(record, "MediaType", "mediaType")
return (
<TableRow key={`diskp-${idx}`} className="border-slate-100">
<TableCell className="text-sm">{model ?? serial ?? "—"}</TableCell>
<TableCell className="text-sm text-muted-foreground">
{typeof size === "number" && size > 0 ? formatBytes(size) : "—"}
</TableCell>
<TableCell className="text-sm text-muted-foreground">{iface ?? "—"}</TableCell>
<TableCell className="text-sm text-muted-foreground">{media ?? "—"}</TableCell>
<TableCell className="text-sm text-muted-foreground">{serial ?? "—"}</TableCell>
</TableRow>
)
})}
</TableBody>
</Table>
</div>
@ -1717,17 +1825,6 @@ function MachineCard({ machine, companyName }: { machine: MachinesQueryItem; com
)
}
function parseNumberLike(value: unknown): number | null {
if (typeof value === "number" && Number.isFinite(value)) return value
if (typeof value === "string") {
const trimmed = value.trim()
if (!trimmed) return null
const numeric = Number(trimmed.replace(/[^0-9.]/g, ""))
if (Number.isFinite(numeric)) return numeric
}
return null
}
function DetailLine({ label, value, classNameValue }: DetailLineProps) {
if (value === null || value === undefined) return null
if (typeof value === "string" && (value.trim() === "" || value === "undefined" || value === "null")) {