# Fases — Plugin Inbox (NewWhats como Motor Central)

**Data de início:** 2026-04-17  
**Última atualização:** 2026-04-17  
**Estratégia:** Contrato mínimo (A) → PoC validação (B) → expansão incremental

---

## ✅ Fase 1 — Contrato Mínimo — CONCLUÍDA

**Objetivo:** Definir o núcleo da API e do protocolo WS antes de escrever qualquer código.  
**Concluída em:** 2026-04-17

### REST — motor `/api/ext/v1/`

| Endpoint | Status |
|---|---|
| `GET  /sessions` | ✅ |
| `GET  /sessions/:id/qr` | ✅ |
| `GET  /inbox?session=&search=&limit=` | ✅ |
| `GET  /inbox/:chatId/messages?limit=&before=` | ✅ |
| `POST /inbox/:chatId/send` | ✅ |

### WebSocket — `WS /api/ext/v1/stream`

| Evento | Emissor |
|---|---|
| `connected` | bridge (handshake) |
| `session.qr` | `WhatsAppConnectionManager` via hookBus |
| `session.status` | `WhatsAppConnectionManager` via hookBus |
| `message.new` | `MessageHandler` via hookBus |

### Envelope padrão (contrato imutável v1)

```json
{
  "event": "session.qr",
  "data": { "instanceId": "uuid", "qrBase64": "data:image/png;base64,..." },
  "timestamp": 1776403129107
}
```

### Decisões técnicas fixadas

- **WebSocket nativo** (`ws` lib, `noServer: true`) — sem dependência de Socket.IO client
- **Versionamento:** `/api/ext/v1/` — quebra de contrato → `/v2/`, nunca silenciosamente
- **Auth:** header `x-nw-key: <integration_key>` — validado contra tabela `api_keys` (Prisma)
- **Rate-limit:** 120 req/min por chave (in-memory, janela deslizante)
- **Multi-tenant:** `integration_key` → `tenantId` — eventos filtrados antes de enviar ao WS
- **hookBus:** `WhatsAppConnectionManager` e `MessageHandler` emitem `ext:*`; plugin `ext-api` subscreve

### Arquivos criados/modificados

**Motor:**
- `backend/src/core/types.ts` — `httpServer: HttpServer` no `PluginContext`
- `backend/src/server.ts` — expõe `httpServer` no `pluginCtx` + `globalThis.__whatsAppManager`
- `backend/src/modules/whatsapp/connection/WhatsAppConnectionManager.ts` — emite `ext:session.qr`, `ext:session.status`
- `backend/src/modules/whatsapp/handlers/MessageHandler.ts` — emite `ext:message.new`
- `plugins/ext-api/manifest.json`, `index.ts`, `backend/apikey-auth.ts`, `backend/routes.ts`, `backend/ws-bridge.ts`

---

## ✅ Fase 2 — PoC QR + WS + Inbox — CONCLUÍDA

**Objetivo:** Validar fluxo end-to-end + inbox completo.  
**Concluída em:** 2026-04-17

### Entregue (absorveu escopo da Fase 3 original)

- Tela de 3 painéis no admin-app (`/wa-inbox`)
- Painel Sessões: lista com status badge, QR inline em tempo real
- Painel Chats: lista com preview, timestamp relativo, badge unread, busca
- Painel Mensagens: histórico, bubbles por lado, status (✓ / ✓✓ / ✓✓ ciano), envio
- Append em tempo real via WS (`message.new`)
- Update optimista no envio (rollback em erro)
- Reconexão automática do WS (retry a cada 3s com badge de status)

### Proxy no satélite

**Satélite:**
- `plugins/newwhats/proxy.js` — proxy REST (HTTP nativo) + proxy WS (TCP tunnel via `net`)
- `plugins/newwhats/index.js` — proxy REST em `/api/nw/v1/*`, WS em `/api/nw/v1/stream`
- `plugins/index.js` — `PluginManager` recebe `httpServer` como 3º argumento
- `server.js` — cria `http.createServer(app)` explicitamente antes de `loadAll()`

**Admin-app:**
- `services/api.ts` — `NwService` + interfaces `NwSession`, `NwChat`, `NwMessage`
- `pages/WhatsAppInbox.tsx` — página de inbox
- `App.tsx` — rota `/wa-inbox` + item de menu

### Fluxo validado

```
Browser → /api/nw/v1/sessions → satélite proxy → motor → retorna instâncias ✅
Browser → WS /api/nw/v1/stream → TCP tunnel → motor WS → handshake OK ✅
Baileys gera QR → hookBus → ws-bridge → browser QR inline ✅
QR escaneado → CONNECTED → browser recarrega chats ✅
Mensagem recebida → hookBus → browser appenda ✅
```

---

## ✅ Fase 3 — Inbox Refinado — CONCLUÍDA

**Concluída em:** 2026-04-17

### Motor — novos endpoints

| Endpoint | Função |
|---|---|
| `POST /inbox/:chatId/read` | Zera `unreadCount` |
| `PATCH /inbox/:chatId/archive` | Arquiva/desarquiva chat |

### Motor — melhorias

- `message.update` via hookBus — emitido em `WhatsAppConnectionManager` após mudança de status de mensagem (delivered/read)
- Filtro `NOT @lid / NOT @broadcast` no SELECT de inbox — elimina 3k+ chats "fantasma" gerados pelo Baileys
- Campos `pushName` e `senderJid` adicionados ao SELECT de mensagens
- `syncExpiredAvatars()` no `ContactHandler` — parse do parâmetro `oe=<hex>` das URLs do CDN WhatsApp; limpa e re-enfileira avatares que expiram em < 48h

### UI — refinamentos

- `markRead` ao selecionar chat, `handleArchive` com atualização otimista da lista
- Handler WS para `message.update` — atualiza status da mensagem em tempo real
- Paginação infinita de mensagens (scroll no topo, cursor `before=<ISO>`, `IntersectionObserver`)
- Grupos: exibição do nome do remetente (`pushName`) nas mensagens
- Smart scroll-to-bottom: só força descida se o chat mudou; preserva posição ao carregar mais

### Arquivos modificados

**Motor:**
- `plugins/ext-api/backend/routes.ts` — `POST /inbox/:chatId/read`, `PATCH /inbox/:chatId/archive`, filtro `@lid/@broadcast`, campos `pushName`/`senderJid`, `message.update` no hookBus
- `backend/src/modules/whatsapp/connection/WhatsAppConnectionManager.ts` — emite `ext:message.update`; chama `syncExpiredAvatars()` 30s após conectar
- `backend/src/modules/whatsapp/handlers/ContactHandler.ts` — `syncExpiredAvatars()`

**Satélite:**
- `services/api.ts` — `markRead`, `archive`; campos `pushName`/`senderJid` em `NwMessage`
- `pages/WhatsAppInbox.tsx` — paginação infinita, grupos, smart scroll, WS `message.update`

---

## ✅ Fase 4 — Mídia e Arquivos — CONCLUÍDA

**Concluída em:** 2026-04-17

### Motor — endpoint de mídia

| Endpoint | Comportamento |
|---|---|
| `GET /media/:messageId` | Redirect 302 se URL externa (CDN/Wasabi); stream com Range se arquivo local |

- `Content-Disposition: inline` para imagens/áudio/vídeo; `attachment` para documentos
- `Accept-Ranges: bytes` + suporte a `Range:` — essencial para `<audio>` e `<video>` no browser
- `Cache-Control: private, max-age=3600`

### UI — suporte completo a tipos de mídia

| Tipo | Renderização |
|---|---|
| `IMAGE` | `<ImageBubble>` com lazy loading + skeleton |
| `AUDIO` / `PTT` | `<audio controls>` nativo |
| `VIDEO` | `<video controls>` com aspect-ratio |
| `DOCUMENT` / `STICKER` | link de download |

- Helper `nwMediaUrl(msg)` — constrói URL via proxy `/api/nw/v1/media/:id`

### Arquivos modificados

**Motor:**
- `plugins/ext-api/backend/routes.ts` — `GET /media/:messageId` com stream/Range/redirect

**Satélite:**
- `pages/WhatsAppInbox.tsx` — `nwMediaUrl()`, `ImageBubble`, players nativos por tipo

---

## ✅ Fase 5 — Webhooks — CONCLUÍDA

**Objetivo:** Satélite reage a eventos sem conexão WS aberta (automações de negócio).  
**Concluída em:** 2026-04-17

### Motor — CRUD de webhooks

| Endpoint | Função |
|---|---|
| `GET    /webhooks` | Lista webhooks do tenant |
| `POST   /webhooks` | Registra novo webhook |
| `DELETE /webhooks/:id` | Remove webhook |
| `PATCH  /webhooks/:id` | Ativa/desativa webhook |

### Motor — dispatcher

- `plugins/ext-api/backend/webhook-dispatcher.ts`
- Subscreve `ext:message.new` e `ext:session.status` no hookBus
- HMAC-SHA256: `x-nw-signature: sha256=<hmac>` calculado sobre `JSON.stringify(body)`
- Timeout de 10s por request (`AbortController`), dispatch paralelo (`Promise.allSettled`)
- `tenantId` removido do payload antes de enviar

### Satélite — receptor

- `plugins/newwhats/webhook-receiver.js` — `POST /api/webhook/newwhats`
- Valida `x-nw-signature` com `timingSafeEqual` (constant-time)
- Usa `req.rawBody` (capturado pelo `verify` callback do `express.json()`)
- Lê `webhook_secret` de `plugin_configs` por chave `webhook_secret`
- Handlers: `message.new` → insere em `whatsapp_logs`; `session.status` → insere em `whatsapp_logs`

### Motor — modelo de dados

```prisma
model ExtWebhook {
  id        String   @id @default(uuid())
  tenantId  String
  url       String
  events    String[]   // ["message.new", "session.status"]
  secret    String
  active    Boolean  @default(true)
  createdAt DateTime @default(now())
  tenant    User @relation(fields: [tenantId], references: [id], onDelete: Cascade)
  @@map("ext_webhooks")
}
```

> Tabela criada via SQL direto (`CREATE TABLE IF NOT EXISTS`) para não acionar `prisma db push` — evita drop das tabelas `sec_*` com dados.

### Configuração ativa (produção)

- Secret armazenado em `plugin_configs`: `webhook_secret = e75c28beaf1fe5f68330364c90eb13f9...`
- Webhook registrado no motor: `POST /api/webhook/newwhats` (127.0.0.1:4001), eventos `message.new` + `session.status`, ativo
- Teste end-to-end confirmado: `{"ok":true}` ✅

### UI — página de gerenciamento

- `pages/WhatsAppWebhooks.tsx` — listagem, criação, toggle ativo/inativo, exclusão com dupla confirmação
- `App.tsx` — rota `/wa-webhooks` + item de menu

### Arquivos criados/modificados

**Motor:**
- `plugins/ext-api/backend/routes.ts` — CRUD `/webhooks`
- `plugins/ext-api/backend/webhook-dispatcher.ts` — novo
- `plugins/ext-api/index.ts` — `buildWebhookDispatcher(prisma, hooks)`
- `backend/prisma/schema.prisma` — modelo `ExtWebhook`

**Satélite:**
- `plugins/newwhats/webhook-receiver.js` — novo
- `plugins/newwhats/index.js` — `createWebhookReceiver()` registrado
- `server.js` — `express.json({ verify: (req, _res, buf) => { req.rawBody = buf.toString('utf8') } })`
- `services/api.ts` — `getWebhooks`, `createWebhook`, `deleteWebhook`, `toggleWebhook`; interface `NwWebhook`
- `pages/WhatsAppWebhooks.tsx` — novo
- `App.tsx` — rota + menu

---

## ✅ Fase 6 — Secretária Virtual — CONCLUÍDA

**Objetivo:** IA atende mensagens WhatsApp com contexto persistido por conversa.  
**Concluída em:** 2026-04-17

### Motor — endpoint

| Endpoint | Função |
|---|---|
| `POST /secretaria/ask` | Envia mensagem ao `ProtocolEngine`; retorna resposta da IA |

**Body:** `{ chatId: string, message: string, contactName?: string, agentId?: string }`  
**Response:** `{ reply: string, conversationId: string, protocolNumber: string }`

### Como funciona

1. A chave de contexto é `"<tenantId>:<chatId>"` — garante isolamento multi-tenant
2. Motor busca `sec_conversations` com `ext_chat_id = chave` e `status = 'active'`
3. Se não existir: cria conversa nova ligada ao primeiro agente ativo (ou ao `agentId` informado)
4. Delega ao `ProtocolEngine` (plugin `secretaria`) — suporta OpenAI, Gemini, Anthropic, Ollama em cadeia de fallback
5. Contexto é sumarizado automaticamente a cada 10 trocas (economia de tokens)
6. Retorna `reply`, `conversationId` e `protocolNumber` para rastreabilidade

### Migração de schema

```sql
-- Adicionado a sec_conversations via alterTable idempotente em secretaria/migrate.ts
ALTER TABLE sec_conversations ADD COLUMN IF NOT EXISTS ext_chat_id VARCHAR(300);
CREATE INDEX IF NOT EXISTS idx_sec_conv_ext_chat ON sec_conversations (ext_chat_id);
```

### UI — botão "Secretária IA" no inbox

- Botão roxo ✨ (Sparkles) aparece na barra de composição quando há texto digitado
- Ao clicar: envia o texto atual como pergunta à IA, preenche o campo com a resposta
- O agente humano revisa/edita antes de enviar via WhatsApp
- Estado de loading com spinner animado durante a chamada

### Teste end-to-end confirmado

```bash
POST /api/ext/v1/secretaria/ask
x-nw-key: nw_test_alemao_integration_key_001
{ "chatId": "5511999999999@s.whatsapp.net", "message": "Quero agendar uma consulta" }

→ { "reply": "Oi! Temos horários disponíveis na sexta-feira, 17 de abril: 09:00–10:00...",
    "conversationId": "b4fc2124-...", "protocolNumber": "170426025928" }
# Segunda chamada reutiliza mesmo conversationId — contexto persistido ✅
```

### Arquivos criados/modificados

**Motor:**
- `plugins/secretaria/migrate.ts` — coluna `ext_chat_id` + índice em `sec_conversations`
- `plugins/ext-api/backend/routes.ts` — `POST /secretaria/ask`; imports `Knex`, `PluginConfigStore`, `ProtocolEngine`
- `plugins/ext-api/index.ts` — `db` e `config` passados para `buildExtRoutes`

**Satélite:**
- `services/api.ts` — `secretariaAsk()`; interface `NwSecretariaReply`
- `pages/WhatsAppInbox.tsx` — estado `askingAi`, handler `handleAskAi`, botão Sparkles no `InputBar`

---

## Regras transversais

- Nunca quebrar contrato silenciosamente — mudança de payload → nova versão `/v2/`
- Rate limit por `integration_key` obrigatório — ✅ implementado desde Fase 1 (120 req/min)
- Isolamento multi-tenant: `integration_key` resolve `tenantId` — ✅ implementado
- Toda funcionalidade nova como plugin no motor (`/plugins/ext-api/`)
- Compilar TypeScript após cada mudança: `cd /home/deploy/projetos/newwhats.local/plugins && npx tsc -p tsconfig.json`
- `noEmitOnError: false` no tsconfig — erros em plugins não relacionados não bloqueiam a compilação

---

## Referências rápidas

| Item | Valor |
|---|---|
| Motor | `/home/deploy/projetos/newwhats.local` |
| Satélite | `/home/deploy/projetos/alemaoconveniencias.local` |
| Plugin ext-api | `newwhats.local/plugins/ext-api/` |
| Plugin newwhats (satélite) | `sistema-nuvem/plugins/newwhats/` |
| Plugin secretaria | `newwhats.local/plugins/secretaria/` |
| Serviço motor | `newwhats-backend.service` (porta 8008) |
| Serviço satélite | `alemaoconveniencias.service` (porta 4001) |
| Admin panel | `http://alemaoconveniencias.local:4001/admin-v2/` |
| API key de teste | `nw_test_alemao_integration_key_001` (tenant: `ruibto@gmail.com`) |
| Webhook secret | `plugin_configs` → `plugin_id='newwhats'`, `key='webhook_secret'` |
