diff --git a/src/app/admin/machines/[id]/page.tsx b/src/app/admin/machines/[id]/page.tsx index 7799167..e72806d 100644 --- a/src/app/admin/machines/[id]/page.tsx +++ b/src/app/admin/machines/[id]/page.tsx @@ -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" diff --git a/src/app/api/admin/users/[id]/route.ts b/src/app/api/admin/users/[id]/route.ts index ef823bb..356e1ad 100644 --- a/src/app/api/admin/users/[id]/route.ts +++ b/src/app/api/admin/users/[id]/route.ts @@ -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 }) } diff --git a/src/components/admin/admin-users-manager.tsx b/src/components/admin/admin-users-manager.tsx index cd7c81c..910514c 100644 --- a/src/components/admin/admin-users-manager.tsx +++ b/src/components/admin/admin-users-manager.tsx @@ -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" diff --git a/src/components/admin/machines/admin-machine-details.client.tsx b/src/components/admin/machines/admin-machine-details.client.tsx index ba5c0af..85e36a4 100644 --- a/src/components/admin/machines/admin-machine-details.client.tsx +++ b/src/components/admin/machines/admin-machine-details.client.tsx @@ -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 ( @@ -25,4 +27,3 @@ export function AdminMachineDetailsClient({ tenantId, machineId }: { tenantId: s return } - diff --git a/src/components/admin/machines/admin-machines-overview.tsx b/src/components/admin/machines/admin-machines-overview.tsx index 4eddae2..413fb3f 100644 --- a/src/components/admin/machines/admin-machines-overview.tsx +++ b/src/components/admin/machines/admin-machines-overview.tsx @@ -43,48 +43,102 @@ 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> + smart?: LinuxSmartEntry[] +} + +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 + PartNumber?: string + SerialNumber?: string + ConfiguredClockSpeed?: number + Speed?: number + ConfiguredVoltage?: number +} + +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 + DisplayVersion?: string + ReleaseId?: string + EditionID?: string + LicenseStatus?: number + IsActivated?: boolean } type WindowsExtended = { - software?: Array> - services?: Array> + software?: MachineSoftware[] + services?: Array<{ name?: string; status?: string; displayName?: string }> defender?: Record hotfix?: Array> - cpu?: Record | Array> + cpu?: WindowsCpuInfo | WindowsCpuInfo[] baseboard?: Record | Array> bios?: Record | Array> - 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 }> - osInfo?: { - ProductName?: string - CurrentBuild?: string | number - CurrentBuildNumber?: string | number - DisplayVersion?: string - ReleaseId?: string - EditionID?: string - LicenseStatus?: number - IsActivated?: boolean - } + 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 | null { + if (!value || typeof value !== "object") return null + return value as Record +} + +function readString(record: Record, ...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, ...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(items: T[], keyFn: (item: T) => string): T[] { + const seen = new Set() + 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>) ?? []) + 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 | 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)[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).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) { {windowsExt?.osInfo ? ( - Build: {String((windowsExt.osInfo as any)?.CurrentBuildNumber ?? (windowsExt.osInfo as any)?.CurrentBuild ?? "—")} + Build: {String(windowsExt.osInfo?.CurrentBuildNumber ?? windowsExt.osInfo?.CurrentBuild ?? "—")} ) : null} {windowsExt?.osInfo ? ( - Ativado: {((windowsExt.osInfo as any)?.IsActivated === true) ? "Sim" : "Não"} + Ativado: {windowsExt.osInfo?.IsActivated === true ? "Sim" : "Não"} + + ) : null} + {primaryGpu?.name ? ( + + GPU: {primaryGpu.name} + {typeof primaryGpu.memoryBytes === "number" ? ` · ${formatBytes(primaryGpu.memoryBytes)}` : ""} ) : null} {companyName ? ( @@ -816,26 +957,12 @@ export function MachineDetails({ machine }: MachineDetailsProps) { value={`${hardware.physicalCores ?? "?"} físicos / ${hardware.logicalCores ?? "?"} lógicos`} /> - {hardwareGpus.length > 0 ? ( + {displayGpus.length > 0 ? (

GPUs

    - {hardwareGpus.slice(0, 3).map((gpu, idx) => { - const gpuObj = gpu as Record - 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 (
  • {name ?? "Adaptador de vídeo"} @@ -845,8 +972,8 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
  • ) })} - {hardwareGpus.length > 3 ? ( -
  • +{hardwareGpus.length - 3} adaptadores adicionais
  • + {displayGpus.length > 3 ? ( +
  • +{displayGpus.length - 3} adaptadores adicionais
  • ) : null}
@@ -855,7 +982,7 @@ export function MachineDetails({ machine }: MachineDetailsProps) { ) : null} - {Array.isArray(network) ? ( + {networkInterfaces ? (

Rede (interfaces)

@@ -868,7 +995,7 @@ export function MachineDetails({ machine }: MachineDetailsProps) { - {(network as Array<{ name?: string; mac?: string; ip?: string }>).map((iface, idx) => ( + {networkInterfaces.map((iface, idx) => ( {iface?.name ?? "—"} {iface?.mac ?? "—"} @@ -879,17 +1006,17 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
- ) : network ? ( + ) : networkSummary ? (

Rede

- - + + @@ -958,7 +1085,7 @@ export function MachineDetails({ machine }: MachineDetailsProps) { {/* Linux */} {linuxExt ? (
- {Array.isArray((linuxExt as any).lsblk) && (linuxExt as any).lsblk.length > 0 ? ( + {linuxLsblk.length > 0 ? (

Montagens (lsblk)

@@ -972,11 +1099,11 @@ export function MachineDetails({ machine }: MachineDetailsProps) { - {((linuxExt as any).lsblk as Array>).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 ( {name} @@ -991,19 +1118,30 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
) : null} - {Array.isArray(linuxExt.smart) && linuxExt.smart.length > 0 ? ( + {linuxSmartEntries.length > 0 ? (

SMART

- {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 ( -
- {model} ({serial}) - {ok ? "OK" : "ALERTA"} +
+ + {model} ({serial}) + + + {ok ? "OK" : "ALERTA"} +
) })} @@ -1054,7 +1192,7 @@ export function MachineDetails({ machine }: MachineDetailsProps) {

GPU

-

{winGpu?.Name ?? "—"}

+

{(windowsPrimaryGpu ?? primaryGpu)?.name ?? "—"}

@@ -1068,23 +1206,21 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
- {windowsExt.cpu ? ( + {windowsCpuDetails.length > 0 ? (

CPU

- {Array.isArray(windowsExt.cpu) ? ( - (windowsExt.cpu as Array>).slice(0,1).map((c, i) => ( -
- - - - - - - - -
- )) - ) : null} + {windowsCpuDetails.slice(0, 1).map((cpuRecord, idx) => ( +
+ + + + + + + + +
+ ))}
) : null} @@ -1106,7 +1242,7 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
) : null} - {Array.isArray(windowsExt.services) ? ( + {windowsServices.length > 0 ? (

Serviços

@@ -1119,30 +1255,42 @@ export function MachineDetails({ machine }: MachineDetailsProps) { - {(windowsExt.services as Array<{ Name?: string; DisplayName?: string; Status?: string }>).slice(0, 10).map((svc, i: number) => ( - - {svc?.Name ?? "—"} - {svc?.DisplayName ?? "—"} - {svc?.Status ?? "—"} - - ))} + {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 ( + + {name} + {displayName} + {status} + + ) + })}
) : null} - {Array.isArray(windowsExt.software) ? ( + {windowsSoftware.length > 0 ? (

Softwares (amostra)

    - {(windowsExt.software as Array<{ DisplayName?: string; name?: string; DisplayVersion?: string; Publisher?: string }>).slice(0, 8).map((s, i: number) => ( -
  • - {s?.DisplayName ?? s?.name ?? "—"} - {s?.DisplayVersion ? {s.DisplayVersion} : null} - {s?.Publisher ? · {s.Publisher} : 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 ( +
  • + {name} + {version ? {version} : null} + {publisher ? · {publisher} : null} +
  • + ) + })}
) : 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 ( -
-
-

CPU

-

{formatPercent(cpu)}

-
-
-

Memória

-

{formatBytes(memory)}

-
-
-

Disco

-

- {Number.isNaN(disk) ? "—" : `${formatPercent(disk)}`} -

-
+
+ {cards.map((card) => ( +
+

{card.label}

+

{card.value}

+
+ ))}
) } diff --git a/src/components/app-sidebar.tsx b/src/components/app-sidebar.tsx index 68ab51d..cc2dce2 100644 --- a/src/components/app-sidebar.tsx +++ b/src/components/app-sidebar.tsx @@ -118,6 +118,15 @@ export function AppSidebar({ ...props }: React.ComponentProps) { 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() navigation.navMain.forEach((group) => { @@ -160,13 +169,6 @@ export function AppSidebar({ ...props }: React.ComponentProps) { 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)