fix: improve admin machine details and role gating
This commit is contained in:
parent
076c0df7f9
commit
42611df0f5
6 changed files with 311 additions and 162 deletions
|
|
@ -1,6 +1,4 @@
|
|||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import { use } from "react"
|
||||
import { AppShell } from "@/components/app-shell"
|
||||
import { SiteHeader } from "@/components/site-header"
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ export async function PATCH(request: Request, { params }: { params: Promise<{ id
|
|||
return NextResponse.json({ error: "Informe um e-mail válido" }, { status: 400 })
|
||||
}
|
||||
|
||||
if (nextRole === "machine") {
|
||||
if ((user.role ?? "").toLowerCase() === "machine") {
|
||||
return NextResponse.json({ error: "Ajustes de máquinas devem ser feitos em Admin ▸ Máquinas" }, { status: 400 })
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -78,11 +78,6 @@ function formatRole(role: string) {
|
|||
return ROLE_LABELS[key] ?? role
|
||||
}
|
||||
|
||||
function normalizeRoleValue(role: string | null | undefined): RoleOption {
|
||||
const candidate = (role ?? "agent").toLowerCase() as RoleOption
|
||||
return ((ROLE_OPTIONS as readonly string[]).includes(candidate) ? candidate : "agent") as RoleOption
|
||||
}
|
||||
|
||||
function formatTenantLabel(tenantId: string, defaultTenantId: string) {
|
||||
if (!tenantId) return "Principal"
|
||||
if (tenantId === defaultTenantId) return "Principal"
|
||||
|
|
|
|||
|
|
@ -8,10 +8,12 @@ import { Card, CardContent } from "@/components/ui/card"
|
|||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
|
||||
export function AdminMachineDetailsClient({ tenantId, machineId }: { tenantId: string; machineId: string }) {
|
||||
const list = (useQuery(api.machines.listByTenant, { tenantId, includeMetadata: true }) ?? []) as MachinesQueryItem[]
|
||||
const machine = useMemo(() => list.find((m) => m.id === machineId) ?? null, [list, machineId])
|
||||
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])
|
||||
|
||||
if (!list) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="space-y-3 p-6">
|
||||
|
|
@ -25,4 +27,3 @@ export function AdminMachineDetailsClient({ tenantId, machineId }: { tenantId: s
|
|||
|
||||
return <MachineDetails machine={machine} />
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,27 +43,54 @@ type MachineSoftware = {
|
|||
source?: string
|
||||
}
|
||||
|
||||
// Props type for DetailLine (used below)
|
||||
type DetailLineProps = { label: string; value?: string | number | null; classNameValue?: string }
|
||||
|
||||
type GpuAdapter = {
|
||||
name?: string
|
||||
vendor?: string
|
||||
driver?: string
|
||||
memoryBytes?: number
|
||||
}
|
||||
|
||||
type LinuxLsblkEntry = {
|
||||
name?: string
|
||||
mountPoint?: string
|
||||
mountpoint?: string
|
||||
fs?: string
|
||||
fstype?: string
|
||||
sizeBytes?: number
|
||||
size?: number
|
||||
}
|
||||
|
||||
type LinuxSmartEntry = {
|
||||
smart_status?: { passed?: boolean }
|
||||
model_name?: string
|
||||
model_family?: string
|
||||
serial_number?: string
|
||||
device?: { name?: string }
|
||||
}
|
||||
|
||||
type LinuxExtended = {
|
||||
lsblk?: unknown
|
||||
lsblk?: LinuxLsblkEntry[]
|
||||
lspci?: string
|
||||
lsusb?: string
|
||||
pciList?: Array<{ text: string }>
|
||||
usbList?: Array<{ text: string }>
|
||||
smart?: Array<Record<string, unknown>>
|
||||
smart?: LinuxSmartEntry[]
|
||||
}
|
||||
|
||||
type WindowsExtended = {
|
||||
software?: Array<Record<string, unknown>>
|
||||
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<{
|
||||
type WindowsCpuInfo = {
|
||||
Name?: string
|
||||
Manufacturer?: string
|
||||
SocketDesignation?: string
|
||||
NumberOfCores?: number
|
||||
NumberOfLogicalProcessors?: number
|
||||
L2CacheSize?: number
|
||||
L3CacheSize?: number
|
||||
MaxClockSpeed?: number
|
||||
}
|
||||
|
||||
type WindowsMemoryModule = {
|
||||
BankLabel?: string
|
||||
Capacity?: number
|
||||
Manufacturer?: string
|
||||
|
|
@ -72,10 +99,24 @@ type WindowsExtended = {
|
|||
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 }>
|
||||
osInfo?: {
|
||||
}
|
||||
|
||||
type WindowsVideoController = {
|
||||
Name?: string
|
||||
AdapterRAM?: number
|
||||
DriverVersion?: string
|
||||
PNPDeviceID?: string
|
||||
}
|
||||
|
||||
type WindowsDiskEntry = {
|
||||
Model?: string
|
||||
SerialNumber?: string
|
||||
Size?: number
|
||||
InterfaceType?: string
|
||||
MediaType?: string
|
||||
}
|
||||
|
||||
type WindowsOsInfo = {
|
||||
ProductName?: string
|
||||
CurrentBuild?: string | number
|
||||
CurrentBuildNumber?: string | number
|
||||
|
|
@ -84,7 +125,20 @@ type WindowsExtended = {
|
|||
EditionID?: string
|
||||
LicenseStatus?: number
|
||||
IsActivated?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
type WindowsExtended = {
|
||||
software?: MachineSoftware[]
|
||||
services?: Array<{ name?: string; status?: string; displayName?: string }>
|
||||
defender?: Record<string, unknown>
|
||||
hotfix?: Array<Record<string, unknown>>
|
||||
cpu?: WindowsCpuInfo | WindowsCpuInfo[]
|
||||
baseboard?: Record<string, unknown> | Array<Record<string, unknown>>
|
||||
bios?: Record<string, unknown> | Array<Record<string, unknown>>
|
||||
memoryModules?: WindowsMemoryModule[]
|
||||
videoControllers?: WindowsVideoController[]
|
||||
disks?: WindowsDiskEntry[]
|
||||
osInfo?: WindowsOsInfo
|
||||
}
|
||||
|
||||
type MacExtended = {
|
||||
|
|
@ -93,6 +147,8 @@ type MacExtended = {
|
|||
launchctl?: string
|
||||
}
|
||||
|
||||
type NetworkInterface = { name?: string; mac?: string; ip?: string }
|
||||
|
||||
type MachineInventory = {
|
||||
hardware?: {
|
||||
vendor?: string
|
||||
|
|
@ -103,10 +159,10 @@ type MachineInventory = {
|
|||
logicalCores?: number
|
||||
memoryBytes?: number
|
||||
memory?: number
|
||||
primaryGpu?: { name?: string; memoryBytes?: number; driver?: string; vendor?: string }
|
||||
gpus?: Array<{ name?: string; memoryBytes?: number; driver?: string; vendor?: string }>
|
||||
primaryGpu?: GpuAdapter
|
||||
gpus?: GpuAdapter[]
|
||||
}
|
||||
network?: { primaryIp?: string; publicIp?: string; macAddresses?: string[] } | Array<{ name?: string; mac?: string; ip?: string }>
|
||||
network?: { primaryIp?: string; publicIp?: string; macAddresses?: string[] } | NetworkInterface[]
|
||||
software?: MachineSoftware[]
|
||||
labels?: MachineLabel[]
|
||||
fleet?: {
|
||||
|
|
@ -115,13 +171,69 @@ type MachineInventory = {
|
|||
detailUpdatedAt?: string
|
||||
osqueryVersion?: string
|
||||
}
|
||||
// Dados enviados pelo agente desktop (inventário básico/estendido)
|
||||
disks?: Array<{ name?: string; mountPoint?: string; fs?: string; interface?: string | null; serial?: string | null; totalBytes?: number; availableBytes?: number }>
|
||||
extended?: { linux?: LinuxExtended; windows?: WindowsExtended; macos?: MacExtended }
|
||||
services?: Array<{ name?: string; status?: string; displayName?: string }>
|
||||
collaborator?: { email?: string; name?: string; role?: string }
|
||||
}
|
||||
|
||||
function toRecord(value: unknown): Record<string, unknown> | null {
|
||||
if (!value || typeof value !== "object") return null
|
||||
return value as Record<string, unknown>
|
||||
}
|
||||
|
||||
function readString(record: Record<string, unknown>, ...keys: string[]): string | undefined {
|
||||
for (const key of keys) {
|
||||
const raw = record[key]
|
||||
if (typeof raw === "string" && raw.trim().length > 0) {
|
||||
return raw
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function readNumber(record: Record<string, unknown>, ...keys: string[]): number | undefined {
|
||||
for (const key of keys) {
|
||||
const raw = record[key]
|
||||
if (typeof raw === "number" && Number.isFinite(raw)) {
|
||||
return raw
|
||||
}
|
||||
if (typeof raw === "string") {
|
||||
const parsed = Number(raw)
|
||||
if (!Number.isNaN(parsed)) {
|
||||
return parsed
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function normalizeGpuSource(value: unknown): GpuAdapter | 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")
|
||||
if (!name && !vendor && !driver && memoryBytes === undefined) {
|
||||
return null
|
||||
}
|
||||
return { name, vendor, driver, memoryBytes }
|
||||
}
|
||||
|
||||
function uniqueBy<T>(items: T[], keyFn: (item: T) => string): T[] {
|
||||
const seen = new Set<string>()
|
||||
const result: T[] = []
|
||||
items.forEach((item) => {
|
||||
const key = keyFn(item)
|
||||
if (key && !seen.has(key)) {
|
||||
seen.add(key)
|
||||
result.push(item)
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
export type MachinesQueryItem = {
|
||||
id: string
|
||||
tenantId: string
|
||||
|
|
@ -425,26 +537,31 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
|||
const { convexUserId } = useAuth()
|
||||
const router = useRouter()
|
||||
// Company name lookup (by slug)
|
||||
const companyQueryArgs = convexUserId && machine ? { tenantId: machine.tenantId, viewerId: convexUserId as Id<"users"> } : undefined
|
||||
const companies = useQuery(
|
||||
convexUserId && machine ? api.companies.list : "skip",
|
||||
convexUserId && machine ? { tenantId: machine.tenantId, viewerId: convexUserId as any } : ("skip" as const)
|
||||
api.companies.list,
|
||||
companyQueryArgs ?? ("skip" as const)
|
||||
) as Array<{ id: string; name: string; slug?: string }> | undefined
|
||||
const metadata = machine?.inventory ?? null
|
||||
const metrics = machine?.metrics ?? null
|
||||
const hardware = metadata?.hardware ?? null
|
||||
const hardware = metadata?.hardware
|
||||
const network = metadata?.network ?? null
|
||||
const networkInterfaces = Array.isArray(network) ? network : null
|
||||
const networkSummary = !Array.isArray(network) && network ? network : null
|
||||
const software = metadata?.software ?? null
|
||||
const labels = metadata?.labels ?? null
|
||||
const fleet = metadata?.fleet ?? null
|
||||
const disks = Array.isArray(metadata?.disks) ? metadata?.disks ?? [] : []
|
||||
const disks = Array.isArray(metadata?.disks) ? metadata.disks : []
|
||||
const extended = metadata?.extended ?? null
|
||||
const linuxExt = extended?.linux ?? null
|
||||
const windowsExt = extended?.windows ?? null
|
||||
const macosExt = extended?.macos ?? null
|
||||
const hardwareGpus = Array.isArray((hardware as any)?.gpus)
|
||||
? (((hardware as any)?.gpus as Array<Record<string, unknown>>) ?? [])
|
||||
const linuxLsblk = linuxExt?.lsblk ?? []
|
||||
const linuxSmartEntries = linuxExt?.smart ?? []
|
||||
const normalizedHardwareGpus = Array.isArray(hardware?.gpus)
|
||||
? hardware.gpus.map((gpu) => normalizeGpuSource(gpu)).filter((gpu): gpu is GpuAdapter => Boolean(gpu))
|
||||
: []
|
||||
const primaryGpu = (hardware as any)?.primaryGpu as Record<string, unknown> | undefined
|
||||
const hardwarePrimaryGpu = hardware?.primaryGpu ? normalizeGpuSource(hardware.primaryGpu) : null
|
||||
|
||||
type WinCpuInfo = {
|
||||
Name?: string
|
||||
|
|
@ -456,22 +573,40 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
|||
L3CacheSize?: number
|
||||
MaxClockSpeed?: number
|
||||
}
|
||||
const winCpu = ((): WinCpuInfo | null => {
|
||||
if (!windowsExt?.cpu) return null
|
||||
if (Array.isArray(windowsExt.cpu)) return (windowsExt.cpu[0] as unknown as WinCpuInfo) ?? null
|
||||
return windowsExt.cpu as unknown as WinCpuInfo
|
||||
const winCpu = (() => {
|
||||
const cpuInfo = windowsExt?.cpu
|
||||
if (!cpuInfo) return null
|
||||
return Array.isArray(cpuInfo) ? cpuInfo[0] ?? null : cpuInfo
|
||||
})()
|
||||
const winMemTotal = Array.isArray(windowsExt?.memoryModules)
|
||||
? (windowsExt?.memoryModules as Array<{ Capacity?: number }>).reduce((acc, m) => acc + Number(m?.Capacity ?? 0), 0)
|
||||
: 0
|
||||
type WinVideoController = { Name?: string; AdapterRAM?: number; DriverVersion?: string; PNPDeviceID?: string }
|
||||
const winGpu = Array.isArray(windowsExt?.videoControllers)
|
||||
? ((windowsExt?.videoControllers as Array<unknown>)[0] as WinVideoController | undefined) ?? null
|
||||
: null
|
||||
const winDiskStats = Array.isArray(windowsExt?.disks)
|
||||
const winMemModules = windowsExt?.memoryModules ?? []
|
||||
const winMemTotal = winMemModules.reduce((acc, module) => acc + Number(module?.Capacity ?? 0), 0)
|
||||
const windowsVideoControllers = windowsExt?.videoControllers ?? []
|
||||
const normalizedWindowsGpus = windowsVideoControllers
|
||||
.map((controller) => normalizeGpuSource(controller))
|
||||
.filter((gpu): gpu is GpuAdapter => Boolean(gpu))
|
||||
const combinedGpus = uniqueBy(
|
||||
[
|
||||
...(hardwarePrimaryGpu ? [hardwarePrimaryGpu] : []),
|
||||
...normalizedHardwareGpus,
|
||||
...normalizedWindowsGpus,
|
||||
],
|
||||
(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 windowsServices = windowsExt?.services ?? []
|
||||
const windowsSoftware = windowsExt?.software ?? []
|
||||
const windowsDisks = windowsExt?.disks ?? []
|
||||
const winDiskStats = windowsDisks.length > 0
|
||||
? {
|
||||
count: (windowsExt?.disks as Array<unknown>).length,
|
||||
total: (windowsExt?.disks as Array<{ Size?: number }>).reduce((acc, d) => acc + Number(d?.Size ?? 0), 0),
|
||||
count: windowsDisks.length,
|
||||
total: windowsDisks.reduce((acc, d) => acc + Number(d?.Size ?? 0), 0),
|
||||
}
|
||||
: { count: 0, total: 0 }
|
||||
|
||||
|
|
@ -637,12 +772,18 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
|||
</Badge>
|
||||
{windowsExt?.osInfo ? (
|
||||
<Badge variant="outline" className="h-7 border-slate-300 bg-slate-100 px-3 text-sm font-medium text-slate-700">
|
||||
Build: {String((windowsExt.osInfo as any)?.CurrentBuildNumber ?? (windowsExt.osInfo as any)?.CurrentBuild ?? "—")}
|
||||
Build: {String(windowsExt.osInfo?.CurrentBuildNumber ?? windowsExt.osInfo?.CurrentBuild ?? "—")}
|
||||
</Badge>
|
||||
) : null}
|
||||
{windowsExt?.osInfo ? (
|
||||
<Badge variant="outline" className="h-7 border-slate-300 bg-slate-100 px-3 text-sm font-medium text-slate-700">
|
||||
Ativado: {((windowsExt.osInfo as any)?.IsActivated === true) ? "Sim" : "Não"}
|
||||
Ativado: {windowsExt.osInfo?.IsActivated === true ? "Sim" : "Não"}
|
||||
</Badge>
|
||||
) : null}
|
||||
{primaryGpu?.name ? (
|
||||
<Badge variant="outline" className="h-7 border-slate-300 bg-slate-100 px-3 text-sm font-medium text-slate-700">
|
||||
GPU: {primaryGpu.name}
|
||||
{typeof primaryGpu.memoryBytes === "number" ? ` · ${formatBytes(primaryGpu.memoryBytes)}` : ""}
|
||||
</Badge>
|
||||
) : null}
|
||||
{companyName ? (
|
||||
|
|
@ -816,26 +957,12 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
|||
value={`${hardware.physicalCores ?? "?"} físicos / ${hardware.logicalCores ?? "?"} lógicos`}
|
||||
/>
|
||||
<DetailLine label="Memória" value={formatBytes(Number(hardware.memoryBytes ?? hardware.memory))} />
|
||||
{hardwareGpus.length > 0 ? (
|
||||
{displayGpus.length > 0 ? (
|
||||
<div className="mt-2 space-y-1 text-xs text-muted-foreground">
|
||||
<p className="font-semibold uppercase text-slate-500">GPUs</p>
|
||||
<ul className="space-y-1">
|
||||
{hardwareGpus.slice(0, 3).map((gpu, idx) => {
|
||||
const gpuObj = gpu as Record<string, unknown>
|
||||
const name =
|
||||
typeof gpuObj?.["name"] === "string"
|
||||
? (gpuObj["name"] as string)
|
||||
: typeof gpuObj?.["Name"] === "string"
|
||||
? (gpuObj["Name"] as string)
|
||||
: undefined
|
||||
const memoryBytes = parseNumberLike(gpuObj?.["memoryBytes"] ?? gpuObj?.["AdapterRAM"])
|
||||
const driver =
|
||||
typeof gpuObj?.["driver"] === "string"
|
||||
? (gpuObj["driver"] as string)
|
||||
: typeof gpuObj?.["DriverVersion"] === "string"
|
||||
? (gpuObj["DriverVersion"] as string)
|
||||
: undefined
|
||||
const vendor = typeof gpuObj?.["vendor"] === "string" ? (gpuObj["vendor"] as string) : undefined
|
||||
{displayGpus.slice(0, 3).map((gpu, idx) => {
|
||||
const { name, memoryBytes, driver, vendor } = gpu
|
||||
return (
|
||||
<li key={`gpu-${idx}`}>
|
||||
<span className="font-medium text-foreground">{name ?? "Adaptador de vídeo"}</span>
|
||||
|
|
@ -845,8 +972,8 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
|||
</li>
|
||||
)
|
||||
})}
|
||||
{hardwareGpus.length > 3 ? (
|
||||
<li className="text-muted-foreground">+{hardwareGpus.length - 3} adaptadores adicionais</li>
|
||||
{displayGpus.length > 3 ? (
|
||||
<li className="text-muted-foreground">+{displayGpus.length - 3} adaptadores adicionais</li>
|
||||
) : null}
|
||||
</ul>
|
||||
</div>
|
||||
|
|
@ -855,7 +982,7 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
|||
</div>
|
||||
) : null}
|
||||
|
||||
{Array.isArray(network) ? (
|
||||
{networkInterfaces ? (
|
||||
<div className="rounded-md border border-slate-200 bg-slate-50/60 p-3">
|
||||
<p className="text-xs font-semibold uppercase text-slate-500">Rede (interfaces)</p>
|
||||
<div className="mt-2 overflow-hidden rounded-md border border-slate-200">
|
||||
|
|
@ -868,7 +995,7 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
|||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{(network as Array<{ name?: string; mac?: string; ip?: string }>).map((iface, idx) => (
|
||||
{networkInterfaces.map((iface, idx) => (
|
||||
<TableRow key={`iface-${idx}`} className="border-slate-100">
|
||||
<TableCell className="text-sm">{iface?.name ?? "—"}</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground">{iface?.mac ?? "—"}</TableCell>
|
||||
|
|
@ -879,17 +1006,17 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
|||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
) : network ? (
|
||||
) : networkSummary ? (
|
||||
<div className="rounded-md border border-slate-200 bg-slate-50/60 p-3">
|
||||
<p className="text-xs font-semibold uppercase text-slate-500">Rede</p>
|
||||
<div className="mt-2 grid gap-1">
|
||||
<DetailLine label="IP primário" value={network.primaryIp} />
|
||||
<DetailLine label="IP público" value={network.publicIp} />
|
||||
<DetailLine label="IP primário" value={networkSummary.primaryIp} />
|
||||
<DetailLine label="IP público" value={networkSummary.publicIp} />
|
||||
<DetailLine
|
||||
label="MAC addresses"
|
||||
value={
|
||||
Array.isArray(network.macAddresses)
|
||||
? (network.macAddresses as string[]).join(", ")
|
||||
Array.isArray(networkSummary.macAddresses)
|
||||
? networkSummary.macAddresses.join(", ")
|
||||
: machine?.macAddresses.join(", ")
|
||||
}
|
||||
/>
|
||||
|
|
@ -958,7 +1085,7 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
|||
{/* Linux */}
|
||||
{linuxExt ? (
|
||||
<div className="space-y-3">
|
||||
{Array.isArray((linuxExt as any).lsblk) && (linuxExt as any).lsblk.length > 0 ? (
|
||||
{linuxLsblk.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">Montagens (lsblk)</p>
|
||||
<div className="mt-2 overflow-hidden rounded-md border border-slate-200">
|
||||
|
|
@ -972,11 +1099,11 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
|||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{((linuxExt as any).lsblk as Array<Record<string, unknown>>).slice(0, 18).map((entry, idx) => {
|
||||
const name = typeof entry["name"] === "string" ? (entry["name"] as string) : "—"
|
||||
const mp = typeof entry["mountPoint"] === "string" ? (entry["mountPoint"] as string) : typeof entry["mountpoint"] === "string" ? (entry["mountpoint"] as string) : "—"
|
||||
const fs = typeof entry["fs"] === "string" ? (entry["fs"] as string) : typeof entry["fstype"] === "string" ? (entry["fstype"] as string) : "—"
|
||||
const sizeRaw = typeof entry["sizeBytes"] === "number" ? (entry["sizeBytes"] as number) : typeof entry["size"] === "number" ? (entry["size"] as number) : undefined
|
||||
{linuxLsblk.slice(0, 18).map((entry, idx) => {
|
||||
const name = entry.name ?? "—"
|
||||
const mp = entry.mountPoint ?? entry.mountpoint ?? "—"
|
||||
const fs = entry.fs ?? entry.fstype ?? "—"
|
||||
const sizeRaw = typeof entry.sizeBytes === "number" ? entry.sizeBytes : entry.size
|
||||
return (
|
||||
<TableRow key={`lsblk-${idx}`} className="border-slate-100">
|
||||
<TableCell className="text-sm text-foreground">{name}</TableCell>
|
||||
|
|
@ -991,19 +1118,30 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
|||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{Array.isArray(linuxExt.smart) && linuxExt.smart.length > 0 ? (
|
||||
{linuxSmartEntries.length > 0 ? (
|
||||
<div className="rounded-md border border-slate-200 bg-emerald-50/40 p-3 dark:bg-emerald-900/10">
|
||||
<p className="text-xs font-semibold uppercase text-slate-500">SMART</p>
|
||||
<div className="mt-2 grid gap-2">
|
||||
{linuxExt.smart.map((s: { smart_status?: { passed?: boolean }; model_name?: string; model_family?: string; serial_number?: string; device?: { name?: string } }, idx: number) => {
|
||||
const ok = s?.smart_status?.passed !== false
|
||||
const model = s?.model_name ?? s?.model_family ?? "Disco"
|
||||
const serial = s?.serial_number ?? s?.device?.name ?? "—"
|
||||
{linuxSmartEntries.map((smartEntry, idx) => {
|
||||
const ok = smartEntry.smart_status?.passed !== false
|
||||
const model = smartEntry.model_name ?? smartEntry.model_family ?? "Disco"
|
||||
const serial = smartEntry.serial_number ?? smartEntry.device?.name ?? "—"
|
||||
return (
|
||||
<div key={`smart-${idx}`} className="flex items-center justify-between gap-2 rounded-md border px-3 py-2 text-sm"
|
||||
style={{ borderColor: ok ? "rgba(16,185,129,0.3)" : "rgba(244,63,94,0.35)", backgroundColor: ok ? "rgba(16,185,129,0.08)" : "rgba(244,63,94,0.06)" }}>
|
||||
<span className="font-medium text-foreground">{model} <span className="text-muted-foreground">({serial})</span></span>
|
||||
<Badge className={cn("border", ok ? "border-emerald-500/20 bg-emerald-500/15 text-emerald-700" : "border-rose-500/20 bg-rose-500/15 text-rose-700")}>{ok ? "OK" : "ALERTA"}</Badge>
|
||||
<div
|
||||
key={`smart-${idx}`}
|
||||
className={cn(
|
||||
"flex items-center justify-between gap-2 rounded-md border px-3 py-2 text-sm",
|
||||
ok
|
||||
? "border-emerald-500/20 bg-emerald-500/15 text-emerald-700"
|
||||
: "border-rose-500/20 bg-rose-500/15 text-rose-700"
|
||||
)}
|
||||
>
|
||||
<span className="font-medium text-foreground">
|
||||
{model} <span className="text-muted-foreground">({serial})</span>
|
||||
</span>
|
||||
<Badge variant="outline" className="text-xs uppercase">
|
||||
{ok ? "OK" : "ALERTA"}
|
||||
</Badge>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
|
@ -1054,7 +1192,7 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
|||
<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">{winGpu?.Name ?? "—"}</p>
|
||||
<p className="truncate text-sm font-semibold text-foreground">{(windowsPrimaryGpu ?? primaryGpu)?.name ?? "—"}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
@ -1068,23 +1206,21 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
|||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
{windowsExt.cpu ? (
|
||||
{windowsCpuDetails.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">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"] ?? "—")} classNameValue="break-words" />
|
||||
<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` : "—"} />
|
||||
{windowsCpuDetails.slice(0, 1).map((cpuRecord, idx) => (
|
||||
<div key={`cpu-${idx}`} className="mt-2 grid gap-1 text-sm text-muted-foreground">
|
||||
<DetailLine label="Modelo" value={cpuRecord?.Name ?? "—"} classNameValue="break-words" />
|
||||
<DetailLine label="Fabricante" value={cpuRecord?.Manufacturer ?? "—"} />
|
||||
<DetailLine label="Socket" value={cpuRecord?.SocketDesignation ?? "—"} />
|
||||
<DetailLine label="Núcleos" value={cpuRecord?.NumberOfCores != null ? `${cpuRecord.NumberOfCores}` : "—"} />
|
||||
<DetailLine label="Threads" value={cpuRecord?.NumberOfLogicalProcessors != null ? `${cpuRecord.NumberOfLogicalProcessors}` : "—"} />
|
||||
<DetailLine label="L2" value={cpuRecord?.L2CacheSize != null ? `${cpuRecord.L2CacheSize} KB` : "—"} />
|
||||
<DetailLine label="L3" value={cpuRecord?.L3CacheSize != null ? `${cpuRecord.L3CacheSize} KB` : "—"} />
|
||||
<DetailLine label="Clock máx" value={cpuRecord?.MaxClockSpeed != null ? `${cpuRecord.MaxClockSpeed} MHz` : "—"} />
|
||||
</div>
|
||||
))
|
||||
) : null}
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
|
@ -1106,7 +1242,7 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
|||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{Array.isArray(windowsExt.services) ? (
|
||||
{windowsServices.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">Serviços</p>
|
||||
<div className="mt-2 overflow-hidden rounded-md border border-slate-200">
|
||||
|
|
@ -1119,30 +1255,42 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
|||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{(windowsExt.services as Array<{ Name?: string; DisplayName?: string; Status?: string }>).slice(0, 10).map((svc, i: number) => (
|
||||
<TableRow key={`svc-${i}`} className="border-slate-100">
|
||||
<TableCell className="text-sm">{svc?.Name ?? "—"}</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground">{svc?.DisplayName ?? "—"}</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground">{svc?.Status ?? "—"}</TableCell>
|
||||
{windowsServices.slice(0, 10).map((service, index) => {
|
||||
const record = toRecord(service) ?? {}
|
||||
const name = readString(record, "Name", "name") ?? "—"
|
||||
const displayName = readString(record, "DisplayName", "displayName") ?? "—"
|
||||
const status = readString(record, "Status", "status") ?? "—"
|
||||
return (
|
||||
<TableRow key={`svc-${index}`} className="border-slate-100">
|
||||
<TableCell className="text-sm">{name}</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground">{displayName}</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground">{status}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{Array.isArray(windowsExt.software) ? (
|
||||
{windowsSoftware.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">Softwares (amostra)</p>
|
||||
<ul className="mt-2 grid gap-1 text-xs text-muted-foreground">
|
||||
{(windowsExt.software as Array<{ DisplayName?: string; name?: string; DisplayVersion?: string; Publisher?: string }>).slice(0, 8).map((s, i: number) => (
|
||||
<li key={`sw-${i}`}>
|
||||
<span className="font-medium text-foreground">{s?.DisplayName ?? s?.name ?? "—"}</span>
|
||||
{s?.DisplayVersion ? <span className="ml-1">{s.DisplayVersion}</span> : null}
|
||||
{s?.Publisher ? <span className="ml-1">· {s.Publisher}</span> : null}
|
||||
{windowsSoftware.slice(0, 8).map((softwareItem, index) => {
|
||||
const record = toRecord(softwareItem) ?? {}
|
||||
const name = readString(record, "DisplayName", "name") ?? "—"
|
||||
const version = readString(record, "DisplayVersion", "version")
|
||||
const publisher = readString(record, "Publisher")
|
||||
return (
|
||||
<li key={`sw-${index}`}>
|
||||
<span className="font-medium text-foreground">{name}</span>
|
||||
{version ? <span className="ml-1">{version}</span> : null}
|
||||
{publisher ? <span className="ml-1">· {publisher}</span> : null}
|
||||
</li>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
|
|
@ -1617,23 +1765,28 @@ function MetricsGrid({ metrics }: { metrics: MachineMetrics }) {
|
|||
return NaN
|
||||
})()
|
||||
const disk = Number(data.diskUsage ?? data.disk ?? NaN)
|
||||
const gpuUsage = Number(
|
||||
data.gpuUsage ?? data.gpu ?? data.gpuUsagePercent ?? data.gpu_percent ?? NaN
|
||||
)
|
||||
|
||||
const cards: Array<{ label: string; value: string }> = [
|
||||
{ label: "CPU", value: formatPercent(cpu) },
|
||||
{ label: "Memória", value: formatBytes(memory) },
|
||||
{ label: "Disco", value: Number.isNaN(disk) ? "—" : formatPercent(disk) },
|
||||
]
|
||||
|
||||
if (!Number.isNaN(gpuUsage)) {
|
||||
cards.push({ label: "GPU", value: formatPercent(gpuUsage) })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid gap-2 rounded-md border border-slate-200 bg-slate-50/60 p-3 text-sm text-muted-foreground sm:grid-cols-3">
|
||||
<div>
|
||||
<p className="text-xs uppercase text-slate-500">CPU</p>
|
||||
<p className="text-sm font-semibold text-foreground">{formatPercent(cpu)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs uppercase text-slate-500">Memória</p>
|
||||
<p className="text-sm font-semibold text-foreground">{formatBytes(memory)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs uppercase text-slate-500">Disco</p>
|
||||
<p className="text-sm font-semibold text-foreground">
|
||||
{Number.isNaN(disk) ? "—" : `${formatPercent(disk)}`}
|
||||
</p>
|
||||
<div className="grid gap-2 sm:grid-cols-3 md:grid-cols-4">
|
||||
{cards.map((card) => (
|
||||
<div key={card.label} className="rounded-md border border-slate-200 bg-slate-50/60 p-3 text-sm text-muted-foreground">
|
||||
<p className="text-xs uppercase text-slate-500">{card.label}</p>
|
||||
<p className="text-sm font-semibold text-foreground">{card.value}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,6 +118,15 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|||
const pathname = usePathname()
|
||||
const { session, isLoading, isAdmin, isStaff } = useAuth()
|
||||
const [isHydrated, setIsHydrated] = React.useState(false)
|
||||
const canAccess = React.useCallback(
|
||||
(requiredRole?: NavRoleRequirement) => {
|
||||
if (!requiredRole) return true
|
||||
if (requiredRole === "admin") return isAdmin
|
||||
if (requiredRole === "staff") return isStaff
|
||||
return false
|
||||
},
|
||||
[isAdmin, isStaff]
|
||||
)
|
||||
const initialExpanded = React.useMemo(() => {
|
||||
const open = new Set<string>()
|
||||
navigation.navMain.forEach((group) => {
|
||||
|
|
@ -160,13 +169,6 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|||
return pathname === url || pathname.startsWith(`${url}/`)
|
||||
}
|
||||
|
||||
function canAccess(requiredRole?: NavRoleRequirement) {
|
||||
if (!requiredRole) return true
|
||||
if (requiredRole === "admin") return isAdmin
|
||||
if (requiredRole === "staff") return isStaff
|
||||
return false
|
||||
}
|
||||
|
||||
const toggleExpanded = React.useCallback((title: string) => {
|
||||
setExpanded((prev) => {
|
||||
const next = new Set(prev)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue