SEC-01+SEC-11: /secretaria/ask agora envia reply via WhatsApp (sock.sendMessage)
e persiste a mensagem no banco Prisma após brain.chat(), com notify Socket.IO.
SEC-02+SEC-03: MessageHandler emite hookBus 'ext:message.new' para todo msg
recebido. Listener no ext-api auto-dispara a Secretária apenas se NÃO
houver AIBot ativo na instância (exclusão mútua — chatbot tem prioridade).
SEC-04: ChatListItem exibe ícone BotOff (âmbar) quando chat.bot_paused = true,
com tooltip "Bot pausado — aguardando atendente".
SEC-05: chatToLegacy() mapeia c.botPaused → bot_paused na bridge legada.
NewWhatsChat recebe campo bot_paused opcional.
SEC-08: POST /secretaria/ask valida que existe instância CONNECTED antes
de criar conversa. Retorna 503 se nenhuma instância disponível.
SEC-10: POST /api/chatbot/bots/:instanceId valida que credentialId pertence
ao mesmo tenantId antes de criar o bot. Retorna 403 se não pertencer.
SEC-12: chatBotState.resumeBot() limpa botSummary (= null) ao retomar,
evitando contexto defasado após escalação humana.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
processHistoryBatch não incluía mediaPayload no createMany, tornando
impossível o re-download de mídias recebidas via history sync (que é a
maioria das mensagens ao reconectar a sessão).
1. Inclui mediaPayload no objeto de cada row para mensagens de mídia.
2. Após o createMany, dispara updateMany para preencher mediaPayload em
registros que já existiam no DB com o campo null (mensagens antigas
recebidas antes desta correção), sem bloquear o event loop.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Estende o tratamento de erro 400/404 do redownload para todos os tipos
de mídia. Quando mediaUnavailable=true (payload ausente no backend),
cada tipo exibe sua mensagem específica e desabilita o clique:
- Imagem: 'Imagem não disponível'
- Vídeo: 'Vídeo não disponível'
- Documento: 'Arquivo não disponível' + cursor-default
- Sticker: 'Figurinha não disponível'
DocumentBubble recebe nova prop mediaUnavailable para controle interno.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mensagens antigas sem mediaPayload retornam 400 do backend (sem como
re-solicitar ao WhatsApp). O erro agora é capturado em handleRedownload,
que seta mediaUnavailable=true — o botão fica desabilitado e exibe
'Áudio não disponível' em vez de explodir com Uncaught (in promise).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. MessageArea: handleRedownloadMedia usava selectedChat?.instanceId (undefined
no formato legado) — substituído por instance_name ?? instanceId. O clique
em 'Toque para baixar áudio' agora chama o backend corretamente.
2. MessageBubble/AudioBubble: botão de velocidade (1×/1.5×/2×) movido para
entre o botão play e as barras do waveform, conforme layout do WhatsApp.
A linha inferior agora exibe apenas o tempo decorrido.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Três correções independentes para mensagens enviadas/recebidas via celular:
1. WhatsAppConnectionManager: o listener CB:message agora captura também
recipient_pn (InfiniteAPI v7+) além de peer_recipient_pn (Baileys oficial),
garantindo que o PN correto do destinatário seja gravado no peerRecipientHints
antes do messages.upsert ser emitido.
2. MessageHandler.processMessage: adicionado fast path fromMe=false via
remoteJidAlt para contatos @lid (InfiniteAPI já resolve o PN do remetente).
Para fromMe=true onde InfiniteAPI resolveu @lid→nosso próprio JID incorretamente,
recupera o JID correto via peerRecipientHints antes de descartar como self-chat.
3. MessageHandler.processHistoryBatch: corrigido guard senderPn que não testava
!msg.key.fromMe; para mensagens enviadas, senderPn é o nosso próprio JID,
não o destinatário — o que criava self-chats corrompidos no DB.
4. whatsapp.send.routes: adicionado .catch P2002 no upsert da rota POST /send;
race condition com eco Baileys causava unhandled rejection → exit(1) no Node 20.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Resolve erros "Cannot find module" no language server do VS Code ao abrir
arquivos de plugins que importam tipos via caminho relativo do backend.
O build de produção não era afetado (tsc seguia os imports normalmente),
mas o extHost2 do editor não resolvia os arquivos fora do include.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Armazena payload WAMessage (serializado com _b64 para Uint8Array) em
messages.mediaPayload no momento da recepção da mensagem
- Nova rota POST /instances/:id/redownload-media/:msgId que usa
sock.updateMediaMessage para renovar URL expirada + re-upload Wasabi
- Frontend: placeholders clicáveis (spinner + texto) para imagem, vídeo,
áudio, documento e figurinha sem media_url; atualização automática via
socket message:media_ready existente
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- /api/storage/view/* não usa mais authMiddleware: <img>/<audio>/<video> do
browser não enviam JWT header, causando 401 em todas as mídias do Wasabi
- sticker.routes.ts: wrap try-catch em todos os handlers — sem isso, erro
Prisma P2021 (tabela não existe) vira unhandled rejection e derruba Node 22
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Stickers: captura automática por fileSha256 (Baileys) no MessageHandler,
tabela Prisma 'stickers' com deduplicação por [tenantId, sha256],
API REST /api/stickers (list + favorite toggle), cache IndexedDB permanente
(sem TTL — conteúdo imutável), StickerPanel.tsx com abas Recentes/Favoritos
- Emoji picker: migrado de emoji-picker-react para @emoji-mart/react com
locale="pt" (tradução completa: categorias, busca, mensagem de sem resultado)
- Preloader: aparece apenas em hard reload (F5/URL direta) via
performance.getEntriesByType('navigation').type + sessionStorage flag,
navegação client-side Next.js pula o overlay instantaneamente
- Avatar cache: Wasabi como storage persistente + IndexedDB client-side,
avatar_version via MD5(avatarUrl) sem coluna extra no banco,
rota /api/storage/view/* para servir arquivos Wasabi ao frontend
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Solução 3 — Wasabi:
- Após download do CDN do WhatsApp, faz upload fire-and-forget para
Wasabi com path determinístico (avatars/{tenant}/{instance}/{jid}.jpg)
- Armazena o path relativo em contact.avatarUrl no banco
- Na próxima requisição, serve bytes direto do Wasabi sem tocar no CDN do WA
- Adiciona customPath em StorageUploadPayload e getBuffer() na interface
StorageProviderInterface/StorageProviderRegistry e plugin
Solução 4 — IndexedDB:
- Backend expõe avatarVersion (MD5 curto do avatarUrl) no response /api/chats
- Campo propagado por chatStore → chatToLegacy → NewWhatsChat
- Hook useAvatarCache (IndexedDB nativo, sem deps) armazena dataUrl do avatar
com chave jid:version — invalida automaticamente quando o avatar muda no banco
- Avatar.tsx serve do IndexedDB antes de disparar qualquer request HTTP;
converte via canvas e persiste no IndexedDB na primeira carga bem-sucedida
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Parse Gitea webhook payload and skip the deploy when all commits
match 'chore(deploy): bump version'. This breaks the infinite cycle
where the deploy script's own version-bump push re-triggered a deploy,
causing repeated container restarts and 502 errors for users.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Move close (X) button to far right, zoom button left of X
- File info (name + size) occupies the left of the top bar
- Zoom now follows the mouse cursor: transform-origin updates in
real-time as the mouse moves, so 2.5× scale always magnifies
the area under the pointer
- overflow-hidden on preview area keeps the scaled image contained
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MessageHandler can insert the echo of a sent message before the HTTP
handler does — replace prisma.message.create() with upsert() so both
paths converge safely instead of crashing with a unique constraint error.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>