Cerebro Studio · Backlog · Changelog
RioNoTeatro • /www/wwwroot/rionoteatro.com.br/docs/BACKLOG.md
Abrir Studio Projeto externo em modo read-only; encaminhamento permitido, escrita bloqueada.

Backlog Unificado

Projeto: RioNoTeatro. Fonte principal: /www/wwwroot/rionoteatro.com.br/docs/BACKLOG.md.

Modo read-only: ações de escrita ficam disponíveis apenas para o Cérebro.

Sem itens pendentes em /www/wwwroot/rionoteatro.com.br/docs/BACKLOG.md.

Especificações Disponíveis (fora da fila pendente)

Detalhe do BK Selecionado

/www/wwwroot/rionoteatro.com.br/docs/backlog/BK-346-ruido-alertas-push-admin.md • 2026-07-28T23:48:15.933Z

BK-346 — Ruído de alertas de push para o admin

Status: entregue em 28/07/2026 (itens 1–5). Item 6 (limpeza da tabela) pendente de execução manual.

Origem: análise das notificações push que o admin vinha recebendo.

Relacionado: BK-345 (Web Push), BK-341 (guardião de integridade MP).

Problema

O dono recebia ~24 push/dia, quase todos do mesmo alerta. A investigação encontrou três causas

empilhadas e, no caminho, um problema de negócio maior que o ruído em si.

Causa 1 — o push furava o rate limit do WhatsApp

enviar_push_admin_from_whatsapp() é chamado no topo de enviar_whatsapp_admin()

(includes/whatsapp_helper.php:1628), antes do enviar_whatsapp() que aplica o rate limit de 24h.

Alertas que no WhatsApp saíam 1x/dia iam no push de hora em hora. O log confirmava: a cada hora

saía BLOQUEIO (RATE LIMIT 24H) no WhatsApp e o push ia assim mesmo.

Causa 2 — o alerta dominante era falso positivo, há 5 meses

admin/cron/monitor_story_roi.php (cron 7 ) lia um caminho hardcoded

/www/server/cron/ecef0b533e9b16166be606a34f900a72.log, congelado desde 05/03/2026 porque o cron

do Facebook virou rnt_sync_facebook_planner / rnt_sync_facebook_dispatcher. O check ficou preso em

error (que usa force=true e throttle de 900s) por ~5 meses.

Causa 3 — o log de push não permitia auditoria

push_dispatch.log recebia só a saída crua do sender (Enviadas: 1 | ...), sem timestamp e sem

título. Foi preciso reconstruir o histórico cruzando com admin/logs/debug.log.

Achado de fundo — o monitor vigia uma iniciativa que não decolou

story_redirect_click tem 11 eventos em toda a série histórica, o último em 10/04/2026.

O go.php, único arquivo que grava esse evento, está órfão: nenhum código do site gera links

apontando pra ele. O que circula hoje é o short.php, cujos cliques são utm_medium=social

(Facebook feed) e crm (WhatsApp) — nenhum com medium story.

E o gargalo real do canal aparece em social_post_queue: nos últimos 30 dias há **42 stories em

pending_approval (22 facebook_story + 20 instagram_story) contra 3 publicados**. Os stories

não estão sendo publicados porque a aprovação não acontece — não porque o tracking quebrou.

Consequência: os três checks do monitor descreviam estado normal, então ele ficava em warn/error

permanente. Corrigir só a causa 2 derrubaria de ~24 para ~1 push/dia, mas manteria um alerta diário

sem nada acionável — o tipo de alerta que se para de ler.

O que foi feito

  1. Dedup próprio no push de adminincludes/push_admin_helper.php. Arquivo

admin/logs/push_admin_dedup.json (www:www 664, flock, limpeza automática), janela padrão

86400s em RNT_PUSH_ADMIN_DEDUP_PADRAO, 4º parâmetro $dedup_seconds em enviar_push_admin()

(0 desativa). Fail-open: erro de I/O nunca silencia alerta.

A chave normaliza data e hora antes do sha1 (_push_admin_dedup_chave) — sem isso o dedup

seria inútil, porque os alertas trazem Data: dd/mm/YYYY HH:ii:ss no corpo. O rate limit do

WhatsApp escapa disso por acidente: usa md5($numero . substr($mensagem, -100)) e a data cai no

meio da mensagem.

  1. Caminho do log do cronmonitor_story_roi.php. Lista de candidatos (dispatcher → planner →

log antigo) escolhendo o de mtime mais recente, em vez de caminho fixo. O cron já foi renomeado

uma vez e foi exatamente isso que criou o falso positivo silencioso.

  1. Log auditável — cada linha agora sai

[data] admin titulo=... => Enviadas: N | Expiradas: 0 | Falhas: 0, e supressões viram

admin SUPRIMIDO (dedup 86400s) titulo=.... Como o disparo é em background, contexto e resultado

são montados numa escrita só (sh -c capturando $OUT); logar em duas etapas embaralharia o log

quando dois alertas saem juntos.

  1. Notificação por transição de estadomonitor_story_roi.php. Só avisa quando o status

muda (piorou, ou normalizou), com o último estado em admin/logs/monitor_story_roi_state.json.

Estado estável = silêncio. Primeira execução sem estado salvo só avisa se já nascer em error,

pra não ficar mudo caso o arquivo se perca durante uma incidência real. O JSON completo continua

indo pro log do cron a cada run: nada deixa de ser observável, só para de ser empurrado.

  1. Dois checks ruidosos ajustados:
  • roi_story_clicks_24hroi_story_clicks_7d: janela de 7 dias, e "zero clique" só é warn se

houve story publicado no período (consulta social_post_queue). Sem post publicado, zero

clique é o esperado.

  • campaign_ads_story: campanha ADS ativa é estado normal do negócio. Agora só alerta na

contaminação real — ads e orgânico dividindo o mesmo utm_campaign.

  1. Allowlist no campo eventoaction.php, case save_tracking. O nome do evento vinha do

cliente e ia direto pro banco. Não era brecha de SQL (escapado com mysqli_real_escape_string,

e peca_id/valor passam por intval/floatval), mas qualquer string virava uma linha nova:

scanners em 12/01 e 11/04/2026 gravaram 185 linhas com payloads (' OR 5*5=25 --,

${@print(md5(31337))}, c:/windows/win.ini) e inflaram a tabela para 110 nomes de evento

distintos contra ~9 reais. api/track-event.php já tinha allowlist; action.php não.

Pendência

A limpeza das 185 linhas de lixo não foi executada (o DELETE foi bloqueado pelo classificador de

permissões do agente). O backup das linhas já está em

/root/.rnt_security/cliente_tracking_lixo_20260729_074504.tsv (600). SQL para rodar:

```sql

DELETE FROM cliente_tracking

WHERE evento NOT IN (

'view_item','view_list','date_selected','add_to_cart','begin_checkout',

'purchase','utm_detected','shortlink_redirect_click','story_redirect_click'

);

```

Sem risco de recontaminação: a allowlist do item 6 já está ativa.

Decisão em aberto — destino do go.php

O canal de story está no pior dos dois mundos: paga o custo do alerta sem entregar o dado. Ou se

reconecta (o dispatcher passa a gerar o link do story via rastreador com utm_medium=story, e o ROI

de story passa a existir), ou se aposenta go.php junto com o monitor. Antes disso, porém, o gargalo

a resolver é o dos 42 stories parados em pending_approval — sem publicação não há clique para

medir, e nenhum ajuste de tracking muda isso.

Verificação

  • Monitor: status geral saiu de error para ok, todos os 9 checks OK.
  • Transição: ok→ok em duas rodadas seguidas = zero push; error→ok = 1 push NORMALIZADO.
  • Dedup: dois envios idênticos seguidos = 1 entregue + 1 SUPRIMIDO; alerta com conteúdo novo passa;

pedidos #1001 vs #1002 não colidem.

  • Guardião MP: action.php é monitorado — --check acusou DRIFT, --seal, --check OK.

Bug lateral encontrado

O php.ini da VPS está com date.timezone = PRC (UTC+8), 11h à frente do sistema (-03). Os

scripts do site escapam porque config/functions_new.php:9 faz

date_default_timezone_set('America/Sao_Paulo'), mas o helper de push roda em contextos que não

carregam esse config — o log saiu com 2026-07-29 07:31 enquanto o sistema marcava 20:31.

Corrigido com DateTime + timezone explícito em _push_admin_agora(), sem mexer no estado global do

processo do chamador. Vale para qualquer script PHP CLI nessa VPS.

Backups

  • includes/push_admin_helper.php.bkp_original_2026-07-28_20-28-27_dedup-log
  • admin/cron/monitor_story_roi.php.bkp_original_2026-07-28_20-28-27_fix-cronlog
  • admin/cron/monitor_story_roi.php.bkp_original_2026-07-28_20-40-*_transicao-checks
  • action.php.bkp_original_2026-07-28_20-4*_allowlist-evento-tracking
  • go.php.bkp_original_2026-07-28_20-40-*_allowlist-evento (go.php acabou não sendo alterado)