2026-06-02_cadencia-aprovou-e-nao-gerou-4-causas-raiz-convergentes¶
2026-06-02 — Cadência: "aprovou ideia e não gerou" — 4 causas raiz independentes convergindo no mesmo sintoma¶
Projeto: Cadência (full stack: cadencia-app + cadencia-growth + Supabase + Vercel + GHL) Severidade: Alta Tags: #backend #frontend #api #ghl #supabase #vercel #schema-drift #silenciosa #regressao #modelo-negocio #falha-detectada
Resumo executivo¶
Felipe relatou: "dezenas de vezes corrigi a mesma coisa de emails que não são gerados, linkedin e blog que também não são gerados". A investigação cross-temporal cross-canal expôs que 4 bugs raiz independentes convergem no mesmo sintoma percebido ("aprovou ideia, esperou, nada apareceu"). Cada fix anterior tratava 1 dos 4, então o sintoma sempre voltava por outro caminho.
Achados da sessão: 22 tenants ativos, só 7 publicaram nos últimos 30 dias; 6 contas pagantes com plano ativo e zero posts em 30d; e quem publicava, frequentemente só blog (sem linkedin/seinfeld/instagram).
As 4 causas raiz¶
Causa 1 — VPS_TRIGGER_URL no Vercel com \n literal no final¶
env pull revelou: len=25 last3=[48, 92, 110] — ASCII para 0, \, n. O valor tinha dois caracteres literais \ + n no final, fruto de um echo "..." | vercel env add antigo. Resultado: http://72.60.4.71:39090\n/trigger é URL inválida; fetch() falhava em todas as aprovações depois de algum momento entre maio e junho. O trigger-generation/route.ts retornava 200 ao frontend (porque a chamada era fire-and-forget) e o cron de 14h era a única fonte de geração — quem aprovasse depois esperava 24h.
Fix: vercel env rm VPS_TRIGGER_URL production --yes && printf 'http://72.60.4.71:39090' | vercel env add VPS_TRIGGER_URL production. Verificado por env pull novo: len=23 (sem o \n).
Eco do README: o arquivo src/app/api/app/trigger-generation/README.md documenta exatamente o mesmo bug, mas para o SECRET. O URL foi setado depois e caiu no mesmo padrão.
Causa 2 — Coluna channels (plural) não existia na tabela generation_queue¶
Schema real (via Management API): channel text DEFAULT 'blog' (singular). Migration original 20260425000000_generation_queue.sql criou só essa coluna. Mas o frontend (POST /api/app/generation-queue/route.ts linha 47) sempre fez:
Supabase silenciosamente descartava a coluna inexistente, o INSERT subia com channel='blog' por default. Toda informação multi-canal era perdida. PDL-171 estava aberto descrevendo o sintoma mas nunca foi corrigido.
Fix:
ALTER TABLE generation_queue ADD COLUMN IF NOT EXISTS channels text[] NOT NULL DEFAULT '{}';
UPDATE generation_queue SET channels = ARRAY[channel] WHERE channels = '{}' AND channel IS NOT NULL;
NOTIFY pgrst, 'reload schema';
269 rows backfillados. Migration commitada em supabase/migrations/20260602230000_generation_queue_channels_array.sql (PR cadencia-app#7).
Causa 3 — GHL_COMPANY_ID errado no /cadencia/.env¶
provision_tenant.py:create_ghl_location() monta body = {'name', 'email', 'companyId': os.environ.get('GHL_COMPANY_ID', '')}. O valor em .env era yXnyB5pagLHLdjEvGpYe — empresa diferente da que o GHL_AGENCY_TOKEN (pit-02f184...) tem permissão. GHL respondia 403 "Forbidden resource". Provisioning ficava em partial para sempre.
Confirmação: rodei curl POST /locations/ com mesmo token + companyId=rIrmvD1WcDqVNAyDRnf8 (extraído de location existente) → 201 Created. Com companyId=yXnyB5... → 403.
Fix: sudo sed -i 's|^GHL_COMPANY_ID=.*|GHL_COMPANY_ID=rIrmvD1WcDqVNAyDRnf8|' /cadencia/.env. Backup salvo em /cadencia/.env.bak-pdl364-20260602-2340.
Reprovisioning manual passou em 4 tenants: Lab. Crescimento (Alexandre Manhães), Diego do Bitcoin, Jhonatan, Petrafix Engenharia, Alejandro Pano. Todos provisioning_status=ready com location_id real.
Causa 4 — Modelo de cobrança migrou pra crédito-only, mas código ainda tem lógica de plano/tier¶
Felipe explicou na sessão: Cadência é vendida por créditos; "planos" (trial/essencial/starter/growth) são apenas convenções comerciais (lotes com desconto). Todo cliente deve ter todos os canais habilitados. Pago = créditos > 0.
Mas o growth_pipeline.py mantinha: - if plan_name in ('trial', 'essencial', 'starter'): steps_to_run = [s for s in steps_to_run if s != 'seinfeld'] — pulava email - weekday not in (0, 3) → pulava blog/linkedin/instagram em terça/quarta/sexta/fim de semana
E o provision-tenant/route.ts só criava tenant_config se houvesse attribution. Resultado: 20/22 tenants em prod com growth_channels=NULL, pipeline filtrava silenciosamente.
Fix: - Remoção do bloco if plan_name in ('trial', 'essencial', 'starter') em growth_pipeline.py (deploy direto na VPS Master, backup bak-creditos-2015). - Backfill: UPDATE tenant_config SET config = jsonb_set(config, '{growth_channels}', '["blog","seinfeld","linkedin","instagram"]') aplicado em 20 tenants. - PR cadencia-app#7: provision-tenant/route.ts agora cria tenant_config sempre com growth_channels default + attribution se houver. - PR cadencia-app#7: trigger-generation/route.ts retorna 400 estruturado (TENANT_NOT_CONFIGURED / GHL_NOT_READY) em vez de fail silencioso; ideas/page.tsx mostra a mensagem.
Por que isso convive há tanto tempo sem ser descoberto¶
Cada bug por si só é parcialmente compensado pelos outros: - Cliente aprova → Causa 1 mata a chamada imediata, mas o cron de 14h vai eventualmente rodar - Cron de 14h corre e pega a queue → Causa 2 perde a info multi-canal, mas blog_generate.py lê "qualquer pending sem filtro" e gera blog mesmo assim - Cliente entusiasmado vê o blog publicado → assume que está funcionando - Mas linkedin/instagram/seinfeld dependem de published_posts + location_id GHL → Causa 3 + Causa 4 matam silenciosamente - Cliente nota "blog vem, redes não" → reclama → fix pontual em algum canal → volta sintoma por outro canal - Felipe corrige "dezenas de vezes a mesma coisa" — sempre era OUTRA das 4 causas
Sinal-chave que destravou o diagnóstico¶
Quando Felipe disse "hoje um user novo aprovou e não gerou, e eu mesmo uso diariamente", a hipótese inicial ("cliente abandona") caiu. Investigação concreta de DOIS casos específicos (Alejandro Pano que aprovou hoje 14:58, Lab. Crescimento que provisionou hoje 13:55) expôs assinaturas diferentes que apontaram pra causas distintas:
- Alejandro:
growth_channels=NULL+ghl.location_id=None+ cron já tinha passado → causas 1+3+4 - Lab. Crescimento: 16 ideias
pendingupdated_at == created_at(nunca tocadas) +provisioning_status=partialGHL 403 → causa 3 isolada, mais Felipe ainda não tinha entrado no app
Prevenção¶
-
Schema drift entre frontend e banco: o INSERT do
generation-queue/route.tsdeveria ter falhado quando incluíssechannels. Supabase aceitar e descartar é faca de dois gumes — pra dev rápido ajuda, em prod silencia bugs. Considerar gerar TypeScript types do schema (supabase gen types) e usar tipagem estrita nos inserts. -
Newline em valores de env: rodar
node -e "console.log(JSON.stringify(process.env.X))"ou similar como smoke test pós-deploy. Documentarprintf(nãoecho) já está no README pro SECRET — adicionar também para URLs. -
Modelo de negócio em código: quando o produto muda (plano → crédito), grep
plan_tier|plan_name|trial|essencial|starterem todo o repo antes de assumir migração completa. Lógicas de filtro escondem em geradores secundários. -
Provisioning como gate, não só log: o frontend hoje deixa o usuário aprovar ideia mesmo com
provisioning_status=partial. Fix do PR#7 transforma isso em erro estruturado, mas a defesa ideal é a UI mostrar "Setup pendente" antes de habilitar o swipe de aprovar. -
Métricas de "tenant ativo sem entregar": hoje não há dashboard que mostre "X dias sem post publicado por tenant pagante". Sintoma só aparecia quando o cliente reclamava. Próximo: cron diário que alerta CS via WhatsApp se algum tenant com
tenant_plans.status=active credits>0temMAX(published_at) < hoje - 3d.
Pendências (não resolvidas neste incidente)¶
- Vercel "10 projects per Git Repo": Alejandro caiu nesse erro uma vez no provisioning; ao reprovisionar, "Blog already provisioned — skip". Continuará bloqueando provisioning de novos tenants quando o repo
cadencia-blog-templatechegar no limite. Soluções: template por shard de N tenants, projetos sem Git deploy, ou plano Vercel maior. - OAuth GHL nunca rola: script sempre cai em "Using static GHL_AGENCY_TOKEN (fallback)". Não impede operação após Causa 3 corrigida, mas é dívida — token estático não renova.
- Bug B "double gate": hoje o
generation-queue/route.tsinsere row pending mas não dispara trigger; oideas/page.tsxchamatrigger-generationseparado. Se algum dos dois falhar isoladamente, o outro não cobre. Considerar centralizar a chamada do trigger dentro degeneration-queue/route.ts.
Recuperação operacional executada¶
- Re-dispatched email do dia 02/06 para tenant Felipe em background (
nohup seinfeld_generate.py 6bb2c1ba --dispatch) — perdido por timeout do cron 14h. - Reprovisionados 5 tenants com
provisioning_status=partial→ todosready. - Backfill 20/22
tenant_config.growth_channels→ 22/22 com 4 canais. - 269/269 rows
generation_queue.channelspopuladas viaARRAY[channel].
Cross-link¶
- Incidente complementar (mesma sessão): 2026-06-02 dual-write log false cron sombra timeout dispatch — bugs cosméticos + timeout que confundiram diagnóstico inicial.
- PR aberto:
felipeluissalgueiro/cadencia-app#7— provisioning default + validação pre-conditions + migration channels[].