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-345-web-push-notificacoes.md • 2026-07-25T17:37:49.084Z

BK-345 - Notificações Web Push (opt-in no site + disparo por peça no admin)

> Documentação retroativa: a funcionalidade foi construída em 11/07/2026 e já está em produção.

> Este arquivo foi criado em 25/07/2026 para registrar a arquitetura e o que ficou pendente,

> conforme a regra de higiene do docs/BACKLOG.md.

Contexto

Canal de notificação próprio, sem depender de WhatsApp: o visitante autoriza notificações no

navegador e o admin dispara avisos de peça (nova temporada, desconto, última semana) para a base

inscrita, com segmentação por zona da cidade e teste A/B/C do texto do botão.

Status geral

  • [x] Opt-in no site (banner + service worker) — em produção desde 11/07/2026
  • [x] Persistência de inscrições (rnt_push_subscriptions) — em produção
  • [x] Opt-out no perfil do cliente — em produção
  • [x] Disparo por peça no admin, com alcance estimado e UTMs — em produção
  • [x] Limpeza automática de inscrição morta (HTTP 404/410 → ativo=0) — em produção
  • [ ] Disparo automático em evento de negócio (nova peça publicada, preço caiu) — hoje é 100% manual
  • [ ] Relatório de entrega/clique por disparo (hoje só existe log em texto)

Arquitetura (por que tem dois PHP)

O site roda PHP 5.6, incompatível com a lib de Web Push (minishlink/web-push exige PHP 7+ e

criptografia moderna). Por isso o envio real ficou num sender separado:

  • Site (PHP 5.6) — grava/remove inscrição e serve o banner de opt-in.
  • Sender (PHP 8.3, fora do webroot)/www/rnt-push-sender/send.php, com vendor/ próprio via

Composer e as chaves VAPID em /www/rnt-push-sender/vapid.json (permissão 640, root:www).

Chamado pelo admin via shell_exec do binário /www/server/php/83/bin/php.

A chave pública VAPID fica no JS do site (js/rnt-push.js) — é pública por design. A chave

privada só existe no vapid.json fora do document root e não está no repositório.

Arquivos

| Arquivo | Papel |

|---|---|

| includes/inc_peca.php | Injeta manifest.json, theme-color, apple-touch-icon e js/rnt-push.js em todas as páginas |

| js/rnt-push.js | Banner de opt-in, registro do service worker, escolha de zonas, chamadas ao endpoint |

| sw.js | Service worker: recebe o push e monta a notificação (título, texto, ícone, botão, URL) |

| manifest.json | Manifest PWA mínimo (nome, ícones, theme-color) |

| push_action.php | Endpoint subscribe / unsubscribe / zonas / status (mysqli preparado) |

| painel/modulos/profile/index.php | Opt-out do cliente logado, logo acima de Zonas de Interesse |

| admin/modulos/pecas/push.php | Tela de disparo por peça (prefill, alcance, zona, variante A/B/C) |

| admin/modulos/pecas/index.php | Botão de sino na coluna Ações da listagem de peças |

| img/push_icon_192.png, img/push_icon_512.png | Ícones da notificação / PWA |

| /www/rnt-push-sender/send.php | Envio real (fora do repositório, PHP 8.3) |

Tabela rnt_push_subscriptions

id, endpoint, endpoint_hash, p256dh, auth, user_id, zonas, user_agent, ativo, criado_em, atualizado_em

  • endpoint_hash = sha1(endpoint), chave única usada no ON DUPLICATE KEY UPDATE (reinscrição do

mesmo navegador atualiza em vez de duplicar).

  • user_id é opcional: dá pra se inscrever sem login. Quando o visitante loga depois, o

COALESCE(VALUES(user_id), user_id) preserva o vínculo já existente.

  • Situação em 25/07/2026: 29 inscrições, todas ativas.

Regra de alcance (precedência de zonas)

Mesma lógica no sender e na estimativa de alcance do admin, nesta ordem:

  1. rnt_push_subscriptions.zonas — zonas escolhidas no próprio banner (funciona sem login);
  2. senão, clientes.zonas_interesse do perfil (quando a inscrição está vinculada a um cliente);
  3. senão, sem preferência declarada → recebe tudo.

Todo o RJ em qualquer um dos dois campos casa com qualquer zona. Disparo sem --zona (ou com

Todo o RJ) vai para toda a base ativa.

Guardrails já implementados

  • Link do disparo é obrigado a começar com https://rionoteatro.com (bloqueia enviar tráfego

para fora do site).

  • Zona e variante do botão são validadas contra allowlist; valor inválido cai no padrão.
  • Todo argumento passado ao sender vai por escapeshellarg().
  • UTMs preenchidas automaticamente (utm_source=push&utm_medium=webpush&utm_campaign=<peca>_<bairro>_<data>)

seguindo o mesmo padrão de modulos/campanhas/incluir.php?origem=disparos_whatsapp;

utm_term=plano_a|b|c marca a variante do botão para comparar cliques no Analytics.

  • Inscrição que retorna 404/410 (navegador desinstalado, permissão revogada) é marcada ativo=0

automaticamente pelo sender — a base não acumula endpoint morto.

  • Log de cada disparo em admin/logs/push_dispatch.log (peça, zona, variante, título, saída).

Como testar envio (REGRA)

Nunca disparar push de teste para a base: diferente de uma campanha de WhatsApp, aqui existem

visitantes reais inscritos desde 11/07/2026 (29 ativos em 25/07/2026) e a notificação chega na hora,

sem fila para cancelar. Regra definida com o dono em 11/07/2026:

  • teste sempre com --user 50877 (inscrição do próprio dono) ou --dry-run;
  • mesmo em teste, não usar a palavra "teste" nem texto provisório no título/corpo — se escapar

para a base, tem que ser algo que não constranja;

  • o --limit N existe para conter o alcance quando for necessário validar com mais de uma inscrição.

Pegadinhas conhecidas do sender: as opções do getopt precisam de : (valor obrigatório) — com ::

o valor separado por espaço não é capturado. E o botão da notificação só aparece depois que o

navegador rebaixa o sw.js atualizado, o que acontece na visita seguinte ao site.

Pendências

  1. Disparo automático — hoje todo push é manual pelo admin. Candidatos naturais: peça nova

publicada, queda de preço, última semana de temporada. Precisa decidir cadência para não

queimar a permissão do navegador (que, diferente do WhatsApp, o usuário revoga sem aviso).

  1. Relatório de entrega/clique — o log em texto não permite medir taxa de entrega nem

comparar as variantes A/B/C dentro do painel; hoje a comparação depende do Analytics via

utm_term. Uma tabela de disparos (rnt_push_envios) resolveria.