Skip to content

Formação — Roadmap de PRs (re-arquitetura)

Plano aprovado. Cada PR é TDD-first (Red → Green → Refactor). PRs posteriores não começam antes do anterior estar verde.

✅ PR-1 — Schema + RLS Least Privilege (CONCLUÍDO)

  • curso_modulos, trilhas, trilha_cursos.
  • cursos.imagem_url, cursos.status, cursos.video_intro_url.
  • cursos_videos.modulo_id (NOT NULL, FK RESTRICT, UNIQUE(modulo_id, ordem)).
  • Trigger fn_validar_status_curso.
  • Bucket privado cursos-imagens + policies.
  • Backfill atômico: 7 cursos → 7 módulos "Módulo 01", 0 órfãos.
  • RLS ADR-018: especialista CRUD, coordenador SELECT, admin negado.
  • <FeatureGate bypassAdmin> prop.
  • Testes Red: tests/security/formacao-rls.test.ts, src/__tests__/feature-gate-admin-bypass.test.tsx.

✅ PR-2 — Edge Functions + Hooks (CONCLUÍDO)

  • Novas: especialista-modulos, especialista-trilhas, cursos-imagens-signed-url.
  • Estendidas: especialista-cursos (imagem/status/video_intro), especialista-curso-videos (modulo_id), coordenador-videos (list_curso_completo).
  • Helper _shared/cursos-imagens.ts (validação path + TTL clamp).
  • Parser tests dual-format (ADR-003) para cada function nova.
  • Hooks frontend: useModulos, useTrilhas, useCursoCompleto, useCursoImagemSignedUrl.
  • config.toml atualizado.
  • Docs: ADR-018, SCHEMA.md, EDGE_FUNCTIONS.md.

✅ PR-3 — UI Especialista (management) (CONCLUÍDO)

  • FormacaoTabs (src/components/formacao/formacao-tabs.tsx) substitui CursosEspecialista direto no App.tsx, alternando entre Cursos e Trilhas.
  • ModulosManager (Sheet) com CRUD + reorder ↑/↓ por curso. Acessado via novo botão Layers em cada card de curso (Especialista).
  • TrilhasManager (global, sem curso_id) com CRUD, ativo/inativo, reorder e attach/detach de cursos.
  • Upload de imagem de curso: hook useCursoImagemUpload valida MIME (jpeg|png|webp) e tamanho (≤5 MB) fail-fast antes de tocar storage, escreve no bucket privado cursos-imagens em {cursoId}/{ts}-{rand}.{ext} e persiste via edge especialista-cursos/upload_imagem.
  • TDD primeiro: 22 testes verdes cobrindo hooks (useModulos, useTrilhas, useCursoImagemUpload) e componentes (ModulosManager, TrilhasManager) — src/hooks/formacao/__tests__/ e src/components/formacao/__tests__/.
  • ADR-018 (Least Privilege) mantido: Especialista CRUD pleno, Coordenador read-only, Admin sem acesso (FeatureGate bypassAdmin={false}).

✅ PR-4 — UI Coordenador: Grid de cursos + sidebar Trilhas (CONCLUÍDO)

  • Edge (coordenador-videos):
    • fetchCursosComStats agora aplica .in("status", ["publicado","em_breve"]) — cursos rascunho nunca chegam ao frontend (defense-in-depth além da RLS).
    • Filtro pós-agregação mantém em_breve mesmo sem vídeos (estado de "coming soon"), e exige ≥1 vídeo publicado para publicado.
    • Nova action list_trilhas retorna trilhas ativas + cursos[] (já podados de rascunho/inativos), respeitando a RLS trilhas_select_especialista_coordenador. Coordenador não chama especialista-trilhas (ADR-018).
  • Hooks: novo useTrilhasCoord (read-only). CursoCoord ganhou status? e imagem_url? para refletir o payload do edge.
  • UI (src/components/formacao/coordenador/):
    • CursosGrid — extraído de videos/index.tsx, com:
      • filtro por searchTerm e trilhaFilterIds (mantém ordem da trilha);
      • badge Em breve + aria-disabled que bloqueia onClick;
      • barra de progresso só renderiza se percentual_concluido > 0;
      • estados vazios distintos (busca / trilha sem cursos / sem cursos publicados).
    • TrilhasSidebar — lista apenas ativo===true, ordenada por ordem; clique toggle (clicar a selecionada limpa o filtro); botão "Limpar" aparece só com seleção.
    • CursoThumbnail — usa signed URL do bucket privado quando imagem_url existe; fallback para ícone/cor de categoria_visual. Não chama o hook quando imagem_url é nulo (evita 4xx ruidoso).
  • Refactor: src/components/videos/index.tsx passou de 246 → ~170 linhas e agora delega a render dos cards/sidebar de trilhas aos novos componentes. Sidebar de destaques mantida intacta.
  • TDD (17 testes verdes):
    • src/hooks/formacao/__tests__/useTrilhasCoord.test.tsx (2)
    • src/components/formacao/coordenador/__tests__/CursosGrid.test.tsx (8)
    • src/components/formacao/coordenador/__tests__/TrilhasSidebar.test.tsx (5)
    • src/components/formacao/coordenador/__tests__/CursoThumbnail.test.tsx (2)
  • ADR-018 mantido: Coordenador continua read-only; toda escrita de trilhas/módulos passa por especialista-*.

✅ PR-5 — UI Coordenador: Detalhe do curso (acordeão de módulos + video_intro) (CONCLUÍDO)

  • Refator de fluxo (src/components/videos/index.tsx): removido obterCurso legado do caminho de detalhe; agora usa selectedCursoId + useCursoCompleto (action list_curso_completo, já com filtro de rascunho/aulas não publicadas no backend). Loading e erro têm estados dedicados (Anti-Flash). Player ainda usa pipeline legado (obterCurso + obterVideo) só no momento do clique na aula — PR-6 unifica.
  • Componentes novos em src/components/formacao/coordenador/:
    • CursoDetalhe.tsx — header com CursoThumbnail (signed URL bucket privado) + totais (Layers/Play/Clock), badge Em breve, busca local de aulas e acordeão Radix.
    • CursoIntroVideo.tsx — embed YouTube/Vimeo via src/lib/video-utils.ts (detectVideoPlatform + getEmbedUrl). URL nula ou desconhecida → retorna null (sem throw).
    • ModuloAccordionItem.tsx — header com contagem de aulas, lista ordenada por aula.ordem. readOnly (status em_breve) desabilita clique mas mantém visibilidade da estrutura.
  • Helpers puros (helpers.ts): totaisCursoCompleto e filtrarModulosPorTermo (case-insensitive, sem mutar input, oculta módulos vazios).
  • Fixture fxCursoCompleto/fxCursoCompletoEmBreve em src/test/fixtures/formacao.ts (2 módulos × 4 aulas, intro YouTube).
  • Componente legado removido: src/components/videos/formacao-curso-detalhe.tsx deletado; nenhuma referência remanescente (rg clean).
  • ADR-018 mantido: zero mutações; Coordenador consome useCursoCompleto + useCursoImagemSignedUrl (read-only).
  • TDD (18 testes verdes nesta PR; 33 verdes no diretório coordenador):
    • __tests__/helpers.test.ts (6)
    • __tests__/CursoIntroVideo.test.tsx (4)
    • __tests__/CursoDetalhe.test.tsx (8) — cobre header/totais, intro presente/ausente, busca + filtro + estado vazio, click publicado vs em_breve (read-only), botão voltar.

✅ PR-6 — UI Coordenador: Player com sidebar module-aware (CONCLUÍDO)

  • Player legado removido (src/components/videos/formacao-video-player.tsx, 216 linhas, lista flat). Fluxo Detalhe → Player agora 100% sob useCursoCompleto.
  • Novo src/components/formacao/coordenador/CursoPlayer.tsx:
    • Embed YouTube/Vimeo (detectVideoPlatform + getEmbedUrl); fallback explícito (data-testid="video-fallback") para URL desconhecida ao invés de carregar <iframe> quebrado.
    • Sidebar = acordeão Radix de módulos (ordem); o módulo da aula atual abre por padrão (helper encontrarModuloDaAula).
    • Aula atual destacada (data-active, aria-current) e não-clicável; outras disparam onAulaChange(aula).
    • Busca local reusa filtrarModulosPorTermo (PR-5) — módulos vazios somem.
    • status === 'em_breve' propaga readOnly para todas as aulas (Least Privilege visual).
    • Detalhes / Materiais / Avaliar / Marcar concluído — mesma UX do legado, callbacks delegados ao container.
  • helpers.ts ganhou três funções puras:
    • encontrarModuloDaAula(curso, aulaId)
    • proximaAula(curso, aulaId) — pronta para auto-avanço (PR futura)
    • mergeAulaDetalhada(resumo, detalhe) — adapter isolando drift entre AulaResumo e VideoDetalhado
  • src/components/videos/index.tsx:
    • useCursoCompleto agora ativo em course-detail e video-player (mesma queryKey → cache compartilhado, sem refetch).
    • handleAulaClick deixou de chamar obterCurso (cursoCompleto já está em cache); obterVideo continua para hidratar materiais/avaliacao_usuario/detalhes.
    • Estados Loading/Error dedicados no player (Anti-Flash + ADR-018).
    • Removidos handleVideoClick, cursoAtual, buscarDestaques (unused após refator).
  • ADR-018 mantido: zero chamadas a especialista-*; Coordenador só consome.
  • TDD (12 testes novos verdes; 50/50 no diretório coordenador):
    • helpers.test.ts +5 testes (encontrarModuloDaAula ×2, proximaAula ×4, mergeAulaDetalhada ×1)
    • CursoPlayer.test.tsx +10 testes (iframe vs fallback, sidebar module-aware, aula ativa não-clicável, em_breve readOnly, busca + estado vazio, voltar, concluir, avaliar)

✅ PR-6.5 — Hardening de contratos (Edge Functions)

  • extractParams(body) = body.params ?? body aplicado em especialista-cursos, especialista-curso-videos, coordenador-videos.
  • Schemas Zod exportados (CreateCurso/UpdateCurso/UploadImagem/UpdateStatus/CreateVideo/UpdateVideo/ListCursoCompleto/ListTrilhas).
  • 43 testes Deno de parser puros — happy path + edge cases + regressão Caso 21 (fatura_id: Required).
  • Auditoria em docs/development/AUDIT.md.

✅ PR-7 — Documentação final + auditoria de fechamento

  • Novo doc UI.md: SSOT da camada de apresentação (componentes, hooks, queryKeys, guardrails ADR-018).
  • SCHEMA.md consolidou seção Constraints & integridade validada via pg_constraint em produção (FKs CASCADE/RESTRICT, UNIQUEs por módulo, CHECKs de domínio).
  • EDGE_FUNCTIONS.md ganhou matriz Action × Zod schema + nota ADR-003.
  • Sidebar VitePress (docs/.vitepress/config.ts) referenciando UI.md.
  • Suíte regressão Formação: 74/74 verdes (bunx vitest run src/components/formacao src/hooks/formacao) + 43/43 parser tests Deno.
  • Auditoria de fechamento registrada em docs/development/AUDIT.md.

Estado final: re-arquitetura Formação encerrada. ADRs envolvidas: 003 (payload dual), 007 (queryKey por escola/curso), 010 (Anti-Flash), 018 (Least Privilege).