feat(admin): Windows hardware cards (CPU/RAM/GPU/Disks) with Lucide icons; feat(desktop): inventory summary cards; feat(agent/windows): extended hardware collectors (CPU/board/BIOS/memory/video/disks); fix(agent): memory units in bytes

This commit is contained in:
Esdras Renan 2025-10-10 00:01:23 -03:00
parent fcd45ff034
commit c70691bce8
6 changed files with 6200 additions and 126 deletions

View file

@ -5,7 +5,7 @@ import { useQuery } from "convex/react"
import { format, formatDistanceToNowStrict } from "date-fns"
import { ptBR } from "date-fns/locale"
import { toast } from "sonner"
import { ClipboardCopy, ServerCog } from "lucide-react"
import { ClipboardCopy, ServerCog, Cpu, MemoryStick, Monitor, HardDrive } from "lucide-react"
import { api } from "@/convex/_generated/api"
import { Badge } from "@/components/ui/badge"
@ -53,6 +53,21 @@ type WindowsExtended = {
services?: Array<Record<string, unknown>>
defender?: Record<string, unknown>
hotfix?: Array<Record<string, unknown>>
cpu?: Record<string, unknown> | Array<Record<string, unknown>>
baseboard?: Record<string, unknown> | Array<Record<string, unknown>>
bios?: Record<string, unknown> | Array<Record<string, unknown>>
memoryModules?: Array<{
BankLabel?: string
Capacity?: number
Manufacturer?: string
PartNumber?: string
SerialNumber?: string
ConfiguredClockSpeed?: number
Speed?: number
ConfiguredVoltage?: number
}>
videoControllers?: Array<{ Name?: string; AdapterRAM?: number; DriverVersion?: string; PNPDeviceID?: string }>
disks?: Array<{ Model?: string; SerialNumber?: string; Size?: number; InterfaceType?: string; MediaType?: string }>
}
type MacExtended = {
@ -409,6 +424,22 @@ function MachineDetails({ machine }: MachineDetailsProps) {
const windowsExt = extended?.windows ?? null
const macosExt = extended?.macos ?? null
const winCpu = Array.isArray(windowsExt?.cpu)
? (windowsExt?.cpu as Array<Record<string, unknown>>)[0] ?? null
: (windowsExt?.cpu as Record<string, unknown> | null)
const winMemTotal = Array.isArray(windowsExt?.memoryModules)
? (windowsExt?.memoryModules as Array<{ Capacity?: number }>).reduce((acc, m) => acc + Number(m?.Capacity ?? 0), 0)
: 0
const winGpu = Array.isArray(windowsExt?.videoControllers)
? (windowsExt?.videoControllers as Array<Record<string, unknown>>)[0] ?? null
: null
const winDiskStats = Array.isArray(windowsExt?.disks)
? {
count: (windowsExt?.disks as Array<Record<string, unknown>>).length,
total: (windowsExt?.disks as Array<{ Size?: number }>).reduce((acc, d) => acc + Number(d?.Size ?? 0), 0),
}
: { count: 0, total: 0 }
const lastHeartbeatDate = machine?.lastHeartbeatAt ? new Date(machine.lastHeartbeatAt) : null
const tokenExpiry = machine?.token?.expiresAt ? new Date(machine.token.expiresAt) : null
const tokenLastUsed = machine?.token?.lastUsedAt ? new Date(machine.token.lastUsedAt) : null
@ -746,6 +777,83 @@ function MachineDetails({ machine }: MachineDetailsProps) {
{/* Windows */}
{windowsExt ? (
<div className="space-y-3">
{/* Cards resumidos: CPU / RAM / GPU / Discos */}
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
<Card className="border-slate-200">
<CardContent className="flex items-center gap-3 py-3">
<Cpu className="size-5 text-slate-500" />
<div className="min-w-0">
<p className="truncate text-xs text-muted-foreground">CPU</p>
<p className="truncate text-sm font-semibold text-foreground">{String((winCpu as any)?.Name ?? "—")}</p>
</div>
</CardContent>
</Card>
<Card className="border-slate-200">
<CardContent className="flex items-center gap-3 py-3">
<MemoryStick className="size-5 text-slate-500" />
<div>
<p className="text-xs text-muted-foreground">Memória total</p>
<p className="text-sm font-semibold text-foreground">{formatBytes(winMemTotal)}</p>
</div>
</CardContent>
</Card>
<Card className="border-slate-200">
<CardContent className="flex items-center gap-3 py-3">
<Monitor className="size-5 text-slate-500" />
<div className="min-w-0">
<p className="truncate text-xs text-muted-foreground">GPU</p>
<p className="truncate text-sm font-semibold text-foreground">{String((winGpu as any)?.Name ?? "—")}</p>
</div>
</CardContent>
</Card>
<Card className="border-slate-200">
<CardContent className="flex items-center gap-3 py-3">
<HardDrive className="size-5 text-slate-500" />
<div>
<p className="text-xs text-muted-foreground">Discos</p>
<p className="text-sm font-semibold text-foreground">{winDiskStats.count} · {formatBytes(winDiskStats.total)}</p>
</div>
</CardContent>
</Card>
</div>
{windowsExt.cpu ? (
<div className="rounded-md border border-slate-200 bg-slate-50/60 p-3">
<p className="text-xs font-semibold uppercase text-slate-500">CPU</p>
{Array.isArray(windowsExt.cpu) ? (
(windowsExt.cpu as Array<Record<string, unknown>>).slice(0,1).map((c, i) => (
<div key={`cpu-${i}`} className="mt-2 grid gap-1 text-sm text-muted-foreground">
<DetailLine label="Modelo" value={String(c?.["Name"] ?? "—")} />
<DetailLine label="Fabricante" value={String(c?.["Manufacturer"] ?? "—")} />
<DetailLine label="Socket" value={String(c?.["SocketDesignation"] ?? "—")} />
<DetailLine label="Núcleos" value={String(c?.["NumberOfCores"] ?? "—")} />
<DetailLine label="Threads" value={String(c?.["NumberOfLogicalProcessors"] ?? "—")} />
<DetailLine label="L2" value={c?.["L2CacheSize"] ? `${c["L2CacheSize"]} KB` : "—"} />
<DetailLine label="L3" value={c?.["L3CacheSize"] ? `${c["L3CacheSize"]} KB` : "—"} />
<DetailLine label="Clock máx" value={c?.["MaxClockSpeed"] ? `${c["MaxClockSpeed"]} MHz` : "—"} />
</div>
))
) : null}
</div>
) : null}
{windowsExt.baseboard || windowsExt.bios ? (
<div className="rounded-md border border-slate-200 bg-slate-50/60 p-3">
<p className="text-xs font-semibold uppercase text-slate-500">Placa-mãe / BIOS</p>
<div className="mt-2 grid gap-1 text-sm text-muted-foreground">
{(() => {
const b = Array.isArray(windowsExt.baseboard) ? windowsExt.baseboard[0] : windowsExt.baseboard
const bios = Array.isArray(windowsExt.bios) ? windowsExt.bios[0] : windowsExt.bios
return (
<>
<DetailLine label="Board" value={b ? `${String(b?.["Manufacturer"] ?? "")} ${String(b?.["Product"] ?? "")}`.trim() : "—"} />
<DetailLine label="Board SN" value={b ? String(b?.["SerialNumber"] ?? "—") : "—"} />
<DetailLine label="BIOS" value={bios ? `${String(bios?.["Manufacturer"] ?? "")} ${String(bios?.["SMBIOSBIOSVersion"] ?? "")}`.trim() : "—"} />
</>
)
})()}
</div>
</div>
) : null}
{Array.isArray(windowsExt.services) ? (
<div className="rounded-md border border-slate-200 bg-slate-50/60 p-3">
<p className="text-xs font-semibold uppercase text-slate-500">Serviços</p>
@ -787,6 +895,81 @@ function MachineDetails({ machine }: MachineDetailsProps) {
</div>
) : null}
{Array.isArray(windowsExt.memoryModules) && windowsExt.memoryModules.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">
<Table>
<TableHeader>
<TableRow className="border-slate-200 bg-slate-100/80">
<TableHead className="text-xs text-slate-500">Banco</TableHead>
<TableHead className="text-xs text-slate-500">Capacidade</TableHead>
<TableHead className="text-xs text-slate-500">Fabricante</TableHead>
<TableHead className="text-xs text-slate-500">PartNumber</TableHead>
<TableHead className="text-xs text-slate-500">Clock</TableHead>
</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>
))}
</TableBody>
</Table>
</div>
</div>
) : null}
{Array.isArray(windowsExt.videoControllers) && windowsExt.videoControllers.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<any>).map((v, idx) => (
<li key={`vid-${idx}`}>
<span className="font-medium text-foreground">{v?.Name ?? "—"}</span>
{v?.AdapterRAM ? <span className="ml-1">{formatBytes(Number(v.AdapterRAM))}</span> : null}
{v?.DriverVersion ? <span className="ml-1">· Driver {v.DriverVersion}</span> : null}
</li>
))}
</ul>
</div>
) : null}
{Array.isArray(windowsExt.disks) && windowsExt.disks.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">
<Table>
<TableHeader>
<TableRow className="border-slate-200 bg-slate-100/80">
<TableHead className="text-xs text-slate-500">Modelo</TableHead>
<TableHead className="text-xs text-slate-500">Tamanho</TableHead>
<TableHead className="text-xs text-slate-500">Interface</TableHead>
<TableHead className="text-xs text-slate-500">Tipo</TableHead>
<TableHead className="text-xs text-slate-500">Serial</TableHead>
</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>
))}
</TableBody>
</Table>
</div>
</div>
) : null}
{windowsExt.defender ? (
<div className="rounded-md border border-slate-200 bg-slate-50/60 p-3">
<p className="text-xs font-semibold uppercase text-slate-500">Defender</p>