Manual de implementação

Como colocar a Plataforma Mobiliza no ar

Documento de implementação ponta a ponta: implantar um projeto novo, configurar o tenant, montar e publicar um curso, e a base técnica que sustenta tudo isso. Extraído do código-fonte real da plataforma.

PúblicoTime de implementação MC + técnico
EscopoMaster · Admin · Editor · Dev
Não cobreJornada do aluno (ver /guia no app)
BaseRepositório vsaraceni/escolascirculares
Começo

Visão geral da plataforma

A Mobiliza é uma aplicação multi-tenant: vários projetos (Mobiliza Curitiba, Semil Circular, etc.) compartilham o mesmo host, isolados por um slug. Cada projeto tem sua homepage pública, curso, gincana, premiação e marca própria — inclusive instalação como app no celular com nome e ícone do projeto.

A implementação se divide em quatro frentes, e este manual segue exatamente essa ordem:

BlocoQuem fazO que entrega
AMasterCria o projeto/tenant, vincula curso e cadastra editores
BAdmin do projetoConfigura branding, PWA, hierarquia, e-mail, gincana, relatórios
CMaster / EditorMonta, revisa e publica o curso; libera módulos por projeto
DTime técnicoArquitetura, banco, edge functions, deploy — e as regras que não podem ser quebradas
◆ Onde está a jornada do aluno

O lado do participante final (perfis, gamificação, como fazer o curso) já está documentado dentro do próprio app em /guia (src/pages/public/PlatformGuide.tsx). Este manual não repete esse conteúdo — o foco aqui é implantação e operação.

Começo

Papéis e permissões

São sete papéis no enum app_role. Vivem em src/contexts/AuthContext.tsx, ProtectedRoute.tsx, ProjectProtectedRoute.tsx e src/App.tsx.

PapelEscopoAcessa
masterGlobal (project_id = null)Tudo. Atalho de autorização em qualquer rota. /master/*
adminUm projeto/projeto/<slug>/admin/* — exige par project_admins + user_roles
editorGlobal, sem projeto/editor/cursos — catálogo e editor de curso
teacherParticipanteFaz curso/gincana (fora do escopo deste manual)
ped_coordinatorEscolaPainel consolidado da unidade
collection_coordinatorEscolaCoordena coleta
collection_operatorProjetoRegistra coleta em campo (precisa aprovação)
▲ Atenção · regra que pega todo mundo

O usuário precisa já existir (ter feito signup) antes de ser promovido a admin ou editor. A promoção só busca o perfil por e-mail e insere o papel — ela não cria conta. Se o e-mail não tiver perfil, em alguns fluxos a promoção é ignorada silenciosamente (ver Bloco A).

◆ Bom saber

Há uma "graça de hidratação" de 1,5 s antes de declarar acesso negado (a sessão é lida do localStorage de forma assíncrona). Telas de skeleton não são bloqueio — é a sessão carregando. /master/login redireciona para /login (legado): o login é único.

Bloco A · Implantar um tenant novo

Criar o projeto

Vive em src/components/master/CreateProjectForm.tsx (rota /master/projetos/novo).

✓ Pré-requisitos
  • Estar autenticado como master.
  • Para já vincular admins na criação, cada e-mail precisa ter conta cadastrada na plataforma (a busca é em profiles).
  1. No painel /master, clicar em "Criar novo projeto".
  2. Preencher os campos do formulário (detalhados abaixo).
  3. Clicar "Salvar projeto". O sistema insere em projects, vincula os admins existentes e redireciona para /master com toast mostrando o /<slug>.

Campos do formulário

Nome do projeto obrigatório
Gera o slug automaticamente (minúsculo, sem acentos, espaços viram -). A URL pública prevista aparece em tempo real.
Descrição opcional · texto curto
Logotipo opcional · PNG/SVG transparente
Validado por upload-validation.ts; sobe para o bucket project-assets em logos/<slug>.<ext>.
Status
Ativo ou Concluído (grava ativo/concluido).
Data de início / Conclusão estimada início default = hoje
Administradores
Digitar e-mail + + (ou Enter). Cada um precisa ter conta. Grava em user_roles (role admin + project_id) e project_admins.
▲ Armadilhas — Criar projeto
  • Slug duplicado: erro Postgres 23505 → toast "Já existe um projeto com esse slug". Nomes parecidos colidem.
  • Admin inexistente é ignorado sem aviso: se o e-mail não tiver perfil, o papel simplesmente não é inserido e nada avisa. Confira depois em ProjectDetails → aba Admins. O caminho de ProjectDetails dá erro explícito; prefira-o.
  • Upload de logo que falha não bloqueia a criação — projeto nasce com logo_url nulo.
  • O slug é imutável na prática: mudá-lo depois quebra URLs públicas, manifest PWA, QR codes de coleta e links já distribuídos. Trate nome/slug como definitivos após publicar.

Gerir o projeto e vincular o curso

Vive em src/pages/master/ProjectDetails.tsx (rota /master/projetos/:id), com abas:

  • Visão geral: seletor "Curso vinculado" — só cursos published aparecem; grava em projects.course_id. Botões de status e arquivar/desarquivar.
  • Redes / Escolas: mesma função do painel admin (Bloco B). Remover rede apaga as escolas em cascata.
  • Admins: adicionar por e-mail com feedback explícito ("Usuário não encontrado", "Já é administrador").
  • Parceiros: nome + ordem.
▲ Armadilha — Trocar curso vinculado

Trocar o curso vinculado de um projeto apaga automaticamente todos os overrides de liberação de módulo (project_module_overrides) do curso antigo. Se já havia um cronograma de liberação configurado, ele se perde na troca.

Cadastrar editores de curso

Vive em src/pages/master/EditorManagement.tsx (rota /master/editores).

  1. Em /master/editores, digitar o e-mail do usuário e clicar Adicionar.
  2. O sistema busca profiles por e-mail. Não achou → "Usuário não encontrado". Já é editor → "Este usuário já é editor".
  3. Sucesso: insere user_roles com role: 'editor' (sem project_id).
⚠ Editor é papel global

Um editor vê o catálogo de cursos inteiro, cria/edita/duplica qualquer curso, mas não exclui curso (só master) e não gera links de compartilhamento de preview (o RLS rejeitaria). Não há tela para promover alguém a master — isso é feito no banco.

Bloco B · Configurar o tenant

Configurações do projeto

Vive em src/pages/admin/AdminConfig.tsx (rota /projeto/<slug>/admin/config). Seis abas: Geral, Hierarquia, Homepage, Visual, Parceiros, KPIs.

⚠ Dois destinos de salvamento — entender antes de mexer
  • saveSection() → grava na tabela projects (cores, KPIs, hierarquia, flags de inscrição/coleta).
  • saveHomepageSection() → grava em project_homepage_settings (hero, rodapé, SEO, PWA, ordem de blocos).
  • Uploads de imagem persistem na hora ao subir o arquivo — não precisa clicar "Salvar" do card. Toggles de visibilidade também.

Aba Geral

Nome e Slug são somente leitura. Logotipo (sobe para <slug>/logo-<ts>.<ext>), Status, Camada de coleta (toggle que esconde Coletores/Coord. Coleta do menu quando off) e quatro toggles de "quem pode se inscrever pelo formulário público".

Aba Hierarquia

Renomeia rótulos sem mudar função: Nível 1 (Rede), Nível 2 (Escola), Papel A (Professor), B (Coord. Pedagógico), C (Coord. Coleta), D (Coletor) + a "área de atuação" do Papel A. Preview mostra o efeito no menu/cadastro.

Aba Visual

Cinco cores: Primária, Secundária, Destaque, Texto Principal, Header/Navbar. Preview ao vivo. Salvar com "Salvar cores".

Aba Parceiros

Nome, Categoria (Iniciativa, Realização, Co-realização, Patrocínio, Apoio, Parceiro), Site, logo. Reordena categorias (não parceiros individuais).

Aba KPIs

Metas (taxas de cadastro/início/conclusão, totais) e pesos de evidências de engajamento — esses pesos alimentam o ranking em Relatórios.

Aba Homepage

Cada card persiste em colunas de project_homepage_settings: ordem dos blocos, Hero (foto/mesh/static), overlay, rodapé (3 colunas), SEO/favicon/OG, e os campos PWA (próxima seção). Cada bloco tem toggle de visibilidade.

PWA dinâmico — instalação no celular

Card "App instalável (PWA)" em AdminConfig.tsx, mais src/lib/pwa-icons.ts e a edge function supabase/functions/project-manifest/index.ts.

Todos os projetos compartilham o host, mas cada um deve instalar no celular com a sua própria marca. O manifest.webmanifest é gerado dinamicamente pela edge function lendo quatro campos de project_homepage_settings.

  1. Nome completo (splash)pwa_name. Vazio = nome do projeto.
  2. Nome curto (homescreen)pwa_short_name. Máx. 12 caracteres.
  3. Cor do apppwa_theme_color. Botão "Usar cor do projeto" copia a primária.
  4. Ícone → PNG quadrado 512×512 ou maior. O upload gera 4 derivados via canvas (icon-192, icon-512, icon-maskable-512, apple-touch-180) com o mesmo timestamp.
  5. Clicar "Salvar configuração do app".
▲ Armadilha central — o padrão de nome do ícone

A edge function recebe só o pwa_icon_url (o 192) e deriva os outros por substituição de nome de arquivo (regex ^(.*)/icon-192-(\d+)\.png$icon-512-<ts>.png, icon-maskable-512-<ts>.png).

Nunca cole uma URL de ícone manualmente fora do padrão icon-192-<timestamp>.png. Se o nome não casar com o regex, o manifest fica só com o 192 (sem 512, sem maskable) e a instalação no Android sai com ícone borrado. Sempre use o upload da tela.

⚠ Outros pontos do PWA
  • Imagem < 512×512: sobe mas avisa (vira borrão). Sem ícone → manifest sem ícones (instalação genérica).
  • Cache do manifest é de 5 minutos. Trocar ícone/cor leva até 5 min para refletir no celular.
  • iOS não usa o manifest: precisa "Compartilhar → Adicionar à Tela de Início" manual. As meta tags Apple são injetadas e removidas ao sair do escopo do projeto.

Hierarquia rede → escola → pessoas

🌐 Plataforma (Master) └── 📁 Projeto (slug) ├── 🏛️ Rede de Ensino │ └── 🏫 Escola (unique_code + QR) │ ├── 👩‍🏫 Professores (Papel A) │ ├── 📋 Coord. Pedagógico (Papel B) │ └── 🚛 Coord. de Coleta (Papel C) ├── 📦 Coletores / Operadores (Papel D — nível projeto) └── ⚙️ Administradores

Redes e Escolas (AdminSchools.tsx): cadastro manual ou import CSV (botão "Modelo CSV"). Cada escola gera unique_code e dispara geocodificação assíncrona.

Professores / Coordenadores (AdminTeachers, AdminCoordPedagogico, AdminCoordColeta): nome, e-mail, escola. Coord. de Coleta tem checkbox "Mesmo que o Coord. Pedagógico".

Coletores e aprovação (AdminColetores.tsx): entram como pendente. Ao aprovar, dispara automaticamente um magic link para o e-mail apontando para /projeto/<slug>/operador. Botão "Identificação" gera cartão imprimível com QR code real por escola.

▲ Armadilhas — Hierarquia
  • Botão "Adicionar escola" fica desabilitado se não houver nenhuma rede. Ordem obrigatória: rede primeiro.
  • No import CSV de escolas, se nome_rede não casar com nenhuma rede existente, a escola vai para a primeira rede do projeto (fallback silencioso). Confira os vínculos depois.
  • Erro de magic link na aprovação do coletor é engolido (só console.warn). Se o coletor não receber, a aprovação foi gravada mesmo assim — reaprovar ou reenviar por campanha.
  • Camada de coleta desativada bloqueia as telas de coletor/coord. coleta inteiras.

Campanhas e régua de e-mail

Campanhas pontuais (AdminEmailCampaign.tsx): escolher grupos destinatários, escrever (ou gerar com IA), enviar agora ou agendar. Variáveis {{nome}}, {{escola}}, {{link_curso}} etc.

Régua automática (AdminEmailAutomation.tsx): réguas por persona com mensagens em sequência (atraso em dias ou vinculado a etapa do projeto). "Log de Envios" lê email_automation_logs (fonte de verdade).

⚠ Histórico da campanha é parcialmente mock

O histórico exibido em AdminEmailCampaign tem itens seedados hardcoded e vive em estado de componente — não é leitura fiel de email_sends. Para auditar envios automáticos, use o "Log de Envios" da régua. Alterações na régua só vão pro banco ao clicar "Salvar Réguas" (inclusive após "Restaurar padrões").

Gincana e premiação

Gincana (AdminGincana.tsx): configurar (ativa, datas, pontos por kg coletado), criar missões (título, categoria, pontos, multiplicador, exigências de relato/foto/link) e aprovar/rejeitar submissões com feedback ao participante. Existe uma missão de sistema "Coleta" não-excluível.

Premiação (AdminPremiacao.tsx): liberar/revogar certificados (personalizáveis: assinatura, logo parceiro, cidade), troféus (pódio + menções honrosas) e relatório consolidado.

▲ Armadilha — Pontos por kg

O ranking combina pontos de missão (× multiplicador) + pontos de coleta (kg × fator). Mudar "Pontos por kg" nas configurações da Gincana recalcula o ranking inteiro retroativamente.

Dashboard e relatórios

Dashboard (AdminDashboard.tsx): três camadas — Conversão, Engajamento, Alertas de intervenção. Cada alerta tem botão "Enviar e-mail" que pré-seleciona grupo e filtro.

Relatórios (AdminRelatorios.tsx): três abas (Curso, Engajamento, Coleta), cada uma com Exportar CSV (BOM UTF-8).

Relatório WhatsApp (AdminWhatsAppReport.tsx): template com variáveis resolvidas por dados reais, "Gerar hoje" + "Copiar".

⚠ Templates de WhatsApp não persistem

Vivem só em estado de componente — recarregar perde templates customizados; o padrão é re-seedado a cada render. Use para gerar-e-copiar, não como repositório.

Editor de curso

Vive em src/pages/master/CourseEditor.tsx (rota /<base>/cursos/:courseId).

✓ Pré-requisito

O curso só recebe módulos/aulas depois de criado — a aba "Conteúdo" só aparece quando não é novo.

  1. Aba Informações: Nome (obrigatório), Descrição, Carga horária (default 40h), Nota mínima (default 70%). "Criar curso" insere em courses como draft.
  2. Aba Conteúdo: "Novo módulo" (só nome). Dentro do módulo, "Adicionar aula".
  3. Aula: Título + tipo de conteúdo (Vídeo URL, Texto rich, Imagem, Galeria até 20, PDF legado) + duração opcional.
  4. Perguntas da aula: 1+, cada uma com tipo, enunciado e pontos.

Tipos de pergunta suportados

TipoComo funciona
objetiva2–5 alternativas, marca a correta
dissertativaAvaliada por IA (edge evaluate-answer); dica opcional
ordenacaoItens na ordem correta (até 8)
associacaoPares conceito → definição (até 8)
lacunasTexto com ___ + respostas + banco de palavras
verdadeiro_falsoResposta correta; se "Falso", opções de correção
escala_likertAutoavaliação 1–5, sem certo/errado
▲ Armadilhas — Editor de curso
  • Perguntas são apagadas e reinseridas a cada save de aula: editar uma aula faz DELETE de todas as lesson_questions dela e recria do formulário. Não há edição in-place — IDs antigos das perguntas se perdem todo save.
  • Só perguntas com enunciado preenchido persistem; vazias são descartadas (badge ⚠️).
  • Auto-correção de tipo: se há imagens carregadas mas o tipo não é image/carousel, o save força image/carousel. Sair desses tipos com imagens pede confirmação ("vai descartar N imagens").
  • Curso publicado em uso é editado ao vivo: alerta âmbar mostra os projetos afetados. Editar muda o conteúdo para todos os participantes em andamento.

Preview e revisão pedagógica externa

Dois caminhos, mesma UI (player em previewModenão grava progresso/XP/streak, não afeta ranking):

  1. Preview autenticado: botão "Pré-visualizar curso" no editor → /cursos/:id/preview (master/editor).
  2. Preview público compartilhável: botão "Compartilhar" (só master). Define rótulo + validade, gera token em course_preview_tokens e copia o link. A rota /cursos/:id/preview/:token é pública; a edge function course-preview valida server-side com service_role e devolve o curso inteiro.
◆ Bom saber

Tokens são por curso (URL com token de outro curso → course_id_mismatch). O preview público funciona com curso em draft (é justamente para revisão pré-publicação). Revogar (revoked_at) tira o acesso na hora. Erros para o revisor são genéricos (não revelam se o token existiu).

Liberação de módulos por projeto

Vive em src/pages/admin/AdminCurso.tsx e AdminCursoLiberacao.tsx. Nota: AdminModulos.tsx gerencia etapas/fases do projeto (project_stages), não módulos de curso.

Quem vincula o curso ao projeto é o master (Bloco A). O admin do projeto controla, por módulo: Liberar, Bloquear ou Programar liberação (data + hora) — grava em project_module_overrides. Ações em massa incluem "Cronograma sequencial" (data inicial + intervalo).

▲ Armadilhas — Liberação
  • A liberação é isolada por projeto: o mesmo curso publicado pode ter calendários diferentes em projetos diferentes.
  • O cronograma sequencial substitui programações anteriores de todos os módulos. Horário no fuso do navegador.
  • Módulo bloqueado trava a conclusão do curso para o participante. Checklist de fim de projeto deve incluir "liberar módulos pendentes".
Bloco D · Base técnica

Arquitetura

SPA Vite + React 18 + TypeScript, Tailwind/shadcn no front, Supabase como backend completo (Postgres + Auth + Storage + Edge Functions Deno). Deploy gerido pelo Lovable.

  • Cliente Supabase (src/integrations/supabase/client.ts): único, tipado por types.ts (gerado — nunca editar à mão; o Lovable regenera após cada migration). Usa a anon/publishable key — o isolamento é todo via RLS.
  • AuthContext: fonte de verdade de identidade e papéis. Master tem project_id = null e acesso global. Fallback via RPC is_master.
  • GamificationContext: estado no localStorage namespaced por slug e resetado na troca de projeto — a gamificação nunca vaza entre tenants.
  • Guardas de rota: ProtectedRoute (auth + papel global, tri-state com grace de 1,5 s) e ProjectProtectedRoute (resolve slug→id e escopa o papel ao tenant).
ÁreaRota baseGuarda
Master/masterProtectedRoute · master
Editor/editorProtectedRoute · editor
Preview por token/cursos/:id/preview/:tokensem guarda (valida server-side)
Admin do projeto/projeto/:slug/adminProjectProtectedRoute · admin
Públicas/projeto/:slug, /guianenhuma

Banco de dados e migrations

São 101 migrations em supabase/migrations/. As escritas à mão recebem nome semântico e um *.README.md companheiro com prompt, validação e rollback.

auth.users ──trigger──▶ profiles (1:1) user_roles (role + project_id; NULL = master) │ ▼ projects ── slug UNIQUE (raiz multi-tenant) ┌────────────┬────────────┼─────────────┬──────────────┐ teaching_ schools course_modules gincana_ collection_ networks │ (course_id) missions operators ▼ ▼ ▼ ▼ teachers/ course_lessons mission_ collection_ coords ▼ submissions records lesson_questions ▼ teacher_course_progress courses (catálogo global) ◀── projects.course_id project_homepage_settings (1:1, por slug · homepage + SEO + PWA)

RLS habilitado em todas as tabelas de domínio. Padrão de policy: master faz tudo (is_master), admin gere o próprio projeto (is_project_admin), participante vê o que é do seu projeto (get_user_project_ids), dono vê o próprio registro. Funções de segurança são SECURITY DEFINER para evitar recursão de RLS.

Migration destacada — operator_security_hardening (20260513100000)

Fechou dois gaps cross-tenant: (1) operador aprovado no projeto A podia gravar coleta com escola do projeto B — nova policy valida que project_id/school_id da linha batem com o vínculo do operador; (2) bucket Storage project-assets aberto a qualquer authenticated — nova policy extrai project_id do path (storage.foldername(name)[1]) e exige vínculo. Idempotente.

▲ REGRA CRÍTICA — Migration não é automática no merge

Push de um .sql em supabase/migrations/ NÃO altera o banco de produção. A migration só roda quando é pedida explicitamente em prompt ao Lovable. Padrão obrigatório: migration idempotente + *.README.md com (1) prompt pronto pro Lovable, (2) SQL de validação em information_schema, (3) SQL de rollback. Pedir, no mesmo prompt, a regeneração de types.ts.

Edge functions

10 funções Deno + _shared/. verify_jwt = false declarado para as públicas (auth-email-hook, evaluate-answer, geocode-address, course-preview, project-manifest) — elas fazem a própria validação.

FunçãoPropósitoTrigger
project-manifestManifest PWA dinâmico por slugGET ?slug=
enroll-participantMatrícula pública; cria auth user + role; re-valida flags server-sidePOST do form
evaluate-answerCorrige dissertativa via AI Gateway LovablePOST do player
course-previewPreview por token (service_role, valida tudo)POST público
auth-email-hookRenderiza e envia e-mails de auth com marca do projetoWebhook assinado
process-email-queueWorker pgmq → envia (anti-duplicata + rate-limit)pg_cron
process-email-automationsDispara a régua por projetopg_cron diário
send-project-emailEnfileira (não envia direto)POST interno
generate-whatsapp-messageGera texto WhatsApp por IAPOST admin
geocode-addressLat/long de escolas (Nominatim)POST
⚠ Secrets das edge functions

Vivem só como secrets no Supabase, nunca no repo: SUPABASE_SERVICE_ROLE_KEY (bypassa RLS — crítico), LOVABLE_API_KEY (AI Gateway/e-mail/webhook), SUPABASE_URL, opcional LOVABLE_SEND_URL. Envio de e-mail é assíncrono via pgmq: produtores enfileiram, só process-email-queue envia. Não chamar envio direto de fluxos novos.

Deploy e ambiente

Lovable (editor) ──prompt──▶ commit automático ──▶ GitHub repo Dev local ──git push──▶ GitHub repo ──▶ reflete no Lovable │ ▼ Build & publish (Share → Publish) preview: escolascirculares.lovable.app prod: escolas.movimentocircular.io

Lovable e o repo são bidirecionais. .env commitado só tem chave pública (anon key — isolamento é via RLS); os segredos reais ficam só nos secrets das edge functions.

git clone <git-url>
cd escolascirculares
npm i
npm run dev      # Vite em http://localhost:8080

O front local aponta para o Supabase de produção (não há Supabase local no repo). Edge functions em dev usam as já deployadas em produção.

▲ O descompasso mais perigoso

Código de app deploya automaticamente; schema de banco NÃO. Sempre que uma feature depender de coluna/tabela nova, o deploy do código vai ao ar antes da migration — por isso o código faz casts defensivos (ex.: SELECT * em vez de campos pwa_*). Coordene: peça a migration ao Lovable antes ou junto de subir o código que depende dela.

Regras críticas de operação

  1. Migration não é automática no merge. Sempre prompt explícito ao Lovable + README idempotente com validação e rollback + regenerar types.ts.
  2. Segredos. service_role e LOVABLE_API_KEY só como secrets do Supabase. Endpoint novo com service_role precisa validar o tenant manualmente.
  3. Multi-tenant por slug. projects.slug é a raiz do isolamento. Auditar policies novas contra escrita/leitura cross-tenant — inclusive Storage (path …/{project_id}/…).
  4. Padrão do ícone PWA. pwa_icon_url deve apontar para …/icon-192-<ts>.png. Nunca colar URL fora do padrão.
  5. Filas de e-mail. Produtores enfileiram; só o worker cron envia. Respeitar anti-duplicata e cooldown.
  6. verify_jwt por função. Função pública nova decide o verify_jwt conscientemente e implementa validação própria.
  7. course_modules.course_id é o link canônico curso↔módulo; project_id ali é legado — não usar para lógica nova.
Anexo

Mapa arquivo → função

TemaArquivo
Criar projetosrc/components/master/CreateProjectForm.tsx
Detalhe/edição projeto, vincular cursosrc/pages/master/ProjectDetails.tsx, useProjectDetail.ts
Cadastro de editoressrc/pages/master/EditorManagement.tsx
Papéis/permissõesAuthContext.tsx, ProtectedRoute.tsx, ProjectProtectedRoute.tsx
Config do tenantsrc/pages/admin/AdminConfig.tsx
Manifest PWAsupabase/functions/project-manifest/index.ts, src/lib/pwa-icons.ts
Redes/escolas/pessoasAdminSchools, AdminTeachers, AdminColetores
E-mailAdminEmailCampaign.tsx, AdminEmailAutomation.tsx
Gincana/premiaçãoAdminGincana.tsx, AdminPremiacao.tsx
Editor/catálogo de cursoCourseEditor.tsx, CourseCatalog.tsx
Preview de cursoCoursePreview.tsx, supabase/functions/course-preview/
Liberação de móduloAdminCurso.tsx, AdminCursoLiberacao.tsx