chore: upgrade next 16.0.8 and tidy local archive

This commit is contained in:
rever-tecnologia 2025-12-10 17:10:52 -03:00
parent 0df1e87f61
commit 821cb7faa7
7 changed files with 270 additions and 45 deletions

View file

@ -1,6 +1,6 @@
## Sistema de Chamados
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** em desenvolvimento (o build de produção roda com o webpack padrão do Next). Todo o código-fonte fica na raiz do monorepo seguindo as convenções do App Router.
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 (webpack permanece disponível como fallback). Todo o código-fonte fica na raiz do monorepo seguindo as convenções do App Router.
## Requisitos
@ -46,7 +46,7 @@ Aplicação **Next.js 16 (App Router)** com **React 19**, **Convex** e **Better
- (Opcional) Para re-sincronizar manualmente as filas padrão, execute `bun run queues:ensure`.
- Em um terminal, rode o backend em tempo real do Convex com `bun run convex:dev:bun` (ou `bun run convex:dev` para o runtime Node).
- Em outro terminal, suba o frontend Next.js (Turpoback) com `bun run dev:bun` (`bun run dev:webpack` serve como fallback).
- Em outro terminal, suba o frontend Next.js (Turbopack) com `bun run dev:bun` (`bun run dev:webpack` serve como fallback).
- Com o Convex rodando, acesse `http://localhost:3000/dev/seed` uma vez para popular dados de demonstração (tickets, usuários, comentários).
> Se o CLI perguntar sobre configuração do projeto Convex, escolha criar um novo deployment local (opção padrão) e confirme. As credenciais são armazenadas em `.convex/` automaticamente.
@ -72,12 +72,12 @@ Para fluxos detalhados de desenvolvimento — banco de dados local (SQLite/Prism
- `bun run dev:bun` — padrão atual para o Next.js com runtime Bun (`bun run dev:webpack` permanece como fallback).
- `bun run convex:dev:bun` — runtime Bun para o Convex (`bun run convex:dev` mantém o fluxo antigo usando Node).
- `bun run build:bun` / `bun run start:bun` — build e serve com Bun; `bun run build` mantém o fallback Node.
- `bun run build:bun` / `bun run start:bun` — build e serve com Bun usando Turbopack (padrão atual).
- `bun run dev:webpack` — fallback do Next.js em modo desenvolvimento (webpack).
- `bun run lint` — ESLint com as regras do projeto.
- `bun test` — suíte de testes unitários usando o runner do Bun (o teste de screenshot fica automaticamente ignorado se o matcher não existir).
- `bun run build` — executa `next build --webpack` (webpack padrão do Next).
- `bun run build:turbopack` — executa `next build --turbopack` para reproduzir/debugar problemas.
- `bun run build` — executa `next build --turbopack` (runtime Node, caso prefira evitar o `--bun`).
- `bun run build:webpack` — executa `next build --webpack` como fallback oficial.
- `bun run auth:seed` — atualiza/cria contas padrão do Better Auth (credenciais em `agents.md`).
- `bunx prisma migrate deploy` — aplica migrações ao banco SQLite local.
- `bun run convex:dev` — roda o Convex em modo desenvolvimento com Node, gerando tipos em `convex/_generated`.
@ -114,7 +114,7 @@ Consulte `PROXIMOS_PASSOS.md` para acompanhar o backlog funcional e o progresso
- `bun install` é o fluxo padrão (o arquivo `bun.lock` deve ser versionado; use `bun install --frozen-lockfile` em CI).
- `bun run dev:bun`, `bun run convex:dev:bun`, `bun run build:bun` e `bun run start:bun` já estão configurados; internamente executam `bun run --bun <script>` para usar o runtime do Bun sem abrir mão dos scripts existentes. O `cross-env` garante os valores esperados de `NODE_ENV` (`development`/`production`).
- Se precisar validar o bundler experimental, use `bun run build:turbopack`; para o fluxo estável mantenha `bun run build` (webpack).
- O bundler padrão é o Turbopack; se precisar comparar/debugar com webpack, use `bun run build:webpack`.
- `bun test` utiliza o test runner do Bun. O teste de snapshot de screenshot é automaticamente ignorado quando o matcher não está disponível; testes de navegador completos continuam via `bun run test:browser` (Vitest + Playwright).
<!-- ci: smoke test 3 -->

View file

@ -20,7 +20,7 @@ Os demais colaboradores reais são provisionados via **Convites & acessos**. Cas
- Para DEV: rode `bun run convex:dev:bun` e acesse `/dev/seed` uma vez para popular dados realistas.
## Stack atual (06/11/2025)
- **Next.js**: `16.0.1` (Turbopack em desenvolvimento; builds de produção usam webpack).
- **Next.js**: `16.0.8` (Turbopack por padrão; webpack fica como fallback).
- Whitelist de domínios em `src/config/allowed-hosts.ts` é aplicada pelo `middleware.ts`.
- **React / React DOM**: `19.2.0`.
- **Trilha de testes**: Vitest (`bun test`) sem modo watch por padrão (`--run --passWithNoTests`).
@ -90,7 +90,7 @@ bun run build:bun
- **Testes unitários/integrados (Vitest)**:
- Cobertura atual inclui utilitários (`tests/*.test.ts`), rotas `/api/machines/*` e `sendSmtpMail`.
- Executar `bun test -- --watch` apenas quando precisar de modo interativo.
- **Build**: `bun run build:bun` (`next build --webpack`, webpack). Para reproduzir problemas do bundler experimental, use `bun run build:turbopack`.
- **Build**: `bun run build:bun` (`next build --turbopack`). Quando precisar do fallback oficial, rode `bun run build:webpack`.
- **CI**: falhas mais comuns
- `ERR_BUN_LOCKFILE_OUTDATED`: confirme que o `bun.lock` foi regenerado (`bun install`) após alterar dependências, especialmente do app desktop.
- Variáveis Better Auth ausentes (`BETTER_AUTH_SECRET`): definidas no workflow (`Quality Checks`).
@ -168,4 +168,4 @@ bun run build:bun
- `docs/admin-inventory-ui.md`, `docs/plano-app-desktop-maquinas.md` — detalhes do inventário/agente.
---
_Última atualização: 06/11/2025 (Next.js 16, build de produção com webpack, fluxos desktop + portal documentados)._
_Última atualização: 10/12/2025 (Next.js 16, build padrão com Turbopack e fallback webpack documentado)._

View file

@ -47,7 +47,7 @@
"date-fns": "^4.1.0",
"dotenv": "17.2.3",
"lucide-react": "0.556.0",
"next": "^16.0.7",
"next": "16.0.8",
"next-themes": "^0.4.6",
"pdfkit": "^0.17.2",
"postcss": "^8.5.6",
@ -82,7 +82,7 @@
"baseline-browser-mapping": "^2.9.2",
"cross-env": "^10.1.0",
"eslint": "^9",
"eslint-config-next": "^16.0.7",
"eslint-config-next": "16.0.8",
"eslint-plugin-react-hooks": "7.0.0",
"jsdom": "^27.0.1",
"playwright": "^1.56.1",
@ -387,25 +387,25 @@
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
"@next/env": ["@next/env@16.0.7", "", {}, "sha512-gpaNgUh5nftFKRkRQGnVi5dpcYSKGcZZkQffZ172OrG/XkrnS7UBTQ648YY+8ME92cC4IojpI2LqTC8sTDhAaw=="],
"@next/env": ["@next/env@16.0.8", "", {}, "sha512-xP4WrQZuj9MdmLJy3eWFHepo+R3vznsMSS8Dy3wdA7FKpjCiesQ6DxZvdGziQisj0tEtCgBKJzjcAc4yZOgLEQ=="],
"@next/eslint-plugin-next": ["@next/eslint-plugin-next@16.0.7", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-hFrTNZcMEG+k7qxVxZJq3F32Kms130FAhG8lvw2zkKBgAcNOJIxlljNiCjGygvBshvaGBdf88q2CqWtnqezDHA=="],
"@next/eslint-plugin-next": ["@next/eslint-plugin-next@16.0.8", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-1miV0qXDcLUaOdHridVPCh4i39ElRIAraseVIbb3BEqyZ5ol9sPyjTP/GNTPV5rBxqxjF6/vv5zQTVbhiNaLqA=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.0.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LlDtCYOEj/rfSnEn/Idi+j1QKHxY9BJFmxx7108A6D8K0SB+bNgfYQATPk/4LqOl4C0Wo3LACg2ie6s7xqMpJg=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.0.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-yjVMvTQN21ZHOclQnhSFbjBTEizle+1uo4NV6L4rtS9WO3nfjaeJYw+H91G+nEf3Ef43TaEZvY5mPWfB/De7tA=="],
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.0.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rtZ7BhnVvO1ICf3QzfW9H3aPz7GhBrnSIMZyr4Qy6boXF0b5E3QLs+cvJmg3PsTCG2M1PBoC+DANUi4wCOKXpA=="],
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.0.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-+zu2N3QQ0ZOb6RyqQKfcu/pn0UPGmg+mUDqpAAEviAcEVEYgDckemOpiMRsBP3IsEKpcoKuNzekDcPczEeEIzA=="],
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.0.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-mloD5WcPIeIeeZqAIP5c2kdaTa6StwP4/2EGy1mUw8HiexSHGK/jcM7lFuS3u3i2zn+xH9+wXJs6njO7VrAqww=="],
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.0.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-LConttk+BeD0e6RG0jGEP9GfvdaBVMYsLJ5aDDweKiJVVCu6sGvo+Ohz9nQhvj7EQDVVRJMCGhl19DmJwGr6bQ=="],
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.0.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-+ksWNrZrthisXuo9gd1XnjHRowCbMtl/YgMpbRvFeDEqEBd523YHPWpBuDjomod88U8Xliw5DHhekBC3EOOd9g=="],
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.0.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-JaXFAlqn8fJV+GhhA9lpg6da/NCN/v9ub98n3HoayoUSPOVdoxEEt86iT58jXqQCs/R3dv5ZnxGkW8aF4obMrQ=="],
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.0.7", "", { "os": "linux", "cpu": "x64" }, "sha512-4WtJU5cRDxpEE44Ana2Xro1284hnyVpBb62lIpU5k85D8xXxatT+rXxBgPkc7C1XwkZMWpK5rXLXTh9PFipWsA=="],
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.0.8", "", { "os": "linux", "cpu": "x64" }, "sha512-O7M9it6HyNhsJp3HNAsJoHk5BUsfj7hRshfptpGcVsPZ1u0KQ/oVy8oxF7tlwxA5tR43VUP0yRmAGm1us514ng=="],
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.0.7", "", { "os": "linux", "cpu": "x64" }, "sha512-HYlhqIP6kBPXalW2dbMTSuB4+8fe+j9juyxwfMwCe9kQPPeiyFn7NMjNfoFOfJ2eXkeQsoUGXg+O2SE3m4Qg2w=="],
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.0.8", "", { "os": "linux", "cpu": "x64" }, "sha512-8+KClEC/GLI2dLYcrWwHu5JyC5cZYCFnccVIvmxpo6K+XQt4qzqM5L4coofNDZYkct/VCCyJWGbZZDsg6w6LFA=="],
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.0.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-EviG+43iOoBRZg9deGauXExjRphhuYmIOJ12b9sAPy0eQ6iwcPxfED2asb/s2/yiLYOdm37kPaiZu8uXSYPs0Q=="],
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.0.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-rpQ/PgTEgH68SiXmhu/cJ2hk9aZ6YgFvspzQWe2I9HufY6g7V02DXRr/xrVqOaKm2lenBFPNQ+KAaeveywqV+A=="],
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.0.7", "", { "os": "win32", "cpu": "x64" }, "sha512-gniPjy55zp5Eg0896qSrf3yB1dw4F/3s8VK1ephdsZZ129j2n6e1WqCbE2YgcKhW9hPB9TVZENugquWJD5x0ug=="],
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.0.8", "", { "os": "win32", "cpu": "x64" }, "sha512-jWpWjWcMQu2iZz4pEK2IktcfR+OA9+cCG8zenyLpcW8rN4rzjfOzH4yj/b1FiEAZHKS+5Vq8+bZyHi+2yqHbFA=="],
"@noble/ciphers": ["@noble/ciphers@2.0.1", "", {}, "sha512-xHK3XHPUW8DTAobU+G0XT+/w+JLM7/8k1UFdB5xg/zTFPnFCobhftzw8wl4Lw2aq/Rvir5pxfZV5fEazmeCJ2g=="],
@ -1189,7 +1189,7 @@
"eslint": ["eslint@9.39.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.1", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g=="],
"eslint-config-next": ["eslint-config-next@16.0.7", "", { "dependencies": { "@next/eslint-plugin-next": "16.0.7", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^7.0.0", "globals": "16.4.0", "typescript-eslint": "^8.46.0" }, "peerDependencies": { "eslint": ">=9.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typescript"] }, "sha512-WubFGLFHfk2KivkdRGfx6cGSFhaQqhERRfyO8BRx+qiGPGp7WLKcPvYC4mdx1z3VhVRcrfFzczjjTrbJZOpnEQ=="],
"eslint-config-next": ["eslint-config-next@16.0.8", "", { "dependencies": { "@next/eslint-plugin-next": "16.0.8", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^7.0.0", "globals": "16.4.0", "typescript-eslint": "^8.46.0" }, "peerDependencies": { "eslint": ">=9.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typescript"] }, "sha512-8J5cOAboXIV3f8OD6BOyj7Fik6n/as7J4MboiUSExWruf/lCu1OPR3ZVSdnta6WhzebrmAATEmNSBZsLWA6kbg=="],
"eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.9", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", "resolve": "^1.22.4" } }, "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g=="],
@ -1571,7 +1571,7 @@
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
"next": ["next@16.0.7", "", { "dependencies": { "@next/env": "16.0.7", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.0.7", "@next/swc-darwin-x64": "16.0.7", "@next/swc-linux-arm64-gnu": "16.0.7", "@next/swc-linux-arm64-musl": "16.0.7", "@next/swc-linux-x64-gnu": "16.0.7", "@next/swc-linux-x64-musl": "16.0.7", "@next/swc-win32-arm64-msvc": "16.0.7", "@next/swc-win32-x64-msvc": "16.0.7", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-3mBRJyPxT4LOxAJI6IsXeFtKfiJUbjCLgvXO02fV8Wy/lIhPvP94Fe7dGhUgHXcQy4sSuYwQNcOLhIfOm0rL0A=="],
"next": ["next@16.0.8", "", { "dependencies": { "@next/env": "16.0.8", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.0.8", "@next/swc-darwin-x64": "16.0.8", "@next/swc-linux-arm64-gnu": "16.0.8", "@next/swc-linux-arm64-musl": "16.0.8", "@next/swc-linux-x64-gnu": "16.0.8", "@next/swc-linux-x64-musl": "16.0.8", "@next/swc-win32-arm64-msvc": "16.0.8", "@next/swc-win32-x64-msvc": "16.0.8", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-LmcZzG04JuzNXi48s5P+TnJBsTGPJunViNKV/iE4uM6kstjTQsQhvsAv+xF6MJxU2Pr26tl15eVbp0jQnsv6/g=="],
"next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],

View file

@ -6,7 +6,7 @@ Este documento consolida o estado atual do ambiente de desenvolvimento, descreve
- **Bun (runtime padrão)**: 1.3+ já instalado no runner e VPS (`bun --version`). Após instalar localmente, exporte `PATH="$HOME/.bun/bin:$PATH"` para tornar o binário disponível. Use `bun install`, `bun run dev:bun`, `bun run convex:dev:bun`, `bun run build:bun` e `bun test` como fluxo principal (scripts Node continuam disponíveis como fallback).
- **Node.js**: mantenha a versão 20.9+ instalada para ferramentas auxiliares (Prisma CLI, scripts legados em Node) quando não estiver usando o runtime do Bun.
- **Next.js 16**: Projeto roda em `next@16.0.1` com Turbopack apenas no ambiente de desenvolvimento; builds de produção usam o webpack padrão do framework.
- **Next.js 16**: Projeto roda em `next@16.0.8` com Turbopack como bundler padrão (dev e build); webpack continua disponível como fallback.
- **Lint/Test/Build**: `bun run lint`, `bun test`, `bun run build:bun`. O test runner do Bun já roda em modo não interativo; utilize `bunx vitest --watch` apenas quando precisar do modo watch manualmente.
- **Banco DEV**: SQLite em `prisma/prisma/db.dev.sqlite`. Defina `DATABASE_URL="file:./prisma/db.dev.sqlite"` ao chamar CLI do Prisma.
- **Desktop (Tauri)**: fonte em `apps/desktop`. Usa Radix tabs + componentes shadcn-like, integra com os endpoints `/api/machines/*` e suporta atualização automática via GitHub Releases.
@ -37,8 +37,8 @@ Este documento consolida o estado atual do ambiente de desenvolvimento, descreve
## Next.js 16 (estável)
- Mantemos o projeto em `next@16.0.1`, com React 19 e o App Router completo.
- **Bundlers**: Turbopack permanece habilitado no `next dev`/`bun run dev:bun` pela velocidade, mas o `next build --webpack` é o caminho oficial para produção. Execute `bun run build:turbopack` apenas para reproduzir bugs.
- Mantemos o projeto em `next@16.0.8`, com React 19 e o App Router completo.
- **Bundlers**: Turbopack permanece habilitado no `next dev`/`bun run dev:bun` e agora também no `next build --turbopack`. Use `next build --webpack` somente para reproduzir bugs ou comparar saídas.
- **Whitelist de hosts**: o release estável continua sem aceitar `server.allowedHosts` (vide [`invalid-next-config`](https://nextjs.org/docs/messages/invalid-next-config)), portanto bloqueamos domínios exclusivamente via `middleware.ts`.
### Editor rich text (TipTap) — menções de ticket
@ -52,9 +52,9 @@ Este documento consolida o estado atual do ambiente de desenvolvimento, descreve
- `bun run lint`: executa ESLint (flat config) sobre os arquivos do projeto.
- `bun test`: roda a suíte de testes utilizando o runner nativo do Bun. Para modo watch, use `bunx vitest --watch` manualmente.
- `bun run build:bun`: `next build --webpack` usando o runtime Bun (webpack).
- `bun run build:bun`: `next build --turbopack` usando o runtime Bun (Turbopack).
- Scripts com Bun (padrão atual): `bun run dev:bun`, `bun run convex:dev:bun`, `bun run build:bun`, `bun run start:bun`. Eles mantêm os scripts existentes, apenas forçando o runtime do Bun via `bun run --bun`. O `cross-env` garante `NODE_ENV` consistente (`development`/`production`).
- `bun run build:turbopack`: build experimental com Turbopack. Use apenas para debugging/local, pois ainda causa inconsistências em produção.
- `bun run build:webpack`: build com o bundler oficial do Next (fallback).
- `bun run dev:webpack`: fallback do Next em dev quando o Turbopack apresentar problemas.
- `bun run prisma:generate`: necessário antes do build quando o client Prisma muda. Para migrações use `bunx prisma migrate deploy`.

View file

@ -342,10 +342,10 @@ Benefícios
- Docs Convex selfhosted: imagem oficial `ghcr.io/get-convex/convex-backend`
## Bundlers (Next.js)
- Em desenvolvimento utilizamos Turbopack (`next dev --turbopack`) pela velocidade incremental.
- Builds de produção rodam com `next build --webpack` para evitar mismatches de chunks vistos com o Turbopack em produção.
- Em desenvolvimento utilizamos Turbopack (`next dev --turbopack`) pela velocidade incremental e mantemos fallback webpack.
- Builds de produção rodam com `next build --turbopack`; use `next build --webpack` apenas se precisar depurar diferenças.
- Scripts principais (package.json):
- `dev`: `next dev --turbopack`
- `build`: `next build --webpack`
- `build:turbopack`: `next build --turbopack` (uso pontual para debug)
- O workflow de CI executa `bun run build:bun` (que agora roda `next build --webpack` via Bun) e a stack continua a usar `bun run start:bun` sobre o artefato gerado.
- `build`: `next build --turbopack`
- `build:webpack`: `next build --webpack` (fallback)
- O workflow de CI executa `bun run build:bun` (roda `next build --turbopack` via Bun) e a stack continua a usar `bun run start:bun` sobre o artefato gerado.

View file

@ -5,9 +5,9 @@
"private": true,
"scripts": {
"prebuild": "prisma generate",
"dev": "next dev",
"dev": "next dev --turbopack",
"dev:webpack": "next dev --webpack",
"build": "next build",
"build": "next build --turbopack",
"build:webpack": "next build --webpack",
"start": "next start",
"lint": "eslint",
@ -69,7 +69,7 @@
"date-fns": "^4.1.0",
"dotenv": "17.2.3",
"lucide-react": "0.556.0",
"next": "^16.0.7",
"next": "16.0.8",
"next-themes": "^0.4.6",
"pdfkit": "^0.17.2",
"postcss": "^8.5.6",
@ -104,7 +104,7 @@
"baseline-browser-mapping": "^2.9.2",
"cross-env": "^10.1.0",
"eslint": "^9",
"eslint-config-next": "^16.0.7",
"eslint-config-next": "16.0.8",
"eslint-plugin-react-hooks": "7.0.0",
"jsdom": "^27.0.1",
"playwright": "^1.56.1",

View file

@ -1,7 +1,8 @@
import { mkdir, writeFile } from "fs/promises"
import { join, dirname } from "path"
import { join, dirname, relative, isAbsolute } from "path"
import { api } from "@/convex/_generated/api"
import type { Id } from "@/convex/_generated/dataModel"
import { DEFAULT_TENANT_ID } from "@/lib/constants"
import { env } from "@/lib/env"
import { createConvexClient } from "@/server/convex-client"
@ -10,6 +11,7 @@ type ArchiveItem = {
ticket: Record<string, unknown>
comments: Array<Record<string, unknown>>
events: Array<Record<string, unknown>>
attachments: ArchivedAttachment[]
}
type ExportResponse = {
@ -17,6 +19,18 @@ type ExportResponse = {
items: ArchiveItem[]
}
type ArchivedAttachment = {
storageId: string
name: string | null
size: number | null
type: string | null
archivedPath: string | null
ticketId: string
commentId: string | null
status: "downloaded" | "skipped" | "failed"
error?: string
}
function assertArchiveSecret(): string {
const secret = env.INTERNAL_HEALTH_TOKEN ?? env.REPORTS_CRON_SECRET
if (!secret) {
@ -29,11 +43,188 @@ function nowIso() {
return new Date().toISOString().replace(/[:.]/g, "-")
}
const DEFAULT_ARCHIVE_DIR = join(process.cwd(), ".data", "archives")
const ARCHIVE_DIR = resolveArchiveDir(process.env.ARCHIVE_DIR)
const ARCHIVE_ENABLED = process.env.LOCAL_ARCHIVE_ENABLED === "true"
const ARCHIVE_FILENAME = "tickets-archive-latest.jsonl"
function resolveArchiveDir(dir: string | undefined | null) {
const normalized = (dir ?? "").trim()
if (!normalized || normalized === "." || normalized === "./") {
return DEFAULT_ARCHIVE_DIR
}
if (!isAbsolute(normalized)) {
throw new Error("ARCHIVE_DIR deve ser um caminho absoluto ou use o padrão ./archives")
}
return normalized
}
function normalizeFilename(name: string | null | undefined, fallback: string) {
const base = (name ?? "").trim() || fallback
return base.replace(/[^a-zA-Z0-9._-]/g, "_")
}
async function downloadAttachment(params: {
storageId: string
name: string | null
ticketId: string
commentId: string | null
archiveDir: string
client: ReturnType<typeof createConvexClient>
}) {
const { storageId, name, ticketId, commentId, archiveDir, client } = params
try {
const url = await client.action(api.files.getUrl, { storageId: storageId as Id<"_storage"> })
if (!url) {
return {
storageId,
name: name ?? null,
size: null,
type: null,
archivedPath: null,
ticketId,
commentId,
status: "skipped" as const,
error: "URL vazia para anexo",
}
}
const response = await fetch(url)
if (!response.ok) {
return {
storageId,
name: name ?? null,
size: null,
type: null,
archivedPath: null,
ticketId,
commentId,
status: "failed" as const,
error: `HTTP ${response.status}`,
}
}
const arrayBuffer = await response.arrayBuffer()
const buffer = Buffer.from(arrayBuffer)
const contentType = response.headers.get("content-type") ?? undefined
const targetDir = join(archiveDir, "attachments", ticketId, commentId ?? "ticket")
await mkdir(targetDir, { recursive: true })
const filename = normalizeFilename(name, `${storageId}.bin`)
const filePath = join(targetDir, filename)
await writeFile(filePath, buffer)
return {
storageId,
name: name ?? null,
size: buffer.byteLength,
type: contentType ?? null,
archivedPath: relative(archiveDir, filePath),
ticketId,
commentId,
status: "downloaded" as const,
}
} catch (error) {
return {
storageId,
name: name ?? null,
size: null,
type: null,
archivedPath: null,
ticketId,
commentId,
status: "failed" as const,
error: error instanceof Error ? error.message : String(error),
}
}
}
async function downloadAttachmentsFromArchive(
items: ArchiveItem[],
archiveDir: string,
client: ReturnType<typeof createConvexClient>
) {
const unique = new Map<string, { ticketId: string; commentId: string | null; name: string | null }>()
for (const item of items) {
for (const comment of item.comments) {
const commentId = (comment as { _id?: string })._id ?? null
const attachments = (comment as { attachments?: Array<Record<string, unknown>> }).attachments ?? []
for (const attachment of attachments) {
const storageId = typeof attachment.storageId === "string" ? attachment.storageId : null
if (!storageId) continue
if (unique.has(storageId)) continue
unique.set(storageId, {
ticketId: String(item.ticket?._id ?? "unknown"),
commentId,
name: typeof attachment.name === "string" ? attachment.name : null,
})
}
}
}
const results: ArchivedAttachment[] = []
for (const [storageId, meta] of unique.entries()) {
const result = await downloadAttachment({
storageId,
name: meta.name,
ticketId: meta.ticketId,
commentId: meta.commentId,
archiveDir,
client,
})
results.push(result)
}
return results
}
export type ArchivedTicketLookup = {
record: ArchiveItem & { archivedAt: number; ticketId: string; tenantId: string }
file: string
}
export async function findArchivedTicket(ticketId: string) {
if (!ARCHIVE_ENABLED) {
return null
}
const archiveDir = ARCHIVE_DIR
const { readFile } = await import("node:fs/promises")
await mkdir(archiveDir, { recursive: true })
const fullPath = join(archiveDir, ARCHIVE_FILENAME)
const contents = await readFile(fullPath, "utf-8").catch(() => null)
if (!contents) return null
const lines = contents.split("\n").filter(Boolean)
for (const line of lines) {
try {
const parsed = JSON.parse(line) as ArchiveItem & { archivedAt: number; ticketId: string; tenantId: string }
if (String(parsed.ticketId) === String(ticketId)) {
return {
record: parsed,
file: fullPath,
} satisfies ArchivedTicketLookup
}
} catch {
continue
}
}
return null
}
export async function exportResolvedTicketsToDisk(options?: {
days?: number
limit?: number
tenantId?: string
includeAttachments?: boolean
}) {
if (!ARCHIVE_ENABLED) {
return {
written: 0,
attachments: { total: 0, failed: 0 },
file: null,
}
}
const days = options?.days ?? 365
const limit = options?.limit ?? 50
const tenantId = options?.tenantId ?? DEFAULT_TENANT_ID
@ -49,25 +240,59 @@ export async function exportResolvedTicketsToDisk(options?: {
secret,
})) as ExportResponse
const archiveDir = env.ARCHIVE_DIR ?? "./archives"
const filename = `tickets-archive-${nowIso()}-resolved-${days}d.jsonl`
const fullPath = join(archiveDir, filename)
const archiveDir = ARCHIVE_DIR
const fullPath = join(archiveDir, ARCHIVE_FILENAME)
await mkdir(dirname(fullPath), { recursive: true })
const lines = res.items.map((item) =>
JSON.stringify({
ticketId: item.ticket?._id ?? null,
let attachments: ArchivedAttachment[] = []
if (options?.includeAttachments ?? true) {
attachments = await downloadAttachmentsFromArchive(res.items, archiveDir, client)
}
const byStorageId = new Map<string, ArchivedAttachment>()
for (const att of attachments) {
byStorageId.set(att.storageId, att)
}
const lines = res.items.map((item) => {
const ticketId = String(item.ticket?._id ?? "")
const ticketAttachments = item.comments.flatMap((comment) => {
const commentId = (comment as { _id?: string })._id ?? null
const raw = (comment as { attachments?: Array<Record<string, unknown>> }).attachments ?? []
return raw.map((att) => {
const storageId = typeof att.storageId === "string" ? att.storageId : null
const archived = storageId ? byStorageId.get(storageId) : null
return {
storageId,
name: typeof att.name === "string" ? att.name : null,
size: typeof att.size === "number" ? att.size : null,
type: typeof att.type === "string" ? att.type : null,
archivedPath: archived?.archivedPath ?? null,
status: archived?.status ?? "skipped",
ticketId,
commentId,
}
})
})
return JSON.stringify({
ticketId,
tenantId,
archivedAt: Date.now(),
ticket: item.ticket,
comments: item.comments,
events: item.events,
attachments: ticketAttachments,
})
)
})
await writeFile(fullPath, lines.join("\n"), { encoding: "utf-8" })
return {
written: res.items.length,
attachments: {
total: attachments.length,
failed: attachments.filter((a) => a.status === "failed").length,
},
file: fullPath,
}
}