Skip to content

Versao 1#15

Closed
CJVW-Code wants to merge 7 commits intomainfrom
versao_1
Closed

Versao 1#15
CJVW-Code wants to merge 7 commits intomainfrom
versao_1

Conversation

@CJVW-Code
Copy link
Copy Markdown
Owner

@CJVW-Code CJVW-Code commented Apr 26, 2026

Summary by CodeRabbit

Notas da Versão

  • Novos Recursos

    • Página de configurações do sistema para gerenciar janelas de acesso ao BI e timezone.
    • Funcionalidade de distribuição de casos entre defensores.
    • Novo papel "Gestor" com visibilidade global e controles administrativos.
    • Bloqueio de BI por horário configurável com opção de liberação temporária.
  • Melhorias de Segurança

    • Isolamento de unidades para prevenir acesso não autorizado entre departamentos.
    • Controle de acesso refinado baseado em funções e status de caso.
  • Testes e Qualidade

    • Suite completa de testes automatizados (backend e frontend).
    • Pipeline de CI/CD com validações de segurança e auditoria de vulnerabilidades.

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 26, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
maes-acao Ready Ready Preview, Comment Apr 26, 2026 9:24pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 26, 2026

📝 Walkthrough

Walkthrough

Implementa infraestrutura completa de CI/CD (GitHub Actions), hardening de segurança com novo middleware requireSameUnit para isolamento por unidade, expansão granular do RBAC com roles gestor e coordenador, cache em memória de configurações do sistema, distribuição de casos via modal, bloqueio de horário para relatórios BI, e suites de testes automatizados (Jest backend, Vitest frontend) com coverage reports.

Changes

Cohort / File(s) Resumo
CI/CD Workflow
.github/workflows/ci.yml
Novo workflow que executa linting, testes Jest/Vitest em paralelo com coverage, auditoria de segurança (npm audit), verificação de sintaxe/build, e gating de jobs upstream.
Documentação de Referência
arquivos/Conhecimento/01-Referencia/*
Atualiza ARCHITECTURE.md (v4.1) com isolamento de unidade e RBAC hierárquico, BUSINESS_RULES.md (v3.1) com modelo de permissões expandido, routes.md (v4.0) com isolamento global via requireSameUnit.
Histórico de Implementação
arquivos/Conhecimento/04-Historico/*
Adiciona plano de implementação detalhado, checklist de tarefas finalizadas, guia de testes, documentação de hardening backend (2026-04-26), e registros de suite CI/CD (2026-04-25).
Configuração Jest
backend/jest.config.js
Expande testMatch, adiciona collectCoverageFrom seletivo, coverageThreshold com limites mínimos, saídas lcov/html, e timeout de 15s.
Infraestrutura Backend
backend/prisma/schema.prisma, backend/seed_permissions.js
Adiciona modelo configuracoes_sistema para armazenar chave-valor; seed agora cria role gestor e configurações BI padrão.
Middleware de Autorização
backend/src/middleware/auth.js, backend/src/middleware/requireSameUnit.js, backend/src/middleware/requireWriteAccess.js
Normaliza cargo para minúsculas, adiciona novo requireSameUnit para validar acesso por unidade/assistência, expande requireWriteAccess para incluir gestor.
Cache & Utilidades
backend/src/utils/configCache.js, backend/src/utils/helpers.js, backend/src/utils/stateMachine.js
Implementa cache TTL (5min) para configuracoes_sistema, safeFormData/safeJsonParse para normalizar JSONB, state machine com TRANSICOES_PERMITIDAS e validateTransition.
Controladores Backend
backend/src/controllers/configController.js, backend/src/controllers/biController.js, backend/src/controllers/casosController.js, backend/src/controllers/lockController.js, backend/src/controllers/defensoresController.js
Novo configController para CRUD de configurações; BI agora bloqueia horário, restringe unidades por role, calcula produtividade; casos adicionam endpoint distribuirCaso com validação Supabase; unlock expandido para admin/gestor/coordenador; defensores listagem aberta para gestor/coordenador com filtro de unidade.
Rotas Backend
backend/src/routes/casos.js, backend/src/routes/config.js, backend/src/routes/bi.js, backend/server.js
Adiciona requireSameUnit em rotas /:id, novo endpoint POST /:id/distribuir, novo módulo config.js com proteção admin/gestor, expande autorização BI, monta /api/config, adiciona CORS para domínio produção.
Testes Backend — Configuração
backend/jest.config.js, backend/tests/setup.js, backend/tests/dicionarioAcoes.test.js, backend/tests/documentGenerationService.test.js
Remove mocks de aiService do setup global; adiciona diretivas ESLint no-unused-vars em test files.
Testes Backend — Middleware
backend/tests/middleware/auth.test.js, backend/tests/middleware/apiKey.test.js, backend/tests/middleware/requireWriteAccess.test.js, backend/tests/middleware/requireSameUnit.test.js
Suites abrangentes validando JWT, API keys, whitelist de write-access, unit isolation com Supabase mock.
Testes Backend — Integração
backend/tests/integration/health.test.js, backend/tests/integration/casosRoutes.test.js, backend/tests/integration/lockRoutes.test.js, backend/tests/integration/scannerRoutes.test.js
Testes E2E com Supertest cobrindo rotas de saúde, casos, lock/unlock, scanner autenticação.
Testes Backend — Segurança & Unitários
backend/tests/security/injection.test.js, backend/tests/security/rateLimiter.test.js, backend/tests/unit/aiService.test.js, backend/tests/unit/securityService.test.js, backend/tests/unit/utils.test.js
Validam proteção contra SQL injection/XSS, rate limiting, PII sanitization, hashing, state transitions e helpers utilitários.
Configuração Vitest
frontend/package.json, frontend/vitest.config.js
Adiciona scripts test/watch/coverage, dependências vitest/@vitest/coverage-v8/jsdom, config com alias @, jsdom runtime, geração de reports text/lcov/html.
Testes Frontend
frontend/src/__tests__/apiBase.test.js, frontend/src/__tests__/caseUtils.test.js, frontend/src/__tests__/formatters.test.js, frontend/src/__tests__/submissionValidation.test.js
Suites Vitest para utilitários, formatadores, validadores, e fluxo de submissão de caso.
Contexto & Hooks Frontend
frontend/src/areas/defensor/contexts/AuthContext.jsx, frontend/src/areas/defensor/hooks/useBiData.js
AuthContext memoiza permissions (canManageTeam, canViewBi, canEditConfig); useBiData persiste filtros/dados em sessionStorage, atualiza import jsPDF.
Páginas & Componentes Frontend — Admin/Gestor
frontend/src/areas/defensor/pages/Cadastro.jsx, frontend/src/areas/defensor/pages/GerenciarEquipe.jsx, frontend/src/areas/defensor/pages/Dashboard.jsx, frontend/src/areas/defensor/pages/Casos.jsx
Adiciona opções servidor/coordenador/gestor em dropdowns; badges para novos roles; dashboard prefixo honorífico por cargo, shortcuts permissionados; casos filtram por unidade (admin/gestor vê todas), abrem ModalDistribuicao.
Páginas Frontend — BI & Configuração
frontend/src/areas/defensor/pages/Relatorios.jsx, frontend/src/areas/defensor/pages/ConfiguracoesSistema.jsx
Relatórios bloqueiam por horário, escondem quando bloqueadoPorHorario, auto-atualizam filtros, exibem ranking produtividade_defensores; nova página ConfiguracoesSistema gerencia horários BI, timezone, liberação temporária.
Componentes Frontend — Modal & Routing
frontend/src/areas/defensor/components/casos/ModalDistribuicao.jsx, frontend/src/areas/defensor/components/layout/Sidebar.jsx, frontend/src/App.jsx, frontend/src/areas/defensor/pages/DetalhesCaso.jsx
Nova ModalDistribuicao com busca/seleção de defensores; Sidebar usa permissions para visibilidade; App adiciona rotas permission-gated GestorRoute/BiRoute/TeamRoute para /configuracoes; DetalhesCaso inclui ação Distribuir.
Formulários & Utilidades Frontend
frontend/src/areas/servidor/components/StepDadosPessoais.jsx, frontend/src/areas/servidor/components/secoes/SecaoProcessoOriginal.jsx, frontend/src/areas/servidor/services/submissionService.js, frontend/src/contexts/ToastContext.jsx, frontend/src/utils/apiBase.js
Remove prop configAcao em StepDadosPessoais; atualiza texto/links em SecaoProcessoOriginal; remove cálculo requeridoEmailTrim; expõe addToast em ToastContext; refatora authFetch event dispatch via createEvent.
Coverage Reports
frontend/coverage/*, frontend/coverage/lcov-report/*
Gera estrutura completa de relatórios HTML/LCOV: páginas index, diretórios, arquivos individuais, CSS base, scripts sorter/prettify/block-navigation, arquivo lcov.info com dados de cobertura.

Sequence Diagrams

sequenceDiagram
    actor User as Defensor/Coordenador
    participant Frontend as Frontend App
    participant API as Backend API
    participant Supabase as Supabase<br/>(BD + RLS)
    participant Cache as Config Cache
    
    User->>Frontend: Acessa "Distribuir Caso"
    Frontend->>API: GET /defensores (lista para seleção)
    API->>API: Valida auth token
    API->>Supabase: Fetch defensores
    Supabase-->>API: Lista defensores
    API-->>Frontend: [{ id, nome, email }]
    
    Frontend->>User: Exibe modal com busca
    User->>Frontend: Seleciona defensor
    Frontend->>API: POST /casos/:id/distribuir<br/>{ usuario_id }
    
    API->>API: authMiddleware (valida JWT)
    API->>API: requireSameUnit (valida unidade)
    API->>API: validateTransition (status permitido?)
    alt Status válido
        API->>Supabase: UPDATE casos<br/>SET defensor_id=:usuario_id<br/>WHERE id=:id (RLS check)
        Supabase-->>API: ✓ Updated
        API->>Supabase: INSERT audit_log<br/>(distribuição registrada)
        API-->>Frontend: { success: true }
        Frontend->>User: "Caso distribuído!"
    else Status inválido
        API-->>Frontend: { ok: false, reason: "..." }
        Frontend->>User: "Transição não permitida"
    end
Loading
sequenceDiagram
    actor User as Usuário (defensor/coordenador)
    participant Frontend as Frontend - Relatórios
    participant API as Backend BI Controller
    participant Cache as ConfigCache<br/>(5min TTL)
    participant DB as Prisma/BD
    
    User->>Frontend: Acessa "/painel/relatorios"
    Frontend->>API: GET /api/relatorio/gerar?filtros
    
    API->>API: verificarBloqueioHorario()
    API->>Cache: getConfiguracoes()
    
    alt Cache válido
        Cache-->>API: { bi_horarios, bi_timezone, ... }
    else Cache expirado
        Cache->>DB: SELECT * FROM configuracoes_sistema
        DB-->>Cache: rows
        Cache-->>API: objeto normalizado
    end
    
    API->>API: Compara hora_atual vs horario_permitido<br/>(com timezone)
    alt Fora do horário
        API-->>Frontend: { bloqueadoPorHorario: true }
        Frontend->>User: "BI bloqueado fora do horário"
    else Dentro do horário (ou admin/gestor)
        API->>DB: Busca casos/protocolo com filtros
        API->>DB: SELECT * FROM defensores<br/>(para produtividade)
        DB-->>API: [casos...], [defensores...]
        API->>API: Agrega contagens por defensor<br/>Gera relatório
        API-->>Frontend: { dados, produtividade_defensores }
        Frontend->>User: Exibe tabelas + ranking
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

A complexidade resulta de múltiplos fatores heterogêneos: novo middleware requireSameUnit com Supabase integration e logic de IDOR, expansão significativa do RBAC com roles gestor/coordenador, state machine centralizado para transições, cache TTL com invalidação, nova feature de distribuição com validações e audit logging, bloqueio de horário em BI com timezone-aware checks, suite completa de testes (150+ casos entre backend/frontend), refatoração de controllers para cargo whitelist, e documentação extensiva. Embora muitos arquivos tenham mudanças menores, cada cohort requer raciocínio separado sobre segurança, autorização, e fluxos de negócio.

Possibly related PRs

Poem

🐰 Testes e segurança, que beleza!
Cache, middleware, sem fraqueza,
Gestor e coordenador em ação,
Distribuição de caso, perfeição,
Cobertura à prova de imperfeição!

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 20

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
backend/src/services/geminiService.js (1)

163-167: ⚠️ Potential issue | 🟡 Minor

Remover função não utilizada.

A função _valueOrPlaceholder não é referenciada em nenhum lugar do código. Em vez de manter uma função com prefixo underscore, remova-a completamente:

Remover a função
-const _valueOrPlaceholder = (value, fallback = PLACEHOLDER_FIELD) => {
-  if (value === undefined || value === null) return fallback;
-  const trimmed = `${value}`.trim();
-  return trimmed ? trimmed : fallback;
-};
-
 const cleanText = (value, fallback = "") => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/services/geminiService.js` around lines 163 - 167, A função
_valueOrPlaceholder definida em geminiService.js não é utilizada; remova a
declaração inteira da função (_valueOrPlaceholder) e qualquer referência
residual a PLACEHOLDER_FIELD se só for usada por ela, garantindo que não quebre
linter/exports; verifique também se não há exportações nomeadas ou comentários
que dependam de _valueOrPlaceholder e limpe-os.
backend/src/controllers/lockController.js (1)

14-34: ⚠️ Potential issue | 🟠 Major

Variável computada e nunca usada — remova ou complete a checagem.

Renomear isDefensorOrCoordenador para _isDefensorOrCoordenador apenas suprime o lint, mas a variável continua sendo dead code — o cálculo é executado em todo request sem uso. Mais preocupante: a presença histórica desta flag sugere que havia uma regra de autorização planejada (por exemplo, impedir defensor/coordenador de pegar lock de nível 1, espelhando o bloqueio de servidor/estagiario em nível 2). Hoje, somente isServidorOrEstagiario é checado na linha 32; defensor e coordenador podem assumir tanto nível 1 quanto nível 2 sem validação extra.

Decida explicitamente:

  • Se a regra adicional deve existir, implemente-a (ex.: if (nivelLock === 1 && isDefensorOrCoordenador) return res.status(403)...).
  • Se não deve existir, remova a variável em vez de deixá-la com _ mascarando o problema.
♻️ Proposta de remoção, caso seja realmente dead code
     const isAdmin = cargo === 'admin';
-    const _isDefensorOrCoordenador = cargo.includes("defensor") || cargo === "coordenador";
     const isServidorOrEstagiario = cargo === "servidor" || cargo === "estagiario";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/controllers/lockController.js` around lines 14 - 34, The variable
_isDefensorOrCoordenador is computed but never used, which either hides dead
code or an omitted authorization rule; decide and act: if you want the
additional restriction, restore a meaningful name (isDefensorOrCoordenador) and
add an explicit check mirroring the servidor/estagiario rule, e.g. return
res.status(403)... when nivelLock === 1 and isDefensorOrCoordenador; otherwise
remove the _isDefensorOrCoordenador calculation entirely to avoid unnecessary
computation and linter suppression—update references to nivelLock,
isServidorOrEstagiario and the new/removed isDefensorOrCoordenador accordingly.
frontend/src/areas/defensor/pages/GerenciarEquipe.jsx (1)

257-257: ⚠️ Potential issue | 🟡 Minor

Texto do cabeçalho desatualizado — recepcionistas foi removido do RBAC.

Este PR removeu o cargo recepcao da matriz RBAC (ARCHITECTURE.md, BUSINESS_RULES.md) e introduziu servidor/coordenador no select. A descrição abaixo continua se referindo a "recepcionistas", o que confunde o usuário sobre quais perfis a tela administra.

✏️ Proposta
-          <p className="text-muted">Gerencie unidades, defensores, estagiários e recepcionistas.</p>
+          <p className="text-muted">Gerencie unidades, defensores, servidores, estagiários e coordenadores.</p>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/areas/defensor/pages/GerenciarEquipe.jsx` at line 257, O texto
do cabeçalho em GerenciarEquipe.jsx (o <p className="text-muted">) ainda
menciona "recepcionistas", que foi removido do RBAC; atualize essa string para
refletir os perfis atuais (por exemplo "Gerencie unidades, defensores,
estagiários, servidores e coordenadores." ou uma versão genérica como "Gerencie
unidades e usuários por funções.") — localize a tag p com className="text-muted"
no componente GerenciarEquipe.jsx e substitua apenas o texto exibido para
remover "recepcionistas" e incluir os nomes de cargos novos/atuais conforme a
matriz RBAC.
backend/src/controllers/defensoresController.js (1)

104-124: ⚠️ Potential issue | 🟡 Minor

Fallback "operador" é inválido — alinhe com o novo default "servidor".

O PR padroniza cargos (admin, coordenador, defensor, servidor, estagiario, visualizador) e até troca o default de registrarDefensor (linha 18) para "servidor". Porém o payload do login e a resposta nas linhas 108 e 121 ainda caem para "operador", que não existe em init.sql nem em seed_permissions.cjs. Um usuário sem cargo populado receberá um token com cargo desconhecido e será automaticamente bloqueado por requireWriteAccess (allowedRoles não inclui "operador"), além de gerar inconsistência com o auth.js que tem o mesmo fallback. Recomendo unificar o fallback ou, melhor, recusar o login quando o cargo for nulo.

🛡️ Sugestão
-      cargo: defensor.cargo?.nome || "operador",
+      cargo: defensor.cargo?.nome || "servidor",
       unidade_id: defensor.unidade_id,
     };
     const token = generateToken(payload);
...
-        cargo: defensor.cargo?.nome || "operador",
+        cargo: defensor.cargo?.nome || "servidor",

Idealmente, valide explicitamente:

if (!defensor.cargo?.nome) {
  return res.status(403).json({ error: "Usuário sem cargo configurado." });
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/controllers/defensoresController.js` around lines 104 - 124, The
login handler currently falls back to the non-existent role "operador" in the
payload and response; update the logic in the block that builds payload and
defensor response (where payload is created and token = generateToken(payload)
and res.status(200).json(...) is returned) to either (preferred) reject logins
when defensor.cargo?.nome is missing by returning res.status(403).json({ error:
"Usuário sem cargo configurado." }) or (if backwards-compatible) change the
fallback to the new default "servidor" everywhere (replace "operador" with
"servidor" in the payload and the defensor response), ensuring the same check is
applied as in auth.js so tokens never contain an invalid role.
🧹 Nitpick comments (16)
backend/src/utils/logger.js (1)

2-3: Remover também o código comentado do Logtail.

Os imports do Logtail foram removidos, mas o código comentado nas linhas 59-62 ainda faz referência a Logtail e LogtailTransport. Se o Logtail foi definitivamente removido do projeto, o código comentado também deve ser excluído. Se há intenção de reabilitá-lo no futuro, considere documentar isso com um comentário explicativo.

🧹 Sugestão para remover código comentado obsoleto
-// Adiciona o transporte do Better Stack (Logtail) se o token existir no .env
-// if (process.env.LOGTAIL_SOURCE_TOKEN) {
-//   const logtail = new Logtail(process.env.LOGTAIL_SOURCE_TOKEN);
-//   logger.add(new LogtailTransport(logtail));
-// }
-
 export default logger;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/utils/logger.js` around lines 2 - 3, Remova o bloco de código
comentado que ainda referencia Logtail e LogtailTransport (as ocorrências
comentadas que mencionam Logtail/LogtailTransport) do arquivo logger.js; se
preferir manter a referência para reativação futura, substitua o código
comentado por um comentário explicativo curto indicando porque foi removido e
como reativá-lo (ex.: "Logtail removido temporariamente — instruções para
reativação...") para evitar referências órfãs.
backend/tests/dicionarioAcoes.test.js (1)

1-1: Revisar necessidade de suprimir regra de ESLint.

A diretiva /* eslint-disable no-unused-vars */ foi adicionada, mas todos os imports parecem estar em uso nos testes (logger é usado nas verificações das linhas 97, 105, 113).

Considere remover a supressão global e identificar especificamente quais variáveis (se houver) precisam ser mantidas apesar de não serem usadas, aplicando supressão inline apenas nesses casos.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/tests/dicionarioAcoes.test.js` at line 1, Remove the global directive
/* eslint-disable no-unused-vars */ at the top of the test file and re-run
ESLint; if a specific import truly remains unused (e.g., an import other than
logger), add a localized inline suppression next to that import (/*
eslint-disable-next-line no-unused-vars */) or remove the import entirely,
keeping the logger import as-is since logger is referenced in the tests (search
for logger usage in assertions) and ensure no other unused variables remain.
backend/src/services/geminiService.js (1)

1-1: Revisar necessidade de suprimir regra de ESLint.

A diretiva /* eslint-disable no-unused-vars */ foi adicionada no arquivo. Ao invés de suprimir a regra globalmente, considere:

  1. Remover variáveis e funções não utilizadas
  2. Usar supressão inline apenas onde necessário

Isso ajuda a manter o código limpo e evita acumular código morto.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/services/geminiService.js` at line 1, Remove the top-level "/*
eslint-disable no-unused-vars */" directive and resolve unused symbols instead:
run the linter or inspect backend/src/services/geminiService.js for any unused
variables or functions and either delete them or use localized inline
suppression (e.g., add "/* eslint-disable-next-line no-unused-vars */"
immediately above the specific declaration) for truly required-but-unused names;
ensure exported functions and helpers remain referenced or are removed so the
global suppression is no longer needed.
backend/src/controllers/scannerController.js (1)

1-1: Revisar necessidade de suprimir regra de ESLint.

A diretiva /* eslint-disable no-unused-vars */ foi adicionada, mas todos os imports e variáveis principais do arquivo parecem estar em uso. Considere:

  1. Identificar especificamente quais variáveis estão não utilizadas
  2. Remover essas variáveis ao invés de suprimir a regra
  3. Se a supressão for necessária, use comentários inline apenas nas linhas específicas

Suprimir regras de lint em todo o arquivo pode mascarar problemas futuros e dificultar a manutenção.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/controllers/scannerController.js` at line 1, Remove the blanket
file-level ESLint suppression "/* eslint-disable no-unused-vars */" from
scannerController.js; instead run the linter to identify any truly unused
imports or variables, delete those unused identifiers (or refactor to use them),
and if a suppression is absolutely required apply an inline comment
(eslint-disable-next-line no-unused-vars) immediately above the specific line(s)
only. Locate the top-of-file directive "/* eslint-disable no-unused-vars */" and
any unused imports/const/let declarations in that file and either remove or
narrow the suppression as described.
backend/src/controllers/defensoresController.js (1)

6-6: Remova o import inteiro em vez de silenciar o lint.

Se isSupabaseConfigured não é mais usado neste controller, remova-o (e a diretiva eslint-disable no-unused-vars da linha 1) — isso evita acumular code smell e manter código morto.

♻️ Proposta
-/* eslint-disable no-unused-vars */
 import { prisma } from "../config/prisma.js";
 import { hashPassword, verifyPassword } from "../services/securityService.js";
 import { generateToken } from "../config/jwt.js";
 import logger from "../utils/logger.js";
-import {  isSupabaseConfigured } from "../config/supabase.js";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/controllers/defensoresController.js` at line 6, Remova o import
não utilizado isSupabaseConfigured do topo do arquivo defensoresController.js e
também remova a diretiva eslint-disable no-unused-vars que foi colocada para
silenciá‑lo; procure pelas ocorrências de "isSupabaseConfigured" e pela linha
com "eslint-disable no-unused-vars" e simplesmente elimine ambas para eliminar
código morto e restaurar a checagem do linter.
backend/src/controllers/statusController.js (1)

1-2: Remova o import não usado em vez de suprimir o lint.

isSupabaseConfigured não é referenciado em nenhum ponto deste controller (só Prisma é usado). Manter um import morto + eslint-disable é pior que remover ambos.

♻️ Proposta
-/* eslint-disable no-unused-vars */
-import {  isSupabaseConfigured } from "../config/supabase.js";
 import { prisma } from "../config/prisma.js";

 import logger from "../utils/logger.js";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/controllers/statusController.js` around lines 1 - 2, Remove the
unused import and the lint suppression: delete the "isSupabaseConfigured" import
statement and the top-line "/* eslint-disable no-unused-vars */" in the
statusController (the unused symbol is isSupabaseConfigured) so the file only
imports and uses Prisma as intended; confirm no other references to
isSupabaseConfigured remain before committing.
backend/tests/documentGenerationService.test.js (1)

1-1: Escopo do eslint-disable excessivo.

Desabilitar no-unused-vars no arquivo inteiro só para silenciar e em catch (e) { } é uma marreta. Como o projeto está em ES2019+, prefira a sintaxe optional catch binding e remova a diretiva:

♻️ Proposta
-/* eslint-disable no-unused-vars */
 import { jest } from "@jest/globals";
...
-      } catch (e) {
+      } catch {
         // Template pode ter erros de formatação ({{ duplo) — isso é esperado.
         // O importante é que o lookup funcionou.
       }
...
-      } catch (e) { /* template error esperado */ }
+      } catch { /* template error esperado */ }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/tests/documentGenerationService.test.js` at line 1, Remova a diretiva
/* eslint-disable no-unused-vars */ do topo do arquivo e substitua quaisquer
blocos de captura que usam catch (e) { ... } por catch { ... } (uso de optional
catch binding no ES2019+) em testes como os dentro de
documentGenerationService.test.js; após a alteração execute o linter/CI para
garantir que não há mais avisos e ajuste quaisquer variáveis realmente não
utilizadas se forem encontradas.
backend/tests/unit/securityService.test.js (1)

48-56: O teste de “unicidade” não valida unicidade de fato.

Hoje ele só confirma tipo em Line 54 e Line 55; pode passar mesmo com colisão de protocolo.

✅ Refator sugerido do teste
   it("gera protocolos distintos em chamadas sucessivas (unicidade via timestamp)", async () => {
     const { protocolo: p1 } = generateCredentials("familia");
     await new Promise((r) => setTimeout(r, 2));
     const { protocolo: p2 } = generateCredentials("familia");
-    // Os últimos 6 dígitos são baseados em Date.now(), podem ser iguais se < 1ms.
-    // Verificamos apenas que são strings válidas — unicidade é garantida em produção.
-    expect(typeof p1).toBe("string");
-    expect(typeof p2).toBe("string");
+    expect(p1).toMatch(/^\d{15}$/);
+    expect(p2).toMatch(/^\d{15}$/);
+    expect(p1).not.toBe(p2);
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/tests/unit/securityService.test.js` around lines 48 - 56, The test
"gera protocolos distintos..." currently only asserts types and doesn't validate
uniqueness; update the test that calls generateCredentials to assert the two
protocolos are not equal (e.g., expect(p1).not.toBe(p2)) after the small delay,
and if flakiness is a concern use a slightly larger delay or retry loop to
ensure Date.now()-based suffix differs; update references to generateCredentials
and the local variables protocolo/p1 and p2 so the assertion reflects real
uniqueness rather than just type checking.
frontend/src/__tests__/caseUtils.test.js (1)

60-73: Considere fortalecer asserções e corrigir comentário enganoso.

  • Linhas 60–66: o comentário afirma que String(null) = "null" resultaria em "Null", mas a implementação faz String(tipoAcao || "").trim() — como null é falsy, o resultado é "Não informado". A asserção typeof result === "string" passa, mas o comentário induz ao erro e a asserção deixa a regressão escapar. Vale fixar expect(formatTipoAcaoLabel(null)).toBe("Não informado").
  • Linhas 68–73: o lookup com acento é determinístico ("execução_alimentos" → normaliza para execucao_alimentos → resolve em knownLabels). Pode-se afirmar diretamente expect(result).toBe("Execução de Alimentos") para travar o comportamento.
♻️ Sugestão de ajuste
-  it("retorna 'Não informado' para null (coerção via String(null) → 'null'... não, testa comportamento real)", () => {
-    // String(null) = "null" que não está em knownLabels e não tem " - ",
-    // então resulta em "Null" capitalizado
-    const result = formatTipoAcaoLabel(null);
-    // Não deve lançar erro
-    expect(typeof result).toBe("string");
-  });
+  it("retorna 'Não informado' para null", () => {
+    // null é falsy em `tipoAcao || ""`, portanto cai no early-return de string vazia.
+    expect(formatTipoAcaoLabel(null)).toBe("Não informado");
+  });

   it("normaliza acentos no lookup (chave com acento encontra versão sem acento)", () => {
-    // "execução_alimentos" com acento deve ser normalizado e resolver
-    const result = formatTipoAcaoLabel("execução_alimentos");
-    expect(typeof result).toBe("string");
-    expect(result.length).toBeGreaterThan(0);
+    expect(formatTipoAcaoLabel("execução_alimentos")).toBe("Execução de Alimentos");
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/__tests__/caseUtils.test.js` around lines 60 - 73, Update the
tests to assert exact expected outputs and fix the misleading comment: for the
null case replace the vague typeof check with
expect(formatTipoAcaoLabel(null)).toBe("Não informado") and remove or correct
the comment about String(null); for the accented lookup case assert the
deterministic label by replacing the generic checks with
expect(formatTipoAcaoLabel("execução_alimentos")).toBe("Execução de Alimentos")
so the behavior of formatTipoAcaoLabel and the knownLabels lookup is locked
down.
arquivos/Conhecimento/04-Historico/2026-04-25_suite_testes_cicd.md (1)

86-90: Adicionar linguagem aos blocos de código (markdownlint MD040).

Os blocos de cerca nas linhas 86 e 125 não declaram linguagem, o que viola MD040 reportado pelo linter. Use text (ou similar) para diagramas/árvores de arquivos.

♻️ Ajuste sugerido
-```
+```text
 lint-backend  ──→ test-backend  ──→ build-backend  ──┐
-```
+```text
 .github/
   workflows/
     ci.yml                                        [NEW]

Also applies to: 125-159

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@arquivos/Conhecimento/04-Historico/2026-04-25_suite_testes_cicd.md` around
lines 86 - 90, Os blocos de código no arquivo de histórico estão sem linguagem
(causando MD040); atualize as fences que envolvem o diagrama "lint-backend  ──→
test-backend  ──→ build-backend  ──┐" e o trecho de árvore de arquivos ".github/
workflows/ ci.yml" para declarar uma linguagem como text (ex.: ```text) em ambas
as ocorrências (linhas mostradas no diff) para satisfazer o linter e manter o
formato do diagrama.
backend/tests/security/rateLimiter.test.js (1)

49-59: Caminho de bloqueio é inalcançável e a Promise pode pendurar em casos não testados aqui.

limiter(req, res, () => { ... }) em express-rate-limit v7 é assíncrono; quando o if (res.status.mock.calls.length > 0) é avaliado logo após a chamada, ainda não houve resposta. Isso não afeta os testes atuais (todos esperam o caminho de next() na primeira requisição), mas o ramo blocked: true é morto na prática. Se um teste futuro tentar exercitar bloqueio, a Promise nunca resolve e o caso será reportado como timeout em vez de falha clara.

Sugestão: aguardar o efeito assíncrono via microtask antes de checar res.status ou estender o res para resolver a Promise dentro de res.status/res.json.

♻️ Esboço de ajuste
   return new Promise((resolve) => {
+    const onResponded = () => resolve({ req, res, next, blocked: true });
+    res.status = jest.fn((...args) => { onResponded(); return res; });
     limiter(req, res, () => {
       next();
       resolve({ req, res, next, blocked: false });
     });
-    // Se o limiter chamar res.status (bloqueou), marcamos
-    if (res.status.mock.calls.length > 0) {
-      resolve({ req, res, next, blocked: true });
-    }
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/tests/security/rateLimiter.test.js` around lines 49 - 59, The current
Promise around limiter(req, res, ...) races with express-rate-limit v7 because
limiter is asynchronous; instead of immediately checking res.status.mock.calls,
make the Promise resolve from the response hooks: override/mock res.status (and
res.json if used) to call the original mock and then resolve({ req, res, next,
blocked: true }); keep the limiter callback to call next() and resolve({ req,
res, next, blocked: false }); or, alternatively, await a microtask (e.g.
Promise.resolve().then(...)) before checking res.status; update the helper that
calls limiter(req, res, ...) so resolution is driven from res.status/res.json or
a microtask to avoid hanging and reliably capture both blocked and non-blocked
branches involving limiter, req, res, next.
.github/workflows/ci.yml (2)

318-335: Gate final não inclui explicitamente test-backend/test-frontend.

needs: [build-backend, build-frontend, security-audit] cobre os builds, e como builds dependem dos testes, uma falha em testes torna o build "skipped" e quebra o gate via result != "success" — funciona na prática. Porém, listar explicitamente os jobs de teste tornaria o gate mais legível e robusto a mudanças futuras na cadeia de needs (p.ex., desacoplar build de test). Considere incluir test-backend, test-frontend, lint-backend, lint-frontend em needs e na tabela do summary.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml around lines 318 - 335, Update the final gate step
"Verificar se todos os jobs passaram" to explicitly include the test and lint
jobs in the needs array and the summary table: add test-backend, test-frontend,
lint-backend, lint-frontend to the needs: [...] declaration and append
corresponding lines for each job to the $GITHUB_STEP_SUMMARY table so the script
checks ${{ needs.test-backend.result }}, ${{ needs.test-frontend.result }}, ${{
needs.lint-backend.result }}, and ${{ needs.lint-frontend.result }} alongside
build-backend, build-frontend, and security-audit to make the gate explicit and
robust to future changes.

104-105: --forceExit pode mascarar handles vazando.

--forceExit mata o processo do Jest mesmo quando há sockets, timers ou conexões de DB pendurados. Em CI de uma aplicação que mocka Prisma/Supabase/QStash, isso costuma esconder configurações faltantes de teardown que vão estourar mais tarde em ambientes reais. Vale tentar rodar sem --forceExit (preferindo --detectOpenHandles localmente para diagnosticar) e só reintroduzi-lo se realmente for inevitável.

♻️ Sugestão
-      - name: Run Jest tests with coverage
-        run: npm test -- --forceExit
+      - name: Run Jest tests with coverage
+        run: npm test
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml around lines 104 - 105, The CI step named "Run Jest
tests with coverage" currently runs "npm test -- --forceExit"; remove the
"--forceExit" flag so Jest is allowed to surface open handles instead of being
killed, updating that command to run tests normally (e.g., "npm test --"). For
local debugging you can run Jest with "--detectOpenHandles" or "--runInBand" to
track lingering timers/sockets; only reintroduce "--forceExit" as a last resort
after fixing teardown issues in tests that involve Prisma/Supabase/QStash.
backend/jest.config.js (1)

6-12: Padrões redundantes em testMatch.

"**/tests/**/*.test.js" já cobre todos os subdiretórios listados (unit/, middleware/, integration/, security/). Jest faz dedup dos arquivos, então o comportamento não muda, mas a configuração pode ser simplificada para reduzir ruído e evitar a falsa impressão de que cada subdiretório precisa ser declarado.

♻️ Simplificação sugerida
-  // Cobre todos os subdiretórios de tests/
-  testMatch: [
-    "**/tests/**/*.test.js",
-    "**/tests/unit/**/*.test.js",
-    "**/tests/middleware/**/*.test.js",
-    "**/tests/integration/**/*.test.js",
-    "**/tests/security/**/*.test.js",
-  ],
+  // Cobre todos os subdiretórios de tests/
+  testMatch: ["**/tests/**/*.test.js"],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/jest.config.js` around lines 6 - 12, The testMatch array in
jest.config.js contains redundant entries: "**/tests/**/*.test.js" already
matches files under unit/, middleware/, integration/, and security/, so remove
the explicit subdirectory patterns and keep a single concise pattern (testMatch)
such as "**/tests/**/*.test.js" to simplify configuration and avoid misleading
duplication; update the testMatch definition accordingly and ensure no other
parts of the config rely on the now-removed explicit entries.
frontend/src/__tests__/submissionValidation.test.js (1)

60-90: Reimplementar utilitários reais nos mocks reduz o valor do teste.

stripNonDigits, validateCpfAlgorithm, formatDateToBr, parseBrDateToIso e normalizeDecimalForSubmit são re-implementados aqui em vez de importados de @/utils/formatters.js. Isso significa que:

  • Se formatters.js introduzir um bug (ex.: aceitar CPF inválido), nenhum destes testes detecta;
  • Divergências entre o "mock" e a implementação real podem mascarar regressões em produção.

Como formatters.test.js já cobre essas funções unitariamente, basta importá-las diretamente em makeMocks() para que processSubmission seja testado contra a implementação real.

+import {
+  stripNonDigits,
+  validateCpfAlgorithm,
+  formatDateToBr,
+  parseBrDateToIso,
+  normalizeDecimalForSubmit,
+} from "@/utils/formatters.js";
@@
-    stripNonDigits: (v) => v.replace(/\D/g, ""),
-    validateCpfAlgorithm: (cpf) => { /* ... */ },
-    formatDateToBr: (v) => { /* ... */ },
-    parseBrDateToIso: (v) => { /* ... */ },
-    normalizeDecimalForSubmit: (v) => { /* ... */ },
+    stripNonDigits,
+    validateCpfAlgorithm,
+    formatDateToBr,
+    parseBrDateToIso,
+    normalizeDecimalForSubmit,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/__tests__/submissionValidation.test.js` around lines 60 - 90,
Replace the hand-rolled implementations inside makeMocks() with the real
formatter exports: import stripNonDigits, validateCpfAlgorithm, formatDateToBr,
parseBrDateToIso and normalizeDecimalForSubmit from "@/utils/formatters.js" and
use those references in the mock object instead of the inline functions so
processSubmission tests exercise the real formatter logic (locate makeMocks and
the mock object where stripNonDigits, validateCpfAlgorithm, formatDateToBr,
parseBrDateToIso and normalizeDecimalForSubmit are currently defined and swap
them to the imported symbols).
arquivos/Conhecimento/04-Historico/taskfinalizacao.md (1)

39-42: Cache em memória pode ficar inconsistente em ambiente com múltiplas instâncias

O plano de configCache funciona por processo Node. Em escala horizontal, a invalidação após PUT não propaga para outras réplicas, então leituras podem servir dados antigos mesmo dentro do TTL.

Sugestão: registrar no plano que, em produção multi-instância, o cache deve ser distribuído (ex.: Redis) ou ter estratégia explícita de coerência entre nós.

Also applies to: 68-70

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@arquivos/Conhecimento/04-Historico/taskfinalizacao.md` around lines 39 - 42,
The in-memory configCache approach (getConfiguracoes(), invalidarCache(), TTL 5
minutes) is unsafe in multi-instance deployments because invalidations from a
PUT won’t propagate; update the doc/plan to state that in production with
multiple Node instances the cache must be distributed or coherent — e.g.,
replace or backstop the process-local configCache with a shared cache like Redis
(or add a cross-instance invalidation mechanism such as pub/sub) and mention
this requirement alongside the TTL and the invalidarCache API so reviewers know
to implement Redis/centralized caching for consistent reads across replicas.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 275-276: The CI step named "Verify server.js imports (syntax
check)" currently silences errors by redirecting stderr to /dev/null and using
"|| echo" fallback; remove the redirection and fallback so the command fails on
real syntax errors — replace the command with a plain node --check server.js (or
node --input-type=module --check server.js if you intentionally need input-type)
so that Node's native ESM-aware syntax check runs and the job fails on syntax
problems.

In `@arquivos/Conhecimento/04-Historico/2026-04-25_suite_testes_cicd.md`:
- Line 30: Documento afirma "thresholds de cobertura (30% global)" mas em
backend/jest.config.js os valores são statements: 28, branches: 20, functions:
25, lines: 28; alinhe-os alterando os thresholds no arquivo
backend/jest.config.js para statements: 30, branches: 30, functions: 30, lines:
30 (ou, alternativamente, atualize a frase no documento para listar os valores
atuais), garantindo consistência entre a string do documento e as chaves de
cobertura (statements, branches, functions, lines) usadas em
backend/jest.config.js.

In `@arquivos/Conhecimento/04-Historico/GUIA_TESTES.md`:
- Around line 451-453: The guide currently normalizes a 500 response for lock
tests due to a Prisma BigInt mock limitation, which masks real failures; fix by
updating the test mock to return numeric ids (e.g., return id: 1 as Number) or
implement a JSON.stringify replacer for BigInt in the test mock so the
controller's BigInt(id) conversion works, then change the test assertion from
accepting [200,500] to a strict expect(res.status).toBe(200) in the lock-related
test (look for the test that uses expect(res.status).toBeOneOf or similar and
the mock that returns id values), and if you must keep temporary tolerance,
instead of documenting it as expected behavior add a referenced follow-up issue
and note the issue ID in the guide.
- Line 46: The command string contains a typo: replace the malformed invocation
`npm test -- tests/integration`1/` by removing the stray backtick and `1` so it
reads a valid path (for example `npm test -- tests/integration/` or `npm test --
tests/integration/*`), updating the line that currently contains `npm test --
tests/integration`1/`.

In `@arquivos/Conhecimento/04-Historico/implementation_planfinal.md`:
- Around line 53-65: The requireSameUnit middleware as written uses a
case-sensitive check on req.user.cargo and an unsafe BigInt(req.params.id)
conversion; update requireSameUnit to normalize cargo to lower case (e.g.,
compare against ['admin','gestor'] using req.user.cargo.toLowerCase()) to match
existing controllers (lockController.js) and other middlewares, and
validate/guard the id conversion before calling BigInt (e.g., ensure
req.params.id matches a numeric pattern or wrap BigInt(...) in a try/catch and
return a 400/403 error when id is invalid) so that non-numeric ids don’t throw a
SyntaxError and admins/gestors aren’t incorrectly denied.
- Around line 222-232: The table's Edge Case Mitigation contradicts Risco 1 by
recommending prisma.$transaction for atomic updates; change that line to specify
using the Supabase JS Client atomic pattern described in Risco 1 (e.g., use
update().eq('id', id).in('status', permitidos).select().single() and rely on
Supabase's conditional update to return 409 semantics), and remove or replace
the prisma.$transaction mention so all guidance consistently mandates Supabase
client mutations for casos to avoid Prisma/Supabase conflicts.

In `@arquivos/Conhecimento/04-Historico/taskfinalizacao.md`:
- Around line 144-146: A permissão para acessar a tela de equipe está
inconsistente: crie duas flags semânticas no objeto passado como value do
AuthContext.Provider — canViewTeam (para admin/gestor) e canManageTeam (apenas
admin) — em vez de usar apenas canManageTeam; atualize todos os lugares que hoje
usam canManageTeam como gate de visualização (ex.: componente GerenciarEquipe e
quaisquer checks semelhantes) para usar canViewTeam quando for apenas
leitura/visualização e mantenha canManageTeam para ações de gestão; ajuste
também a declaração existente de permissões (onde são definidos canManageTeam,
canViewBi, canDistribuir) para incluir a nova canViewTeam e exportá-la no
contexto.
- Around line 118-119: Escolha um valor canônico para o cargo (por exemplo
"coordenador") e atualize todos os locais que usam a variante curta ("coord")
para o valor escolhido: revise o helper de permissões que verifica cargos
(substitua quaisquer ocorrências de 'coord' por 'coordenador'), ajuste os
seeds/fixtures, garanta que o JWT issuer/generator emita o cargo canônico, e
atualize o frontend e quaisquer verificações nos endpoints /api/bi/estatisticas,
/api/bi/exportar/pdf e /api/bi/exportar/xlsx para aceitar 'admin', 'gestor' e o
valor canônico 'coordenador' consistentemente; depois rode os testes/validações
de autorização para confirmar que a mudança não quebrou permissões.

In `@backend/tests/integration/casosRoutes.test.js`:
- Around line 103-118: The tests "cria caso com payload válido → 201" and
"retorna campo 'protocolo' na resposta de sucesso" are accepting mutually
exclusive outcomes (they allow 400), neutralizing the assertions; update the
mocks for Prisma/Supabase used by POST /api/casos/novo so the happy path is
deterministic (ensure the create flow returns a success and a protocolo), then
change the first test to only accept 201 (remove 400 from the allowed statuses)
and make the second test unconditionally assert res.body.protocolo (or assert
only when res.status === 201 if you prefer strictness) so the test truly
verifies the success path (reference tests named "cria caso com payload válido →
201" and "retorna campo 'protocolo' na resposta de sucesso" in
backend/tests/integration/casosRoutes.test.js and the Prisma/Supabase mocks used
by that route).
- Around line 179-186: The test currently allows either 401 or 403 which hides
RBAC regressions; change the assertion to require a 403 and ensure the setup
produces a valid authenticated active "visualizador" so the requireWriteAccess
middleware runs: keep using makeJwt("visualizador") and
mockPrismaDefensores.findUnique.mockResolvedValue(makeUserProfile("visualizador"))
but update the assertion to expect(res.status).toBe(403); if it intermittently
returns 401, inspect and fix the auth/mocking setup (e.g., makeJwt,
mockPrismaDefensores, or how the app applies requireWriteAccess) rather than
loosening the assertion.
- Around line 147-156: The test sets mockPrismaCasos.findFirst = undefined which
causes TypeError when the controller calls prisma.casos.findFirst; replace that
mutation with a proper Jest mock operation on mockPrismaCasos.findFirst (e.g.,
call mockPrismaCasos.findFirst.mockReset() to clear prior behavior or
mockPrismaCasos.findFirst.mockResolvedValue(null) to simulate no record) so the
mock remains a function and does not leak state to other tests; update the
testcase that uses makeJwt("defensor") and mockPrismaDefensores.findUnique to
use one of these mock methods instead of assigning undefined.

In `@backend/tests/integration/lockRoutes.test.js`:
- Around line 91-103: The test currently tolerates 500 by using expect([200,
500]).toContain(res.status), which hides real regressions; fix by ensuring mocks
serialize correctly (either add a global BigInt to JSON replacer in your test
setup like adding BigInt.prototype.toJSON = function(){ return this.toString();
} in tests/setup.js) or change the mock returns used by
mockPrismaCasos.findUnique / makeCasoAtual to return numeric id: 1 instead of
BigInt(1), then replace the tolerant assertion expect([200,
500]).toContain(res.status) with a strict expect(res.status).toBe(200) in
lockRoutes.test.js (and the other occurrence at line ~173) so genuine 500 errors
fail the test.
- Around line 105-132: Two tests in lockRoutes.test.js use permissive assertions
that accept the opposite status; update the assertions so they assert the exact
expected status: for the "servidor é bloqueado de tentar lock em caso
liberado_para_protocolo → 403" test (the one that mocks
makeCasoAtual("liberado_para_protocolo") and calls lockController.lockCaso)
replace expect([403, 200]).toContain(res.status) with
expect(res.status).toBe(403); for the "retorna 423 quando caso já está bloqueado
por outro usuário" test (the one that mocks updateMany.mockResolvedValue({
count: 0 })) replace expect([423, 200]).toContain(res.status) with
expect(res.status).toBe(423).

In `@backend/tests/middleware/auth.test.js`:
- Around line 220-227: The test creates an unused variable `_expired` via
makeTicket({ expiresIn: "-1s" }) while the actual expired token used is
`expiredTicket` created with jwt.sign; remove the dead `_expired` declaration
or, if preferred, update the helper `makeTicket` to accept signing options
(e.g., makeTicket(payload, signOptions = {})) so tests can call makeTicket({ ...
}, { expiresIn: "-1s" }) and avoid duplicating jwt.sign — update references in
this test to use the corrected helper (`makeTicket`) or simply delete the unused
`_expired` variable and keep `expiredTicket`.
- Around line 87-99: Replace the current HS256-with-wrong-secret test with one
that actually signs a JWT using RSA/RS256 and verifies authMiddleware rejects
it: generate an RSA keypair (crypto.generateKeyPairSync), sign a token with the
privateKey using algorithm "RS256" and expiresIn, call authMiddleware with
mockReqRes({ authorization: `Bearer ${rs256Token}` }) and assert res.status(401)
was called and next was not called; ensure mockFindUnique behavior remains
appropriate (e.g., resolved/null) so the failure is due to algorithm enforcement
in authMiddleware rather than user lookup.

In `@backend/tests/security/injection.test.js`:
- Around line 50-71: Update the SQLi tests to assert more than "not 500": for
each payload in payloadsSqli and the explicit injection test for GET
"/api/casos/buscar-cpf" ensure the response status is a client error (e.g., 400
or 404) rather than accepting 200, and additionally assert that res.body does
not include sensitive fields such as dados_representante or cpf when the request
is unauthenticated; modify the specs named like `não retorna 500 para SQLi em
buscar-cpf` and `CPF com SQL injection resulta em JSON válido (não crash)` to
check res.status is in the 4xx range and that typeof res.body === "object" while
verifying absence of PII keys (dados_representante, cpf) to prevent accidental
data exposure.
- Around line 122-138: The test "bloqueia origin não autorizado em produção"
mutates process.env.NODE_ENV unsafely; wrap the request and assertions in a
try/finally so NODE_ENV is always restored, and when restoring use: if
(originalEnv === undefined) delete process.env.NODE_ENV else
process.env.NODE_ENV = originalEnv to avoid setting the string "undefined". Also
consider reloading/creating the Express instance after setting NODE_ENV to
"production" (the test uses the symbol app) if you need to exercise code paths
that are evaluated at import time rather than just corsOptions.origin.

In `@backend/tests/security/rateLimiter.test.js`:
- Around line 83-105: The test inspects internals (getMessage, globalLimiter)
which don't exist on express-rate-limit; instead export the Portuguese message
strings as named constants from rateLimiter.js (e.g., RATE_LIMIT_MESSAGE_GLOBAL,
RATE_LIMIT_MESSAGE_IP, RATE_LIMIT_MESSAGE_MINUTES) and import them into the test
to assert equality, or update the test to use the existing runLimiter helper to
simulate exceeding max and assert that the mocked res.json was called with the
expected message; modify either the test to import the new constants or replace
the getMessage branch with a runLimiter-based request that checks res.json
payload against the exported constants.

In `@backend/tests/unit/aiService.test.js`:
- Around line 27-30: The tests import generateLegalText from aiService.js after
mocks, but they don't set GROQ_API_KEY or GEMINI_API_KEY, so the module's
conditional client initialization may skip mocked clients; before the dynamic
import of "../../src/services/aiService.js" set fake environment API keys
(GROQ_API_KEY and GEMINI_API_KEY) so the module code takes the branches that
create the clients (and thus exercise the mocks), then perform the import and
run assertions against generateLegalText.

In `@frontend/src/__tests__/submissionValidation.test.js`:
- Around line 261-296: Remove the conditional guards that skip assertions and
make the expectations unconditional: in the "chama fetch para /casos/novo com
dados válidos" test, always assert that global.fetch was called and that the
first call URL contains "/casos/novo" (still use makeFormState and
processSubmission as setup); in the "chama setLoading(false) no finally mesmo em
caso de erro de rede" test, always assert that mocks.setLoading was called with
false at least once after calling processSubmission (do not wrap this expect in
an if checking global.fetch). Reference: processSubmission, makeFormState,
global.fetch, mocks.setFormErrors, and mocks.setLoading.

---

Outside diff comments:
In `@backend/src/controllers/defensoresController.js`:
- Around line 104-124: The login handler currently falls back to the
non-existent role "operador" in the payload and response; update the logic in
the block that builds payload and defensor response (where payload is created
and token = generateToken(payload) and res.status(200).json(...) is returned) to
either (preferred) reject logins when defensor.cargo?.nome is missing by
returning res.status(403).json({ error: "Usuário sem cargo configurado." }) or
(if backwards-compatible) change the fallback to the new default "servidor"
everywhere (replace "operador" with "servidor" in the payload and the defensor
response), ensuring the same check is applied as in auth.js so tokens never
contain an invalid role.

In `@backend/src/controllers/lockController.js`:
- Around line 14-34: The variable _isDefensorOrCoordenador is computed but never
used, which either hides dead code or an omitted authorization rule; decide and
act: if you want the additional restriction, restore a meaningful name
(isDefensorOrCoordenador) and add an explicit check mirroring the
servidor/estagiario rule, e.g. return res.status(403)... when nivelLock === 1
and isDefensorOrCoordenador; otherwise remove the _isDefensorOrCoordenador
calculation entirely to avoid unnecessary computation and linter
suppression—update references to nivelLock, isServidorOrEstagiario and the
new/removed isDefensorOrCoordenador accordingly.

In `@backend/src/services/geminiService.js`:
- Around line 163-167: A função _valueOrPlaceholder definida em geminiService.js
não é utilizada; remova a declaração inteira da função (_valueOrPlaceholder) e
qualquer referência residual a PLACEHOLDER_FIELD se só for usada por ela,
garantindo que não quebre linter/exports; verifique também se não há exportações
nomeadas ou comentários que dependam de _valueOrPlaceholder e limpe-os.

In `@frontend/src/areas/defensor/pages/GerenciarEquipe.jsx`:
- Line 257: O texto do cabeçalho em GerenciarEquipe.jsx (o <p
className="text-muted">) ainda menciona "recepcionistas", que foi removido do
RBAC; atualize essa string para refletir os perfis atuais (por exemplo "Gerencie
unidades, defensores, estagiários, servidores e coordenadores." ou uma versão
genérica como "Gerencie unidades e usuários por funções.") — localize a tag p
com className="text-muted" no componente GerenciarEquipe.jsx e substitua apenas
o texto exibido para remover "recepcionistas" e incluir os nomes de cargos
novos/atuais conforme a matriz RBAC.

---

Nitpick comments:
In @.github/workflows/ci.yml:
- Around line 318-335: Update the final gate step "Verificar se todos os jobs
passaram" to explicitly include the test and lint jobs in the needs array and
the summary table: add test-backend, test-frontend, lint-backend, lint-frontend
to the needs: [...] declaration and append corresponding lines for each job to
the $GITHUB_STEP_SUMMARY table so the script checks ${{
needs.test-backend.result }}, ${{ needs.test-frontend.result }}, ${{
needs.lint-backend.result }}, and ${{ needs.lint-frontend.result }} alongside
build-backend, build-frontend, and security-audit to make the gate explicit and
robust to future changes.
- Around line 104-105: The CI step named "Run Jest tests with coverage"
currently runs "npm test -- --forceExit"; remove the "--forceExit" flag so Jest
is allowed to surface open handles instead of being killed, updating that
command to run tests normally (e.g., "npm test --"). For local debugging you can
run Jest with "--detectOpenHandles" or "--runInBand" to track lingering
timers/sockets; only reintroduce "--forceExit" as a last resort after fixing
teardown issues in tests that involve Prisma/Supabase/QStash.

In `@arquivos/Conhecimento/04-Historico/2026-04-25_suite_testes_cicd.md`:
- Around line 86-90: Os blocos de código no arquivo de histórico estão sem
linguagem (causando MD040); atualize as fences que envolvem o diagrama
"lint-backend  ──→ test-backend  ──→ build-backend  ──┐" e o trecho de árvore de
arquivos ".github/ workflows/ ci.yml" para declarar uma linguagem como text
(ex.: ```text) em ambas as ocorrências (linhas mostradas no diff) para
satisfazer o linter e manter o formato do diagrama.

In `@arquivos/Conhecimento/04-Historico/taskfinalizacao.md`:
- Around line 39-42: The in-memory configCache approach (getConfiguracoes(),
invalidarCache(), TTL 5 minutes) is unsafe in multi-instance deployments because
invalidations from a PUT won’t propagate; update the doc/plan to state that in
production with multiple Node instances the cache must be distributed or
coherent — e.g., replace or backstop the process-local configCache with a shared
cache like Redis (or add a cross-instance invalidation mechanism such as
pub/sub) and mention this requirement alongside the TTL and the invalidarCache
API so reviewers know to implement Redis/centralized caching for consistent
reads across replicas.

In `@backend/jest.config.js`:
- Around line 6-12: The testMatch array in jest.config.js contains redundant
entries: "**/tests/**/*.test.js" already matches files under unit/, middleware/,
integration/, and security/, so remove the explicit subdirectory patterns and
keep a single concise pattern (testMatch) such as "**/tests/**/*.test.js" to
simplify configuration and avoid misleading duplication; update the testMatch
definition accordingly and ensure no other parts of the config rely on the
now-removed explicit entries.

In `@backend/src/controllers/defensoresController.js`:
- Line 6: Remova o import não utilizado isSupabaseConfigured do topo do arquivo
defensoresController.js e também remova a diretiva eslint-disable no-unused-vars
que foi colocada para silenciá‑lo; procure pelas ocorrências de
"isSupabaseConfigured" e pela linha com "eslint-disable no-unused-vars" e
simplesmente elimine ambas para eliminar código morto e restaurar a checagem do
linter.

In `@backend/src/controllers/scannerController.js`:
- Line 1: Remove the blanket file-level ESLint suppression "/* eslint-disable
no-unused-vars */" from scannerController.js; instead run the linter to identify
any truly unused imports or variables, delete those unused identifiers (or
refactor to use them), and if a suppression is absolutely required apply an
inline comment (eslint-disable-next-line no-unused-vars) immediately above the
specific line(s) only. Locate the top-of-file directive "/* eslint-disable
no-unused-vars */" and any unused imports/const/let declarations in that file
and either remove or narrow the suppression as described.

In `@backend/src/controllers/statusController.js`:
- Around line 1-2: Remove the unused import and the lint suppression: delete the
"isSupabaseConfigured" import statement and the top-line "/* eslint-disable
no-unused-vars */" in the statusController (the unused symbol is
isSupabaseConfigured) so the file only imports and uses Prisma as intended;
confirm no other references to isSupabaseConfigured remain before committing.

In `@backend/src/services/geminiService.js`:
- Line 1: Remove the top-level "/* eslint-disable no-unused-vars */" directive
and resolve unused symbols instead: run the linter or inspect
backend/src/services/geminiService.js for any unused variables or functions and
either delete them or use localized inline suppression (e.g., add "/*
eslint-disable-next-line no-unused-vars */" immediately above the specific
declaration) for truly required-but-unused names; ensure exported functions and
helpers remain referenced or are removed so the global suppression is no longer
needed.

In `@backend/src/utils/logger.js`:
- Around line 2-3: Remova o bloco de código comentado que ainda referencia
Logtail e LogtailTransport (as ocorrências comentadas que mencionam
Logtail/LogtailTransport) do arquivo logger.js; se preferir manter a referência
para reativação futura, substitua o código comentado por um comentário
explicativo curto indicando porque foi removido e como reativá-lo (ex.: "Logtail
removido temporariamente — instruções para reativação...") para evitar
referências órfãs.

In `@backend/tests/dicionarioAcoes.test.js`:
- Line 1: Remove the global directive /* eslint-disable no-unused-vars */ at the
top of the test file and re-run ESLint; if a specific import truly remains
unused (e.g., an import other than logger), add a localized inline suppression
next to that import (/* eslint-disable-next-line no-unused-vars */) or remove
the import entirely, keeping the logger import as-is since logger is referenced
in the tests (search for logger usage in assertions) and ensure no other unused
variables remain.

In `@backend/tests/documentGenerationService.test.js`:
- Line 1: Remova a diretiva /* eslint-disable no-unused-vars */ do topo do
arquivo e substitua quaisquer blocos de captura que usam catch (e) { ... } por
catch { ... } (uso de optional catch binding no ES2019+) em testes como os
dentro de documentGenerationService.test.js; após a alteração execute o
linter/CI para garantir que não há mais avisos e ajuste quaisquer variáveis
realmente não utilizadas se forem encontradas.

In `@backend/tests/security/rateLimiter.test.js`:
- Around line 49-59: The current Promise around limiter(req, res, ...) races
with express-rate-limit v7 because limiter is asynchronous; instead of
immediately checking res.status.mock.calls, make the Promise resolve from the
response hooks: override/mock res.status (and res.json if used) to call the
original mock and then resolve({ req, res, next, blocked: true }); keep the
limiter callback to call next() and resolve({ req, res, next, blocked: false });
or, alternatively, await a microtask (e.g. Promise.resolve().then(...)) before
checking res.status; update the helper that calls limiter(req, res, ...) so
resolution is driven from res.status/res.json or a microtask to avoid hanging
and reliably capture both blocked and non-blocked branches involving limiter,
req, res, next.

In `@backend/tests/unit/securityService.test.js`:
- Around line 48-56: The test "gera protocolos distintos..." currently only
asserts types and doesn't validate uniqueness; update the test that calls
generateCredentials to assert the two protocolos are not equal (e.g.,
expect(p1).not.toBe(p2)) after the small delay, and if flakiness is a concern
use a slightly larger delay or retry loop to ensure Date.now()-based suffix
differs; update references to generateCredentials and the local variables
protocolo/p1 and p2 so the assertion reflects real uniqueness rather than just
type checking.

In `@frontend/src/__tests__/caseUtils.test.js`:
- Around line 60-73: Update the tests to assert exact expected outputs and fix
the misleading comment: for the null case replace the vague typeof check with
expect(formatTipoAcaoLabel(null)).toBe("Não informado") and remove or correct
the comment about String(null); for the accented lookup case assert the
deterministic label by replacing the generic checks with
expect(formatTipoAcaoLabel("execução_alimentos")).toBe("Execução de Alimentos")
so the behavior of formatTipoAcaoLabel and the knownLabels lookup is locked
down.

In `@frontend/src/__tests__/submissionValidation.test.js`:
- Around line 60-90: Replace the hand-rolled implementations inside makeMocks()
with the real formatter exports: import stripNonDigits, validateCpfAlgorithm,
formatDateToBr, parseBrDateToIso and normalizeDecimalForSubmit from
"@/utils/formatters.js" and use those references in the mock object instead of
the inline functions so processSubmission tests exercise the real formatter
logic (locate makeMocks and the mock object where stripNonDigits,
validateCpfAlgorithm, formatDateToBr, parseBrDateToIso and
normalizeDecimalForSubmit are currently defined and swap them to the imported
symbols).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 7ccdb4c7-f605-4654-9e66-e10229e434f4

📥 Commits

Reviewing files that changed from the base of the PR and between fe20ca3 and fc23776.

⛔ Files ignored due to path filters (2)
  • backend/uploads/peticoes/T1/peticao_inicial_T1.docx is excluded by !**/*.docx
  • frontend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (49)
  • .github/workflows/ci.yml
  • arquivos/Conhecimento/01-Referencia/ARCHITECTURE.md
  • arquivos/Conhecimento/01-Referencia/BUSINESS_RULES.md
  • arquivos/Conhecimento/04-Historico/2026-04-25_suite_testes_cicd.md
  • arquivos/Conhecimento/04-Historico/GUIA_TESTES.md
  • arquivos/Conhecimento/04-Historico/feitos/2026-04-25_otimizacao_bi_rbac.md
  • arquivos/Conhecimento/04-Historico/feitos/implementation_plan.md
  • arquivos/Conhecimento/04-Historico/implementation_planfinal.md
  • arquivos/Conhecimento/04-Historico/taskfinalizacao.md
  • backend/jest.config.js
  • backend/new_output.txt
  • backend/server.js
  • backend/src/controllers/biController.js
  • backend/src/controllers/casosController.js
  • backend/src/controllers/defensoresController.js
  • backend/src/controllers/lockController.js
  • backend/src/controllers/scannerController.js
  • backend/src/controllers/statusController.js
  • backend/src/middleware/upload.js
  • backend/src/services/geminiService.js
  • backend/src/services/securityService.js
  • backend/src/utils/logger.js
  • backend/test_output.txt
  • backend/tests/dicionarioAcoes.test.js
  • backend/tests/documentGenerationService.test.js
  • backend/tests/integration/casosRoutes.test.js
  • backend/tests/integration/health.test.js
  • backend/tests/integration/lockRoutes.test.js
  • backend/tests/integration/scannerRoutes.test.js
  • backend/tests/middleware/apiKey.test.js
  • backend/tests/middleware/auth.test.js
  • backend/tests/middleware/requireWriteAccess.test.js
  • backend/tests/security/injection.test.js
  • backend/tests/security/rateLimiter.test.js
  • backend/tests/setup.js
  • backend/tests/unit/aiService.test.js
  • backend/tests/unit/securityService.test.js
  • frontend/package.json
  • frontend/src/__tests__/apiBase.test.js
  • frontend/src/__tests__/caseUtils.test.js
  • frontend/src/__tests__/formatters.test.js
  • frontend/src/__tests__/submissionValidation.test.js
  • frontend/src/areas/defensor/contexts/AuthContext.jsx
  • frontend/src/areas/defensor/pages/Cadastro.jsx
  • frontend/src/areas/defensor/pages/GerenciarEquipe.jsx
  • frontend/src/areas/servidor/components/StepDadosPessoais.jsx
  • frontend/src/areas/servidor/components/secoes/SecaoProcessoOriginal.jsx
  • frontend/src/areas/servidor/services/submissionService.js
  • frontend/vitest.config.js
💤 Files with no reviewable changes (4)
  • backend/tests/setup.js
  • frontend/src/areas/servidor/services/submissionService.js
  • frontend/src/areas/servidor/components/secoes/SecaoProcessoOriginal.jsx
  • frontend/src/areas/servidor/components/StepDadosPessoais.jsx

Comment thread .github/workflows/ci.yml Outdated

### Arquivo modificado

- `backend/jest.config.js` — expandido para cobrir todos os subdiretórios, adicionadas thresholds de cobertura (30% global) e exportação de relatório HTML/lcov em `logs/coverage/`.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistência: limites de cobertura informados não batem com backend/jest.config.js.

A documentação afirma "thresholds de cobertura (30% global)", mas em backend/jest.config.js os valores configurados são statements: 28, branches: 20, functions: 25, lines: 28. Como esse documento serve de histórico/fonte para configuração de Branch Protection e auditoria, vale alinhar para evitar divergência futura.

♻️ Ajuste sugerido
-- `backend/jest.config.js` — expandido para cobrir todos os subdiretórios, adicionadas thresholds de cobertura (30% global) e exportação de relatório HTML/lcov em `logs/coverage/`.
+- `backend/jest.config.js` — expandido para cobrir todos os subdiretórios, adicionadas thresholds de cobertura (statements 28% / branches 20% / functions 25% / lines 28%) e exportação de relatório HTML/lcov em `logs/coverage/`.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- `backend/jest.config.js` — expandido para cobrir todos os subdiretórios, adicionadas thresholds de cobertura (30% global) e exportação de relatório HTML/lcov em `logs/coverage/`.
- `backend/jest.config.js` — expandido para cobrir todos os subdiretórios, adicionadas thresholds de cobertura (statements 28% / branches 20% / functions 25% / lines 28%) e exportação de relatório HTML/lcov em `logs/coverage/`.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@arquivos/Conhecimento/04-Historico/2026-04-25_suite_testes_cicd.md` at line
30, Documento afirma "thresholds de cobertura (30% global)" mas em
backend/jest.config.js os valores são statements: 28, branches: 20, functions:
25, lines: 28; alinhe-os alterando os thresholds no arquivo
backend/jest.config.js para statements: 30, branches: 30, functions: 30, lines:
30 (ou, alternativamente, atualize a frase no documento para listar os valores
atuais), garantindo consistência entre a string do documento e as chaves de
cobertura (statements, branches, functions, lines) usadas em
backend/jest.config.js.

```bash
npm test -- tests/unit/
npm test -- tests/middleware/
npm test -- tests/integration`1/
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Erro de digitação no comando.

tests/integration\1/contém um backtick e um1` perdidos — isso quebra o exemplo se alguém copiar/colar.

📝 Correção sugerida
-npm test -- tests/integration`1/
+npm test -- tests/integration/
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@arquivos/Conhecimento/04-Historico/GUIA_TESTES.md` at line 46, The command
string contains a typo: replace the malformed invocation `npm test --
tests/integration`1/` by removing the stray backtick and `1` so it reads a valid
path (for example `npm test -- tests/integration/` or `npm test --
tests/integration/*`), updating the line that currently contains `npm test --
tests/integration`1/`.

Comment on lines +451 to +453
### Teste de lock retorna 500 em vez do status esperado

Os testes de lock aceitam `[200, 500]` onde o 500 pode ocorrer por limitação do mock de `BigInt` do Prisma. Isso **não afeta a produção** — apenas o ambiente de teste isolado.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Tolerância a 500 no guia oficial é um sinal de teste mascarado.

Documentar oficialmente que "os testes de lock aceitam [200, 500] por limitação de mock de BigInt" institucionaliza uma asserção fraca: um 500 real causado por um bug de produção (não pelo mock) passaria despercebido. Recomendo:

  1. Resolver o problema do BigInt no mock (ex.: JSON.stringify com replacer, ou retornar id: 1 como Number no mock já que o controlador converte com BigInt(id)); e
  2. Endurecer a asserção dos testes para expect(res.status).toBe(200).

Se preferir manter a tolerância temporariamente, abra um issue de follow-up referenciado neste guia em vez de tratá-lo como comportamento esperado.

Posso abrir um issue rastreando a remoção desse 500-tolerado e propor o fix do mock?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@arquivos/Conhecimento/04-Historico/GUIA_TESTES.md` around lines 451 - 453,
The guide currently normalizes a 500 response for lock tests due to a Prisma
BigInt mock limitation, which masks real failures; fix by updating the test mock
to return numeric ids (e.g., return id: 1 as Number) or implement a
JSON.stringify replacer for BigInt in the test mock so the controller's
BigInt(id) conversion works, then change the test assertion from accepting
[200,500] to a strict expect(res.status).toBe(200) in the lock-related test
(look for the test that uses expect(res.status).toBeOneOf or similar and the
mock that returns id values), and if you must keep temporary tolerance, instead
of documenting it as expected behavior add a referenced follow-up issue and note
the issue ID in the guide.

Comment on lines +53 to +65
- Criar middleware `requireSameUnit` que pode ser aplicado seletivamente:
```js
export const requireSameUnit = async (req, res, next) => {
if (['admin', 'gestor'].includes(req.user.cargo)) return next(); // bypass
const caso = await prisma.casos.findUnique({ where: { id: BigInt(req.params.id) }, select: { unidade_id: true } });
if (!caso || caso.unidade_id !== req.user.unidade_id) {
return res.status(403).json({ error: 'Acesso negado. Caso pertence a outra unidade.' });
}
next();
};
```
- Aplicar nas rotas: `router.post('/:id/distribuir', authMiddleware, requireCoordenadorOrAbove, requireSameUnit, distribuirCaso)`
- A listagem de defensores no modal "Distribuir" também filtra no banco: `WHERE unidade_id = req.user.unidade_id AND ativo = true`
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

requireSameUnit é case-sensitive em req.user.cargo — inconsistente com o resto do código.

backend/src/controllers/lockController.js faz req.user.cargo.toLowerCase() (snippet de contexto) e o middleware requireWriteAccess também é case-insensitive (per GUIA_TESTES.md linha 175). Já o exemplo proposto:

if (['admin', 'gestor'].includes(req.user.cargo)) return next();

falha se o JWT vier com cargo: "Admin" ou "Gestor" (algo que o seed antigo tinha em cargos.nome capitalizado). Em produção isso significa: o admin perde o bypass de unidade e recebe 403 ao tentar distribuir caso de outra unidade.

 export const requireSameUnit = async (req, res, next) => {
-  if (['admin', 'gestor'].includes(req.user.cargo)) return next(); // bypass
+  const cargo = (req.user.cargo || '').toLowerCase();
+  if (['admin', 'gestor'].includes(cargo)) return next(); // bypass
   const caso = await prisma.casos.findUnique({ where: { id: BigInt(req.params.id) }, select: { unidade_id: true } });
   if (!caso || caso.unidade_id !== req.user.unidade_id) {
     return res.status(403).json({ error: 'Acesso negado. Caso pertence a outra unidade.' });
   }
   next();
 };

Vale também tratar BigInt(req.params.id) quando id não é numérico — BigInt("abc") lança SyntaxError e gera 500.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@arquivos/Conhecimento/04-Historico/implementation_planfinal.md` around lines
53 - 65, The requireSameUnit middleware as written uses a case-sensitive check
on req.user.cargo and an unsafe BigInt(req.params.id) conversion; update
requireSameUnit to normalize cargo to lower case (e.g., compare against
['admin','gestor'] using req.user.cargo.toLowerCase()) to match existing
controllers (lockController.js) and other middlewares, and validate/guard the id
conversion before calling BigInt (e.g., ensure req.params.id matches a numeric
pattern or wrap BigInt(...) in a try/catch and return a 400/403 error when id is
invalid) so that non-numeric ids don’t throw a SyntaxError and admins/gestors
aren’t incorrectly denied.

Comment on lines +50 to +71
const payloadsSqli = [
"' OR '1'='1",
"'; DROP TABLE casos; --",
"1 UNION SELECT * FROM defensores --",
"999' OR '1'='1' --",
];

payloadsSqli.forEach((cpf) => {
it(`não retorna 500 para SQLi em buscar-cpf: "${cpf.slice(0, 30)}"`, async () => {
const res = await request(app)
.get(`/api/casos/buscar-cpf?cpf=${encodeURIComponent(cpf)}`);
// Deve retornar 400 (validação) ou 404 (não encontrado), NUNCA 500
expect(res.status).not.toBe(500);
});
});

it("CPF com SQL injection resulta em JSON válido (não crash)", async () => {
const res = await request(app)
.get("/api/casos/buscar-cpf?cpf=%27%20OR%20%271%27%3D%271");
expect(res.headers["content-type"]).toMatch(/json/);
expect(typeof res.body).toBe("object");
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Asserção fraca: not.toBe(500) só protege contra crash, não contra exposição de dados.

A premissa do teste ("Prisma usa queries parametrizadas") é correta, mas expect(res.status).not.toBe(500) aceita 200 com qualquer payload — inclusive uma listagem completa de casos retornada por engano. Para um teste de segurança contra SQLi, vale também asserir que a resposta é 4xx (esperada para input inválido) e/ou que o res.body não contém campos sensíveis (dados_representante, cpf, etc.).

Considerando o aprendizado do PR #14 sobre PII em /buscar-cpf (resposta condicional baseada em req.user), seria valioso garantir explicitamente que esta rota pública não retorna dados_representante quando chamada sem JWT, mesmo com payload "válido".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/tests/security/injection.test.js` around lines 50 - 71, Update the
SQLi tests to assert more than "not 500": for each payload in payloadsSqli and
the explicit injection test for GET "/api/casos/buscar-cpf" ensure the response
status is a client error (e.g., 400 or 404) rather than accepting 200, and
additionally assert that res.body does not include sensitive fields such as
dados_representante or cpf when the request is unauthenticated; modify the specs
named like `não retorna 500 para SQLi em buscar-cpf` and `CPF com SQL injection
resulta em JSON válido (não crash)` to check res.status is in the 4xx range and
that typeof res.body === "object" while verifying absence of PII keys
(dados_representante, cpf) to prevent accidental data exposure.

Comment on lines +122 to +138
it("bloqueia origin não autorizado em produção", async () => {
const originalEnv = process.env.NODE_ENV;
process.env.NODE_ENV = "production";

const res = await request(app)
.get("/api/health")
.set("Origin", "https://site-malicioso.com");

// Em produção com origin não listado, CORS deve negar ou não incluir o header
// Verificamos que não vaza dados sensíveis (400 ou sem Access-Control-Allow-Origin)
const allowOrigin = res.headers["access-control-allow-origin"];
if (allowOrigin) {
expect(allowOrigin).not.toBe("https://site-malicioso.com");
}

process.env.NODE_ENV = originalEnv;
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Restauração de NODE_ENV é frágil e propaga estado entre testes.

Dois problemas:

  1. Se originalEnv for undefined (caso comum em ambiente de teste local), a linha process.env.NODE_ENV = originalEnv define a string literal "undefined" em vez de remover a variável — process.env converte qualquer valor atribuído em string. Isso pode contaminar testes subsequentes que dependem de process.env.NODE_ENV !== "production".
  2. Se o request(...) lançar antes da linha 137, o NODE_ENV permanece como "production" para o restante da suíte. Use try/finally ou um helper que garanta a restauração.

Além disso, vale lembrar que partes do server.js (Helmet, rate limiters, error handler) leem NODE_ENV no momento do import, então alternar a variável depois do app já carregado pode não exercitar realmente o comportamento de produção — apenas o ramo dinâmico do corsOptions.origin.

🛡️ Sugestão de fix
   it("bloqueia origin não autorizado em produção", async () => {
     const originalEnv = process.env.NODE_ENV;
     process.env.NODE_ENV = "production";
-
-    const res = await request(app)
-      .get("/api/health")
-      .set("Origin", "https://site-malicioso.com");
-
-    // Em produção com origin não listado, CORS deve negar ou não incluir o header
-    // Verificamos que não vaza dados sensíveis (400 ou sem Access-Control-Allow-Origin)
-    const allowOrigin = res.headers["access-control-allow-origin"];
-    if (allowOrigin) {
-      expect(allowOrigin).not.toBe("https://site-malicioso.com");
-    }
-
-    process.env.NODE_ENV = originalEnv;
+    try {
+      const res = await request(app)
+        .get("/api/health")
+        .set("Origin", "https://site-malicioso.com");
+
+      const allowOrigin = res.headers["access-control-allow-origin"];
+      if (allowOrigin) {
+        expect(allowOrigin).not.toBe("https://site-malicioso.com");
+      }
+    } finally {
+      if (originalEnv === undefined) {
+        delete process.env.NODE_ENV;
+      } else {
+        process.env.NODE_ENV = originalEnv;
+      }
+    }
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it("bloqueia origin não autorizado em produção", async () => {
const originalEnv = process.env.NODE_ENV;
process.env.NODE_ENV = "production";
const res = await request(app)
.get("/api/health")
.set("Origin", "https://site-malicioso.com");
// Em produção com origin não listado, CORS deve negar ou não incluir o header
// Verificamos que não vaza dados sensíveis (400 ou sem Access-Control-Allow-Origin)
const allowOrigin = res.headers["access-control-allow-origin"];
if (allowOrigin) {
expect(allowOrigin).not.toBe("https://site-malicioso.com");
}
process.env.NODE_ENV = originalEnv;
});
it("bloqueia origin não autorizado em produção", async () => {
const originalEnv = process.env.NODE_ENV;
process.env.NODE_ENV = "production";
try {
const res = await request(app)
.get("/api/health")
.set("Origin", "https://site-malicioso.com");
const allowOrigin = res.headers["access-control-allow-origin"];
if (allowOrigin) {
expect(allowOrigin).not.toBe("https://site-malicioso.com");
}
} finally {
if (originalEnv === undefined) {
delete process.env.NODE_ENV;
} else {
process.env.NODE_ENV = originalEnv;
}
}
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/tests/security/injection.test.js` around lines 122 - 138, The test
"bloqueia origin não autorizado em produção" mutates process.env.NODE_ENV
unsafely; wrap the request and assertions in a try/finally so NODE_ENV is always
restored, and when restoring use: if (originalEnv === undefined) delete
process.env.NODE_ENV else process.env.NODE_ENV = originalEnv to avoid setting
the string "undefined". Also consider reloading/creating the Express instance
after setting NODE_ENV to "production" (the test uses the symbol app) if you
need to exercise code paths that are evaluated at import time rather than just
corsOptions.origin.

Comment on lines +83 to +105
function getMessage(limiter) {
// express-rate-limit v7+ armazena em .options ou similar
// Tentamos acessar a mensagem de forma defensiva
try {
if (limiter._options?.message) return limiter._options.message;
if (limiter.message) return limiter.message;
} catch {
// Não conseguiu acessar internals
}
return null;
}

it("globalLimiter tem mensagem em português", () => {
// Criamos manualmente para verificar
const msg = getMessage(globalLimiter);
if (msg) {
const errorText = typeof msg === "object" ? msg.error : msg;
expect(errorText).toMatch(/requisições|IP|minutos/i);
} else {
// Se não conseguiu acessar a config interna, o teste passa (limitador existe)
expect(typeof globalLimiter).toBe("function");
}
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🌐 Web query:

express-rate-limit v7 public API for inspecting configured message option

💡 Result:

In express-rate-limit v7 (and later versions), the rate limiting middleware returned by the rateLimit function is a RateLimitRequestHandler. This is a standard Express RequestHandler extended with public methods for inspecting and managing rate limit state, but there is no direct public API method to retrieve the configured message option or other configuration options. Key public methods on the middleware instance (limiter): - limiter.resetKey(key: string): void - Resets the hit counter for a specific client key. - limiter.getKey(key: string): ClientRateLimitInfo | undefined | Promise<ClientRateLimitInfo | undefined> - Retrieves the current hit count (totalHits) and reset time for a specific client key. During request processing, the middleware adds rate limit info to req.rateLimit (configurable via requestPropertyName option, defaults to 'rateLimit'): - req.rateLimit.limit: number - Configured limit. - req.rateLimit.used: number - Hits so far (includes current request). - req.rateLimit.remaining: number - Requests remaining. - req.rateLimit.resetTime?: Date - Time when counter resets (if store provides it). - req.rateLimit.key: string - Client identifier key. The message option (string, JSON, or function returning the response body when limit exceeded) is used internally by the default handler function when a client is rate-limited. It is not exposed as a public property on the middleware instance. To access or customize the message: - Set a custom handler option, which receives options.message as a parameter: handler(req, res, next, options). - Store configuration separately when creating the limiter if inspection is needed outside requests. No sources explicitly document a getter for the message option itself. The configuration is passed internally but not publicly inspectable beyond req.rateLimit during requests.

Citations:


🏁 Script executed:

# First, find the test file and rate limiter implementation
fd -t f "rateLimiter" --search-path "backend"

Repository: CJVW-Code/maes_acao

Length of output: 144


🏁 Script executed:

# Check the test file to understand what it's testing
wc -l backend/tests/security/rateLimiter.test.js

Repository: CJVW-Code/maes_acao

Length of output: 110


🏁 Script executed:

# Read the test file to examine the getMessage function and the test in question
cat -n backend/tests/security/rateLimiter.test.js

Repository: CJVW-Code/maes_acao

Length of output: 4370


🏁 Script executed:

# Now examine the rateLimiter implementation
cat -n backend/src/middleware/rateLimiter.js

Repository: CJVW-Code/maes_acao

Length of output: 1284


A introspeção de internals do express-rate-limit torna este teste inefetivo; ele se reduz a uma verificação já coberta.

O getMessage retorna null porque _options.message e .message não existem na API pública do express-rate-limit v7. O teste cai no else (linha 103) verificando apenas typeof globalLimiter — comportamento já validado na primeira suite de testes. As mensagens em português estão de fato definidas na implementação (linhas 7-9, 18-20, 29-31 de backend/src/middleware/rateLimiter.js), mas não são inspecionáveis pelo middleware retornado.

Considere:

  • Exportar as strings de mensagem como constantes em rateLimiter.js e importá-las no teste para verificação direta.
  • Usar o runLimiter existente (linhas 32-59) para exceder max e validar que res.json é chamado com a mensagem esperada.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/tests/security/rateLimiter.test.js` around lines 83 - 105, The test
inspects internals (getMessage, globalLimiter) which don't exist on
express-rate-limit; instead export the Portuguese message strings as named
constants from rateLimiter.js (e.g., RATE_LIMIT_MESSAGE_GLOBAL,
RATE_LIMIT_MESSAGE_IP, RATE_LIMIT_MESSAGE_MINUTES) and import them into the test
to assert equality, or update the test to use the existing runLimiter helper to
simulate exceeding max and assert that the mocked res.json was called with the
expected message; modify either the test to import the new constants or replace
the getMessage branch with a runLimiter-based request that checks res.json
payload against the exported constants.

Comment thread backend/tests/unit/aiService.test.js
Comment thread frontend/src/__tests__/submissionValidation.test.js
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
arquivos/Conhecimento/01-Referencia/BUSINESS_RULES.md (2)

629-633: ⚠️ Potential issue | 🟡 Minor

Salta da seção 9 direto para a 11 — falta a ## 10.

A seção ## 9. Geração do Protocolo e Chave de Acesso é seguida diretamente por ## 11. Session Locking e Concorrência (linha 631). Ou existia uma seção 10 que foi removida sem renumerar, ou a numeração simplesmente está incorreta. Corrigir para manter consistência editorial e evitar confusão com referências cruzadas.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@arquivos/Conhecimento/01-Referencia/BUSINESS_RULES.md` around lines 629 -
633, The document jumps from heading "## 9. Geração do Protocolo e Chave de
Acesso" to "## 11. Session Locking e Concorrência"; locate those headings in
BUSINESS_RULES.md and fix the numbering by inserting or restoring a proper "##
10" heading (and content if missing) or renumbering "## 11" to "## 10" (and
adjust subsequent headings accordingly) so that section order is sequential and
any cross-references remain consistent.

273-289: ⚠️ Potential issue | 🟡 Minor

Numeração duplicada: dois subtítulos ### 3.6 consecutivos.

As linhas 273 e 281 ambas começam com ### 3.6 ("Validação do Formulário de Triagem" e "Campos que o assistido preenche vs. apenas o defensor"). Provavelmente o segundo deveria ser ### 3.7. Como o documento é referenciado por TOC/links externos, vale corrigir a numeração para manter ancoragem estável.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@arquivos/Conhecimento/01-Referencia/BUSINESS_RULES.md` around lines 273 -
289, Há duas ocorrências consecutivas do subtítulo "### 3.6": renomeie a segunda
ocorrência ("### 3.6 Campos que o assistido preenche vs. apenas o defensor")
para "### 3.7 Campos que o assistido preenche vs. apenas o defensor" e
verifique/ajuste a numeração dos subtítulos seguintes e quaisquer âncoras/links
referentes para manter consistência; localize pelo texto exato dos subtítulos
para aplicar a correção.
backend/src/controllers/casosController.js (1)

3053-3085: ⚠️ Potential issue | 🟠 Major

A definição de power user ficou inconsistente entre os fluxos.

Aqui o bypass ficou restrito a admin/gestor, mas lockController já trata coordenador como cargo privilegiado, e carregarCasoDetalhado() ainda continua admin-only. O efeito prático é RBAC diferente por endpoint: coordenador pode liberar caso, mas segue tomando 423/autoassumindo caso ao abrir detalhes; gestor pode ver detalhes aqui e continuar bloqueado em fluxos que passam pelo helper antigo.

♻️ Ajuste recomendado
-    const userCargo = req.user.cargo.toLowerCase();
-    const isPowerUser = ["admin", "gestor"].includes(userCargo);
+    const userCargo = req.user.cargo.toLowerCase();
+    const isPowerUser = ["admin", "gestor", "coordenador"].includes(userCargo);

O ideal é extrair essa regra para um helper único e reutilizá-lo também em carregarCasoDetalhado() e nos controladores de lock, para evitar novas divergências.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/controllers/casosController.js` around lines 3053 - 3085, The
power-user role check is inconsistent across endpoints (here using userCargo ->
isPowerUser = ["admin","gestor"]) while lockController and carregarCasoDetalhado
include "coordenador"; extract a single helper (e.g., isPrivilegedRole(role) or
isPowerUser(req.user)) in a shared util module and replace the local checks:
update this file's userCargo/isPowerUser logic to call the helper, and update
lockController and carregarCasoDetalhado to import and use the same helper;
ensure the canonical privileged set includes "admin", "gestor", and
"coordenador" and run tests to verify consistent RBAC behavior across all
lock/assume/detail flows.
🧹 Nitpick comments (7)
backend/src/controllers/defensoresController.js (1)

1-6: Evite eslint-disable no escopo do arquivo inteiro.

/* eslint-disable no-unused-vars */ desliga a regra para o módulo todo, escondendo problemas futuros. Se isSupabaseConfigured é usado em outras partes do arquivo, remova a diretiva. Se realmente não é usado neste arquivo, prefira excluir o import. Caso seja necessário manter por algum motivo (ex.: side effect), restrinja a diretiva à linha específica.

♻️ Alternativa mais segura
-/* eslint-disable no-unused-vars */
 import { prisma } from "../config/prisma.js";
 import { hashPassword, verifyPassword } from "../services/securityService.js";
 import { generateToken } from "../config/jwt.js";
 import logger from "../utils/logger.js";
-import {  isSupabaseConfigured } from "../config/supabase.js";
+// eslint-disable-next-line no-unused-vars
+import { isSupabaseConfigured } from "../config/supabase.js";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/controllers/defensoresController.js` around lines 1 - 6, Remova a
diretiva de arquivo inteiro "/* eslint-disable no-unused-vars */" e corrija o
import não utilizado "isSupabaseConfigured" em
backend/src/controllers/defensoresController.js: se não for necessário, exclua a
importação; se for necessária por side effect, documente esse motivo e aplique
uma diretiva de eslint apenas na linha do import (ex.: //
eslint-disable-next-line no-unused-vars) ou utilize realmente
"isSupabaseConfigured" no código (por exemplo em validação/condicional) para
evitar avisos supérfluos.
backend/src/middleware/requireSameUnit.js (2)

24-26: Validação de ID está correta, mas considere normalizar erros de parse.

A regex ^\d+$ previne crash, mas BigInt(id) ainda pode lançar para strings extremamente longas (acima do limite prático). Como a chamada está dentro do try/catch (linha 28), o erro vai cair no catch e retornar 500 — quando o ideal seria 400. Fluxo atual é seguro; só vale considerar tratar o erro de BigInt como entrada inválida, caso queira retornar status apropriado.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/middleware/requireSameUnit.js` around lines 24 - 26, The ID
pre-check is good but BigInt(id) can still throw for pathological inputs; in the
requireSameUnit middleware wrap the BigInt(id) conversion (the BigInt(id) call)
in its own try/catch or validateable guard so parse errors return
res.status(400).json({ error: "ID de caso inválido." }) instead of bubbling to
the outer catch (which returns 500); specifically catch the exception from
BigInt(id) inside the middleware and map it to a 400 response.

30-37: Campo status selecionado mas não utilizado no middleware.

O select traz status, porém nenhuma lógica do middleware o consome. Se o objetivo é hidratar req.caso para reuso nos controllers (linha 52), tudo bem — mas considere documentar isso explicitamente para que ninguém remova o campo no futuro pensando que é dead code. Alternativamente, remova o status daqui e busque-o no controller que efetivamente o usa.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/middleware/requireSameUnit.js` around lines 30 - 37, The select
in prisma.casos.findUnique inside the requireSameUnit middleware includes status
but the middleware never reads it; either remove status from the select and
fetch caso.status in the controller that needs it, or keep it and add an
explicit comment and/or JSDoc in requireSameUnit explaining that caso is
intentionally hydrated (req.caso) for downstream controllers to use so future
maintainers don't remove it; update any controller code that relies on
caso.status if you choose to move the fetch there and ensure references to
req.caso remain correct.
backend/src/middleware/auth.js (1)

42-42: Mudança de default para visualizador está coerente.

A normalização para minúsculas alinha com os middlewares requireWriteAccess e requireSameUnit, que comparam cargo em lowercase. Vale notar que loginDefensor em backend/src/controllers/defensoresController.js (linhas 108 e 121) ainda usa "operador" como fallback ao montar o payload do JWT — recomendo unificar esse default com "visualizador" para evitar confusão entre o token recém-emitido e o perfil hidratado pelo authMiddleware.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/middleware/auth.js` at line 42, The JWT payload built in
loginDefensor (function loginDefensor in
backend/src/controllers/defensoresController.js) still uses "operador" as the
fallback role which conflicts with the auth middleware default "visualizador";
update the fallback to "visualizador" and apply the same .toLowerCase()
normalization when constructing the payload so the emitted token's cargo matches
the lowercase default used by auth middleware (and by
requireWriteAccess/requireSameUnit).
backend/src/utils/stateMachine.js (1)

30-34: Pequena inconsistência com o JSDoc: adminBypass ausente no retorno de erro.

O JSDoc declara { ok: boolean, adminBypass: boolean, reason?: string }, mas o caminho de erro (linhas 30-33) omite adminBypass, deixando-o undefined para consumidores. Tornar a chave explícita evita surpresas em comparações estritas (result.adminBypass === false) e mantém o shape consistente.

♻️ Sugestão
     return {
       ok: false,
+      adminBypass: false,
       reason: `Transição inválida: ${from} -> ${to}`,
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/utils/stateMachine.js` around lines 30 - 34, The error return
object for invalid transitions is missing the adminBypass key declared in the
JSDoc; update the branch that returns { ok: false, reason: `Transição inválida:
${from} -> ${to}` } to explicitly include adminBypass: false so the returned
shape matches the documented type (the invalid-transition return in
stateMachine.js where the object is constructed).
arquivos/Conhecimento/03-Guias/GUIA_TESTES.md (1)

219-228: Documentação reflete asserções "OR" frouxas em casosRoutes.test.js.

Linhas 219 ("Retorna 200 ou 201"), 223 ("Retorna 401 ou 403") e 225 ("Retorna 400/404/422") documentam exatamente o padrão de asserção que mascara regressões — assunto repetidamente apontado em reviews. Se os testes forem endurecidos para status únicos (recomendado), a documentação deve refletir isso. Caso contrário, o guia funciona como justificativa documental para um teste fraco.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@arquivos/Conhecimento/03-Guias/GUIA_TESTES.md` around lines 219 - 228, The
guide currently documents loose "OR" status assertions for specific endpoints;
update the three affected entries to reflect a single, explicit expected status
that matches the real test behavior in casosRoutes.test.js (or adjust the tests
to match the doc). Specifically replace the "`POST /api/casos/novo` com payload
válido | Retorna 200 ou 201" entry with the exact expected status (e.g., 201),
change the "`GET /api/casos` com usuário inativo | Retorna 401 ou 403" entry to
the single expected status (401 or 403 as enforced by casosRoutes.test.js), and
change the "`GET /api/casos/buscar-cpf` sem CPF | Retorna 400/404/422" entry to
one definite status; ensure the text and any adjacent examples or assertions in
GUIA_TESTES.md match the status enforced by casosRoutes.test.js.
backend/tests/middleware/requireSameUnit.test.js (1)

55-63: Cobertura de bypass incompleta para cargos com acesso global.

O middleware permite bypass para admin e gestor, mas a suíte só testa admin. Vale incluir caso de gestor (e, de quebra, validar que findUnique não é chamado no bypass).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/tests/middleware/requireSameUnit.test.js` around lines 55 - 63, Add a
test covering the other global-role bypass: create a new test in
requireSameUnit.test.js that sets req.user.cargo = "gestor" (and a differing
req.user.unidade_id), ensures mockPrisma.casos.findUnique is not called, invokes
requireSameUnit(req, res, next), and asserts next was called; reference the
existing test that sets req.user.cargo = "admin" and the mock
mockPrisma.casos.findUnique and reuse req/next/res setup to keep behavior
consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 115-129: The CI step currently expects coverage-summary.json but
the test configs (jest.config.js and vitest.config.js) don't produce it; update
both test configurations to include the JSON summary reporter (json-summary /
json) in their coverage reporters so a coverage-summary.json is emitted, or
alternatively change the CI step to read whatever existing artifact those
configs already produce (e.g., lcov or the existing JSON path) and point cat to
that file; specifically modify the coverageReporters/coverageReporter settings
in the jest.config.js and vitest.config.js entries referenced in the diff and/or
adjust the CI's cat path to the actual file emitted so the node JSON-parsing
block can read a real coverage-summary.json.

In `@arquivos/Conhecimento/01-Referencia/ARCHITECTURE.md`:
- Line 184: The text in the Nível 2 description (the line that lists
"Defensor/Coordenador/Admin" and the `defensor_id` locking behavior) conflicts
with the RBAC matrix in section 9 where `gestor` has Protocolo/Finalizar rights;
update the Nível 2 line to either include `Gestor` (i.e.,
"Defensor/Coordenador/Gestor/Admin") so `gestor` acquires `defensor_id` and
follows the same protocol/finalize locking, or add a short clarifying sentence
explaining why `gestor` can Protocolo/Finalizar without acquiring Nível 2 and
how locking/`defensor_id` is handled for that role; reference the "Nível 2",
`defensor_id`, and the RBAC "Protocolo/Finalizar" entry for `gestor` when making
the edit.

In `@arquivos/Conhecimento/03-Guias/GUIA_TESTES.md`:
- Around line 482-484: The guide's note states OR-asserts like [200,500] were
fixed in lockRoutes.test.js but other tests in the same PR (notably
casosRoutes.test.js) still use OR-style assertions (e.g.,
expect([200,201,400]).toContain(...) and expect([401,403]).toContain(...)), so
either update the guide to explicitly scope the correction to lockRoutes.test.js
or update the tests to remove OR-asserts; locate the tests by the filenames
lockRoutes.test.js and casosRoutes.test.js and either (A) change the note text
in GUIA_TESTES.md to say the fix only applied to lockRoutes.test.js, or (B)
modify casosRoutes.test.js to enforce precise status expectations instead of
arrays (replace toContain checks with exact expect(...).toBe(...) or explicit
branching assertions) so the documentation and tests are consistent.

In `@backend/src/controllers/casosController.js`:
- Around line 2861-2863: O filtro de unidade está aberto para "gestor" em
whereClause, mas partesWhere só trata "admin" como visão global, deixando
métricas (total/porStatus) globais enquanto representacao/proprio são unitários;
atualize a lógica que monta partesWhere para também considerar req.user.cargo ==
"gestor" como visão global (ou aplicar a mesma condição usada em whereClause),
garantindo que as mesmas permissões de unidade sejam usadas tanto em whereClause
quanto em partesWhere para as funções/consultas que calculam total, porStatus,
representacao e proprio.

In `@backend/src/controllers/defensoresController.js`:
- Around line 133-145: The comment and 403 message in listarDefensores are
inconsistent with the whereClause: CARGOS_VIEW_TEAM includes
"admin","gestor","coordenador" but the logic treats "admin" and "gestor" as
global and only "coordenador" as scoped by unidade_id. Update the code comments
and the 403.json message to reflect the actual behavior (e.g., state that admins
and gestores see all units while coordenadores are limited to their unidade_id)
so the comment, the status message, and the behavior in listarDefensores /
CARGOS_VIEW_TEAM / whereClause remain consistent.

In `@backend/src/controllers/lockController.js`:
- Around line 100-104: Update the outdated unlock error/text messages to reflect
the expanded allowed roles (admin, gestor, coordenador): locate the
authorization check that uses hasPowerToUnlock and the associated
res.status(403).json response and replace the current Portuguese message that
mentions only “administrador” with one that lists or generically references
“administradores, gestores e coordenadores”; also update any other
unlock-related user-facing messages in this controller (the messages around the
unlock flow where hasPowerToUnlock is used, including the other strings
referenced near the same block) so they consistently mention the three allowed
roles.
- Around line 69-70: O log de contenção em lockController.js está incluindo o
nome do titular via a variável holder; substitua essa referência por um
identificador interno (por exemplo holderId) ou por uma string anonimizada para
evitar expor dados pessoais: localizar a chamada logger.warn que usa userId, id,
nivelLock e holder e alterar para usar holderId (ou "outro usuário") garantindo
que o valor logado seja um ID interno vindo do objeto de lock/holder (ou
anonimizado) e não um nome, e ajustar qualquer código que popula holder para
fornecer o ID em vez do nome.

In `@backend/src/middleware/requireSameUnit.js`:
- Around line 8-58: The requireSameUnit middleware currently blocks
collaborators because it never checks the caso.compartilhado flag; update the
prisma.casos.findUnique select in requireSameUnit to include compartilhado, then
change the unit check to allow access when caso.compartilhado is true (i.e.,
only deny when caso.unidade_id !== user.unidade_id AND !caso.compartilhado); for
stricter correctness optionally query assistencia_casos to allow access if the
user is an accepted destinatario—use the existing variables/functions
(requireSameUnit, caso, user.unidade_id, compartilhado, assistencia_casos) to
locate and implement this logic.

In `@backend/src/routes/casos.js`:
- Around line 76-78: The requireSameUnit middleware is currently mounted with
router.use("/:id", requireSameUnit) which captures non-numeric segments (e.g.,
"assistencia") and causes the regex check inside requireSameUnit (the /^\d+$/
test) to fail and return 400; fix this by restricting the middleware to only
match numeric IDs (so it won't run for routes like
/assistencia/:assistencia_id/responder) or, alternatively, update the
requireSameUnit function to early-return/skip validation when req.params.id is
non-numeric; adjust the route mount or the middleware logic (reference
router.use("/:id", requireSameUnit) and the /^\d+$/ test inside requireSameUnit)
accordingly.

In `@backend/src/utils/helpers.js`:
- Line 47: Remova o parâmetro não utilizado do bloco catch em
backend/src/utils/helpers.js: troque "catch (e) {" por "catch {" para eliminar o
warning de lint sobre variável não usada (localize o bloco catch que atualmente
declara "e" e atualize-o), mantendo o corpo do catch inalterado.
- Around line 8-13: The code uses safeJsonParse to get dados from rawDados then
spreads it into safeDados, but if dados is an array or primitive this breaks the
expected key/value object contract; update the logic after calling
safeJsonParse(rawDados, {}) to ensure dados is a plain object (e.g., check
typeof dados === 'object' && dados !== null && !Array.isArray(dados')) and if it
fails, replace with an empty object before creating safeDados (use safeDados = {
...validatedDados } or {}), and apply the same guard to the other occurrence
around the block referencing lines 38-47 so both places always produce a plain
object for form data (referencing rawDados, dados, safeJsonParse, and
safeDados).

In `@backend/tests/security/rateLimiter.test.js`:
- Around line 49-58: The synchronous check for res.status.mock.calls in
runLimiter is dead code because express-rate-limit invokes the block handler
asynchronously; remove that immediate if-branch and instead make the Promise
resolve when either next() is called or res.status(429) is invoked by
hooking/resolving inside the mocked res.status implementation. Concretely, in
the helper that calls limiter(req, res, ...) replace the post-call synchronous
check with a mocked res.status = jest.fn((code) => { resolve({ req, res, next,
blocked: code === 429 }); return res; }) (or similar) so the Promise resolves
with blocked: true when limiter actually calls res.status(429) and with blocked:
false when next() runs.

In `@frontend/src/__tests__/submissionValidation.test.js`:
- Around line 6-40: The tests fail because makeFormState() does not include the
required field valor_pensao while the mocked configAcao.secoes contains
"SecaoValoresPensao" and processSubmission (in submissionService.js) validates
formState.valor_pensao; fix by updating the factory function makeFormState to
include a sensible default valor_pensao (e.g., a numeric/string amount) so that
formState.valor_pensao exists for the happy-path tests, ensuring
processSubmission's validation block (checking
configAcao?.secoes?.includes("SecaoValoresPensao") and formState.acaoEspecifica
!== "execucao_alimentos") does not add a validation error and allows fetch to be
called and tests to proceed.

In `@frontend/src/areas/defensor/pages/Casos.jsx`:
- Line 48: The expression computing isAdminOrGestor can throw a TypeError when
user.cargo is undefined; update the check in the Casos.jsx component to guard
access to cargo (use optional chaining like user?.cargo and/or coerce to a
string before calling toLowerCase) so the includes call uses a safe string
(e.g., (user?.cargo || '').toLowerCase()); locate the constant isAdminOrGestor
and replace the direct user.cargo.toLowerCase() usage with the safe,
optional-chained variant to match Sidebar.jsx and Dashboard.jsx.

In `@frontend/src/areas/defensor/pages/Dashboard.jsx`:
- Around line 168-169: Dashboard.jsx applies the "Dr(a). " prefix using
isJuridico and toLowerCase but Header.jsx still checks user?.cargo ===
"defensor", causing inconsistent behavior; extract a shared helper (e.g., export
isCargoJuridico(user) and prefixoCargo(user) from a new utils/cargo.js) that
normalizes cargo with toLowerCase and checks
["defensor","coordenador","gestor"], then replace the local checks in both
Dashboard.jsx (where isJuridico/prefixo is used) and Header.jsx (replace the
strict equality check) to call the shared functions so both components use the
same normalized logic.

---

Outside diff comments:
In `@arquivos/Conhecimento/01-Referencia/BUSINESS_RULES.md`:
- Around line 629-633: The document jumps from heading "## 9. Geração do
Protocolo e Chave de Acesso" to "## 11. Session Locking e Concorrência"; locate
those headings in BUSINESS_RULES.md and fix the numbering by inserting or
restoring a proper "## 10" heading (and content if missing) or renumbering "##
11" to "## 10" (and adjust subsequent headings accordingly) so that section
order is sequential and any cross-references remain consistent.
- Around line 273-289: Há duas ocorrências consecutivas do subtítulo "### 3.6":
renomeie a segunda ocorrência ("### 3.6 Campos que o assistido preenche vs.
apenas o defensor") para "### 3.7 Campos que o assistido preenche vs. apenas o
defensor" e verifique/ajuste a numeração dos subtítulos seguintes e quaisquer
âncoras/links referentes para manter consistência; localize pelo texto exato dos
subtítulos para aplicar a correção.

In `@backend/src/controllers/casosController.js`:
- Around line 3053-3085: The power-user role check is inconsistent across
endpoints (here using userCargo -> isPowerUser = ["admin","gestor"]) while
lockController and carregarCasoDetalhado include "coordenador"; extract a single
helper (e.g., isPrivilegedRole(role) or isPowerUser(req.user)) in a shared util
module and replace the local checks: update this file's userCargo/isPowerUser
logic to call the helper, and update lockController and carregarCasoDetalhado to
import and use the same helper; ensure the canonical privileged set includes
"admin", "gestor", and "coordenador" and run tests to verify consistent RBAC
behavior across all lock/assume/detail flows.

---

Nitpick comments:
In `@arquivos/Conhecimento/03-Guias/GUIA_TESTES.md`:
- Around line 219-228: The guide currently documents loose "OR" status
assertions for specific endpoints; update the three affected entries to reflect
a single, explicit expected status that matches the real test behavior in
casosRoutes.test.js (or adjust the tests to match the doc). Specifically replace
the "`POST /api/casos/novo` com payload válido | Retorna 200 ou 201" entry with
the exact expected status (e.g., 201), change the "`GET /api/casos` com usuário
inativo | Retorna 401 ou 403" entry to the single expected status (401 or 403 as
enforced by casosRoutes.test.js), and change the "`GET /api/casos/buscar-cpf`
sem CPF | Retorna 400/404/422" entry to one definite status; ensure the text and
any adjacent examples or assertions in GUIA_TESTES.md match the status enforced
by casosRoutes.test.js.

In `@backend/src/controllers/defensoresController.js`:
- Around line 1-6: Remova a diretiva de arquivo inteiro "/* eslint-disable
no-unused-vars */" e corrija o import não utilizado "isSupabaseConfigured" em
backend/src/controllers/defensoresController.js: se não for necessário, exclua a
importação; se for necessária por side effect, documente esse motivo e aplique
uma diretiva de eslint apenas na linha do import (ex.: //
eslint-disable-next-line no-unused-vars) ou utilize realmente
"isSupabaseConfigured" no código (por exemplo em validação/condicional) para
evitar avisos supérfluos.

In `@backend/src/middleware/auth.js`:
- Line 42: The JWT payload built in loginDefensor (function loginDefensor in
backend/src/controllers/defensoresController.js) still uses "operador" as the
fallback role which conflicts with the auth middleware default "visualizador";
update the fallback to "visualizador" and apply the same .toLowerCase()
normalization when constructing the payload so the emitted token's cargo matches
the lowercase default used by auth middleware (and by
requireWriteAccess/requireSameUnit).

In `@backend/src/middleware/requireSameUnit.js`:
- Around line 24-26: The ID pre-check is good but BigInt(id) can still throw for
pathological inputs; in the requireSameUnit middleware wrap the BigInt(id)
conversion (the BigInt(id) call) in its own try/catch or validateable guard so
parse errors return res.status(400).json({ error: "ID de caso inválido." })
instead of bubbling to the outer catch (which returns 500); specifically catch
the exception from BigInt(id) inside the middleware and map it to a 400
response.
- Around line 30-37: The select in prisma.casos.findUnique inside the
requireSameUnit middleware includes status but the middleware never reads it;
either remove status from the select and fetch caso.status in the controller
that needs it, or keep it and add an explicit comment and/or JSDoc in
requireSameUnit explaining that caso is intentionally hydrated (req.caso) for
downstream controllers to use so future maintainers don't remove it; update any
controller code that relies on caso.status if you choose to move the fetch there
and ensure references to req.caso remain correct.

In `@backend/src/utils/stateMachine.js`:
- Around line 30-34: The error return object for invalid transitions is missing
the adminBypass key declared in the JSDoc; update the branch that returns { ok:
false, reason: `Transição inválida: ${from} -> ${to}` } to explicitly include
adminBypass: false so the returned shape matches the documented type (the
invalid-transition return in stateMachine.js where the object is constructed).

In `@backend/tests/middleware/requireSameUnit.test.js`:
- Around line 55-63: Add a test covering the other global-role bypass: create a
new test in requireSameUnit.test.js that sets req.user.cargo = "gestor" (and a
differing req.user.unidade_id), ensures mockPrisma.casos.findUnique is not
called, invokes requireSameUnit(req, res, next), and asserts next was called;
reference the existing test that sets req.user.cargo = "admin" and the mock
mockPrisma.casos.findUnique and reuse req/next/res setup to keep behavior
consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 308d8e85-aa55-400a-a146-bf2d0c6fd46f

📥 Commits

Reviewing files that changed from the base of the PR and between fc23776 and 7a49e82.

⛔ Files ignored due to path filters (1)
  • backend/uploads/peticoes/T1/peticao_inicial_T1.docx is excluded by !**/*.docx
📒 Files selected for processing (34)
  • .github/workflows/ci.yml
  • arquivos/Conhecimento/01-Referencia/ARCHITECTURE.md
  • arquivos/Conhecimento/01-Referencia/BUSINESS_RULES.md
  • arquivos/Conhecimento/01-Referencia/routes.md
  • arquivos/Conhecimento/03-Guias/GUIA_TESTES.md
  • arquivos/Conhecimento/04-Historico/2026-04-26_hardening_backend_seguranca.md
  • arquivos/Conhecimento/README.md
  • backend/new_test_output.txt
  • backend/rateLimiter_output.txt
  • backend/server.js
  • backend/src/controllers/casosController.js
  • backend/src/controllers/defensoresController.js
  • backend/src/controllers/lockController.js
  • backend/src/middleware/auth.js
  • backend/src/middleware/requireSameUnit.js
  • backend/src/middleware/requireWriteAccess.js
  • backend/src/routes/casos.js
  • backend/src/utils/helpers.js
  • backend/src/utils/stateMachine.js
  • backend/test_output.txt
  • backend/tests/integration/casosRoutes.test.js
  • backend/tests/integration/lockRoutes.test.js
  • backend/tests/middleware/auth.test.js
  • backend/tests/middleware/requireSameUnit.test.js
  • backend/tests/security/rateLimiter.test.js
  • backend/tests/unit/aiService.test.js
  • backend/tests/unit/utils.test.js
  • claude.md
  • frontend/src/__tests__/submissionValidation.test.js
  • frontend/src/areas/defensor/components/layout/Sidebar.jsx
  • frontend/src/areas/defensor/pages/Cadastro.jsx
  • frontend/src/areas/defensor/pages/Casos.jsx
  • frontend/src/areas/defensor/pages/Dashboard.jsx
  • frontend/src/areas/defensor/pages/GerenciarEquipe.jsx
✅ Files skipped from review due to trivial changes (1)
  • backend/server.js
🚧 Files skipped from review as they are similar to previous changes (3)
  • backend/tests/unit/aiService.test.js
  • backend/tests/integration/lockRoutes.test.js
  • frontend/src/areas/defensor/pages/GerenciarEquipe.jsx

Comment thread .github/workflows/ci.yml
Comment on lines +115 to +129
- name: Coverage summary
if: always()
run: |
echo "## Backend Coverage" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
cat logs/coverage/coverage-summary.json 2>/dev/null | node -e "
const data = require('fs').readFileSync('/dev/stdin','utf8');
try {
const j = JSON.parse(data).total;
console.log('Statements:', j.statements.pct + '%');
console.log('Branches: ', j.branches.pct + '%');
console.log('Functions: ', j.functions.pct + '%');
console.log('Lines: ', j.lines.pct + '%');
} catch { console.log('Coverage data not available'); }
" 2>/dev/null || echo "Coverage data not available"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Os resumos de cobertura nunca vão ler percentuais reais.

Hoje esses steps procuram coverage-summary.json, mas nem backend/jest.config.js nem frontend/vitest.config.js geram esse arquivo — ambos só exportam text, lcov e html. Na prática, o summary sempre cai em Coverage data not available, mesmo com cobertura gerada. Alinhe o workflow com um artefato existente ou adicione json-summary nos dois configs.

💡 Ajuste sugerido nos configs de teste
# backend/jest.config.js
-  coverageReporters: ["text", "lcov", "html"],
+  coverageReporters: ["text", "lcov", "html", "json-summary"],
# frontend/vitest.config.js
-      reporter: ["text", "lcov", "html"],
+      reporter: ["text", "lcov", "html", "json-summary"],

Also applies to: 166-180

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml around lines 115 - 129, The CI step currently
expects coverage-summary.json but the test configs (jest.config.js and
vitest.config.js) don't produce it; update both test configurations to include
the JSON summary reporter (json-summary / json) in their coverage reporters so a
coverage-summary.json is emitted, or alternatively change the CI step to read
whatever existing artifact those configs already produce (e.g., lcov or the
existing JSON path) and point cat to that file; specifically modify the
coverageReporters/coverageReporter settings in the jest.config.js and
vitest.config.js entries referenced in the diff and/or adjust the CI's cat path
to the actual file emitted so the node JSON-parsing block can read a real
coverage-summary.json.

- **HTTP 423 (Locked):** Retorno padrão quando outro usuário detém o lock
- **Admin Bypass:** Administradores podem forçar destravamento via painel
- **Nível 1 (Servidor/Estagiário/Defensor/Coordenador):** Atribuição de `servidor_id` — bloqueia edição de dados jurídicos e relato. Ativo em `pronto_para_analise` e `em_atendimento`.
- **Nível 2 (Defensor/Coordenador/Admin):** Atribuição de `defensor_id` — bloqueia etapa de protocolo e finalização. Ativo em `liberado_para_protocolo` e `em_protocolo`. **`servidor` e `estagiario` NUNCA adquirem Nível 2.**
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistência: gestor ausente do Nível 2 mas listado com permissão Protocolo/Finalizar na seção 9.

A linha 184 declara que o Nível 2 é adquirido por "Defensor/Coordenador/Admin", omitindo gestor. Porém, na matriz RBAC da seção 9 (linha 335), gestor aparece com Protocolo/Finalizar = ✅. Se gestor pode finalizar/protocolar, ele deveria também adquirir Nível 2 (defensor_id). Caso contrário, há uma contradição entre o modelo de locking e o de permissões. Recomendo alinhar para Defensor/Coordenador/Gestor/Admin ou esclarecer por que gestor realiza a finalização sem adquirir Nível 2.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@arquivos/Conhecimento/01-Referencia/ARCHITECTURE.md` at line 184, The text in
the Nível 2 description (the line that lists "Defensor/Coordenador/Admin" and
the `defensor_id` locking behavior) conflicts with the RBAC matrix in section 9
where `gestor` has Protocolo/Finalizar rights; update the Nível 2 line to either
include `Gestor` (i.e., "Defensor/Coordenador/Gestor/Admin") so `gestor`
acquires `defensor_id` and follows the same protocol/finalize locking, or add a
short clarifying sentence explaining why `gestor` can Protocolo/Finalizar
without acquiring Nível 2 and how locking/`defensor_id` is handled for that
role; reference the "Nível 2", `defensor_id`, and the RBAC "Protocolo/Finalizar"
entry for `gestor` when making the edit.

Comment on lines +482 to +484
### Teste de lock retorna 500 em vez do status esperado

Em versões anteriores do código, os testes de lock aceitavam `[200, 500]` devido à serialização imperfeita de campos `BigInt` usados pelos mocks do Prisma. Essa prática é desencorajada pois mascara falhas legítimas de servidor (500). O problema foi corrigido forçando que os ids sejam tipados via mocks fixos, permitindo que as asserções exijam precisamente os status corretos (`200`, `403` ou `423`).
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Nota sobre asserções de lock contradiz o estado atual de outros testes do mesmo PR.

A nota afirma que asserções tipo [200, 500] foram corrigidas em lockRoutes.test.js, mas casosRoutes.test.js (mesmo PR) ainda mantém expect([200, 201, 400]).toContain(...) e expect([401, 403]).toContain(...) (vide review naquele arquivo). Vale alinhar o discurso: ou a recomendação anti-OR-asserts vale para todos os testes, ou a nota deve ser mais específica sobre o escopo da correção (apenas lockRoutes.test.js).

🧰 Tools
🪛 LanguageTool

[uncategorized] ~484-~484: Esta conjunção deve ser separada por vírgulas e só deve ser utilizada no início duma frase para efeitos de estilo.
Context: ...s pelos mocks do Prisma. Essa prática é desencorajada pois mascara falhas legítimas de servidor (5...

(VERB_COMMA_CONJUNCTION)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@arquivos/Conhecimento/03-Guias/GUIA_TESTES.md` around lines 482 - 484, The
guide's note states OR-asserts like [200,500] were fixed in lockRoutes.test.js
but other tests in the same PR (notably casosRoutes.test.js) still use OR-style
assertions (e.g., expect([200,201,400]).toContain(...) and
expect([401,403]).toContain(...)), so either update the guide to explicitly
scope the correction to lockRoutes.test.js or update the tests to remove
OR-asserts; locate the tests by the filenames lockRoutes.test.js and
casosRoutes.test.js and either (A) change the note text in GUIA_TESTES.md to say
the fix only applied to lockRoutes.test.js, or (B) modify casosRoutes.test.js to
enforce precise status expectations instead of arrays (replace toContain checks
with exact expect(...).toBe(...) or explicit branching assertions) so the
documentation and tests are consistent.

Comment thread backend/src/controllers/casosController.js
Comment on lines +133 to +145
// --- LISTAR EQUIPE (Admin vê tudo, Gestor/Coordenador vê unidade) ---
export const listarDefensores = async (req, res) => {
try {
if (!req.user || req.user.cargo !== "admin") {
return res.status(403).json({ error: "Acesso negado." });
const CARGOS_VIEW_TEAM = ["admin", "gestor", "coordenador"];
const userCargo = req.user?.cargo?.toLowerCase();

if (!req.user || !CARGOS_VIEW_TEAM.includes(userCargo)) {
return res.status(403).json({ error: "Acesso negado. Apenas administradores e gestores podem visualizar a equipe." });
}

const whereClause = ["admin", "gestor"].includes(userCargo)
? {}
: { unidade_id: req.user.unidade_id };
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Comentário e mensagem de erro divergem do comportamento real.

O comentário diz “Gestor/Coordenador vê unidade”, mas em whereClause (linha 143) gestor é tratado igual a admin e enxerga todas as unidades — apenas coordenador é escopado por unidade_id. Além disso, a mensagem de 403 (linha 140) menciona apenas “administradores e gestores”, ignorando coordenador, que também é permitido pelo CARGOS_VIEW_TEAM.

Recomendo alinhar comentário e mensagem com o comportamento real para evitar confusão em manutenção futura.

🛠️ Correção sugerida
-// --- LISTAR EQUIPE (Admin vê tudo, Gestor/Coordenador vê unidade) ---
+// --- LISTAR EQUIPE (Admin/Gestor veem tudo; Coordenador vê apenas a própria unidade) ---
 export const listarDefensores = async (req, res) => {
   try {
     const CARGOS_VIEW_TEAM = ["admin", "gestor", "coordenador"];
     const userCargo = req.user?.cargo?.toLowerCase();

     if (!req.user || !CARGOS_VIEW_TEAM.includes(userCargo)) {
-      return res.status(403).json({ error: "Acesso negado. Apenas administradores e gestores podem visualizar a equipe." });
+      return res.status(403).json({ error: "Acesso negado. Apenas administradores, gestores e coordenadores podem visualizar a equipe." });
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/controllers/defensoresController.js` around lines 133 - 145, The
comment and 403 message in listarDefensores are inconsistent with the
whereClause: CARGOS_VIEW_TEAM includes "admin","gestor","coordenador" but the
logic treats "admin" and "gestor" as global and only "coordenador" as scoped by
unidade_id. Update the code comments and the 403.json message to reflect the
actual behavior (e.g., state that admins and gestores see all units while
coordenadores are limited to their unidade_id) so the comment, the status
message, and the behavior in listarDefensores / CARGOS_VIEW_TEAM / whereClause
remain consistent.

}
try {
return JSON.parse(input);
} catch (e) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remova o parâmetro de erro não utilizado no catch.

Há warning de lint para variável não usada em Line [47].

✅ Ajuste sugerido
-  } catch (e) {
+  } catch {
     return fallback;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (e) {
} catch {
return fallback;
}
🧰 Tools
🪛 GitHub Check: Lint · Backend

[warning] 47-47:
'e' is defined but never used

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/utils/helpers.js` at line 47, Remova o parâmetro não utilizado do
bloco catch em backend/src/utils/helpers.js: troque "catch (e) {" por "catch {"
para eliminar o warning de lint sobre variável não usada (localize o bloco catch
que atualmente declara "e" e atualize-o), mantendo o corpo do catch inalterado.

Comment on lines +49 to +58
return new Promise((resolve) => {
limiter(req, res, () => {
next();
resolve({ req, res, next, blocked: false });
});
// Se o limiter chamar res.status (bloqueou), marcamos
if (res.status.mock.calls.length > 0) {
resolve({ req, res, next, blocked: true });
}
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Detecção de bloqueio em runLimiter é dead code.

O if (res.status.mock.calls.length > 0) (linhas 55-57) executa sincronamente, antes do limiter ter qualquer chance de invocar res.status(429). O express-rate-limit v7 chama o handler de bloqueio de forma assíncrona (após consultar o store), então res.status.mock.calls está sempre vazio neste ponto. A função sempre resolve via o next() callback, e o ramo blocked: true nunca é atingido — embora isso não afete os 3 primeiros testes (que só validam next() na primeira requisição), torna o helper enganoso para futuros testes.

♻️ Sugestão
   function runLimiter(limiter, ip = "127.0.0.1") {
     const req = { ... };
-    const res = {
-      status: jest.fn().mockReturnThis(),
-      json: jest.fn().mockReturnThis(),
+    let blocked = false;
+    const res = {
+      status: jest.fn().mockReturnThis(),
+      json: jest.fn(function () { blocked = true; return this; }),
+      send: jest.fn(function () { blocked = true; return this; }),
       ...
     };
     const next = jest.fn();
     return new Promise((resolve) => {
       limiter(req, res, () => {
         next();
-        resolve({ req, res, next, blocked: false });
+        resolve({ req, res, next, blocked });
       });
-      if (res.status.mock.calls.length > 0) {
-        resolve({ req, res, next, blocked: true });
-      }
+      // Fallback: dá tempo para o handler async resolver
+      setTimeout(() => resolve({ req, res, next, blocked }), 50);
     });
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/tests/security/rateLimiter.test.js` around lines 49 - 58, The
synchronous check for res.status.mock.calls in runLimiter is dead code because
express-rate-limit invokes the block handler asynchronously; remove that
immediate if-branch and instead make the Promise resolve when either next() is
called or res.status(429) is invoked by hooking/resolving inside the mocked
res.status implementation. Concretely, in the helper that calls limiter(req,
res, ...) replace the post-call synchronous check with a mocked res.status =
jest.fn((code) => { resolve({ req, res, next, blocked: code === 429 }); return
res; }) (or similar) so the Promise resolves with blocked: true when limiter
actually calls res.status(429) and with blocked: false when next() runs.

Comment thread frontend/src/__tests__/submissionValidation.test.js
const [unidadeFiltro, setUnidadeFiltro] = useState("todas");
const { token, user } = useAuth();

const isAdminOrGestor = user && ["admin", "gestor"].includes(user.cargo.toLowerCase());
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Possível TypeError quando user.cargo for indefinido.

Se user estiver presente mas user.cargo for undefined/null (por exemplo, durante hidratação inicial do contexto ou após mudança no backend), user.cargo.toLowerCase() lança erro e quebra a página. Use optional chaining para alinhar com o padrão usado em Sidebar.jsx e Dashboard.jsx.

🛠️ Correção sugerida
-  const isAdminOrGestor = user && ["admin", "gestor"].includes(user.cargo.toLowerCase());
+  const isAdminOrGestor = ["admin", "gestor"].includes(user?.cargo?.toLowerCase());
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const isAdminOrGestor = user && ["admin", "gestor"].includes(user.cargo.toLowerCase());
const isAdminOrGestor = ["admin", "gestor"].includes(user?.cargo?.toLowerCase());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/areas/defensor/pages/Casos.jsx` at line 48, The expression
computing isAdminOrGestor can throw a TypeError when user.cargo is undefined;
update the check in the Casos.jsx component to guard access to cargo (use
optional chaining like user?.cargo and/or coerce to a string before calling
toLowerCase) so the includes call uses a safe string (e.g., (user?.cargo ||
'').toLowerCase()); locate the constant isAdminOrGestor and replace the direct
user.cargo.toLowerCase() usage with the safe, optional-chained variant to match
Sidebar.jsx and Dashboard.jsx.

Comment on lines +168 to +169
const isJuridico = ["defensor", "coordenador", "gestor"].includes(user?.cargo?.toLowerCase());
const prefixo = isJuridico ? "Dr(a). " : "";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistência de prefixo entre Dashboard e Header.

Aqui o prefixo Dr(a). é aplicado para defensor, coordenador e gestor, mas em frontend/src/areas/defensor/components/layout/Header.jsx (linhas ~33-34) a verificação ainda é user?.cargo === "defensor" (sem normalização e sem incluir os novos cargos jurídicos). Usuários coordenador/gestor verão o prefixo no Dashboard mas não no Header. Recomendo extrair essa regra para um helper compartilhado (ex.: isCargoJuridico(user)) e aplicar em ambos os locais.

♻️ Sugestão de extração
// utils/cargo.js
export const isCargoJuridico = (user) =>
  ["defensor", "coordenador", "gestor"].includes(user?.cargo?.toLowerCase());
export const prefixoCargo = (user) => (isCargoJuridico(user) ? "Dr(a). " : "");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/areas/defensor/pages/Dashboard.jsx` around lines 168 - 169,
Dashboard.jsx applies the "Dr(a). " prefix using isJuridico and toLowerCase but
Header.jsx still checks user?.cargo === "defensor", causing inconsistent
behavior; extract a shared helper (e.g., export isCargoJuridico(user) and
prefixoCargo(user) from a new utils/cargo.js) that normalizes cargo with
toLowerCase and checks ["defensor","coordenador","gestor"], then replace the
local checks in both Dashboard.jsx (where isJuridico/prefixo is used) and
Header.jsx (replace the strict equality check) to call the shared functions so
both components use the same normalized logic.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
frontend/coverage/sorter.js (1)

1-211: 🛠️ Refactor suggestion | 🟠 Major

Não versionar artefatos gerados de cobertura.

Este arquivo (e todo o diretório frontend/coverage/) é saída gerada automaticamente pelo istanbul-reports quando o Jest/Vitest roda com cobertura. Versionar esses artefatos:

  • polui o histórico do Git com diffs gigantes a cada execução de testes;
  • desencoraja a regeneração local (o relatório fica defasado em relação ao código);
  • aumenta desnecessariamente o tamanho do repositório e do clone em CI;
  • pode incluir caminhos absolutos da máquina que gerou o relatório.

Recomendo remover o diretório do versionamento e ignorá-lo. O eslint-disable no topo (linha 1) e o motivo de o lint ter alertas neste arquivo reforçam que ele não pertence à árvore versionada — é código de terceiros distribuído pelo reporter.

📦 Sugestão de ajuste no `.gitignore`
# frontend/.gitignore (ou .gitignore na raiz, conforme estrutura do monorepo)
+# Artefatos de cobertura gerados pelo Jest/Vitest
+coverage/
+frontend/coverage/

E remover do índice o diretório já comitado:

git rm -r --cached frontend/coverage
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/coverage/sorter.js` around lines 1 - 211, This file (identified by
the addSorting self-invoking function and the sorter UI code) is a generated
coverage artifact and should be removed from the repo and ignored: remove the
tracked coverage output from git (e.g., git rm -r --cached frontend/coverage),
add the coverage directory pattern to .gitignore (e.g., ignore
frontend/coverage/), commit the removal, and ensure CI/test docs reference
regeneration from istanbul/jest/vitest rather than checking in generated files.
backend/src/routes/bi.js (1)

17-22: ⚠️ Potential issue | 🟡 Minor

Mensagem de erro inconsistente com a lógica de autorização.

canExportBiLote agora permite admin E gestor (linha 8), mas a mensagem de erro na linha 19 ainda diz "restrita a administradores". Um coordenador bloqueado verá uma mensagem incorreta e nunca saberá que gestores também podem usar o recurso. Adicionalmente, a string usa "Exportacao" sem cedilha, enquanto o erro do requireBiAccess (linha 12) já foi corrigido para "módulo" com diacrítico.

🛠️ Sugestão
 const requireBatchExport = (req, res, next) => {
   if (!canExportBiLote(req.user)) {
-    return res.status(403).json({ error: "Exportacao em lote restrita a administradores." });
+    return res.status(403).json({ error: "Exportação em lote restrita a administradores e gestores." });
   }
   next();
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/routes/bi.js` around lines 17 - 22, The error string in
requireBatchExport is inaccurate and unaccented: update the response message
returned by requireBatchExport to correctly reflect the allowed roles (admin and
gestor) and fix diacritics, e.g. change the JSON error value to a Portuguese
message that mentions both "administradores e gestores" and uses "Exportação"
(matching the style used in requireBiAccess); locate requireBatchExport and
replace the existing message accordingly.
♻️ Duplicate comments (9)
frontend/coverage/lcov-report/block-navigation.js (1)

1-87: ⚠️ Potential issue | 🟠 Major

Mesmo problema do sorter.js: arquivo gerado e ainda quebrando o CI.

Este block-navigation.js é literalmente o asset distribuído pelo istanbul-reports (mesma IIFE jumpToCode, mesmos atalhos n/j/b/k/p). Ele só existe aqui porque o diretório coverage/ foi comitado — veja o comentário em frontend/coverage/sorter.js para a correção raiz (gitignore + git rm -r --cached).

Adicionalmente, o CI já está falhando neste arquivo:

ESLint warning na linha 1: "Unused eslint-disable directive (no problems were reported)".

Enquanto o diretório existir no repositório, ele também precisa ser excluído do lint, caso contrário a pipeline vai continuar falhando a cada regeneração (não adianta editar a linha 1, ela voltará na próxima execução de cobertura).

🔧 Sugestão para silenciar o lint enquanto o diretório existir

Em eslint.config.js (flat config) do frontend:

 export default [
   {
+    ignores: ['coverage/**', '**/coverage/**', 'dist/**', 'node_modules/**'],
   },
   // ...demais blocos
 ];

Ou, se ainda usar .eslintrc legado, criar/atualizar frontend/.eslintignore:

+coverage/
+**/coverage/**

A correção definitiva continua sendo remover o diretório do versionamento.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/coverage/lcov-report/block-navigation.js` around lines 1 - 87, This
file (the generated asset containing the IIFE jumpToCode) and the whole
frontend/coverage directory are tracked but should be removed from version
control and ignored; stop committing generated istanbul-reports assets by
removing the directory from the repo (git rm -r --cached frontend/coverage) and
add frontend/coverage to .gitignore, and silence ESLint while the directory
exists by adding frontend/coverage to your ESLint ignore (either via
eslint.config.js ignorePatterns or frontend/.eslintignore); after that, revert
any edits to block-navigation.js/jumpToCode and ensure the CI no longer lints
the generated coverage files.
backend/src/utils/helpers.js (1)

52-52: ⚠️ Potential issue | 🟡 Minor

Remova o parâmetro não usado do catch.

Esse e continua abrindo warning de lint aqui, sem agregar nada ao fallback.

💡 Ajuste direto
-  } catch (e) {
+  } catch {
     return fallback;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/utils/helpers.js` at line 52, Remova o parâmetro não usado do
bloco catch: altere o `catch (e)` presente em helpers.js para `catch` (sem
parâmetro) para eliminar o warning de lint; se for necessário logar o erro
depois, use o identificador dentro do bloco (por exemplo capturando via `catch
(err)` e usando-o) — caso contrário simplesmente remover o `e` resolve o
problema no bloco que contém `catch (e)` na função utilitária referida.
frontend/src/areas/defensor/contexts/AuthContext.jsx (1)

115-132: ⚠️ Potential issue | 🟠 Major

O helper de permissões não expõe a flag de distribuição.

O PR adiciona a distribuição de casos no frontend, mas o contexto só publica canManageTeam, canViewBi e canEditConfig. Qualquer tela que leia permissions.canDistribuir vai receber undefined e esconder a ação até para admin/gestor/coordenador.

➕ Ajuste sugerido
   const permissions = {
     canManageTeam: (user?.cargo || "").toLowerCase() === "admin",
     canViewBi: ["admin", "gestor", "coordenador"].includes((user?.cargo || "").toLowerCase()),
     canEditConfig: ["admin", "gestor"].includes((user?.cargo || "").toLowerCase()),
+    canDistribuir: ["admin", "gestor", "coordenador"].includes(
+      (user?.cargo || "").toLowerCase(),
+    ),
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/areas/defensor/contexts/AuthContext.jsx` around lines 115 - 132,
The permissions helper is missing the canDistribuir flag so components reading
permissions.canDistribuir get undefined; update the permissions object to
include canDistribuir (e.g. canDistribuir:
["admin","gestor","coordenador"].includes((user?.cargo || "").toLowerCase()))
alongside canManageTeam/canViewBi/canEditConfig, and ensure the same permissions
identifier is what you pass in contextValue (permissions is already included) so
consumers of AuthContext receive the new flag.
arquivos/Conhecimento/01-Referencia/ARCHITECTURE.md (1)

184-185: ⚠️ Potential issue | 🟡 Minor

Nível 2 ainda contradiz a própria matriz RBAC.

Aqui gestor segue fora do Nível 2, mas na seção 9 ele continua com Protocolo/Finalizar = ✅. A documentação fica ambígua sobre quem pode adquirir defensor_id e travar a etapa de protocolo.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@arquivos/Conhecimento/01-Referencia/ARCHITECTURE.md` around lines 184 - 185,
The RBAC doc is inconsistent about who can set defensor_id and lock protocol
steps: reconcile Nível 2 and Section 9 by explicitly stating whether the role
gestor is included in Nível 2 (or list which roles can perform
Protocolo/Finalizar). Update the Nível 2 line (mentioning `defensor_id`,
`liberado_para_protocolo`, `em_protocolo`) or Section 9 so both state the same
allowed roles (reference `gestor`, `admin`, `servidor`, `estagiario`), and
clarify how middleware `requireSameUnit` and global bypass for `admin`/`gestor`
interact with acquiring Nível 2.
backend/tests/integration/casosRoutes.test.js (4)

150-159: ⚠️ Potential issue | 🟡 Minor

mockPrismaCasos.findFirst = undefined continua sendo um anti-padrão.

Atribuir undefined à propriedade do mock deixa de ser uma jest.fn(), fazendo qualquer chamada do controller a prisma.casos.findFirst(...) lançar TypeError: ... is not a function — o que produziria um 500, contradizendo o expect(res.status).not.toBe(500) da linha 158. Além disso, a mutação não é revertida e vaza para os testes seguintes que importem este mesmo mock. Prefira mockPrismaCasos.findFirst.mockResolvedValue(null) ou .mockReset().

🛡️ Sugestão
-    // Mock listagem de casos
-    mockPrismaCasos.findFirst = undefined;
+    mockPrismaCasos.findFirst.mockResolvedValue(null);
+    mockPrismaCasos.findMany.mockResolvedValue([]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/tests/integration/casosRoutes.test.js` around lines 150 - 159, The
test sets mockPrismaCasos.findFirst = undefined which breaks the mock (making
calls to prisma.casos.findFirst throw) and leaks state; change this to
restore/mock the function instead — e.g., use
mockPrismaCasos.findFirst.mockResolvedValue(null) or
mockPrismaCasos.findFirst.mockReset() before calling request(app) in the test
that uses makeJwt("defensor") and mockPrismaDefensores.findUnique, so the
controller can call prisma.casos.findFirst safely and the expectation
expect(res.status).not.toBe(500) remains valid.

182-191: ⚠️ Potential issue | 🟡 Minor

Asserção [401, 403] ainda mascara violações de RBAC.

Para um JWT válido com visualizador ativo, requireWriteAccess (e/ou requireSameUnit) deve retornar especificamente 403. Aceitar 401 esconde tanto rejeição indevida do JWT (bug de auth) quanto o middleware de write-access nem sendo invocado. Recomendo expect(res.status).toBe(403) e investigar a configuração do mock de prisma.defensores.findUnique se houver intermitência.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/tests/integration/casosRoutes.test.js` around lines 182 - 191, The
test currently masks RBAC failures by accepting either 401 or 403; change the
assertion to expect(res.status).toBe(403) and ensure the auth/mocks are
correctly set so the request is authenticated: verify makeJwt("visualizador")
produces a valid token for the app's auth middleware and that
mockPrismaDefensores.findUnique.mockResolvedValue(makeUserProfile("visualizador"))
is configured before calling request(app).delete("/api/casos/1"); also confirm
mockPrismaCasos.findUnique returns the case with unidade_id and, if flakiness
persists, reset/clear mocks or align the test JWT signing key with the
middleware so requireWriteAccess / requireSameUnit are exercised rather than
short-circuiting to 401.

106-121: ⚠️ Potential issue | 🟠 Major

Asserções aceitam status mutuamente exclusivos, neutralizando o teste.

Aceitar [200, 201, 400] em "cria caso com payload válido → 201" significa que tanto sucesso quanto rejeição por validação passam, e a verificação de protocolo na linha 118 só roda se o status for 200/201 — uma regressão silenciosa que comece a rejeitar payload válido não falhará o teste. Complete os mocks de Prisma/Supabase do happy path e troque para expect(res.status).toBe(201) + expect(res.body).toHaveProperty("protocolo") incondicional.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/tests/integration/casosRoutes.test.js` around lines 106 - 121, The
tests "cria caso com payload válido → 201" and "retorna campo 'protocolo' na
resposta de sucesso" currently accept mutually exclusive statuses and
conditionally assert the protocolo field, which hides regressions; update the
happy-path mocks for Prisma/Supabase so the endpoint returns the success flow
for payloadMinimo, change the status assertion in the "cria caso com payload
válido → 201" test to expect(res.status).toBe(201), and make the protocolo
assertion unconditional (always expect(res.body).toHaveProperty("protocolo"))
after the request to request(app).post("/api/casos/novo").send(payloadMinimo) to
ensure failures surface.

138-148: ⚠️ Potential issue | 🟡 Minor

Asserção frouxa para "usuário inativo".

O título diz retorna 403 quando usuário está inativo, mas o expect([401, 403]).toContain(res.status) aceita qualquer um dos dois — o middleware de auth deve diferenciar usuário inativo (403) de credenciais inválidas (401). Após estabilizar os mocks, prenda em expect(res.status).toBe(403) para de fato cobrir o caminho de inativo descrito no título.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/tests/integration/casosRoutes.test.js` around lines 138 - 148, The
test titled "retorna 403 quando usuário está inativo" uses a loose assertion
expecting either 401 or 403; update the assertion to explicitly expect 403 so it
actually verifies the inactive-user path: replace expect([401,
403]).toContain(res.status) with expect(res.status).toBe(403), keeping the same
setup that uses makeJwt("defensor"),
mockPrismaDefensores.findUnique.mockResolvedValue(makeUserProfile("defensor", {
ativo: false })) and the request(app).get("/api/casos") call to target the
inactive-user middleware behavior.
backend/src/controllers/lockController.js (1)

75-79: ⚠️ Potential issue | 🟡 Minor

Mensagem 423 ainda menciona apenas "Administrador".

A regra de unlock foi expandida para admin, gestor e coordenador, mas a mensagem retornada na contenção continua dizendo "Apenas o Administrador pode liberar este caso." Isso confunde o usuário final (gestores/coordenadores também podem liberar, com a restrição de status para coordenadores).

📝 Sugestão de ajuste
-        message: `Este caso já está vinculado ao profissional ${holderName || "outro colega"}. Apenas o Administrador pode liberar este caso.`,
+        message: `Este caso já está vinculado ao profissional ${holderName || "outro colega"}. Apenas administradores, gestores ou coordenadores podem liberar este caso.`,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/controllers/lockController.js` around lines 75 - 79, Update the
423 response message in lockController.js (the block that returns
res.status(423).json with properties error/message/holder) so it no longer
mentions only "Administrador"; change the text to list the roles that can unlock
(Administrador, Gestor e Coordenador) and, if needed, append a short
parenthetical noting that Coordenador can unlock only when case status permits.
Ensure the holder value (holderName || "outro colega") remains unchanged and
only the human-facing message string is updated to reflect the expanded roles.
🧹 Nitpick comments (11)
backend/server.js (1)

47-52: Leia a allowlist de CORS da env, não do código.

A nova origem funciona, mas cada domínio extra agora exige commit e redeploy. Como a PR já documenta ALLOWED_ORIGINS, vale montar allowedOrigins a partir da variável de ambiente e manter essa lista fixa só como fallback local.

⚙️ Exemplo de ajuste
-const allowedOrigins = [
-  "https://maes-acao.vercel.app",
-  "https://maesemacao.defsulbahia.com.br",
-  "http://localhost:5173",
-  "http://127.0.0.1:5173",
-];
+const allowedOrigins = (
+  process.env.ALLOWED_ORIGINS ||
+  "https://maes-acao.vercel.app,https://maesemacao.defsulbahia.com.br,http://localhost:5173,http://127.0.0.1:5173"
+)
+  .split(",")
+  .map((origin) => origin.trim())
+  .filter(Boolean);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/server.js` around lines 47 - 52, Substitua a lista fixa
allowedOrigins por uma leitura da variável de ambiente ALLOWED_ORIGINS (por
exemplo via process.env.ALLOWED_ORIGINS), dividindo a string por vírgulas e
fazendo trim/filter para remover entradas vazias; mantenha a matriz atual como
fallback local caso a env não exista ou esteja vazia; update o uso de
allowedOrigins (onde quer que seja referenciada para configurar CORS) para usar
essa matriz resultante para que novos domínios possam ser adicionados sem
precisar de commit/redeploy.
frontend/src/areas/defensor/pages/DetalhesCaso.jsx (1)

2212-2217: Verificar se o modal não interfere com caso antes da distribuição.

ModalDistribuicao é montado sempre (mesmo quando isOpen é falso o early return interno previne o render do conteúdo, então isso é OK). O onRefresh={() => mutate()} e o onClose estão corretamente fechando o modal e revalidando o SWR. Considerar passar mutate diretamente sem wrap em arrow para evitar recriação a cada render — micro otimização opcional.

♻️ Micro otimização
       <ModalDistribuicao
         caso={caso}
         isOpen={isDistribuirOpen}
         onClose={() => setIsDistribuirOpen(false)}
-        onRefresh={() => mutate()}
+        onRefresh={mutate}
       />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/areas/defensor/pages/DetalhesCaso.jsx` around lines 2212 - 2217,
Ensure ModalDistribuicao does not mutate or read/modify the shared caso object
when closed; confirm its implementation only uses caso when isOpen is true (or
clones it before modifying). Also replace the wrapped onRefresh prop with the
SWR mutate function directly by passing onRefresh={mutate} to avoid creating a
new arrow on every render, while keeping onClose={() =>
setIsDistribuirOpen(false)} as the modal close handler; reference
ModalDistribuicao, props caso, isOpen, onClose and onRefresh/mutate.
backend/src/routes/config.js (1)

10-19: Acesso defensivo a req.user.cargo.

A linha 11 assume que req.user e req.user.cargo sempre existem. Embora o authMiddleware (linha 21) garanta isso no fluxo atual, qualquer regressão no middleware (ou uso desta rota num contexto sem auth, como teste) faria o handler lançar TypeError: Cannot read properties of undefined em vez de retornar 401/403 limpo. Como cargo já vem em lowercase do authMiddleware, o .toLowerCase() adicional também é redundante.

♻️ Sugestão
 const requireAdminOrGestor = (req, res, next) => {
-  const cargo = req.user.cargo.toLowerCase();
+  const cargo = req.user?.cargo?.toLowerCase();
   if (!["admin", "gestor"].includes(cargo)) {
     return res.status(403).json({ 
       error: "Acesso negado", 
       message: "Apenas administradores e gestores podem acessar as configurações do sistema." 
     });
   }
   next();
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/routes/config.js` around lines 10 - 19, The middleware
requireAdminOrGestor assumes req.user and req.user.cargo exist and calls
.toLowerCase(), which can throw if authMiddleware is absent or regresses; update
requireAdminOrGestor to defensively check for req.user and req.user.cargo and
return a 401 (or 403 per project convention) JSON response when missing, then
use the existing cargo value directly (remove the redundant .toLowerCase() call
since authMiddleware already normalizes it) and keep the role check against
["admin","gestor"] before calling next(); reference requireAdminOrGestor and
authMiddleware when making this change.
frontend/src/areas/defensor/pages/Dashboard.jsx (1)

197-241: LGTM — atalhos rápidos coerentes com os guards de rota.

As três condições (canManageTeam, canViewBi, canEditConfig) batem 1:1 com os componentes TeamRoute, BiRoute e GestorRoute em frontend/src/App.jsx (linhas 76-96 e 170-200), evitando que um usuário consiga acessar o atalho mas seja redirecionado pelo guard.

Ponto opcional: se nenhuma das três permissões for true, o <div className="grid ..."> permanece no DOM ocupando espaço/gap. Considerar envolver o bloco em uma checagem (permissions.canManageTeam || permissions.canViewBi || permissions.canEditConfig) && (...) para suprimir o container vazio.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/areas/defensor/pages/Dashboard.jsx` around lines 197 - 241, The
grid container currently renders even when all three flags are false; update
Dashboard.jsx to only render the container when at least one permission is true
by wrapping the div in a single conditional: (permissions.canManageTeam ||
permissions.canViewBi || permissions.canEditConfig) && (<div className="grid
...">...</div>); this keeps the existing inner Link blocks (guarded by
permissions.canManageTeam, canViewBi, canEditConfig) unchanged and prevents an
empty grid from occupying layout space.
frontend/src/areas/defensor/hooks/useBiData.js (1)

107-115: Proteger gravações de cache contra QuotaExceededError.

sessionStorage.setItem(DATA_CACHE_KEY, JSON.stringify(data)) é executado a cada mudança de data. Para relatórios BI volumosos (muitas linhas/widgets), o JSON serializado pode estourar a cota do sessionStorage (~5MB), lançando QuotaExceededError e quebrando o ciclo de render do hook silenciosamente. Os loaders já têm try/catch (linhas 77-93), mas os writers não.

🛡️ Sugestão
   useEffect(() => {
-    sessionStorage.setItem(FILTROS_CACHE_KEY, JSON.stringify(filtros));
+    try {
+      sessionStorage.setItem(FILTROS_CACHE_KEY, JSON.stringify(filtros));
+    } catch {
+      /* sessionStorage indisponível ou cota excedida */
+    }
   }, [filtros]);
 
   useEffect(() => {
     if (data) {
-      sessionStorage.setItem(DATA_CACHE_KEY, JSON.stringify(data));
+      try {
+        sessionStorage.setItem(DATA_CACHE_KEY, JSON.stringify(data));
+      } catch {
+        /* payload muito grande para sessionStorage */
+      }
     }
   }, [data]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/areas/defensor/hooks/useBiData.js` around lines 107 - 115, Wrap
the sessionStorage writes inside the useEffect hooks that call
sessionStorage.setItem for FILTROS_CACHE_KEY and DATA_CACHE_KEY in a try/catch;
catch QuotaExceededError (or any error) and silently skip or remove stale cache
(e.g., sessionStorage.removeItem(DATA_CACHE_KEY) or simply not write) and log a
warning so the render cycle doesn't break — update the useEffect blocks around
the DATA_CACHE_KEY and FILTROS_CACHE_KEY writes in useBiData.js to perform the
guarded write and handle failures gracefully.
backend/src/controllers/lockController.js (1)

16-16: Variável _isDefensorOrCoordenador declarada mas nunca utilizada.

O prefixo _ sugere uso intencionalmente descartado, porém a variável não é referenciada em nenhum lugar do controller. Se a regra de cargo não influencia mais o fluxo de lockCaso (apenas isAdmin e isServidorOrEstagiario são usados), remova-a para evitar confusão na próxima manutenção.

♻️ Sugestão de remoção
     const isAdmin = cargo === 'admin';
-    const _isDefensorOrCoordenador = cargo.includes("defensor") || cargo === "coordenador";
     const isServidorOrEstagiario = cargo === "servidor" || cargo === "estagiario";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/controllers/lockController.js` at line 16, Variável
_isDefensorOrCoordenador está declarada mas não usada; remova sua declaração
para evitar confusão no controller. Abra o arquivo contendo a função lockCaso e
elimine a linha que cria _isDefensorOrCoordenador (a expressão
cargo.includes("defensor") || cargo === "coordenador"), garantindo que
referências a isAdmin e isServidorOrEstagiario permaneçam inalteradas; rode os
testes/linter para confirmar que nenhuma outra parte do módulo depende dessa
variável.
backend/src/utils/configCache.js (2)

29-29: Use o logger aplicacional em vez de console.

O restante do backend usa logger (../utils/logger.js) com níveis estruturados; misturar console.error/console.log aqui prejudica a observabilidade (filtragem por nível, formato, sinks).

♻️ Sugestão
-import { prisma } from "../config/prisma.js";
+import { prisma } from "../config/prisma.js";
+import logger from "./logger.js";
@@
-    console.error("❌ Erro ao buscar configurações do sistema:", error);
+    logger.error(`Erro ao buscar configurações do sistema: ${error.message}`);
@@
-  console.log("🧹 Cache de configurações do sistema invalidado.");
+  logger.info("Cache de configurações do sistema invalidado.");

Also applies to: 42-42

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/utils/configCache.js` at line 29, Substitua as chamadas diretas a
console (por exemplo a linha com console.error("❌ Erro ao buscar configurações
do sistema:", error) e a outra ocorrência mencionada) pelo logger aplicacional
importado de ../utils/logger.js; importe logger se ainda não houver e use
logger.error("Erro ao buscar configurações do sistema", { error }) para
preservar o objeto de erro e o formato estruturado, e troque
console.log/console.warn por logger.info/logger.warn conforme o nível apropriado
nas funções/constantes relacionadas dentro de configCache.js.

39-43: Invalidação de cache só atua no processo local — atenção em ambientes multi-instância.

invalidarCache() zera apenas a memória do processo Node atual. Se o backend rodar em múltiplas instâncias (PM2 cluster, várias réplicas atrás de um load balancer, serverless), as demais continuarão servindo o valor cacheado até o TTL de 5 min expirar. Para uma propagação consistente após PUT /config, considere uma das opções:

  • Reduzir o TTL para algo como 30–60s (compromisso simples).
  • Publicar a invalidação via Redis pub/sub / Postgres LISTEN/NOTIFY.
  • Persistir a versão do config (updated_at) e revalidar quando mudar.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/utils/configCache.js` around lines 39 - 43, A invalidação atual
em invalidarCache() só limpa a memória do processo (variáveis _cache e
_cacheExpiry), o que não propaga em ambientes com múltiplas instâncias; atualize
a implementação para propagar invalidações entre instâncias: escolher e
implementar uma estratégia (ex.: reduzir o TTL de _cacheExpiry para 30–60s, ou
publicar uma mensagem de invalidação via Redis pub/sub ou Postgres
LISTEN/NOTIFY, ou persistir/checar uma versão/updated_at no armazenamento e
revalidar quando mudar) e integrar essa lógica ao handler de PUT /config para
que, além de chamar invalidarCache(), publique a notificação ou atualize a
versão compartilhada; mantenha nomes existentes (invalidarCache, _cache,
_cacheExpiry) para localizar o código e ajuste onde o TTL de 5 min é definido se
optar por reduzir o tempo.
frontend/src/areas/defensor/components/casos/ModalDistribuicao.jsx (1)

13-20: Falta cleanup/abort no useEffect — possível "set state on unmounted" e race condition.

Se o usuário fechar o modal antes da resposta de /defensores chegar (ou abrir/fechar várias vezes em sequência), o setDefensores(data) será chamado em um componente já desmontado e a resposta de uma abertura anterior pode sobrescrever a atual. Use um AbortController (encaminhado ao authFetch/fetch) ou um flag de cancelamento.

♻️ Sugestão
-  React.useEffect(() => {
-    if (isOpen) {
-      authFetch("/defensores")
-        .then(r => r.json())
-        .then(data => setDefensores(data))
-        .catch(() => setDefensores([]));
-    }
-  }, [isOpen]);
+  useEffect(() => {
+    if (!isOpen) return;
+    let cancelado = false;
+    authFetch("/defensores")
+      .then((r) => (r.ok ? r.json() : []))
+      .then((data) => { if (!cancelado) setDefensores(Array.isArray(data) ? data : []); })
+      .catch(() => { if (!cancelado) setDefensores([]); });
+    return () => { cancelado = true; };
+  }, [isOpen]);

E ajuste o import:

-import React, { useState } from "react";
+import React, { useEffect, useState } from "react";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/areas/defensor/components/casos/ModalDistribuicao.jsx` around
lines 13 - 20, The effect in ModalDistribuicao uses authFetch and calls
setDefensores without cleanup, causing state updates after unmount and race
conditions; fix by creating an AbortController inside the useEffect, pass
controller.signal to authFetch (and down to fetch), and in the cleanup call
controller.abort(); also guard the then() that calls setDefensores to ignore
AbortError (or check controller.signal.aborted) so responses from previous
requests don’t overwrite newer state — update the useEffect, authFetch
call-site, and any fetch invocation to accept and forward a signal parameter.
backend/seed_permissions.js (1)

36-38: Refator opcional: extrair regras de vinculação para uma estrutura declarativa.

A cadeia de condicionais para decidir quais cargos pulam quais permissões fica frágil conforme cargos/permissões crescem (já temos 7 cargos × 3 permissões e a regra do gerenciar_equipe lista 6 cargos). Considere uma matriz/whitelist por permissão para tornar a intenção explícita e reduzir o risco de erros de digitação ao adicionar novos cargos.

♻️ Sugestão de estrutura
+const PERMISSOES_POR_CARGO = {
+  atender_caso:    new Set(["admin", "gestor", "coordenador", "defensor", "servidor", "estagiario"]),
+  protocolar_caso: new Set(["admin", "gestor", "coordenador", "defensor"]),
+  gerenciar_equipe: new Set(["admin"]),
+};
+
 for (const perm of permissoesDB) {
-  // Regras de vinculação
-  if ((cargoNome === "servidor" || cargoNome === "estagiario" || cargoNome === "visualizador") && perm.chave === "protocolar_caso") continue;
-  if ((cargoNome === "gestor" || cargoNome === "defensor" || cargoNome === "coordenador" || cargoNome === "servidor" || cargoNome === "estagiario" || cargoNome === "visualizador") && perm.chave === "gerenciar_equipe") continue;
-  if (cargoNome === "visualizador" && perm.chave === "atender_caso") continue;
+  if (!PERMISSOES_POR_CARGO[perm.chave]?.has(cargoNome)) continue;
   ...
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/seed_permissions.js` around lines 36 - 38, The current chained
conditionals using cargoNome and perm.chave are fragile; replace them with a
declarative mapping (e.g., a plain object like permissionRoleWhitelist or
permissionExclusions) that maps each permission key ("protocolar_caso",
"gerenciar_equipe", "atender_caso") to the array of roles that should be
excluded or allowed, then update the loop that references cargoNome and
perm.chave to consult that mapping (use Array.includes or a Set for lookup)
instead of long OR-chains; update any variable names in seed_permissions.js
accordingly so the logic in the loop is a single lookup against the mapping.
frontend/src/areas/defensor/pages/ConfiguracoesSistema.jsx (1)

49-53: Mutação de estado + key={index} em lista mutável.

Dois pontos relacionados:

  1. handleUpdateHorario faz cópia rasa ([...horarios]) e em seguida muta o objeto: newHorarios[index][field] = value. Como o spread mantém as mesmas referências, o objeto em horarios[index] também é mutado, o que viola a imutabilidade do estado e pode esconder bugs com React.memo/comparações.
  2. O key={index} na renderização (linha 169), combinado com handleRemoveHorario (filter), faz com que ao remover uma janela do meio o React reaproveite os componentes de input com valores deslocados — gerando estado fantasma no DOM (foco, validação nativa do <input type="time">, etc.).
♻️ Sugestão
   const handleAddHorario = () => {
-    setHorarios([...horarios, { dia: "todos", inicio: "08:00", fim: "18:00" }]);
+    setHorarios([
+      ...horarios,
+      { _key: crypto.randomUUID(), dia: "todos", inicio: "08:00", fim: "18:00" },
+    ]);
   };
@@
   const handleUpdateHorario = (index, field, value) => {
-    const newHorarios = [...horarios];
-    newHorarios[index][field] = value;
-    setHorarios(newHorarios);
+    setHorarios((prev) =>
+      prev.map((h, i) => (i === index ? { ...h, [field]: value } : h))
+    );
   };

E na renderização:

-              horarios.map((h, index) => (
-                <div 
-                  key={index} 
+              horarios.map((h, index) => (
+                <div
+                  key={h._key ?? `${h.dia}-${index}`}

Lembre-se de remover _key antes de serializar em JSON.stringify(horarios) no handleSave, ou usar uma estrutura paralela só para keys.

Also applies to: 167-170

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/areas/defensor/pages/ConfiguracoesSistema.jsx` around lines 49 -
53, O problema é mutação de estado em handleUpdateHorario (spread raso + mutação
do objeto) e uso de key={index} que causa reaproveitamento incorreto; para
corrigir, ao atualizar use uma cópia do item alvo em vez de mutar a referência
(por exemplo: setHorarios(h => h.map((it,i)=> i===index ? {...it, [field]:
value} : it)) no lugar de newHorarios[index][field] = value), introduza uma
chave estável única por item (adicionar um campo _key/id ao criar um novo
horário e usar key={item._key} na renderização) e ajuste handleRemoveHorario
para filtrar por esse id; por fim, remova/ignore _key antes de serializar em
handleSave (ou mantenha uma estrutura paralela só para keys) para não enviar
metadados extras.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@arquivos/Conhecimento/04-Historico/feitos/2026-04-25_suite_testes_cicd.md`:
- Around line 86-90: Os blocos de código com três backticks no documento estão
sem identificador de linguagem — isso quebra a regra MD040; atualize os fence
blocks que contêm o diagrama ASCII e o bloco listado (os trechos que começam com
"lint-backend ..." e o bloco que contém ".github/ workflows/ ci.yml ..." /
"submissionValidation.test.js") para usar um language tag apropriado (por
exemplo troque ``` por ```text para diagramas ASCII e use ```yaml quando o
conteúdo for YAML), garantindo que todos os blocos de código possuam
identificador de linguagem.

In `@arquivos/Conhecimento/04-Historico/feitos/task.md`:
- Line 52: Edit the wording in the task description that references exportarPdf
and hiddenControls: replace the informal phrase “pro print” with the formal
“para print” so the sentence reads that the code should save
node.dataset.wasHidden = node.classList.contains("hidden") ? "true" : "false"
during the search for buttons to hide for printing; update the phrase near the
exportarPdf/hiddenControls instruction accordingly.

In `@backend/prisma/schema.prisma`:
- Around line 302-309: No modelo configuracoes_sistema, o campo updated_at está
sem a anotação de fuso (`@db.Timestamptz(6)`), causando inconsistência com os
demais modelos; atualize a definição do campo updated_at em
configuracoes_sistema (o símbolo é updated_at no model configuracoes_sistema)
para manter `@default`(now()) `@updatedAt` e adicionar `@db.Timestamptz`(6) para que o
tipo Prisma mapeie para timestamptz(6) no PostgreSQL.

In `@backend/src/controllers/casosController.js`:
- Around line 1037-1041: Os campos atuais (nome_mae_assistido,
nome_pai_assistido e filiacao) estão caindo para
nome_mae_representante/nome_pai_representante e assim podem atribuir os nomes
dos avós como filiação do assistido; ajuste a lógica para que nome_mae_assistido
e nome_pai_assistido nunca usem nome_mae_representante/nome_pai_representante
como fallback — apenas use caso.nome_mae_assistido,
caso.partes?.nome_mae_assistido e caso.nome_pai_assistido,
caso.partes?.nome_pai_assistido respectivamente, e para filiacao monte a string
a partir desses mesmos campos do assistido (usando "N/I" quando ambos forem
indefinidos) para evitar derivar dados dos representantes.
- Around line 3212-3266: The distribuirCaso handler currently allows
redistribution without validating the caller's role or the target user; before
performing the Supabase update, fetch and validate the target user and the
caller: (1) query the target user by usuario_id and ensure they exist and are
active; (2) verify the caller's cargo (req.user.cargo) is authorized to perform
this distribution for the chosen campoAlvo (e.g., only servidores can be
assigned to servidor_id, only defenders to defensor_id) and that the target
user's cargo matches the expected role; (3) ensure unidade isolation by checking
targetUser.unidade_id === casoAtual.unidade_id (or other permitted cross-unit
rules) and reject with 403 if mismatched; only then build updateData and run the
conditional atomic update as before (preserve the statusPermitidos logic and the
.in('status', statusPermitidos) guard); return clear 4xx responses for
missing/invalid usuario_id, non-existent or inactive user, cargo mismatch, or
unidade mismatch.

In `@backend/src/controllers/configController.js`:
- Around line 32-33: The code currently uses String(v) when building the
Prisma/DB payload (update: { valor: String(v) }, create: { chave: k, valor:
String(v) }) which turns null/undefined into the literal strings
"null"/"undefined" and later breaks JSON.parse in verificarBloqueioHorario;
update the logic in the batch/config upsert path to first reject or handle
null/undefined values (e.g. throw a validation error or skip the key) and
otherwise serialize non-string values with JSON.stringify instead of String;
specifically modify the code that constructs payloads for update/create to check
if v === null || v === undefined and handle accordingly, and for valid
non-string objects use valor: (typeof v === 'string' ? v : JSON.stringify(v)),
referencing the upsert/update/create construction in configController.js and the
downstream verificarBloqueioHorario in biController.js.
- Around line 27-36: The batch upserts for configs (built as updates and
executed with Promise.all) are not atomic and can leave partial state on
failure; replace the parallel Promise.all call with a Prisma transaction by
executing the upsert operations via prisma.$transaction([...upsertPromises])
(using the same upsert calls created for prisma.configuracoes_sistema.upsert) so
all changes commit or all rollback, and propagate or handle errors as before.
Ensure you still build the entries array and map to the same upsert operations
(the variable updates) and pass that array into prisma.$transaction, rather than
Promise.all.

In `@frontend/coverage/prettify.css`:
- Line 1: The coverage output (e.g., generated files like prettify.css,
base.css, prettify.js and other files under the coverage report) should not be
versioned: add the coverage directory to .gitignore, stop tracking it with git
(git rm -r --cached <coverage-dir> and commit the removal) and ensure CI
publishes the coverage folder as an artifact (e.g., actions/upload-artifact)
instead of committing generated files; update any relevant workflow steps to
upload the coverage folder and remove any commits/changes that add these
generated files.

In `@frontend/src/App.jsx`:
- Around line 90-96: The TeamRoute component uses permissions.canManageTeam from
useAuth but AuthContext sets that flag as cargo === "admin", which conflicts
with the comment claiming "Admin, Gestor, Coordenador"; either update the
permission logic or update the comment/summary: Option A — change the logic in
AuthContext.jsx where permissions.canManageTeam is computed to return true for
cargo values "admin", "gestor", or "coordenador" (and update any tests/docs
accordingly); Option B — keep the current admin-only logic and update the
comment in App.jsx (and the AI summary/title at line ~169) to state that
TeamRoute is admin-only; locate the code via the TeamRoute component, useAuth
hook, permissions.canManageTeam, and the cargo === "admin" check to make the
change.

In `@frontend/src/areas/defensor/components/casos/ModalDistribuicao.jsx`:
- Around line 24-28: The filter can throw when d.nome or d.email is
null/undefined; update the defensores.filter callback used to compute
defensoresFiltrados so it safely normalizes those fields before calling
toLowerCase (e.g., use (d.nome ?? '') and (d.email ?? '') or (d.nome || '') and
(d.email || '') or optional chaining with fallback) and compare against
busca.toLowerCase(); ensure Busca is still lowercased once (busca.toLowerCase())
and reused so the filter won't break on missing nome/email.
- Line 54: Replace the non-standard Tailwind class "z-100" with the arbitrary
value syntax "z-[100]" in the ModalDistribuicao component's modal wrapper div so
the z-index is applied correctly (look for the JSX div with className containing
"fixed inset-0 ... z-100" in ModalDistribuicao.jsx and update that class token
to "z-[100]").
- Line 108: The component is accessing d.cargo?.nome and d.unidade?.nome but the
backend returns cargo as a string and the unit name as unidade_nome, so those
expressions are always undefined; update the render to use the string fields
directly (use d.cargo as the job label and d.unidade_nome as the unit label) and
keep the existing fallbacks (e.g., d.cargo || "Profissional" and d.unidade_nome
|| "Sem Unidade") in the ModalDistribuicao.jsx output.

In `@frontend/src/areas/defensor/contexts/AuthContext.jsx`:
- Around line 106-112: The empty catch in marcarNotificacaoLida is causing lint
failures and the error parameter is unused; update the catch to handle the error
(e.g., log it with console.error or forward to your app logger and/or show a
user-facing notification) and use the caught variable so it is not unused, while
preserving existing behavior of not updating state on failure; locate the
function marcarNotificacaoLida in AuthContext.jsx and replace the empty catch
block with a minimal error handler that logs the error and optionally triggers
any existing toast/notification helper.

In `@frontend/src/areas/defensor/pages/ConfiguracoesSistema.jsx`:
- Around line 55-76: handleLiberarAgora currently only shows success when
response.ok and stays silent on 4xx/5xx; add an else branch after the
response.ok check that reads the error payload (await response.json() or
response.text() as appropriate) and calls toast.error with a clear message
including either the returned error.message or response.status/statusText; also
handle JSON parse failures gracefully (fallback to statusText) so users get
feedback when the backend rejects the request.

In `@frontend/src/areas/defensor/pages/DetalhesCaso.jsx`:
- Around line 1994-2002: The permissions object in AuthContext is missing the
canDistribuir flag, so checks like permissions.canDistribuir in the DetalhesCaso
and Casos components never pass; update the permissions object inside the
AuthContext provider (the permissions returned/constructed in AuthContext.jsx)
to include a canDistribuir boolean (computed from the user/roles the same way
canManageTeam/canViewBi/canEditConfig are computed), ensure the logic uses the
correct roles list per business rules, and then verify ModalDistribuicao-related
buttons in DetalhesCaso and Casos now render when canDistribuir is true.

In `@frontend/src/areas/defensor/pages/Relatorios.jsx`:
- Around line 139-168: The useEffect that auto-triggers gerar based on filtros
is declared after early returns, which violates React hooks order when data or
permissions cause an early return; move the useEffect block (the one referencing
filtros.periodo, filtros.dataInicio, filtros.dataFim, filtros.unidade_id,
filtros.topN and calling gerar().catch(() => {})) to be declared before any
conditional returns (i.e., above the checks for data?.bloqueadoPorHorario and
!permissions?.canViewBi) so hooks run in a stable order; keep the same
dependency array and handler logic and remove or adjust the eslint-disable
comment if needed.

---

Outside diff comments:
In `@backend/src/routes/bi.js`:
- Around line 17-22: The error string in requireBatchExport is inaccurate and
unaccented: update the response message returned by requireBatchExport to
correctly reflect the allowed roles (admin and gestor) and fix diacritics, e.g.
change the JSON error value to a Portuguese message that mentions both
"administradores e gestores" and uses "Exportação" (matching the style used in
requireBiAccess); locate requireBatchExport and replace the existing message
accordingly.

In `@frontend/coverage/sorter.js`:
- Around line 1-211: This file (identified by the addSorting self-invoking
function and the sorter UI code) is a generated coverage artifact and should be
removed from the repo and ignored: remove the tracked coverage output from git
(e.g., git rm -r --cached frontend/coverage), add the coverage directory pattern
to .gitignore (e.g., ignore frontend/coverage/), commit the removal, and ensure
CI/test docs reference regeneration from istanbul/jest/vitest rather than
checking in generated files.

---

Duplicate comments:
In `@arquivos/Conhecimento/01-Referencia/ARCHITECTURE.md`:
- Around line 184-185: The RBAC doc is inconsistent about who can set
defensor_id and lock protocol steps: reconcile Nível 2 and Section 9 by
explicitly stating whether the role gestor is included in Nível 2 (or list which
roles can perform Protocolo/Finalizar). Update the Nível 2 line (mentioning
`defensor_id`, `liberado_para_protocolo`, `em_protocolo`) or Section 9 so both
state the same allowed roles (reference `gestor`, `admin`, `servidor`,
`estagiario`), and clarify how middleware `requireSameUnit` and global bypass
for `admin`/`gestor` interact with acquiring Nível 2.

In `@backend/src/controllers/lockController.js`:
- Around line 75-79: Update the 423 response message in lockController.js (the
block that returns res.status(423).json with properties error/message/holder) so
it no longer mentions only "Administrador"; change the text to list the roles
that can unlock (Administrador, Gestor e Coordenador) and, if needed, append a
short parenthetical noting that Coordenador can unlock only when case status
permits. Ensure the holder value (holderName || "outro colega") remains
unchanged and only the human-facing message string is updated to reflect the
expanded roles.

In `@backend/src/utils/helpers.js`:
- Line 52: Remova o parâmetro não usado do bloco catch: altere o `catch (e)`
presente em helpers.js para `catch` (sem parâmetro) para eliminar o warning de
lint; se for necessário logar o erro depois, use o identificador dentro do bloco
(por exemplo capturando via `catch (err)` e usando-o) — caso contrário
simplesmente remover o `e` resolve o problema no bloco que contém `catch (e)` na
função utilitária referida.

In `@backend/tests/integration/casosRoutes.test.js`:
- Around line 150-159: The test sets mockPrismaCasos.findFirst = undefined which
breaks the mock (making calls to prisma.casos.findFirst throw) and leaks state;
change this to restore/mock the function instead — e.g., use
mockPrismaCasos.findFirst.mockResolvedValue(null) or
mockPrismaCasos.findFirst.mockReset() before calling request(app) in the test
that uses makeJwt("defensor") and mockPrismaDefensores.findUnique, so the
controller can call prisma.casos.findFirst safely and the expectation
expect(res.status).not.toBe(500) remains valid.
- Around line 182-191: The test currently masks RBAC failures by accepting
either 401 or 403; change the assertion to expect(res.status).toBe(403) and
ensure the auth/mocks are correctly set so the request is authenticated: verify
makeJwt("visualizador") produces a valid token for the app's auth middleware and
that
mockPrismaDefensores.findUnique.mockResolvedValue(makeUserProfile("visualizador"))
is configured before calling request(app).delete("/api/casos/1"); also confirm
mockPrismaCasos.findUnique returns the case with unidade_id and, if flakiness
persists, reset/clear mocks or align the test JWT signing key with the
middleware so requireWriteAccess / requireSameUnit are exercised rather than
short-circuiting to 401.
- Around line 106-121: The tests "cria caso com payload válido → 201" and
"retorna campo 'protocolo' na resposta de sucesso" currently accept mutually
exclusive statuses and conditionally assert the protocolo field, which hides
regressions; update the happy-path mocks for Prisma/Supabase so the endpoint
returns the success flow for payloadMinimo, change the status assertion in the
"cria caso com payload válido → 201" test to expect(res.status).toBe(201), and
make the protocolo assertion unconditional (always
expect(res.body).toHaveProperty("protocolo")) after the request to
request(app).post("/api/casos/novo").send(payloadMinimo) to ensure failures
surface.
- Around line 138-148: The test titled "retorna 403 quando usuário está inativo"
uses a loose assertion expecting either 401 or 403; update the assertion to
explicitly expect 403 so it actually verifies the inactive-user path: replace
expect([401, 403]).toContain(res.status) with expect(res.status).toBe(403),
keeping the same setup that uses makeJwt("defensor"),
mockPrismaDefensores.findUnique.mockResolvedValue(makeUserProfile("defensor", {
ativo: false })) and the request(app).get("/api/casos") call to target the
inactive-user middleware behavior.

In `@frontend/coverage/lcov-report/block-navigation.js`:
- Around line 1-87: This file (the generated asset containing the IIFE
jumpToCode) and the whole frontend/coverage directory are tracked but should be
removed from version control and ignored; stop committing generated
istanbul-reports assets by removing the directory from the repo (git rm -r
--cached frontend/coverage) and add frontend/coverage to .gitignore, and silence
ESLint while the directory exists by adding frontend/coverage to your ESLint
ignore (either via eslint.config.js ignorePatterns or frontend/.eslintignore);
after that, revert any edits to block-navigation.js/jumpToCode and ensure the CI
no longer lints the generated coverage files.

In `@frontend/src/areas/defensor/contexts/AuthContext.jsx`:
- Around line 115-132: The permissions helper is missing the canDistribuir flag
so components reading permissions.canDistribuir get undefined; update the
permissions object to include canDistribuir (e.g. canDistribuir:
["admin","gestor","coordenador"].includes((user?.cargo || "").toLowerCase()))
alongside canManageTeam/canViewBi/canEditConfig, and ensure the same permissions
identifier is what you pass in contextValue (permissions is already included) so
consumers of AuthContext receive the new flag.

---

Nitpick comments:
In `@backend/seed_permissions.js`:
- Around line 36-38: The current chained conditionals using cargoNome and
perm.chave are fragile; replace them with a declarative mapping (e.g., a plain
object like permissionRoleWhitelist or permissionExclusions) that maps each
permission key ("protocolar_caso", "gerenciar_equipe", "atender_caso") to the
array of roles that should be excluded or allowed, then update the loop that
references cargoNome and perm.chave to consult that mapping (use Array.includes
or a Set for lookup) instead of long OR-chains; update any variable names in
seed_permissions.js accordingly so the logic in the loop is a single lookup
against the mapping.

In `@backend/server.js`:
- Around line 47-52: Substitua a lista fixa allowedOrigins por uma leitura da
variável de ambiente ALLOWED_ORIGINS (por exemplo via
process.env.ALLOWED_ORIGINS), dividindo a string por vírgulas e fazendo
trim/filter para remover entradas vazias; mantenha a matriz atual como fallback
local caso a env não exista ou esteja vazia; update o uso de allowedOrigins
(onde quer que seja referenciada para configurar CORS) para usar essa matriz
resultante para que novos domínios possam ser adicionados sem precisar de
commit/redeploy.

In `@backend/src/controllers/lockController.js`:
- Line 16: Variável _isDefensorOrCoordenador está declarada mas não usada;
remova sua declaração para evitar confusão no controller. Abra o arquivo
contendo a função lockCaso e elimine a linha que cria _isDefensorOrCoordenador
(a expressão cargo.includes("defensor") || cargo === "coordenador"), garantindo
que referências a isAdmin e isServidorOrEstagiario permaneçam inalteradas; rode
os testes/linter para confirmar que nenhuma outra parte do módulo depende dessa
variável.

In `@backend/src/routes/config.js`:
- Around line 10-19: The middleware requireAdminOrGestor assumes req.user and
req.user.cargo exist and calls .toLowerCase(), which can throw if authMiddleware
is absent or regresses; update requireAdminOrGestor to defensively check for
req.user and req.user.cargo and return a 401 (or 403 per project convention)
JSON response when missing, then use the existing cargo value directly (remove
the redundant .toLowerCase() call since authMiddleware already normalizes it)
and keep the role check against ["admin","gestor"] before calling next();
reference requireAdminOrGestor and authMiddleware when making this change.

In `@backend/src/utils/configCache.js`:
- Line 29: Substitua as chamadas diretas a console (por exemplo a linha com
console.error("❌ Erro ao buscar configurações do sistema:", error) e a outra
ocorrência mencionada) pelo logger aplicacional importado de ../utils/logger.js;
importe logger se ainda não houver e use logger.error("Erro ao buscar
configurações do sistema", { error }) para preservar o objeto de erro e o
formato estruturado, e troque console.log/console.warn por
logger.info/logger.warn conforme o nível apropriado nas funções/constantes
relacionadas dentro de configCache.js.
- Around line 39-43: A invalidação atual em invalidarCache() só limpa a memória
do processo (variáveis _cache e _cacheExpiry), o que não propaga em ambientes
com múltiplas instâncias; atualize a implementação para propagar invalidações
entre instâncias: escolher e implementar uma estratégia (ex.: reduzir o TTL de
_cacheExpiry para 30–60s, ou publicar uma mensagem de invalidação via Redis
pub/sub ou Postgres LISTEN/NOTIFY, ou persistir/checar uma versão/updated_at no
armazenamento e revalidar quando mudar) e integrar essa lógica ao handler de PUT
/config para que, além de chamar invalidarCache(), publique a notificação ou
atualize a versão compartilhada; mantenha nomes existentes (invalidarCache,
_cache, _cacheExpiry) para localizar o código e ajuste onde o TTL de 5 min é
definido se optar por reduzir o tempo.

In `@frontend/src/areas/defensor/components/casos/ModalDistribuicao.jsx`:
- Around line 13-20: The effect in ModalDistribuicao uses authFetch and calls
setDefensores without cleanup, causing state updates after unmount and race
conditions; fix by creating an AbortController inside the useEffect, pass
controller.signal to authFetch (and down to fetch), and in the cleanup call
controller.abort(); also guard the then() that calls setDefensores to ignore
AbortError (or check controller.signal.aborted) so responses from previous
requests don’t overwrite newer state — update the useEffect, authFetch
call-site, and any fetch invocation to accept and forward a signal parameter.

In `@frontend/src/areas/defensor/hooks/useBiData.js`:
- Around line 107-115: Wrap the sessionStorage writes inside the useEffect hooks
that call sessionStorage.setItem for FILTROS_CACHE_KEY and DATA_CACHE_KEY in a
try/catch; catch QuotaExceededError (or any error) and silently skip or remove
stale cache (e.g., sessionStorage.removeItem(DATA_CACHE_KEY) or simply not
write) and log a warning so the render cycle doesn't break — update the
useEffect blocks around the DATA_CACHE_KEY and FILTROS_CACHE_KEY writes in
useBiData.js to perform the guarded write and handle failures gracefully.

In `@frontend/src/areas/defensor/pages/ConfiguracoesSistema.jsx`:
- Around line 49-53: O problema é mutação de estado em handleUpdateHorario
(spread raso + mutação do objeto) e uso de key={index} que causa
reaproveitamento incorreto; para corrigir, ao atualizar use uma cópia do item
alvo em vez de mutar a referência (por exemplo: setHorarios(h => h.map((it,i)=>
i===index ? {...it, [field]: value} : it)) no lugar de newHorarios[index][field]
= value), introduza uma chave estável única por item (adicionar um campo _key/id
ao criar um novo horário e usar key={item._key} na renderização) e ajuste
handleRemoveHorario para filtrar por esse id; por fim, remova/ignore _key antes
de serializar em handleSave (ou mantenha uma estrutura paralela só para keys)
para não enviar metadados extras.

In `@frontend/src/areas/defensor/pages/Dashboard.jsx`:
- Around line 197-241: The grid container currently renders even when all three
flags are false; update Dashboard.jsx to only render the container when at least
one permission is true by wrapping the div in a single conditional:
(permissions.canManageTeam || permissions.canViewBi ||
permissions.canEditConfig) && (<div className="grid ...">...</div>); this keeps
the existing inner Link blocks (guarded by permissions.canManageTeam, canViewBi,
canEditConfig) unchanged and prevents an empty grid from occupying layout space.

In `@frontend/src/areas/defensor/pages/DetalhesCaso.jsx`:
- Around line 2212-2217: Ensure ModalDistribuicao does not mutate or read/modify
the shared caso object when closed; confirm its implementation only uses caso
when isOpen is true (or clones it before modifying). Also replace the wrapped
onRefresh prop with the SWR mutate function directly by passing
onRefresh={mutate} to avoid creating a new arrow on every render, while keeping
onClose={() => setIsDistribuirOpen(false)} as the modal close handler; reference
ModalDistribuicao, props caso, isOpen, onClose and onRefresh/mutate.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

Comment on lines +86 to +90
```
lint-backend ──→ test-backend ──→ build-backend ──┐
lint-frontend ──→ test-frontend ──→ build-frontend ──┤──→ ✅ ci-success
security-audit ──────────────────────────────────────┘
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Adicione linguagem nos fenced code blocks para cumprir markdownlint (MD040).

Os blocos iniciados em Line 86 e Line 125 estão sem identificador de linguagem. Use text (ou yaml quando aplicável) para eliminar warning de lint.

♻️ Ajuste sugerido
-```
+```text
 lint-backend  ──→ test-backend  ──→ build-backend  ──┐
 lint-frontend ──→ test-frontend ──→ build-frontend ──┤──→ ✅ ci-success
 security-audit ──────────────────────────────────────┘

- +text
.github/
workflows/
ci.yml [NEW]
...
submissionValidation.test.js [NEW]

Also applies to: 125-159

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 86-86: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@arquivos/Conhecimento/04-Historico/feitos/2026-04-25_suite_testes_cicd.md`
around lines 86 - 90, Os blocos de código com três backticks no documento estão
sem identificador de linguagem — isso quebra a regra MD040; atualize os fence
blocks que contêm o diagrama ASCII e o bloco listado (os trechos que começam com
"lint-backend ..." e o bloco que contém ".github/ workflows/ ci.yml ..." /
"submissionValidation.test.js") para usar um language tag apropriado (por
exemplo troque ``` por ```text para diagramas ASCII e use ```yaml quando o
conteúdo for YAML), garantindo que todos os blocos de código possuam
identificador de linguagem.

- **Context:** Hook que processa exportação de gráficos do BI para PDF.
- **Action:** Armazenar o estado visual original (classe `hidden`) e preservá-lo.
- **Logic Details:**
- Em `exportarPdf()`, durante a busca dos botões que devem ser escondidos pro print (`hiddenControls`), salvar `node.dataset.wasHidden = node.classList.contains("hidden") ? "true" : "false"`.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Ajuste de redação para manter consistência formal

Em Line 52, prefira “para print” no lugar de “pro print” para manter o tom técnico/formal do documento.

🧰 Tools
🪛 LanguageTool

[style] ~52-~52: Em contextos mais formais, prefira “para”.
Context: ...sca dos botões que devem ser escondidos pro print (hiddenControls), salvar `node....

(FORMAL_PRA_PARA)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@arquivos/Conhecimento/04-Historico/feitos/task.md` at line 52, Edit the
wording in the task description that references exportarPdf and hiddenControls:
replace the informal phrase “pro print” with the formal “para print” so the
sentence reads that the code should save node.dataset.wasHidden =
node.classList.contains("hidden") ? "true" : "false" during the search for
buttons to hide for printing; update the phrase near the
exportarPdf/hiddenControls instruction accordingly.

Comment on lines +302 to +309
model configuracoes_sistema {
chave String @id @db.VarChar(100)
valor String
descricao String?
updated_at DateTime @default(now()) @updatedAt

@@map("configuracoes_sistema")
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistência de tipo em updated_at — falta @db.Timestamptz(6).

Todos os outros modelos com updated_at no schema (defensores, casos, casos_partes, casos_juridico, casos_ia) usam @db.Timestamptz(6). Sem essa anotação, o Prisma mapeia o campo para timestamp(3) sem timezone no PostgreSQL, gerando inconsistência de fuso horário com o restante das tabelas (especialmente relevante por o seed de bi_timezone/bi_horarios envolver lógica de horário).

♻️ Sugestão de correção
 model configuracoes_sistema {
   chave      String   `@id` `@db.VarChar`(100)
   valor      String
   descricao  String?
-  updated_at DateTime `@default`(now()) `@updatedAt`
+  updated_at DateTime `@default`(now()) `@updatedAt` `@db.Timestamptz`(6)
 
   @@map("configuracoes_sistema")
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
model configuracoes_sistema {
chave String @id @db.VarChar(100)
valor String
descricao String?
updated_at DateTime @default(now()) @updatedAt
@@map("configuracoes_sistema")
}
model configuracoes_sistema {
chave String `@id` `@db.VarChar`(100)
valor String
descricao String?
updated_at DateTime `@default`(now()) `@updatedAt` `@db.Timestamptz`(6)
@@map("configuracoes_sistema")
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/prisma/schema.prisma` around lines 302 - 309, No modelo
configuracoes_sistema, o campo updated_at está sem a anotação de fuso
(`@db.Timestamptz(6)`), causando inconsistência com os demais modelos; atualize
a definição do campo updated_at em configuracoes_sistema (o símbolo é updated_at
no model configuracoes_sistema) para manter `@default`(now()) `@updatedAt` e
adicionar `@db.Timestamptz`(6) para que o tipo Prisma mapeie para timestamptz(6)
no PostgreSQL.

Comment on lines +1037 to +1041
nome_mae_assistido:
caso.nome_mae_assistido || caso.partes?.nome_mae_assistido || caso.nome_mae_representante || "",
nome_pai_assistido:
caso.nome_pai_assistido || caso.partes?.nome_pai_assistido || caso.nome_pai_representante || "",
filiacao: `Mãe: ${caso.nome_mae_assistido || caso.partes?.nome_mae_assistido || caso.nome_mae_representante || "N/I"}, Pai: ${caso.nome_pai_assistido || caso.partes?.nome_pai_assistido || caso.nome_pai_representante || "N/I"}`,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Não derive a filiação do assistido a partir dos pais do representante.

Esses fallbacks exportam nome_mae_assistido/nome_pai_assistido com nome_mae_representante/nome_pai_representante. Em casos com menor representado pela mãe, isso transforma os avós maternos em filiação do assistido dentro do atendimento.json e do payload do SOLAR.

🩹 Direção de correção
-    nome_mae_assistido:
-      caso.nome_mae_assistido || caso.partes?.nome_mae_assistido || caso.nome_mae_representante || "",
-    nome_pai_assistido:
-      caso.nome_pai_assistido || caso.partes?.nome_pai_assistido || caso.nome_pai_representante || "",
-    filiacao: `Mãe: ${caso.nome_mae_assistido || caso.partes?.nome_mae_assistido || caso.nome_mae_representante || "N/I"}, Pai: ${caso.nome_pai_assistido || caso.partes?.nome_pai_assistido || caso.nome_pai_representante || "N/I"}`,
+    nome_mae_assistido:
+      caso.nome_mae_assistido || caso.partes?.nome_mae_assistido || "",
+    nome_pai_assistido:
+      caso.nome_pai_assistido || caso.partes?.nome_pai_assistido || "",
+    filiacao: `Mãe: ${caso.nome_mae_assistido || caso.partes?.nome_mae_assistido || "N/I"}, Pai: ${caso.nome_pai_assistido || caso.partes?.nome_pai_assistido || "N/I"}`,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/controllers/casosController.js` around lines 1037 - 1041, Os
campos atuais (nome_mae_assistido, nome_pai_assistido e filiacao) estão caindo
para nome_mae_representante/nome_pai_representante e assim podem atribuir os
nomes dos avós como filiação do assistido; ajuste a lógica para que
nome_mae_assistido e nome_pai_assistido nunca usem
nome_mae_representante/nome_pai_representante como fallback — apenas use
caso.nome_mae_assistido, caso.partes?.nome_mae_assistido e
caso.nome_pai_assistido, caso.partes?.nome_pai_assistido respectivamente, e para
filiacao monte a string a partir desses mesmos campos do assistido (usando "N/I"
quando ambos forem indefinidos) para evitar derivar dados dos representantes.

Comment on lines +3212 to +3266
export const distribuirCaso = async (req, res) => {
const { id } = req.params;
const { usuario_id } = req.body;
const userCargo = req.user.cargo.toLowerCase();

if (!usuario_id) {
return res.status(400).json({ error: "ID do usuário alvo é obrigatório." });
}

try {
// 1. Busca status atual via Supabase (garantindo dados mais recentes)
const { data: casoAtual, error: fetchError } = await supabase
.from('casos')
.select('status, unidade_id')
.eq('id', id)
.single();

if (fetchError || !casoAtual) {
return res.status(404).json({ error: "Caso não encontrado." });
}

// 2. Determina o campo alvo e o novo status com base no fluxo
let campoAlvo = null;
let novoStatus = null;
let statusPermitidos = [];

if (['pronto_para_analise', 'em_atendimento'].includes(casoAtual.status)) {
campoAlvo = 'servidor_id';
novoStatus = 'em_atendimento';
statusPermitidos = ['pronto_para_analise', 'em_atendimento'];
} else if (['liberado_para_protocolo', 'em_protocolo'].includes(casoAtual.status)) {
campoAlvo = 'defensor_id';
novoStatus = 'em_protocolo';
statusPermitidos = ['liberado_para_protocolo', 'em_protocolo'];
} else {
return res.status(409).json({
error: "Operação inválida",
message: `Não é possível distribuir um caso com status '${casoAtual.status}'.`
});
}

// 3. Executa a mutação atômica no Supabase
const updateData = {
[campoAlvo]: usuario_id,
[`${campoAlvo.split('_')[0]}_at`]: new Date().toISOString(),
status: novoStatus
};

const { data: casoAtualizado, error: updateError } = await supabase
.from('casos')
.update(updateData)
.eq('id', id)
.in('status', statusPermitidos) // Segurança da máquina de estados
.select()
.single();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

distribuirCaso aceita redistribuição sem validar nem o cargo chamador nem o usuário alvo.

Do jeito que está, qualquer perfil que passe por requireWriteAccess chega aqui e pode reatribuir o lock. Além disso, usuario_id é gravado sem checar unidade, cargo compatível com a etapa ou até existência/atividade do usuário, o que abre redistribuição indevida e quebra do isolamento por unidade.

🔒 Direção de correção
 export const distribuirCaso = async (req, res) => {
   const { id } = req.params;
   const { usuario_id } = req.body;
   const userCargo = req.user.cargo.toLowerCase();

+  if (!["admin", "gestor", "coordenador"].includes(userCargo)) {
+    return res.status(403).json({ error: "Seu cargo não pode distribuir casos." });
+  }
+
   if (!usuario_id) {
     return res.status(400).json({ error: "ID do usuário alvo é obrigatório." });
   }

   try {
@@
-    // 3. Executa a mutação atômica no Supabase
+    const usuarioAlvo = await prisma.defensores.findUnique({
+      where: { id: usuario_id },
+      select: { id: true, cargo: true, unidade_id: true, ativo: true },
+    });
+
+    if (!usuarioAlvo?.ativo) {
+      return res.status(400).json({ error: "Usuário alvo inválido ou inativo." });
+    }
+
+    if (
+      !["admin", "gestor"].includes(userCargo) &&
+      String(usuarioAlvo.unidade_id) !== String(casoAtual.unidade_id)
+    ) {
+      return res.status(403).json({ error: "Distribuição permitida apenas dentro da mesma unidade." });
+    }
+
+    const cargosPermitidos =
+      campoAlvo === "defensor_id"
+        ? ["defensor", "coordenador", "gestor", "admin"]
+        : ["servidor", "estagiario", "coordenador", "gestor", "admin"];
+
+    if (!cargosPermitidos.includes((usuarioAlvo.cargo || "").toLowerCase())) {
+      return res.status(400).json({ error: "Cargo incompatível com esta etapa." });
+    }
+
+    // 3. Executa a mutação atômica no Supabase
     const updateData = {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/controllers/casosController.js` around lines 3212 - 3266, The
distribuirCaso handler currently allows redistribution without validating the
caller's role or the target user; before performing the Supabase update, fetch
and validate the target user and the caller: (1) query the target user by
usuario_id and ensure they exist and are active; (2) verify the caller's cargo
(req.user.cargo) is authorized to perform this distribution for the chosen
campoAlvo (e.g., only servidores can be assigned to servidor_id, only defenders
to defensor_id) and that the target user's cargo matches the expected role; (3)
ensure unidade isolation by checking targetUser.unidade_id ===
casoAtual.unidade_id (or other permitted cross-unit rules) and reject with 403
if mismatched; only then build updateData and run the conditional atomic update
as before (preserve the statusPermitidos logic and the .in('status',
statusPermitidos) guard); return clear 4xx responses for missing/invalid
usuario_id, non-existent or inactive user, cargo mismatch, or unidade mismatch.

{d.nome}
</p>
<p className="text-xs text-gray-500">
{d.cargo?.nome || "Profissional"} • {d.unidade?.nome || "Sem Unidade"}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirma o shape retornado por GET /defensores
rg -nP -C3 'cargo\s*:\s*d\.cargo' backend/src/controllers/defensoresController.js
rg -nP -C3 'unidade_nome' backend/src/controllers/defensoresController.js

Repository: CJVW-Code/maes_acao

Length of output: 665


🏁 Script executed:

# Encontrar e inspecionar ModalDistribuicao.jsx
fd -t f "ModalDistribuicao.jsx" -x cat -n {}

Repository: CJVW-Code/maes_acao

Length of output: 6065


🏁 Script executed:

# Procurar por chamadas ao endpoint /defensores no frontend
rg -i "defensores" frontend/src/areas/defensor/components/casos/ -A 2 -B 2

Repository: CJVW-Code/maes_acao

Length of output: 2553


🏁 Script executed:

# Verificar se há outras referências a cargo?.nome no frontend
rg "cargo\?" frontend/src/areas/defensor/ -A 1 -B 1

Repository: CJVW-Code/maes_acao

Length of output: 738


Corrigir acesso às propriedades de cargo e unidade que o backend retorna como strings.

O endpoint GET /defensores mapeia cargo: d.cargo.nome (linha 168 do backend), retornando cargo como string, não como objeto. Da mesma forma, retorna unidade_nome (linha 169), não unidade. Logo, d.cargo?.nome e d.unidade?.nome sempre resultam em undefined, forçando os fallbacks.

Sugestão de correção
-                      <p className="text-xs text-gray-500">
-                        {d.cargo?.nome || "Profissional"} • {d.unidade?.nome || "Sem Unidade"}
-                      </p>
+                      <p className="text-xs text-gray-500">
+                        {(typeof d.cargo === "string" ? d.cargo : d.cargo?.nome) || "Profissional"} • {d.unidade_nome || d.unidade?.nome || "Sem Unidade"}
+                      </p>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/areas/defensor/components/casos/ModalDistribuicao.jsx` at line
108, The component is accessing d.cargo?.nome and d.unidade?.nome but the
backend returns cargo as a string and the unit name as unidade_nome, so those
expressions are always undefined; update the render to use the string fields
directly (use d.cargo as the job label and d.unidade_nome as the unit label) and
keep the existing fallbacks (e.g., d.cargo || "Profissional" and d.unidade_nome
|| "Sem Unidade") in the ModalDistribuicao.jsx output.

Comment on lines 106 to +112
const marcarNotificacaoLida = async (id) => {
try {
await authFetch(`/casos/notificacoes/${id}/lida`, {
method: "PATCH",
});
await authFetch(`/casos/notificacoes/${id}/lida`, { method: "PATCH" });
setNotificacoes((prev) =>
prev.map((n) => (n.id === id ? { ...n, lida: true } : n)),
);
} catch (error) {
if (error.message !== "Sessão expirada") {
console.error("Erro ao marcar como lida", error);
}
}
} catch (error) {}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Esse catch vazio continua quebrando o lint da pipeline.

Além do bloco vazio, o parâmetro error também fica sem uso. Hoje isso já aparece como falha no CI.

🧹 Ajuste mínimo
-    } catch (error) {}
+    } catch {
+      // noop: falha ao marcar notificação como lida não deve quebrar a sessão
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const marcarNotificacaoLida = async (id) => {
try {
await authFetch(`/casos/notificacoes/${id}/lida`, {
method: "PATCH",
});
await authFetch(`/casos/notificacoes/${id}/lida`, { method: "PATCH" });
setNotificacoes((prev) =>
prev.map((n) => (n.id === id ? { ...n, lida: true } : n)),
);
} catch (error) {
if (error.message !== "Sessão expirada") {
console.error("Erro ao marcar como lida", error);
}
}
} catch (error) {}
const marcarNotificacaoLida = async (id) => {
try {
await authFetch(`/casos/notificacoes/${id}/lida`, { method: "PATCH" });
setNotificacoes((prev) =>
prev.map((n) => (n.id === id ? { ...n, lida: true } : n)),
);
} catch {
// noop: falha ao marcar notificação como lida não deve quebrar a sessão
}
🧰 Tools
🪛 GitHub Check: Lint · Frontend

[failure] 112-112:
Empty block statement


[failure] 112-112:
'error' is defined but never used

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/areas/defensor/contexts/AuthContext.jsx` around lines 106 - 112,
The empty catch in marcarNotificacaoLida is causing lint failures and the error
parameter is unused; update the catch to handle the error (e.g., log it with
console.error or forward to your app logger and/or show a user-facing
notification) and use the caught variable so it is not unused, while preserving
existing behavior of not updating state on failure; locate the function
marcarNotificacaoLida in AuthContext.jsx and replace the empty catch block with
a minimal error handler that logs the error and optionally triggers any existing
toast/notification helper.

Comment on lines +55 to +76
const handleLiberarAgora = async () => {
const umaHoraDepois = new Date(Date.now() + 60 * 60 * 1000).toISOString();
setSaving(true);
try {
const response = await authFetch("/config", {
method: "PUT",
body: JSON.stringify({
configs: {
bi_liberado_ate: umaHoraDepois
}
}),
});

if (response.ok) {
toast.success("BI liberado por 1 hora!");
}
} catch {
toast.error("Erro ao liberar acesso.");
} finally {
setSaving(false);
}
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Falha silenciosa quando o backend rejeita a liberação.

handleLiberarAgora só emite toast no caminho response.ok. Se o backend retornar 4xx/5xx (ex.: usuário sem permissão para alterar bi_liberado_ate, validação de payload, conflito), o usuário recebe nenhum feedback e fica imaginando que a liberação ocorreu. Diferente de handleSave, aqui não há else lendo a mensagem de erro.

🛡️ Sugestão de correção
       if (response.ok) {
         toast.success("BI liberado por 1 hora!");
+      } else {
+        const err = await response.json().catch(() => ({}));
+        toast.error(err.message || "Erro ao liberar acesso.");
       }
     } catch {
       toast.error("Erro de conexão.");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/areas/defensor/pages/ConfiguracoesSistema.jsx` around lines 55 -
76, handleLiberarAgora currently only shows success when response.ok and stays
silent on 4xx/5xx; add an else branch after the response.ok check that reads the
error payload (await response.json() or response.text() as appropriate) and
calls toast.error with a clear message including either the returned
error.message or response.status/statusText; also handle JSON parse failures
gracefully (fallback to statusText) so users get feedback when the backend
rejects the request.

Comment on lines +1994 to +2002
{permissions.canDistribuir && (
<button
onClick={() => setIsDistribuirOpen(true)}
className="btn btn-primary w-full py-3 mb-4 flex items-center justify-center gap-2"
>
<UserPlus size={18} />
Distribuir Caso
</button>
)}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirma que canDistribuir não está definido em nenhum lugar do frontend
rg -n --type=js --type=jsx 'canDistribuir' frontend/src

Repository: CJVW-Code/maes_acao

Length of output: 91


🏁 Script executed:

#!/bin/bash
# Search for canDistribuir in frontend code (corrected for rg)
rg -n 'canDistribuir' frontend/src --type=js

Repository: CJVW-Code/maes_acao

Length of output: 271


🏁 Script executed:

#!/bin/bash
# Also search without file type restriction
rg -n 'canDistribuir' frontend/src

Repository: CJVW-Code/maes_acao

Length of output: 271


🏁 Script executed:

#!/bin/bash
# Find and examine AuthContext.jsx
find frontend/src -name 'AuthContext.jsx' -o -name 'AuthContext.js' | head -5

Repository: CJVW-Code/maes_acao

Length of output: 116


🏁 Script executed:

#!/bin/bash
# Look for permissions object definition in context files
rg -n 'permissions\s*=' frontend/src -A 5 --type=js | head -50

Repository: CJVW-Code/maes_acao

Length of output: 680


🏁 Script executed:

#!/bin/bash
# Examine DetalhesCaso.jsx to confirm line 1994 context
find frontend/src -name 'DetalhesCaso.jsx' -o -name 'DetalhesCaso.js'

Repository: CJVW-Code/maes_acao

Length of output: 114


🏁 Script executed:

#!/bin/bash
# Read DetalhesCaso.jsx around line 1994 to see full context
sed -n '1990,2005p' frontend/src/areas/defensor/pages/DetalhesCaso.jsx

Repository: CJVW-Code/maes_acao

Length of output: 545


🏁 Script executed:

#!/bin/bash
# Check if ModalDistribuicao is imported and used in DetalhesCaso.jsx
rg -n 'ModalDistribuicao' frontend/src/areas/defensor/pages/DetalhesCaso.jsx

Repository: CJVW-Code/maes_acao

Length of output: 171


🏁 Script executed:

#!/bin/bash
# Search for any definition of canDistribuir in the entire frontend
rg -n 'canDistribuir\s*:' frontend/src

Repository: CJVW-Code/maes_acao

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Check if canDistribuir might be added dynamically elsewhere
rg -n 'permissions\[.*canDistribuir' frontend/src

Repository: CJVW-Code/maes_acao

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Check if canDistribuir is defined in the backend (permissions configuration)
find backend -name '*.js' | xargs rg -l 'canDistribuir' 2>/dev/null | head -10

Repository: CJVW-Code/maes_acao

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Check Casos.jsx at line 259 to verify same issue there
sed -n '255,270p' frontend/src/areas/defensor/pages/Casos.jsx

Repository: CJVW-Code/maes_acao

Length of output: 905


🏁 Script executed:

#!/bin/bash
# Check if there's any middleware or provider that might add permissions dynamically
rg -n 'permissions\.' frontend/src/areas/defensor/contexts/AuthContext.jsx -A 2 -B 2

Repository: CJVW-Code/maes_acao

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Verify if there are any other references to definePermission or dynamic permission setting
rg -n 'permissions\s*\[|permissions\s*=' frontend/src/areas/defensor/contexts/AuthContext.jsx -B 5 -A 10

Repository: CJVW-Code/maes_acao

Length of output: 592


🏁 Script executed:

#!/bin/bash
# Check the full AuthContext.jsx file to ensure we're not missing anything
cat -n frontend/src/areas/defensor/contexts/AuthContext.jsx | sed -n '100,130p'

Repository: CJVW-Code/maes_acao

Length of output: 1078


Botão "Distribuir Caso" nunca renderiza — permissions.canDistribuir não existe.

O objeto permissions em frontend/src/areas/defensor/contexts/AuthContext.jsx (linhas 115–119) contém apenas canManageTeam, canViewBi e canEditConfig. A propriedade canDistribuir não está definida, portanto permissions.canDistribuir é sempre undefined (falsy), tornando o gate inoperante em duas páginas: DetalhesCaso.jsx:1994 e Casos.jsx:259. O botão nunca renderiza, tornando toda a integração com ModalDistribuicao código morto.

Adicione a permissão ao AuthContext:

Correção sugerida

Em frontend/src/areas/defensor/contexts/AuthContext.jsx, adicione a propriedade ao objeto permissions:

  const permissions = {
    canManageTeam: (user?.cargo || "").toLowerCase() === "admin",
    canViewBi: ["admin", "gestor", "coordenador"].includes((user?.cargo || "").toLowerCase()),
    canEditConfig: ["admin", "gestor"].includes((user?.cargo || "").toLowerCase()),
+   canDistribuir: ["admin", "gestor", "coordenador"].includes((user?.cargo || "").toLowerCase()),
  };

Ajuste a lista de cargos conforme a regra de negócio real para distribuição.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/areas/defensor/pages/DetalhesCaso.jsx` around lines 1994 - 2002,
The permissions object in AuthContext is missing the canDistribuir flag, so
checks like permissions.canDistribuir in the DetalhesCaso and Casos components
never pass; update the permissions object inside the AuthContext provider (the
permissions returned/constructed in AuthContext.jsx) to include a canDistribuir
boolean (computed from the user/roles the same way
canManageTeam/canViewBi/canEditConfig are computed), ensure the logic uses the
correct roles list per business rules, and then verify ModalDistribuicao-related
buttons in DetalhesCaso and Casos now render when canDistribuir is true.

Comment on lines +139 to +168
if (data?.bloqueadoPorHorario) {
return (
<div className="flex flex-col items-center justify-center py-24 px-6 text-center animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="w-24 h-24 bg-amber-100 rounded-full flex items-center justify-center text-amber-600 mb-6 shadow-inner">
<Clock size={48} />
</div>
<h2 className="heading-1 text-amber-900 mb-2">Acesso Temporariamente Restrito</h2>
<p className="text-amber-800 max-w-md mx-auto leading-relaxed">
O módulo de BI está configurado para acesso apenas em horários específicos.
Por favor, tente novamente durante o horário de expediente definido nas configurações do sistema.
</p>
</div>
);
}

if (!permissions?.canViewBi) {
return <Navigate to="/painel" replace />;
}

const updateFiltro = (key, value) => {
setFiltros((current) => ({ ...current, [key]: value }));
};

// Auto-filtro: Se ja tem dados, atualiza ao mudar filtros importantes
useEffect(() => {
if (data && !loading) {
gerar().catch(() => {});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filtros.periodo, filtros.dataInicio, filtros.dataFim, filtros.unidade_id, filtros.topN]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Violação das regras dos Hooks — useEffect chamado após return antecipado.

Os blocos return nas linhas 139–152 (data?.bloqueadoPorHorario) e 154–156 (!permissions?.canViewBi) executam antes do useEffect da linha 163. A cada render, o número de hooks invocados pode variar (zero quando há retorno antecipado, vs. um quando o componente segue o fluxo), o que quebra o invariante do React e causa erros do tipo "Rendered fewer hooks than expected" assim que o usuário transita entre os estados (ex.: antes/depois de receber data, ou ao mudar de cargo). O lint do CI já está falhando neste ponto.

Mova o useEffect para antes de qualquer return condicional.

🐛 Correção sugerida
   const kpiCards = useMemo(() => {
     ...
   }, [data]);

+  // Auto-filtro: Se ja tem dados, atualiza ao mudar filtros importantes
+  useEffect(() => {
+    if (data && !loading) {
+      gerar().catch(() => {});
+    }
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [filtros.periodo, filtros.dataInicio, filtros.dataFim, filtros.unidade_id, filtros.topN]);
+
   if (data?.bloqueadoPorHorario) {
     return ( ... );
   }

   if (!permissions?.canViewBi) {
     return <Navigate to="/painel" replace />;
   }

   const updateFiltro = (key, value) => {
     setFiltros((current) => ({ ...current, [key]: value }));
   };

-  // Auto-filtro: Se ja tem dados, atualiza ao mudar filtros importantes
-  useEffect(() => {
-    if (data && !loading) {
-      gerar().catch(() => {});
-    }
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [filtros.periodo, filtros.dataInicio, filtros.dataFim, filtros.unidade_id, filtros.topN]);
-
   return (
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (data?.bloqueadoPorHorario) {
return (
<div className="flex flex-col items-center justify-center py-24 px-6 text-center animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="w-24 h-24 bg-amber-100 rounded-full flex items-center justify-center text-amber-600 mb-6 shadow-inner">
<Clock size={48} />
</div>
<h2 className="heading-1 text-amber-900 mb-2">Acesso Temporariamente Restrito</h2>
<p className="text-amber-800 max-w-md mx-auto leading-relaxed">
O módulo de BI está configurado para acesso apenas em horários específicos.
Por favor, tente novamente durante o horário de expediente definido nas configurações do sistema.
</p>
</div>
);
}
if (!permissions?.canViewBi) {
return <Navigate to="/painel" replace />;
}
const updateFiltro = (key, value) => {
setFiltros((current) => ({ ...current, [key]: value }));
};
// Auto-filtro: Se ja tem dados, atualiza ao mudar filtros importantes
useEffect(() => {
if (data && !loading) {
gerar().catch(() => {});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filtros.periodo, filtros.dataInicio, filtros.dataFim, filtros.unidade_id, filtros.topN]);
// Auto-filtro: Se ja tem dados, atualiza ao mudar filtros importantes
useEffect(() => {
if (data && !loading) {
gerar().catch(() => {});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filtros.periodo, filtros.dataInicio, filtros.dataFim, filtros.unidade_id, filtros.topN]);
if (data?.bloqueadoPorHorario) {
return (
<div className="flex flex-col items-center justify-center py-24 px-6 text-center animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="w-24 h-24 bg-amber-100 rounded-full flex items-center justify-center text-amber-600 mb-6 shadow-inner">
<Clock size={48} />
</div>
<h2 className="heading-1 text-amber-900 mb-2">Acesso Temporariamente Restrito</h2>
<p className="text-amber-800 max-w-md mx-auto leading-relaxed">
O módulo de BI está configurado para acesso apenas em horários específicos.
Por favor, tente novamente durante o horário de expediente definido nas configurações do sistema.
</p>
</div>
);
}
if (!permissions?.canViewBi) {
return <Navigate to="/painel" replace />;
}
const updateFiltro = (key, value) => {
setFiltros((current) => ({ ...current, [key]: value }));
};
🧰 Tools
🪛 GitHub Check: Lint · Frontend

[failure] 163-163:
React Hook "useEffect" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/areas/defensor/pages/Relatorios.jsx` around lines 139 - 168, The
useEffect that auto-triggers gerar based on filtros is declared after early
returns, which violates React hooks order when data or permissions cause an
early return; move the useEffect block (the one referencing filtros.periodo,
filtros.dataInicio, filtros.dataFim, filtros.unidade_id, filtros.topN and
calling gerar().catch(() => {})) to be declared before any conditional returns
(i.e., above the checks for data?.bloqueadoPorHorario and
!permissions?.canViewBi) so hooks run in a stable order; keep the same
dependency array and handler logic and remove or adjust the eslint-disable
comment if needed.

@CJVW-Code CJVW-Code closed this Apr 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants