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_nivelDEPRECATED, aguardandoDROP 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
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-controleTodos os 7 consumidores leem exclusivamente de fases_olimpiada. O JSONB nunca é consultado.
3. Schema
| Tabela | Propósito | RLS | Constraints |
|---|---|---|---|
olimpiadas | Cabeçalho da olimpíada + modo de configuração | Especialista CRUD, leitura pública | modo_config_fases ∈ ('uniforme', 'por_nivel') |
niveis_competicao | Níveis customizados por olimpíada (em modo por_nivel) | Especialista CRUD | UNIQUE (olimpiada_id, ordem) |
fases_olimpiada | Fases relacionais (uma linha por fase de cada nível) | Especialista CRUD, leitura por papéis autorizados | FK nivel_id → niveis_competicao(id) |
Colunas relevantes — olimpiadas
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
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' | etc4. Invariantes (CRÍTICAS — violar = regressão)
I1 — Toda fase em modo por_nivel DEVE ter nivel_id NOT NULL
- Fases sem
nivel_idem modopor_nivelsã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_olimpiadapara olimpíadas commodo_config_fases = 'por_nivel'.
I2 — save_cronograma em por_nivel SÓ persiste via persistirFasesPorNivelRelacional
- O array plano
params.fasesenviado pelo frontend é apenas eco de leitura do GET (comdata: ''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_olimpiadadireto, combuildFasesPorNivelFromRelationalreconstruindo 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
// 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ário | Modo | fases | Retorno | Comportamento |
|---|---|---|---|---|
Salvamento normal uniforme | uniforme | [{...}] | true | Atualiza lista plana |
Salvamento por_nivel com eco | por_nivel | [{...}] | false | Bloqueia lista plana, usa só fasesPorNivel |
| Payload sem fases | qualquer | [] ou null | false | Não toca em nada |
Cobertura: 7 testes em supabase/functions/_shared/__tests__/olimpiada-helpers.test.ts (seção "H1 gate").
6. Cadeia de consumo
| Consumidor | Caminho | Leitura | Escrita |
|---|---|---|---|
especialista-olimpiadas (save_fases / save_cronograma) | _shared/olimpiada-helpers.ts | fases_olimpiada JOIN niveis_competicao | UPSERT fases_olimpiada por (olimpiada_id, nivel_id, ordem). JSONB NUNCA escrito. |
coordenador-olimpiadas | index.ts | fases_olimpiada + buildFasesPorNivelFromRelational | — |
coordenador-controle | index.ts | fases_olimpiada (3 SELECTs) | INSERT controle_aplicacoes com fase_id real |
eventos-calendario (coord) | index.ts | fases_olimpiada + fail-close de órfãs em por_nivel | — |
eventos-calendario-trial | index.ts | idem coord | — |
mural-escola (batch_init / get_olimpiadas) | index.ts | fases_olimpiada (Etapa 5 — sem fallback JSONB) | — |
Frontend src/components/olimpiada-detalhes/ | tab-fases.tsx, tab-cronograma.tsx | Consome fasesPorNivel camelCase derivado pelo backend | Envia fasesPorNivel UUID-keyed via save_fases / save_cronograma |
7. Anti-padrões (NÃO fazer)
// ❌ 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)// ✅ 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ão | Referê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-07 — olimpiadas.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
- AUDIT_FASES_POR_NIVEL_2026-04-12.md — histórico de execução das Etapas 1-5 + hotfix H1
- DEV_WORKFLOW.md §Migração de modelo de dados — protocolo a seguir em refactors JSONB → relacional
- MIGRATION_SAFETY_PROTOCOL.md — protocolo obrigatório de 3 fases
- DATABASE_SCHEMA.md — schema completo
- Memória:
mem://architecture/olympics-phases-by-level-standard