Skip to content

feat(crm): WhatsApp só como viewer (anti-bloqueio) + ficha do lead rica#67

Merged
sejaanomalo-web merged 1 commit into
mainfrom
claude/crm-fase8-viewer-whatsapp
Jul 15, 2026
Merged

feat(crm): WhatsApp só como viewer (anti-bloqueio) + ficha do lead rica#67
sejaanomalo-web merged 1 commit into
mainfrom
claude/crm-fase8-viewer-whatsapp

Conversation

@sejaanomalo-web

@sejaanomalo-web sejaanomalo-web commented Jul 15, 2026

Copy link
Copy Markdown
Owner

Muda a estratégia anti-banimento: o CRM deixa de enviar/receber mensagem pela Evolution (o principal vetor de bloqueio do número) e passa a usar o WhatsApp só como "viewer". Conversar de fato vira 1 clique pro wa.me, fora do app.

Envio removido de vez:

  • evolution.ts: removidas as funções de envio de texto/áudio/mídia e o download de mídia; sobrou só o que faz o viewer (perfil nome/foto, verificar número, admin da instância).
  • crm-mensagens-actions.ts: sem enviarMensagem/enviarAudio; sobrou só marcarLeadComoLido (zera não-lidas ao abrir a ficha).
  • Thread.tsx (tela de conversa) removido.

Webhook 100% passivo (sem chamada de API por mensagem):

  • crm-inbound.ts: mensagem recebida é só gravada pra alimentar a lista (última interação/prévia/badge) — sem baixar mídia nem buscar foto.
  • CONTACTS_UPSERT agora auto-cria lead de contato NOVO adicionado no celular (guardas: lote pequeno + fora da janela de sync inicial), pra não inundar o CRM com o despejo da agenda ao conectar.

Ficha do lead como tela principal (no lugar da conversa):

  • FichaContato.tsx: cabeçalho (avatar/nome/telefone sincronizados) + botão "Conversar no WhatsApp" (wa.me) + ficha embutida.
  • InformacoesContato.tsx enriquecido: Instagram, Facebook e histórico de reuniões (data + anotação, repetível); modo embutido (cabecalho=false).
  • Migração 20260720: colunas instagram/facebook/reunioes em crm_leads.
  • excluirLead não mexe mais no WhatsApp (viewer não edita o celular).

Summary by CodeRabbit

  • New Features

    • Added a detailed contact profile view with editable names, tags, notes, social links, and meeting history.
    • Added WhatsApp contact actions and automatic contact information synchronization.
    • Added options to delete or restore archived contacts.
    • Expanded CRM contact records with Instagram, Facebook, and meeting details.
  • Changes

    • The CRM now displays contact information instead of conversation threads.
    • Opening a contact clears its unread count.
    • CRM no longer sends messages or media through WhatsApp.
    • Contact creation from synchronization events is more selective.

Muda a estratégia anti-banimento: o CRM deixa de enviar/receber mensagem
pela Evolution (o principal vetor de bloqueio do número) e passa a usar o
WhatsApp só como "viewer". Conversar de fato vira 1 clique pro wa.me, fora
do app.

Envio removido de vez:
- evolution.ts: removidas as funções de envio de texto/áudio/mídia e o
  download de mídia; sobrou só o que faz o viewer (perfil nome/foto,
  verificar número, admin da instância).
- crm-mensagens-actions.ts: sem enviarMensagem/enviarAudio; sobrou só
  marcarLeadComoLido (zera não-lidas ao abrir a ficha).
- Thread.tsx (tela de conversa) removido.

Webhook 100% passivo (sem chamada de API por mensagem):
- crm-inbound.ts: mensagem recebida é só gravada pra alimentar a lista
  (última interação/prévia/badge) — sem baixar mídia nem buscar foto.
- CONTACTS_UPSERT agora auto-cria lead de contato NOVO adicionado no
  celular (guardas: lote pequeno + fora da janela de sync inicial), pra
  não inundar o CRM com o despejo da agenda ao conectar.

Ficha do lead como tela principal (no lugar da conversa):
- FichaContato.tsx: cabeçalho (avatar/nome/telefone sincronizados) +
  botão "Conversar no WhatsApp" (wa.me) + ficha embutida.
- InformacoesContato.tsx enriquecido: Instagram, Facebook e histórico de
  reuniões (data + anotação, repetível); modo embutido (cabecalho=false).
- Migração 20260720: colunas instagram/facebook/reunioes em crm_leads.
- excluirLead não mexe mais no WhatsApp (viewer não edita o celular).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
metas-anomalo Ready Ready Preview, Comment Jul 15, 2026 2:39pm

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The CRM now presents selected leads through an enriched contact profile instead of a conversation thread. It adds social links and meeting history, removes outbound and media Evolution operations, and conditionally creates leads from incremental contact updates.

Changes

CRM viewer and contact profile transition

Layer / File(s) Summary
Lead data and persistence contracts
lib/crm-leads.ts, lib/crm-leads-actions.ts, supabase/migrations/*
Lead records now include Instagram, Facebook, and meeting history, with normalized values persisted by the contact information action.
Embedded contact information editing
components/crm/InformacoesContato.tsx
The form supports embedded and standalone modes, social fields, meeting history, and mode-specific save and cancel behavior.
Contact profile and lead actions
components/crm/FichaContato.tsx, lib/crm-leads-actions.ts
The profile marks leads as read, supports WhatsApp and contact synchronization, allows renaming, and handles CRM-only deletion or archive removal.
Viewer-only Evolution and inbound processing
lib/evolution.ts, lib/crm-mensagens-actions.ts, lib/crm-inbound.ts
Outbound messaging, media downloads, and WhatsApp chat archiving are removed; inbound processing uses cached contact data and conditionally creates leads from incremental upserts.
CRM page profile wiring
app/dashboard/crm/page.tsx, components/crm/Thread.tsx
The selected-lead pane stops loading messages and renders FichaContato instead of Thread.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CRMPage
  participant FichaContato
  participant InformacoesContato
  participant editarInformacoesContatoAction
  participant crm_leads
  CRMPage->>FichaContato: render selected lead
  FichaContato->>InformacoesContato: display contact details
  InformacoesContato->>editarInformacoesContatoAction: submit contact fields
  editarInformacoesContatoAction->>crm_leads: persist normalized data
  editarInformacoesContatoAction-->>InformacoesContato: return save result
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: CRM WhatsApp becomes viewer-only and the lead detail view is enriched.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/crm-fase8-viewer-whatsapp

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@components/crm/FichaContato.tsx`:
- Around line 171-179: Update the synchronization flow in the useEffect and the
update-button handler to share a single in-flight guard, setting it before
either automatic or forced sincronizarContatoAction request and clearing it when
the request finishes. Use this guard to disable the button while either request
is pending, preventing concurrent synchronization calls.
- Around line 159-160: Replace the useTransition-based request tracking in
components/crm/FichaContato.tsx lines 159-160 with explicit useState locks, and
wrap the async save/sync/delete/desarquive handlers at lines 330-370 in
try/finally so each lock is set before the request and always cleared afterward.
Apply the same request-lock pattern to the async actions in
components/crm/InformacoesContato.tsx lines 111-129; ensure all affected
controls remain disabled until their awaited server action completes.

In `@lib/crm-inbound.ts`:
- Around line 346-355: Update the crm_instancias query in the contact-event
handling flow to select and require ativo to be true before processing the
instance. Preserve the existing unknown-instance response for missing or
inactive instances, preventing subsequent contact updates or lead creation.
- Around line 446-452: Update the initial-sync gating around conectadoEm and
foraDaJanelaInicial so a missing conectado_em timestamp is not considered
outside the initial window. Require a valid connection timestamp to establish
that the initial sync window has elapsed before ehAdicaoIncremental can permit
automatic creation; preserve the existing time-window and batch-size checks
otherwise.
- Around line 494-516: Update the insert error handling in the lead-creation
flow around the crm_leads insert and errLead. Continue only for duplicate-key
errors where errLead.code is "23505"; for other errors or a missing novo record,
surface the failure instead of silently treating it as processed.

In `@lib/crm-leads-actions.ts`:
- Around line 58-84: Atualize normalizarReunioes para validar datas reais por
meio de conversão e round-trip em UTC, rejeitando valores como 2026-99-99
enquanto preserva o formato YYYY-MM-DD. Ordene as reuniões por data em ordem
decrescente antes de aplicar o limite de 200 entradas.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 74348c2e-8fa8-4084-bc00-13e87a5577a8

📥 Commits

Reviewing files that changed from the base of the PR and between 6bb22fa and 2b1f1b2.

📒 Files selected for processing (10)
  • app/dashboard/crm/page.tsx
  • components/crm/FichaContato.tsx
  • components/crm/InformacoesContato.tsx
  • components/crm/Thread.tsx
  • lib/crm-inbound.ts
  • lib/crm-leads-actions.ts
  • lib/crm-leads.ts
  • lib/crm-mensagens-actions.ts
  • lib/evolution.ts
  • supabase/migrations/20260720_crm_fase8_viewer_reunioes_redes.sql
💤 Files with no reviewable changes (1)
  • components/crm/Thread.tsx

Comment on lines +159 to +160
const [pendingNome, startNome] = useTransition()
const [pendingSync, startSync] = useTransition()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -a '^package\.json$' --exec sh -c \
  'echo "== $1 =="; jq "{react: .dependencies.react, next: .dependencies.next}" "$1"' sh {}

rg -n -C3 \
  'useTransition|startTransition\s*\(\s*async|startNome|startSync' \
  components/crm/InformacoesContato.tsx \
  components/crm/FichaContato.tsx

Repository: sejaanomalo-web/metas-anomalo

Length of output: 5798


🏁 Script executed:

#!/bin/bash
sed -n '200,310p' components/crm/FichaContato.tsx
printf '\n----\n'
sed -n '344,390p' components/crm/FichaContato.tsx
printf '\n----\n'
sed -n '100,145p' components/crm/InformacoesContato.tsx

Repository: sejaanomalo-web/metas-anomalo

Length of output: 6484


🌐 Web query:

React 19 useTransition async callback isPending active across async transition callbacks documentation

💡 Result:

In React 19, the useTransition hook supports async callbacks, commonly referred to as Actions [1][2]. When you pass an async function to the startTransition returned by useTransition, the isPending state is designed to accurately track the duration of the async transition [3][1]. Specifically, the isPending flag becomes true as soon as the transition starts [4] and remains true while the async function (and any state updates scheduled within it) is processing [1][4]. React effectively includes awaited async calls within the transition context, and isPending will switch to false only after the transition work is complete and the final UI state is rendered [3][1][4]. Key technical details to note: 1. Automatic Inclusion: Unlike the standalone startTransition function—which often requires explicit re-wrapping of state updates occurring after an await—the useTransition hook is designed to automatically handle state updates that occur after an await within the provided async callback, ensuring they remain part of the transition [5][6]. 2. isPending Behavior: The isPending flag is intended to provide immediate feedback to the user [4]. It is managed as an urgent state, ensuring that loading indicators (like spinners or disabled button states) can trigger immediately without waiting for the transition's background rendering work to finish [4]. 3. Potential Caveats: While the React 19 API supports this flow natively, developers should be aware that highly frequent urgent updates can potentially interrupt or delay the completion of a transition, which may keep isPending true for longer than expected if the transition is repeatedly abandoned or starved [4]. Additionally, ensuring the hook is correctly utilized in the component lifecycle is important for consistent behavior; some edge cases in early versions were reported where improper placement of the hook could impact state reset [7]. In summary, for standard async operations (such as API calls inside an Action), you can rely on isPending to correctly reflect the loading state throughout the lifecycle of the async task and its subsequent UI updates [3][1].

Citations:


🌐 Web query:

React 18 useTransition async callback await isPending behavior pending resets before await

💡 Result:

In React 18, the isPending state provided by useTransition does not natively track the entire lifecycle of an asynchronous callback [1][2]. When you pass an async function to startTransition, React only marks the synchronous code executed before the first await as part of the transition [3][4][5]. As a result, isPending will revert to false as soon as the execution hits the first await keyword, rather than waiting for the asynchronous operation to complete [1][6][2]. Additionally, any state updates scheduled after an await are not automatically treated as part of the transition [4][1][5]. To ensure those updates are correctly marked as transitions and to keep the pending state active, you must manually wrap the subsequent state updates in a nested startTransition call [4][7][1]. This behavior is a known limitation in React 18 due to the loss of the asynchronous execution context [1][2]. This was addressed in React 19, where startTransition and useTransition were updated to natively support asynchronous actions, allowing isPending to remain true for the duration of the entire asynchronous operation [6][8][9]. If you are working in a React 18 environment, the manual nested startTransition pattern is the standard workaround [4][7][5].

Citations:


Use an explicit request lock for these async actions.

startTransition(async () => …) on React 18.3.1 won’t keep isPending true through the awaited server action, so these controls can re-enable early and allow duplicate saves/syncs/deletes/desarquives.

  • components/crm/FichaContato.tsx#L159-L160
  • components/crm/FichaContato.tsx#L330-L370
  • components/crm/InformacoesContato.tsx#L111-L129

Use useState + try/finally for the lock, or upgrade the React/Next stack together.

📍 Affects 2 files
  • components/crm/FichaContato.tsx#L159-L160 (this comment)
  • components/crm/FichaContato.tsx#L330-L370
  • components/crm/InformacoesContato.tsx#L111-L129
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/crm/FichaContato.tsx` around lines 159 - 160, Replace the
useTransition-based request tracking in components/crm/FichaContato.tsx lines
159-160 with explicit useState locks, and wrap the async
save/sync/delete/desarquive handlers at lines 330-370 in try/finally so each
lock is set before the request and always cleared afterward. Apply the same
request-lock pattern to the async actions in
components/crm/InformacoesContato.tsx lines 111-129; ensure all affected
controls remain disabled until their awaited server action completes.

Comment on lines +171 to +179
useEffect(() => {
if (jaTentouRef.current) return
if (lead.nome && lead.foto_url) return
jaTentouRef.current = true
sincronizarContatoAction(lead.id).then((r) => {
if (r.ok) router.refresh()
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [lead.id])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Prevent automatic and forced synchronization from overlapping.

The automatic request does not set pendingSync, so the update button stays enabled and can launch a concurrent { forcar: true } request. Use one shared in-flight guard and disable the button for both synchronization paths.

Also applies to: 293-303

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/crm/FichaContato.tsx` around lines 171 - 179, Update the
synchronization flow in the useEffect and the update-button handler to share a
single in-flight guard, setting it before either automatic or forced
sincronizarContatoAction request and clearing it when the request finishes. Use
this guard to disable the button while either request is pending, preventing
concurrent synchronization calls.

Comment thread lib/crm-inbound.ts
Comment on lines 346 to +355
const { data: inst } = await db
.from("crm_instancias")
.select("id, empresa_slug")
.select("id, empresa_slug, usuario_id, usuario_nome, conectado_em")
.eq("instance_name", instanceName)
.maybeSingle()
if (!inst) return { ok: true, info: `instancia desconhecida: ${instanceName}` }
const instanciaId = inst.id as string
const empresaSlug = inst.empresa_slug as string
const usuarioId = inst.usuario_id as string
const usuarioNome = (inst.usuario_nome as string) ?? null

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject contact events from inactive instances.

Unlike the message path, this query never checks crm_instancias.ativo. A delayed webhook from an inactive instance can therefore update contacts and create leads.

Proposed fix
   const { data: inst } = await db
     .from("crm_instancias")
-    .select("id, empresa_slug, usuario_id, usuario_nome, conectado_em")
+    .select("id, empresa_slug, usuario_id, usuario_nome, conectado_em, ativo")
     .eq("instance_name", instanceName)
     .maybeSingle()
-  if (!inst) return { ok: true, info: `instancia desconhecida: ${instanceName}` }
+  if (!inst || inst.ativo === false) {
+    return { ok: true, info: `instancia desconhecida/inativa: ${instanceName}` }
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { data: inst } = await db
.from("crm_instancias")
.select("id, empresa_slug")
.select("id, empresa_slug, usuario_id, usuario_nome, conectado_em")
.eq("instance_name", instanceName)
.maybeSingle()
if (!inst) return { ok: true, info: `instancia desconhecida: ${instanceName}` }
const instanciaId = inst.id as string
const empresaSlug = inst.empresa_slug as string
const usuarioId = inst.usuario_id as string
const usuarioNome = (inst.usuario_nome as string) ?? null
const { data: inst } = await db
.from("crm_instancias")
.select("id, empresa_slug, usuario_id, usuario_nome, conectado_em, ativo")
.eq("instance_name", instanceName)
.maybeSingle()
if (!inst || inst.ativo === false) {
return { ok: true, info: `instancia desconhecida/inativa: ${instanceName}` }
}
const instanciaId = inst.id as string
const empresaSlug = inst.empresa_slug as string
const usuarioId = inst.usuario_id as string
const usuarioNome = (inst.usuario_nome as string) ?? null
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crm-inbound.ts` around lines 346 - 355, Update the crm_instancias query
in the contact-event handling flow to select and require ativo to be true before
processing the instance. Preserve the existing unknown-instance response for
missing or inactive instances, preventing subsequent contact updates or lead
creation.

Comment thread lib/crm-inbound.ts
Comment on lines +446 to +452
const conectadoEm = inst.conectado_em
? new Date(inst.conectado_em as string).getTime()
: null
const foraDaJanelaInicial =
conectadoEm === null || Date.now() - conectadoEm > JANELA_SYNC_INICIAL_MS
const ehAdicaoIncremental =
podeCriar && foraDaJanelaInicial && lista.length <= LIMIAR_ADICAO_INCREMENTAL

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not treat a missing connection timestamp as outside initial sync.

Line 450 permits automatic creation when conectado_em is null—the exact case where completion of the initial catalog dump cannot be established. Repeated small batches could then populate the CRM unintentionally.

Proposed fix
   const foraDaJanelaInicial =
-    conectadoEm === null || Date.now() - conectadoEm > JANELA_SYNC_INICIAL_MS
+    conectadoEm !== null &&
+    Number.isFinite(conectadoEm) &&
+    Date.now() - conectadoEm > JANELA_SYNC_INICIAL_MS
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const conectadoEm = inst.conectado_em
? new Date(inst.conectado_em as string).getTime()
: null
const foraDaJanelaInicial =
conectadoEm === null || Date.now() - conectadoEm > JANELA_SYNC_INICIAL_MS
const ehAdicaoIncremental =
podeCriar && foraDaJanelaInicial && lista.length <= LIMIAR_ADICAO_INCREMENTAL
const foraDaJanelaInicial =
conectadoEm !== null &&
Number.isFinite(conectadoEm) &&
Date.now() - conectadoEm > JANELA_SYNC_INICIAL_MS
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crm-inbound.ts` around lines 446 - 452, Update the initial-sync gating
around conectadoEm and foraDaJanelaInicial so a missing conectado_em timestamp
is not considered outside the initial window. Require a valid connection
timestamp to establish that the initial sync window has elapsed before
ehAdicaoIncremental can permit automatic creation; preserve the existing
time-window and batch-size checks otherwise.

Comment thread lib/crm-inbound.ts
Comment on lines +494 to +516
const { data: novo, error: errLead } = await db
.from("crm_leads")
.insert({
empresa_slug: empresaSlug,
empresa_nome: empresaNome,
usuario_id: usuarioId,
usuario_nome: usuarioNome,
telefone_e164: l.telefone_e164,
nome: l.nome,
// nome_manual=false: veio do WhatsApp, entao sync futuro pode
// atualizar (so o nome digitado no CRM vira manual).
nome_manual: false,
foto_url: l.foto_url,
origem: "whatsapp",
etapa_id: etapaInicial?.id ?? null,
status: "aberto",
ultima_interacao_em: agora,
nao_lidas: 0,
})
.select("id")
.single()
// 23505 = outra entrega criou em paralelo; ignora (idempotente).
if (errLead || !novo) continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- lib/crm-inbound.ts around target lines ---'
nl -ba lib/crm-inbound.ts | sed -n '430,560p'

echo
echo '--- search for errLead / 23505 / processed response ---'
rg -n 'errLead|23505|ok: false|erro:|processed|continue' lib/crm-inbound.ts

echo
echo '--- search for webhook route / caller references ---'
rg -n 'crm-inbound|crm_leads|webhook|ok: false|erro:' -S .

Repository: sejaanomalo-web/metas-anomalo

Length of output: 254


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- lib/crm-inbound.ts around target lines ---'
python3 - <<'PY'
from pathlib import Path
path = Path('lib/crm-inbound.ts')
for i, line in enumerate(path.read_text().splitlines(), 1):
    if 430 <= i <= 560:
        print(f"{i:4d} {line}")
PY

echo
echo '--- search for errLead / 23505 / processed response ---'
rg -n 'errLead|23505|ok: false|erro:|processed|continue' lib/crm-inbound.ts

echo
echo '--- search for webhook route / caller references ---'
rg -n 'crm-inbound|crm_leads|webhook|ok: false|erro:' -S .

Repository: sejaanomalo-web/metas-anomalo

Length of output: 50385


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- processarEventoWebhook section ---'
python3 - <<'PY'
from pathlib import Path
path = Path('lib/crm-inbound.ts')
for i, line in enumerate(path.read_text().splitlines(), 1):
    if 611 <= i <= 690:
        print(f"{i:4d} {line}")
PY

echo
echo '--- later insert-error handling in lib/crm-inbound.ts ---'
python3 - <<'PY'
from pathlib import Path
path = Path('lib/crm-inbound.ts')
for i, line in enumerate(path.read_text().splitlines(), 1):
    if 740 <= i <= 790:
        print(f"{i:4d} {line}")
PY

echo
echo '--- callers of processarEventoWebhook ---'
rg -n 'processarEventoWebhook\(' -S .

echo
echo '--- route files mentioning crm-inbound ---'
fd -t f -a 'crm-inbound*' .
rg -n 'processarEventoWebhook|webhook|crm-inbound' app lib pages src .next 2>/dev/null

Repository: sejaanomalo-web/metas-anomalo

Length of output: 10106


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
path = Path('app/api/crm/wa/webhook/[segredo]/route.ts')
for i, line in enumerate(path.read_text().splitlines(), 1):
    if 1 <= i <= 140:
        print(f"{i:4d} {line}")
PY

Repository: sejaanomalo-web/metas-anomalo

Length of output: 4055


Only ignore duplicate-key insert errors. if (errLead || !novo) continue treats every insert failure as a successful no-op, so the webhook records the event as processed and drops the lead creation silently. Handle errLead.code === "23505" explicitly and surface the other cases.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crm-inbound.ts` around lines 494 - 516, Update the insert error handling
in the lead-creation flow around the crm_leads insert and errLead. Continue only
for duplicate-key errors where errLead.code is "23505"; for other errors or a
missing novo record, surface the failure instead of silently treating it as
processed.

Comment thread lib/crm-leads-actions.ts
Comment on lines +58 to +84
/** Normaliza uma reunião (data YYYY-MM-DD + anotação). Descarta linhas sem
* data nem anotação. Data inválida vira "" (a UI valida antes; o servidor só
* não confia). Até 200 reuniões por lead — teto defensivo. */
function normalizarReunioes(bruto: string): ReuniaoNota[] | null {
let parsed: unknown
try {
parsed = JSON.parse(bruto || "[]")
} catch {
return null
}
if (!Array.isArray(parsed)) return null
return parsed
.map((r): ReuniaoNota => {
const item = (r ?? {}) as Record<string, unknown>
const id =
String(item.id ?? "").slice(0, 60) ||
`reuniao_${Math.random().toString(36).slice(2)}`
const dataBruta = String(item.data ?? "").trim().slice(0, 10)
const data = /^\d{4}-\d{2}-\d{2}$/.test(dataBruta) ? dataBruta : ""
return {
id,
data,
anotacao: String(item.anotacao ?? "").trim().slice(0, 5000),
}
})
.filter((r) => r.data || r.anotacao)
.slice(0, 200)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Validate real calendar dates and enforce newest-first ordering.

The regex accepts values such as 2026-99-99, and the returned array is never sorted despite the CrmLeadRow contract. Validate through a UTC date round-trip, then sort descending before applying the 200-entry limit.

Proposed fix
+function dataIsoValida(valor: string): boolean {
+  if (!/^\d{4}-\d{2}-\d{2}$/.test(valor)) return false
+  const data = new Date(`${valor}T00:00:00Z`)
+  return !Number.isNaN(data.getTime()) &&
+    data.toISOString().slice(0, 10) === valor
+}
+
 function normalizarReunioes(bruto: string): ReuniaoNota[] | null {
@@
-      const data = /^\d{4}-\d{2}-\d{2}$/.test(dataBruta) ? dataBruta : ""
+      const data = dataIsoValida(dataBruta) ? dataBruta : ""
@@
     })
     .filter((r) => r.data || r.anotacao)
+    .sort((a, b) => b.data.localeCompare(a.data))
     .slice(0, 200)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/** Normaliza uma reunião (data YYYY-MM-DD + anotação). Descarta linhas sem
* data nem anotação. Data inválida vira "" (a UI valida antes; o servidor
* não confia). Até 200 reuniões por lead teto defensivo. */
function normalizarReunioes(bruto: string): ReuniaoNota[] | null {
let parsed: unknown
try {
parsed = JSON.parse(bruto || "[]")
} catch {
return null
}
if (!Array.isArray(parsed)) return null
return parsed
.map((r): ReuniaoNota => {
const item = (r ?? {}) as Record<string, unknown>
const id =
String(item.id ?? "").slice(0, 60) ||
`reuniao_${Math.random().toString(36).slice(2)}`
const dataBruta = String(item.data ?? "").trim().slice(0, 10)
const data = /^\d{4}-\d{2}-\d{2}$/.test(dataBruta) ? dataBruta : ""
return {
id,
data,
anotacao: String(item.anotacao ?? "").trim().slice(0, 5000),
}
})
.filter((r) => r.data || r.anotacao)
.slice(0, 200)
/** Normaliza uma reunião (data YYYY-MM-DD + anotação). Descarta linhas sem
* data nem anotação. Data inválida vira "" (a UI valida antes; o servidor
* não confia). Até 200 reuniões por lead teto defensivo. */
function dataIsoValida(valor: string): boolean {
if (!/^\d{4}-\d{2}-\d{2}$/.test(valor)) return false
const data = new Date(`${valor}T00:00:00Z`)
return !Number.isNaN(data.getTime()) &&
data.toISOString().slice(0, 10) === valor
}
function normalizarReunioes(bruto: string): ReuniaoNota[] | null {
let parsed: unknown
try {
parsed = JSON.parse(bruto || "[]")
} catch {
return null
}
if (!Array.isArray(parsed)) return null
return parsed
.map((r): ReuniaoNota => {
const item = (r ?? {}) as Record<string, unknown>
const id =
String(item.id ?? "").slice(0, 60) ||
`reuniao_${Math.random().toString(36).slice(2)}`
const dataBruta = String(item.data ?? "").trim().slice(0, 10)
const data = dataIsoValida(dataBruta) ? dataBruta : ""
return {
id,
data,
anotacao: String(item.anotacao ?? "").trim().slice(0, 5000),
}
})
.filter((r) => r.data || r.anotacao)
.sort((a, b) => b.data.localeCompare(a.data))
.slice(0, 200)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crm-leads-actions.ts` around lines 58 - 84, Atualize normalizarReunioes
para validar datas reais por meio de conversão e round-trip em UTC, rejeitando
valores como 2026-99-99 enquanto preserva o formato YYYY-MM-DD. Ordene as
reuniões por data em ordem decrescente antes de aplicar o limite de 200
entradas.

@sejaanomalo-web
sejaanomalo-web merged commit 87a0691 into main Jul 15, 2026
3 checks passed
@sejaanomalo-web
sejaanomalo-web deleted the claude/crm-fase8-viewer-whatsapp branch July 15, 2026 21:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant