feat: dispositivos e ajustes de csat e relatórios

This commit is contained in:
codex-bot 2025-11-03 19:29:50 -03:00
parent 25d2a9b062
commit e0ef66555d
86 changed files with 5811 additions and 992 deletions

View file

@ -10,7 +10,7 @@ export async function GET(_req: NextRequest, ctx: { params: Promise<{ id: string
const client = createConvexClient()
const { id } = await ctx.params
const machineId = id as Id<"machines">
const data = (await client.query(api.machines.getById, { id: machineId, includeMetadata: true })) as unknown
const data = (await client.query(api.devices.getById, { id: machineId, includeMetadata: true })) as unknown
if (!data) return NextResponse.json({ error: "Not found" }, { status: 404 })
return NextResponse.json(data, { status: 200 })
} catch (err) {

View file

@ -36,13 +36,13 @@ export async function GET(_request: Request, context: RouteContext) {
const tenantId = session.user.tenantId ?? DEFAULT_TENANT_ID
try {
const machine = (await client.query(api.machines.getById, {
const machine = (await client.query(api.devices.getById, {
id: machineId,
includeMetadata: true,
})) as MachineInventoryRecord | null
if (!machine || machine.tenantId !== tenantId) {
return NextResponse.json({ error: "Máquina não encontrada" }, { status: 404 })
return NextResponse.json({ error: "Dispositivo não encontrada" }, { status: 404 })
}
const workbook = buildMachinesInventoryWorkbook([machine], {
@ -64,6 +64,6 @@ export async function GET(_request: Request, context: RouteContext) {
})
} catch (error) {
console.error("Failed to export machine inventory", error)
return NextResponse.json({ error: "Falha ao gerar planilha da máquina" }, { status: 500 })
return NextResponse.json({ error: "Falha ao gerar planilha da dispositivo" }, { status: 500 })
}
}

View file

@ -38,7 +38,7 @@ export async function POST(request: Request) {
const client = new ConvexHttpClient(convexUrl)
try {
const machine = (await client.query(api.machines.getContext, {
const machine = (await client.query(api.devices.getContext, {
machineId: parsed.machineId as Id<"machines">,
})) as {
id: string
@ -47,7 +47,7 @@ export async function POST(request: Request) {
} | null
if (!machine) {
return NextResponse.json({ error: "Máquina não encontrada" }, { status: 404 })
return NextResponse.json({ error: "Dispositivo não encontrada" }, { status: 404 })
}
const tenantId = machine.tenantId ?? session.user.tenantId ?? DEFAULT_TENANT_ID
@ -61,7 +61,7 @@ export async function POST(request: Request) {
companyId: machine.companyId ? (machine.companyId as Id<"companies">) : undefined,
})) as { _id?: Id<"users"> } | null
await client.mutation(api.machines.updatePersona, {
await client.mutation(api.devices.updatePersona, {
machineId: parsed.machineId as Id<"machines">,
persona: parsed.persona,
assignedUserId: ensuredUser?._id,
@ -73,6 +73,6 @@ export async function POST(request: Request) {
return NextResponse.json({ ok: true })
} catch (error) {
console.error("[machines.access]", error)
return NextResponse.json({ error: "Falha ao atualizar acesso da máquina" }, { status: 500 })
return NextResponse.json({ error: "Falha ao atualizar acesso da dispositivo" }, { status: 500 })
}
}

View file

@ -28,7 +28,7 @@ vi.mock("@/lib/auth-server", () => ({
assertAuthenticatedSession: assertAuthenticatedSession,
}))
describe("POST /api/admin/machines/delete", () => {
describe("POST /api/admin/devices/delete", () => {
const originalEnv = process.env.NEXT_PUBLIC_CONVEX_URL
let restoreConsole: (() => void) | undefined
@ -65,7 +65,7 @@ describe("POST /api/admin/machines/delete", () => {
it("returns ok when the machine removal succeeds", async () => {
const { POST } = await import("./route")
const response = await POST(
new Request("http://localhost/api/admin/machines/delete", {
new Request("http://localhost/api/admin/devices/delete", {
method: "POST",
body: JSON.stringify({ machineId: "jn_machine" }),
})
@ -81,13 +81,13 @@ describe("POST /api/admin/machines/delete", () => {
it("still succeeds when the Convex machine is already missing", async () => {
mutationMock.mockImplementation(async (_ctx, payload) => {
if (payload && typeof payload === "object" && "machineId" in payload) {
throw new Error("Máquina não encontrada")
throw new Error("Dispositivo não encontrada")
}
return { _id: "user_123" }
})
const { POST } = await import("./route")
const response = await POST(
new Request("http://localhost/api/admin/machines/delete", {
new Request("http://localhost/api/admin/devices/delete", {
method: "POST",
body: JSON.stringify({ machineId: "jn_machine" }),
})
@ -107,14 +107,14 @@ describe("POST /api/admin/machines/delete", () => {
})
const { POST } = await import("./route")
const response = await POST(
new Request("http://localhost/api/admin/machines/delete", {
new Request("http://localhost/api/admin/devices/delete", {
method: "POST",
body: JSON.stringify({ machineId: "jn_machine" }),
})
)
expect(response.status).toBe(500)
await expect(response.json()).resolves.toEqual({ error: "Falha ao remover máquina no Convex" })
await expect(response.json()).resolves.toEqual({ error: "Falha ao remover dispositivo no Convex" })
expect(deleteManyMock).not.toHaveBeenCalled()
})
})

View file

@ -50,17 +50,17 @@ export async function POST(request: Request) {
let machineMissing = false
try {
await convex.mutation(api.machines.remove, {
await convex.mutation(api.devices.remove, {
machineId: parsed.data.machineId as Id<"machines">,
actorId,
})
} catch (error) {
const message = error instanceof Error ? error.message : ""
if (message.includes("Máquina não encontrada")) {
if (message.includes("Dispositivo não encontrada")) {
machineMissing = true
} else {
console.error("[machines.delete] Convex failure", error)
return NextResponse.json({ error: "Falha ao remover máquina no Convex" }, { status: 500 })
return NextResponse.json({ error: "Falha ao remover dispositivo no Convex" }, { status: 500 })
}
}
@ -70,6 +70,6 @@ export async function POST(request: Request) {
return NextResponse.json({ ok: true, machineMissing })
} catch (error) {
console.error("[machines.delete] Falha ao excluir", error)
return NextResponse.json({ error: "Falha ao excluir máquina" }, { status: 500 })
return NextResponse.json({ error: "Falha ao excluir dispositivo" }, { status: 500 })
}
}

View file

@ -27,7 +27,7 @@ export async function POST(request: Request) {
const client = new ConvexHttpClient(convexUrl)
try {
await client.mutation(api.machines.linkUser, {
await client.mutation(api.devices.linkUser, {
machineId: parsed.machineId as Id<"machines">,
email: parsed.email,
})
@ -53,7 +53,7 @@ export async function DELETE(request: Request) {
const client = new ConvexHttpClient(convexUrl)
try {
await client.mutation(api.machines.unlinkUser, {
await client.mutation(api.devices.unlinkUser, {
machineId: parsed.data.machineId as Id<"machines">,
userId: parsed.data.userId as Id<"users">,
})

View file

@ -59,7 +59,7 @@ export async function POST(request: Request) {
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 })
return NextResponse.json({ error: "Falha ao renomear dispositivo" }, { status: 500 })
}
}

View file

@ -54,7 +54,7 @@ export async function POST(request: Request) {
return NextResponse.json({ ok: true, revoked: result?.revoked ?? 0 })
} catch (error) {
console.error("[machines.resetAgent] Falha ao resetar agente", error)
return NextResponse.json({ error: "Falha ao resetar agente da máquina" }, { status: 500 })
return NextResponse.json({ error: "Falha ao resetar agente da dispositivo" }, { status: 500 })
}
}

View file

@ -56,6 +56,6 @@ export async function POST(request: Request) {
return NextResponse.json({ ok: true })
} catch (error) {
console.error("[machines.toggleActive] Falha ao atualizar status", error)
return NextResponse.json({ error: "Falha ao atualizar status da máquina" }, { status: 500 })
return NextResponse.json({ error: "Falha ao atualizar status da dispositivo" }, { status: 500 })
}
}

View file

@ -41,7 +41,7 @@ export async function POST(request: Request, { params }: { params: Promise<{ id:
}
if (targetRole === "machine") {
return NextResponse.json({ error: "Contas de máquina não possuem senha web" }, { status: 400 })
return NextResponse.json({ error: "Contas de dispositivo não possuem senha web" }, { status: 400 })
}
const body = (await request.json().catch(() => null)) as { password?: string } | null

View file

@ -158,7 +158,7 @@ export async function PATCH(request: Request, { params }: { params: Promise<{ id
}
if ((user.role ?? "").toLowerCase() === "machine") {
return NextResponse.json({ error: "Ajustes de máquinas devem ser feitos em Admin ▸ Máquinas" }, { status: 400 })
return NextResponse.json({ error: "Ajustes de dispositivos devem ser feitos em Admin ▸ Dispositivos" }, { status: 400 })
}
if (!sessionIsAdmin && !canManageRole(nextRole)) {
@ -356,7 +356,7 @@ export async function DELETE(_: Request, { params }: { params: Promise<{ id: str
}
if (target.role === "machine") {
return NextResponse.json({ error: "Os agentes de máquina devem ser removidos via módulo de máquinas." }, { status: 400 })
return NextResponse.json({ error: "Os agentes de dispositivo devem ser removidos via módulo de dispositivos." }, { status: 400 })
}
if (target.email === session.user.email) {

View file

@ -162,7 +162,7 @@ export async function POST(request: Request) {
const client = new ConvexHttpClient(convexUrl)
try {
const result = await client.mutation(api.machines.upsertInventory, {
const result = await client.mutation(api.devices.upsertInventory, {
provisioningCode: fleetSecret,
hostname,
os: osInfo,

View file

@ -36,7 +36,7 @@ describe("POST /api/machines/heartbeat", () => {
expect(response.status).toBe(200)
const body = await response.json()
expect(body).toEqual({ ok: true })
expect(mutationMock).toHaveBeenCalledWith(api.machines.heartbeat, payload)
expect(mutationMock).toHaveBeenCalledWith(api.devices.heartbeat, payload)
})
it("rejects an invalid payload", async () => {

View file

@ -56,7 +56,7 @@ export async function POST(request: Request) {
}
try {
const response = await client.mutation(api.machines.heartbeat, payload)
const response = await client.mutation(api.devices.heartbeat, payload)
return jsonWithCors(response, 200, origin, CORS_METHODS)
} catch (error) {
console.error("[machines.heartbeat] Falha ao registrar heartbeat", error)

View file

@ -35,7 +35,7 @@ describe("POST /api/machines/inventory", () => {
expect(response.status).toBe(200)
expect(mutationMock).toHaveBeenCalledWith(
api.machines.heartbeat,
api.devices.heartbeat,
expect.objectContaining({
machineToken: "token-123",
hostname: "machine",
@ -67,7 +67,7 @@ describe("POST /api/machines/inventory", () => {
expect(response.status).toBe(200)
expect(mutationMock).toHaveBeenCalledWith(
api.machines.upsertInventory,
api.devices.upsertInventory,
expect.objectContaining({
provisioningCode: "a".repeat(32),
hostname: "machine",

View file

@ -64,11 +64,11 @@ export async function POST(request: Request) {
)
}
// Modo A: com token da máquina (usa heartbeat para juntar inventário)
// Modo A: com token da dispositivo (usa heartbeat para juntar inventário)
const tokenParsed = tokenModeSchema.safeParse(raw)
if (tokenParsed.success) {
try {
const result = await client.mutation(api.machines.heartbeat, {
const result = await client.mutation(api.devices.heartbeat, {
machineToken: tokenParsed.data.machineToken,
hostname: tokenParsed.data.hostname,
os: tokenParsed.data.os,
@ -87,7 +87,7 @@ export async function POST(request: Request) {
const provParsed = provisioningModeSchema.safeParse(raw)
if (provParsed.success) {
try {
const result = await client.mutation(api.machines.upsertInventory, {
const result = await client.mutation(api.devices.upsertInventory, {
provisioningCode: provParsed.data.provisioningCode.trim().toLowerCase(),
hostname: provParsed.data.hostname,
os: provParsed.data.os,

View file

@ -120,7 +120,7 @@ export async function POST(request: Request) {
provisioningCode: companyRecord.provisioningCode,
})
const registration = await client.mutation(api.machines.register, {
const registration = await client.mutation(api.devices.register, {
provisioningCode,
hostname: payload.hostname,
os: payload.os,
@ -138,7 +138,7 @@ export async function POST(request: Request) {
persona,
})
await client.mutation(api.machines.linkAuthAccount, {
await client.mutation(api.devices.linkAuthAccount, {
machineId: registration.machineId as Id<"machines">,
authUserId: account.authUserId,
authEmail: account.authEmail,
@ -165,7 +165,7 @@ export async function POST(request: Request) {
if (persona) {
assignedUserId = ensuredUser?._id
await client.mutation(api.machines.updatePersona, {
await client.mutation(api.devices.updatePersona, {
machineId: registration.machineId as Id<"machines">,
persona,
...(assignedUserId ? { assignedUserId } : {}),
@ -174,13 +174,13 @@ export async function POST(request: Request) {
assignedUserRole: persona === "manager" ? "MANAGER" : "COLLABORATOR",
})
} else {
await client.mutation(api.machines.updatePersona, {
await client.mutation(api.devices.updatePersona, {
machineId: registration.machineId as Id<"machines">,
persona: "",
})
}
} else {
await client.mutation(api.machines.updatePersona, {
await client.mutation(api.devices.updatePersona, {
machineId: registration.machineId as Id<"machines">,
persona: "",
})
@ -211,7 +211,7 @@ export async function POST(request: Request) {
const isCompanyNotFound = msg.includes("empresa não encontrada")
const isConvexError = msg.includes("convexerror")
const status = isInvalidCode ? 401 : isCompanyNotFound ? 404 : isConvexError ? 400 : 500
const payload = { error: "Falha ao provisionar máquina", details }
const payload = { error: "Falha ao provisionar dispositivo", details }
return jsonWithCors(payload, status, origin, CORS_METHODS)
}
}

View file

@ -43,7 +43,7 @@ describe("GET /api/machines/session", () => {
expect(response.status).toBe(403)
const payload = await response.json()
expect(payload).toEqual({ error: "Sessão de máquina não encontrada." })
expect(payload).toEqual({ error: "Sessão de dispositivo não encontrada." })
expect(mockCreateConvexClient).not.toHaveBeenCalled()
})

View file

@ -21,7 +21,7 @@ export const runtime = "nodejs"
export async function GET(request: NextRequest) {
const session = await assertAuthenticatedSession()
if (!session || session.user?.role !== "machine") {
return NextResponse.json({ error: "Sessão de máquina não encontrada." }, { status: 403 })
return NextResponse.json({ error: "Sessão de dispositivo não encontrada." }, { status: 403 })
}
let client
@ -42,23 +42,23 @@ export async function GET(request: NextRequest) {
if (!machineId) {
try {
const lookup = (await client.query(api.machines.findByAuthEmail, {
const lookup = (await client.query(api.devices.findByAuthEmail, {
authEmail: session.user.email.toLowerCase(),
})) as { id: string } | null
if (!lookup?.id) {
return NextResponse.json({ error: "Máquina não vinculada à sessão atual." }, { status: 404 })
return NextResponse.json({ error: "Dispositivo não vinculada à sessão atual." }, { status: 404 })
}
machineId = lookup.id as Id<"machines">
} catch (error) {
console.error("[machines.session] Falha ao localizar máquina por e-mail", error)
return NextResponse.json({ error: "Não foi possível localizar a máquina." }, { status: 500 })
console.error("[machines.session] Falha ao localizar dispositivo por e-mail", error)
return NextResponse.json({ error: "Não foi possível localizar a dispositivo." }, { status: 500 })
}
}
try {
let context = (await client.query(api.machines.getContext, {
let context = (await client.query(api.devices.getContext, {
machineId,
})) as {
id: string
@ -109,7 +109,7 @@ export async function GET(request: NextRequest) {
ensuredAssignedUserRole = ensuredUser.role ?? ensuredAssignedUserRole ?? assignedRole
ensuredPersona = normalizedPersona
await client.mutation(api.machines.updatePersona, {
await client.mutation(api.devices.updatePersona, {
machineId: machineId as Id<"machines">,
persona: normalizedPersona,
assignedUserId: ensuredUser._id as Id<"users">,
@ -118,7 +118,7 @@ export async function GET(request: NextRequest) {
assignedUserRole: (ensuredAssignedUserRole ?? assignedRole).toUpperCase(),
})
context = (await client.query(api.machines.getContext, {
context = (await client.query(api.devices.getContext, {
machineId,
})) as typeof context
@ -172,7 +172,7 @@ export async function GET(request: NextRequest) {
return response
} catch (error) {
console.error("[machines.session] Falha ao obter contexto da máquina", error)
return NextResponse.json({ error: "Falha ao obter contexto da máquina." }, { status: 500 })
console.error("[machines.session] Falha ao obter contexto da dispositivo", error)
return NextResponse.json({ error: "Falha ao obter contexto da dispositivo." }, { status: 500 })
}
}

View file

@ -127,13 +127,13 @@ export async function POST(request: Request) {
} catch (error) {
if (error instanceof MachineInactiveError) {
return jsonWithCors(
{ error: "Máquina desativada. Entre em contato com o suporte da Rever para reativar o acesso." },
{ error: "Dispositivo desativada. Entre em contato com o suporte da Rever para reativar o acesso." },
423,
origin,
CORS_METHODS
)
}
console.error("[machines.sessions] Falha ao criar sessão", error)
return jsonWithCors({ error: "Falha ao autenticar máquina" }, 500, origin, CORS_METHODS)
return jsonWithCors({ error: "Falha ao autenticar dispositivo" }, 500, origin, CORS_METHODS)
}
}

View file

@ -6,6 +6,7 @@ import { env } from "@/lib/env"
import { assertAuthenticatedSession } from "@/lib/auth-server"
import { DEFAULT_TENANT_ID } from "@/lib/constants"
import { buildMachinesInventoryWorkbook, type MachineInventoryRecord } from "@/server/machines/inventory-export"
import type { DeviceInventoryColumnConfig } from "@/lib/device-inventory-columns"
export const runtime = "nodejs"
@ -22,6 +23,31 @@ export async function GET(request: Request) {
const companyId = searchParams.get("companyId") ?? undefined
const machineIdParams = searchParams.getAll("machineId").filter(Boolean)
const machineIdFilter = machineIdParams.length > 0 ? new Set(machineIdParams) : null
const columnsParam = searchParams.get("columns")
let columnConfig: DeviceInventoryColumnConfig[] | undefined
if (columnsParam) {
try {
const parsed = JSON.parse(columnsParam)
if (Array.isArray(parsed)) {
columnConfig = parsed
.map((item) => {
if (typeof item === "string") {
return { key: item }
}
if (item && typeof item === "object" && typeof item.key === "string") {
return {
key: item.key,
label: typeof item.label === "string" && item.label.length > 0 ? item.label : undefined,
}
}
return null
})
.filter((item): item is DeviceInventoryColumnConfig => item !== null)
}
} catch (error) {
console.warn("Invalid columns parameter for machines export", error)
}
}
const client = new ConvexHttpClient(convexUrl)
const tenantId = session.user.tenantId ?? DEFAULT_TENANT_ID
@ -46,7 +72,7 @@ export async function GET(request: Request) {
}
try {
const machines = (await client.query(api.machines.listByTenant, {
const machines = (await client.query(api.devices.listByTenant, {
tenantId,
includeMetadata: true,
})) as MachineInventoryRecord[]
@ -77,6 +103,7 @@ export async function GET(request: Request) {
generatedBy: session.user.name ?? session.user.email,
companyFilterLabel,
generatedAt: new Date(),
columns: columnConfig,
})
const body = new Uint8Array(workbook)