Problem Statement
Operadores e administradores do ISing precisam acessar três áreas de configuração distintas — Buffer Humanizado, Personas & IA e Disparo em Massa — em páginas separadas, o que fragmenta o fluxo de trabalho e dificulta a visão geral do sistema.
Além disso, os cards de contato e conversas não exibem a foto de perfil do WhatsApp do cliente, tornando a identificação visual lenta e obrigando o operador a memorizar nomes ou números. Quando chega uma mensagem de um novo contato, o sistema não busca automaticamente a foto de perfil na Pastorini API, e não existe nenhum mecanismo de sincronização em lote para contatos existentes.
Por fim, o sistema não filtra adequadamente JIDs de broadcast (@lid) e grupos (@g.us) na camada de negócio, o que pode causar tentativas de busca de foto para JIDs inválidos e poluição do banco de dados.
Solution
1. Página de Monitoramento Unificada: Consolidar as páginas de Buffer, Personas e Disparo em uma única página com quatro abas — adicionando uma aba nova de Leads & Fotos. A navegação entre abas persiste o estado localmente para não perder configurações ao trocar de aba.
2. Captura Automática de Foto de Perfil: Ao receber webhook de mensagem de novo contato com JID válido (@s.whatsapp.net), o backend dispara em background uma requisição à Pastorini API para buscar a foto de perfil e armazena a URL no banco. O frontend recebe a atualização via WebSocket e exibe o avatar em tempo real.
3. Sincronização em Lote: A aba Leads & Fotos expõe um componente de sincronização que busca todos os contatos com status pending e processa em batches de 10, com barra de progresso e estatísticas em tempo real.
4. Validação de JID: Utilitários centralizados garantem que apenas JIDs de clientes diretos (@s.whatsapp.net) sejam processados, rejeitando broadcasts (@lid) e grupos (@g.us) em todas as camadas.
User Stories
Monitoramento Unificado
- Como operador, quero acessar o Buffer Humanizado, Personas & IA, Disparo em Massa e Leads & Fotos em um único lugar, para não precisar navegar entre múltiplas páginas para configurar o sistema.
- Como operador, quero que a aba ativa seja lembrada entre sessões, para retomar o trabalho exatamente onde parei sem renavegar.
- Como operador, quero visualizar um indicador visual claro de qual aba está ativa, para saber em que contexto de configuração estou trabalhando.
- Como operador, quero que as abas carreguem de forma independente, para que uma aba lenta não bloqueie o acesso às outras.
- Como administrador, quero poder acessar a página de Monitoramento diretamente pelo menu lateral, para não precisar lembrar URLs específicas.
Foto de Perfil — Captura Automática
- Como operador, quero que ao receber uma mensagem de um novo contato a foto de perfil do WhatsApp seja capturada automaticamente, para que o avatar apareça no card de conversa sem intervenção manual.
- Como operador, quero que a foto seja exibida em tempo real após a captura (sem recarregar a página), para que a tela de atendimento se atualize fluidamente.
- Como operador, quero ver um avatar com as iniciais do nome como fallback quando o contato não possui foto de perfil, para que o card nunca fique com uma imagem quebrada ou em branco.
- Como operador, quero que uma foto que falhou de ser buscada seja marcada como "erro" e não tente novamente indefinidamente, para que a fila de sincronização não fique bloqueada.
- Como administrador, quero que contatos de grupos e listas de broadcast sejam automaticamente ignorados no processo de captura de foto, para não gerar requisições desnecessárias à Pastorini API.
Foto de Perfil — Sincronização em Lote
- Como administrador, quero iniciar uma sincronização de fotos para todos os contatos existentes com status pendente, para atualizar a base histórica de uma só vez.
- Como administrador, quero acompanhar o progresso da sincronização em tempo real (barra de progresso, contador de processados/atualizados/falhas), para saber quanto tempo ainda levará e se há erros.
- Como administrador, quero que a sincronização processe os contatos em batches de tamanho configurável, para não sobrecarregar a Pastorini API com requisições simultâneas.
- Como administrador, quero poder sincronizar individualmente a foto de um contato específico clicando em um botão no card, para corrigir pontualmente registros desatualizados.
- Como administrador, quero filtrar os leads por status de foto (todos, pendentes, com foto, sem foto, erro), para priorizar ações de sincronização.
Aba Leads & Fotos
- Como administrador, quero visualizar todos os contatos diretos (não grupos) em um grid de cards com foto de perfil, nome e número formatado, para ter uma visão de identidade dos clientes.
- Como administrador, quero ver estatísticas no rodapé da aba (total, com foto, sem foto, erros), para entender rapidamente a cobertura de fotos da base.
- Como administrador, quero que contatos com JID
@lid (broadcast) e @g.us (grupo) sejam filtrados e não apareçam na listagem, para manter o foco em clientes individuais.
Validação de JID
- Como desenvolvedor, quero usar uma função centralizada
isValidClientJid para validar JIDs em frontend e backend, para garantir consistência de filtragem em todas as camadas.
- Como desenvolvedor, quero que o JID
5511999999999@s.whatsapp.net seja formatado como +55 11 99999-9999 nos displays do frontend, para melhorar a legibilidade.
- Como desenvolvedor, quero poder normalizar um número de telefone em qualquer formato para o JID padrão do WhatsApp, para aceitar uploads de listas de leads em formatos variados.
Implementation Decisions
Frontend — Módulos a Criar ou Modificar
- MonitoringPage (container): Gerencia estado da aba ativa, persiste no
localStorage, renderiza TabNavigation e o painel da aba selecionada. Mantém estética LiquidGlass existente no projeto.
- TabNavigation: Componente de navegação entre as quatro abas. Recebe mapa de abas com label e ícone, emite evento de mudança. Sem lógica de negócio — apenas apresentação.
- BufferTab, PersonasTab, DispatchTab: Conteúdo migrado das páginas atuais (BufferPage, PersonasPage, DisparadorPage) sem alteração de funcionalidade. As páginas originais são removidas após migração validada.
- LeadsTab: Aba nova. Busca contatos via API filtrados por instância e status de avatar. Filtra JIDs inválidos no frontend. Renderiza grid de
LeadCard e o componente ProfilePictureSync.
- LeadCard: Card de lead com avatar (imagem ou fallback de iniciais), nome (
pushName), número formatado, badge de status e botão de sincronização individual. Trata erros de carregamento de imagem com fallback automático.
- ProfilePictureSync: Busca contatos pendentes, processa em batches de 10 com intervalo entre batches, atualiza barra de progresso e estatísticas em tempo real.
- Utilitário JID (
jid.ts): Funções puras: isValidClientJid, extractPhoneFromJid, formatJid, normalizeJid, validateJidBatch, normalizeJidBatch. Sem dependências externas — 100% testável em isolamento.
- Tipos TypeScript: Novos tipos
LeadWithAvatar, AvatarStatus, SyncProfilePicturesRequest, SyncProfilePicturesResponse, ProfilePictureResponse e eventos WebSocket associados.
- Sidebar: Adicionar entrada "Monitoramento" apontando para
/monitoring. Remover entradas separadas das páginas unificadas.
- Roteamento: Adicionar rota
/monitoring → MonitoringPage. Remover rotas das páginas que foram unificadas.
Backend — Módulos a Criar ou Modificar
- PastoriniClient (extensão): Adicionar método
get_profile_picture(instance_id, jid) que faz GET /api/instances/{instance_id}/profile-picture/{jid} e retorna a URL ou None. Adicionar método sync_profile_pictures_batch(instance_id, jids, concurrency=5) que usa asyncio.Semaphore para paralelismo limitado.
- Endpoint
GET /api/v1/profile-pictures/{jid}: Busca em cache no banco primeiro. Se avatar_url está preenchida retorna de cache. Caso contrário, chama Pastorini, persiste e retorna. Parâmetros de query: instance_id (obrigatório).
- Endpoint
POST /api/v1/profile-pictures/sync: Recebe lista de JIDs no body. Valida cada JID (rejeita @lid e @g.us). Chama sync_profile_pictures_batch. Retorna { updated, failed, failures }.
- Webhook (atualização): No handler de mensagens inbound, após validar o JID (
@s.whatsapp.net, não @lid), agendar fetch_and_store_profile_picture como BackgroundTask. Armazenar pushName quando disponível no payload.
- Modelo Contact (migração): Adicionar colunas:
avatar_status ENUM('pending','fetched','not_found','error') DEFAULT 'pending', avatar_fetched_at TIMESTAMP NULL, avatar_retry_count INT DEFAULT 0, push_name VARCHAR(255) NULL. Criar índice em (avatar_status, avatar_fetched_at) para queries de sincronização.
- Evento WebSocket
profile_picture_updated: Após persistir foto de perfil com sucesso, emitir evento à instância com { jid, avatar_url, push_name } para atualização em tempo real no frontend.
Decisões Arquiteturais
- Cache-first para foto de perfil: O endpoint de foto consulta o banco antes de chamar a Pastorini API, reduzindo chamadas externas para contatos já processados.
- Background task no webhook: A busca de foto não bloqueia a resposta do webhook. O Pastorini recebe
200 OK imediatamente; a foto chega ao frontend via WebSocket quando pronta.
- Semaphore para batch: Limitar concorrência a 5 requisições simultâneas na Pastorini protege contra rate-limiting (
429).
- Persist
avatar_status: O campo de status evita retentativas infinitas e permite filtragem eficiente na aba Leads.
- Filtragem de JID em todas as camadas: Frontend filtra antes de exibir, backend valida antes de processar, webhook valida antes de agendar task.
Contratos de API
| Método |
Endpoint |
Descrição |
| GET |
/api/v1/profile-pictures/{jid}?instance_id={id} |
Obter foto de um contato (cache-first) |
| POST |
/api/v1/profile-pictures/sync |
Sincronizar fotos em lote (body: {jids:[...]}) |
| GET |
/api/v1/contacts?instance_id={id}&avatar_status={status} |
Listar contatos com filtro de status de avatar |
Testing Decisions
Princípio: Testes validam comportamento externo observável — o que o usuário ou sistema vê — não detalhes de implementação interna.
Utilitários de JID (alta prioridade — lógica de negócio crítica)
isValidClientJid deve retornar true para @s.whatsapp.net, false para @lid, false para @g.us, false para string vazia ou null
normalizeJid deve converter +55 (11) 99999-9999 → 5511999999999@s.whatsapp.net
normalizeJid deve lançar erro para números com menos de 10 dígitos
formatJid deve retornar +55 11 99999-9999 para JID brasileiro de 13 dígitos
Endpoint de Foto de Perfil (integração)
GET /profile-pictures/{jid} com contato que já tem avatar_url no banco deve retornar sem chamar Pastorini
GET /profile-pictures/{jid} com contato novo deve chamar Pastorini e persistir resultado
POST /profile-pictures/sync deve rejeitar JIDs @lid e @g.us antes de processar
POST /profile-pictures/sync deve retornar { updated, failed } mesmo se alguns JIDs falharem
LeadCard (componente)
- Deve renderizar a imagem quando
avatar_url é válida
- Deve renderizar fallback de iniciais quando
avatar_url é null ou imagem falha ao carregar
- Botão de sincronização deve ficar desabilitado durante operação em andamento
ProfilePictureSync (componente)
- Deve exibir barra de progresso com percentual correto durante sincronização
- Deve chamar
onSyncEnd quando o processamento de todos os batches completar
- Deve continuar processando batches subsequentes mesmo se um batch falhar
Out of Scope
- Download e armazenamento local das imagens de perfil (URLs externas da Pastorini são usadas diretamente)
- Atualização automática periódica de fotos de perfil (as fotos do WhatsApp expiram — esse mecanismo de refresh não está no escopo)
- Exibição de foto de perfil na janela de chat durante o atendimento (escopo deste PRD é apenas a aba Leads & Fotos e o card de conversa na lista)
- Suporte a grupos e listas de broadcast no gerenciamento de fotos
- Upload manual de foto de perfil pelo operador
- Configuração de Typebot, Chatwoot ou AI Agent dentro da página de Monitoramento
- Implementação de
BufferTab, PersonasTab e DispatchTab — apenas a migração das páginas existentes para o novo container
Further Notes
- O campo
pushName (nome do contato no WhatsApp) deve ser capturado no webhook quando disponível e armazenado em contacts.push_name. O frontend deve priorizar pushName sobre name para exibição.
- A Pastorini API retorna
{ "url": null } (não 404) quando o contato não tem foto. O backend deve tratar ambos os casos como avatar_status = 'not_found'.
- URLs de foto de perfil do WhatsApp têm prazo de expiração (
expires_at). Não é necessário implementar refresh automático neste PRD, mas o campo de data de busca (avatar_fetched_at) deve ser mantido para facilitar a implementação futura.
- O estilo LiquidGlass já existente no projeto deve ser mantido em todos os novos componentes — usar as variáveis CSS
--glass-*, --border-subtle, --color-brand, etc. já definidas em globals.css e liquidglass.css.
Problem Statement
Operadores e administradores do ISing precisam acessar três áreas de configuração distintas — Buffer Humanizado, Personas & IA e Disparo em Massa — em páginas separadas, o que fragmenta o fluxo de trabalho e dificulta a visão geral do sistema.
Além disso, os cards de contato e conversas não exibem a foto de perfil do WhatsApp do cliente, tornando a identificação visual lenta e obrigando o operador a memorizar nomes ou números. Quando chega uma mensagem de um novo contato, o sistema não busca automaticamente a foto de perfil na Pastorini API, e não existe nenhum mecanismo de sincronização em lote para contatos existentes.
Por fim, o sistema não filtra adequadamente JIDs de broadcast (
@lid) e grupos (@g.us) na camada de negócio, o que pode causar tentativas de busca de foto para JIDs inválidos e poluição do banco de dados.Solution
1. Página de Monitoramento Unificada: Consolidar as páginas de Buffer, Personas e Disparo em uma única página com quatro abas — adicionando uma aba nova de Leads & Fotos. A navegação entre abas persiste o estado localmente para não perder configurações ao trocar de aba.
2. Captura Automática de Foto de Perfil: Ao receber webhook de mensagem de novo contato com JID válido (
@s.whatsapp.net), o backend dispara em background uma requisição à Pastorini API para buscar a foto de perfil e armazena a URL no banco. O frontend recebe a atualização via WebSocket e exibe o avatar em tempo real.3. Sincronização em Lote: A aba Leads & Fotos expõe um componente de sincronização que busca todos os contatos com status
pendinge processa em batches de 10, com barra de progresso e estatísticas em tempo real.4. Validação de JID: Utilitários centralizados garantem que apenas JIDs de clientes diretos (
@s.whatsapp.net) sejam processados, rejeitando broadcasts (@lid) e grupos (@g.us) em todas as camadas.User Stories
Monitoramento Unificado
Foto de Perfil — Captura Automática
Foto de Perfil — Sincronização em Lote
Aba Leads & Fotos
@lid(broadcast) e@g.us(grupo) sejam filtrados e não apareçam na listagem, para manter o foco em clientes individuais.Validação de JID
isValidClientJidpara validar JIDs em frontend e backend, para garantir consistência de filtragem em todas as camadas.5511999999999@s.whatsapp.netseja formatado como+55 11 99999-9999nos displays do frontend, para melhorar a legibilidade.Implementation Decisions
Frontend — Módulos a Criar ou Modificar
localStorage, renderizaTabNavigatione o painel da aba selecionada. Mantém estética LiquidGlass existente no projeto.LeadCarde o componenteProfilePictureSync.pushName), número formatado, badge de status e botão de sincronização individual. Trata erros de carregamento de imagem com fallback automático.jid.ts): Funções puras:isValidClientJid,extractPhoneFromJid,formatJid,normalizeJid,validateJidBatch,normalizeJidBatch. Sem dependências externas — 100% testável em isolamento.LeadWithAvatar,AvatarStatus,SyncProfilePicturesRequest,SyncProfilePicturesResponse,ProfilePictureResponsee eventos WebSocket associados./monitoring. Remover entradas separadas das páginas unificadas./monitoring→MonitoringPage. Remover rotas das páginas que foram unificadas.Backend — Módulos a Criar ou Modificar
get_profile_picture(instance_id, jid)que fazGET /api/instances/{instance_id}/profile-picture/{jid}e retorna a URL ouNone. Adicionar métodosync_profile_pictures_batch(instance_id, jids, concurrency=5)que usaasyncio.Semaphorepara paralelismo limitado.GET /api/v1/profile-pictures/{jid}: Busca em cache no banco primeiro. Seavatar_urlestá preenchida retorna de cache. Caso contrário, chama Pastorini, persiste e retorna. Parâmetros de query:instance_id(obrigatório).POST /api/v1/profile-pictures/sync: Recebe lista de JIDs no body. Valida cada JID (rejeita@lide@g.us). Chamasync_profile_pictures_batch. Retorna{ updated, failed, failures }.@s.whatsapp.net, não@lid), agendarfetch_and_store_profile_picturecomoBackgroundTask. ArmazenarpushNamequando disponível no payload.avatar_status ENUM('pending','fetched','not_found','error') DEFAULT 'pending',avatar_fetched_at TIMESTAMP NULL,avatar_retry_count INT DEFAULT 0,push_name VARCHAR(255) NULL. Criar índice em(avatar_status, avatar_fetched_at)para queries de sincronização.profile_picture_updated: Após persistir foto de perfil com sucesso, emitir evento à instância com{ jid, avatar_url, push_name }para atualização em tempo real no frontend.Decisões Arquiteturais
200 OKimediatamente; a foto chega ao frontend via WebSocket quando pronta.429).avatar_status: O campo de status evita retentativas infinitas e permite filtragem eficiente na aba Leads.Contratos de API
/api/v1/profile-pictures/{jid}?instance_id={id}/api/v1/profile-pictures/sync{jids:[...]})/api/v1/contacts?instance_id={id}&avatar_status={status}Testing Decisions
Princípio: Testes validam comportamento externo observável — o que o usuário ou sistema vê — não detalhes de implementação interna.
Utilitários de JID (alta prioridade — lógica de negócio crítica)
isValidClientJiddeve retornartruepara@s.whatsapp.net,falsepara@lid,falsepara@g.us,falsepara string vazia ounullnormalizeJiddeve converter+55 (11) 99999-9999→5511999999999@s.whatsapp.netnormalizeJiddeve lançar erro para números com menos de 10 dígitosformatJiddeve retornar+55 11 99999-9999para JID brasileiro de 13 dígitosEndpoint de Foto de Perfil (integração)
GET /profile-pictures/{jid}com contato que já temavatar_urlno banco deve retornar sem chamar PastoriniGET /profile-pictures/{jid}com contato novo deve chamar Pastorini e persistir resultadoPOST /profile-pictures/syncdeve rejeitar JIDs@lide@g.usantes de processarPOST /profile-pictures/syncdeve retornar{ updated, failed }mesmo se alguns JIDs falharemLeadCard (componente)
avatar_urlé válidaavatar_urlénullou imagem falha ao carregarProfilePictureSync (componente)
onSyncEndquando o processamento de todos os batches completarOut of Scope
BufferTab,PersonasTabeDispatchTab— apenas a migração das páginas existentes para o novo containerFurther Notes
pushName(nome do contato no WhatsApp) deve ser capturado no webhook quando disponível e armazenado emcontacts.push_name. O frontend deve priorizarpushNamesobrenamepara exibição.{ "url": null }(não 404) quando o contato não tem foto. O backend deve tratar ambos os casos comoavatar_status = 'not_found'.expires_at). Não é necessário implementar refresh automático neste PRD, mas o campo de data de busca (avatar_fetched_at) deve ser mantido para facilitar a implementação futura.--glass-*,--border-subtle,--color-brand, etc. já definidas emglobals.csseliquidglass.css.