Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughImplementa infraestrutura completa de CI/CD (GitHub Actions), hardening de segurança com novo middleware Changes
Sequence DiagramssequenceDiagram
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes A complexidade resulta de múltiplos fatores heterogêneos: novo middleware Possibly related PRs
Poem
✨ Finishing Touches📝 Generate docstrings
|
There was a problem hiding this comment.
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 | 🟡 MinorRemover função não utilizada.
A função
_valueOrPlaceholdernã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 | 🟠 MajorVariável computada e nunca usada — remova ou complete a checagem.
Renomear
isDefensorOrCoordenadorpara_isDefensorOrCoordenadorapenas 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, impedirdefensor/coordenadorde pegar lock de nível 1, espelhando o bloqueio deservidor/estagiarioem nível 2). Hoje, somenteisServidorOrEstagiarioé checado na linha 32;defensorecoordenadorpodem 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 | 🟡 MinorTexto do cabeçalho desatualizado —
recepcionistasfoi removido do RBAC.Este PR removeu o cargo
recepcaoda matriz RBAC (ARCHITECTURE.md,BUSINESS_RULES.md) e introduziuservidor/coordenadorno 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 | 🟡 MinorFallback
"operador"é inválido — alinhe com o novo default"servidor".O PR padroniza cargos (
admin,coordenador,defensor,servidor,estagiario,visualizador) e até troca o default deregistrarDefensor(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 eminit.sqlnem emseed_permissions.cjs. Um usuário semcargopopulado receberá um token com cargo desconhecido e será automaticamente bloqueado porrequireWriteAccess(allowedRoles não inclui"operador"), além de gerar inconsistência com oauth.jsque tem o mesmo fallback. Recomendo unificar o fallback ou, melhor, recusar o login quando ocargofor 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
LogtaileLogtailTransport. 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:
- Remover variáveis e funções não utilizadas
- 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:
- Identificar especificamente quais variáveis estão não utilizadas
- Remover essas variáveis ao invés de suprimir a regra
- 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
isSupabaseConfigurednão é mais usado neste controller, remova-o (e a diretivaeslint-disable no-unused-varsda 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.
isSupabaseConfigurednã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 doeslint-disableexcessivo.Desabilitar
no-unused-varsno arquivo inteiro só para silenciareemcatch (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 fazString(tipoAcao || "").trim()— comonullé falsy, o resultado é"Não informado". A asserçãotypeof result === "string"passa, mas o comentário induz ao erro e a asserção deixa a regressão escapar. Vale fixarexpect(formatTipoAcaoLabel(null)).toBe("Não informado").- Linhas 68–73: o lookup com acento é determinístico (
"execução_alimentos"→ normaliza paraexecucao_alimentos→ resolve emknownLabels). Pode-se afirmar diretamenteexpect(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
MD040reportado pelo linter. Usetext(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, () => { ... })emexpress-rate-limitv7 é assíncrono; quando oif (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 denext()na primeira requisição), mas o ramoblocked: 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.statusou estender orespara resolver a Promise dentro deres.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 explicitamentetest-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 viaresult != "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 deneeds(p.ex., desacoplar build de test). Considere incluirtest-backend,test-frontend,lint-backend,lint-frontendemneedse 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:--forceExitpode mascarar handles vazando.
--forceExitmata 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--detectOpenHandleslocalmente 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 emtestMatch.
"**/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,parseBrDateToIsoenormalizeDecimalForSubmitsão re-implementados aqui em vez de importados de@/utils/formatters.js. Isso significa que:
- Se
formatters.jsintroduzir 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.jsjá cobre essas funções unitariamente, basta importá-las diretamente emmakeMocks()para queprocessSubmissionseja 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ânciasO plano de
configCachefunciona por processo Node. Em escala horizontal, a invalidação apósPUTnã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
⛔ Files ignored due to path filters (2)
backend/uploads/peticoes/T1/peticao_inicial_T1.docxis excluded by!**/*.docxfrontend/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (49)
.github/workflows/ci.ymlarquivos/Conhecimento/01-Referencia/ARCHITECTURE.mdarquivos/Conhecimento/01-Referencia/BUSINESS_RULES.mdarquivos/Conhecimento/04-Historico/2026-04-25_suite_testes_cicd.mdarquivos/Conhecimento/04-Historico/GUIA_TESTES.mdarquivos/Conhecimento/04-Historico/feitos/2026-04-25_otimizacao_bi_rbac.mdarquivos/Conhecimento/04-Historico/feitos/implementation_plan.mdarquivos/Conhecimento/04-Historico/implementation_planfinal.mdarquivos/Conhecimento/04-Historico/taskfinalizacao.mdbackend/jest.config.jsbackend/new_output.txtbackend/server.jsbackend/src/controllers/biController.jsbackend/src/controllers/casosController.jsbackend/src/controllers/defensoresController.jsbackend/src/controllers/lockController.jsbackend/src/controllers/scannerController.jsbackend/src/controllers/statusController.jsbackend/src/middleware/upload.jsbackend/src/services/geminiService.jsbackend/src/services/securityService.jsbackend/src/utils/logger.jsbackend/test_output.txtbackend/tests/dicionarioAcoes.test.jsbackend/tests/documentGenerationService.test.jsbackend/tests/integration/casosRoutes.test.jsbackend/tests/integration/health.test.jsbackend/tests/integration/lockRoutes.test.jsbackend/tests/integration/scannerRoutes.test.jsbackend/tests/middleware/apiKey.test.jsbackend/tests/middleware/auth.test.jsbackend/tests/middleware/requireWriteAccess.test.jsbackend/tests/security/injection.test.jsbackend/tests/security/rateLimiter.test.jsbackend/tests/setup.jsbackend/tests/unit/aiService.test.jsbackend/tests/unit/securityService.test.jsfrontend/package.jsonfrontend/src/__tests__/apiBase.test.jsfrontend/src/__tests__/caseUtils.test.jsfrontend/src/__tests__/formatters.test.jsfrontend/src/__tests__/submissionValidation.test.jsfrontend/src/areas/defensor/contexts/AuthContext.jsxfrontend/src/areas/defensor/pages/Cadastro.jsxfrontend/src/areas/defensor/pages/GerenciarEquipe.jsxfrontend/src/areas/servidor/components/StepDadosPessoais.jsxfrontend/src/areas/servidor/components/secoes/SecaoProcessoOriginal.jsxfrontend/src/areas/servidor/services/submissionService.jsfrontend/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
|
|
||
| ### 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/`. |
There was a problem hiding this comment.
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.
| - `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/ |
There was a problem hiding this comment.
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/`.
| ### 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. |
There was a problem hiding this comment.
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:
- Resolver o problema do
BigIntno mock (ex.:JSON.stringifycom replacer, ou retornarid: 1comoNumberno mock já que o controlador converte comBigInt(id)); e - 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.
| - 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` |
There was a problem hiding this comment.
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.
| 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"); | ||
| }); |
There was a problem hiding this comment.
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.
| 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; | ||
| }); |
There was a problem hiding this comment.
Restauração de NODE_ENV é frágil e propaga estado entre testes.
Dois problemas:
- Se
originalEnvforundefined(caso comum em ambiente de teste local), a linhaprocess.env.NODE_ENV = originalEnvdefine a string literal"undefined"em vez de remover a variável —process.envconverte qualquer valor atribuído em string. Isso pode contaminar testes subsequentes que dependem deprocess.env.NODE_ENV !== "production". - Se o
request(...)lançar antes da linha 137, oNODE_ENVpermanece como"production"para o restante da suíte. Usetry/finallyou 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.
| 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.
| 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"); | ||
| } | ||
| }); |
There was a problem hiding this comment.
🛠️ 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:
- 1: https://www.npmjs.com/package/express-rate-limit
- 2: https://www.npmjs.com/package/express-rate-limit/v/7.1.1
- 3: https://express-rate-limit.mintlify.app/reference/configuration
- 4: https://www.jsdocs.io/package/express-rate-limit
- 5: https://express-rate-limit.mintlify.app/reference/request-api
🏁 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.jsRepository: 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.jsRepository: CJVW-Code/maes_acao
Length of output: 4370
🏁 Script executed:
# Now examine the rateLimiter implementation
cat -n backend/src/middleware/rateLimiter.jsRepository: 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.jse importá-las no teste para verificação direta. - Usar o
runLimiterexistente (linhas 32-59) para excedermaxe validar queres.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.
There was a problem hiding this comment.
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 | 🟡 MinorSalta 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 | 🟡 MinorNumeração duplicada: dois subtítulos
### 3.6consecutivos.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 | 🟠 MajorA definição de power user ficou inconsistente entre os fluxos.
Aqui o bypass ficou restrito a
admin/gestor, maslockControllerjá tratacoordenadorcomo cargo privilegiado, ecarregarCasoDetalhado()ainda continua admin-only. O efeito prático é RBAC diferente por endpoint: coordenador pode liberar caso, mas segue tomando423/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: Eviteeslint-disableno escopo do arquivo inteiro.
/* eslint-disable no-unused-vars */desliga a regra para o módulo todo, escondendo problemas futuros. SeisSupabaseConfiguredé 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, masBigInt(id)ainda pode lançar para strings extremamente longas (acima do limite prático). Como a chamada está dentro dotry/catch(linha 28), o erro vai cair nocatche retornar 500 — quando o ideal seria 400. Fluxo atual é seguro; só vale considerar tratar o erro deBigIntcomo 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: Campostatusselecionado mas não utilizado no middleware.O
selecttrazstatus, porém nenhuma lógica do middleware o consome. Se o objetivo é hidratarreq.casopara 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 ostatusdaqui 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 paravisualizadorestá coerente.A normalização para minúsculas alinha com os middlewares
requireWriteAccesserequireSameUnit, que comparamcargoem lowercase. Vale notar queloginDefensorembackend/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 peloauthMiddleware.🤖 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:adminBypassausente no retorno de erro.O JSDoc declara
{ ok: boolean, adminBypass: boolean, reason?: string }, mas o caminho de erro (linhas 30-33) omiteadminBypass, deixando-oundefinedpara 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 emcasosRoutes.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
adminegestor, mas a suíte só testaadmin. Vale incluir caso degestor(e, de quebra, validar quefindUniquenã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
⛔ Files ignored due to path filters (1)
backend/uploads/peticoes/T1/peticao_inicial_T1.docxis excluded by!**/*.docx
📒 Files selected for processing (34)
.github/workflows/ci.ymlarquivos/Conhecimento/01-Referencia/ARCHITECTURE.mdarquivos/Conhecimento/01-Referencia/BUSINESS_RULES.mdarquivos/Conhecimento/01-Referencia/routes.mdarquivos/Conhecimento/03-Guias/GUIA_TESTES.mdarquivos/Conhecimento/04-Historico/2026-04-26_hardening_backend_seguranca.mdarquivos/Conhecimento/README.mdbackend/new_test_output.txtbackend/rateLimiter_output.txtbackend/server.jsbackend/src/controllers/casosController.jsbackend/src/controllers/defensoresController.jsbackend/src/controllers/lockController.jsbackend/src/middleware/auth.jsbackend/src/middleware/requireSameUnit.jsbackend/src/middleware/requireWriteAccess.jsbackend/src/routes/casos.jsbackend/src/utils/helpers.jsbackend/src/utils/stateMachine.jsbackend/test_output.txtbackend/tests/integration/casosRoutes.test.jsbackend/tests/integration/lockRoutes.test.jsbackend/tests/middleware/auth.test.jsbackend/tests/middleware/requireSameUnit.test.jsbackend/tests/security/rateLimiter.test.jsbackend/tests/unit/aiService.test.jsbackend/tests/unit/utils.test.jsclaude.mdfrontend/src/__tests__/submissionValidation.test.jsfrontend/src/areas/defensor/components/layout/Sidebar.jsxfrontend/src/areas/defensor/pages/Cadastro.jsxfrontend/src/areas/defensor/pages/Casos.jsxfrontend/src/areas/defensor/pages/Dashboard.jsxfrontend/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
| - 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" |
There was a problem hiding this comment.
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.** |
There was a problem hiding this comment.
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.
| ### 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`). |
There was a problem hiding this comment.
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.
| // --- 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 }; |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
| } 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.
| 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 }); | ||
| } | ||
| }); |
There was a problem hiding this comment.
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.
| const [unidadeFiltro, setUnidadeFiltro] = useState("todas"); | ||
| const { token, user } = useAuth(); | ||
|
|
||
| const isAdminOrGestor = user && ["admin", "gestor"].includes(user.cargo.toLowerCase()); |
There was a problem hiding this comment.
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.
| 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.
| const isJuridico = ["defensor", "coordenador", "gestor"].includes(user?.cargo?.toLowerCase()); | ||
| const prefixo = isJuridico ? "Dr(a). " : ""; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 | 🟠 MajorNão versionar artefatos gerados de cobertura.
Este arquivo (e todo o diretório
frontend/coverage/) é saída gerada automaticamente peloistanbul-reportsquando 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-disableno 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 | 🟡 MinorMensagem de erro inconsistente com a lógica de autorização.
canExportBiLoteagora permiteadminEgestor(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 dorequireBiAccess(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 | 🟠 MajorMesmo problema do
sorter.js: arquivo gerado e ainda quebrando o CI.Este
block-navigation.jsé literalmente o asset distribuído peloistanbul-reports(mesma IIFEjumpToCode, mesmos atalhosn/j/b/k/p). Ele só existe aqui porque o diretóriocoverage/foi comitado — veja o comentário emfrontend/coverage/sorter.jspara 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
.eslintrclegado, criar/atualizarfrontend/.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 | 🟡 MinorRemova o parâmetro não usado do
catch.Esse
econtinua 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 | 🟠 MajorO 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,canViewBiecanEditConfig. Qualquer tela que leiapermissions.canDistribuirvai receberundefinede esconder a ação até paraadmin/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 | 🟡 MinorNível 2 ainda contradiz a própria matriz RBAC.
Aqui
gestorsegue fora do Nível 2, mas na seção 9 ele continua comProtocolo/Finalizar = ✅. A documentação fica ambígua sobre quem pode adquirirdefensor_ide 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 = undefinedcontinua sendo um anti-padrão.Atribuir
undefinedà propriedade do mock deixa de ser umajest.fn(), fazendo qualquer chamada do controller aprisma.casos.findFirst(...)lançarTypeError: ... is not a function— o que produziria um 500, contradizendo oexpect(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. PrefiramockPrismaCasos.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 | 🟡 MinorAsserção
[401, 403]ainda mascara violações de RBAC.Para um JWT válido com
visualizadorativo,requireWriteAccess(e/ourequireSameUnit) 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. Recomendoexpect(res.status).toBe(403)e investigar a configuração do mock deprisma.defensores.findUniquese 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 | 🟠 MajorAsserçõ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 deprotocolona 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 paraexpect(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 | 🟡 MinorAsserção frouxa para "usuário inativo".
O título diz
retorna 403 quando usuário está inativo, mas oexpect([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 emexpect(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 | 🟡 MinorMensagem 423 ainda menciona apenas "Administrador".
A regra de unlock foi expandida para
admin,gestorecoordenador, 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 montarallowedOriginsa 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 comcasoantes da distribuição.
ModalDistribuicaoé montado sempre (mesmo quandoisOpené falso oearly returninterno previne o render do conteúdo, então isso é OK). OonRefresh={() => mutate()}e oonCloseestão corretamente fechando o modal e revalidando o SWR. Considerar passarmutatediretamente 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 areq.user.cargo.A linha 11 assume que
req.userereq.user.cargosempre existem. Embora oauthMiddleware(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çarTypeError: Cannot read properties of undefinedem vez de retornar 401/403 limpo. Comocargojá vem em lowercase doauthMiddleware, 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 componentesTeamRoute,BiRouteeGestorRouteemfrontend/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 contraQuotaExceededError.
sessionStorage.setItem(DATA_CACHE_KEY, JSON.stringify(data))é executado a cada mudança dedata. Para relatórios BI volumosos (muitas linhas/widgets), o JSON serializado pode estourar a cota do sessionStorage (~5MB), lançandoQuotaExceededErrore 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_isDefensorOrCoordenadordeclarada 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 delockCaso(apenasisAdmineisServidorOrEstagiariosã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 ologgeraplicacional em vez deconsole.O restante do backend usa
logger(../utils/logger.js) com níveis estruturados; misturarconsole.error/console.logaqui 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ósPUT /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 nouseEffect— possível "set state on unmounted" e race condition.Se o usuário fechar o modal antes da resposta de
/defensoreschegar (ou abrir/fechar várias vezes em sequência), osetDefensores(data)será chamado em um componente já desmontado e a resposta de uma abertura anterior pode sobrescrever a atual. Use umAbortController(encaminhado aoauthFetch/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_equipelista 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:
handleUpdateHorariofaz 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 emhorarios[index]também é mutado, o que viola a imutabilidade do estado e pode esconder bugs comReact.memo/comparações.- O
key={index}na renderização (linha 169), combinado comhandleRemoveHorario(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
_keyantes de serializar emJSON.stringify(horarios)nohandleSave, 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
| ``` | ||
| lint-backend ──→ test-backend ──→ build-backend ──┐ | ||
| lint-frontend ──→ test-frontend ──→ build-frontend ──┤──→ ✅ ci-success | ||
| security-audit ──────────────────────────────────────┘ | ||
| ``` |
There was a problem hiding this comment.
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"`. |
There was a problem hiding this comment.
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.
| model configuracoes_sistema { | ||
| chave String @id @db.VarChar(100) | ||
| valor String | ||
| descricao String? | ||
| updated_at DateTime @default(now()) @updatedAt | ||
|
|
||
| @@map("configuracoes_sistema") | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| 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"}`, |
There was a problem hiding this comment.
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.
| 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(); |
There was a problem hiding this comment.
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"} |
There was a problem hiding this comment.
🧩 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.jsRepository: 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 2Repository: 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 1Repository: 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.
| 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) {} |
There was a problem hiding this comment.
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.
| 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.
| 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); | ||
| } | ||
| }; |
There was a problem hiding this comment.
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.
| {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> | ||
| )} |
There was a problem hiding this comment.
🧩 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/srcRepository: 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=jsRepository: CJVW-Code/maes_acao
Length of output: 271
🏁 Script executed:
#!/bin/bash
# Also search without file type restriction
rg -n 'canDistribuir' frontend/srcRepository: 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 -5Repository: 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 -50Repository: 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.jsxRepository: 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.jsxRepository: 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/srcRepository: 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/srcRepository: 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 -10Repository: 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.jsxRepository: 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 2Repository: 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 10Repository: 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.
| 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]); |
There was a problem hiding this comment.
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.
| 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.
Summary by CodeRabbit
Notas da Versão
Novos Recursos
Melhorias de Segurança
Testes e Qualidade