🪼 🪼 🪼
Mascote do MEDUSA

🪼 MEDUSA

Provisionador e preenchedor de tenants do Vitrine Pro — do banco vazio ao tenant no ar na AWS com tema, capas, cursos, aulas e DNS, em um comando.
vitrine/medusa · Node 18+ · CLI commander · internal: true

🚀 Fluxo principal medusa full-tenant --slug X --name "X" --deploy --dns
Orquestração ponta a ponta dirigida por um JSON único (tenants/<slug>.json: tema, assets, cursos, aulas). Cada etapa é pulável (--skip-*) para retomar após falha parcial sem repetir trabalho.
🏗️1/5

Provisionamento local + Neon

Cria o tenant do zero nos dois mundos, reaproveitando o Artisan do vtp-webservices — nada é reimplementado em JS.

  • Local (Docker): php artisan tenant:provision-local → banco vtp_{slug}, migrations, seed e domínio api-{slug}.vitrinepro.local
  • Remoto (Neon): cria o banco no projeto compartilhado VitrinePro via Management API, depois migrate + db:seed contra ele (env override)
  • Runbook impresso: o que fica de fora de propósito — stack AWS, custom domain na API Gateway, CNAME, gate
🎨2/5

Publicação do tema PUT /admin/theme

Monta o payload a partir do bloco theme do JSON e publica no tenant local.

  • Tokens de marca: colors, radius, flags + components
  • base_preset opcional (ex. clean-light) herda o semantic do catálogo — foi assim que a morgana virou o 1º tenant de tema claro
  • Contrato exige base_preset_version quando há preset (catálogo todo na v1)
🖼️3/5

Geração de assets asset-gen.mjs

Gera capa por capa os slots do bloco assets (capas de curso, hero, avisos, logo…), sobe via POST /file/{type} e grava as keys no manifest {slug}.assets-manifest.json.

📁
Mídia local
TENANT_MEDIA_DIR/{slug}/{slot}.png — sempre vence
🎭
Inspiração
pool livre inspiration/ — Gemini escolhe e recorta 16:9, ou ignora
📷
Stock (Pexels)
--stock, foto real licença livre
🤖
IA generativa
Gemini/Imagen 4 ou Leonardo.ai (--provider)
  • Slots raw_prompt (logo) e no_stock nunca vêm de banco de imagem — nem do pool de inspiração
  • Inspiração: imagens de nome livre em inspiration/; matching numa chamada só de visão (gemini-2.5-flash) + crop por saliência (sharp); falha no matching não derruba o run

🔁 [2b] Republica o tema com logo

Se o JSON aponta theme.logo_asset (convenção: slot brand_emblem), o tema é republicado com a logo_key recém-gerada no manifest.

🌱4/5

Semeadura de conteúdo tenant:seed-content

docker exec -e DB_DATABASE=vtp_{slug} … php artisan tenant:seed-content {slug}

  • Categorias, cursos, módulos e aulas descritos no JSON viram registros reais
  • Aulas com vídeo do YouTube (video_host=youtube) — padrão Cozy Videos, sem passar pelo video-processor
  • Curso associado a categoria já aparece nas seções da Home automaticamente
☁️5/5

Deploy AWS opt-in --deploy

deploy-tenant-stack.sh {slug} — backend e frontend do tenant sobem num comando.

  • Backend: stack Lambda dedicado (Bref/serverless), 1 stack por tenant
  • PWA: build do vtp-app-web → S3 + CloudFront; ícones por tenant se existir {slug}.pwa-icons/
  • Se api-{slug}.aws ainda não resolve, builda com APP_URL = URL crua do API Gateway (evita links NXDOMAIN); volta ao host bonito no próximo deploy
🌐6

DNS no registro.br opt-in --dns

Por último de propósito: o CNAME da API exige o custom domain na API Gateway, e o da PWA exige o CloudFront do deploy. Não existe API de zona no registro.br — o medusa invoca o vtp-dns-configurator (Puppeteer humanizado no painel web, idempotente, aborta em conflito).

  • api-{slug}.aws → alvo descoberto via aws apigatewayv2 get-domain-name (regionalDomainName)
  • app-{slug}.aws → alvo descoberto pelo alias na distribuição CloudFront; pulado sem falhar se a PWA não foi deployada
🧰 Comandos avulsos
Cada peça do pipeline também roda sozinha — contra tenant local ou remoto (--tenant-url).

🔑 login

Testa as credenciais admin (POST /api/v1/admin/login, guard custom, sem 2FA) e cacheia o token bearer em ~/.config/vtp-tenant-filler, com refresh automático.

🔎 audit

Diagnóstico: mostra o que falta pro tenant ficar apresentável — cursos sem capa (heurística placeholder/robot), categorias vazias, hub ausente.

📚 populate

Cria categoria + curso + módulo + aulas a partir de um --topic: busca vídeos reais no YouTube (embeddable + público), gera a capa via Imagen e opcionalmente associa um hub.

🖌️ fill-images

Só gera capas pros cursos que ainda estão com o placeholder ("robô"), sem mexer no resto. Faz round-trip completo no PUT pra não esbarrar nas validações.

🎬 attach-videos

Vídeo local em videos/{slug-da-aula}.mp4 sobe pro PandaVideo via TUS e vence até YouTube já aplicado — mas nunca re-sobe por cima de Panda (idempotente, sem custo duplicado).

🌐 dns / 🏗️ provision

As etapas 6 e 1 do pipeline, avulsas: CNAME individual (--app pra PWA) e provisionamento local+Neon com runbook do restante.

🔌 Integrações externas
Tudo isolado em módulos próprios (src/*.js), injetável e mockável nos testes (vitest, 117+ testes).
🐘 Neon · Management API — banco vtp_{slug} no projeto compartilhado
🐳 Docker + Artisan · provision-local, migrate, seed-content
Gemini / Imagen 4 · capas e logos por prompt
🎨 Leonardo.ai · provider alternativo (job assíncrono, poll até COMPLETE)
📷 Pexels · fotos stock de licença livre (--stock)
▶️ YouTube Data API v3 · vídeos reais pras aulas
🐼 PandaVideo · upload TUS de vídeo local por aula
☁️ AWS · Lambda · API Gateway · S3 · CloudFront (deploy + descoberta de alvos DNS)
🇧🇷 registro.br · via vtp-dns-configurator (Puppeteer humanizado)
⚠️ Fora do escopo (de propósito): Highlights/Top 10 (dependem de engagement real), contas admin com 2FA, e a variante iOS do tenant (Tuist em vtp-ios/apps/<Nome>/ — passo manual do Bruno). O provision nunca cria recursos pagos sozinho: deploy e DNS são sempre opt-in explícito.