Resolução de Problemas — Metodologia OLP
REGRA ZERO
Ler este documento ANTES de iniciar qualquer investigação ou implementação de correção.
1. Diagnóstico (antes de qualquer código)
1.1 Coletar evidências
- Screenshot/descrição do usuário → entender o SINTOMA
- Console logs → warnings e erros reais
- Network requests → respostas do backend
- Banco de dados → estado real dos dados
- NUNCA assumir a causa pelo sintoma
1.2 Separar problemas
- Cada sintoma pode ter causa independente
- Listar todos os problemas reportados
- Investigar cada um separadamente
- Só unificar se a causa raiz for comprovadamente a mesma
1.3 Eliminar hipóteses por camada
Ordem obrigatória (do mais barato ao mais caro):
- Ler o código-fonte relevante
- Verificar console/logs
- Consultar banco de dados
- Testar no browser (último recurso)
2. Análise de causa raiz
2.1 Perguntas obrigatórias
- "Isso é sintoma ou causa?"
- "Onde na cadeia (banco → backend → frontend → UI) o problema NASCE?"
- "Existem warnings/erros no console que apontam a causa?"
- "O fix anterior criou um novo sintoma?" (regressão)
2.2 Padrão "3 porquês"
Exemplo real do projeto:
- Por que o modal quebra? → Select expande além do container
- Por que o Select expande? →
truncateCSS não funciona - Por que
truncatenão funciona? →forwardRefausente no componente base, Radix não consegue medir/conter o elemento
2.3 Classificação do fix
| Tipo | Quando usar | Risco |
|---|---|---|
| Paliativo | Emergência, sem tempo para root cause | Alto (mascara o problema) |
| Contenção | Limita o impacto enquanto investiga | Médio |
| Estrutural | Causa raiz identificada e comprovada | Baixo |
SEMPRE preferir estrutural. Paliativo APENAS com nota de débito técnico documentada.
3. Planejamento da solução
3.1 Mapear impacto
- Listar TODOS os arquivos que usam o componente/função afetada
- Verificar se a correção quebra outros consumidores
- Se for componente base (
ui/*), o impacto é global
3.2 Ordem de execução
Para mudanças coordenadas (banco + backend + frontend):
- Migration SQL primeiro (adiciona antes de remover)
- Backend (alinhado com novo schema)
- Frontend (consome novo contrato)
Deploy atômico: tudo junto, nunca parcial.
3.3 Retrocompatibilidade
- Decisão EXPLÍCITA: manter ou não
- Se não manter: varrer TODAS as referências (
grep/search global) - Se manter: documentar quando será removido
4. Implementação
4.1 Princípio da cirurgia mínima
- Alterar APENAS o necessário para resolver a causa raiz
- Não refatorar código adjacente "de oportunidade"
- Cada mudança deve ser justificável pela causa raiz
4.2 Verificação pós-fix
- O sintoma original sumiu?
- Novos warnings/erros no console?
- Outros consumidores do componente continuam funcionando?
- O fix introduziu regressão visual ou funcional?
5. Anti-padrões (erros reais cometidos)
| Anti-padrão | Consequência | Correção |
|---|---|---|
| Aplicar CSS sem investigar componente base | Fix falha, sintoma migra (lateral→vertical) | Investigar a raiz antes de estilizar |
| Assumir perda de dados sem consultar banco | Pânico desnecessário, ações destrutivas | SELECT antes de qualquer "recuperação" |
| Deployar schema change sem código alinhado | Queries quebram, dados "somem" | Deploy atômico (migration+code) |
Adicionar overflow-hidden sem min-w-0 | Container respeita max-w mas filhos grid expandem | Ambos necessários em layouts grid/flex |
| Tratar warning do console como cosmético | Warning de ref causa falha real de medição/layout | Warnings são sintomas de bugs estruturais |
Funções de hook sem useCallback em deps de useEffect | Loop infinito de re-renders, UI congela | useCallback obrigatório em toda função retornada por hook |
enabled que depende de dado que só vem da própria query | Deadlock circular, query nunca dispara | Primeira chamada com enabled: true, filtro refinado em chamada subsequente |
| Query N+1 em loop por item (buscar fases por olimpíada) | Latência O(n), 20+ queries por page load | .in() consolidado + batch_init action |
queryFn retorna [] em erro em vez de throw | React Query acha que deu certo, não faz retry | Sempre throw em erro dentro de queryFn |
| Tratar timeout (504) como sessão expirada | Logout silencioso sem explicação ao usuário | Diferenciar _transient vs auth error, retry antes de deslogar |
useEffect redundante chamando funções que React Query já auto-executa | Requests duplicados no mount, risco de loop | Remover useEffect manual — confiar no useQuery |
6. Template de investigação
Para cada problema reportado, preencher antes de codar:
### Problema: [descrição em 1 linha]
- **Sintoma**: o que o usuário vê
- **Console**: warnings/erros relevantes
- **Hipótese 1**: [causa provável] → [como validar]
- **Hipótese 2**: [causa provável] → [como validar]
- **Causa raiz comprovada**: [após investigação]
- **Tipo de fix**: paliativo / contenção / estrutural
- **Arquivos afetados**: [lista]
- **Risco de regressão**: baixo / médio / alto
- **Validação**: [como confirmar que resolveu]7. Casos de estudo
Caso 1: Modal da Agenda (março/2026)
Contexto
Modal de detalhes da tarefa quebrava layout ao selecionar olimpíada com nome longo. Três tentativas de CSS falharam antes da resolução definitiva.
Tentativas falhadas
truncateno SelectTrigger → Não funcionou porque o componenteSelectnão usavaforwardRef, impedindo Radix de medir o elementooverflow-hiddenno DialogContent → O conteúdo parou de vazar lateralmente mas passou a crescer verticalmentemax-wforçado no trigger → Limitou o trigger mas não o popover
Resolução definitiva
- Causa raiz:
src/components/ui/select.tsxusava function components semforwardRef - Fix estrutural: Refatorar Select para usar
React.forwardRefem todos os sub-componentes (Trigger, Content, Item, Label, Separator, ScrollUp/Down) - Contenção complementar:
max-h-[calc(100vh-2rem)]+overflow-y-autono DialogContent - Resultado: Zero warnings, truncamento funcional, layout estável
Segundo problema (mesmo ticket)
- Sintoma: "Tarefas do dia 20/03 sumiram"
- Investigação: Query no banco → tarefas existem em outra escola do mesmo usuário
- Causa: Contexto de escola ativa diferente, não perda de dados
- Resolução: Nenhuma mudança de código necessária — comportamento correto
Lições
- Console warnings (
Function components cannot be given refs) eram a pista direta da causa raiz - Sintomas visuais diferentes (overflow lateral vs vertical) podem ter a mesma causa
- "Dados sumiram" quase sempre é problema de contexto/filtro, não perda real
Caso 2: Feature multi-camada com deploy atômico (2026-03-20)
Contexto
Implementação de cor de identificação por coordenador em tarefas. Envolve 3 camadas: migration SQL (nova tabela coordenador_cores), backend (3 actions em user-profile, JOIN em tarefas-escola), frontend (hooks, card visual, perfil).
Padrão aplicado
- Tabela sem RLS policies:
coordenador_coresusa padrão "RLS enabled, zero policies" — acesso exclusivo viaservice_roleno backend (padrão do projeto para tabelas de sistema). createSupabaseSystem()para queries em tabela sem policies (cores),createSupabaseClient(req)para queries com RLS (tarefas).- Promise.all para buscar tarefas + cores + contagem de coordenadores em paralelo, sem degradar latência.
- Meta no response: Campo
meta.total_coordenadoresretornado junto com a lista de tarefas para o frontend decidir se aplica cores (regra: >1 coordenador). - Paleta fixa: 10 cores pastel pré-definidas, validadas no backend. Cores em uso por outros coordenadores desabilitadas no frontend.
- Upsert com conflito:
onConflict: "usuario_id,escola_id"para idempotência na atribuição de cor.
Lições
- Features visuais condicionais (cor só se >1 coord) precisam de dados contextuais do backend — não assumir no frontend
- Tabelas auxiliares de UI (cores) seguem o padrão
service_roledo projeto para simplicidade - Deploy atômico: migration → backend → frontend, tudo na mesma entrega
Caso 3: Performance N+1 e Batching (março/2026)
Contexto
Tela de Resultados fazia 4 requests HTTP paralelos no mount, cada um passando pelo pipeline completo de auth + feature flag + CORS. No backend, cada olimpíada aderida disparava queries individuais para fases, inscrições e resultados — um padrão N+1 clássico.
Sintomas
- Page load ~5 segundos (com 3-4 olimpíadas)
- Network tab mostrava 4 requests paralelos para o mesmo backend
- Cada request interno fazia ~4 queries ao banco por olimpíada
3 Porquês
- Por que demora 5s? → 4 requests HTTP paralelos, cada um com overhead de auth+flag
- Por que cada request é lento? → Dentro de cada um, N queries sequenciais por olimpíada
- Por que N queries? →
for (const olp of olimpiadas) { await supabase.from('fases')... }— loop sequencial
Resolução
- Action
batch_init_resultados: Consolida 4 requests em 1 único .in('olimpiada_id', ids): Busca fases/inscrições/resultados de todas olimpíadas de uma vezPromise.allno backend para paralelizar queries independentes- Resultado: De ~5s para <1s
Padrão Replicável
Aplicado também no Mural (batch_init no mural-escola). Qualquer tela com 3+ requests para o mesmo backend é candidata.
Caso 4: Dependência Circular em useQuery (março/2026)
Contexto
useResultadosInit tinha enabled: anoEdicao !== null para filtrar por ano. Porém, anoEdicao só era populado quando a query retornava dados — criando um deadlock: query não roda → dados não vêm → ano nunca é setado → query nunca roda.
Sintomas
- Tela mostra "Nenhuma olimpíada encontrada" permanentemente
- Nenhum request disparado no Network tab (query
enabled: false) - Sem erros no console
3 Porquês
- Por que mostra "sem olimpíadas"? →
olimpiadasestá[] - Por que
[]? → Query nunca executou (enabled: false) - Por que
enabled: false? → Depende deanoEdicaoque só vem da própria query → deadlock circular
Resolução
enabled: true— primeira chamada sem filtro carrega dados base- Segunda chamada com ano específico usa queryKey diferente → cacheada separadamente
- Regra:
enablednunca deve depender de dado que só existe no resultado da própria query
Caso 5: Loop Infinito de Re-renders — Mensagens (março/2026)
Contexto
Hook useComunicacaoEscola retornava 12 funções imperativas sem useCallback. No componente comunicacao.tsx, essas funções eram usadas como dependências de useEffect. Nova ref a cada render → useEffect dispara → setState → re-render → loop infinito.
Sintomas
- UI congela completamente ao clicar em "Templates"
- Network tab mostra avalanche de requests idênticos (100+ em <2 segundos)
- Browser fica em estado "não responde"
- Cursor trava em estado pointer
Padrão de Detecção
Abrir Network tab → avalanche de requests idênticos com timestamp crescente em <1s é sinal inequívoco de loop infinito de re-renders.
3 Porquês
- Por que a UI congela? → Loop infinito de re-renders saturando o event loop
- Por que loop infinito? →
useEffectdispara a cada render porque deps mudam - Por que deps mudam? → Funções retornadas pelo hook sem
useCallbackcriam nova referência a cada render
Resolução
useCallbackem todas as 12 funções retornadas pelo hook- Remoção do
useEffectde mount redundante — React Query já auto-carrega viauseQuery - Resultado: Zero re-renders desnecessários, UI responsiva
Regra Derivada
Toda função retornada por hook custom DEVE usar
useCallback. Dependências devem ser estáveis (queryClient, mutation, refetch) — nunca dados reativos comomensagens.length.
Caso 6: Timeout Transitório vs Sessão Expirada (março/2026)
Contexto
Supabase retornou 504 (timeout de infra transitório). O auth-context interpretou como "sessão expirada" → logout silencioso. Simultaneamente, banners falharam com 500 e retornavam [] em vez de throw → React Query não fez retry. Resultado: usuário redirecionado para login com tela de banners vazia.
Cascata de Falhas
Infra timeout (504) ──┬──→ auth-context: "sem sessão" → logout silencioso
└──→ banners: retorna [] → sem retry → tela vazia1 falha de infra gerou 3 sintomas aparentemente independentes.
3 Porquês
- Por que logout silencioso? → auth-context trata qualquer falha do
/mecomo sessão expirada - Por que banners vazios? →
queryFnretorna[]em erro → React Query acha que deu certo - Por que tudo ao mesmo tempo? → Timeout de infra afetou ambos os endpoints simultaneamente
Resolução
invokeEdge: retry automático (max 2) com backoff para 502/503/504auth-context: flag_transientdiferencia timeout de sessão expirada; toast informativo + retry em 5s- Banners:
throwem erro dentro dequeryFn→ React Query faz retry - Resultado: Timeout transitório é invisível para o usuário; sessão realmente expirada mostra toast explicativo
Regra Derivada
queryFnDEVE fazerthrowem erro — nunca retornar estado vazio. Retornar[]em erro impede retry e mascara falha.
Caso 7: Colisão de Permissões Multi-Papel na Mesma Escola (abril/2026)
Contexto
Usuário com diretor e coordenador na mesma escola. Ao abrir o badge diretor no modal admin, as permissões apareciam vazias (sem tabs, sub-flags). Ao clicar "Salvar permissões", nada acontecia — sem toast de sucesso nem erro. Na visão do usuário, o papel selecionado ficava sem acesso correto.
Sintomas
- Badge
diretorabre sem permissões, mesmo com features globais ativas - Botão "Salvar permissões" não produz feedback (silencioso)
- Usuário logado como
diretorvê sidebar/tabs inconsistentes
3 Porquês (Causa Raiz Múltipla)
Por que permissões aparecem vazias? →
get_permissoes_usuariorecebia sóusuario_id + escola_ide pegavavinculos?.[0](primeiro papel). Secoordenadorveio primeiro,diretorrecebia permissões do coordenador (ou zero se incompatíveis).Por que salvar não funciona? → Modelo de dados:
usuarios_escola_permissoescom UNIQUE(usuario_id, escola_id, permissao)— sempapel_id. Salvardiretorsobrescreviacoordenadorou gerava conflito silencioso.Por que sem toast? → Wrappers em
admin-usuarios.tsxengoliam erro nocatche retornavam{ success: false }sem chamarolpToast. Frontend não reidratava após save.
Cascata de Falhas
Modelo sem papel_id ──┬──→ get_permissoes: retorna papel errado → UI vazia
├──→ update_permissoes: salva contra papel errado → 0 permissões válidas
├──→ /me: resolve menu sem distinguir papel ativo → sidebar inconsistente
└──→ Frontend: wrappers silenciosos → sem feedback1 falha estrutural (modelo de dados) gerou 4 sintomas aparentemente independentes.
Resolução (Estrutural — 5 camadas)
- Migration: Adicionou
papel_idemusuarios_escola_permissoeseusuarios_escola_sub_permissoes. Novo UNIQUE:(usuario_id, escola_id, papel_id, permissao). Backfill de dados existentes por papel compatível. - Backend CRUD:
get_permissoes_usuarioeupdate_permissoes_usuariorecebem e filtram porpapel_idexplícito. - Helpers:
seedPermissionsForRoleeremovePermissionsForRoleescopados porpapel_id. - Frontend admin:
AdminPermissoesGridrecebepapel_id, implementa feedback comolpToast, rehydrata após save. /me: Resolvepapel_iddo papel ativo e retorna permissões isoladas.
Regras Derivadas
Toda tabela de permissão DEVE ter escopo de
papel_id— nunca apenasusuario_id + escola_id. Se um usuário pode ter múltiplos papéis na mesma escola, escopo por papel é obrigatório.
Toda operação de save DEVE ter feedback visual (toast sucesso/erro) — sem caminhos silenciosos. Wrappers que engolem erros com
catch { return { success: false } }são 🔴 CRÍTICO.
Pós-save: rehydratar estado da UI — após save com sucesso, invalidar cache ou refetch para que a UI reflita o estado persistido. Nunca confiar no estado local pré-save.
Anti-padrão Catalogado
| Anti-padrão | Consequência | Correção |
|---|---|---|
Permissões escopadas só por (usuario_id, escola_id) | Papéis colidem na mesma escola | Adicionar papel_id ao modelo + queries |
Backend pega "primeiro vínculo" sem papel_id | UI mostra dados de outro papel | Receber e filtrar por papel_id explícito |
| Wrapper com catch silencioso | Usuário sem feedback de save | olpToast em todo success/error path |
Frontend mapeia papel_id: '' | Backend não encontra vínculo | Preservar UUID real do mapeamento |
gestao-usuarios-escola INSERT sem papel_id | Registros órfãos → /me não encontra → AccessBlockedScreen | Resolver papel_id do vínculo ativo antes de INSERT |
DELETE sem scoping por papel_id | Apaga permissões de todos os papéis | .eq("papel_id", resolved) em todo DELETE |
| Falta seed de sub_permissoes no create | Tabs vazias até admin salvar manualmente | seedSubPermissoesForRole() após criação |
Unique index com NULL (papel_id) | Duplicatas ilimitadas no PostgreSQL | ALTER COLUMN papel_id SET NOT NULL |