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:
parent
129407dbce
commit
b5fbf69cc1
2 changed files with 101 additions and 13 deletions
65
src/app/api/admin/machines/rename/route.ts
Normal file
65
src/app/api/admin/machines/rename/route.ts
Normal 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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue