Fix GPU inventory typing and user role mapping
This commit is contained in:
parent
42611df0f5
commit
4f812a2e4c
3 changed files with 183 additions and 84 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import { NextResponse } from "next/server"
|
||||
import { api } from "@/convex/_generated/api"
|
||||
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 { prisma } from "@/lib/prisma"
|
||||
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
|
||||
}
|
||||
|
||||
function mapToUserRole(role: RoleOption) {
|
||||
const value = role.toUpperCase()
|
||||
if (["ADMIN", "MANAGER", "AGENT", "COLLABORATOR"].includes(value)) {
|
||||
return value
|
||||
}
|
||||
return "AGENT"
|
||||
const USER_ROLE_OPTIONS: ReadonlyArray<UserRole> = ["ADMIN", "MANAGER", "AGENT", "COLLABORATOR"]
|
||||
|
||||
function mapToUserRole(role: RoleOption): UserRole {
|
||||
const candidate = role.toUpperCase() as UserRole
|
||||
return USER_ROLE_OPTIONS.includes(candidate) ? candidate : "AGENT"
|
||||
}
|
||||
|
||||
export const runtime = "nodejs"
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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) => (
|
||||
{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">{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>
|
||||
<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) => (
|
||||
{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">{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>
|
||||
<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")) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue