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.tomlatualizado.- Docs: ADR-018,
SCHEMA.md,EDGE_FUNCTIONS.md.
✅ PR-3 — UI Especialista (management) (CONCLUÍDO)
FormacaoTabs(src/components/formacao/formacao-tabs.tsx) substituiCursosEspecialistadireto noApp.tsx, alternando entre Cursos e Trilhas.ModulosManager(Sheet) com CRUD + reorder ↑/↓ por curso. Acessado via novo botãoLayersem cada card de curso (Especialista).TrilhasManager(global, semcurso_id) com CRUD, ativo/inativo, reorder e attach/detach de cursos.- Upload de imagem de curso: hook
useCursoImagemUploadvalida MIME (jpeg|png|webp) e tamanho (≤5 MB) fail-fast antes de tocar storage, escreve no bucket privadocursos-imagensem{cursoId}/{ts}-{rand}.{ext}e persiste via edgeespecialista-cursos/upload_imagem. - TDD primeiro: 22 testes verdes cobrindo hooks (
useModulos,useTrilhas,useCursoImagemUpload) e componentes (ModulosManager,TrilhasManager) —src/hooks/formacao/__tests__/esrc/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):fetchCursosComStatsagora aplica.in("status", ["publicado","em_breve"])— cursosrascunhonunca chegam ao frontend (defense-in-depth além da RLS).- Filtro pós-agregação mantém
em_brevemesmo sem vídeos (estado de "coming soon"), e exige ≥1 vídeo publicado parapublicado. - Nova action
list_trilhasretorna trilhas ativas +cursos[](já podados derascunho/inativos), respeitando a RLStrilhas_select_especialista_coordenador. Coordenador não chamaespecialista-trilhas(ADR-018).
- Hooks: novo
useTrilhasCoord(read-only).CursoCoordganhoustatus?eimagem_url?para refletir o payload do edge. - UI (
src/components/formacao/coordenador/):CursosGrid— extraído devideos/index.tsx, com:- filtro por
searchTermetrilhaFilterIds(mantém ordem da trilha); - badge
Em breve+aria-disabledque bloqueiaonClick; - barra de progresso só renderiza se
percentual_concluido > 0; - estados vazios distintos (busca / trilha sem cursos / sem cursos publicados).
- filtro por
TrilhasSidebar— lista apenasativo===true, ordenada porordem; clique toggle (clicar a selecionada limpa o filtro); botão "Limpar" aparece só com seleção.CursoThumbnail— usa signed URL do bucket privado quandoimagem_urlexiste; fallback para ícone/cor decategoria_visual. Não chama o hook quandoimagem_urlé nulo (evita 4xx ruidoso).
- Refactor:
src/components/videos/index.tsxpassou 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): removidoobterCursolegado do caminho de detalhe; agora usaselectedCursoId+useCursoCompleto(actionlist_curso_completo, já com filtro derascunho/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 comCursoThumbnail(signed URL bucket privado) + totais (Layers/Play/Clock), badgeEm breve, busca local de aulas e acordeão Radix.CursoIntroVideo.tsx— embed YouTube/Vimeo viasrc/lib/video-utils.ts(detectVideoPlatform+getEmbedUrl). URL nula ou desconhecida → retornanull(sem throw).ModuloAccordionItem.tsx— header com contagem de aulas, lista ordenada poraula.ordem.readOnly(statusem_breve) desabilita clique mas mantém visibilidade da estrutura.
- Helpers puros (
helpers.ts):totaisCursoCompletoefiltrarModulosPorTermo(case-insensitive, sem mutar input, oculta módulos vazios). - Fixture
fxCursoCompleto/fxCursoCompletoEmBreveemsrc/test/fixtures/formacao.ts(2 módulos × 4 aulas, intro YouTube). - Componente legado removido:
src/components/videos/formacao-curso-detalhe.tsxdeletado; nenhuma referência remanescente (rgclean). - 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 vsem_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% sobuseCursoCompleto. - 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 disparamonAulaChange(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.
- Embed YouTube/Vimeo (
helpers.tsganhou três funções puras:encontrarModuloDaAula(curso, aulaId)proximaAula(curso, aulaId)— pronta para auto-avanço (PR futura)mergeAulaDetalhada(resumo, detalhe)— adapter isolando drift entreAulaResumoeVideoDetalhado
src/components/videos/index.tsx:useCursoCompletoagora ativo emcourse-detailevideo-player(mesma queryKey → cache compartilhado, sem refetch).handleAulaClickdeixou de chamarobterCurso(cursoCompleto já está em cache);obterVideocontinua para hidratarmateriais/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 ?? bodyaplicado emespecialista-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.mdconsolidou seção Constraints & integridade validada viapg_constraintem produção (FKs CASCADE/RESTRICT, UNIQUEs por módulo, CHECKs de domínio).EDGE_FUNCTIONS.mdganhou matriz Action × Zod schema + nota ADR-003.- Sidebar VitePress (
docs/.vitepress/config.ts) referenciandoUI.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).