From 25d2a9b062a7d1a8cd154549c7168714a20b5df0 Mon Sep 17 00:00:00 2001 From: codex-bot Date: Mon, 3 Nov 2025 15:16:34 -0300 Subject: [PATCH] feat: add agent reset flow and document machine handover --- README.md | 21 +++++-- convex/machines.ts | 47 +++++++++++++++ docs/OPERATIONS.md | 23 +++++++ .../api/admin/machines/reset-agent/route.ts | 60 +++++++++++++++++++ .../machines/admin-machines-overview.tsx | 39 ++++++++++++ src/components/app-shell.tsx | 14 ++++- 6 files changed, 196 insertions(+), 8 deletions(-) create mode 100644 src/app/api/admin/machines/reset-agent/route.ts diff --git a/README.md b/README.md index bb5f336..18131a3 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ ## Sistema de Chamados -Aplicação Next.js 15 com Convex e Better Auth para gestão de tickets da Rever. Todo o código-fonte está organizado diretamente na raiz do repositório, conforme convenções do Next.js. +Aplicação **Next.js 16 (App Router)** com **React 19**, **Convex** e **Better Auth** para gestão de tickets da Rever. A stack ainda inclui **Prisma 6** (SQLite padrão para DEV), **Tailwind** e **Turbopack** como bundler padrão. Todo o código-fonte fica na raiz do monorepo seguindo as convenções do App Router. ## Requisitos - Node.js >= 20 -- pnpm >= 8 +- pnpm >= 9 (habilite via `corepack prepare pnpm@9 --activate`) - CLI do Convex (`pnpm dlx convex dev` instalará automaticamente no primeiro uso) ## Configuração rápida @@ -34,7 +34,7 @@ Aplicação Next.js 15 com Convex e Better Auth para gestão de tickets da Rever ```bash pnpm convex:dev ``` -7. Em outro terminal, suba o frontend Next.js: +7. Em outro terminal, suba o frontend Next.js (Turbopack): ```bash pnpm dev ``` @@ -44,7 +44,7 @@ Aplicação Next.js 15 com Convex e Better Auth para gestão de tickets da Rever ### Documentação - Índice de docs: `docs/README.md` -- Operações (produção): `docs/operations.md` +- Operações (produção): `docs/OPERATIONS.md` (versão EN) e `docs/OPERACAO-PRODUCAO.md` (PT-BR) - Guia de DEV: `docs/DEV.md` - Testes automatizados (Vitest/Playwright): `docs/testes-vitest.md` - Stack Swarm: `stack.yml` (roteado por Traefik, rede `traefik_public`). @@ -62,11 +62,22 @@ Para fluxos detalhados de desenvolvimento — banco de dados local (SQLite/Prism ## Scripts úteis - `pnpm lint` — ESLint com as regras do projeto. -- `pnpm exec vitest run` — suíte de testes unitários. +- `pnpm test` — suíte de testes unitários (Vitest) em modo não interativo. +- `pnpm build` — `next build --turbopack` com otimizações para produção. - `pnpm auth:seed` — atualiza/cria contas padrão do Better Auth (credenciais em `agents.md`). - `pnpm prisma migrate deploy` — aplica migrações ao banco SQLite local. - `pnpm convex:dev` — roda o Convex em modo desenvolvimento, gerando tipos em `convex/_generated`. +## Transferir máquina entre colaboradores + +Quando uma máquina trocar de responsável: + +1. Abra `Admin > Máquinas`, selecione o equipamento e clique em **Resetar agente**. +2. No equipamento, execute o reset local do agente (`rever-agent reset` ou reinstale o serviço) e reprovisione com o código da empresa. +3. Após o agente gerar um novo token, associe a máquina ao novo colaborador no painel. + +Sem o reset de agente, o Convex reaproveita o token anterior e o inventário continua vinculado ao usuário antigo. + ## Estrutura principal - `app/` dentro de `src/` — rotas e layouts do Next.js (App Router). diff --git a/convex/machines.ts b/convex/machines.ts index d19caec..bb0845f 100644 --- a/convex/machines.ts +++ b/convex/machines.ts @@ -1634,6 +1634,53 @@ export const toggleActive = mutation({ }, }) +export const resetAgent = mutation({ + args: { + machineId: v.id("machines"), + actorId: v.id("users"), + }, + handler: async (ctx, { machineId, actorId }) => { + const machine = await ctx.db.get(machineId) + if (!machine) { + throw new ConvexError("Máquina não encontrada") + } + + const actor = await ctx.db.get(actorId) + if (!actor || actor.tenantId !== machine.tenantId) { + throw new ConvexError("Acesso negado ao tenant da máquina") + } + const normalizedRole = (actor.role ?? "AGENT").toUpperCase() + const STAFF = new Set(["ADMIN", "MANAGER", "AGENT"]) + if (!STAFF.has(normalizedRole)) { + throw new ConvexError("Apenas equipe interna pode resetar o agente da máquina") + } + + const tokens = await ctx.db + .query("machineTokens") + .withIndex("by_machine", (q) => q.eq("machineId", machineId)) + .collect() + + const now = Date.now() + let revokedCount = 0 + for (const token of tokens) { + if (!token.revoked) { + await ctx.db.patch(token._id, { + revoked: true, + expiresAt: now, + }) + revokedCount += 1 + } + } + + await ctx.db.patch(machineId, { + status: "unknown", + updatedAt: now, + }) + + return { machineId, revoked: revokedCount } + }, +}) + type RemoteAccessEntry = { id: string provider: string diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index 144e76b..b278960 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -58,6 +58,29 @@ Este documento consolida as mudanças recentes, o racional por trás delas e o p - “Acquire Convex admin key” (via container `sistema_convex_backend`). - “Bring convex.json from live app if present” (usa o arquivo de link do projeto em `/srv/apps/sistema`). - “convex env list” e “convex deploy” com `CONVEX_SELF_HOSTED_URL` + `CONVEX_SELF_HOSTED_ADMIN_KEY`. + +## 4) Troca de colaborador / reaproveitamento de máquina + +Quando um computador muda de dono (ex.: João entrega o equipamento antigo para Maria e recebe uma máquina nova), siga este checklist para manter o inventário consistente: + +1. **No painel (Admin → Máquinas)** + - Abra os detalhes da máquina que será reaproveitada (ex.: a “amarela” que passará da TI/João para a Maria). + - Clique em **Resetar agente**. Isso revoga todos os tokens gerados para aquele equipamento; ele precisará ser reprovisionado antes de voltar a reportar dados. + - Abra **Ajustar acesso** e altere o e-mail para o do novo usuário (Maria). Assim, quando o agente se registrar novamente, o painel já mostrará a responsável correta. + +2. **Na máquina física que ficará com o novo colaborador** + - Desinstale o desktop agent (Painel de Controle → remover programas). + - Instale novamente o desktop agent. Use o mesmo **código da empresa/tenant** e informe o **e-mail do novo usuário** (Maria). O backend emite um token novo e reaproveita o registro da máquina, mantendo o histórico. + +3. **Máquina nova para o colaborador antigo** + - Instale o desktop agent do zero na máquina que o João vai usar (ex.: a “azul”). Utilize o mesmo código da empresa e o e-mail do João. + - A máquina azul aparecerá como um **novo registro** no painel (inventário/tickets começarão do zero). Renomeie/associe conforme necessário. + +4. **Verificação final** + - A máquina antiga (amarela) continua listada, agora vinculada à Maria, com seus tickets históricos. + - A máquina nova (azul) aparece como um segundo registro para o João. Ajuste hostname/descrição para facilitar a identificação. + +> Não é necessário excluir registros. Cada máquina mantém seu histórico; o reset garante apenas que o token antigo não volte a sobrescrever dados quando o hardware mudar de mãos. - Importante: não usar `CONVEX_DEPLOYMENT` em conjunto com URL + ADMIN_KEY. - Como forçar o deploy do Convex diff --git a/src/app/api/admin/machines/reset-agent/route.ts b/src/app/api/admin/machines/reset-agent/route.ts new file mode 100644 index 0000000..26916e2 --- /dev/null +++ b/src/app/api/admin/machines/reset-agent/route.ts @@ -0,0 +1,60 @@ +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), +}) + +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 { + 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 }) + } + + const client = convex as unknown as { mutation: (name: string, args: unknown) => Promise } + const result = (await client.mutation("machines:resetAgent", { + machineId: parsed.data.machineId, + actorId, + })) as { revoked?: number } | null + + 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 }) + } +} + diff --git a/src/components/admin/machines/admin-machines-overview.tsx b/src/components/admin/machines/admin-machines-overview.tsx index 6f0b448..241d268 100644 --- a/src/components/admin/machines/admin-machines-overview.tsx +++ b/src/components/admin/machines/admin-machines-overview.tsx @@ -2280,6 +2280,7 @@ export function MachineDetails({ machine }: MachineDetailsProps) { [editingRemoteAccessClientId, remoteAccessEntries] ) const [togglingActive, setTogglingActive] = useState(false) + const [isResettingAgent, setIsResettingAgent] = useState(false) const [showAllWindowsSoftware, setShowAllWindowsSoftware] = useState(false) const jsonText = useMemo(() => { const payload = { @@ -2569,6 +2570,34 @@ export function MachineDetails({ machine }: MachineDetailsProps) { } } + const handleResetAgent = useCallback(async () => { + if (!machine) return + toast.dismiss("machine-reset") + toast.loading("Resetando agente...", { id: "machine-reset" }) + setIsResettingAgent(true) + try { + const response = await fetch("/api/admin/machines/reset-agent", { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify({ machineId: machine.id }), + }) + const payload = (await response.json().catch(() => null)) as { error?: string; revoked?: number } | null + if (!response.ok) { + const message = payload?.error ?? "Falha ao resetar agente." + throw new Error(message) + } + const revokedLabel = typeof payload?.revoked === "number" && payload.revoked > 0 ? ` (${payload.revoked} token(s) revogados)` : "" + toast.success(`Agente resetado${revokedLabel}. Reprovisione o agente na máquina.`, { id: "machine-reset" }) + router.refresh() + } catch (error) { + const message = error instanceof Error ? error.message : "Falha ao resetar agente." + toast.error(message, { id: "machine-reset" }) + } finally { + setIsResettingAgent(false) + } + }, [machine, router]) + const handleCopyRemoteIdentifier = useCallback(async (identifier: string | null | undefined) => { if (!identifier) return try { @@ -2702,6 +2731,16 @@ export function MachineDetails({ machine }: MachineDetailsProps) { Ajustar acesso +