mirror of
https://github.com/melgarafael/DeskcommCRM.git
synced 2026-10-02 01:28:34 +08:00
fix(crm-vivo): os quatro canais mortos passam pelo hook curado
A correção do realtime (24b9ec2 — `setAuth` antes do `subscribe`) mora em
`useRealtimeChannel`, e curava os 5 hooks que o usavam. QUATRO lugares abriam
`.channel()` direto no cliente de navegador e ficaram de fora: assinavam como
ANÔNIMOS, recebiam "ok", logavam "subscribed" e nunca entregavam evento.
Provado morto pelo @QAVivo com A/B na MESMA página, mesma sessão, mesmo socket: o
canal do board (curado) ia com token, o de `useAgentRuns` ia sem — e um UPDATE real
na tabela, dentro do filtro do canal, não produziu quadro nenhum.
CONSEQUÊNCIA AO USUÁRIO, e é a que justifica o conserto: a tela de execuções promete
acompanhamento ao vivo e dispara "nova execução iniciada" / "execução concluída".
Esses avisos NUNCA apareceram. Quem abria para acompanhar um agente trabalhando via
lista parada e concluía que nada estava acontecendo — enquanto o agente rodava.
Não é recurso ausente; é recurso que APARENTA existir e mente por omissão.
Migrados: useAlertsRealtime, useTenantHealth (broadcast), useAgentRuns e a tela de
fontes de conhecimento (postgres_changes com filtro).
Depois disto, o único `.channel()` do repositório é o de dentro do próprio hook —
consumidores foram de 5 para 9. Assim a correção deixa de depender de alguém lembrar
que existe um caminho curado.
unit 918/918 · lint 0 erros · typecheck 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gpdoAwEGCcM9DwzC6qtkL
This commit is contained in:
co-authored by
Claude Opus 5
parent
f017ac3240
commit
6ca75d8535
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
import { useEffect } from "react";
|
||||
import { useCallback } from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { createClient } from "@/lib/supabase/browser";
|
||||
import { useRealtimeChannel } from "@/hooks/realtime/useRealtimeChannel";
|
||||
import {
|
||||
sourcesQueryKey,
|
||||
useKnowledgeSources,
|
||||
@@ -34,30 +34,24 @@ export function KnowledgeSourcesClient({ agentId, initialSources }: Props) {
|
||||
const { data: sources } = useKnowledgeSources(agentId, { initialData: initialSources });
|
||||
const reindex = useReindexSource(agentId);
|
||||
|
||||
// Realtime subscription.
|
||||
useEffect(() => {
|
||||
const supabase = createClient();
|
||||
const channel = supabase
|
||||
.channel(`ai-knowledge-sources-${agentId}`)
|
||||
.on(
|
||||
"postgres_changes" as never,
|
||||
{
|
||||
event: "*",
|
||||
schema: "public",
|
||||
table: "ai_knowledge_sources",
|
||||
filter: `agent_id=eq.${agentId}`,
|
||||
},
|
||||
() => {
|
||||
qc.invalidateQueries({ queryKey: sourcesQueryKey(agentId) });
|
||||
},
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
return () => {
|
||||
supabase.removeChannel(channel);
|
||||
};
|
||||
const onChange = useCallback(() => {
|
||||
qc.invalidateQueries({ queryKey: sourcesQueryKey(agentId) });
|
||||
}, [agentId, qc]);
|
||||
|
||||
// Pelo hook compartilhado: `.channel()` cru assina como ANÔNIMO (cookie de
|
||||
// sessão httpOnly) — recebe "ok" e nunca entrega. Aqui o efeito era a lista de
|
||||
// fontes não atualizar sozinha depois de uma reindexação.
|
||||
useRealtimeChannel({
|
||||
name: `ai-knowledge-sources-${agentId}`,
|
||||
postgresChanges: {
|
||||
event: "*",
|
||||
schema: "public",
|
||||
table: "ai_knowledge_sources",
|
||||
filter: `agent_id=eq.${agentId}`,
|
||||
},
|
||||
onChange,
|
||||
});
|
||||
|
||||
const list = sources ?? [];
|
||||
|
||||
const bySlot: Record<KnowledgeSourceType, SourceRow | undefined> = {
|
||||
|
||||
+35
-36
@@ -8,12 +8,12 @@
|
||||
* controla subscribe/unsubscribe pra não vazar canais quando a tab Runs não
|
||||
* está ativa.
|
||||
*/
|
||||
import { useEffect } from "react";
|
||||
import { useCallback } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
import { createClient } from "@/lib/supabase/browser";
|
||||
import { useRealtimeChannel } from "@/hooks/realtime/useRealtimeChannel";
|
||||
|
||||
export type RunStatus =
|
||||
| "pending"
|
||||
@@ -77,41 +77,40 @@ export function useAgentRuns(
|
||||
enabled: !!agentId && enabled,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!agentId || !realtime) return;
|
||||
const supabase = createClient();
|
||||
const channel = supabase
|
||||
.channel(`ai-agent-runs-${agentId}`)
|
||||
.on(
|
||||
"postgres_changes" as never,
|
||||
{
|
||||
event: "*",
|
||||
schema: "public",
|
||||
table: "ai_agent_runs",
|
||||
filter: `agent_id=eq.${agentId}`,
|
||||
},
|
||||
(payload: { eventType?: string; new?: AgentRunRow }) => {
|
||||
qc.invalidateQueries({ queryKey: agentRunsKey(agentId) });
|
||||
if (payload?.eventType === "INSERT" && !payload.new?.is_dry_run) {
|
||||
toast.info("Nova execução iniciada.");
|
||||
}
|
||||
if (payload?.eventType === "UPDATE" && payload.new?.status === "completed") {
|
||||
toast.success("Execução concluída.");
|
||||
}
|
||||
if (
|
||||
payload?.eventType === "UPDATE" &&
|
||||
(payload.new?.status === "failed" || payload.new?.status === "aborted")
|
||||
) {
|
||||
toast.error(`Execução ${payload.new?.status}.`);
|
||||
}
|
||||
},
|
||||
)
|
||||
.subscribe();
|
||||
const onChange = useCallback(
|
||||
(raw: unknown) => {
|
||||
const payload = raw as { eventType?: string; new?: AgentRunRow } | null;
|
||||
qc.invalidateQueries({ queryKey: agentRunsKey(agentId) });
|
||||
if (payload?.eventType === "INSERT" && !payload.new?.is_dry_run) {
|
||||
toast.info("Nova execução iniciada.");
|
||||
}
|
||||
if (payload?.eventType === "UPDATE" && payload.new?.status === "completed") {
|
||||
toast.success("Execução concluída.");
|
||||
}
|
||||
if (
|
||||
payload?.eventType === "UPDATE" &&
|
||||
(payload.new?.status === "failed" || payload.new?.status === "aborted")
|
||||
) {
|
||||
toast.error(`Execução ${payload.new?.status}.`);
|
||||
}
|
||||
},
|
||||
[agentId, qc],
|
||||
);
|
||||
|
||||
return () => {
|
||||
supabase.removeChannel(channel);
|
||||
};
|
||||
}, [agentId, realtime, qc]);
|
||||
// Pelo hook compartilhado. Este canal foi o PROVADO MORTO pelo @QAVivo: A/B na
|
||||
// mesma página, mesma sessão, mesmo socket — o do board (curado) ia com token,
|
||||
// este ia sem. Um UPDATE real na tabela, dentro do filtro, não produziu quadro
|
||||
// nenhum. Consequência ao usuário: a tela promete acompanhamento ao vivo, e os
|
||||
// avisos abaixo NUNCA apareceram — quem abria para ver o agente trabalhando via
|
||||
// lista parada e concluía que nada estava acontecendo.
|
||||
useRealtimeChannel({
|
||||
name: agentId ? `ai-agent-runs-${agentId}` : "ai-agent-runs-disabled",
|
||||
postgresChanges: agentId
|
||||
? { event: "*", schema: "public", table: "ai_agent_runs", filter: `agent_id=eq.${agentId}` }
|
||||
: undefined,
|
||||
onChange,
|
||||
enabled: !!agentId && realtime,
|
||||
});
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
+18
-20
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
import { useEffect } from "react";
|
||||
import { useCallback } from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { createClient } from "@/lib/supabase/browser";
|
||||
import { useRealtimeChannel } from "@/hooks/realtime/useRealtimeChannel";
|
||||
import type { AlertKind } from "@/app/api/v1/admin/dashboard/kpis/route";
|
||||
|
||||
const KIND_LABELS: Record<AlertKind, string> = {
|
||||
@@ -20,23 +20,21 @@ interface AlertBroadcast {
|
||||
export function useAlertsRealtime() {
|
||||
const qc = useQueryClient();
|
||||
|
||||
useEffect(() => {
|
||||
const supabase = createClient();
|
||||
const channel = supabase
|
||||
.channel("alerts-platform")
|
||||
.on("broadcast", { event: "*" }, (payload: { payload?: AlertBroadcast }) => {
|
||||
const data = payload?.payload;
|
||||
const kind = data?.kind;
|
||||
const label = kind ? (KIND_LABELS[kind] ?? kind) : "alerta";
|
||||
toast.warning(`Novo ${label}`, {
|
||||
description: data?.message,
|
||||
});
|
||||
qc.invalidateQueries({ queryKey: ["admin", "dashboard", "kpis"] });
|
||||
})
|
||||
.subscribe();
|
||||
const onChange = useCallback(
|
||||
(payload: unknown) => {
|
||||
const data = (payload as { payload?: AlertBroadcast } | null)?.payload;
|
||||
const kind = data?.kind;
|
||||
const label = kind ? (KIND_LABELS[kind] ?? kind) : "alerta";
|
||||
toast.warning(`Novo ${label}`, { description: data?.message });
|
||||
qc.invalidateQueries({ queryKey: ["admin", "dashboard", "kpis"] });
|
||||
},
|
||||
[qc],
|
||||
);
|
||||
|
||||
return () => {
|
||||
supabase.removeChannel(channel);
|
||||
};
|
||||
}, [qc]);
|
||||
// Pelo hook compartilhado, e não por `.channel()` cru: o cookie de sessão é
|
||||
// httpOnly, então o supabase-js do browser não enxerga a sessão e assina como
|
||||
// ANÔNIMO — canal que recebe "ok", loga "subscribed" e nunca entrega evento.
|
||||
// A correção (24b9ec2) mora em `useRealtimeChannel`, que chama `setAuth` antes
|
||||
// do `subscribe`; quem abre canal direto ficou de fora dela.
|
||||
useRealtimeChannel({ name: "alerts-platform", broadcast: { event: "*" }, onChange });
|
||||
}
|
||||
|
||||
+11
-16
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
import { useEffect, useCallback } from "react";
|
||||
import { useCallback } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
import { createClient } from "@/lib/supabase/browser";
|
||||
import { useRealtimeChannel } from "@/hooks/realtime/useRealtimeChannel";
|
||||
import type { TenantHealthResponse } from "@/app/api/v1/admin/tenants/[id]/health/route";
|
||||
|
||||
export type { TenantHealthResponse };
|
||||
@@ -28,20 +28,15 @@ export function useTenantHealth(id: string) {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [queryClient, id]);
|
||||
|
||||
// Realtime: subscribe to broadcast channel; any event triggers a refetch
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
const supabase = createClient();
|
||||
const channelName = `tenant-health-${id}`;
|
||||
const channel = supabase
|
||||
.channel(channelName)
|
||||
.on("broadcast", { event: "*" }, invalidate)
|
||||
.subscribe();
|
||||
|
||||
return () => {
|
||||
void supabase.removeChannel(channel);
|
||||
};
|
||||
}, [id, invalidate]);
|
||||
// Pelo hook compartilhado: `.channel()` cru assina como ANÔNIMO (cookie de
|
||||
// sessão httpOnly), recebe "ok" e nunca entrega evento. A correção mora em
|
||||
// `useRealtimeChannel`, que chama `setAuth` antes do `subscribe`.
|
||||
useRealtimeChannel({
|
||||
name: id ? `tenant-health-${id}` : "tenant-health-disabled",
|
||||
broadcast: { event: "*" },
|
||||
onChange: invalidate,
|
||||
enabled: !!id,
|
||||
});
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user