Pular para conteúdo

Relatório semanal de performance

TL;DR

O componente DEV-1653 materializa um relatório semanal canônico por tenant_id + user_id + janela fechada. O DEV-1654 entrega um resumo curto por WhatsApp, o DEV-1655 envia a leitura completa em HTML e o DEV-1887 drena os ajustes editoriais sociais depois das análises. Tudo reutiliza o único cron semanal da VPS Master e falha de forma observável, sem publicação social.

Identidade

Campo Valor
Tipo worker batch + cron
Stack Python 3.12, PostgREST/Supabase, rota interna Next.js
Entry point crons/weekly_performance_reports.py
Núcleo pipeline/weekly_performance_report.py
Ajuste editorial pipeline/social_editorial_scheduler.py
Entrega pipeline/weekly_performance_whatsapp.py + pipeline/weekly_performance_email.py
Ledger compartilhado pipeline/weekly_performance_delivery.py
Persistência weekly_performance_reports + weekly_performance_deliveries
Schedule segunda-feira, 08:00 BRT (0 11 * * 1 UTC)
Issues DEV-1653 (agregação) + DEV-1654 (WhatsApp) + DEV-1655 (email HTML) + DEV-1887 (ajuste editorial)

Responsabilidades

  • Descobrir cada owner de tenant não arquivado com carteira de créditos válida (active ou grace).
  • Calcular a última janela segunda-domingo fechada em America/Sao_Paulo.
  • Reivindicar a execução no banco antes de qualquer coleta externa.
  • Pedir ao cadencia-app que execute a análise social já existente para cada conexão Instagram/LinkedIn.
  • Ler o score e as análises sociais da janela exata.
  • Consolidar envios e eventos do espelho Resend na mesma janela meio-aberta.
  • Persistir um payload versionado, determinístico e rastreável.
  • Montar uma copy curta sem LLM, com período, score/fallback, até dois insights e deep-link.
  • Entregar pelo tenant remetente definido em WHATSAPP_REPORT_SENDER_TENANT_ID.
  • Renderizar um email HTML determinístico com score, leitura da semana, cards por canal, próximos passos e CTA para /app/performance.
  • Enviar email pelo remetente central definido em WEEKLY_REPORT_EMAIL_SENDER_TENANT_ID, cuja configuração verificada vive em tenant_config.config.email.
  • Registrar uma única entrega por report_id + channel, sem armazenar telefone ou email completos.
  • Depois de concluir todas as análises, drenar POST /api/v1/cron/social-editorials enquanto a rota indicar truncated=true.
  • Limitar o ajuste editorial a 30 chamadas LLM por semana, em lotes de 3, com falha fechada ao esgotar o teto.
  • Atualizar o heartbeat de sucesso apenas quando agregação, entregas e ajuste editorial terminarem sem falha.

Não altera dossier e não publica conteúdo social.

Elegibilidade do tenant

A fonte de verdade do predicado é a migration 20260824230500_weekly_performance_report_credit_eligibility_dev1805.sql, exposta pela RPC list_weekly_performance_report_scopes e reutilizada pelo claim. O relatório não consulta tenants.status: valores como trial são legado e não representam uma modalidade do produto. Em resumo, exige owner, tenant não arquivado e carteira em active ou grace; saldo zero continua elegível, porque o relatório não gera conteúdo nem consome crédito.

No rollout da DEV-1805, a migration 20260824230500_weekly_performance_report_credit_eligibility_dev1805.sql do cadencia-app deve ser aplicada antes do deploy do cadencia-growth; o worker falha fechado se a RPC ainda não existir.

Fluxo

  1. claim_weekly_performance_report cria ou recupera a linha única da janela. Uma execução completed é reutilizada; running com lease válido não é tomada; lease expirado e falha anterior podem ser retomados.
  2. O worker lista conexões sociais ativas com filtros explícitos de tenant e usuário.
  3. Para cada conexão, chama POST /api/v1/social/analytics/weekly com Authorization: Bearer <TRIGGER_SECRET>. A rota valida novamente tenant, usuário e conexão antes de reutilizar runWeeklySocialAnalysis.
  4. Falha de uma rede vira proveniência failed daquele canal. O relatório continua e registra unavailable; falha técnica de banco/claim/persistência encerra a execução como failed.
  5. O worker relê social_account_scores e social_channel_analyses da janela exata, depois agrega resend_email_sends e resend_email_events somente em [window_start, window_end).
  6. persist_weekly_performance_report valida token, lease, tenant, usuário, janela, versão e fingerprint antes de tornar a linha completed.
  7. claim_weekly_performance_delivery cria ou reutiliza a entrega whatsapp do relatório concluído.
  8. O destinatário é lido de users.phone com filtros simultâneos de tenant_id e user_id. O remetente é a instância Lara/Evolution do tenant configurado no worker.
  9. A Lara chama o Evolution, persiste a timeline com source=weekly_performance_report e confirma o ledger com o provider message ID.
  10. O canal email reivindica outra linha no mesmo ledger, lê o endereço do owner em users.email com escopo tenant_id + user_id e resolve o remetente central em tenant_config.
  11. O worker envia o HTML pelo Resend com a chave weekly-performance-email/<delivery_id> e persiste o provider message ID.
  12. Falha comprovadamente anterior ao provider vira failed; timeout, 5xx ou resposta ambígua vira unknown terminal. Uma falha de email não altera o resultado do WhatsApp, e vice-versa.
  13. Com SOCIAL_EDITORIALS_ENABLED=1, o scheduler chama POST /api/v1/cron/social-editorials usando o alias seguro SOCIAL_EDITORIALS_CRON_SECRET.
  14. Enquanto a resposta trouxer truncated=true, novas chamadas enviam o nextCursor devolvido pelo worker e retomam o scan depois do último tenant processado. Avançar o cursor conta como progresso mesmo quando o lote inteiro é skip e consome zero LLM. O total não pode ultrapassar SOCIAL_EDITORIALS_MAX_LLM_CALLS (30 por padrão e sempre múltiplo de 3).
  15. Sucesso completo grava atomicamente /cadencia/logs/weekly_performance_reports.success. Falha, resultado parcial, teto esgotado ou três chamadas truncadas sem progresso mantêm o heartbeat antigo e produzem exit code não zero.

Contrato do payload V1

Campos de topo: schemaVersion, tenantId, userId, windowStart, windowEnd, generatedAt, coverageStatus, accountScore, channels, missingChannels, activationActions e provenance.

Cada canal possui status, coverageStatus, reason, metrics e evidence. Na V1:

  • email: coorte de envios da semana e último estado conhecido no espelho Resend;
  • instagram/linkedin: payload e evidência da análise semanal existente;
  • blog/tiktok: fallback unavailable até integração analítica própria.

score = null e canal unavailable significam ausência de evidência, nunca nota ou métrica zero.

Idempotência e concorrência

  • Unicidade: (tenant_id, user_id, window_start, window_end, schema_version).
  • Claim: token UUID e lease de 20 minutos.
  • Retry concluído: não coleta nem persiste novamente.
  • Retry após crash: somente depois do lease expirar.
  • Fingerprint: SHA-256 de score, análises e evidências de e-mail, serializados canonicamente.
  • Escrita: apenas service_role; usuário autenticado lê somente relatório próprio por RLS.

Ledger de entregas

  • Unicidade: (report_id, channel) para whatsapp e email; a infraestrutura de claim/finalização é compartilhada.
  • Claim: token UUID e lease de 5 minutos.
  • Estados: pending → running → sent | failed | unknown.
  • sent é imutável e reutilizado em qualquer replay.
  • failed só pode ser retomado por uma nova execução explícita.
  • unknown é terminal: indica que o provider pode ter aceitado a mensagem e bloqueia reenvio cego.
  • O resumo do cron separa falha, parcial e unknown para WhatsApp e email, além do status editorial. Esses contadores, falha de agregação, falha editorial ou falha ao gravar heartbeat produzem exit code não zero.
  • Auditoria guarda sender_tenant_id, provider ID e somente um hint mascarado: quatro últimos dígitos no WhatsApp ou f***@dominio no email.

Entrega por email

  • Destinatário: users.email, sempre filtrado por tenant_id + user_id; logs e resultados exibem apenas máscara como f***@dominio.
  • Remetente: tenant_config.config.email do tenant central apontado por WEEKLY_REPORT_EMAIL_SENDER_TENANT_ID; endereço ausente ou qualquer status diferente de verified falha antes do provider.
  • HTML: determinístico, responsivo e sem LLM. Todo conteúdo vindo do payload é escapado antes de entrar no markup.
  • Assunto: Seu desempenho semanal — DD/MM a DD/MM.
  • CTA: APP_BASE_URL/app/performance?week=YYYY-MM-DD.
  • Idempotência externa: header Resend Idempotency-Key estável por delivery_id, além da unicidade do ledger.
  • Confirmação: somente resposta com provider ID vira sent; 4xx definitivo vira failed; timeout/5xx/resultado ambíguo vira unknown e bloqueia reenvio cego.

Contrato da mensagem

Formato determinístico, limitado a 900 caracteres:

  1. período fechado da análise;
  2. Score geral: N/100 ou Ainda não há histórico suficiente para o score geral;
  3. até dois insights disponíveis de Email, Instagram ou LinkedIn; sem evidência, ações de ativação;
  4. deep-link APP_BASE_URL/app/performance?week=YYYY-MM-DD.

O remetente não é hardcoded. Inicialmente a configuração pode apontar para o tenant que contém o número comercial do Felipe; a troca futura para um número oficial da Cadência exige apenas alterar WHATSAPP_REPORT_SENDER_TENANT_ID.

Quickstart

# Readiness check sem claim, coleta social, escrita ou envio externo; valida também destinatários e remetentes
python3 crons/weekly_performance_reports.py --dry-run --tenant <TENANT_ID> --json

# Agregação controlada sem tentar WhatsApp
python3 crons/weekly_performance_reports.py --skip-whatsapp --json

# Agregação controlada sem tentar email
python3 crons/weekly_performance_reports.py --skip-email --json

# Rollback operacional: mantém relatório e entregas, sem chamar ajuste editorial
python3 crons/weekly_performance_reports.py --skip-editorials --json

# Execução do cron — somente segunda-feira BRT
python3 crons/weekly_performance_reports.py --json

# Testes
python3 -m pytest tests/test_dev1653_weekly_performance_report.py tests/test_dev1654_weekly_performance_whatsapp.py tests/test_dev1655_weekly_performance_email.py tests/test_dev1887_social_editorial_scheduler.py -q

Linha de crontab instalada pelo DEV-1653; após os rollouts DEV-1654/DEV-1655 o mesmo entry point entrega WhatsApp e email:

0 11 * * 1 flock -n /tmp/weekly_performance_reports.lock -c 'cd /cadencia && /usr/bin/python3 crons/weekly_performance_reports.py --json >> /cadencia/logs/weekly_performance_reports.log 2>&1'

Don'ts

  • Não aceitar tenant_id ou identidade de conexão vindos do client final.
  • Não executar com escrita fora da segunda-feira BRT.
  • Não usar resend_email_analytics(p_since) para retry histórico: a função termina em now(), enquanto este relatório exige janela final exata.
  • Não converter ausência em zero nem esconder canal sem cobertura.
  • Não colocar o cron antes de migration, rota do app e secret compartilhado estarem no mesmo rollout.
  • Não registrar o valor de TRIGGER_SECRET em log, doc ou linha de comando.
  • Não colocar LARA_ADMIN_KEY, número remetente ou número destinatário no código, argv ou logs.
  • Não hardcodar remetente ou destinatário de email; o primeiro vem de tenant_config, o segundo de users.email com escopo completo.
  • Não registrar HTML completo, endereço completo do destinatário nem credenciais do Resend em logs.
  • Não considerar HTTP 5xx ou timeout do Resend como falha segura para retry; o resultado é unknown.
  • Não usar a instância Lara do destinatário como remetente; o envio sai exclusivamente do tenant central configurado.
  • Não retentar automaticamente entrega unknown em nenhum canal.
  • Não criar um segundo cron para editorias sociais; o único gatilho é o final deste job semanal.
  • Não reutilizar TRIGGER_SECRET por suposição. O worker usa o alias dedicado SOCIAL_EDITORIALS_CRON_SECRET, resolvido pelo adapter _shared.secrets a partir do runtime/1Password.
  • Não habilitar SOCIAL_EDITORIALS_ENABLED antes de configurar o alias seguro e validar o Preview.
  • Não colocar o segredo editorial em .env, argv, crontab, log ou documentação.

Troubleshooting

Sintoma Causa provável Ação
APP_HTTP_401 TRIGGER_SECRET e VPS_TRIGGER_SECRET divergentes Reconciliar a referência no 1Password e os dois runtimes, sem imprimir o valor
APP_HTTP_409 chamada fora da segunda-feira BRT Não forçar; aguardar a janela ou usar --dry-run
status running reaparece outro processo detém lease válido Não iniciar paralelo; conferir lock e aguardar o lease
relatório partial pelo menos um canal sem evidência Ver missingChannels, reason e socialAnalysisTriggers
report_persist_failed migration ausente, claim perdido ou payload inválido Conferir rollout da migration e logs sanitizados do worker
recipient_phone_missing owner não possui telefone no onboarding Corrigir users.phone; a entrega fica failed e pode ser reivindicada depois
sender_tenant_missing WHATSAPP_REPORT_SENDER_TENANT_ID ausente Configurar o tenant remetente no runtime do growth
sem_instancia_evolution número remetente ainda não está pareado na Lara Parear a instância do tenant remetente antes do E2E real
email_sender_tenant_missing WEEKLY_REPORT_EMAIL_SENDER_TENANT_ID ausente Configurar o tenant central de email no runtime do growth
email_sender_address_missing tenant_config.config.email não possui remetente resolvível Corrigir o sender no tenant central e validar o domínio
email_sender_domain_unverified configuração marca o domínio como não verificado Concluir a verificação no Resend antes de habilitar o cron
recipient_email_missing owner não possui email válido Corrigir users.email; a entrega pode ser retomada explicitamente após a correção
status unknown timeout/5xx depois que o provider pode ter aceitado Não reenviar; reconciliar provider ID e ledger manualmente
SOCIAL_EDITORIALS_CRON_SECRET_MISSING alias ausente no adapter de secrets da VPS Popular /etc/onboarding/op.env via referência do 1Password antes de habilitar a flag
WORKER_HTTP_401 alias do growth diverge de CRON_SECRET no worker Reconciliar a mesma referência sem imprimir os valores
SOCIAL_EDITORIALS_LLM_BUDGET_EXHAUSTED ainda havia tenants quando as 30 chamadas foram consumidas Investigar volume/falhas; não elevar o teto sem decisão explícita de custo
SOCIAL_EDITORIALS_NO_PROGRESS três respostas truncadas não avançaram nextCursor Inspecionar cursor, reasonStopped, falhas por tenant e saúde do worker
heartbeat semanal antigo o cron não rodou ou alguma etapa terminou com falha/parcial Ver o último JSON do log e a issue criada pelo health check

Entregas relacionadas e próximos consumidores

  • DEV-1654: entrega curta por WhatsApp baseada neste payload, sem reconsultar providers.
  • DEV-1655: entrega por email HTML reutilizando weekly_performance_deliveries e o payload V1.
  • DEV-1887: ajuste automático e versionado de editorias com aprendizados sociais aceitos.
  • DEV-1656: relatório detalhado na interface.
  • DEV-1792: benchmark Cadência e Social Blade; não faz parte do payload/scoring V1.

Gate de rollout do email

Antes de habilitar o código no cron, configurar WEEKLY_REPORT_EMAIL_SENDER_TENANT_ID, confirmar tenant_config.config.email.verification_status=verified e executar o dry-run controlado. Env ausente ou sender não verificado deve bloquear o rollout; não é skip silencioso.

Histórico

  • 2026-08-28 — Roll-forward produtivo da DEV-1887 validado com cadencia-app@f5f37d315190c2618f8cda331821fe70e11f81fc e cadencia-growth@f563a6343884a0cb7096f54ee44089b7f560884c: 43 tenants em duas invocações, 43 skips, zero falhas, zero LLM, zero ajustes e zero publicação. A flag ficou habilitada; o primeiro heartbeat semanal real permanece pendente antes do fechamento.
  • 2026-08-24 — DEV-1653 validado em produção com migration, worker e cron instalados.
  • 2026-08-24 — DEV-1654 adicionou destinatário do onboarding, remetente central configurável e entrega idempotente por WhatsApp; rollout e E2E controlado concluídos.
  • 2026-08-24 — DEV-1805 remove o gate legado de tenants.status e centraliza a elegibilidade no modelo de créditos + lifecycle de arquivamento.
  • 2026-08-25 — DEV-1655 adicionou email HTML completo, sender central em tenant_config, idempotência no Resend e isolamento de falhas por canal; dry-run com dados reais validado sem escrita nem envio externo.
  • 2026-08-27 — DEV-1887 integrou o dreno editorial ao único scheduler semanal, com feature flag fail-closed, teto de 30 chamadas LLM e heartbeat de sucesso monitorável.
  • 2026-08-27 — E2E Preview da DEV-1887 validou quatro tenants em dois lotes, quatro replays sem nova LLM, falha isolada, cleanup total e zero publicação. Evidência: docs/runtime-reviews/DEV-1887-preview-e2e.md. O teste também abriu a correção upstream cadencia-app#395, ainda sem merge.