Skip to content

Claude/crm fase1 inbox conexoes whatsapp#49

Merged
sejaanomalo-web merged 3 commits into
mainfrom
claude/crm-fase1-inbox-conexoes-whatsapp
Jul 9, 2026
Merged

Claude/crm fase1 inbox conexoes whatsapp#49
sejaanomalo-web merged 3 commits into
mainfrom
claude/crm-fase1-inbox-conexoes-whatsapp

Conversation

@sejaanomalo-web

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

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Added a full CRM inbox with conversation list, message thread, and unread indicators.
    • Added a WhatsApp connections dashboard for viewing, creating, reactivating, and QR-generating connections.
    • CRM pages now refresh automatically when connection or message activity changes.
    • Commercial reports now open with a searchable date range and company filter.
  • Bug Fixes

    • Improved CRM message handling and read status updates.
    • Simplified form access so only supported commercial forms appear.

sejaanomalo-web and others added 3 commits June 24, 2026 14:58
Tira o formulario de trafego da secao Formularios (Configuracoes) + o wiring morto (FormId narrowado, responsaveisTrafego e listarTimePorPapel('gestor_trafego') removidos). A rota publica /formulario e o componente FormularioManual permanecem intactos.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Espelha o gerenciador de Trafego pago: listarRelatoriosComerciaisAdmin passa a receber filtros (empresa + periodo, intervalo obrigatorio) e GerenciadorRelatoriosComerciais comeca vazio, mostrando a lista so depois do Buscar. Configuracoes deixa de pre-carregar todos os relatorios.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fecha o ciclo mínimo do CRM: conectar um número real (webhook agora
processa QRCODE_UPDATED/CONNECTION_UPDATE, e há UI pra criar instância e
escanear o QR em /dashboard/crm/conexoes, sem precisar de SQL manual) e
conversar pelo dashboard (inbox com lista de leads + thread, finalmente
chamando enviarTextoEvolution). Realtime via crm_realtime_ping. Inclui
runbook de provisionamento do servidor Evolution (VPS + Docker Compose).

Pesquisa prévia confirmou manter Evolution API como conector (vs WAHA —
multi-sessão paga contradiz o requisito de 10 números — e Baileys puro —
reinventaria o que a Evolution já entrega pronta).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 9, 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 9, 2026 10:08pm

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

CRM operations

Layer / File(s) Summary
CRM data and Evolution contracts
lib/crm-leads.ts, lib/evolution.ts
Adds typed CRM lead/message queries and standardized Evolution instance API results.
Instance lifecycle and webhook processing
lib/crm-instancias-actions.ts, lib/crm-inbound.ts, lib/evolution.ts, docs/CRM-EVOLUTION-SETUP.md
Adds instance lifecycle actions, QR and connection webhook updates, realtime pings, and Evolution deployment documentation.
Connections dashboard UI
app/dashboard/crm/conexoes/page.tsx, components/crm/GerenciadorInstancias.tsx, components/crm/CrmRealtime.tsx
Adds the connections page, instance creation and status controls, QR actions, and realtime refresh handling.
Inbox, thread, and outbound messaging
app/dashboard/crm/page.tsx, components/crm/ListaConversas.tsx, components/crm/Thread.tsx, lib/crm-mensagens-actions.ts
Replaces the CRM placeholder with lead navigation, message threads, read-state updates, and outbound message sending.

Commercial configuration

Layer / File(s) Summary
Commercial report search flow
lib/relatorios-comerciais.ts, components/GerenciadorRelatoriosComerciais.tsx, app/dashboard/configuracoes/page.tsx
Adds date/company-filtered report searches and changes the admin component to load and manage results after submission.
Commercial-only form configuration
components/GerenciadorFormularios.tsx, app/dashboard/configuracoes/page.tsx
Removes the traffic form and traffic responsibility wiring, leaving only the commercial form.

Estimated code review effort: 5 (Critical) | ~100 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GerenciadorInstancias
  participant crm-instancias-actions
  participant EvolutionAPI
  participant crm-inbound
  participant Supabase
  GerenciadorInstancias->>crm-instancias-actions: criarInstanciaAction
  crm-instancias-actions->>EvolutionAPI: POST /instance/create
  EvolutionAPI-->>crm-inbound: QRCODE_UPDATED or CONNECTION_UPDATE
  crm-inbound->>Supabase: update crm_instancias
  crm-inbound->>Supabase: insert crm_realtime_ping
Loading
sequenceDiagram
  participant Thread
  participant enviarMensagemAction
  participant EvolutionAPI
  participant Supabase
  Thread->>enviarMensagemAction: send lead message
  enviarMensagemAction->>EvolutionAPI: enviarTextoEvolution
  enviarMensagemAction->>Supabase: persist crm_mensagens
  enviarMensagemAction->>Supabase: insert crm_realtime_ping
  enviarMensagemAction-->>Thread: ResultadoEnvio
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.30% 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 phase 1 work for the inbox and WhatsApp connections.
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-fase1-inbox-conexoes-whatsapp

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: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/api/crm/wa/webhook/[segredo]/route.ts (1)

73-84: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Persist the handler’s actual success state.

processarEventoWebhook() can return ok: false, but this code always writes processado: true. The webhook may correctly return HTTP 200 while the audit row falsely claims processing succeeded. Persist processado: r.ok and retain r.erro when present.

🤖 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 `@app/api/crm/wa/webhook/`[segredo]/route.ts around lines 73 - 84, The webhook
audit update in the route handler always marks `processado` as true even when
`processarEventoWebhook()` returns `ok: false`. Update the `NextResponse.json`
flow so the `crm_wa_eventos` update uses the actual result from
`processarEventoWebhook` by persisting `processado: r.ok` and keeping
`erro_processamento` populated from `r.erro` when present.
🧹 Nitpick comments (3)
docs/CRM-EVOLUTION-SETUP.md (1)

48-49: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Pin the Evolution image version or digest.

Using atendai/evolution-api:latest makes deployments non-reproducible and allows unreviewed upstream changes to alter production behavior. Pin a tested release, preferably by immutable digest.

🤖 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 `@docs/CRM-EVOLUTION-SETUP.md` around lines 48 - 49, The Evolution API service
definition is using a mutable image tag, which makes deployments
non-reproducible; update the evolution-api image reference to a pinned, tested
release or immutable digest so the setup stays stable over time. Locate the
evolution-api entry in the setup document and replace the current latest tag
with a fixed version/digest that matches the intended deployment target.
components/GerenciadorFormularios.tsx (1)

196-200: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider guarding the render by form.id for future-proofing.

Since FORMS currently only has the "comercial" entry, this is correct today. But rendering FormularioComercial unconditionally (rather than gated on form.id === "comercial") removes the safety net if another form type is added later without updating this block.

Defensive guard
-                  <div style={{ marginTop: 16 }}>
-                    <FormularioComercial
-                      empresas={empresas}
-                      responsaveis={responsaveisComercial}
-                      clientesPorEmpresa={clientesPorEmpresa}
-                    />
-                  </div>
+                  <div style={{ marginTop: 16 }}>
+                    {form.id === "comercial" && (
+                      <FormularioComercial
+                        empresas={empresas}
+                        responsaveis={responsaveisComercial}
+                        clientesPorEmpresa={clientesPorEmpresa}
+                      />
+                    )}
+                  </div>
🤖 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/GerenciadorFormularios.tsx` around lines 196 - 200, The render in
GerenciadorFormularios is currently unconditional, which will bypass the
intended form-type safety check. Update the conditional branch around
FormularioComercial so it is explicitly gated by form.id === "comercial", using
the existing form selection logic in GerenciadorFormularios to keep future form
types from rendering this component accidentally.
components/GerenciadorRelatoriosComerciais.tsx (1)

15-24: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

BRT calculation depends on the server running with TZ=UTC.

agora.getTime() is already a UTC epoch value; adding getTimezoneOffset() * 60_000 only produces the correct BRT offset if the host process's local timezone is UTC. If this ever runs in a non-UTC environment (e.g., local dev on a machine set to BRT), the computed default range shifts by hours, which can flip the "day" component near midnight boundaries.

Safer alternative independent of host TZ
-function rangeMesAtual(): { inicio: string; fim: string } {
-  const agora = new Date()
-  const utcMs = agora.getTime() + agora.getTimezoneOffset() * 60_000
-  const brt = new Date(utcMs - 3 * 60 * 60_000)
-  const ano = brt.getUTCFullYear()
-  const mes = String(brt.getUTCMonth() + 1).padStart(2, "0")
-  const dia = String(brt.getUTCDate()).padStart(2, "0")
-  return { inicio: `${ano}-${mes}-01`, fim: `${ano}-${mes}-${dia}` }
-}
+function rangeMesAtual(): { inicio: string; fim: string } {
+  const brt = new Date(Date.now() - 3 * 60 * 60_000)
+  const ano = brt.getUTCFullYear()
+  const mes = String(brt.getUTCMonth() + 1).padStart(2, "0")
+  const dia = String(brt.getUTCDate()).padStart(2, "0")
+  return { inicio: `${ano}-${mes}-01`, fim: `${ano}-${mes}-${dia}` }
+}

Date.now() is UTC epoch already, so subtracting the fixed 3h BRT offset is TZ-agnostic — no need to round-trip through getTimezoneOffset().

Also worth checking if this exact helper is duplicated in the tráfego manager referenced in the lib doc comment, to consolidate into a shared utility.

🤖 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/GerenciadorRelatoriosComerciais.tsx` around lines 15 - 24, The BRT
range calculation in rangeMesAtual() is host-TZ dependent because it adds
getTimezoneOffset() before subtracting the fixed BRT offset, so the default
dates can shift on non-UTC machines. Update this helper to derive the BRT date
from a UTC epoch source like Date.now() with only the fixed -3h adjustment, and
keep the YYYY-MM-DD formatting logic in rangeMesAtual() unchanged. Also check
whether the same date-range helper exists in the tráfego manager and consolidate
it into a shared utility if so.
🤖 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 `@app/dashboard/crm/page.tsx`:
- Around line 73-76: The Thread component is being reused across different
leads, which can carry over draft and error state between conversations. Update
the lead rendering in the page component so the Thread instance is keyed by the
current lead identity, using the Thread symbol and the lead object’s id to force
a remount when switching conversations. Keep the existing lead conditional
rendering intact and apply the key directly where Thread is created.

In `@components/crm/Thread.tsx`:
- Around line 28-29: The send button can re-enable too early because Thread’s
useTransition pending state stops tracking after the first await. Replace the
reliance on pending in Thread/enviar with a local async loading flag that is set
before calling enviarMensagemAction and cleared only after it fully settles, and
use that flag to keep Enviar disabled so Enter/click cannot trigger a second
send while the request is still in flight.

In `@docs/CRM-EVOLUTION-SETUP.md`:
- Around line 69-70: The port mapping for Evolution is currently published on
all host interfaces, which conflicts with the intended Caddy-only exposure.
Update the docker compose port entry in the setup docs to bind 8080 explicitly
to localhost, using the existing ports block for the Evolution service so it
cannot be reached directly from external networks.

In `@lib/crm-inbound.ts`:
- Around line 114-127: The QR/status update path in the handler around the
crm_instancias update and crm_realtime_ping insert is swallowing Supabase write
failures and still returning success. Update the logic in the affected
handler(s) so each db.from(...).update(...) and db.from(...).insert(...) call
checks for errors, and if any write fails, return { ok: false, erro: ... }
instead of { ok: true }; use the existing handler flow in crm-inbound.ts to
propagate the failure so the route can record the failed processing state.

In `@lib/crm-instancias-actions.ts`:
- Around line 30-42: Add authorization checks to the CRM instance server actions
before any Supabase admin read/write. In listarInstancias and the related mutate
actions (the ones that call getSupabaseAdmin and operate on crm_instancias),
first validate the authenticated caller and ensure their role/company scope
permits the operation; reject unauthorized requests before querying or updating.
Use the existing action entry points and helpers around listarInstancias and the
other crm-instancias actions to centralize this guard so all instance listing,
deactivation, and reactivation paths enforce the same access rules.
- Around line 98-106: The retry flow in criarInstanciaEvolution handling is
incorrect: when instance/create fails, the local row stays saved but the UI
tells the user to use gerarQrAction, which only retries instance/connect and
cannot recover a never-created Evolution instance. Update the recovery path in
lib/crm-instancias-actions.ts so failed provisioning either retries the create
step for that instance or removes/recreates the local row before allowing QR
generation, and apply the same fix to the related flow in the 111-130 block.

In `@lib/crm-mensagens-actions.ts`:
- Around line 61-84: In the CRM message send flow that performs the
crm_mensagens insert, crm_leads update, and crm_realtime_ping insert, stop
ignoring Supabase write results and explicitly inspect each returned error. If
any of these operations fails, log/return the failure instead of continuing so
the caller does not get a false success when the WhatsApp send succeeded but
persistence did not. Update the same action that contains the
db.from(...).insert/update chain to propagate the error and only report success
after all three writes complete without errors.
- Around line 19-30: Both CRM server actions are missing the required CRM
authorization guard. In enviarMensagemAction, after confirming the current user
via getUsuarioAtual, add requererPermissao("crm") before proceeding with the
Supabase work. In marcarLeadComoLidoAction, add the same session check and CRM
permission check at the start so both actions are protected consistently. Use
the existing getUsuarioAtual and requererPermissao helpers in
lib/crm-mensagens-actions.ts to locate the affected paths.

---

Outside diff comments:
In `@app/api/crm/wa/webhook/`[segredo]/route.ts:
- Around line 73-84: The webhook audit update in the route handler always marks
`processado` as true even when `processarEventoWebhook()` returns `ok: false`.
Update the `NextResponse.json` flow so the `crm_wa_eventos` update uses the
actual result from `processarEventoWebhook` by persisting `processado: r.ok` and
keeping `erro_processamento` populated from `r.erro` when present.

---

Nitpick comments:
In `@components/GerenciadorFormularios.tsx`:
- Around line 196-200: The render in GerenciadorFormularios is currently
unconditional, which will bypass the intended form-type safety check. Update the
conditional branch around FormularioComercial so it is explicitly gated by
form.id === "comercial", using the existing form selection logic in
GerenciadorFormularios to keep future form types from rendering this component
accidentally.

In `@components/GerenciadorRelatoriosComerciais.tsx`:
- Around line 15-24: The BRT range calculation in rangeMesAtual() is host-TZ
dependent because it adds getTimezoneOffset() before subtracting the fixed BRT
offset, so the default dates can shift on non-UTC machines. Update this helper
to derive the BRT date from a UTC epoch source like Date.now() with only the
fixed -3h adjustment, and keep the YYYY-MM-DD formatting logic in
rangeMesAtual() unchanged. Also check whether the same date-range helper exists
in the tráfego manager and consolidate it into a shared utility if so.

In `@docs/CRM-EVOLUTION-SETUP.md`:
- Around line 48-49: The Evolution API service definition is using a mutable
image tag, which makes deployments non-reproducible; update the evolution-api
image reference to a pinned, tested release or immutable digest so the setup
stays stable over time. Locate the evolution-api entry in the setup document and
replace the current latest tag with a fixed version/digest that matches the
intended deployment target.
🪄 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: 98778b86-aa1f-4b1a-9fbe-c4a9aeed7103

📥 Commits

Reviewing files that changed from the base of the PR and between a705d8e and 06246bd.

📒 Files selected for processing (17)
  • app/api/crm/wa/webhook/[segredo]/route.ts
  • app/dashboard/configuracoes/page.tsx
  • app/dashboard/crm/conexoes/page.tsx
  • app/dashboard/crm/page.tsx
  • components/GerenciadorFormularios.tsx
  • components/GerenciadorRelatoriosComerciais.tsx
  • components/crm/CrmRealtime.tsx
  • components/crm/GerenciadorInstancias.tsx
  • components/crm/ListaConversas.tsx
  • components/crm/Thread.tsx
  • docs/CRM-EVOLUTION-SETUP.md
  • lib/crm-inbound.ts
  • lib/crm-instancias-actions.ts
  • lib/crm-leads.ts
  • lib/crm-mensagens-actions.ts
  • lib/evolution.ts
  • lib/relatorios-comerciais.ts

Comment on lines +73 to +76
<div style={{ minWidth: 0 }}>
{lead ? (
<Thread lead={lead} mensagens={mensagens} />
) : (

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map relevant files first
git ls-files 'app/dashboard/crm/*' 'app/dashboard/**/Thread*' 'app/**/Thread*' 'components/**/Thread*'

# Find the page and Thread component definitions/usages
rg -n --hidden --no-ignore-vcs '\bThread\b|lead=' app/dashboard/crm app -g '!**/node_modules/**'

# If the page file is small enough, print it with line numbers
if [ -f app/dashboard/crm/page.tsx ]; then
  wc -l app/dashboard/crm/page.tsx
  cat -n app/dashboard/crm/page.tsx | sed -n '1,220p'
fi

Repository: sejaanomalo-web/metas-anomalo

Length of output: 3957


🏁 Script executed:

#!/bin/bash
set -euo pipefail

wc -l components/crm/Thread.tsx
cat -n components/crm/Thread.tsx | sed -n '1,260p'

Repository: sejaanomalo-web/metas-anomalo

Length of output: 5823


Add key={lead.id} to remount Thread per conversation. Without it, switching leads reuses the same client instance, so the draft and error state can bleed into the next thread.

🤖 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 `@app/dashboard/crm/page.tsx` around lines 73 - 76, The Thread component is
being reused across different leads, which can carry over draft and error state
between conversations. Update the lead rendering in the page component so the
Thread instance is keyed by the current lead identity, using the Thread symbol
and the lead object’s id to force a remount when switching conversations. Keep
the existing lead conditional rendering intact and apply the key directly where
Thread is created.

Comment thread components/crm/Thread.tsx
Comment on lines +28 to +29
const [pending, startTransition] = useTransition()
const router = useRouter()

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
set -euo pipefail

printf '\n== Thread.tsx outline ==\n'
ast-grep outline components/crm/Thread.tsx --view expanded || true

printf '\n== Relevant lines in Thread.tsx ==\n'
nl -ba components/crm/Thread.tsx | sed -n '1,220p'

printf '\n== Search for useTransition/startTransition/enviarMensagemAction ==\n'
rg -n "useTransition|startTransition|enviarMensagemAction|disabled=.*pending|setPending|pending" components/crm -S

Repository: sejaanomalo-web/metas-anomalo

Length of output: 388


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Thread.tsx outline ==\n'
ast-grep outline components/crm/Thread.tsx --view expanded || true

printf '\n== Relevant lines in Thread.tsx ==\n'
nl -ba components/crm/Thread.tsx | sed -n '1,220p'

printf '\n== Search for useTransition/startTransition/enviarMensagemAction/disabled logic ==\n'
rg -n "useTransition|startTransition|enviarMensagemAction|disabled=.*pending|setPending|pending" components/crm -S

Repository: sejaanomalo-web/metas-anomalo

Length of output: 388


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== package.json react versions ==\n'
if [ -f package.json ]; then
  rg -n '"react"|"react-dom"|"next"' package.json -n -S
fi

printf '\n== Thread.tsx with line numbers ==\n'
sed -n '1,240p' components/crm/Thread.tsx | cat -n

printf '\n== Nearby references in components/crm ==\n'
rg -n "useTransition|startTransition|pending|enviarMensagemAction|router.refresh|disabled=" components/crm -S

Repository: sejaanomalo-web/metas-anomalo

Length of output: 8613


🌐 Web query:

React 18.3.1 useTransition async callback pending state first await official docs

💡 Result:

In React 18.3.1, the useTransition hook does not natively support asynchronous callbacks in the way introduced in React 19 [1][2]. Specifically, if you pass an async function to the startTransition returned by useTransition, the transition completes immediately at the first await [2]. Consequently, the isPending state will revert to false even though the asynchronous operation is still in progress [3][2]. To manage isPending correctly in React 18.3.1 during an asynchronous operation, you must manually ensure that any state updates occurring after an await are also wrapped in startTransition [4][5][6]. However, even with this workaround, the isPending flag will not remain true for the entire duration of the asynchronous callback [4][6][2]. The native support for async actions, where isPending automatically stays true until the entire asynchronous callback (including all awaited work) completes, was officially introduced in React 19 [1][7][2].

Citations:


🌐 Web query:

site:react.dev useTransition async actions pending first await React 18 19

💡 Result:

In React 19, useTransition allows you to pass async functions to startTransition [1][2]. When you use an async function in a transition, the isPending state becomes true immediately when the transition starts and remains true until all actions initiated within the transition are completed [3]. A critical detail for React 19 is how state updates are handled after an await expression within an async transition [3][4]. React calls the transition function synchronously, and any state updates scheduled during that synchronous phase are tracked as part of the transition [3]. However, if you perform an async operation (e.g., fetch) and need to update state after the await, React requires that those subsequent state updates be wrapped in an additional startTransition to ensure they are correctly associated with the pending state [3][4][5]. Without this additional wrapping, updates occurring after an await may not correctly maintain the isPending state, or they may be treated as standard, non-transition updates [3][4]. While the initial isPending flag is triggered at the start of the transition, maintaining that "pending" UI feedback throughout the entire async lifecycle—especially after asynchronous data is returned—often necessitates explicit handling or the use of higher-level hooks like useActionState, which are designed to manage these async patterns and their pending states automatically [4][5].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== GerenciadorInstancias.tsx relevant lines ==\n'
sed -n '1,360p' components/crm/GerenciadorInstancias.tsx | cat -n

printf '\n== crm-mensagens-actions.ts ==\n'
sed -n '1,260p' lib/crm-mensagens-actions.ts | cat -n

Repository: sejaanomalo-web/metas-anomalo

Length of output: 16924


Keep send disabled until the async action settles In React 18.3.1, startTransition drops pending at the first await, so Enviar can re-enable while enviarMensagemAction is still running. That lets Enter/click trigger the same WhatsApp send twice. Track loading with a local async flag around enviar() instead.

🤖 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/Thread.tsx` around lines 28 - 29, The send button can
re-enable too early because Thread’s useTransition pending state stops tracking
after the first await. Replace the reliance on pending in Thread/enviar with a
local async loading flag that is set before calling enviarMensagemAction and
cleared only after it fully settles, and use that flag to keep Enviar disabled
so Enter/click cannot trigger a second send while the request is still in
flight.

Comment on lines +69 to +70
ports:
- "8080:8080" # atrás do Caddy, não exposto direto

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Bind port 8080 to localhost explicitly.

"8080:8080" publishes Evolution on all host interfaces, contradicting the claim that it is only reachable through Caddy. Use "127.0.0.1:8080:8080" so a firewall mistake cannot expose the API directly.

🤖 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 `@docs/CRM-EVOLUTION-SETUP.md` around lines 69 - 70, The port mapping for
Evolution is currently published on all host interfaces, which conflicts with
the intended Caddy-only exposure. Update the docker compose port entry in the
setup docs to bind 8080 explicitly to localhost, using the existing ports block
for the Evolution service so it cannot be reached directly from external
networks.

Comment thread lib/crm-inbound.ts
Comment on lines +114 to +127
await db
.from("crm_instancias")
.update({
ultimo_qr: qr,
status_conexao: "qrcode",
updated_at: new Date().toISOString(),
})
.eq("id", inst.id)

await db
.from("crm_realtime_ping")
.insert({ empresa_slug: inst.empresa_slug as string, kind: "conexao" })

return { ok: true, info: "qrcode atualizado" }

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

Propagate Supabase write failures.

Both handlers ignore errors from crm_instancias.update() and crm_realtime_ping.insert(), then return { ok: true }. A transient database failure can therefore lose the QR/status update while the webhook responds successfully and will not retry. Capture each error, return { ok: false, erro: ... }, and let the route record the failed processing state.

Also applies to: 163-169

🤖 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 114 - 127, The QR/status update path in the
handler around the crm_instancias update and crm_realtime_ping insert is
swallowing Supabase write failures and still returning success. Update the logic
in the affected handler(s) so each db.from(...).update(...) and
db.from(...).insert(...) call checks for errors, and if any write fails, return
{ ok: false, erro: ... } instead of { ok: true }; use the existing handler flow
in crm-inbound.ts to propagate the failure so the route can record the failed
processing state.

Comment on lines +30 to +42
/** Todas as instâncias (ativas e inativas) — a UI decide o que mostrar. */
export async function listarInstancias(): Promise<CrmInstanciaRow[]> {
const db = getSupabaseAdmin()
if (!db) return []
const { data, error } = await db
.from("crm_instancias")
.select("*")
.order("created_at", { ascending: false })
if (error) {
console.error("[crm_instancias] list error", error.message)
return []
}
return (data ?? []) as CrmInstanciaRow[]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Enforce authorization before using the admin client.

These server actions list and mutate CRM instances through the service-role client without checking the caller’s identity, role, or company scope. Since Server Actions are callable from the client, a user who can invoke them could enumerate all instances or deactivate/reactivate arbitrary records by ID. Validate the authenticated user and permitted company/role inside every action before querying or updating.

Also applies to: 59-92, 111-168

🤖 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-instancias-actions.ts` around lines 30 - 42, Add authorization checks
to the CRM instance server actions before any Supabase admin read/write. In
listarInstancias and the related mutate actions (the ones that call
getSupabaseAdmin and operate on crm_instancias), first validate the
authenticated caller and ensure their role/company scope permits the operation;
reject unauthorized requests before querying or updating. Use the existing
action entry points and helpers around listarInstancias and the other
crm-instancias actions to centralize this guard so all instance listing,
deactivation, and reactivation paths enforce the same access rules.

Comment on lines +98 to +106
const r = await criarInstanciaEvolution(instanceName)
revalidarConexoes()
if (!r.ok) {
return {
ok: true,
erro: `Instância salva, mas a Evolution falhou (${r.erro}). Use "Gerar QR" para tentar de novo.`,
}
}
return { ok: true }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Retry instance creation, not only QR generation.

When criarInstanciaEvolution() fails, the local row remains, but the UI directs the user to gerarQrAction; that action calls only instance/connect/:instanceName, which requires the Evolution instance to already exist. A failed instance/create therefore cannot be recovered through the advertised retry flow. Either retry instance/create for rows that were never provisioned or remove/recreate the local row.

Also applies to: 111-130

🤖 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-instancias-actions.ts` around lines 98 - 106, The retry flow in
criarInstanciaEvolution handling is incorrect: when instance/create fails, the
local row stays saved but the UI tells the user to use gerarQrAction, which only
retries instance/connect and cannot recover a never-created Evolution instance.
Update the recovery path in lib/crm-instancias-actions.ts so failed provisioning
either retries the create step for that instance or removes/recreates the local
row before allowing QR generation, and apply the same fix to the related flow in
the 111-130 block.

Comment on lines +19 to +30
export async function enviarMensagemAction(
leadId: string,
texto: string
): Promise<ResultadoEnvio> {
const textoLimpo = texto.trim()
if (!textoLimpo) return { ok: false, erro: "Mensagem vazia." }

const db = getSupabaseAdmin()
if (!db) return { ok: false, erro: "Supabase indisponível." }

const usuario = await getUsuarioAtual()
if (!usuario) return { ok: false, erro: "Sessão expirada." }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

git ls-files 'lib/crm-mensagens-actions.ts' 'app/dashboard/crm/page.tsx' | sed 's#^`#FILE` #'
echo
for f in lib/crm-mensagens-actions.ts app/dashboard/crm/page.tsx; do
  echo "### $f"
  wc -l "$f"
  echo
  sed -n '1,220p' "$f"
  echo
done

Repository: sejaanomalo-web/metas-anomalo

Length of output: 5717


🏁 Script executed:

#!/bin/bash
set -euo pipefail

for f in lib/auth.ts components/crm/Thread.tsx components/crm/ListaConversas.tsx; do
  echo "### $f"
  wc -l "$f"
  echo
  sed -n '1,240p' "$f"
  echo
done

Repository: sejaanomalo-web/metas-anomalo

Length of output: 15417


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "export async function getUsuarioAtual|function getUsuarioAtual|getUsuarioAtual\(" lib/auth.ts lib/crm-mensagens-actions.ts
echo
sed -n '220,330p' lib/auth.ts

Repository: sejaanomalo-web/metas-anomalo

Length of output: 4162


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "enviarMensagemAction|marcarLeadComoLidoAction" .

Repository: sejaanomalo-web/metas-anomalo

Length of output: 630


Add CRM authorization checks to both server actions lib/crm-mensagens-actions.ts:19-30,92-97

enviarMensagemAction only checks that a session exists; it still needs requererPermissao("crm"). marcarLeadComoLidoAction has no session or permission guard at all, so add the same check there too.

🤖 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-mensagens-actions.ts` around lines 19 - 30, Both CRM server actions
are missing the required CRM authorization guard. In enviarMensagemAction, after
confirming the current user via getUsuarioAtual, add requererPermissao("crm")
before proceeding with the Supabase work. In marcarLeadComoLidoAction, add the
same session check and CRM permission check at the start so both actions are
protected consistently. Use the existing getUsuarioAtual and requererPermissao
helpers in lib/crm-mensagens-actions.ts to locate the affected paths.

Comment on lines +61 to +84
await db.from("crm_mensagens").insert({
lead_id: leadId,
instancia_id: inst.id,
empresa_slug: lead.empresa_slug,
direcao: "out",
tipo: "texto",
conteudo: textoLimpo,
wa_message_id: envio.messageId ?? null,
status: envio.ok ? "enviada" : "falha",
erro: envio.ok ? null : envio.erro ?? null,
autor_id: usuario.id,
autor_nome: usuario.nome,
from_me: true,
wa_timestamp: agora,
})

await db
.from("crm_leads")
.update({ ultima_interacao_em: agora, updated_at: agora })
.eq("id", leadId)

await db
.from("crm_realtime_ping")
.insert({ empresa_slug: lead.empresa_slug, lead_id: leadId, kind: "msg" })

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

Supabase write errors are silently ignored — message can be lost while the caller still sees success.

None of the three writes (message insert, lead update, realtime ping insert) check the returned error. If the crm_mensagens insert fails (RLS, constraint, transient DB issue) while envio.ok is true, the function still returns { ok: true } even though the message was never persisted — it disappears from the thread entirely despite having actually been sent via WhatsApp. Same silent-failure risk applies to the lead's ultima_interacao_em update and the realtime ping.

🐛 Suggested fix
-  await db.from("crm_mensagens").insert({
+  const { error: insertError } = await db.from("crm_mensagens").insert({
     lead_id: leadId,
     instancia_id: inst.id,
     empresa_slug: lead.empresa_slug,
     direcao: "out",
     tipo: "texto",
     conteudo: textoLimpo,
     wa_message_id: envio.messageId ?? null,
     status: envio.ok ? "enviada" : "falha",
     erro: envio.ok ? null : envio.erro ?? null,
     autor_id: usuario.id,
     autor_nome: usuario.nome,
     from_me: true,
     wa_timestamp: agora,
   })
+  if (insertError) {
+    console.error("[crm_mensagens] insert error", insertError.message)
+    return { ok: false, erro: "Falha ao registrar a mensagem." }
+  }
🤖 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-mensagens-actions.ts` around lines 61 - 84, In the CRM message send
flow that performs the crm_mensagens insert, crm_leads update, and
crm_realtime_ping insert, stop ignoring Supabase write results and explicitly
inspect each returned error. If any of these operations fails, log/return the
failure instead of continuing so the caller does not get a false success when
the WhatsApp send succeeded but persistence did not. Update the same action that
contains the db.from(...).insert/update chain to propagate the error and only
report success after all three writes complete without errors.

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