Auditoria: Reestruturação Fases por Nível — 2026-04-12
Data: 2026-04-12
Escopo: Migrationnivel_idemfases_olimpiada+ correção de regressões
Status: ✅ Concluída (Fases 1-4). Coluna JSONBfases_por_nivelnão é mais lida/escrita por nenhum consumidor.
1. Contexto
O sistema tinha duas fontes de verdade incompatíveis para fases de olimpíadas com modo_config_fases = 'por_nivel':
fases_olimpiada(tabela relacional) — fases genéricas compartilhadas entre níveis, referenciadas por FK em 7 tabelas.olimpiadas.fases_por_nivel(JSONB) — fases específicas por nível, sem IDs reais.
Impacto: controle_aplicacoes.fase_id só conseguia criar registros para fases genéricas, ignorando a estrutura real por nível.
2. Migrations Executadas
2.1. Schema: nivel_id em fases_olimpiada
ALTER TABLE fases_olimpiada
ADD COLUMN nivel_id uuid REFERENCES niveis_competicao(id) ON DELETE SET NULL;
CREATE INDEX idx_fases_olimpiada_nivel ON fases_olimpiada(nivel_id) WHERE nivel_id IS NOT NULL;Semântica:
nivel_id = NULL→ fase uniforme (comportamento inalterado)nivel_id = <uuid>→ fase específica daquele nível
2.2. Migration de Dados
Para cada olimpíada por_nivel, as fases foram migradas do JSONB para a tabela relacional:
- Leitura de
olimpiadas.fases_por_nivelJSONB - Cruzamento com
niveis_competicaopara obternivel_id - Criação de fases reais com
nivel_idpreenchido - Preservação de fases compartilhadas existentes
Mapeamento de campos:
| JSONB | Tabela |
|---|---|
nome | nome_fase |
data | data |
dataFim | data_fim |
tempoMinutos | tempo_maximo_minutos |
2.3. Constraint Refactor
-- Removida constraint única por (olimpiada_id, nome_fase)
-- Nova constraint: (olimpiada_id, nivel_id, nome_fase, ano_edicao)
ALTER TABLE fases_olimpiada
ADD CONSTRAINT fases_olimpiada_unique_por_nivel
UNIQUE NULLS NOT DISTINCT (olimpiada_id, nivel_id, nome_fase, ano_edicao);2.4. Backend: coordenador-controle
create_aplicacoes: nova lógica — detectamodo_config_fases, sepor_nivel→ itera por nível com séries específicaslist_aplicacoes: inclui dados de nível no response (JOIN comniveis_competicao)buildOlimpiadasAderidas: retorna fases agrupadas por nível
2.5. Frontend: Modal "Criar Aplicação"
- Detecta modo → mostra fases agrupadas por nível com sub-grupos
- Seleção por nível gera registros com os segmentos corretos
3. Regressões Encontradas e Corrigidas
3.1. resultados-queries.ts — Query .single() sem scoping (P0)
Problema: Query de "fase anterior" usava .eq("ordem", X).single() sem filtrar nivel_id. Com fases duplicadas por nível (mesma ordem), retornava PGRST116 multiple rows.
Fix: Escopo por nivel_id + .maybeSingle():
let q = supabase.from("fases_olimpiada")
.eq("olimpiada_id", id).eq("ordem", targetOrdem);
if (faseAtual.nivel_id) {
q = q.eq("nivel_id", faseAtual.nivel_id);
} else {
q = q.is("nivel_id", null);
}
const { data } = await q.maybeSingle();3.2. resultados-mutations.ts — Mesmo padrão (P0)
Problema: handleUpsertBatch e handleUpsertSingle buscavam fase anterior sem scoping por nivel_id.
Fix: Mesmo padrão aplicado em ambas as funções.
3.3. resultados-compute.ts — Detecção de "última fase" (P0)
Problema: ehUltimaFase não filtrava por nivel_id, podendo retornar fase de outro nível como "última".
Fix: Scoping da query de max ordem por nivel_id.
3.4. resultados-import.ts — Mesma regressão (P0)
Problema: Importação background buscava "última fase" sem escopo de nível.
Fix: Aplicado scoping consistente.
3.5. mural-escola/index.ts — Matching por nome (P1)
Problema: Matching de fases usava apenas nome_fase, que não é mais único dentro de uma olimpíada.
Fix: Prioriza matching via nivel_id quando disponível.
3.6. especialista-olimpiadas/index.ts — Cópia entre edições (P1)
Problema: Ao copiar fases entre edições, nivel_id não era preservado.
Fix: nivel_id incluído na cópia.
4. Testes Executados
| Suite | Resultado | Cobertura |
|---|---|---|
gestao-resultados | ✅ 26/26 | CORS, auth, queries, mutations, import |
coordenador-controle | ✅ 5/5 | CORS, auth, actions |
especialista-olimpiadas | ✅ 49/49 | Cópia, edição, gestão de fases |
liberacoes-logic (frontend) | ✅ 33/33 | Liberações, snapshots |
mural-escola | ✅ Pass | Matching, renderização |
eventos-calendario | ✅ Pass | Leitura JSONB (inalterado) |
5. Fase 4 — Migração Consumidores P1 (Concluída)
Data: 2026-04-12
Mudanças
| Arquivo | Mudança |
|---|---|
eventos-calendario/index.ts | Removida leitura JSONB; query unificada fases_olimpiada com JOIN niveis_competicao(titulo) para todas as olimpíadas |
eventos-calendario-trial/index.ts | Idem; removido SELECT modo_config_fases, fases_por_nivel |
coordenador-olimpiadas/index.ts | fasesPorNivel construído via buildFasesPorNivelFromRelational() em vez de olimpiada.fases_por_nivel |
_shared/olimpiada-helpers.ts | Removido dual-write (updateData.fases_por_nivel); handleGet constrói response da relacional |
Testes Pós-Migração
| Suite | Resultado |
|---|---|
coordenador-olimpiadas | ✅ 15/15 |
especialista-olimpiadas | ✅ 49/49 |
eventos-calendario | ✅ Pass |
6. Dívidas Técnicas Remanescentes
| # | Dívida | Severidade | Detalhe | Status |
|---|---|---|---|---|
| 1 | coordenador-controle/index.ts >1390 linhas | Média | Ultrapassa limite de 400 linhas; candidato a code-split por action | Pendente |
| 2 | Fases genéricas órfãs em fases_olimpiada | Baixa | Fases compartilhadas antigas (sem nivel_id) permanecem para olimpíadas por_nivel | Pendente |
| 3 | Coluna JSONB fases_por_nivel ainda existe | Baixa | Não é mais lida/escrita; pode ser removida em migration futura (DROP COLUMN) | Pendente |
Plano de Resolução
Code Splitting:
coordenador-controle/index.ts→ separar em módulos por action:actions/create.ts,actions/list.ts,actions/update.ts, etc.
Cleanup:
- DROP COLUMN
fases_por_nivelem migration futura (após confirmar que nenhum consumidor externo depende)
6. Arquivos Modificados
Criados/Migrados
- Migration:
nivel_id+ index + constraint - Migration de dados: fases JSONB → relacional
Backend (Edge Functions)
| Arquivo | Mudança |
|---|---|
_shared/resultados-queries.ts | Scoping por nivel_id em queries de fase anterior/última |
_shared/resultados-mutations.ts | Scoping por nivel_id em upsert batch/single |
_shared/resultados-compute.ts | Scoping por nivel_id em detecção de última fase |
_shared/resultados-import.ts | Scoping por nivel_id em importação background |
mural-escola/index.ts | Matching por nivel_id (prioridade sobre nome) |
especialista-olimpiadas/index.ts | Preservação de nivel_id na cópia entre edições |
coordenador-controle/index.ts | create_aplicacoes por nível, list_aplicacoes com JOIN, buildOlimpiadasAderidas agrupado |
Frontend
| Arquivo | Mudança |
|---|---|
criar-aplicacao-modal.tsx | UI agrupada por nível |
helpers.ts | Novos tipos FasesPorNivel |
7. Referências
.lovable/plan.md— Plano original da reestruturaçãodocs/features/CONTROLE.md— Documentação da seção Controledocs/features/RESULTADOS.md— Documentação de Resultadosdocs/development/MIGRATION_GUIDELINES.md— Diretrizes de migrations