From acc106f70bed7573cc2f0dafd7e83790a5b715a4 Mon Sep 17 00:00:00 2001 From: W1 CAPITAL Date: Fri, 9 Oct 2026 17:31:33 -0300 Subject: [PATCH 01/13] fix(waauto): use actual cloud integration-token headers for background sends instead of unsupported HMAC bearer --- src/lib/wa-auto-client.ts | 43 +++++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/src/lib/wa-auto-client.ts b/src/lib/wa-auto-client.ts index 64da57b7..97a4b451 100644 --- a/src/lib/wa-auto-client.ts +++ b/src/lib/wa-auto-client.ts @@ -341,20 +341,47 @@ export async function logoutWaAuto() { } /** Already-authorized background identity; never accepts identity from a browser argument. */ +/** + * WA.Auto Cloud expects the shared integration token plus x-lexis-user-id. + * The older lexishwa1 HMAC credential is not accepted by its integration + * middleware and caused every server-side queue send to return HTTP 401. + * Caller resolves company ownership and management permission before here. + */ export async function waAutoForOwner(userId:string,empresaId:string,to?:string,message?:string) { const cfg=getWaAutoConfig(); + const validId=/^[0-9a-f-]{36}$/i; + if(!validId.test(userId)||!validId.test(empresaId))return { + ok:false as const,rejected:true,httpStatus:403,error:'Responsável pela sessão não identificado.' + }; + if(!cfg.integrationToken)return { + ok:false as const,rejected:true,httpStatus:503, + error:'WA_INTEGRATION_TOKEN não configurado no LexisPredict para envios do servidor.' + }; try { - const {signWaWorkerIdentity}=await import('@/lib/wa-worker-auth'); - const token=signWaWorkerIdentity(userId,empresaId,cfg.integrationToken); const response=await fetch(cfg.baseUrl+'/api/integrations/lexispredict/'+(to?'send':'status'),{ - method:to?'POST':'GET',headers:{Authorization:'Bearer '+token,'Content-Type':'application/json'}, - ...(to?{body:JSON.stringify({to,message})}:{}),cache:'no-store',signal:AbortSignal.timeout(to?40000:12000), + method:to?'POST':'GET', + headers:{ + Authorization:'Bearer '+cfg.integrationToken, + 'x-wa-integration-token':cfg.integrationToken, + 'x-lexis-user-id':userId, + 'Content-Type':'application/json', + }, + ...(to?{body:JSON.stringify({to,message})}:{}), + cache:'no-store', + signal:AbortSignal.timeout(to?40000:12000), }); const body=await readJson(response); - if(!response.ok||body?.ok===false)return {ok:false as const,rejected:response.status>=400&&response.status<500, - httpStatus:response.status,error:String(body?.error||'WA.Auto HTTP '+response.status),raw:body}; - if(!to && body?.connection?.status!=='ready')return {ok:false as const,rejected:true, - httpStatus:409,error:'WhatsApp do responsável desconectado',raw:body}; + if(!response.ok||body?.ok===false)return { + ok:false as const,rejected:response.status>=400&&response.status<500, + httpStatus:response.status, + error:response.status===401 + ? 'WA.Auto rejeitou a credencial do servidor (HTTP 401). Confira WA_INTEGRATION_TOKEN em ambas as plataformas; a campanha foi bloqueada.' + : String(body?.error||'WA.Auto HTTP '+response.status),raw:body, + }; + if(!to && body?.connection?.status!=='ready')return { + ok:false as const,rejected:true,httpStatus:409, + error:'WhatsApp do responsável desconectado no WA.Auto.',raw:body, + }; return {ok:true as const,raw:body}; }catch(e:any){return {ok:false as const,rejected:false,error:String(e?.message||'WA.Auto indisponível')};} } From 8acb60c3505624a516f2e9df9354f8fbd185e5dc Mon Sep 17 00:00:00 2001 From: W1 CAPITAL Date: Fri, 9 Oct 2026 17:32:27 -0300 Subject: [PATCH 02/13] fix(waauto): check background authentication before campaigns and speed portfolio preview + bulk queue inserts --- src/lib/wa-movement-campaign.ts | 34 ++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/src/lib/wa-movement-campaign.ts b/src/lib/wa-movement-campaign.ts index b24e647f..8bd4c714 100644 --- a/src/lib/wa-movement-campaign.ts +++ b/src/lib/wa-movement-campaign.ts @@ -5,7 +5,7 @@ import 'server-only'; import { getSupabaseAdmin, getUserContext } from '@/lib/server-db'; import { resolveWaAutoPermissions } from '@/lib/wa-auto-permissions'; -import { waAutoHealth, waAutoForOwner, sendViaWaAuto } from '@/lib/wa-auto-client'; +import { waAutoForOwner, sendViaWaAuto } from '@/lib/wa-auto-client'; import { persistWhatsAppMessage } from '@/lib/whatsapp-persist'; import { prepareMovementAlert, type SourceRow, type Alert } from '@/lib/wa-movement-builder'; import {getDailyReturnSettings,upsertDailyReturnSettings} from '@/lib/wa-daily-return-service'; @@ -31,7 +31,7 @@ async function collectPortfolio(empresaId: string, kind:CampaignKind='movement', alreadyClosed:0, consentMissing:0, needsReview:0, alreadyNotified:0, missingReturn:0, noNewMovement:0 }; let after=0; for (;;) { - const {data,error}=await db.rpc('wa_notice_portfolio_rows',{p_empresa:empresaId,p_after:after,p_limit:600}); + const {data,error}=await db.rpc('wa_notice_portfolio_rows',{p_empresa:empresaId,p_after:after,p_limit:3000}); if(error)throw new Error('Carteira: '+error.message); const rows=(data||[]) as SourceRow[]; for(const row of rows) { @@ -48,7 +48,7 @@ async function collectPortfolio(empresaId: string, kind:CampaignKind='movement', else counts.withoutEvent++; } else entries.push(alert); } - if(rows.length<600)break; + if(rows.length<3000)break; after=Number(rows[rows.length-1].id); } return {entries,counts}; @@ -58,10 +58,14 @@ export async function previewMovementCampaign(kind:CampaignKind='movement') { try { const ctx = await requireManager(); const settings=await getDailyReturnSettings(ctx.empresa_id); - const { entries, counts } = await collectPortfolio(ctx.empresa_id,kind,settings.consentAttested); const db = await getSupabaseAdmin(); + const [portfolio, priorResponse] = await Promise.all([ + collectPortfolio(ctx.empresa_id,kind,settings.consentAttested), + db.rpc('wa_notice_prior',{p_empresa:ctx.empresa_id}), + ]); + const {entries,counts}=portfolio; // Preview excludes already queued/sent versions (database uniqueness also guards races). - const {data,error} = await db.rpc('wa_notice_prior',{p_empresa:ctx.empresa_id}); + const {data,error}=priorResponse; if (error) throw new Error('Fila não instalada no Supabase. Aplique a migração: ' + error.message); const prior = new Set((data || []).map((x:{processo_id:number;event_hash:string}) => x.processo_id + ':' + x.event_hash)); const pending = entries.filter(x => !prior.has(x.processo_id + ':' + x.event_hash)); @@ -82,7 +86,9 @@ export async function createMovementCampaign(confirmed: boolean,kind:CampaignKin const settings=await getDailyReturnSettings(ctx.empresa_id); if (!settings.consentAttested && confirmed !== true) throw new Error('Confirme previamente a autorização dos contatos de acompanhamento.'); - const health = await waAutoHealth(); + // Health must use the SAME server credential as the background dispatch. + // A browser-connected session alone did not validate automated sends (401). + const health = await waAutoForOwner(ctx.auth_id,ctx.empresa_id); if (!health.ok) throw new Error('Conecte o WA.Auto antes de iniciar. ' + (health.error || 'Sessão indisponível')); const db=await getSupabaseAdmin(); const {data: existing} = await db.from('wa_movement_campaigns').select('id') @@ -102,13 +108,15 @@ export async function createMovementCampaign(confirmed: boolean,kind:CampaignKin if (createErr || !campaign) throw new Error(createErr?.message || 'Não foi possível criar a campanha.'); let inserted=0; try { - for(let offset=0;offset - db.rpc('wa_add_campaign_notice',{p_campaign:campaign.id,p_notice:notice}))); - for(const result of results) { - if(result.error)throw new Error(result.error.message); - if(result.data)inserted++; - } + // One atomic SQL batch per 300 records rather than 150+ sequential + // network round trips for a portfolio of ~2,600 cases. + for(let offset=0;offset Date: Fri, 9 Oct 2026 17:32:53 -0300 Subject: [PATCH 03/13] fix(db): preview 3000 compact portfolio rows in one call and enqueue campaigns by atomic batches --- ...61009192100_wa_fast_preview_bulk_queue.sql | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 supabase/migrations/20261009192100_wa_fast_preview_bulk_queue.sql diff --git a/supabase/migrations/20261009192100_wa_fast_preview_bulk_queue.sql b/supabase/migrations/20261009192100_wa_fast_preview_bulk_queue.sql new file mode 100644 index 00000000..1e35cb1b --- /dev/null +++ b/supabase/migrations/20261009192100_wa_fast_preview_bulk_queue.sql @@ -0,0 +1,52 @@ +-- Reduce portfolio preview from five slow network pages to one compact RPC. +-- Reuses the existing small judicial projection; never dumps entire 'dados'. +create or replace function public.wa_notice_portfolio_rows(p_empresa uuid,p_after bigint default 0,p_limit integer default 600) +returns setof jsonb language sql stable security invoker set search_path='' as $$ + select jsonb_build_object( + 'id',p.id,'empresa_id',p.empresa_id,'cliente',p.cliente,'telefone',p.telefone,'protocolo_ref',p.protocolo_ref, + 'ultimo_retorno',p.ultimo_retorno,'proximo_retorno',p.proximo_retorno,'status',p.status,'status_interno',p.status_interno, + 'datajud_ultimo_movimento',p.datajud_ultimo_movimento,'datajud_ultimo_nome',p.datajud_ultimo_nome, + 'djen_ultima_data',p.djen_ultima_data,'djen_ultimo_resumo',p.djen_ultimo_resumo, + 'dados',jsonb_strip_nulls(jsonb_build_object( + 'cliente',p.dados->'cliente','CLIENTE',p.dados->'CLIENTE','telefone',p.dados->'telefone','TELEFONE',p.dados->'TELEFONE', + 'protocolo',p.dados->'protocolo','situacao',p.dados->'situacao','SITUACAO',p.dados->'SITUACAO', + 'status',p.dados->'status','status_interno',p.dados->'status_interno','statusManual',p.dados->'statusManual','STATUS_MANUAL',p.dados->'STATUS_MANUAL', + 'whatsapp_opt_in',p.dados->'whatsapp_opt_in','consentimento_whatsapp',p.dados->'consentimento_whatsapp','whatsapp_autorizado',p.dados->'whatsapp_autorizado', + 'nao_contatar',p.dados->'nao_contatar','não_contatar',p.dados->'não_contatar','whatsapp_opt_out',p.dados->'whatsapp_opt_out', + 'optOut',p.dados->'optOut','optout',p.dados->'optout','bloquear_whatsapp',p.dados->'bloquear_whatsapp','naoEnviarWhatsapp',p.dados->'naoEnviarWhatsapp', + 'ultimoRetorno',p.dados->'ultimoRetorno','ultimo_retorno',p.dados->'ultimo_retorno', + 'proximoRetorno',p.dados->'proximoRetorno','proximo_retorno',p.dados->'proximo_retorno' + ))) + from public.processos p where p.empresa_id=p_empresa and p.id>p_after + order by p.id limit least(3000,greatest(1,p_limit)) +$$; + +revoke all on function public.wa_notice_portfolio_rows(uuid,bigint,integer) from public,anon,authenticated; +grant execute on function public.wa_notice_portfolio_rows(uuid,bigint,integer) to service_role; + +-- Bulk insert is atomic per request and restricted to existing per-tenant +-- campaign rows. It delegates actual constraints to wa_add_campaign_notice. +create or replace function public.wa_add_campaign_notices(p_campaign uuid,p_notices jsonb) +returns integer language plpgsql security invoker set search_path='' as $$ +declare + campaign public.wa_movement_campaigns%rowtype; + item jsonb; + accepted uuid; + total integer:=0; +begin + if jsonb_typeof(p_notices)<>'array' or jsonb_array_length(p_notices)>500 + then raise exception 'Invalid campaign batch'; end if; + select * into campaign from public.wa_movement_campaigns + where id=p_campaign and status='paused' and consent_attested=true + for update; + if not found then raise exception 'Campaign not ready for queueing'; end if; + for item in select value from jsonb_array_elements(p_notices) loop + if item->>'empresa_id' is distinct from campaign.empresa_id::text + then raise exception 'Tenant mismatch'; end if; + accepted:=public.wa_add_campaign_notice(p_campaign,item); + if accepted is not null then total:=total+1; end if; + end loop; + return total; +end $$; +revoke all on function public.wa_add_campaign_notices(uuid,jsonb) from public,anon,authenticated; +grant execute on function public.wa_add_campaign_notices(uuid,jsonb) to service_role; From 9f82e8d4d8e5bce4c4f696d14bfa47343fa61727 Mon Sep 17 00:00:00 2001 From: W1 CAPITAL Date: Fri, 9 Oct 2026 17:34:22 -0300 Subject: [PATCH 04/13] fix(db): include timestamped DataJud verification and original DJEN proof in portfolio preview --- .../migrations/20261009192100_wa_fast_preview_bulk_queue.sql | 3 +++ 1 file changed, 3 insertions(+) diff --git a/supabase/migrations/20261009192100_wa_fast_preview_bulk_queue.sql b/supabase/migrations/20261009192100_wa_fast_preview_bulk_queue.sql index 1e35cb1b..805085d6 100644 --- a/supabase/migrations/20261009192100_wa_fast_preview_bulk_queue.sql +++ b/supabase/migrations/20261009192100_wa_fast_preview_bulk_queue.sql @@ -5,6 +5,7 @@ returns setof jsonb language sql stable security invoker set search_path='' as $ select jsonb_build_object( 'id',p.id,'empresa_id',p.empresa_id,'cliente',p.cliente,'telefone',p.telefone,'protocolo_ref',p.protocolo_ref, 'ultimo_retorno',p.ultimo_retorno,'proximo_retorno',p.proximo_retorno,'status',p.status,'status_interno',p.status_interno, + 'datajud_consultado_em',p.datajud_consultado_em, 'datajud_ultimo_movimento',p.datajud_ultimo_movimento,'datajud_ultimo_nome',p.datajud_ultimo_nome, 'djen_ultima_data',p.djen_ultima_data,'djen_ultimo_resumo',p.djen_ultimo_resumo, 'dados',jsonb_strip_nulls(jsonb_build_object( @@ -14,6 +15,8 @@ returns setof jsonb language sql stable security invoker set search_path='' as $ 'whatsapp_opt_in',p.dados->'whatsapp_opt_in','consentimento_whatsapp',p.dados->'consentimento_whatsapp','whatsapp_autorizado',p.dados->'whatsapp_autorizado', 'nao_contatar',p.dados->'nao_contatar','não_contatar',p.dados->'não_contatar','whatsapp_opt_out',p.dados->'whatsapp_opt_out', 'optOut',p.dados->'optOut','optout',p.dados->'optout','bloquear_whatsapp',p.dados->'bloquear_whatsapp','naoEnviarWhatsapp',p.dados->'naoEnviarWhatsapp', + 'wa_djen_evidence',p.dados->'wa_djen_evidence','tribunal_conferencia',p.dados->'tribunal_conferencia', + 'datajud_consultado_em',p.dados->'datajud_consultado_em', 'ultimoRetorno',p.dados->'ultimoRetorno','ultimo_retorno',p.dados->'ultimo_retorno', 'proximoRetorno',p.dados->'proximoRetorno','proximo_retorno',p.dados->'proximo_retorno' ))) From 46344b391be11b321cb340903da10fc34955bb62 Mon Sep 17 00:00:00 2001 From: W1 CAPITAL Date: Fri, 9 Oct 2026 17:36:09 -0300 Subject: [PATCH 05/13] fix(whatsapp): restore natural page scrolling while chat and process list retain independent scroll areas --- src/app/whatsapp/page.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/whatsapp/page.tsx b/src/app/whatsapp/page.tsx index d55e2256..bcea4d5e 100644 --- a/src/app/whatsapp/page.tsx +++ b/src/app/whatsapp/page.tsx @@ -1151,9 +1151,9 @@ function WhatsAppTerminalInner() { return ( <> -
+
-
+
@@ -1220,7 +1220,7 @@ function WhatsAppTerminalInner() { -
+
{/* Lista */}