DB (supabase_admin, manual/f6_2e_cron_rpcs.supabase_admin.sql):
- E2 (varrem TODOS os tenants via loop FROM tenant_schemas): cleanup/unstick_
notification_queue, sync_overdue_financial_records (EXECUTE format por schema),
populate_notification_queue (set_config search_path por tenant; profiles global)
- E1 (per-tenant via service_role): novo helper _tenant_schema_unchecked (sem
is_tenant_member — service_role nao e membro; REVOKE de anon/authenticated).
sla_open_breach, sla_mark_notified(+p_tenant_id), whatsapp_heartbeat_open_
incident/mark_notified/resolve(+p_tenant_id). convert_abandoned_intake_to_lead
resolve tenant internamente (intake public/F1b -> writes no schema).
first_response_stats/_runs: _tenant_route (user-facing, frontend ja passa
p_tenant_id); _first_response_runs computa thread_key (coluna nao existe).
- REVOKE das RPCs de servico de anon/authenticated; GRANT service_role
Edge: whatsapp-heartbeat-check (tdb.rpc->admin.rpc + p_tenant_id nos heartbeat
RPCs), conversation-sla-check (sla_mark_notified + p_tenant_id).
Gotchas: (1) service_role nao e tenant_member -> helper unchecked + REVOKE;
(2) conversation_messages nao tem coluna thread_key (computar); (3) DROP+CREATE
de nova assinatura precisa dropar ambas p/ idempotencia.
Smoke: E2 sync_overdue 13 across tenants; E1 sla_open_breach roteia; first_
response_stats user OK.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- _shared/tenant.ts: helper (adminClient, tenantDbForId, schemaForTenant,
listTenantSchemas, resolveTenantByChannel, tenantSchemaName)
- _shared/whatsapp-hooks.ts: hooks de tabela tenant recebem tdb; RPCs de
credito (deduct/add_whatsapp_credits) e tenant_members seguem em supa+p_tenant_id
- inbound (twilio/evolution): tenant_id da URL -> tdb pra conversation_messages
e notification_channels
- crons de fila (process-notification/email/sms/whatsapp-queue): varrem
listTenantSchemas e drenam a fila de cada schema (Q3: filas sao per-tenant);
modo single-tenant se body.tenant_id vier
- crons reminders/checks (send-session-reminders, conversation-sla-check,
whatsapp-heartbeat-check, convert-abandoned-intakes, sync-email-templates):
loop por tenant
- routing por tenant_id (send-whatsapp-message, send-session-reminder-manual,
twilio-provision, de/reactivate-channel, twilio-webhook): tenantDbForId;
channel-actions sem tenant_id varrem schemas por channel_id
- asaas-*: tenant_id do body -> tdb; asaas-webhook fica global (whatsapp_credit_purchases)
- notification-webhook (Meta): resolve tenant via channel_routing por phone_number_id,
fan-out por message_id quando nao resolve
- caller send-session-reminder-manual passa tenant_id (evento vive no schema)
Pendente: save-intake-progress e fluxos anon por token (decisao de roteamento)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bug: toast do SLA tinha deeplink /crm/conversas que caía em NotFound.
As rotas reais são /therapist/conversas (terapeuta) e /admin/conversas
(clinic_admin), contextuais por role.
Fix: novo sistema de aliases em AppLayout.resolveDeeplink.
DEEPLINK_ALIASES traduz links semânticos (ex: /conversas, /crm/conversas)
pra rota real baseado em tenantStore.activeRole. Edge do SLA agora
emite /conversas (alias) em vez de path hardcoded; frontend resolve.
Padrão aplicável pras próximas features — basta registrar novo alias
aqui quando a rota depender de contexto.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Completa o Grupo 3 do CRM com alerta de conversa sem resposta além
do tempo configurado — reutiliza o pipeline system_alert (toast
vermelho sticky + sininho + drawer).
Banco (migration 20260423000005):
- conversation_sla_rules: 1 linha por tenant com threshold global
(1-1440 min), respect_business_hours, business_hours_start/end,
business_days (ISO 1=seg..7=dom), alert_scope (assigned_only|all),
notify_admin_on_breach. Default: enabled=false.
- conversation_sla_breaches: incidents com UNIQUE parcial
(tenant_id, thread_key) WHERE resolved_at IS NULL — idempotência.
- Trigger AFTER INSERT em conversation_messages resolve o breach
automaticamente quando chega nova outbound na thread.
- RPCs service_role: sla_open_breach (idempotente), sla_mark_notified.
- RLS: membros do tenant leem; clinic_admin/tenant_admin/saas_admin
escrevem na config; service_role escreve em breaches.
Edge function conversation-sla-check (cron 5min):
- Varre tenants com enabled=true.
- Query conversation_threads onde last_message_direction='inbound'
(+ assigned_to NOT NULL se scope='assigned_only').
- Se respect_business_hours: calcula businessMinutesElapsed em TS
iterando dia por dia a interseção da janela [start,end] com
[last_inbound_at, now], só em dias marcados em business_days. TZ
fixa em America/Sao_Paulo via Intl.DateTimeFormat.
- Se elapsed >= threshold: sla_open_breach (idempotente) + notifica
assigned_to sempre + admins se notify_admin_on_breach (deduplicado
via Set).
- Anti-spam: só notifica 1x por incident (checa notified_at).
- Notification leva deeplink pra /crm/conversas e payload.thread_key
pro frontend destacar a conversa (fora de escopo deste commit).
UI em /configuracoes/conversas-sla:
- Toggle enabled + InputNumber threshold com preview "≈ Xh Ymin".
- Toggle respect_business_hours → revela start/end + seletor de dias
úteis (pills toggleáveis Seg..Dom, ISO order).
- Select scope.
- Toggle notify_admin_on_breach.
- Card abaixo com breaches dos últimos 7 dias (status aberto/resolvido,
thread_key, limite configurado no momento do breach, duração).
- Adicionada na ConfiguracoesPage landing + rota /configuracoes/conversas-sla.
Cron template comentado no fim da migration (mesmo padrão do heartbeat).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>