Comunicação — Hub Operacional
SSOT da seção
/admin/comunicacao— centro único de envio e análise de toda comunicação saída pela plataforma (e-mail, WhatsApp, SMS, notificação in-app). Substitui a antiga aba "E-mails" do Financeiro e a aba "Notificações" do Monitoramento.
1. Escopo
Estrutura de tabs (v3, rev. 2026-05-09):
| Tab | O que mostra | Fonte de dados |
|---|---|---|
| Visão geral | KPIs cross-canal (volume, entrega, abertura, clique, bounce, fila/DLQ) | email_outbox, fatura_cobrancas, notificacoes, email_unsubscribes |
| Enviar | Landing 6 cards (canal × modo) → composer dedicado por canal | endpoints enviar.* + notificacoes/create_batch |
| Histórico | Log unificado paginado com timeline por mensagem | merge dos 3 stores acima + email_eventos |
| Canais | Configuração técnica/operacional por canal (E-mail, WhatsApp, Push) | ver §"Tab Canais" |
A seção NÃO duplica logs_transacoes. Auditoria de quem disparou ou editou continua em admin-logs — aqui medimos estado de entrega, não ação administrativa.
1.1 Tab Canais (Hub-Shell pattern)
Substituiu as antigas tabs Templates | Remetentes | Supressões (que eram todas domínio E-mail) por uma estrutura por canal. Padrão visual idêntico à tab Enviar — landing N cards → painel da entidade com pills internas. Ver docs/development/FRONTEND_UI_STANDARDS.md §Hub-Shell pattern.
| Canal | Pills (seções) | Fonte de dados |
|---|---|---|
| Remetentes · Templates · Supressões · Domínio · Reputação | remetentes_email, email_templates, email_unsubscribes | |
| Sessão · Rate-limit · Opt-out (read-only) | _shared/messaging-rate-limits.ts (espelho) | |
| Push | Esqueleto "Em breve" — Tópicos / Quiet hours / Retry | — |
Compat de URL legada (replaceState automático em index.tsx):
| URL antiga | URL nova |
|---|---|
?tab=envios | ?tab=historico |
?tab=notificacoes | ?tab=enviar&composer=notificacao&modo=massa |
?tab=templates | ?tab=canais&canal=email&secao=templates |
?tab=remetentes | ?tab=canais&canal=email&secao=remetentes |
?tab=supressoes | ?tab=canais&canal=email&secao=supressoes |
2. Arquitetura
Frontend Edge function Banco
───────── ─────────────── ─────
admin-comunicacao/ admin-comunicacao email_outbox
index.tsx ──invokeEdge──▶ action: kpis ────▶ fatura_cobrancas
visao-geral-tab action: envios_lista ────▶ notificacoes
envios-unificados-tab action: envio_detalhe ────▶ email_eventos
supressoes-tab action: supressoes_lista ──▶ email_unsubscribes
useAdminComunicacao.ts (RQ keys: ['admin-comunicacao', ...])Decisão: sem view materializada. 3 selects paralelos + merge ordenado por criado_em DESC no servidor. Quando volume passar de ~50k linhas/7d, extrair RPC sem mudar contrato.
3. Contrato da edge function
3.1 Filtro de Status (dropdown contextual)
A aba Envios usa um Select cuja lista de status muda conforme o canal selecionado (STATUS_POR_CANAL em envios-unificados-tab.tsx). Tokens enviados ao backend permanecem canônicos; rótulos exibidos são em PT. Todos é representado pelo sentinel __all__ no estado e undefined no payload.
3.2 Webhook Resend (KPIs e saúde)
KPIs de e-mail (entrega, abertura, clique, bounce) só são populados quando o webhook do Resend está apontando para a edge resend-webhook. A Visão geral exibe um card Webhook Resend com a última callback recebida (max(email_eventos.criado_em)) e popover com as instruções de configuração:
- URL:
https://mjvuzsizjlcalyfmbquy.functions.supabase.co/resend-webhook - Eventos:
email.sent,email.delivered,email.delivery_delayed,email.bounced,email.complained,email.opened,email.clicked,email.failed - Secret:
RESEND_WEBHOOK_SECRET(formatowhsec_...) — já configurado no projeto
Saudável = última callback ≤ 6h.
3.3 Modal "Detalhe do envio" (humanizado)
action=envio_detalhe retorna 4 blocos: resumo (campos resolvidos sem UUIDs — escola_nome, numero_fatura, enviado_por, etc.), mensagem (payload de preview), timeline (eventos Resend para e-mail) e registro (raw, exibido apenas dentro do collapsible "Detalhes técnicos"). O preview renderiza:
- WhatsApp: bolha verde com markdown leve (
*bold*,_italic_,~strike~,`code`,\n). - SMS: bolha neutra texto puro.
- E-mail: card com
assunto+ render doscorpo_blocos(saudacao/paragrafo/cta/rodape) quando presentes empayload.blocos. - In-app: card com
título+mensagem.
POST /functions/v1/admin-comunicacao — body com action. Auth: somente principal_role = 'administrador' (especialista terá hub próprio).
| action | Body extra | Retorno |
|---|---|---|
kpis | intervalo_dias (1–90), escola_id? | { email, cobranca, notificacao, supressoes, serie_diaria } |
envios_lista | canal, status?, escola_id?, busca?, intervalo_dias, referencia_tipo?, referencia_id?, page, pageSize | { items: EnvioUnificado[], total, page, pageSize } |
envio_detalhe | canal, id | { canal, registro, timeline } |
supressoes_lista | busca?, origem?, page, pageSize | { items, total, page, pageSize } |
Todos os campos PII (to_email, destinatario) são mascarados server-side via _shared/pii-helpers.ts antes de sair. Frontend nunca recebe e-mail/telefone bruto.
4. Mapeamento de coluna por tabela (importante)
| Tabela | Timestamp de criação |
|---|---|
email_outbox | criado_em |
fatura_cobrancas | criado_em |
notificacoes | criada_em (fem.) |
email_unsubscribes | criado_em |
Esquecer o criada_em quebra todas as queries de notificação com erro PostgREST 42703. Já documentado neste SSOT para evitar regressão.
5. Filtro de origem do e-mail
origem no log unificado é inferido (não é coluna):
referencia_tipo === 'fatura'→cobrancatemplate_slugcomeça commanual_→campanha- caso contrário →
transacional
E-mails de cobrança aparecem somente em email_outbox (a coluna canal='email' em fatura_cobrancas é filtrada fora para evitar duplicação no log unificado).
6. Atalhos contextuais (entram em outras telas)
admin-financeiro › Faturas→ "Ver e-mails enviados" →/admin/comunicacaoaba Envios.admin-financeiro › Cobranças→ drilldown filtrareferencia_tipo=fatura.Monitoramento › Notificações→ removido; passou a viver no Hub.
7. Fases
- Fase A — Reorganização (Templates/Remetentes migrados de Financeiro, Notificações migradas de Monitoramento, sidebar e roteamento).
- Fase B — KPIs + Envios unificados + drawer de detalhe com timeline
email_eventos. - Fase C — Supressões (
email_unsubscribes) + correção denotificacoes.criada_emno envios_lista/kpis. - Fase D (futuro) — Campanhas (Fases 1–5 de
docs/plans/COMUNICACAO_CAMPANHAS.md), unificação decobranca_templates+templates_mensagemna aba Templates.
8. Padrões aplicados
- Lazy Mount Once nas tabs (cache RQ preservado).
keepPreviousDataem paginação.- Anti-Flash:
isLoading && !data→ Skeleton. - PII masking server-side obrigatório.
- queryKey
['admin-comunicacao', …]— escopo global de admin (semescola_idpor padrão; quando filtro de escola é aplicado, entra como parâmetro da key).
8.1 Abertura e fail-safe de tabs
Lições aprendidas no incidente de 2026-05-09 (React error #31 derrubando a seção inteira):
- Allowlist da aba inicial:
AdminComunicacaovalidasessionStorage.admin_comunicacao_return.tabcontra a lista canônica (visao | envios | notificacoes | templates | remetentes | supressoes). Valor inválido/legado cai emenvios(default seguro). - Boundary local por aba (
SectionErrorBoundary): cada tab é embrulhada por um boundary próprio. Uma aba que quebrar (ex: payload inesperado, campo objeto onde se esperava string) exibe fallback localizado e mantém as outras abas operacionais. Não usar oErrorBoundaryglobal para isso — ele derruba a seção inteira. - Renderização defensiva (
toText): campos vindos de payloads dinâmicos (status, origem, template_slug, assunto_ou_titulo, timeline.tipo) passam portoText(value, fallback)antes de virarem children React. Isso elimina a janela em que o React error #31 pode ocorrer. - Anti-padrão: confiar no tipo TypeScript do payload (
string | null) sem normalização runtime — o backend pode retornar objeto em colunas JSONB recém-introduzidas, e isso quebra silenciosamente.
9. Referências cruzadas
docs/plans/COMUNICACAO_CAMPANHAS.md— backlog de campanhas (Fase D).docs/features/billing/email-billing.md— pipelineemail_outbox+ Resend.docs/features/billing/cobrancas-templates.md— régua WhatsApp/SMS.docs/architecture/observability-and-logging-standard(memória) — por que NÃO usarlogs_transacoespara análise de entrega.