Fluxo de reativação de canal WhatsApp + alerta toast sticky + notify owner

Cadeia de fixes descoberta ao testar o heartbeat 6.1 num tenant que migrou
de Evolution → Twilio e precisava voltar pro Evolution.

1. RLS notification_channels (migration 20260423000003)
   - Policy antiga tinha `deleted_at IS NULL` como primeira condição AND,
     bloqueando leitura de soft-deleted até pro próprio owner/saas_admin.
   - Isso fazia o chooser nunca detectar "canal antigo pra reativar".
   - Relaxada: owner/membro/saas_admin leem inclusive soft-deleted.
   - Filtro de deleted_at fica no código aplicativo (todos os queries já
     filtram explicitamente quando querem apenas ativos).

2. Edge function reactivate-notification-channel (nova)
   - Espelho da deactivate existente; service_role bypass RLS.
   - Aceita {channel_id} OU {tenant_id + provider}.
   - Autoriza saas_admin OU membro ativo do tenant.
   - Garante exclusividade: soft-deleta qualquer OUTRO canal ativo do
     mesmo tenant+channel.
   - Reseta metadata.first_unhealthy_at + connection_status=disconnected
     (heartbeat começa do zero).

3. SaasWhatsappPage (/saas/whatsapp)
   - loadChannel busca soft-deleted como fallback quando não tem ativo.
   - saveCredentials detecta soft-deleted e chama reactivate edge,
     depois atualiza credentials+display_name.
   - Banner âmbar "Canal configurado anteriormente" + botão vira
     "Reativar e salvar".

4. ConfiguracoesWhatsappPage tenant (/configuracoes/whatsapp-pessoal)
   - loadCredentials busca soft-deleted como fallback.
   - Card âmbar "WhatsApp Pessoal foi usado anteriormente" com botão
     "Reativar WhatsApp Pessoal" em vez de mostrar apenas "chame o suporte".

5. ChooserPage (/configuracoes/whatsapp)
   - Fix bug lateral: comparava activeProvider === 'evolution' (template)
     com 'evolution_api' (DB) — card nunca mostrava estado ativo. Agora
     normaliza via computed activeProviderKey.
   - softDeletedByProvider map carregado no mount; cards que têm row
     soft-deleted mostram "Reativar" em vez de "Ativar".
   - handleChoose chama reactivate edge antes de goSetup se detecta
     soft-deleted do provider escolhido.

6. whatsapp-heartbeat-check: notifica owner do channel + admins
   - notifyChannelStakeholders substitui notifyTenantAdmins.
   - Set dedupa o owner_id do channel + clinic_admin + tenant_admin.
   - Em tenant solo: 1 notificação; em clínica com canal de terapeuta
     específico: terapeuta (owner) + admin recebem; em clínica com canal
     do próprio admin: 1 (owner=admin).

7. Toast frontend para system_alert
   - notificationStore.subscribeRealtime aceita callback onInsert.
   - useNotifications registra callback que dispara toast PrimeVue
     (severity error, life 24h, closable) para type='system_alert'.
   - Usuário precisa fechar manualmente — alerta crítico de infra
     não pode sumir sozinho.

Cron heartbeat ativado em runtime local via cron.schedule()
(não vai neste commit — é config de ambiente, não migration).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leonardo
2026-04-23 09:21:29 -03:00
parent e1f756ea82
commit 881fa16c27
8 changed files with 534 additions and 50 deletions
@@ -0,0 +1,37 @@
-- ==========================================================================
-- Agencia PSI — Migracao: RLS notification_channels permite ler soft-deleted
-- ==========================================================================
-- Criado por: Leonardo Nohama
-- Data: 2026-04-23 · Sao Carlos/SP — Brasil
--
-- Contexto: a policy antiga "notif_channels_select" filtrava
-- `deleted_at IS NULL` como primeira condicao AND, bloqueando qualquer
-- leitura de canais soft-deleted mesmo pros donos/saas_admin. Isso impedia
-- o fluxo de reativacao (chooser e pagina do tenant nao conseguiam detectar
-- que havia canal deletado pra oferecer "Reativar").
--
-- Solucao: remover o filtro da policy. O controle de soft-delete fica no
-- codigo aplicativo (cada query filtra .is('deleted_at', null) quando
-- quer apenas canais ativos). Canais soft-deleted continuam acessiveis
-- apenas pros roles autorizados (owner, membro do tenant, saas_admin) —
-- a privacidade nao muda.
-- ==========================================================================
DROP POLICY IF EXISTS notif_channels_select ON public.notification_channels;
CREATE POLICY notif_channels_select
ON public.notification_channels
FOR SELECT
USING (
public.is_saas_admin()
OR owner_id = auth.uid()
OR tenant_id IN (
SELECT tm.tenant_id
FROM public.tenant_members tm
WHERE tm.user_id = auth.uid()
AND tm.status = 'active'
)
);
COMMENT ON POLICY notif_channels_select ON public.notification_channels IS
'Owner, membros ativos do tenant e saas_admin leem todos os canais (inclusive soft-deleted). Filtro deleted_at fica no codigo aplicativo.';