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)
- BK-136
- BK-137
- BK-138
- BK-147
- BK-148
- BK-149
- BK-150
- BK-151
- BK-156
- BK-158
- BK-159
- BK-160
- BK-161
- BK-162
- BK-163
- BK-164
- BK-165
- BK-166
- BK-170
- BK-171
- BK-172
- BK-177
- BK-183
- BK-186
- BK-187
- BK-189
- BK-190
- BK-191
- BK-192
- BK-193
- BK-195
- BK-196
- BK-197
- BK-198
- BK-199
- BK-201
- BK-205
- BK-207
- BK-208
- BK-209
- BK-210
- BK-211
- BK-212
- BK-213
- BK-214
- BK-215
- BK-216
- BK-217
- BK-218
- BK-219
- BK-220
- BK-221
- BK-229
- BK-230
- BK-231
- BK-232
- BK-233
- BK-234
- BK-235
- BK-236
- BK-239
- BK-240
- BK-241
- BK-242
- BK-243
- BK-244
- BK-245
- BK-246
- BK-248
- BK-249
- BK-250
- BK-251
- BK-252
- BK-253
- BK-254
- BK-255
- BK-256
- BK-257
- BK-258
- BK-259
- BK-260
- BK-261
- BK-262
- BK-263
- BK-264
- BK-265
- BK-266
- BK-267
- BK-268
- BK-269
- BK-270
- BK-271
- BK-272
- BK-275
- BK-276
- BK-277
- BK-278
- BK-279
- BK-280
- BK-295
- BK-313
- BK-341
- BK-345
- BK-346
Detalhe do BK Selecionado
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, comvendor/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 noON 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:
rnt_push_subscriptions.zonas— zonas escolhidas no próprio banner (funciona sem login);- senão,
clientes.zonas_interessedo perfil (quando a inscrição está vinculada a um cliente); - 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 Nexiste 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
- 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).
- 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.