Skip to content

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):

  1. Ler o código-fonte relevante
  2. Verificar console/logs
  3. Consultar banco de dados
  4. 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? → truncate CSS não funciona
  • Por que truncate não funciona? → forwardRef ausente no componente base, Radix não consegue medir/conter o elemento

2.3 Classificação do fix

TipoQuando usarRisco
PaliativoEmergência, sem tempo para root causeAlto (mascara o problema)
ContençãoLimita o impacto enquanto investigaMédio
EstruturalCausa raiz identificada e comprovadaBaixo

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):

  1. Migration SQL primeiro (adiciona antes de remover)
  2. Backend (alinhado com novo schema)
  3. 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ãoConsequênciaCorreção
Aplicar CSS sem investigar componente baseFix falha, sintoma migra (lateral→vertical)Investigar a raiz antes de estilizar
Assumir perda de dados sem consultar bancoPânico desnecessário, ações destrutivasSELECT antes de qualquer "recuperação"
Deployar schema change sem código alinhadoQueries quebram, dados "somem"Deploy atômico (migration+code)
Adicionar overflow-hidden sem min-w-0Container respeita max-w mas filhos grid expandemAmbos necessários em layouts grid/flex
Tratar warning do console como cosméticoWarning de ref causa falha real de medição/layoutWarnings são sintomas de bugs estruturais
Funções de hook sem useCallback em deps de useEffectLoop infinito de re-renders, UI congelauseCallback obrigatório em toda função retornada por hook
enabled que depende de dado que só vem da própria queryDeadlock circular, query nunca disparaPrimeira 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 throwReact Query acha que deu certo, não faz retrySempre throw em erro dentro de queryFn
Tratar timeout (504) como sessão expiradaLogout silencioso sem explicação ao usuárioDiferenciar _transient vs auth error, retry antes de deslogar
useEffect redundante chamando funções que React Query já auto-executaRequests duplicados no mount, risco de loopRemover 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

  1. truncate no SelectTrigger → Não funcionou porque o componente Select não usava forwardRef, impedindo Radix de medir o elemento
  2. overflow-hidden no DialogContent → O conteúdo parou de vazar lateralmente mas passou a crescer verticalmente
  3. max-w forçado no trigger → Limitou o trigger mas não o popover

Resolução definitiva

  1. Causa raiz: src/components/ui/select.tsx usava function components sem forwardRef
  2. Fix estrutural: Refatorar Select para usar React.forwardRef em todos os sub-componentes (Trigger, Content, Item, Label, Separator, ScrollUp/Down)
  3. Contenção complementar: max-h-[calc(100vh-2rem)] + overflow-y-auto no DialogContent
  4. 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

  1. Console warnings (Function components cannot be given refs) eram a pista direta da causa raiz
  2. Sintomas visuais diferentes (overflow lateral vs vertical) podem ter a mesma causa
  3. "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_cores usa padrão "RLS enabled, zero policies" — acesso exclusivo via service_role no 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_coordenadores retornado 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

  1. Features visuais condicionais (cor só se >1 coord) precisam de dados contextuais do backend — não assumir no frontend
  2. Tabelas auxiliares de UI (cores) seguem o padrão service_role do projeto para simplicidade
  3. 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

  1. Por que demora 5s? → 4 requests HTTP paralelos, cada um com overhead de auth+flag
  2. Por que cada request é lento? → Dentro de cada um, N queries sequenciais por olimpíada
  3. Por que N queries? → for (const olp of olimpiadas) { await supabase.from('fases')... } — loop sequencial

Resolução

  1. Action batch_init_resultados: Consolida 4 requests em 1 único
  2. .in('olimpiada_id', ids): Busca fases/inscrições/resultados de todas olimpíadas de uma vez
  3. Promise.all no backend para paralelizar queries independentes
  4. 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

  1. Por que mostra "sem olimpíadas"? → olimpiadas está []
  2. Por que []? → Query nunca executou (enabled: false)
  3. Por que enabled: false? → Depende de anoEdicao que 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: enabled nunca 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

  1. Por que a UI congela? → Loop infinito de re-renders saturando o event loop
  2. Por que loop infinito? → useEffect dispara a cada render porque deps mudam
  3. Por que deps mudam? → Funções retornadas pelo hook sem useCallback criam nova referência a cada render

Resolução

  1. useCallback em todas as 12 funções retornadas pelo hook
  2. Remoção do useEffect de mount redundante — React Query já auto-carrega via useQuery
  3. 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 como mensagens.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

text
Infra timeout (504) ──┬──→ auth-context: "sem sessão" → logout silencioso
                      └──→ banners: retorna [] → sem retry → tela vazia

1 falha de infra gerou 3 sintomas aparentemente independentes.

3 Porquês

  1. Por que logout silencioso? → auth-context trata qualquer falha do /me como sessão expirada
  2. Por que banners vazios? → queryFn retorna [] em erro → React Query acha que deu certo
  3. Por que tudo ao mesmo tempo? → Timeout de infra afetou ambos os endpoints simultaneamente

Resolução

  1. invokeEdge: retry automático (max 2) com backoff para 502/503/504
  2. auth-context: flag _transient diferencia timeout de sessão expirada; toast informativo + retry em 5s
  3. Banners: throw em erro dentro de queryFn → React Query faz retry
  4. Resultado: Timeout transitório é invisível para o usuário; sessão realmente expirada mostra toast explicativo

Regra Derivada

queryFn DEVE fazer throw em 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 diretor abre sem permissões, mesmo com features globais ativas
  • Botão "Salvar permissões" não produz feedback (silencioso)
  • Usuário logado como diretor vê sidebar/tabs inconsistentes

3 Porquês (Causa Raiz Múltipla)

  1. Por que permissões aparecem vazias?get_permissoes_usuario recebia só usuario_id + escola_id e pegava vinculos?.[0] (primeiro papel). Se coordenador veio primeiro, diretor recebia permissões do coordenador (ou zero se incompatíveis).

  2. Por que salvar não funciona? → Modelo de dados: usuarios_escola_permissoes com UNIQUE (usuario_id, escola_id, permissao) — sem papel_id. Salvar diretor sobrescrevia coordenador ou gerava conflito silencioso.

  3. Por que sem toast? → Wrappers em admin-usuarios.tsx engoliam erro no catch e retornavam { success: false } sem chamar olpToast. Frontend não reidratava após save.

Cascata de Falhas

text
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 feedback

1 falha estrutural (modelo de dados) gerou 4 sintomas aparentemente independentes.

Resolução (Estrutural — 5 camadas)

  1. Migration: Adicionou papel_id em usuarios_escola_permissoes e usuarios_escola_sub_permissoes. Novo UNIQUE: (usuario_id, escola_id, papel_id, permissao). Backfill de dados existentes por papel compatível.
  2. Backend CRUD: get_permissoes_usuario e update_permissoes_usuario recebem e filtram por papel_id explícito.
  3. Helpers: seedPermissionsForRole e removePermissionsForRole escopados por papel_id.
  4. Frontend admin: AdminPermissoesGrid recebe papel_id, implementa feedback com olpToast, rehydrata após save.
  5. /me: Resolve papel_id do papel ativo e retorna permissões isoladas.

Regras Derivadas

Toda tabela de permissão DEVE ter escopo de papel_id — nunca apenas usuario_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ãoConsequênciaCorreção
Permissões escopadas só por (usuario_id, escola_id)Papéis colidem na mesma escolaAdicionar papel_id ao modelo + queries
Backend pega "primeiro vínculo" sem papel_idUI mostra dados de outro papelReceber e filtrar por papel_id explícito
Wrapper com catch silenciosoUsuário sem feedback de saveolpToast em todo success/error path
Frontend mapeia papel_id: ''Backend não encontra vínculoPreservar UUID real do mapeamento
gestao-usuarios-escola INSERT sem papel_idRegistros órfãos → /me não encontra → AccessBlockedScreenResolver papel_id do vínculo ativo antes de INSERT
DELETE sem scoping por papel_idApaga permissões de todos os papéis.eq("papel_id", resolved) em todo DELETE
Falta seed de sub_permissoes no createTabs vazias até admin salvar manualmenteseedSubPermissoesForRole() após criação
Unique index com NULL (papel_id)Duplicatas ilimitadas no PostgreSQLALTER COLUMN papel_id SET NOT NULL