Drop internal WhatsApp bridge plans; keep wa.me only.

Unofficial linked-device bridges risk banning staff phone numbers; D3–D5 stay wontfix and their plan files are removed.
This commit is contained in:
Francisco Ignácio Fontoura Solla 2026-08-02 13:38:01 -03:00
parent 8c6b838797
commit a7e1a8f2e7
10 changed files with 8 additions and 442 deletions

View file

@ -133,7 +133,7 @@ alwaysApply: false
- **A10 entregue em código (2026-07-23; deploy pendente com remodelagem):** cenários pessimista/média/otimista em `votePledge.estimatedVotes` e `municipality.expectedVotes` (groups + migration `20260723_124200_add_vote_estimate_scenarios`, backfill scalar→`central`); agregação por cenário em `votePledgeData`/`municipalityMapData`/overview/dashboard; seletor de cenário no mapa 2026; lista/dashboard com média + faixa; liderança intacta (`declaredVotes` único). Plano: [`cenarios-estimativa-votos.md`](../../../docs/plans/cenarios-estimativa-votos.md). Desbloqueia **E8**.
- **Polish overview “Votos estimados” (2026-07-23):** `MunicipalityEstimateScenarioProvider` sincroniza Cenário do mapa com overview + células da lista; seletor Cenário visível em qualquer ano (hint quando ≠2026); labels `Média nas Praças filtradas` + `Pessimista · Otimista`; `sem estimativa` sob o hero. Critique: `.impeccable/critique/2026-07-23T21-29-20Z__src-components-campaign-municipalitylistoverview-tsx.md`.
- **Cenário no overview (2026-07-23):** seletor relocado para a visão geral; no mapa só com Ano=2026; disclaimer inline → `CampaignInfoHint` (Popover `?`); flash no overview ao trocar. Critique: `.impeccable/critique/2026-07-23T21-52-39Z__src-components-campaign-municipalitymappanel-tsx-cenario.md`. **capture-review-debts:** 0 IDs no roadmap; flash compartilhado adiado no plano A10 (gatilho: 2º flash client-side em `/campanha`).
- **Programa WhatsApp interno D3–D5 registrado (2026-07-23):** comunicação 1:1 staff↔lideranças via bridge não oficial (não WABA / não massa); **número pessoal** de cada assessor/CG com pareamento QR no app (parece chat pessoal — pedido produto); fatias D3 multi-sessão → D4 envio pela sessão do ator → D5 inbox da própria sessão→rascunhos; baixa/média prioridade; Consent `campanha-whatsapp-canal`. Plano-mestre: [`whatsapp-interno-campanha.md`](../../../docs/plans/whatsapp-interno-campanha.md).
- **WhatsApp interno D3–D5 descartado (2026-08-02):** Issues #30–#32 `wontfix`. Bridge não oficial (Baileys/Evolution/WAHA / linked device) arrisca ban do número pessoal do staff (ToS Meta + fingerprint de N números no IP de datacenter). Canal no produto = **só deep link `wa.me`** (convites, share, kit de apoiador). Planos `whatsapp-interno-campanha.md` / `whatsapp-canal-fundacao.md` / `whatsapp-envio-liderancas.md` / `whatsapp-sugestao-atualizacoes.md` removidos.
- **Discovery "Inteligência de campanha" aprovado (2026-07-21):** ciclo literatura→persona→entrevista sintética concluído e colaborado; embasamento canônico em [`docs/research/`](../../../docs/research/) (compêndio ~67 fontes, persona, relatório com kernel Rumelt + playbook de 25 padrões dado→decisão P/T/I/J/K + anti-goals). Programa de produto registrado no roadmap como **E8–E16 + B13 + C12** (+ adjacentes A11/E17 desde 2026-07-24; [plano-mestre `inteligencia-campanha.md`](../../../docs/plans/inteligencia-campanha.md)) — conta da cadeira, fila de alocação, registro-fundação (trajetória de pledge/sinais/decisões), classificação relativa, escala relativa no mapa, níveis N0–N4, motor de sugestões (humano no loop), camada TI, planejador de giros, backtest. Decisões de produto travadas: % estadual absoluto é anti-métrica; denominadores usam válidos projetados (nunca aptos); sugestão nunca decide sozinha; nível de envolvimento não vaza para fora do staff.
- **B12 entregue em código (2026-07-21; deploy pendente com remodelagem):** `fitToKeys` + `interactiveKeys` no Mapa das Praças (`municipalitiesByIbgeCode`); `canonicalMapKeysKey`; hover stroke-only (fill inalterado); clear síncrono no mouseout; fit ao footprint filtrado sem re-zoom em Ano/Escala. Plano: [`aproximar-mapa-pracas.md`](../../../docs/plans/aproximar-mapa-pracas.md).
- **B6 entregue em código (2026-07-21; deploy pendente com remodelagem):** `BahiaMap` setStyle incremental — layer GeoJSON estável entre troca de ano/métrica/escala; `pathByKeyRef` + restyle O(2) no hover/select; `fitBounds` só em `mode`/`highlightKeys`; helpers puros em `bahiaMapStyle.ts` + unit `bahiaMapStyle.unit.spec.ts`. Plano: [`escala-dry-pos-b3.md`](../../../docs/plans/escala-dry-pos-b3.md). Fecha débitos de perf de **B10** e **B11**.
@ -208,7 +208,7 @@ alwaysApply: false
- Em 2026-07-23, **access explícito em toda collection (hardening Fase 0):** o default do Payload ("qualquer autenticado pode CRUD") é tratado como bug; toda collection declara `access` explícito — `users`/`signature`/`subscription`/`consent` só Payload admin (`payloadAdminOnly`), escrita de CMS admin-only com leitura pública preservada. JWTs de `campaignUser` não operam mais essas collections via `/api`. **Rejeitado:** confiar no default do Payload; RBAC só na camada de UI.
- Em 2026-07-23, **padrões de engenharia vigentes (hardening Fases 1–6):** zero lint warnings (`--max-warnings=0`); `as never` banido (helpers tipados `hookFilledCreateData` / `stub<T>` nos testes); knip no CI com files/dependencies em nível de erro; dead code se apaga imediatamente após qualquer mudança; convenção `lib/` (dado estático + lógica pura) vs `utilities/` (runtime Payload/Next); caching ladder (React `cache()` → `unstable_cache`+tag → artefato commitado). Regra sempre aplicada: [`.cursor/rules/engineering-standards.mdc`](../engineering-standards.mdc); débitos: [`docs/TECH-DEBT.md`](../../../docs/TECH-DEBT.md).
- Em 2026-07-23, **auto-save sem botão Salvar:** campos de edição devem persistir automaticamente sempre que for seguro (change de select, blur ou debounce curto); sem botão “Salvar” em edição in-context / campo único. Submit explícito só para formulários multi-campo atômicos, nota/confirmação ou consentimento. Princípio 4 de [`PRODUCT.md`](../../../PRODUCT.md); regra Cursor [`.cursor/rules/campanha-edit-where-you-see.mdc`](../campanha-edit-where-you-see.mdc). **Rejeitado / supersede:** preferência B9 por Enter + botão Salvar em quick-edit; Save “por precaução” em Popover.
- Em 2026-07-23, **WhatsApp interno 1:1 (D3–D5):** bridge não oficial; **sessão = número pessoal** de cada `coordinator`/`advisor` (QR no `/campanha`, estilo WhatsApp Web); liderança recebe no chat de sempre; Teqo = system of record; humano no loop em D5; sidecar multi-sessão. **Rejeitado:** WABA; massa; chip institucional único; auto-commit; inbox cruzado CG↔assessores na v1. Cortável (`wa.me`).
- Em 2026-08-02, **WhatsApp no produto = só `wa.me` (supersede D3–D5 de 2026-07-23):** bridge não oficial / QR / sidecar / número pessoal pareado descartados — risco de ban do chip do assessor/CG. Deep link `wa.me` (humano envia no app dele) permanece para convites, share e kit. **Rejeitado também:** WABA; disparo em massa (TSE art. 33); chatbot eleitoral. Issues #30–#32 `wontfix`.
- Em 2026-07-23, **estimativas staff em três cenários (A10):** pessimista / média (`central`) / otimista no pledge e no total da Praça; liderança declara um valor; default de leitura = média; não fundir com `voteGoals`. **Rejeitado:** um único estimado; expor faixa à liderança; três choropleths. Desbloqueia E8.
- Em 2026-07-21, **embasamento de produto/domínio via `docs/research/`:** decisões de análise territorial e mapa citam o relatório de discovery (aprovado 2026-07-21) antes de inventar métrica nova. Regras derivadas: leitura relativa (quantis/LQ/rank/captura) sobre % absoluto; limiares defesa/ataque 35/20/10 degeneram para DF e serão recalibrados com âncoras relativas (E10); roll-off (brancos+nulos DF − majoritária) é o headroom de orientação; abstenção estrutural corrige denominador (válidos projetados) e não é mercado; agregação regional default = Território de Identidade com salvaguardas MAUP (praça manda no voto, TI na logística). **Rejeitado:** cartogramas; pesquisa estadual de intenção para DF; persuasão de território hostil como funcionalidade.
- Em 2026-07-21, **toolchain TypeScript dual:** `typescript` → `@typescript/typescript6@6.0.2` (API/editor/ESLint + `tsc6`); `typescript-7` → `typescript@7.0.2` (Go `tsc` para `pnpm typecheck`). Motivo: typescript-eslint peer `<6.1.0` e API estável só em TS ~7.1. Não usar `@typescript/native-preview`/`tsgo` (canal nightly). `typecheck:tsc6` existe para paridade. Ambient CSS/SCSS em `src/css-modules.d.ts` (TS6 `noUncheckedSideEffectImports`).

View file

@ -4,6 +4,8 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente
---
**Recently resolved (2026-08-02):** **WhatsApp interno D3–D5 descartado** — Issues #30–#32 `wontfix` (bridge Baileys/Evolution/WAHA / linked device): risco de ban do número pessoal do staff. Canal no produto = só `wa.me`. Planos `whatsapp-interno-campanha.md`, `whatsapp-canal-fundacao.md`, `whatsapp-envio-liderancas.md`, `whatsapp-sugestao-atualizacoes.md` removidos; refs em `notifications.md` / convite·email listas / fluxos-ação / regra `nucleos-eleitorais` atualizadas.
**Recently resolved (2026-08-02):** **Agent pool hard max → 12** — `POOL_HARD_MAX_SLOTS` de 8 → 12 (`scripts/lib/agent-pool-state.mjs`); default continua 5. Skill `agent-pool` alinhada.
**Recently resolved (2026-08-01):** **B102** (Issue #129) — crash na busca do bottom drawer (focus/digitação): `TooltipProvider` do layout `(app)` envolvia só `{children}` e o drawer (irmão do scrollport) renderizava hits `priority:alta` via `CampaignHoverTooltip` sem provider. Fix: provider envolve `CampaignAppScrollChrome`; pins unit focus→suggest e type→search. Plano: [`crash-busca-bottom-drawer.md`](docs/plans/crash-busca-bottom-drawer.md).

View file

@ -88,7 +88,7 @@ Componentes:
- Convite de acesso ao app (`kind: 'login'`) na lista — só existe no detalhe hoje (condicionado a `supportStatus === 'engajado'`); o pedido do usuário é só o convite de completar cadastro. Se precisar, é extensão trivial do mesmo componente, mas fora deste item.
- Disparo em massa / seleção múltipla de lideranças para convite em lote — vedado por Res. TSE 23.610 art. 33 e fora do desenho do produto (ver `docs/roadmap.md` § Fora de escopo); esta ação é sempre 1:1, uma linha por vez.
- Editar/validar o telefone da liderança a partir da lista — isso é o formulário de edição existente (`/campanha/liderancas/[id]/editar` ou equivalente); esta ação só lê `contact.phone`.
- Programa WhatsApp interno D3–D5 (bridge de sessão, inbox) — este item usa o mesmo `wa.me` manual de sempre, não abre canal novo.
- Bridge WhatsApp / sessão pareada (D3–D5 descartados) — este item usa só `wa.me` 1:1.
## Rabbit holes

View file

@ -111,7 +111,7 @@ Depth check: reusa `CampaignTable`, `phone` helpers, padrão visual do B19; extr
- Editar contato na ficha interna / action `updateLeadershipContact` — Adiado.
- Busca por e-mail/telefone; filtros novos; sort por coluna.
- Painel de lideranças no município (`MunicipalityLeadershipsPanel`) / dossiê.
- Programa WhatsApp interno **D3–D5** (bridge) — aqui é só `wa.me` 1:1 como no B19.
- Bridge WhatsApp / sessão pareada (D3–D5 descartados) — aqui é só `wa.me` 1:1 como no B19.
- Seletor de colunas (**B17**) — se existir, estas colunas entram como `defaultVisible` quando B17 chegar às lideranças (fora deste item).
## Rabbit holes

View file

@ -245,5 +245,5 @@ Não competir com os botões.
- Redesign completo do detalhe / abas
- Remover o mapa
- Substituir o sistema de listas
- App nativo / WhatsApp bridge (D3)
- App nativo / bridge WhatsApp (D3–D5 descartados — só `wa.me`)
- Treinar o CG no IA atual

View file

@ -24,7 +24,7 @@ Coordenadores e lideranças precisam ser avisados de eventos da campanha (convit
- **Push web** via Push API + Service Worker + VAPID (entrega em segundo plano, inclusive com o app fechado).
- **Sino de notificações in-app** dentro do `/campanha` (central de avisos lidos/não lidos).
Notificações por WhatsApp Business API ficam **fora** deste plano (Meta + Res. TSE 23.610 art. 33). Canal WhatsApp **1:1 interno** staff↔lideranças (bridge não oficial) é o programa **D3–D5** — [whatsapp-interno-campanha.md](whatsapp-interno-campanha.md) — complementar a push/sino, não substituto.
Notificações por WhatsApp Business API ficam **fora** deste plano (Meta + Res. TSE 23.610 art. 33). Bridge WhatsApp não oficial (Baileys/Evolution/WAHA / linked device) também ficou **fora** — Issues D3–D5 (#30–#32) `wontfix`: risco de ban do número pessoal do staff. Canal WhatsApp no produto = só deep link `wa.me` (humano envia no app dele).
## Objetivos

View file

@ -1,127 +0,0 @@
# D3 — Fundação do canal WhatsApp interno
Status: rascunho
Atualizado em: 2026-07-23
Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha D, item D3; programa [whatsapp-interno-campanha.md](whatsapp-interno-campanha.md))
Impeccable: B — pareamento QR self-service por staff (sem inbox/compose de produto)
Appetite: ~3 dias eng; sidecar multi-sessão + migration + Consent + UI QR/status; **sem** envio/inbox de produto
Responsável: —
## Design (Impeccable)
Âncoras: `PRODUCT.md` (princípios 1 soberania, 2 clareza, 4 depth/simplicity) / `DESIGN.md` (register `product`) · tema `data-theme='campaign'` · shells `CampaignPageShell` · superfície de perfil existente (`/campanha/perfil` se houver).
Na implementação (`implement-roadmap-item`): craft compacto → critique → polish (fluxo QR + estados; sem redesign de CRM).
Brief compacto:
- **Persona / contexto:** Assessor ou Coordenador Geral no celular/notebook quer **ligar o próprio Zap** uma vez e seguir falando com lideranças “como sempre” — só que o Teqo passa a enxergar o fio.
- **Job principal:** parear a conta WhatsApp **pessoal** do `campaignUser` via QR no app e manter a sessão viva no sidecar.
- **Estratégia de cor:** Restrained; sessão caída = `destructive`/`cadence-overdue`; aviso ToS em muted, não modal de terror.
- **Edit where you see:** pairing self-service no **perfil** do staff (ação rara); não no CRM de lideranças.
- **Anti-goals:** número institucional único; admin pareando o chip de outra pessoa; chat UI; inbox; sync de histórico completo; esconder risco ToS/ban.
## Dados → decisão → apresentação
- **Vou apresentar dados?** Sim, superfície mínima neste item (status **da minha** sessão).
- **Decisões desbloqueadas:**
- Assessor/CG: “meu WhatsApp está conectado ao Teqo? preciso escanear de novo?”
- (Aggregate para D4/D5: sessão do ator healthy → habilitar compose/inbox daquela pessoa.)
- **Forma escolhida:** **número + contexto** (estado `conectado | aguardando QR | erro` + telefone mascarado da conta pareada + `lastSeenAt`). **Rejeitado:** dashboard de N sessões para o time inteiro na v1 (CG pode ter lista read-only “quem conectou” — Adiado); chart de uptime; KPI de conversas.
- **Profile:** categórico; 1 sessão por `campaignUser` staff.
- **Anti-goals de dado:** sem vanity de volume de msgs nesta fatia.
## Contexto
O programa exige que a liderança continue vendo o **Zap pessoal** do assessor/CG — não um robô institucional. Isso implica **N sessões** (uma por staff), pareadas no app (QR), não um chip compartilhado.
Socket WhatsApp não roda em Vercel serverless → sidecar multi-sessão. Sem fundação (sessão↔ `campaignUser`, adapter, log, Consent), D4/D5 não têm onde plugar.
## Objetivos
- Sidecar Node longo com **um** adapter não oficial, **N sessões** indexadas por `campaignUser.id` (só roles `coordinator` | `advisor`).
- Fluxo de pareamento no `/campanha`: `startPairing` → sidecar gera QR → UI mostra QR (refresh se expirar) → evento `paired` → status `connected` + telefone detectado (normalizado).
- App Next.js ↔ sidecar via API autenticada (HMAC); o Next **não** abre socket WA.
- Collections: `whatsappSession` (**1:1 com `campaignUser`**, unique) + `whatsappMessage` (log com `session` / `campaignUser`, direção, phones, texto, timestamps, match opcional `leadership`).
- Consent **`campanha-whatsapp-canal`** (fail-closed) + aceite explícito no UI de pairing (risco ToS/conta pessoal). Texto no lote jurídico.
- UI: status + QR + desconectar **só da própria sessão**; `leader` não pareia.
- Guardrails: allowlist inbound = phones de lideranças engajadas no CRM (e, para advisor, preferir as dos municípios acessíveis — pode afrouxar para “todas lideranças engajadas” se match for só por phone); rate limit por sessão; secrets em env.
## Decisões travadas
- **Sessão = `campaignUser` staff, número pessoal.** UNIQUE `(campaignUser)`. **Rejeitado:** sessão singleton institucional; parear número digitado sem QR; um CG operar o chip de um assessor.
- **Login/pareamento = QR no app (estilo WhatsApp Web).** **Rejeitado:** só deep-link `wa.me`; pairing por SMS inventado; exigir app Companion oficial da Meta como único caminho (se o adapter oferecer código de pareamento sem QR como fallback técnico, ok — UX primária = QR).
- **Sidecar multi-sessão fora do Vercel.** Credenciais por sessão em volume do worker. **Rejeitado:** Baileys dentro de Server Action; Cloud API / WABA.
- **Um adapter na v1.** Baileys _ou_ Evolution self-hosted — um só. **Rejeitado:** multi-provider.
- **Log mínimo pós-`pairedAt`; sem histórico prévio; sem mídia binária na v1.** **Rejeitado:** dump completo do chip; gravar chats com não-CRM.
- **Consent `campanha-whatsapp-canal` + copy de risco pessoal.** **Rejeitado:** hardcode de ID; silêncio sobre ban/ToS.
- **i18n/naming:** `whatsappSession`, `whatsappMessage`, `startWhatsappPairing`, `whatsappBridge`; UI pt-BR (“Conectar meu WhatsApp”, “Escaneie o QR com o celular”).
## Questões em aberto
- **Onde hospedar o sidecar?** **Opções:** Fly.io / Railway / VPS. **Recomendação:** Fly/Railway com volume; runbook na implementação. _(ops)_
- **CG vê quem do time está conectado?** **Opções:** não na v1 | lista read-only de status. **Recomendação:** não na v1 (só a própria sessão); lista de saúde do time = Adiado com gatilho no programa.
## Abordagem proposta
```mermaid
flowchart LR
Staff["Assessor / CG<br/>/campanha/perfil"]
QR["QR pairing<br/>própria sessão"]
Next["Next.js<br/>HMAC + queue"]
Worker["Sidecar<br/>N sessões WA"]
Sess["whatsappSession<br/>1:1 campaignUser"]
Log["whatsappMessage"]
CRM["leadership via phone"]
Staff --> QR --> Next
Next <--> Worker
Worker --> Sess
Worker --> Log
Log --> CRM
```
Componentes:
- **`whatsappBridge`** (`src/utilities/whatsappBridge.ts`, `server-only`): `startPairing(userId)`, `getSessionStatus(userId)`, `disconnect(userId)`, (D4) `enqueueSend(userId, …)`; timeout curto.
- **Sidecar** (`workers/whatsapp-bridge/`): map `campaignUserId → adapter session`; emite QR/status/inbound; allowlist CRM.
- **`whatsappSession`**: `user` → `campaignUser` (unique, required), `status`, `pairedPhone`, `pairedAt`, `lastSeenAt`, `lastError`.
- **`whatsappMessage`**: `session`/`user`, `direction`, `peerPhone`, `leadership` (nullable), `body`, `providerMessageId`, timestamps.
- **UI** `WhatsappPairingPanel` no perfil: QR image/data-URL ou SVG, polling/SSE de status, botão desconectar, aviso Consent/ToS.
- **Actions** `startWhatsappPairing` / `disconnectWhatsappSession` / `getWhatsappSessionStatus` — actor só na própria sessão (`user.id`).
- **Migration:** `pnpm migrate:create add_whatsapp_channel` — session + message; sem backfill.
- **Reuse:** `normalizeBrazilianPhone`, `campaignConsent`, `withPayloadTransaction`, `overrideAccess: false`.
## Dependências
- **Dura:** remodelagem deployada (`campaignUser` roles + `leadership`/`Contact`).
- **Suave:** Onda 0 (texto Consent); telefones reais nas lideranças.
- **Não depende de D2.**
## Não escopo
- Compose/envio → [D4](whatsapp-envio-liderancas.md).
- Inbox / rascunhos → [D5](whatsapp-sugestao-atualizacoes.md).
- Número institucional / WABA / massa → fora.
- Mídia no log; grupos; histórico pré-pairing → Adiado.
## Rabbit holes
- **N sockets × memória no worker.** Mitigação: lazy connect on pairing; disconnect idle com re-QR; monitorar RSS antes de escalar horizontalmente (sticky sessions por user se shardear — Adiado).
- **Staff desconecta no celular e o Teqo não sabe.** Mitigação: health ping + status `erro` + CTA re-parear no compose D4.
- **Parear conta errada (pessoal demais / família).** Mitigação: mostrar telefone detectado pós-QR + “Desconectar”; copy de que deve ser o número que as lideranças já usam.
## Adiado com gatilho
- **Retention/expurgo do log.** Revisitar quando: volume ou pedido jurídico.
- **Lista CG “quem conectou”.** Revisitar quando: onboarding do time travar sem visão de cobertura.
- **Advisor-only allowlist por município no inbound.** Revisitar se falso positivo de match cruzado aparecer.
## Referências
- [whatsapp-interno-campanha.md](whatsapp-interno-campanha.md)
- `docs/roadmap.md` — Trilha D
- `src/utilities/phone.ts`, `campaignConsent.ts`, `campaignAccess.ts`
- `docs/plans/notifications.md` — D2
- `PRODUCT.md` / `DESIGN.md`
- AGENTS.md — Consent, migrations, campaign auth

View file

@ -1,104 +0,0 @@
# D4 — Envio WhatsApp 1:1 para lideranças
Status: rascunho
Atualizado em: 2026-07-23
Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha D, item D4; programa [whatsapp-interno-campanha.md](whatsapp-interno-campanha.md))
Impeccable: B — compose encaixado no CRM de liderança / detalhe (sem rota de “marketing blast”)
Appetite: ~1–2 dias eng; actions + UI compose + templates curtos; reusa sessão pessoal D3
Responsável: —
## Design (Impeccable)
Âncoras: `PRODUCT.md` (princípios 2, 3 Edit where you see, 4) / `DESIGN.md` (register `product`) · tema `campaign` · precedente `LeadershipInviteButtons` / share `wa.me`.
Na implementação: craft compacto → critique → polish.
Brief compacto:
- **Persona / contexto:** Assessor na estrada cobra atualização; CG manda um “como está Orolândia?” — a liderança deve ver a mensagem **no chat pessoal de sempre** com aquela pessoa.
- **Job principal:** enviar **uma** mensagem 1:1 pela **sessão WhatsApp do próprio ator** para **uma** liderança do escopo.
- **Estratégia de cor:** Restrained; ícone WA ok; primário Signal Red.
- **Edit where you see:** sim — Sheet “Mensagem” no detalhe `/campanha/liderancas/[id]`.
- **Anti-goals:** multi-destinatário; enviar pela sessão de outro staff; número institucional; templates de propaganda; matar `wa.me` de convites.
## Dados → decisão → apresentação
Dados: N/A — superfície de ação (compose). Feedback de entrega = Feel the action (§7), não KPI.
## Contexto
Com [D3](whatsapp-canal-fundacao.md), cada staff tem (ou não) a própria sessão pareada. O valor de produto é: **parece Zap pessoal** — a liderança não “fala com a campanha”, fala com o assessor/CG que já conhece.
## Objetivos
- Action `sendLeadershipWhatsappMessage({ leadershipId, body })`: access OK → resolve phone → exige **`whatsappSession` do `req.user` em `connected`** + Consent → `enqueueSend(actorUserId, …)` → log outbound com `user` = ator.
- UI compose: textarea + templates curtos editáveis; se sessão down → CTA “Conectar meu WhatsApp” (perfil D3) **e** fallback `wa.me` (“Abrir no seu WhatsApp”).
- Rate limit por ator/hora e por destinatário; **hard reject** N>1 destinatários.
- Guardrails: só `coordinator`/`advisor`; advisor só lideranças dos municípios acessíveis; nunca `supporter`; nunca enviar usando sessão de terceiro.
## Decisões travadas
- **Remetente = sessão do ator logado.** A liderança recebe do número pessoal pareado daquela pessoa. **Rejeitado:** “enviar como campanha”; CG disparar pelo chip de um assessor; escolher remetente na UI.
- **Um destinatário por envio.** **Rejeitado:** lista/BCC; “todas as lideranças do município”.
- **Escopo = access da liderança.** **Rejeitado:** telefone livre digitado.
- **`wa.me` = fallback + convites.** **Rejeitado:** migrar convites para o bridge nesta fatia.
- **i18n/naming:** `sendLeadershipWhatsappMessage`, `LeadershipWhatsappCompose`; copy pt-BR (“Enviar pelo meu WhatsApp”).
## Questões em aberto
- **Templates: hardcoded vs collection?** **Opções:** 3–5 strings em código | collection. **Recomendação:** constantes na v1.
- **Thread no detalhe?** **Opções:** só compose | compose + últimas N do par **nesta sessão**. **Recomendação:** últimas 10 msgs do par na sessão do ator (não misturar threads de outro assessor). _(assumido)_
## Abordagem proposta
```mermaid
flowchart LR
UI["LeadershipWhatsappCompose"]
Action["sendLeadershipWhatsappMessage"]
Sess["whatsappSession do ator"]
Bridge["enqueueSend(actorId, …)"]
Log["whatsappMessage outbound"]
Lead["leadership phone"]
UI --> Action
Action --> Sess
Action --> Lead
Action --> Bridge --> Log
```
Componentes:
- **`LeadershipWhatsappCompose`**: Sheet; estados connected / needs-pairing / sending / failed; deep-link perfil se unpaired.
- **Action** em `actions/whatsapp.ts`: `overrideAccess: false`; nunca aceita `sessionUserId` do client (só `req.user.id`).
- **Reuse D3:** bridge, session, Consent, phone normalize.
- **Migration:** nenhuma se D3 já tem `user` no log.
## Dependências
- **Dura:** **D3** (sessão pessoal + log + Consent).
- **Suave:** telefones reais nas lideranças.
## Não escopo
- Inbox / rascunhos → [D5](whatsapp-sugestao-atualizacoes.md).
- Apoiadores / grupos / auto-resposta → fora.
- Push “mensagem enviada” → D2.
## Rabbit holes
- **Templates ricos.** Mitigação: `{nome}`, `{praca}` no máximo.
- **Fila com retry infinito.** Mitigação: retry curto + `failed` visível.
## Adiado com gatilho
- Bulk UI. Revisitar só com ok jurídico explícito.
- CG enviar “em cópia” vendo thread do assessor. Revisitar com o item de inbox agregado do programa (acesso cruzado).
## Referências
- [whatsapp-interno-campanha.md](whatsapp-interno-campanha.md), [whatsapp-canal-fundacao.md](whatsapp-canal-fundacao.md)
- `src/components/campaign/LeadershipInviteButtons.tsx`
- `src/app/(campaign)/campanha/(app)/liderancas/[id]/page.tsx`
- `src/utilities/campaignAccess.ts`, `phone.ts`
- `PRODUCT.md` / `DESIGN.md`
- AGENTS.md — access, transactions

View file

@ -1,92 +0,0 @@
# WhatsApp interno da campanha (programa D3–D5)
Status: rascunho
Atualizado em: 2026-07-23
Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha D, programa D3–D5)
Impeccable: N/A — plano-mestre; classes por fatia (D3 A/B, D4 B, D5 C)
Appetite: programa ~5–8 dias eng fatiados; **não** implementar como monólito
Responsável: —
## Dados → decisão → apresentação
Dados: N/A neste plano-mestre — cada fatia declara a sua. O programa existe para **fechar o Little Hire** (confiança no quadro atualizado): o canal real (ZAP) deixa de competir com o Field Desk e passa a alimentá-lo.
## Contexto
Feedback de produto (2026-07-23) + evidência de discovery/entrevista: lideranças e staff dificilmente sentirão segurança registrando só no `/campanha` enquanto o trabalho real continua no WhatsApp. O coordenador geral descreve o ZAP como o campo (“tudo após o ZAP”; mensagem > ligação). O [`IMPROVE-APP-PLAN.md`](../IMPROVE-APP-PLAN.md) nomeia o leak: assessores param de atualizar → WhatsApp/planilha → ferramenta defasada → abandono.
Hoje o produto só usa WhatsApp como **atalho `wa.me`** (convites, share, kit de apoiador): o humano envia no app dele; o Teqo não vê a conversa nem grava o sinal. **D2** (push + sino) cobre avisos _dentro_ do app — complementar, não substituto do canal de campo.
**Fronteira legal/produto já travada no roadmap:** WhatsApp Business API e disparo em massa continuam **fora de escopo** (Meta veda campanhas políticas; Res. TSE 23.610 art. 33). Este programa é **comunicação 1:1 interna** staff ↔ lideranças já cadastradas (`Contact`/`leadership`) — não propaganda a terceiros nem blast.
**Decisão de produto (2026-07-23):** o envio deve parecer o canal **pessoal** do assessor/coordenador (é o que a liderança espera), não um número institucional da campanha. Pareamento no próprio `/campanha` (QR, estilo WhatsApp Web).
## Objetivos do programa
- Trazer o canal ZAP para _dentro_ do fluxo de poder do Field Desk sem virar o sistema de registro (princípio 1 de `PRODUCT.md`: atalho pragmático; verdade no Teqo).
- Cada `coordinator`/`advisor` vincula **o próprio WhatsApp pessoal** e fala com lideranças **como se fosse o chip dele** — a liderança recebe no chat pessoal de sempre.
- Fatiar em três entregas shipáveis (D3 → D4 → D5), cada uma com appetite próprio.
- Humano no loop em qualquer escrita de domínio (`municipalityUpdate`, demanda, pledge) — alinhado a E11/C12.
- Fail-closed em Consent, access por papel/município, e auditoria mínima das mensagens processadas.
## Fatias (ordem de execução)
| ID | Plano | Entrega essencial | Classe | Appetite | Depende de |
| --- | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ------ | -------- | ----------------------------------- |
| D3 | [whatsapp-canal-fundacao.md](whatsapp-canal-fundacao.md) | Bridge multi-sessão + **QR por staff** + log + Consent; **zero** envio em massa | B | ~3d | remodelagem em prod (suave: Onda 0) |
| D4 | [whatsapp-envio-liderancas.md](whatsapp-envio-liderancas.md) | Compose/envio 1:1 pela **sessão do ator** (CRM → liderança; parece chat pessoal) | B | ~1–2d | **D3** |
| D5 | [whatsapp-sugestao-atualizacoes.md](whatsapp-sugestao-atualizacoes.md) | Inbox inbound **na sessão de quem recebeu** → rascunhos com aceite humano | C | ~2–3d | **D3** (suave: D4, C12) |
```mermaid
flowchart TD
D3["D3 Fundação<br/>QR por campaignUser"]
D4["D4 Envio 1:1<br/>sessão do ator"]
D5["D5 Inbox → rascunhos<br/>humano confirma"]
D3 --> D4
D3 --> D5
D4 -.thread/contexto.-> D5
```
## Decisões travadas (programa)
- **Canal interno 1:1, framework não oficial permitido.** Destinatários = lideranças já no CRM; never blast a apoiadores/eleitores. **Rejeitado:** WhatsApp Business API; disparo em massa / listas (TSE art. 33); bot que responde sozinho a terceiros.
- **Número pessoal por staff (`campaignUser`), não linha institucional.** Cada assessor/coordenador geral pareia o próprio WhatsApp; a liderança vê o remetente que já conhece. **Rejeitado:** chip único da campanha (quebra a relação pessoal — pedido explícito de produto 2026-07-23); um único CG enviando “em nome de” todos os assessores.
- **Pareamento no app via QR (estilo WhatsApp Web).** Staff abre `/campanha`, inicia pareamento, escaneia com o celular; sessão fica no sidecar. **Rejeitado:** só `wa.me` forever; pedir senha/OTP inventado; forçar app nativo Meta.
- **Teqo permanece system of record.** Mensagens são evidência/auditoria; commits de domínio só após ação humana (D5). **Rejeitado:** auto-criar `municipalityUpdate`/`votePledge` sem aceite.
- **Sidecar obrigatório (multi-sessão).** N sockets (um por staff vinculado); não vive em Vercel serverless. **Rejeitado:** “só mais uma API route” no Next.
- **Não substitui D2 nem `wa.me`.** D2 = push in-app; `wa.me` = fallback se a sessão do ator estiver down. **Rejeitado:** remover convites `wa.me` neste programa.
- **Risco ToS/ban é pessoal e explícito.** Consent + copy no pairing avisam que o vínculo usa cliente não oficial e pode afetar a conta WhatsApp da pessoa. **Rejeitado:** esconder o risco; usar conta do deputado/candidato como default.
## Questões em aberto (programa)
- **Onde vive o QR no IA?** **Opções:** `/campanha/perfil` | `/campanha/configuracoes/whatsapp` | Sheet no compose D4 (“conectar para enviar”). **Recomendação:** `/campanha/perfil` (self-service, cada um liga o seu) + estado “WhatsApp conectado/desconectado” no compose D4 com deep-link para parear. _(shape D3)_
- **LLM para classificar inbound em D5?** **Opções:** regras/heurística | LLM com revisão humana | só triagem manual. **Recomendação:** D5 v1 = heurística + templates + edição humana; LLM só com gatilho.
## Não escopo (programa)
- Business API / templates Meta / disparo a base nominal → continua fora de escopo.
- Chatbot eleitoral público / atendimento a cidadão → fora.
- Número institucional / “WhatsApp da campanha” compartilhado → fora (conflita com a decisão de canal pessoal).
- Substituir onboarding presencial / seed (Big Hire) → IMPROVE-APP-PLAN.
## Rabbit holes
- **Evolution API / Baileys / multi-device / grupos WA.** Mitigação: D3 escolhe **um** adapter; multi-**sessão** (N staff) sim; multi-**número por pessoa** e grupos = Adiado.
- **“Sincronizar todo o histórico do chip.”** Mitigação: só mensagens após pairing + filtro por telefones do CRM (e, no inbox, só o que chegou naquela sessão).
- **Espionar conversas pessoais do staff com não-lideranças.** Mitigação: allowlist estrita de phones do CRM; não indexar chats fora da allowlist.
## Adiado com gatilho
- Sessão secundária / segundo chip por pessoa. Revisitar quando: pedido explícito de um assessor com dois números de trabalho.
- LLM de classificação. Revisitar quando: D5 v1 com ≥50 rascunhos/semana e taxa de descarte >40% por “categoria errada”.
- Coordenador ver inbox agregado de todos os assessores. Revisitar quando: CG pedir visão transversal **e** jurídico ok com acesso cruzado a threads pessoais.
## Referências
- `docs/roadmap.md` — Trilha D; Fora de escopo (Business API / massa)
- Entrevista CG 2026-07-23 (`private/transcribe/general-coordinator-interview-20260723/`) — “ZAP é o campo”
- `docs/IMPROVE-APP-PLAN.md` — Little Hire / leak WhatsApp
- `PRODUCT.md` §1 soberania + atalho pragmático; §5 humano no loop
- `docs/plans/notifications.md` (D2) — complementar
- `src/utilities/phone.ts` — `buildWhatsAppUrl` (fallback `wa.me`)
- AGENTS.md — Contact, Consent por chave, campaign access, Vercel

View file

@ -1,113 +0,0 @@
# D5 — Sugestões de atualização a partir do WhatsApp inbound
Status: rascunho
Atualizado em: 2026-07-23
Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha D, item D5; programa [whatsapp-interno-campanha.md](whatsapp-interno-campanha.md))
Impeccable: C — superfície nova (inbox + revisão de rascunhos); sem design-ref
Appetite: ~2–3 dias eng; inbox staff + draft suggestions + aceite → domínio; **sem** auto-commit
Responsável: —
## Design (Impeccable)
Âncoras: `PRODUCT.md` (princípios 1, 2, 5 humano no loop, 7) / `DESIGN.md` (register `product`) · tema `campaign` · shells `CampaignPageShell`, feeds `MunicipalityUpdateFeed` / forms `MunicipalityUpdateForm` · paralelo E11 (menu + aceite/descarte).
Na implementação: shape → craft → critique → polish (classe C).
Brief compacto:
- **Persona / contexto:** Assessor que já conversou no ZAP com a liderança e precisa **não redigitar** o reporte no Teqo; Coordenador que quer o quadro atualizado sem caçar print no grupo.
- **Job principal:** transformar mensagem inbound em **rascunho editável** (`municipalityUpdate` / demanda / nota de voto) e só gravar no domínio após aceite humano.
- **Estratégia de cor:** Restrained; fila por frescor, não por “AI score”.
- **Edit where you see:** sim — aceitar/editar/descartar no card do rascunho; deep-link para o município/liderança.
- **Anti-goals:** bot que grava sozinho; LLM obrigatório na v1; inbox estilo WhatsApp Web clone; “caça” a mensagens de não-lideranças.
## Dados → decisão → apresentação
- **Vou apresentar dados?** Sim, superfície neste item (fila de rascunhos + origem).
- **Decisões desbloqueadas:**
- Assessor: “esta mensagem vira atualização do município X, demanda, ou só arquivo?”
- Assessor/CG: “aceito o texto sugerido ou edito antes de gravar?”
- Staff: “descarto (ruído / off-topic) para não poluir o frescor do município?”
- **Forma escolhida:** **lista/fila** de itens (mensagem → tipo sugerido → preview) + detalhe com editar/aceitar/descartar. **Rejeitado:** dashboard de % auto-classificados; chart de volume WA; mapa de mensagens.
- **Profile:** itens categóricos + texto; dezenas/dia típico (não milhares); granularidade liderança×município.
- **Anti-goals de dado:** sem vanity “msgs processadas”; sem auto-atualizar `lastUpdateAt` sem aceite (frescor mentiroso).
## Contexto
O Little Hire falha quando o reporte vive só no ZAP ([IMPROVE-APP-PLAN](../IMPROVE-APP-PLAN.md); entrevista CG: canal real = mensagem). D3 grava inbound no log; D4 cobre outbound. Falta o caminho **ZAP → registro** com humano no loop — o mesmo estatuto de E11 (“sugestão nunca decide”).
Domínio alvo já existe: `municipalityUpdate` (`MunicipalityUpdateForm` / feed), `campaignDemand`, `votePledge.declaredVotes` (liderança) / estimativas staff (assimetria).
## Objetivos
- Inbox staff (`/campanha/whatsapp` ou seção no dashboard): só mensagens inbound da **própria sessão** do ator (o Zap pessoal dele); match a `leadership` por phone; unmatched→triagem.
- Gerador de **draft** (heurística v1): classifica intent grosso (`atualizacao | demanda | voto | outro`) + sugere município (se liderança tem 1 município; se N, pede escolha) + texto pré-preenchido editável.
- Aceite cria o documento de domínio na transação (`withPayloadTransaction`), liga `whatsappMessage` → entidade criada (auditoria), respeita access/locks existentes (`municipalityUpdate` locks).
- Descarte registra motivo curto (opcional na v1) e marca mensagem como `ignored` — não some do log.
- Guardrails: **nunca** auto-create; inbox = sessão do ator (não ver thread do colega na v1); advisor ainda filtrável aos municípios acessíveis; `leader` não usa inbox; sem processar phones fora do CRM.
## Decisões travadas
- **Humano no loop obrigatório.** Draft ≠ commit. **Rejeitado:** auto-`municipalityUpdate` por NLP; webhook que escreve domínio direto.
- **Inbox = sessão pessoal do ator.** Alinhado a D3/D4 (canal pessoal). **Rejeitado:** inbox único da “campanha”; CG ler todas as threads dos assessores na v1 (Adiado no programa).
- **Heurística v1, não LLM.** Palavras-chave / botões de tipo manual + textarea. **Rejeitado:** LLM na v1 (custo, indeterminismo, appetite); ver Adiado.
- **Teqo é o registro; WA é evidência.** `lastUpdateAt` e versões (C12) só mudam no aceite. **Rejeitado:** usar timestamp da msg WA como frescor oficial sem commit.
- **i18n/naming:** `whatsappInboundDraft`, `suggestMunicipalityUpdateFromMessage`, `acceptWhatsappDraft`; rotas/params em inglês; UI pt-BR (“Sugestões do WhatsApp”).
## Questões em aberto
- **Rota dedicada vs. painel?** **Opções:** `/campanha/whatsapp` | aba no dashboard | Sheet na liderança. **Recomendação:** rota `/campanha/whatsapp` (inbox) + entrada “Criar a partir do WhatsApp” no detalhe da liderança se houver msgs pendentes. _(shape fecha layout)_
- **Quais tipos de domínio na v1?** **Opções:** só `municipalityUpdate` | update+demanda | update+demanda+voto. **Recomendação:** `municipalityUpdate` + `campaignDemand` na v1; voto (declared/estimated) só se o texto for inequívoco **e** o aceite abrir o form de pledge existente — senão Adiado. _(assumido — validar)_
## Abordagem proposta
```mermaid
flowchart LR
In["whatsappMessage inbound<br/>(D3 allowlist)"]
Match["match Contact → leadership"]
Draft["whatsappInboundDraft<br/>heurística + edição"]
Human["Aceitar / Editar / Descartar"]
Dom["municipalityUpdate / campaignDemand<br/>(+ pledge form)"]
In --> Match --> Draft --> Human --> Dom
```
Componentes:
- **Collection ou table `whatsappInboundDraft`** (status `pending|accepted|discarded`, `suggestedKind`, `municipality`, `body`, rel `whatsappMessage`): migration própria desta fatia **ou** campos no `whatsappMessage` se depth preferir menos collections — **recomendação:** collection draft (ciclo de vida claro; não polui log imutável).
- **`suggestDraftFromMessage`** puro + action `acceptWhatsappDraft` / `discardWhatsappDraft` reusando creates existentes de update/demanda (não duplicar invariantes).
- **UI** `WhatsappInboxPage` + `WhatsappDraftReviewCard`; reusa `MunicipalityUpdateForm` fields onde der (depth check).
- **Suave C12:** se sinais tipados em `municipalityUpdate` existirem, o aceite pode preencher `kind`/sinal — senão texto livre como hoje.
- **Migration:** `pnpm migrate:create add_whatsapp_inbound_draft`.
## Dependências
- **Dura:** **D3** (inbound log + match phone + Consent).
- **Suave:** **D4** (thread/contexto outbound); **C12** (sinais tipados); remodelagem/demandas já em prod.
## Não escopo
- Auto-resposta / chatbot → fora.
- Classificação LLM → Adiado com gatilho (programa).
- Envio em massa / apoiadores → fora.
- Clone de WhatsApp Web (sync de mídia, status, reactions) → fora.
## Rabbit holes
- **Resolver município quando liderança tem N municípios.** Mitigação: obrigar select no draft; sem “adivinhar” por NLP geográfico.
- **Mesclar com motor E11.** Mitigação: filas separadas (E11 = território/meta; D5 = inbox de mensagens); no máximo link “abrir município”.
- **Reprocessar histórico em massa.** Mitigação: só msgs após feature flag / `draftsEnabledAt`.
## Adiado com gatilho
- **LLM classifier.** Revisitar quando: ≥50 drafts/semana e descarte >40% por tipo errado (critério do programa).
- **Sugerir `declaredVotes` / cenários A10.** Revisitar quando: assessores pedirem explicitamente e houver padrão estável de msgs numéricas.
- **Mídia → anexo em `municipalityUpdate`.** Revisitar quando: D3 passar a persistir mídia.
## Referências
- [whatsapp-interno-campanha.md](whatsapp-interno-campanha.md), [whatsapp-canal-fundacao.md](whatsapp-canal-fundacao.md), [whatsapp-envio-liderancas.md](whatsapp-envio-liderancas.md)
- `src/collections/MunicipalityUpdate.ts`, `src/components/campaign/MunicipalityUpdateForm.tsx`
- `docs/plans/motor-de-sugestoes.md` (E11) — precedente humano no loop
- `docs/IMPROVE-APP-PLAN.md` — Little Hire
- `PRODUCT.md` / `DESIGN.md`
- AGENTS.md — transactions, access, Consent