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

@ -1,6 +1,7 @@
import { NextResponse } from "next/server" import { NextResponse } from "next/server"
import { api } from "@/convex/_generated/api"
import type { Id } from "@/convex/_generated/dataModel" import type { Id } from "@/convex/_generated/dataModel"
import type { UserRole } from "@prisma/client"
import { api } from "@/convex/_generated/api"
import { ConvexHttpClient } from "convex/browser" import { ConvexHttpClient } from "convex/browser"
import { prisma } from "@/lib/prisma" import { prisma } from "@/lib/prisma"
import { DEFAULT_TENANT_ID } from "@/lib/constants" import { DEFAULT_TENANT_ID } from "@/lib/constants"
@ -12,12 +13,11 @@ function normalizeRole(input: string | null | undefined): RoleOption {
return ((ROLE_OPTIONS as readonly string[]).includes(candidate) ? candidate : "agent") as RoleOption return ((ROLE_OPTIONS as readonly string[]).includes(candidate) ? candidate : "agent") as RoleOption
} }
function mapToUserRole(role: RoleOption) { const USER_ROLE_OPTIONS: ReadonlyArray<UserRole> = ["ADMIN", "MANAGER", "AGENT", "COLLABORATOR"]
const value = role.toUpperCase()
if (["ADMIN", "MANAGER", "AGENT", "COLLABORATOR"].includes(value)) { function mapToUserRole(role: RoleOption): UserRole {
return value const candidate = role.toUpperCase() as UserRole
} return USER_ROLE_OPTIONS.includes(candidate) ? candidate : "AGENT"
return "AGENT"
} }
export const runtime = "nodejs" export const runtime = "nodejs"

View file

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

View file

@ -199,22 +199,97 @@ function readNumber(record: Record<string, unknown>, ...keys: string[]): number
return raw return raw
} }
if (typeof raw === "string") { if (typeof raw === "string") {
const parsed = Number(raw) const trimmed = raw.trim()
if (!Number.isNaN(parsed)) { if (!trimmed) continue
return parsed 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 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 { function normalizeGpuSource(value: unknown): GpuAdapter | null {
if (typeof value === "string") {
const name = value.trim()
return name ? { name } : null
}
const record = toRecord(value) const record = toRecord(value)
if (!record) return null if (!record) return null
const name = readString(record, "name", "Name") const name = readString(record, "name", "Name", "_name", "AdapterCompatibility")
const vendor = readString(record, "vendor", "Vendor", "PNPDeviceID") const vendor = deriveVendor(record)
const driver = readString(record, "driver", "DriverVersion") const driver = readString(record, "driver", "DriverVersion", "driverVersion")
const memoryBytes = readNumber(record, "memoryBytes", "MemoryBytes", "AdapterRAM") const memoryBytes =
readNumber(record, "memoryBytes", "MemoryBytes", "AdapterRAM", "VRAM", "vramBytes") ??
parseBytesLike(record["AdapterRAM"] ?? record["VRAM"] ?? record["vram"])
if (!name && !vendor && !driver && memoryBytes === undefined) { if (!name && !vendor && !driver && memoryBytes === undefined) {
return null return null
} }
@ -399,7 +474,7 @@ export function AdminMachinesOverview({ tenantId }: { tenantId: string }) {
.toLowerCase() .toLowerCase()
return hay.includes(text) return hay.includes(text)
}) })
}, [machines, q, statusFilter, osFilter, companyQuery, onlyAlerts]) }, [machines, q, statusFilter, osFilter, companyQuery, onlyAlerts, companyNameBySlug])
return ( return (
<div className="grid gap-6"> <div className="grid gap-6">
@ -556,6 +631,9 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
const linuxExt = extended?.linux ?? null const linuxExt = extended?.linux ?? null
const windowsExt = extended?.windows ?? null const windowsExt = extended?.windows ?? null
const macosExt = extended?.macos ?? 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 linuxLsblk = linuxExt?.lsblk ?? []
const linuxSmartEntries = linuxExt?.smart ?? [] const linuxSmartEntries = linuxExt?.smart ?? []
const normalizedHardwareGpus = Array.isArray(hardware?.gpus) const normalizedHardwareGpus = Array.isArray(hardware?.gpus)
@ -563,24 +641,11 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
: [] : []
const hardwarePrimaryGpu = hardware?.primaryGpu ? normalizeGpuSource(hardware.primaryGpu) : null const hardwarePrimaryGpu = hardware?.primaryGpu ? normalizeGpuSource(hardware.primaryGpu) : null
type WinCpuInfo = { const windowsCpuRaw = windowsExt?.cpu
Name?: string const winCpu = windowsCpuRaw
Manufacturer?: string ? (Array.isArray(windowsCpuRaw) ? windowsCpuRaw[0] ?? null : windowsCpuRaw)
SocketDesignation?: string : null
NumberOfCores?: number const winMemTotal = windowsMemoryModules.reduce((acc, module) => acc + (parseBytesLike(module?.Capacity) ?? 0), 0)
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 normalizedWindowsGpus = windowsVideoControllers const normalizedWindowsGpus = windowsVideoControllers
.map((controller) => normalizeGpuSource(controller)) .map((controller) => normalizeGpuSource(controller))
.filter((gpu): gpu is GpuAdapter => Boolean(gpu)) .filter((gpu): gpu is GpuAdapter => Boolean(gpu))
@ -592,21 +657,24 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
], ],
(gpu) => `${gpu.name ?? ""}|${gpu.vendor ?? ""}|${gpu.driver ?? ""}` (gpu) => `${gpu.name ?? ""}|${gpu.vendor ?? ""}|${gpu.driver ?? ""}`
) )
const primaryGpu = combinedGpus[0] ?? null const displayGpus = [...combinedGpus].sort(
const displayGpus = combinedGpus (a, b) => (b.memoryBytes ?? 0) - (a.memoryBytes ?? 0)
const windowsPrimaryGpu = normalizedWindowsGpus[0] ?? null )
const windowsCpuDetails = windowsExt?.cpu const primaryGpu = hardwarePrimaryGpu ?? displayGpus[0] ?? null
? Array.isArray(windowsExt.cpu) const windowsPrimaryGpu = [...normalizedWindowsGpus].sort(
? windowsExt.cpu (a, b) => (b.memoryBytes ?? 0) - (a.memoryBytes ?? 0)
: [windowsExt.cpu] )[0] ?? null
const windowsCpuDetails = windowsCpuRaw
? Array.isArray(windowsCpuRaw)
? windowsCpuRaw
: [windowsCpuRaw]
: [] : []
const windowsServices = windowsExt?.services ?? [] const windowsServices = windowsExt?.services ?? []
const windowsSoftware = windowsExt?.software ?? [] const windowsSoftware = windowsExt?.software ?? []
const windowsDisks = windowsExt?.disks ?? [] const winDiskStats = windowsDiskEntries.length > 0
const winDiskStats = windowsDisks.length > 0
? { ? {
count: windowsDisks.length, count: windowsDiskEntries.length,
total: windowsDisks.reduce((acc, d) => acc + Number(d?.Size ?? 0), 0), total: windowsDiskEntries.reduce((acc, disk) => acc + (parseBytesLike(disk?.Size) ?? 0), 0),
} }
: { count: 0, total: 0 } : { count: 0, total: 0 }
@ -1295,7 +1363,7 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
</div> </div>
) : null} ) : 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"> <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> <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"> <div className="mt-2 overflow-hidden rounded-md border border-slate-200">
@ -1310,34 +1378,64 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{(windowsExt.memoryModules as Array<any>).map((m, idx) => ( {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"> <TableRow key={`mem-${idx}`} className="border-slate-100">
<TableCell className="text-sm">{m?.BankLabel ?? "—"}</TableCell> <TableCell className="text-sm">{bank}</TableCell>
<TableCell className="text-sm text-muted-foreground">{formatBytes(Number(m?.Capacity ?? 0))}</TableCell> <TableCell className="text-sm text-muted-foreground">
<TableCell className="text-sm text-muted-foreground">{m?.Manufacturer ?? "—"}</TableCell> {capacityBytes > 0 ? formatBytes(capacityBytes) : "—"}
<TableCell className="text-sm text-muted-foreground">{m?.PartNumber ?? "—"}</TableCell> </TableCell>
<TableCell className="text-sm text-muted-foreground">{m?.ConfiguredClockSpeed ?? m?.Speed ? `${m?.ConfiguredClockSpeed ?? m?.Speed} MHz` : "—"}</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> </TableRow>
))} )
})}
</TableBody> </TableBody>
</Table> </Table>
</div> </div>
</div> </div>
) : null} ) : 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"> <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> <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"> <ul className="mt-2 grid gap-1 text-xs text-muted-foreground">
{(windowsExt.videoControllers as Array<unknown>).map((vRaw, idx) => { {windowsVideoControllers.map((controller, idx) => {
const v = (vRaw && typeof vRaw === "object") ? (vRaw as Record<string, unknown>) : undefined const record = toRecord(controller) ?? {}
const name = typeof v?.["Name"] === "string" ? (v["Name"] as string) : "—" const normalized = normalizeGpuSource(record)
const ram = typeof v?.["AdapterRAM"] === "number" ? (v["AdapterRAM"] as number) : undefined const name =
const driver = typeof v?.["DriverVersion"] === "string" ? (v["DriverVersion"] as string) : undefined 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 ( return (
<li key={`vid-${idx}`}> <li key={`vid-${idx}`}>
<span className="font-medium text-foreground">{name}</span> <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} {driver ? <span className="ml-1">· Driver {driver}</span> : null}
</li> </li>
) )
@ -1346,7 +1444,7 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
</div> </div>
) : null} ) : 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"> <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> <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"> <div className="mt-2 overflow-hidden rounded-md border border-slate-200">
@ -1361,15 +1459,25 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{(windowsExt.disks as Array<any>).map((d, idx) => ( {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"> <TableRow key={`diskp-${idx}`} className="border-slate-100">
<TableCell className="text-sm">{d?.Model ?? "—"}</TableCell> <TableCell className="text-sm">{model ?? serial ?? "—"}</TableCell>
<TableCell className="text-sm text-muted-foreground">{formatBytes(Number(d?.Size ?? 0))}</TableCell> <TableCell className="text-sm text-muted-foreground">
<TableCell className="text-sm text-muted-foreground">{d?.InterfaceType ?? "—"}</TableCell> {typeof size === "number" && size > 0 ? formatBytes(size) : "—"}
<TableCell className="text-sm text-muted-foreground">{d?.MediaType ?? "—"}</TableCell> </TableCell>
<TableCell className="text-sm text-muted-foreground">{d?.SerialNumber ?? "—"}</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> </TableRow>
))} )
})}
</TableBody> </TableBody>
</Table> </Table>
</div> </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) { function DetailLine({ label, value, classNameValue }: DetailLineProps) {
if (value === null || value === undefined) return null if (value === null || value === undefined) return null
if (typeof value === "string" && (value.trim() === "" || value === "undefined" || value === "null")) { if (typeof value === "string" && (value.trim() === "" || value === "undefined" || value === "null")) {