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 (
activeougrace). - 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-appque 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 emtenant_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-editorialsenquanto a rota indicartruncated=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¶
claim_weekly_performance_reportcria ou recupera a linha única da janela. Uma execuçãocompletedé reutilizada;runningcom lease válido não é tomada; lease expirado e falha anterior podem ser retomados.- O worker lista conexões sociais ativas com filtros explícitos de tenant e usuário.
- Para cada conexão, chama
POST /api/v1/social/analytics/weeklycomAuthorization: Bearer <TRIGGER_SECRET>. A rota valida novamente tenant, usuário e conexão antes de reutilizarrunWeeklySocialAnalysis. - Falha de uma rede vira proveniência
faileddaquele canal. O relatório continua e registraunavailable; falha técnica de banco/claim/persistência encerra a execução comofailed. - O worker relê
social_account_scoresesocial_channel_analysesda janela exata, depois agregaresend_email_sendseresend_email_eventssomente em[window_start, window_end). persist_weekly_performance_reportvalida token, lease, tenant, usuário, janela, versão e fingerprint antes de tornar a linhacompleted.claim_weekly_performance_deliverycria ou reutiliza a entregawhatsappdo relatório concluído.- O destinatário é lido de
users.phonecom filtros simultâneos detenant_ideuser_id. O remetente é a instância Lara/Evolution do tenant configurado no worker. - A Lara chama o Evolution, persiste a timeline com
source=weekly_performance_reporte confirma o ledger com o provider message ID. - O canal
emailreivindica outra linha no mesmo ledger, lê o endereço do owner emusers.emailcom escopotenant_id + user_ide resolve o remetente central emtenant_config. - O worker envia o HTML pelo Resend com a chave
weekly-performance-email/<delivery_id>e persiste o provider message ID. - Falha comprovadamente anterior ao provider vira
failed; timeout, 5xx ou resposta ambígua viraunknownterminal. Uma falha de email não altera o resultado do WhatsApp, e vice-versa. - Com
SOCIAL_EDITORIALS_ENABLED=1, o scheduler chamaPOST /api/v1/cron/social-editorialsusando o alias seguroSOCIAL_EDITORIALS_CRON_SECRET. - Enquanto a resposta trouxer
truncated=true, novas chamadas enviam onextCursordevolvido 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 ultrapassarSOCIAL_EDITORIALS_MAX_LLM_CALLS(30 por padrão e sempre múltiplo de 3). - 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: fallbackunavailableaté 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)parawhatsappeemail; 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.failedsó 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
unknownpara 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 ouf***@dominiono email.
Entrega por email¶
- Destinatário:
users.email, sempre filtrado portenant_id + user_id; logs e resultados exibem apenas máscara comof***@dominio. - Remetente:
tenant_config.config.emaildo tenant central apontado porWEEKLY_REPORT_EMAIL_SENDER_TENANT_ID; endereço ausente ou qualquer status diferente deverifiedfalha 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-Keyestável pordelivery_id, além da unicidade do ledger. - Confirmação: somente resposta com provider ID vira
sent; 4xx definitivo virafailed; timeout/5xx/resultado ambíguo viraunknowne bloqueia reenvio cego.
Contrato da mensagem¶
Formato determinístico, limitado a 900 caracteres:
- período fechado da análise;
Score geral: N/100ouAinda não há histórico suficiente para o score geral;- até dois insights disponíveis de Email, Instagram ou LinkedIn; sem evidência, ações de ativação;
- 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_idou 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 emnow(), 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_SECRETem 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 deusers.emailcom 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
unknownem nenhum canal. - Não criar um segundo cron para editorias sociais; o único gatilho é o final deste job semanal.
- Não reutilizar
TRIGGER_SECRETpor suposição. O worker usa o alias dedicadoSOCIAL_EDITORIALS_CRON_SECRET, resolvido pelo adapter_shared.secretsa partir do runtime/1Password. - Não habilitar
SOCIAL_EDITORIALS_ENABLEDantes 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_deliveriese 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@f5f37d315190c2618f8cda331821fe70e11f81fcecadencia-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.statuse 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 upstreamcadencia-app#395, ainda sem merge.