Skip to content

Latest commit

 

History

History
521 lines (427 loc) · 122 KB

AUDIT.md

File metadata and controls

521 lines (427 loc) · 122 KB

AUDIT.md — Auditoria do Sistema de Gestão de EPIs da ENGENOVA

Fase 1 — Auditoria. Nenhum arquivo de código foi alterado para produzir este relatório. Data: 2026-10-06 · Commit auditado: c4a02b4 (branch master)


0. Resumo executivo

O repositório não é um app legado em JavaScript à espera de migração. Na verdade, ele contém duas coisas:

  1. O legado (legado/): um protótipo navegável em HTML + JavaScript puro (legado/index.html, ~1.200 linhas, tudo com dados fictícios), 11 screenshots do produto, uma landing page de apresentação (index_final.html) e um scaffold React vazio (boilerplate do Vite). Ele define o comportamento e a identidade visual de referência (ENGENOVA, laranja, fluxo de entrega em 9 telas).
  2. A reescrita em andamento (apps/, packages/, database/): um monorepo pnpm + Turborepo que já usa TypeScript strict, React 19 + Vite, Express 5, Zod, Prisma 7 e PostgreSQL. Ou seja, a stack-alvo da Seção 5 do pedido já foi adotada.

Estado real da reescrita:

Área Situação
API — autenticação (JWT + refresh rotativo + RBAC) Implementada, com falhas pontuais (ver A-04, A-05)
API — empresas, unidades, setores, cargos, colaboradores, EPIs, CAs Implementadas (CRUD sem exclusão)
API — estoque, entregas, devoluções, assinatura, QR Code, auditoria, relatórios Não existem
Banco Schema Prisma completo, sem nenhuma migration
Web 9 telas do fluxo de entrega, 100% com dados fictícios: nenhuma chamada à API, sem login funcional e sem guarda de rotas
Testes 16 testes unitários na API (todos passam); zero no web
Build / Lint Passam

Os 4 problemas críticos:

  • C-01: o fluxo de entrega exibe "Entrega registrada com sucesso", mas não grava nada.
  • C-02: não há migrations versionadas.
  • C-03: o modelo de estoque permite saldo negativo e não tem controle de concorrência.
  • C-04: a entrega não registra o CA nem uma assinatura com valor de evidência, o que é exigido pela NR-6.

Achado de alto impacto em produto: a reescrita trocou a identidade ENGENOVA (laranja #EE5A24, logo, Sora) por uma marca genérica "EPI Manager" em azul (A-01).

Consequência para o plano: a "migração JS → TS" (Fase 3) se resume a portar o comportamento do protótipo e endurecer os contratos de tipo (DTOs compartilhados, eliminar any implícito de req.body). O grosso do trabalho está em domínio (entrega/estoque/assinatura), integração web↔API, segurança e testes.


1. Método

Arquivos lidos integralmente:

  • Todos os 183 arquivos versionados: configs raiz, apps/api/src/**, apps/web/src/**, packages/**, database/prisma/**, CI, husky e legado/index.html.
  • Os scripts apply_ui_v2.*, por amostragem.
  • As 11 screenshots em legado/*.png.

Comandos executados:

Comando Resultado
pnpm lint ✅ 2/2 pacotes OK (sem warnings)
pnpm turbo run build test --force ✅ build API (tsc -b) e web (tsc -b && vite build) OK. ✅ 16/16 testes (4 arquivos). ⚠️ chunk único de 769 KB (232 KB gzip)
Import do pacote @epi-manager/database em runtime (Node 24) ✅ funciona, mas só porque o Node 24 remove tipos de .ts nativamente (ver M-06)
Busca de segredos no histórico git (git log -p) ✅ nenhum segredo real commitado. .env e apps/api/.env estão no .gitignore
Checagem do apps/api/.env local ⚠️ usa o placeholder JWT_SECRET do .env.example (aceito pela validação de ≥16 caracteres)
Busca por any, dangerouslySetInnerHTML, localStorage ✅ nenhuma ocorrência no código novo

2. Stack atual (3.1)

Item Legado (legado/index.html) Reescrita (apps/*)
Framework Nenhum (DOM manual + innerHTML) React 19.2 (web) · Express 5.2 (API)
Linguagem JavaScript (inline no HTML) TypeScript 5.7, strict + noUncheckedIndexedAccess
Bundler Nenhum Vite 8 (rolldown) · tsc -b na API
Monorepo — pnpm 9.15 workspaces + Turborepo 2
UI CSS próprio (variáveis, Sora/Inter/JetBrains Mono) TailwindCSS 4 + componentes próprios estilo shadcn (cva + tailwind-merge), lucide-react
Estado Objeto global state mutável Zustand 5 (fluxo de entrega + sessão, esta não usada)
Server state — TanStack Query 5 (provider instalado, nenhuma query)
Roteamento Troca de classe .active em <section> React Router 7 (createBrowserRouter)
HTTP — axios (instância criada, nunca importada)
Formulários — react-hook-form + @hookform/resolvers (instalados, não usados)
Validação — Zod 4 (API: body, query e env). Web: não usado
Banco / ORM — PostgreSQL 16 (docker-compose) · Prisma 7.9 com @prisma/adapter-pg
Autenticação doLogin(){ goTo('home') } (falsa) JWT de acesso (15 min, Bearer) + refresh token opaco (7 dias, cookie httpOnly, hash SHA-256 no banco, rotação) · bcrypt
Autorização — Middleware authorize(...roles) por rota (7 perfis)
Logs — winston (JSON em produção)
Segurança HTTP — helmet, cors (origem única), express-rate-limit (global 300/15 min + login 10/15 min)
Docs de API — swagger-jsdoc + swagger-ui em /docs
QR Code Botão "Simular leitura" html5-qrcode (câmera real, mas qualquer QR lido → funcionário fictício)
Assinatura <canvas> manual react-signature-canvas → data URL em memória
Testes — Vitest 4 (somente API, unitários com mocks)
Lint oxlint (scaffold) ESLint 9 flat config + typescript-eslint + react-hooks + react-refresh
Formatter — Prettier 3 (sem verificação no CI)
Commits — commitlint (conventional) + husky
CI — GitHub Actions: install → lint → build (sem testes)

3. Estrutura atual (3.2)

.
├── apps/
│   ├── api/                 API REST (Express 5). Módulos em camadas: domain / application / http / infra
│   │   └── src/
│   │       ├── modules/
│   │       │   ├── auth/            login, refresh, logout, me
│   │       │   ├── organization/    companies, business-units, departments, job-roles
│   │       │   ├── employee/        colaboradores (CRUD + bloquear/desbloquear/desligar)
│   │       │   ├── epi/             itens de EPI + histórico de CA
│   │       │   └── health/          GET /health
│   │       └── shared/              env (zod), logger, middlewares (auth, rbac, validação, erro, rate limit),
│   │                                segurança (bcrypt, JWT), paginação, swagger
│   ├── web/                 SPA React. Hoje só o fluxo de entrega (mock) + dashboard/histórico/menu (mock)
│   │   └── src/
│   │       ├── app/                 App, router, query-client, error-page, layout (sidebar, bottom-nav, focus-header)
│   │       ├── features/            auth (login visual), dashboard, delivery (9 telas + store zustand + mocks), menu
│   │       ├── shared/              components/ui (button, input, card, badge, checkbox, radio, textarea), lib (cn, http-client)
│   │       ├── stores/              session-store (não usado)
│   │       └── styles/              globals.css (tokens Tailwind; paleta azul)
│   └── mobile-pwa/          Apenas package.json (vazio)
├── packages/
│   ├── config/              ESLint base/react + tsconfig.base (usado)
│   ├── database/            Singleton PrismaClient com adapter pg (usado; exporta o TS fonte)
│   ├── types/               Só o tipo UserRole (usado pela API)
│   ├── ui/ hooks/ utils/    Apenas package.json (vazios)
├── database/
│   └── prisma/              schema.prisma (completo), seeds/seed.ts, migrations/ (VAZIA)
├── docker/                  docker-compose: só PostgreSQL
├── legado/                  Protótipo HTML/JS, 11 screenshots, landing de apresentação, scripts de restyle, scaffold React vazio
├── docs/ scripts/           Diretórios vazios (não versionados)
├── .github/workflows/ci.yml lint + build
└── prisma.config.ts         Aponta o Prisma CLI para database/prisma

Avaliação: a separação domain / application / http / infra na API é coerente e pragmática: interfaces de repositório são usadas de verdade nos testes. Não é preciso reorganizar a API, apenas completá-la. No web, a organização por features/ é adequada, mas falta a camada de dados (api/, hooks de query) e de sessão.


4. Mapa funcional: legado × reescrita

Legenda: ✅ implementado · 🟡 só visual / mock · ❌ ausente

Funcionalidade (fonte: protótipo + screenshots) Legado Web novo API nova Observação
Login usuário/senha 🟡 🟡 sem submit ✅ Legado usa "usuário"; API usa e-mail
"Lembrar de mim" / "Esqueci minha senha" 🟡 🟡 ❌ Link href="#"
Logout 🟡 🟡 botão morto ✅
Dashboard: saudação, almoxarifado, resumo do dia, últimas entregas 🟡 🟡 hardcoded "Marcio" ❌ Não existe vínculo usuário↔almoxarifado nem User.name
Notificações (sino) 🟡 toast 🟡 botão morto ❌
Escanear QR do crachá 🟡 simulado 🟡 câmera real, resultado fictício ❌ Ver A-03
Buscar funcionário por nome/matrícula (lista) 🟡 ❌ perdida 🟡 lista sem busca textual Card "Buscar Funcionário" leva ao scanner
Busca manual por matrícula no scanner — 🟡 ignora o valor digitado ❌
Funcionário encontrado (foto, status, matrícula, função, empresa, CC) 🟡 🟡 ✅ dados existem Bloqueia continuar se não "Ativo" (bom)
Seleção múltipla de EPIs com busca 🟡 🟡 ✅ catálogo
EPI "Outro (especificar)" 🟡 ❌ perdida ❌
Quantidade por EPI (passo a passo, 1–50) + observação (200 caracteres) 🟡 🟡 tudo em uma tela, sem limite superior ❌
Tamanho / lote ❌ ❌ 🟡 StockItem.batchNumber Pedido na Seção 11
Validação de estoque ❌ ❌ ❌
Motivo da entrega (8 opções) 🟡 🟡 ❌ sem campo no schema "Outro" sem texto livre
Assinatura digital 🟡 🟡 data URL em memória ❌
Confirmação/sucesso com resumo 🟡 🟡 afirma sucesso sem gravar ❌ C-01
Comprovante 🟡 🟡 window.print() da tela escura ❌ receiptPdfUrl sem gerador
Histórico com busca e filtros (Hoje/Ontem/Semana/Mês) 🟡 🟡 filtros sem efeito; busca só por nome ❌ Screenshot mostra detalhe (chevron)
Menu: Funcionários, EPIs, Relatórios, Estatísticas, Almoxarifados, Configurações 🟡 🟡 "em breve" 🟡 parcial (colab./EPIs)
Cadastros organizacionais ❌ ❌ ✅ Sem telas
Estoque / movimentações ❌ ❌ ❌ Só schema
Devolução ❌ ❌ ❌ Só schema (EpiReturn)
Auditoria ❌ ❌ ❌ AuditLog nunca escrito
EPIs obrigatórios por cargo ❌ ❌ ❌ Só schema (JobRoleRequiredEpi), útil para sugerir EPIs
Treinamentos ❌ ❌ ❌ Só schema (Training)

5. Problemas encontrados (3.3)

CRÍTICO

ID Arquivo(s) Problema Impacto Solução recomendada
C-01 apps/web/src/features/delivery/signature-page.tsx, success-page.tsx; ausência de apps/api/src/modules/delivery O fluxo termina em "Entrega registrada com sucesso!" com "Responsável: Marcio (Almoxarifado)" hardcoded, mas nenhuma entrega é enviada ou gravada. Não existe módulo de entrega na API. Se usado como está, o sistema mente sobre a operação mais importante; não há rastreabilidade legal da entrega. Criar o caso de uso RegisterDelivery no backend (transação única: entrega + itens + baixa de estoque + movimentações + assinatura + auditoria). A tela de sucesso só é exibida após 201 da API, com dados vindos da resposta.
C-02 database/prisma/migrations/ (vazio) Nenhuma migration versionada. O banco só pode ser criado por db push ou manualmente. Impossível evoluir o schema com segurança, reproduzir ambientes ou implantar em produção sem alteração manual. Gerar a migration inicial (prisma migrate dev --name init) depois de corrigir o schema (A-09), versionar e usar prisma migrate deploy no deploy. Restrições que o Prisma não expressa (ex.: CHECK (quantity >= 0)) entram em SQL na própria migration.
C-03 database/prisma/schema.prisma (StockItem, StockMovement) O modelo de estoque é um número sobrescrito (StockItem.quantity), sem CHECK >= 0, sem saldo anterior/posterior na movimentação, sem vínculo movimentação↔item de entrega, e com tipos incompletos (ENTRADA, SAIDA, TRANSFERENCIA, AJUSTE, INVENTARIO; faltam ENTREGA, DEVOLUCAO, PERDA, AVARIA). Não há módulo de estoque. O cenário da Seção 14 (dois almoxarifes, estoque = 1) resultaria em estoque negativo, e o histórico não explica como se chegou ao saldo. Manter o saldo materializado (necessário para leitura rápida), mas só alterá-lo junto com a movimentação, na mesma transação, via UPDATE stock_items SET quantity = quantity - $n WHERE id = $id AND quantity >= $n (0 linhas afetadas → 409 ESTOQUE_INSUFICIENTE). Adicionar CHECK (quantity >= 0) como rede de segurança, balanceBefore/balanceAfter e deliveryItemId? em StockMovement, e os tipos faltantes. Teste de concorrência real contra PostgreSQL.
C-04 schema.prisma (Delivery, DeliveryItem) A entrega não guarda o número do CA vigente no momento da entrega (apenas epiItemId; o CA pode mudar ou vencer depois), e a assinatura é só signatureUrl String?, sem data/hora da assinatura, hash do conteúdo assinado, dispositivo ou vínculo com os itens. A ficha de EPI da NR-6 exige CA, data, quantidade e assinatura do trabalhador. Sem esses dados congelados, o registro perde valor de prova. Snapshot em DeliveryItem (caNumber, epiName). Entidade DeliverySignature (1:1 com Delivery): imageKey, contentHash (SHA-256 do payload canônico da entrega + imagem), signedAt, ipAddress, userAgent. Entrega imutável após assinada (correções via devolução ou estorno, nunca UPDATE).

ALTO

ID Arquivo(s) Problema Impacto Solução recomendada
A-01 apps/web/src/styles/globals.css, sidebar.tsx, login-page.tsx, index.html Identidade ENGENOVA substituída por "EPI Manager" com paleta azul e ícone genérico. O legado usa laranja #EE5A24/#FF8A3D, tinta #1B1B1F, fonte Sora em títulos, logo da construtora e textos com acentuação. Viola "preservar a identidade da ENGENOVA". O produto deixa de ser reconhecível. Recriar os tokens a partir de legado/index.html (:root) e legado/ui_v2_styles.css. Extrair o logo oficial (PNG base64 dentro de legado/index_final.html) para apps/web/src/assets. Nome do produto: "ENGENOVA · Controle de Entrega de EPIs".
A-02 apps/web/src/app/router.tsx, login-page.tsx, shared/lib/http-client.ts, stores/session-store.ts O web não tem autenticação: o formulário de login não submete, não há guarda de rota (todas as rotas, inclusive /entregas/nova/*, ficam acessíveis), não há interceptor de token/refresh, o httpClient e o useSessionStore nunca são importados e "Sair" não faz nada. O frontend é inutilizável contra a API real (todas as rotas da API exigem Bearer). Camada auth: access token só em memória; refresh via cookie httpOnly já existente; bootstrap com POST /auth/refresh ao abrir o app; interceptor 401 → refresh único (com lock) → repetir a requisição; RequireAuth / RequireRole no router; logout chama /auth/logout e limpa o cache do React Query.
A-03 apps/web/src/features/delivery/scanner-page.tsx; schema sem identificador de crachá Qualquer QR lido vai para o funcionário fictício. Não há definição do conteúdo do QR nem endpoint que o resolva. Risco de, na implementação real, colocar ID interno, CPF ou matrícula no QR, que são dados previsíveis ou pessoais e permitem enumeração. Coluna Employee.badgeCode (aleatório, ≥128 bits, único, revogável/reemitível). O QR contém só esse código. GET /employees/by-badge/:code exige perfil de almoxarifado, retorna o mínimo necessário (sem CPF) e grava auditoria. O backend valida status (ATIVO) antes de permitir a entrega.
A-04 apps/api/src/modules/auth/application/refresh-token.use-case.ts, schema.prisma (RefreshToken) A rotação faz findValidByHash → revoke → create sem atomicidade: duas requisições concorrentes com o mesmo token ambas recebem um novo par. Também não há detecção de reuso (token já revogado reapresentado), e tokenHash não é @unique nem indexado (findFirst faz varredura). Um refresh token roubado continua utilizável em paralelo ao legítimo, sem detecção. A performance degrada com o volume de tokens. tokenHash @unique; revogar com updateMany({ where: { id, revokedAt: null } }) e exigir count === 1; se o token apresentado estiver revogado, revogar toda a família (revokeAllForUser, hoje código morto) e registrar auditoria.
A-05 apps/api/src/shared/middlewares/authenticate.ts, authorize.ts, token-service.ts O access token não é revalidado: usuário desativado ou com perfil alterado mantém acesso por até 15 min. role vem do JWT como string e é convertido com as UserRole; jwt.verify não fixa algorithms, issuer nem audience; o payload é convertido com as AccessTokenPayload sem validação. Revogação de acesso atrasada; conversões de tipo escondem payload malformado. Validar o payload com Zod; fixar algorithms: ["HS256"], issuer e audience; incluir tokenVersion no usuário (incrementado ao desativar ou trocar perfil) e checá-lo no authenticate com cache curto, ou aceitar 15 min explicitamente documentados no SECURITY.md.
A-06 apps/api/src/modules/employee/http/employee.routes.ts (GETs), company.routes.ts etc. GET /employees e /employees/:id estão abertos a qualquer usuário autenticado (inclusive CONSULTA) e retornam o registro completo com CPF, e-mail e telefone. Também não há escopo por unidade: um almoxarife da unidade A lista colaboradores da unidade B. Exposição de dados pessoais (LGPD) e IDOR horizontal entre unidades. Projeções de resposta por caso de uso (ex.: EmployeeSummaryDto sem CPF para o fluxo de entrega; CPF mascarado salvo para ADMIN/RH). authorize nos GETs. Escopo por unidade derivado do usuário (User.businessUnitIds ou do almoxarifado) aplicado no repositório.
A-07 Toda a API; schema.prisma (AuditLog) O modelo AuditLog existe, mas nenhum código o escreve (0 ocorrências). Login, criação/edição de cadastros e mudanças de status não são registrados. Não atende a Seção 27; é impossível investigar quem fez o quê. Serviço audit.record({ actorId, action, entity, entityId, before, after, ip, userAgent }) chamado dentro da mesma transação das operações críticas (entrega, estoque, status, perfis) e nos eventos de login (sucesso/falha) e refresh com reuso.
A-08 apps/api/src/shared/middlewares/error-handler.ts e todos os *.service.ts Erros do Prisma não são mapeados: P2002 (unique), P2025 (não encontrado) e P2003 (FK) viram 500 "Erro interno". As checagens de unicidade são "consulta, depois insere" (ex.: CPF, CNPJ, matrícula), então requisições concorrentes passam pela checagem e explodem no banco com 500. Erros não têm code estável para o frontend traduzir. Mensagens inúteis ao usuário (viola a Seção 18) e corrida em cadastros. Formato de erro estruturado { code: "ESTOQUE_INSUFICIENTE", message, details? }; mapear erros do Prisma no handler (P2002 → 409 CONFLITO, P2025 → 404, P2003 → 409/422). Manter a checagem prévia só para mensagem amigável e confiar na constraint do banco.
A-09 database/prisma/schema.prisma Lacunas de domínio para o que o produto já mostra ou o pedido exige: Delivery sem status, reason, notes; DeliveryItem sem size, notes, snapshot de CA; nenhum conceito de tamanho/grade no estoque; EpiItem.category é texto livre (sem entidade Categoria); User sem name (a UI precisa de "Olá, Márcio"); sem vínculo usuário↔almoxarifado; EpiReturn.restockedToStockId sem FK; Employee.companyId sem relação/FK (e redundante com businessUnit.companyId). Impossível implementar as telas existentes sem retrabalho; dados inconsistentes possíveis. Ajustar o schema antes da migration inicial (não há dados de produção, então é o momento mais barato). Ver §11 para a lista completa.
A-10 apps/api/src/app.ts, shared/middlewares/rate-limiters.ts Rate limit em memória, só por IP, sem app.set("trust proxy", …). Atrás de proxy/load balancer, todos os clientes compartilham o IP do proxy (um usuário bloqueia o login de todos) ou o cabeçalho é ignorado. Não há limite por conta. Proteção contra força bruta ineficaz ou causando negação de serviço em produção. Configurar trust proxy conforme o deploy; limitador de login por IP e por e-mail; bloqueio progressivo por conta registrado em auditoria. Store compartilhada (Postgres/Redis) só se houver mais de uma instância.
A-11 schema.prisma (UserRole), packages/types O schema define 7 perfis (ADMINISTRADOR, SUPERVISOR, ALMOXARIFE, TECNICO_SEGURANCA, RH, GESTOR, CONSULTA); o pedido fala em COLABORADOR, ALMOXARIFADO, ADMIN. Não existe perfil de colaborador, e não está definido se o colaborador faz login ou assina no aparelho do almoxarife (como no legado). Matriz de permissões indefinida; risco de overengineering (7 perfis sem usuários reais) ou de lacuna (colaborador sem fluxo). Decisão pendente (§14).
A-12 apps/web/src/features/delivery/*, history-page.tsx, dashboard-page.tsx Comportamentos do legado perdidos ou quebrados: busca de funcionário por nome/matrícula; EPI "Outro (especificar)"; limite de quantidade (50); filtros de período (os chips não filtram); busca do histórico por matrícula (o placeholder promete, o código só filtra por nome); detalhe da entrega no histórico; texto livre para motivo "Outro". Regressão funcional em relação à referência. Reimplementar sobre a API real (§4).

MÉDIO

ID Arquivo(s) Problema Impacto Solução recomendada
M-01 apps/web/src/app/router.tsx, vite.config.ts Todas as rotas são importadas estaticamente. O bundle é um chunk de 769 KB (232 KB gzip), incluindo html5-qrcode e react-signature-canvas já na tela de login. Carga inicial lenta em celular na obra (rede ruim). lazy por rota (React Router lazy), import dinâmico do scanner e da assinatura. Meta: < 200 KB gzip na rota inicial. Medir antes/depois.
M-02 apps/web/src/features/delivery/store.ts, scanner-page.tsx O estado do fluxo só é limpo em "Nova entrega" na tela de sucesso. Abandonar um fluxo e iniciar outro pela navegação mantém os EPIs, quantidades e motivo anteriores para o próximo funcionário. Voltar da tela de sucesso permite "confirmar" de novo. Entrega errada para o colaborador seguinte; dupla submissão quando houver API. reset() ao entrar em /entregas/nova; chave de idempotência por entrega (Idempotency-Key) gerada no início do fluxo e validada no backend; replace na navegação pós-sucesso.
M-03 Todos os *.controller.ts da API req.body (tipo any do Express) é repassado direto aos serviços; req.params.id as string sem validar UUID; req.body.terminationDate usado sem tipo. A validação existe (middleware), mas o tipo não flui: o compilador não protege o contrato. Erros de contrato só aparecem em runtime; refatorações inseguras. Helper validated<T>(schema) que valida body/params/query e entrega o tipo inferido ao handler; schema idParam = z.object({ id: z.uuid() }).
M-04 apps/api/src/modules/*/domain/*.ts, apps/web/src/features/delivery/types.ts, packages/types Tipos de domínio copiados à mão do Prisma (risco de divergência). Enums repetidos em 4 lugares, com grafias diferentes: web "Ativo" | "Bloqueado" × API "ATIVO" | "BLOQUEADO". packages/types só tem UserRole. O web não consegue consumir a API sem conversões ad hoc; drift silencioso. packages/contracts: schemas Zod de request/response compartilhados (a fonte única) → tipos via z.infer. A API valida com eles; o web os usa para tipar respostas e formulários. Rótulos de exibição (ATIVO → "Ativo") só na camada de UI.
M-05 apps/api/src/shared/swagger.ts, app.ts /docs é exposto em produção, e o glob ./src/modules/**/*.routes.ts depende do diretório corrente e de arquivos .ts, que não existem no dist. Superfície desnecessária em produção; documentação vazia no build. Servir /docs só fora de produção (ou atrás de auth ADMIN). Gerar o OpenAPI a partir dos schemas Zod (ex.: zod-openapi), eliminando JSDoc duplicado.
M-06 packages/database/package.json (main: ./src/client.ts) A API compilada importa código TypeScript fonte do pacote de banco. Funciona no Node 24 (remoção de tipos nativa) mas engines declara >=20 e o CI usa Node 20. node dist/main.js pode falhar no Node 20/22 de produção. Compilar packages/database (ou fixar engines.node >= 24 e documentar), e adicionar ao CI um passo de smoke test do dist.
M-07 .github/workflows/ci.yml O CI não roda testes, typecheck dos pacotes, prisma generate, prisma validate nem prettier --check. Numa instalação limpa, provavelmente falta o client gerado (não há postinstall); não verificado, porque o CI não foi executado nesta auditoria. Regressões e build quebrado chegam ao main. Passos: prisma generate → lint → typecheck → test (com Postgres de serviço para os testes de integração) → build.
M-08 schema.prisma (Employee, Department), department.service.ts, employee.service.ts Employee.cpf @unique global conflita com o soft delete (deletedAt): não dá para recadastrar. A hierarquia de setores só impede ser pai de si mesmo (ciclos A→B→A passam). O supervisor pode ser de outra empresa ou o próprio colaborador. Inconsistências de dados. Índice único parcial WHERE deleted_at IS NULL (SQL na migration); validação de ciclo ao trocar parentId; supervisor da mesma empresa e ≠ self.
M-09 apps/api/src/modules/auth/application/login.use-case.ts, auth.schemas.ts Quando o e-mail não existe, o bcrypt não roda, e a diferença de tempo revela quais e-mails existem. O e-mail não é normalizado (maiúsculas/minúsculas). Enumeração de usuários; contas "duplicadas" por caixa. Comparar com um hash fictício quando o usuário não existir; email.trim().toLowerCase() no schema e na gravação (ou citext).
M-10 shared/components/ui/checkbox.tsx, radio.tsx, select-epis-page.tsx, history-page.tsx, scanner-page.tsx, várias telas Acessibilidade: checkbox e radio usam sr-only sem indicador de foco visível; campos de busca sem <label> (só placeholder); texto neutral-400 (#94a3b8) sobre branco ≈ 2,6:1, abaixo de 4,5:1 (CA, horários, versão); controles mortos (lanterna, filtro, sino, "Esqueci minha senha", "Sair"); quantidade sem aria-live. Uso por teclado e leitura em ambiente externo (sol forte na obra) prejudicados. peer-focus-visible:ring, labels visualmente ocultos, tokens de contraste AA, remover ou implementar controles mortos, aria-live="polite" no stepper. Adicionar eslint-plugin-jsx-a11y.
M-11 apps/web (sem testes), apps/api (só unitários) Web: zero testes e nenhum setup (Vitest/RTL). API: 16 testes unitários com mocks; não há testes de refresh/logout, middlewares de autenticação/autorização, rotas HTTP, nem integração com banco. Regras críticas (autorização, estoque, concorrência) ficariam sem rede de segurança. Ver §13.
M-12 apps/web/package.json, apps/api/package.json Dependências instaladas sem uso: web framer-motion, react-hook-form, @hookform/resolvers, zod, @epi-manager/types; API multer (+ @types/multer). Ruído e superfície de supply chain. Ver §12 (algumas serão usadas, outras removidas).
M-13 apps/web/src/styles/globals.css html, body { overflow-x: hidden } esconde estouros de layout em vez de corrigi-los. Bugs de responsividade passam despercebidos (Seção 20). Remover e corrigir as causas; testar em 360/390/768/1024/1366/1920 px.
M-14 database/prisma/seeds/seed.ts Cria ADMIN com senha conhecida (Trocar@123) sem trava de ambiente nem troca obrigatória no primeiro acesso. Se rodar em produção, cria uma credencial administrativa pública. Bloquear o seed quando NODE_ENV=production; senha do admin inicial via variável de ambiente; flag mustChangePassword.
M-15 apps/api/src/modules/auth/http/auth.controller.ts Cookie de refresh SameSite=Lax, path=/auth. Se web e API ficarem em sites diferentes em produção, o cookie não é enviado e o refresh quebra. Sessão cai a cada 15 min. Decisão de deploy (§14): mesmo site (ex.: app. e api. do mesmo domínio, ou proxy /api no mesmo host, que é o recomendado) mantém Lax; caso contrário, SameSite=None; Secure + token CSRF.

BAIXO

ID Arquivo(s) Problema Solução
B-01 packages/ui, packages/hooks, packages/utils, apps/mobile-pwa; docs/, scripts/, database/diagrams Pacotes com apenas package.json; diretórios vazios. Remover até haver conteúdo real (evita a impressão de estrutura que não existe).
B-02 legado/App/**, legado/apply_ui_v2.py + .ps1, legado/ui_v2_styles.css, legado/index_final.html (3,6 MB) Scaffold React com boilerplate do Vite; legado/App/package.json com MUI/chart.js nunca usados; dois scripts que fazem a mesma coisa; landing de apresentação de 3,6 MB. Manter index.html + screenshots como referência funcional; extrair o logo; remover o restante (fica no histórico git). Confirmar antes (§14).
B-03 Toda a UI nova Textos sem acentuação ("Funcionario", "Historico", "Ola", "Matricula"); o legado tem acentos corretos. Corrigir as strings (arquivos já são UTF-8).
B-04 dashboard-page.tsx, success-page.tsx, login-page.tsx, menu-page.tsx "Marcio", "Almoxarifado Central" e "Versão 1.0.0" hardcoded (versão repetida 3×). Dados da sessão; versão via import.meta.env / package.json.
B-05 .husky/pre-commit Roda o lint do monorepo inteiro a cada commit. lint-staged.
B-06 raiz Aviso do turbo: versão global (2.10.6) ≠ local (^2.3.3). Usar pnpm turbo/pnpm exec; alinhar a versão.
B-07 shared/security/password-hasher.ts bcrypt custo 10. Subir para 12 (medir a latência do login).
B-08 shared/middlewares/not-found.ts Ecoa método e URL na resposta. Mensagem fixa + code: "ROTA_NAO_ENCONTRADA".
B-09 apps/web/index.html Fontes do Google carregadas externamente. Self-host (@fontsource): privacidade, offline/PWA, sem bloqueio de render.
B-10 README.md Diz que "os apps ainda não foram implementados", cita docs/roadmap.md (inexistente) e shadcn/ui (não usado). Reescrever na Fase 10 (+ ARCHITECTURE.md e SECURITY.md).
B-11 card.tsx (CardHeader, CardContent), ui/index.ts, refresh-token-repository.revokeAllForUser, http-client.ts, session-store.ts Código não referenciado. Usar (A-02, A-04) ou remover.
B-12 auth.schemas.ts password.min(8) no login revela a política e rejeita senhas antigas antes da checagem. Política só na criação/troca de senha; no login, min(1).

6. Dívida técnica (3.4) — funciona, mas está arquiteturalmente frágil

  1. Fluxo de entrega acoplado a mocks e à navegação. Cada tela lê um zustand global e faz <Navigate> defensivo. Não existe um "rascunho de entrega" com regras (ex.: colaborador ativo, quantidade 1..50, EPI com CA válido). A regra "só ativo pode receber" está no componente (employee-found-page.tsx:61). Proposta: features/deliveries/model com funções puras de validação do rascunho (testáveis) e a mesma regra validada no backend.
  2. Composição de dependências espalhada. Cada *.routes.ts instancia seus repositórios e serviços (PrismaEpiItemRepository é criado 2×, PrismaBusinessUnitRepository 2×). Funciona, mas dificulta transações entre módulos. Entrega e estoque precisarão compartilhar a mesma transação (prisma.$transaction(tx => …)). Proposta: repositórios aceitarem um tx opcional, ou o caso de uso de entrega usar o Prisma transacional diretamente. Não introduzir container de DI.
  3. Classes Service monolíticas por entidade (EmployeeService com create/update/block/unblock/terminate/list). Aceitável hoje. Para entrega e estoque, preferir casos de uso nomeados (RegisterDelivery, ReturnEpi, AdjustStock), como já é feito em auth/.
  4. Datas e números como Date/number crus no JSON. unitValue Decimal → Number (perde precisão em valores grandes); datas sem padrão de timezone. Definir no contrato: ISO-8601 UTC e dinheiro em centavos (inteiro) ou string decimal.
  5. Paginação offset (skip/take) + count em todas as listas. Suficiente para o porte esperado; registrar o limite (histórico de entregas deve ter filtro de data obrigatório e índice (delivered_at)).
  6. Status de colaborador sem máquina de estados. block/unblock/terminate aceitam qualquer origem (é possível desbloquear um DESLIGADO). Regras de transição explícitas no domínio.
  7. Delivery.latitude/longitude/deviceInfo existem no schema sem requisito correspondente. Coletar geolocalização é dado pessoal (LGPD): manter apenas se houver base legal; senão, remover.

7. Código duplicado (3.5)

Tipo Onde Duplicação Ação
Componentes 6 telas de features/delivery/* Rodapé fixo com CTA (sticky bottom-0 border-t … max-w-xl + <Button fullWidth size="lg">) copiado 6×; wrapper de página flex min-h-screen flex-col bg-neutral-50 copiado 6× FlowLayout com title, children, footer
Componentes select-epis-page.tsx, history-page.tsx, scanner-page.tsx Campo de busca com ícone repetido; history-page reimplementa o <Input> com <input> cru e as mesmas classes SearchInput
Componentes dashboard-page.tsx Cabeçalho e resumo renderizados duas vezes (mobile e desktop) com markup quase igual Um bloco responsivo
Navegação nav-items.ts, menu-page.tsx (ACTIVE_ITEMS), bottom-nav.tsx (MOBILE_ITEMS) Lista de destinos repetida Fonte única de navegação, filtrada por perfil
Ações sidebar.tsx, menu-page.tsx Botão "Sair" duplicado (e ambos mortos) useLogout() + um componente
Constantes de domínio mock-data.ts (DELIVERY_REASONS), schema (inexistente), API (inexistente) Motivos só existem no frontend Enum no schema/contrato; rótulos na UI
Tipos/enums Prisma, */domain/*.ts, *.schemas.ts (Zod), web/.../types.ts, packages/types EmployeeStatus, CaSituation, EpiControlType, UserRole definidos 3–4× packages/contracts (M-04)
Controllers HTTP 7 controllers da API Mesmo formato create/update/getById/list com paginationQuerySchema.parse(req.query) Helper pequeno parsePage(req); não criar controller genérico
Repositórios 6 repositórios Prisma where → Promise.all([findMany, count]) Helper paginate(model, args)
Serviços 6 serviços getOrThrow e list (cálculo de skip) idênticos Aceitável. Extrair só toPage(page, pageSize)
Lógica avatar.tsx initials() e legado initials() Mesma função — (o legado sai)
Legado apply_ui_v2.py × apply_ui_v2.ps1 × ui_v2_styles.css Mesmo restyle em três arquivos Remover (B-02)

8. Segurança (3.6)

Tema Situação Severidade Referência
XSS Código novo: React escapa, sem dangerouslySetInnerHTML. Legado: innerHTML com template strings (${e.nome}, onclick='…("${e.mat}")'), seguro só porque os dados são fictícios; não portar esse padrão. Futuro: a imagem da assinatura deve ser servida com Content-Type: image/png fixo e nunca como SVG enviado pelo cliente. Baixo hoje —
Injection (SQL) Só Prisma parametrizado; nenhum $queryRaw. O UPDATE condicional de estoque (C-03) deve usar $executeRaw com template tag (parametrizado), nunca $executeRawUnsafe. OK C-03
IDOR Sem escopo por unidade; qualquer autenticado lê qualquer colaborador/empresa por ID. Alto A-06
Exposição de dados CPF, e-mail e telefone em listas para todos os perfis. GET /auth/me devolve só {id, role} do token. Alto A-06
Autenticação fraca Bom desenho (bcrypt, refresh opaco com hash, cookie httpOnly/secure em produção, rotação). Falhas: corrida e reuso do refresh (A-04), revogação atrasada (A-05), timing (M-09), sem fluxo de troca/reset de senha nem gestão de usuários. Alto A-04, A-05, M-09
Autorização authorize() presente em todas as escritas; leituras sem restrição de perfil. O frontend não esconde nem protege nada (sem login). Alto A-02, A-06
Secrets no frontend Nenhum. VITE_API_URL é a única variável (pública por natureza). OK —
Secrets no repositório Nenhum segredo real no histórico. Credenciais do docker-compose são de desenvolvimento (aceitável, documentar). .env local usa o placeholder do exemplo. Baixo —
Armazenamento inseguro Nada em localStorage. O desenho previsto (access token em memória + refresh httpOnly) é o correto; manter. A assinatura (data URL) fica só em memória: não persistir em storage do navegador. OK A-02
Manipulação de IDs UUIDs nos bodies validados por Zod; :id nas rotas não validados (vão direto ao Prisma; inofensivo hoje porque as colunas são text, mas retornam 404/500 inconsistentes). QR deve usar código opaco. Médio M-03, A-03
Endpoints sem proteção Públicos: /health, /auth/login, /auth/refresh, /auth/logout, /docs. /docs não deveria ser público em produção. Médio M-05
Validação server-side Presente em todos os bodies (Zod) e nas queries de listagem. Faltam: params, regras de domínio (quantidade máxima, transições de status, ciclo de setor), normalização de e-mail. Médio M-03, M-08, M-09
Rate limiting Existe, mas é ineficaz atrás de proxy e só por IP. Alto A-10
Vazamento via erros O handler devolve 500 genérico e loga o stack (correto). notFound ecoa a URL. ZodError devolve fieldErrors (aceitável). Erros do Prisma não mapeados viram 500 em vez de mensagens úteis. Médio A-08, B-08
CORS Origem única via env, credentials: true. OK. Validar que CORS_ORIGIN não seja * em produção (o schema do env deveria exigir URL). Baixo —
Headers helmet() padrão. Faltam CSP específica para a SPA (servida pelo host do front) e HSTS no proxy. Baixo —
CSRF Cookie de refresh SameSite=Lax + path=/auth + POST-only: protegido no cenário mesmo-site. Depende do deploy M-15
Uploads multer instalado, nenhum upload ainda. Assinatura e fotos precisarão de limite de tamanho, validação de MIME por magic bytes e armazenamento fora do webroot com nome gerado pelo servidor. Futuro —
Body limit express.json({ limit: "2mb" }) global. A assinatura em base64 cabe; reduzir o global para ~100 KB e liberar 2 MB só na rota de entrega. Baixo —
Logging winston sem redação de campos sensíveis; hoje não loga bodies (bom). Ao adicionar log de requisição, redigir password, authorization, cookies, CPF. Baixo —
LGPD Foto, CPF, geolocalização e assinatura são dados pessoais. Faltam política de retenção e minimização. Médio §6.7

9. Performance (3.7)

Tema Achado Ação
Bundle Chunk único de 769 KB (232 KB gzip); html5-qrcode (~350 KB min) e o canvas de assinatura carregados no login Lazy routes + import dinâmico (M-01). Medir com vite build antes/depois
Imports desnecessários Nenhum import morto (tree-shaking remove o que não é importado); as dependências não usadas só pesam no install §12
Renders Seletores do zustand granulares (bom). Object.values(selected) a cada render é trivial. Não há problema de render mensurável; não adicionar memo preventivo —
Requests duplicados Não há requests. Ao integrar: React Query com staleTime já configurado (30 s); catálogo de EPIs com staleTime maior; busca de colaborador com debounce de 250–300 ms Fase 6
Listas Catálogo e histórico renderizam tudo. Com paginação no servidor (já existe na API) não é necessário virtualizar Paginação/infinite query no histórico
Loading Nenhum estado de loading/erro/vazio além do "Nenhum … encontrado" Seção 22: idle/loading/success/error/empty em toda operação
Imagens Novo: só ícones SVG (lucide). Legado: fotos do Unsplash (externas). Fotos de colaborador futuras: miniatura redimensionada no servidor, loading="lazy", width/height fixos —
Fontes Google Fonts bloqueante Self-host (B-09)
API validateReferences faz 4–5 consultas sequenciais (paralelizáveis); faltam índices para as buscas futuras: employees(name) (trigram ou lower(name)), employees(registration), deliveries(delivered_at), deliveries(employee_id, delivered_at), stock_movements(created_at) Fase 5
Cálculos repetidos Nenhum relevante —

10. Responsividade e acessibilidade (Seções 20–21)

  • Mobile: as telas do fluxo usam max-w-xl centralizado e rodapé fixo; o padrão é bom para 360–430 px. Não testado em dispositivo nesta auditoria. O overflow-x: hidden global (M-13) pode estar escondendo estouros.
  • Desktop: sidebar a partir de lg. O fluxo de entrega continua com layout de celular centralizado no desktop. Para o almoxarifado (desktop-first) faltará uma versão densa: busca + seleção + quantidades na mesma tela.
  • Alvos de toque: botões de 44–48 px (bom). Os ícones do cabeçalho têm 36 px (h-9 w-9), abaixo de 44 px.
  • Acessibilidade: ver M-10. Positivos: lang="pt-BR", aria-label nos ícones, <label htmlFor> no login, <h1> por tela.

11. Banco de dados

11.1 Schema atual (resumo)

Company 1─N BusinessUnit 1─N Department (auto-relação parent/children)
                         1─N Warehouse 1─N StockItem N─1 EpiItem
                         1─N Employee  N─1 Department, N─1 JobRole, N─1 Employee (supervisor)
User 0..1─1 Employee · User 1─N RefreshToken · User 1─N AuditLog · User 1─N StockMovement · User 1─N Delivery (deliveredBy)
JobRole N─N EpiItem (JobRoleRequiredEpi, com quantity)
EpiItem 1─N EpiCA
StockItem 1─N StockMovement
Delivery 1─N DeliveryItem N─1 EpiItem, N─0..1 StockItem · DeliveryItem 1─0..1 EpiReturn
Employee 1─N Training · Employee 1─N Delivery

Pontos positivos: UUIDs, snake_case via @map, createdAt/updatedAt, onDelete: Restrict nas relações críticas, uniques compostos ([businessUnitId, registration], [companyId, code]).

11.2 Inconsistências e lacunas

# Problema Proposta
1 Employee.companyId sem relação/FK, redundante com businessUnit.companyId Remover (derivar pela unidade) ou criar a FK + checagem de consistência
2 Employee.cpf @unique global × soft delete Único parcial WHERE deleted_at IS NULL
3 RefreshToken.tokenHash sem índice/unique @unique
4 StockItem.quantity sem CHECK >= 0; sem tamanho CHECK; coluna size (nullable) e unique (epiItemId, warehouseId, size, batchNumber)
5 StockMovementType incompleto; movimentação sem balanceBefore/After, sem deliveryItemId/returnId Completar (C-03)
6 Delivery sem status, reason, notes, warehouseId, idempotencyKey Adicionar; reason como enum (DESGASTE, DANO, ROTINA, ATIVIDADE_ESPECIAL, PERDA, EXTRAVIO, NOVO_COLABORADOR, OUTRO) + reasonDetail
7 DeliveryItem sem size, notes, caNumber (snapshot), epiName (snapshot) Adicionar (C-04)
8 Assinatura como signatureUrl solto Entidade DeliverySignature (C-04)
9 EpiItem.category texto livre Entidade EpiCategory (o domínio pede "categoria")
10 EpiCA.situation gravada (VENCIDO fica desatualizado) Derivar de expiresAt; gravar só CANCELADO manual
11 User sem name, sem vínculo com almoxarifado/unidade, sem tokenVersion, mustChangePassword, failedLoginCount/lockedUntil Adicionar conforme as decisões de §14
12 Employee sem badgeCode (QR) Adicionar (A-03)
13 EpiReturn.restockedToStockId sem FK Relação com StockItem
14 Department sem proteção a ciclos Validação no serviço
15 Delivery.latitude/longitude Remover salvo requisito com base legal

Não há dados em produção (nenhuma migration foi aplicada em lugar algum rastreável). Corrigir o schema antes de gerar a migration inicial é a opção de menor custo e risco. Se existir um banco local com dados de teste, ele pode ser recriado pelo seed.


12. Dependências (Seção 31)

apps/web

Dependência Decisão Justificativa
react, react-dom 19 MANTER Base
react-router-dom 7 MANTER Já usado. Adotar lazy por rota
@tanstack/react-query 5 MANTER Server state (cache, dedupe, retry) quando integrar a API
zustand 5 MANTER Só para o rascunho do fluxo de entrega (UI state entre telas). Não usar para server state
axios SUBSTITUIR por fetch tipado (~40 linhas) Usado em 0 lugares. Interceptor de refresh é simples com fetch. Remove ~13 KB gzip e uma dependência. Alternativa aceitável: manter axios se preferirem interceptors prontos.
react-hook-form + @hookform/resolvers MANTER (passarão a ser usados) Formulários de login e cadastros (EPI, colaborador, estoque)
zod (web) MANTER Virá de packages/contracts; validar formulários com os mesmos schemas da API
framer-motion REMOVER Não usado; a Seção 19 pede evitar animações inúteis
html5-qrcode MANTER (com lazy) Funciona e já está integrado. Biblioteca sem manutenção ativa: reavaliar (@zxing/browser ou BarcodeDetector nativo) se der problema em iOS
react-signature-canvas MANTER (com lazy) Pequena e adequada
class-variance-authority, clsx, tailwind-merge MANTER Base do design system
lucide-react MANTER Ícones tree-shaken
@epi-manager/types SUBSTITUIR Por @epi-manager/contracts
tailwindcss 4, @tailwindcss/vite MANTER
novas: vitest, @testing-library/react, @testing-library/user-event, jsdom, msw ADICIONAR (dev) Testes de componentes/fluxo com API simulada (msw evita mocks manuais de fetch)
nova: eslint-plugin-jsx-a11y ADICIONAR (dev) Acessibilidade (M-10)
nova: @fontsource/inter, @fontsource/sora ADICIONAR Self-host das fontes da marca (B-09)

apps/api

Dependência Decisão Justificativa
express 5, helmet, cors, cookie-parser, express-rate-limit MANTER Usados e adequados
zod 4 MANTER Validação + contratos
bcrypt MANTER Funciona. (argon2 seria opcional, sem necessidade)
jsonwebtoken MANTER Fixar algoritmo (A-05)
winston MANTER Já integrado. (pino seria mais rápido, mas sem ganho que justifique a troca)
swagger-jsdoc + swagger-ui-express SUBSTITUIR swagger-jsdoc por geração a partir do Zod (zod-openapi) e MANTER swagger-ui Elimina JSDoc duplicado e documentação que diverge do código (M-05)
multer REMOVER agora; readicionar quando houver upload de foto Não usado
dotenv MANTER
novas: supertest ADICIONAR (dev) Testes HTTP de rotas, autenticação e autorização
nova: testcontainers (ou Postgres de serviço no CI) ADICIONAR (dev) Testes de integração de estoque/concorrência contra Postgres real

Raiz / packages

Item Decisão Justificativa
prisma / @prisma/client 7.9 + @prisma/adapter-pg + pg MANTER Já é a stack-alvo
bcrypt, @types/bcrypt, @prisma/client, @epi-manager/database na raiz MOVER para packages/database (junto com o seed) A raiz não deveria ter dependências de runtime
turbo ATUALIZAR para alinhar com a versão em uso (2.10.x) B-06
husky, commitlint, prettier MANTER; ADICIONAR lint-staged B-05
legado/App/** (MUI, emotion, chart.js, react-hook-form, oxlint, vite) REMOVER Scaffold não usado (B-02)

13. Testes — situação e prioridade

Situação: 4 arquivos e 16 testes, todos unitários de serviço com repositórios simulados (login, company, employee, epi-ca). Eles têm valor real (cobrem regras de unicidade, referência cruzada e credenciais) e devem ser mantidos.

Lacunas, em ordem de prioridade (Seção 32):

  1. Estoque e entrega: integração contra Postgres real, incluindo o teste de concorrência (N entregas paralelas com saldo 1 → exatamente 1 sucesso, saldo final 0, N−1 recebem 409 ESTOQUE_INSUFICIENTE) e idempotência (mesma chave → uma entrega).
  2. Autenticação: refresh com rotação, reuso de token revogado, logout, usuário inativo.
  3. Autorização: matriz perfil × rota via supertest (cada rota de escrita retorna 403 para os perfis não permitidos), escopo por unidade (IDOR).
  4. Entrega (domínio): colaborador não ativo, CA vencido, quantidade fora de 1..50, snapshot de CA, assinatura obrigatória.
  5. Assinatura: hash calculado e verificável; entrega imutável após assinada.
  6. Web: fluxo de entrega ponta a ponta com msw (incluindo erro de estoque insuficiente exibindo mensagem específica e o botão desabilitado durante o envio); guarda de rotas; interceptor de refresh.

14. Decisões

14.1 Decididas (2026-10-06)

# Decisão
1 Colaborador não faz login nesta fase: assina no aparelho do almoxarife, como no legado. Login próprio (matrícula + PIN) fica para uma fase posterior.
2 Perfis: ADMIN e ALMOXARIFADO. COLABORADOR entra junto com o login do colaborador. Os outros 5 perfis do schema saem.
3 App web único e responsivo, com layout e rotas por perfil; PWA depois. apps/mobile-pwa será removido.
4 legado/ será enxugado: ficam index.html + screenshots; o logo é extraído; scaffold, scripts e landing saem (permanecem no histórico git).

14.2 Em aberto

Os itens 1–4 abaixo foram decididos (ver 14.1); os itens 5–7 continuam em aberto.

  1. O colaborador faz login? No legado e nas telas atuais, quem opera é o almoxarife; o colaborador só assina no aparelho do almoxarife. A Seção 10 descreve um fluxo próprio do colaborador. Opções:

    • (a) manter como no legado (sem login do colaborador; COLABORADOR é uma pessoa, não um usuário);
    • (b) colaborador com login próprio (matrícula + PIN) para consultar suas entregas e assinar;
    • (c) (a) agora e (b) depois.

    Recomendação: (c).

  2. Perfis: reduzir os 7 perfis do schema para os 3 do pedido (ADMIN, ALMOXARIFADO, COLABORADOR), ou manter alguns (ex.: TECNICO_SEGURANCA, CONSULTA)? Recomendação: ADMIN, ALMOXARIFADO e, se (b)/(c), COLABORADOR. Adicionar outros só quando houver um usuário real com necessidade distinta.

  3. Um app ou dois? apps/mobile-pwa está vazio. Recomendação: um único app web responsivo (rotas e layout por perfil) e, depois, torná-lo PWA. Remover apps/mobile-pwa.

  4. Pasta legado/: manter só index.html + screenshots como referência e remover scaffold, scripts e a landing de 3,6 MB (após extrair o logo)? Tudo permanece no histórico git.

  5. Tamanho e lote: os EPIs têm grade de tamanho (bota 38–44, luva P/M/G) no estoque? Há mais de um almoxarifado por unidade em uso real?

  6. Deploy: web e API ficarão no mesmo domínio (ex.: proxy /api)? Isso define a política do cookie (M-15) e o trust proxy (A-10).

  7. Login por e-mail ou por usuário? O legado mostra "Usuário" (marcio.almox); a API usa e-mail.


15. Próximo passo

Com as respostas de §14, a Fase 2 (Planejamento) produzirá o plano de refatoração com a arquitetura proposta e a ordem de execução. A ordem prevista, ajustável pelas decisões acima:

  1. Schema + migration inicial (C-02, C-03, C-04, A-09, §11).
  2. Contratos compartilhados (M-04) e endurecimento de tipos/erros na API (M-03, A-08).
  3. Segurança de auth (A-04, A-05, A-10, M-09) + auditoria (A-07).
  4. Módulos de estoque e entrega com transação e controle de concorrência + testes de integração.
  5. Web: auth/guardas (A-02) → identidade ENGENOVA (A-01) → integração do fluxo com a API, estados de UI e recuperação dos comportamentos perdidos (A-12, M-02) → lazy loading (M-01) → acessibilidade (M-10).
  6. CI completo, limpeza (§12, B-01, B-02) e documentação (README, ARCHITECTURE.md, SECURITY.md).

Cada etapa termina com lint + typecheck + test + build verdes e uma entrada de changelog no formato ATUAL → problema / NOVO → solução / IMPACTO → compatibilidade.


16. Situação após a refatoração (2026-10-06)

Legenda: ✅ resolvido · 🟡 parcial · ⏳ pendente (com motivo)

ID Situação Como foi resolvido / observação
C-01 ✅ POST /api/deliveries grava tudo numa transação; a tela de sucesso só aparece com os dados devolvidos pela API (testado no Edge de ponta a ponta).
C-02 ✅ Migration inicial versionada (database/prisma/migrations/20261006000000_init), aplicada com migrate deploy; CI confere ausência de drift.
C-03 ✅ Baixa condicional atômica + CHECK (quantity >= 0) + razão de movimentações com saldo antes/depois. Teste: 10 entregas simultâneas por 1 unidade → 1 sucesso, saldo 0.
C-04 ✅ DeliveryItem congela nome e CA; DeliverySignature com PNG validado, SHA-256, data/hora, IP/UA; entrega exige CA vigente.
A-01 ✅ Paleta, tipografia (Sora/Inter self-hosted), logo e textos do legado, com ajustes de contraste AA.
A-02 ✅ Sessão em memória + refresh por cookie, guardas de rota, logout real, troca de senha obrigatória.
A-03 ✅ badgeCode opaco e reemitível; GET /employees/by-badge/:code com escopo; QR gerado na ficha do colaborador.
A-04 ✅ tokenHash único, rotação atômica, detecção de reuso com revogação total e auditoria.
A-05 ✅ Claims validadas, alg/iss/aud fixos, tokenVersion e active conferidos a cada requisição.
A-06 ✅ GETs com perfil e escopo por unidade; CPF mascarado e crachá omitido para o almoxarifado.
A-07 ✅ recordAudit na mesma transação para login, sessão, cadastros, status, permissões, estoque, entregas, devoluções.
A-08 ✅ Erro estruturado {error:{code,message,details}}; Prisma P2002/P2025/P2003 mapeados; web traduz por código.
A-09 ✅ Schema revisado (ver docs/refactor-plan.md, Etapa 1).
A-10 ✅ TRUST_PROXY, bloqueio por conta no banco, limitadores separados para login e refresh (o e2e revelou que o refresh consumia o limite do login; corrigido).
A-11 ✅ Perfis ADMIN e ALMOXARIFADO (decisão 14.1).
A-12 🟡 Recuperados: busca por nome/matrícula, limite 50, filtros de período, busca por matrícula, detalhe, motivo "Outro" com descrição. Mudança justificada: o item "Outro (especificar)" do catálogo foi removido — EPI sem cadastro não tem CA nem estoque e não pode ser entregue conforme NR-6; o caminho é cadastrar o EPI. Quantidades passaram a ser informadas numa única tela (menos toques que o passo a passo do legado).
M-01 ✅ Lazy por rota; carga inicial ≈ 157 KB gzip (antes 233 KB num chunk único); leitor de QR (107 KB gzip) só na tela do scanner.
M-02 ✅ Rascunho reiniciado em todo ponto de entrada; idempotencyKey; navegação pós-sucesso com replace.
M-03 ✅ Helper handler() valida params/query/body e entrega tipos inferidos; IDs validados como UUID.
M-04 ✅ packages/contracts é a fonte única de enums, schemas e DTOs.
M-05 🟡 /docs (swagger-jsdoc) removido por estar vazio no build e exposto em produção. A referência de rotas está no README; gerar OpenAPI a partir dos schemas Zod (z.toJSONSchema) fica como próximo passo.
M-06 ✅ contracts e database compilados para dist.
M-07 ✅ CI com Postgres de serviço: generate/validate, format, lint, typecheck, testes, integração, drift e build.
M-08 ✅ deletedAt removido (desligado = status), ciclo de setores e supervisor validados.
M-09 ✅ Hash fictício real para e-mail inexistente; e-mail normalizado.
M-10 ✅ Foco visível, rótulos (o e2e encontrou e corrigiu o campo de senha sem rótulo), contraste AA, controles mortos removidos ou implementados (lanterna real), jsx-a11y.
M-11 ✅ 103 testes: 34 unitários API + 28 integração (Postgres real) + 9 contratos + 20 web; e2e no Edge (fora do repositório) validou o fluxo completo.
M-12 ✅ Removidos: axios, framer-motion, multer, swagger-*; adicionados só com uso real (§12).
M-13 ✅ overflow-x: hidden removido; 0 overflow medido em 7 telas × 6 larguras.
M-14 ✅ Seed bloqueado em produção, senhas por variável, ADMIN com troca obrigatória.
M-15 ✅ Mesmo site via proxy /api; cookie SameSite=Strict.
B-01, B-02 ✅ Pacotes vazios, mobile-pwa, scaffold e scripts do legado removidos (decisões 3 e 4).
B-03, B-04 ✅ Acentuação na UI e no seed; nome/almoxarifado/versão vêm da sessão e do package.json.
B-05 ⏳ Pre-commit ainda roda o lint completo; lint-staged é melhoria opcional.
B-06 ✅ Comandos via pnpm/turbo local.
B-07, B-08, B-09, B-10, B-11, B-12 ✅ bcrypt 12; 404 sem eco de URL; fontes self-hosted; README reescrito; código morto removido; login sem política de senha.

Pendências conhecidas

  • Mensagens de erro geradas pela API ainda estão sem acentuação (o web substitui as mais comuns por textos acentuados por código).
  • Recuperação de senha por e-mail, cancelamento de entrega (status CANCELADA existe no modelo, sem fluxo), relatórios/estatísticas e telas de cadastro organizacional (via API/seed por enquanto).
  • Login do colaborador (decisão 14.1, fase futura) e PWA/offline.