Skip to content

Auditoria: Reestruturação Fases por Nível — 2026-04-12

Data: 2026-04-12
Escopo: Migration nivel_id em fases_olimpiada + correção de regressões
Status: ✅ Concluída (Fases 1-4). Coluna JSONB fases_por_nivel nã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':

  1. fases_olimpiada (tabela relacional) — fases genéricas compartilhadas entre níveis, referenciadas por FK em 7 tabelas.
  2. 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

sql
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_nivel JSONB
  • Cruzamento com niveis_competicao para obter nivel_id
  • Criação de fases reais com nivel_id preenchido
  • Preservação de fases compartilhadas existentes

Mapeamento de campos:

JSONBTabela
nomenome_fase
datadata
dataFimdata_fim
tempoMinutostempo_maximo_minutos

2.3. Constraint Refactor

sql
-- 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 — detecta modo_config_fases, se por_nivel → itera por nível com séries específicas
  • list_aplicacoes: inclui dados de nível no response (JOIN com niveis_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():

typescript
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

SuiteResultadoCobertura
gestao-resultados✅ 26/26CORS, auth, queries, mutations, import
coordenador-controle✅ 5/5CORS, auth, actions
especialista-olimpiadas✅ 49/49Cópia, edição, gestão de fases
liberacoes-logic (frontend)✅ 33/33Liberações, snapshots
mural-escola✅ PassMatching, renderização
eventos-calendario✅ PassLeitura JSONB (inalterado)

5. Fase 4 — Migração Consumidores P1 (Concluída)

Data: 2026-04-12

Mudanças

ArquivoMudança
eventos-calendario/index.tsRemovida leitura JSONB; query unificada fases_olimpiada com JOIN niveis_competicao(titulo) para todas as olimpíadas
eventos-calendario-trial/index.tsIdem; removido SELECT modo_config_fases, fases_por_nivel
coordenador-olimpiadas/index.tsfasesPorNivel construído via buildFasesPorNivelFromRelational() em vez de olimpiada.fases_por_nivel
_shared/olimpiada-helpers.tsRemovido dual-write (updateData.fases_por_nivel); handleGet constrói response da relacional

Testes Pós-Migração

SuiteResultado
coordenador-olimpiadas✅ 15/15
especialista-olimpiadas✅ 49/49
eventos-calendario✅ Pass

6. Dívidas Técnicas Remanescentes

#DívidaSeveridadeDetalheStatus
1coordenador-controle/index.ts >1390 linhasMédiaUltrapassa limite de 400 linhas; candidato a code-split por actionPendente
2Fases genéricas órfãs em fases_olimpiadaBaixaFases compartilhadas antigas (sem nivel_id) permanecem para olimpíadas por_nivelPendente
3Coluna JSONB fases_por_nivel ainda existeBaixaNã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_nivel em 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)

ArquivoMudança
_shared/resultados-queries.tsScoping por nivel_id em queries de fase anterior/última
_shared/resultados-mutations.tsScoping por nivel_id em upsert batch/single
_shared/resultados-compute.tsScoping por nivel_id em detecção de última fase
_shared/resultados-import.tsScoping por nivel_id em importação background
mural-escola/index.tsMatching por nivel_id (prioridade sobre nome)
especialista-olimpiadas/index.tsPreservação de nivel_id na cópia entre edições
coordenador-controle/index.tscreate_aplicacoes por nível, list_aplicacoes com JOIN, buildOlimpiadasAderidas agrupado

Frontend

ArquivoMudança
criar-aplicacao-modal.tsxUI agrupada por nível
helpers.tsNovos tipos FasesPorNivel

7. Referências

  • .lovable/plan.md — Plano original da reestruturação
  • docs/features/CONTROLE.md — Documentação da seção Controle
  • docs/features/RESULTADOS.md — Documentação de Resultados
  • docs/development/MIGRATION_GUIDELINES.md — Diretrizes de migrations