Skip to content

Olimpíadas — Fases por Nível — [Especialista, Coordenador, Mural]

Última atualização: 2026-04-27 Status: ✅ SSOT relacional (Etapa 5 concluída) — JSONB fases_por_nivel DEPRECATED, aguardando DROP COLUMN.


1. Visão Geral

Olimpíadas no sistema podem configurar fases de duas formas: uniforme (mesma fase aplicada a todos os níveis) ou por nível (cada nível tem suas próprias fases, datas e parâmetros). O modelo por_nivel permite cenários reais como OBM (Nv1 com 1 dia, Nv2-3 com 2 dias) ou OBI Programação (níveis Júnior/P1/P2/Mirim com cronogramas distintos).

A fonte única da verdade é a tabela relacional fases_olimpiada, com FK nivel_id → niveis_competicao. A coluna JSONB legada olimpiadas.fases_por_nivel foi descontinuada na Etapa 5 (2026-04-27) — zero leitores, zero escritores em produção.


2. Arquitetura

text
Frontend                        Backend                              Banco
────────                        ───────                              ─────
tab-fases.tsx ──┐               especialista-olimpiadas              olimpiadas (modo_config_fases)
tab-cronograma  ├── invokeAction ─── _shared/olimpiada-helpers ───► fases_olimpiada (nivel_id, data, ...)
tab-aplicacoes  │                    (handleSaveFases,               niveis_competicao
                │                     handleSaveCronograma)
olimpiadas.tsx ─┘               coordenador-olimpiadas         ◄─── controle_aplicacoes (FK fase_id)
                                eventos-calendario(-trial)
                                mural-escola
                                coordenador-controle

Todos os 7 consumidores leem exclusivamente de fases_olimpiada. O JSONB nunca é consultado.


3. Schema

TabelaPropósitoRLSConstraints
olimpiadasCabeçalho da olimpíada + modo de configuraçãoEspecialista CRUD, leitura públicamodo_config_fases ∈ ('uniforme', 'por_nivel')
niveis_competicaoNíveis customizados por olimpíada (em modo por_nivel)Especialista CRUDUNIQUE (olimpiada_id, ordem)
fases_olimpiadaFases relacionais (uma linha por fase de cada nível)Especialista CRUD, leitura por papéis autorizadosFK nivel_id → niveis_competicao(id)

Colunas relevantes — olimpiadas

sql
modo_config_fases   text NOT NULL DEFAULT 'uniforme'  -- 'uniforme' | 'por_nivel'
-- (coluna fases_por_nivel JSONB removida em 2026-05-07 — Etapa 6)

Colunas relevantes — fases_olimpiada

sql
nivel_id          uuid REFERENCES niveis_competicao(id)  -- NULL em modo uniforme; UUID em por_nivel
ordem             int  NOT NULL                          -- ordem dentro do nível (1..N)
nome_fase         text NOT NULL                          -- ex: "1ª Fase", "Dia 1", "Fase 2.1"
data              date                                   -- data de aplicação (NULL = ainda não preenchida)
data_fim          date                                   -- período (multi-dia)
ano_edicao        int  NOT NULL DEFAULT 2026
tempo_maximo_minutos       int
questoes_por_nivel         jsonb
tempo_por_nivel            jsonb
pontuacao_maxima_por_nivel jsonb
tipo_aplicacao             text   -- 'online' | 'presencial' | etc

4. Invariantes (CRÍTICAS — violar = regressão)

I1 — Toda fase em modo por_nivel DEVE ter nivel_id NOT NULL

  • Fases sem nivel_id em modo por_nivel são órfãs (resíduo de migração). São saneadas pelo fail-close em todos os consumidores (não aparecem no calendário, mural ou controle).
  • Estado atual (2026-05-09): zero fases órfãs em produção, validado por query direta. A exceção transitória da OMU UNICAMP (ab745e0f...) foi resolvida na Etapa 6.
  • Onde se aplica: fases_olimpiada para olimpíadas com modo_config_fases = 'por_nivel'.

I2 — save_cronograma em por_nivel SÓ persiste via persistirFasesPorNivelRelacional

  • O array plano params.fases enviado pelo frontend é apenas eco de leitura do GET (com data: '' para níveis recém-criados). Ele NÃO deve gravar nada quando o modo é por_nivel.
  • A SSOT do payload é params.fasesPorNivel (objeto UUID-keyed).
  • Violação causa double-persistence overwrite: as datas corretas gravadas via UPSERT relacional são sobrescritas com NULL pelo bloco legado da lista plana (regressão H1, 2026-04-27).
  • Gate canônico: shouldUpdateFasesPlanasInCronograma(modoConfig, fases) exportado em _shared/olimpiada-helpers.ts.

I3 — Zero fallback para olimpiadas.fases_por_nivel (JSONB)

  • Nenhum consumidor backend ou frontend deve ler a coluna JSONB. Todos consomem fases_olimpiada direto, com buildFasesPorNivelFromRelational reconstruindo o objeto camelCase quando necessário.
  • O JSONB existe apenas no schema até o DROP COLUMN, com dados desatualizados (ex: nomes diferentes do relacional).

5. Gate canônico

ts
// supabase/functions/_shared/olimpiada-helpers.ts
export function shouldUpdateFasesPlanasInCronograma(
  modoConfig: 'uniforme' | 'por_nivel',
  fases: unknown,
): boolean {
  if (!Array.isArray(fases) || fases.length === 0) return false
  return modoConfig === 'uniforme'
}
CenárioModofasesRetornoComportamento
Salvamento normal uniformeuniforme[{...}]trueAtualiza lista plana
Salvamento por_nivel com ecopor_nivel[{...}]falseBloqueia lista plana, usa só fasesPorNivel
Payload sem fasesqualquer[] ou nullfalseNão toca em nada

Cobertura: 7 testes em supabase/functions/_shared/__tests__/olimpiada-helpers.test.ts (seção "H1 gate").


6. Cadeia de consumo

ConsumidorCaminhoLeituraEscrita
especialista-olimpiadas (save_fases / save_cronograma)_shared/olimpiada-helpers.tsfases_olimpiada JOIN niveis_competicaoUPSERT fases_olimpiada por (olimpiada_id, nivel_id, ordem). JSONB NUNCA escrito.
coordenador-olimpiadasindex.tsfases_olimpiada + buildFasesPorNivelFromRelational
coordenador-controleindex.tsfases_olimpiada (3 SELECTs)INSERT controle_aplicacoes com fase_id real
eventos-calendario (coord)index.tsfases_olimpiada + fail-close de órfãs em por_nivel
eventos-calendario-trialindex.tsidem coord
mural-escola (batch_init / get_olimpiadas)index.tsfases_olimpiada (Etapa 5 — sem fallback JSONB)
Frontend src/components/olimpiada-detalhes/tab-fases.tsx, tab-cronograma.tsxConsome fasesPorNivel camelCase derivado pelo backendEnvia fasesPorNivel UUID-keyed via save_fases / save_cronograma

7. Anti-padrões (NÃO fazer)

ts
// ❌ Ler do JSONB legado (Etapa 5 erradicou todos os consumidores)
const fpn = olimp.fases_por_nivel as Record<string, any[]>
fpn[nivel.id]

// ❌ Gravar lista plana em modo por_nivel
if (modoConfig === 'por_nivel') {
  for (const fase of params.fases) {
    await supabase.from('fases_olimpiada').update({ data: fase.data }).eq('id', fase.id)
  }
}

// ❌ Criar fase sem nivel_id em modo por_nivel
await supabase.from('fases_olimpiada').insert({
  olimpiada_id: id, nome_fase: '1ª Fase', ordem: 1
  // nivel_id ausente → vira órfã, será saneada pelo fail-close
})

// ❌ Comparar nome_fase com chave do JSONB (estavam fora de sync na OMU)
const faseReal = fasesOrdenadas.find(fr => fr.nome_fase === f.nome)
ts
// ✅ Ler do relacional + reconstruir camelCase para o frontend
import { buildFasesPorNivelFromRelational } from '../_shared/olimpiada-helpers.ts'
const fasesPorNivel = buildFasesPorNivelFromRelational(fases || [])

// ✅ Persistir cronograma em modo por_nivel
if (shouldUpdateFasesPlanasInCronograma(modoConfig, params.fases)) {
  // só uniforme entra aqui
}
await persistirFasesPorNivelRelacional(supabase, id, fasesPorNivel, ...)

8. Padrões aplicados

PadrãoReferência
Migration JSONB → relacional→ Ref: docs/audits/AUDIT_FASES_POR_NIVEL_2026-04-12.md
Renderização Atômica→ Ref: docs/development/ATOMIC_RENDERING.md
Gate predicado puro + testes unitários→ Ref: supabase/functions/_shared/__tests__/olimpiada-helpers.test.ts
Fail-close de órfãs→ Ref: eventos-calendario/index.ts (descarte de fases sem nivel_id em por_nivel)
Migration Safety Protocol (Fase 3)→ Ref: docs/development/MIGRATION_SAFETY_PROTOCOL.md

9. Pendências

Concluído 2026-05-07olimpiadas.fases_por_nivel JSONB removida via DROP COLUMN após verificação de zero leitores em código e zero fases órfãs em modo por_nivel. Backlog encerrado.

Na mesma entrega: persistirFasesPorNivelRelacional passou a aceitar escalares questoes / pontuacaoMaxima / tempoMinutos da UI por_nivel (1 fase = 1 nível), promovendo-os para { [nivelId]: valor } nos JSONB existentes em fases_olimpiada. Antes, a UI por_nivel salvava silenciosamente NULL nessas colunas (regressão visível primeiro na OBF).


10. Referências