diff --git a/src/app/api/admin/users/[id]/route.ts b/src/app/api/admin/users/[id]/route.ts index 356e1ad..b7f5a38 100644 --- a/src/app/api/admin/users/[id]/route.ts +++ b/src/app/api/admin/users/[id]/route.ts @@ -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 = ["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" diff --git a/src/components/admin/machines/admin-machine-details.client.tsx b/src/components/admin/machines/admin-machine-details.client.tsx index 85e36a4..7bb8d53 100644 --- a/src/components/admin/machines/admin-machine-details.client.tsx +++ b/src/components/admin/machines/admin-machine-details.client.tsx @@ -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 ( diff --git a/src/components/admin/machines/admin-machines-overview.tsx b/src/components/admin/machines/admin-machines-overview.tsx index 413fb3f..591d5ee 100644 --- a/src/components/admin/machines/admin-machines-overview.tsx +++ b/src/components/admin/machines/admin-machines-overview.tsx @@ -199,22 +199,97 @@ function readNumber(record: Record, ...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 = { + 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 | 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 = { + "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 (
@@ -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) {
) : null} - {Array.isArray(windowsExt.memoryModules) && windowsExt.memoryModules.length > 0 ? ( + {windowsMemoryModules.length > 0 ? (

Módulos de memória

@@ -1310,34 +1378,64 @@ export function MachineDetails({ machine }: MachineDetailsProps) { - {(windowsExt.memoryModules as Array).map((m, idx) => ( - - {m?.BankLabel ?? "—"} - {formatBytes(Number(m?.Capacity ?? 0))} - {m?.Manufacturer ?? "—"} - {m?.PartNumber ?? "—"} - {m?.ConfiguredClockSpeed ?? m?.Speed ? `${m?.ConfiguredClockSpeed ?? m?.Speed} MHz` : "—"} - - ))} + {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 ( + + {bank} + + {capacityBytes > 0 ? formatBytes(capacityBytes) : "—"} + + {manufacturer} + {partNumber} + {clockLabel} + + ) + })}
) : null} - {Array.isArray(windowsExt.videoControllers) && windowsExt.videoControllers.length > 0 ? ( + {windowsVideoControllers.length > 0 ? (

Adaptadores de vídeo

    - {(windowsExt.videoControllers as Array).map((vRaw, idx) => { - const v = (vRaw && typeof vRaw === "object") ? (vRaw as Record) : 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 (
  • {name} - {typeof ram === "number" ? {formatBytes(ram)} : null} + {typeof ram === "number" && ram > 0 ? {formatBytes(ram)} : null} + {vendor ? · {vendor} : null} {driver ? · Driver {driver} : null}
  • ) @@ -1346,7 +1444,7 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
) : null} - {Array.isArray(windowsExt.disks) && windowsExt.disks.length > 0 ? ( + {windowsDiskEntries.length > 0 ? (

Discos físicos

@@ -1361,15 +1459,25 @@ export function MachineDetails({ machine }: MachineDetailsProps) { - {(windowsExt.disks as Array).map((d, idx) => ( - - {d?.Model ?? "—"} - {formatBytes(Number(d?.Size ?? 0))} - {d?.InterfaceType ?? "—"} - {d?.MediaType ?? "—"} - {d?.SerialNumber ?? "—"} - - ))} + {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 ( + + {model ?? serial ?? "—"} + + {typeof size === "number" && size > 0 ? formatBytes(size) : "—"} + + {iface ?? "—"} + {media ?? "—"} + {serial ?? "—"} + + ) + })}
@@ -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")) {