refactor(admin/machines): remove all any casts; add typed helpers for metrics/Windows WMI; implement server route for rename to avoid client any; keep strict types

This commit is contained in:
Esdras Renan 2025-10-10 10:30:57 -03:00
parent 129407dbce
commit b5fbf69cc1
2 changed files with 101 additions and 13 deletions

View file

@ -0,0 +1,65 @@
import { NextResponse } from "next/server"
import { z } from "zod"
import { ConvexHttpClient } from "convex/browser"
import { assertAuthenticatedSession } from "@/lib/auth-server"
import { DEFAULT_TENANT_ID } from "@/lib/constants"
import { api } from "@/convex/_generated/api"
export const runtime = "nodejs"
const schema = z.object({
machineId: z.string().min(1),
hostname: z.string().min(2),
})
export async function POST(request: Request) {
const session = await assertAuthenticatedSession()
if (!session) {
return NextResponse.json({ error: "Não autorizado" }, { status: 401 })
}
const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL
if (!convexUrl) {
return NextResponse.json({ error: "Convex não configurado" }, { status: 500 })
}
const payload = await request.json().catch(() => null)
const parsed = schema.safeParse(payload)
if (!parsed.success) {
return NextResponse.json({ error: "Payload inválido", details: parsed.error.flatten() }, { status: 400 })
}
const tenantId = session.user.tenantId ?? DEFAULT_TENANT_ID
try {
// Garante usuário no Convex e obtém seu Id
const convex = new ConvexHttpClient(convexUrl)
const ensured = await convex.mutation(api.users.ensureUser, {
tenantId,
email: session.user.email,
name: session.user.name ?? session.user.email,
avatarUrl: session.user.avatarUrl ?? undefined,
role: session.user.role.toUpperCase(),
})
const actorId = ensured?._id
if (!actorId) {
return NextResponse.json({ error: "Falha ao obter ID do usuário no Convex" }, { status: 500 })
}
// Chamada por string reference (evita depender do tipo gerado imediatamente)
const client = convex as unknown as { mutation: (name: string, args: unknown) => Promise<unknown> }
await client.mutation("machines:rename", {
machineId: parsed.data.machineId,
actorId,
tenantId,
hostname: parsed.data.hostname,
})
return NextResponse.json({ ok: true })
} catch (error) {
console.error("[machines.rename] Falha ao renomear", error)
return NextResponse.json({ error: "Falha ao renomear máquina" }, { status: 500 })
}
}

View file

@ -2,7 +2,6 @@
import { useEffect, useMemo, useState } from "react" import { useEffect, useMemo, useState } from "react"
import { useQuery } from "convex/react" import { useQuery } from "convex/react"
import { useMutation } from "convex/react"
import { format, formatDistanceToNowStrict } from "date-fns" import { format, formatDistanceToNowStrict } from "date-fns"
import { ptBR } from "date-fns/locale" import { ptBR } from "date-fns/locale"
import { toast } from "sonner" import { toast } from "sonner"
@ -368,18 +367,31 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
const windowsExt = extended?.windows ?? null const windowsExt = extended?.windows ?? null
const macosExt = extended?.macos ?? null const macosExt = extended?.macos ?? null
const winCpu = Array.isArray(windowsExt?.cpu) type WinCpuInfo = {
? (windowsExt?.cpu as Array<Record<string, unknown>>)[0] ?? null Name?: string
: (windowsExt?.cpu as Record<string, unknown> | null) Manufacturer?: string
SocketDesignation?: string
NumberOfCores?: number
NumberOfLogicalProcessors?: number
L2CacheSize?: number
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 winMemTotal = Array.isArray(windowsExt?.memoryModules) const winMemTotal = Array.isArray(windowsExt?.memoryModules)
? (windowsExt?.memoryModules as Array<{ Capacity?: number }>).reduce((acc, m) => acc + Number(m?.Capacity ?? 0), 0) ? (windowsExt?.memoryModules as Array<{ Capacity?: number }>).reduce((acc, m) => acc + Number(m?.Capacity ?? 0), 0)
: 0 : 0
type WinVideoController = { Name?: string; AdapterRAM?: number; DriverVersion?: string; PNPDeviceID?: string }
const winGpu = Array.isArray(windowsExt?.videoControllers) const winGpu = Array.isArray(windowsExt?.videoControllers)
? (windowsExt?.videoControllers as Array<Record<string, unknown>>)[0] ?? null ? ((windowsExt?.videoControllers as Array<unknown>)[0] as WinVideoController | undefined) ?? null
: null : null
const winDiskStats = Array.isArray(windowsExt?.disks) const winDiskStats = Array.isArray(windowsExt?.disks)
? { ? {
count: (windowsExt?.disks as Array<Record<string, unknown>>).length, count: (windowsExt?.disks as Array<unknown>).length,
total: (windowsExt?.disks as Array<{ Size?: number }>).reduce((acc, d) => acc + Number(d?.Size ?? 0), 0), total: (windowsExt?.disks as Array<{ Size?: number }>).reduce((acc, d) => acc + Number(d?.Size ?? 0), 0),
} }
: { count: 0, total: 0 } : { count: 0, total: 0 }
@ -398,7 +410,6 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
} }
} }
const renameMachine = useMutation(api.machines.rename as any)
const [renaming, setRenaming] = useState(false) const [renaming, setRenaming] = useState(false)
const [newName, setNewName] = useState<string>(machine?.hostname ?? "") const [newName, setNewName] = useState<string>(machine?.hostname ?? "")
@ -539,14 +550,19 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
<Button variant="outline" onClick={() => setRenaming(false)}>Cancelar</Button> <Button variant="outline" onClick={() => setRenaming(false)}>Cancelar</Button>
<Button <Button
onClick={async () => { onClick={async () => {
if (!machine || !convexUserId) return if (!machine) return
const name = (newName ?? "").trim() const name = (newName ?? "").trim()
if (name.length < 2) { if (name.length < 2) {
toast.error("Informe um nome válido") toast.error("Informe um nome válido")
return return
} }
try { try {
await renameMachine({ machineId: machine.id as Id<"machines">, actorId: convexUserId as Id<"users">, hostname: name }) const res = await fetch("/api/admin/machines/rename", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ machineId: machine.id, hostname: name }),
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
toast.success("Máquina renomeada") toast.success("Máquina renomeada")
setRenaming(false) setRenaming(false)
} catch (err) { } catch (err) {
@ -1124,10 +1140,17 @@ function MachinesGrid({ machines }: { machines: MachinesQueryItem[] }) {
function MachineCard({ machine }: { machine: MachinesQueryItem }) { function MachineCard({ machine }: { machine: MachinesQueryItem }) {
const { className } = getStatusVariant(machine.status) const { className } = getStatusVariant(machine.status)
const lastHeartbeat = machine.lastHeartbeatAt ? new Date(machine.lastHeartbeatAt) : null const lastHeartbeat = machine.lastHeartbeatAt ? new Date(machine.lastHeartbeatAt) : null
const memUsed = Number((machine.metrics as any)?.memoryUsedBytes ?? NaN) type AgentMetrics = {
const memTotal = Number((machine.metrics as any)?.memoryTotalBytes ?? NaN) memoryUsedBytes?: number
const memPct = Number((machine.metrics as any)?.memoryUsedPercent ?? (memUsed && memTotal ? (memUsed / memTotal) * 100 : NaN)) memoryTotalBytes?: number
const cpuPct = Number((machine.metrics as any)?.cpuUsagePercent ?? NaN) memoryUsedPercent?: number
cpuUsagePercent?: number
}
const mm = (machine.metrics ?? null) as unknown as AgentMetrics | null
const memUsed = mm?.memoryUsedBytes ?? NaN
const memTotal = mm?.memoryTotalBytes ?? NaN
const memPct = mm?.memoryUsedPercent ?? (Number.isFinite(memUsed) && Number.isFinite(memTotal) ? (Number(memUsed) / Number(memTotal)) * 100 : NaN)
const cpuPct = mm?.cpuUsagePercent ?? NaN
return ( return (
<Link href={`/admin/machines/${machine.id}`} className="group"> <Link href={`/admin/machines/${machine.id}`} className="group">