@danhachuel/leadmaster 0.1.14 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/backend/__pycache__/main.cpython-311.pyc +0 -0
- package/backend/services/__pycache__/__init__.cpython-311.pyc +0 -0
- package/backend/services/__pycache__/agents.cpython-311.pyc +0 -0
- package/backend/services/__pycache__/crm_agent.cpython-311.pyc +0 -0
- package/backend/services/__pycache__/extractors.cpython-311.pyc +0 -0
- package/backend/services/__pycache__/flags.cpython-311.pyc +0 -0
- package/backend/services/__pycache__/llm.cpython-311.pyc +0 -0
- package/backend/services/__pycache__/storage.cpython-311.pyc +0 -0
- package/backend/tests/__pycache__/conftest.cpython-311-pytest-8.4.2.pyc +0 -0
- package/backend/tests/__pycache__/test_cnpj_extractor.cpython-311-pytest-8.4.2.pyc +0 -0
- package/backend/tests/__pycache__/test_crm_routes.cpython-311-pytest-8.4.2.pyc +0 -0
- package/backend/tests/__pycache__/test_instagram_extractor.cpython-311-pytest-8.4.2.pyc +0 -0
- package/backend/tests/__pycache__/test_launcher_protections.cpython-311-pytest-8.4.2.pyc +0 -0
- package/backend/tests/__pycache__/test_maps_extractor.cpython-311-pytest-8.4.2.pyc +0 -0
- package/backend/tests/__pycache__/test_process_flags.cpython-311-pytest-8.4.2.pyc +0 -0
- package/backend/tests/test_launcher_protections.py +16 -3
- package/frontend/dist/assets/{index-DgsUzQ92.js → index-CAvEEMWg.js} +1 -1
- package/frontend/dist/index.html +1 -1
- package/package.json +1 -1
- package/scripts/cli.mjs +139 -24
package/CHANGELOG.md
CHANGED
|
@@ -37,6 +37,36 @@ normaliza para a forma canônica (ex.: `0.1.0`).
|
|
|
37
37
|
falha se `process.stdin.resume();` desaparecer, se `.unref()` aparecer, se o
|
|
38
38
|
ciclo de vida vinculado for removido ou se os handlers globais sumirem.
|
|
39
39
|
|
|
40
|
+
## [0.1.15] — 2026-11-08
|
|
41
|
+
|
|
42
|
+
### Corrigido
|
|
43
|
+
|
|
44
|
+
- **Launcher — kill switch: a versão nova SEMPRE assume o controle** — o
|
|
45
|
+
bloqueio de porta foi eliminado. Antes, ao detectar a porta ocupada, o
|
|
46
|
+
`cli.mjs` apenas imprimia "LeadMaster já está rodando" e encerrava: a versão
|
|
47
|
+
antiga continuava no ar, o usuário não conseguia testar a nova e o `npx`
|
|
48
|
+
falhava com `EBUSY` (arquivos travados no Windows). Agora:
|
|
49
|
+
- **`encerrarInstanciaAnterior()`** roda no início do `install` (libera os
|
|
50
|
+
arquivos antes de instalar — fim do `EBUSY`) e do `start`;
|
|
51
|
+
- descoberta do PID dono da porta via `netstat -ano` (Windows) /
|
|
52
|
+
`lsof -ti:porta` (Linux/macOS, com fallback `ps`), seguida de
|
|
53
|
+
`taskkill /F /T /PID` ou `kill -9`;
|
|
54
|
+
- espera de até 5s pela liberação da porta antes de subir o novo servidor;
|
|
55
|
+
- **higiene preservada:** só mata processos **confirmados** como LeadMaster
|
|
56
|
+
(`/api/health` com payload próprio + PID dono da porta). Portas ocupadas
|
|
57
|
+
por outros projetos são respeitadas — o launcher avança para a próxima
|
|
58
|
+
porta livre, como sempre;
|
|
59
|
+
- ciclo de vida vinculado mantido: com o Python antigo morto, o Node antigo
|
|
60
|
+
encerra sozinho (`child.on('exit')`/`close`).
|
|
61
|
+
- **AGENTS.md:** 6ª regra inviolável do launcher — *Kill switch*: nunca ignorar
|
|
62
|
+
porta ocupada por instância do LeadMaster; mensagem de reuso **proibida**.
|
|
63
|
+
- **Gate de regressão:** `test_launcher_protections.py` falha se o kill switch
|
|
64
|
+
desaparecer (`encerrarInstanciaAnterior`/`taskkill`/`netstat`/`lsof`) ou se a
|
|
65
|
+
mensagem proibida voltar. Suíte completa: **32 passed**.
|
|
66
|
+
- **Testado ao vivo:** instância anterior na mesma porta detectada, encerrada
|
|
67
|
+
(PID confirmado) e nova instância assumida com PID novo; node antigo caiu
|
|
68
|
+
pelo ciclo de vida; instância de outros projetos preservada.
|
|
69
|
+
|
|
40
70
|
## [0.1.14] — 2026-11-08
|
|
41
71
|
|
|
42
72
|
### Adicionado
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -6,9 +6,11 @@ Windows (MobaXterm) for removida:
|
|
|
6
6
|
1. Keep-alive com setInterval (event loop ativo sem I/O de console);
|
|
7
7
|
2. Terminal preso: `process.stdin.resume();` (PTY do MobaXterm nunca fecha);
|
|
8
8
|
sem `process.exit(0)` no caminho de sucesso e sem `.unref()` no worker;
|
|
9
|
-
3.
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
3. Kill switch: instância anterior do LeadMaster encerrada antes de subir a
|
|
10
|
+
nova versão (taskkill/lsof pelo PID da porta) — nunca reuso de instância;
|
|
11
|
+
4. Absorção de SIGINT/SIGHUP no Windows (apenas log, sem encerrar);
|
|
12
|
+
5. Função guardConsoleStream (anti-EPIPE com espelho em arquivo de log);
|
|
13
|
+
6. Isolamento do worker Python (detached: true / CREATE_NEW_PROCESS_GROUP).
|
|
12
14
|
"""
|
|
13
15
|
|
|
14
16
|
from pathlib import Path
|
|
@@ -31,6 +33,17 @@ class TestLauncherProtections:
|
|
|
31
33
|
def test_worker_nunca_unref(self):
|
|
32
34
|
assert ".unref()" not in _conteudo()
|
|
33
35
|
|
|
36
|
+
def test_kill_switch_mata_instancia_anterior(self):
|
|
37
|
+
# Regra inviolável: nunca ignorar uma porta ocupada por outra instância
|
|
38
|
+
# do LeadMaster — matar o processo antigo antes de subir o novo. Sem
|
|
39
|
+
# isso a versão antiga trava arquivos (EBUSY no npx) e a nova não sobe.
|
|
40
|
+
c = _conteudo()
|
|
41
|
+
assert "encerrarInstanciaAnterior" in c
|
|
42
|
+
assert "taskkill" in c # Windows: mata a árvore do PID da porta
|
|
43
|
+
assert "netstat" in c or "lsof" in c # descoberta do PID pela porta
|
|
44
|
+
# Avisar que já existe instância no ar é proibido (impede atualizações).
|
|
45
|
+
assert "já está rodando" not in c
|
|
46
|
+
|
|
34
47
|
def test_ciclo_de_vida_vinculado(self):
|
|
35
48
|
c = _conteudo()
|
|
36
49
|
assert "child.on('exit'" in c or "child.on(\"exit\"" in c
|
|
@@ -64,7 +64,7 @@ Error generating stack: `+i.message+`
|
|
|
64
64
|
* LICENSE.md file in the root directory of this source tree.
|
|
65
65
|
*
|
|
66
66
|
* @license MIT
|
|
67
|
-
*/function Al(){return Al=Object.assign?Object.assign.bind():function(t){for(var e=1;e<arguments.length;e++){var n=arguments[e];for(var s in n)({}).hasOwnProperty.call(n,s)&&(t[s]=n[s])}return t},Al.apply(null,arguments)}function py(t,e){if(t==null)return{};var n={};for(var s in t)if({}.hasOwnProperty.call(t,s)){if(e.indexOf(s)!==-1)continue;n[s]=t[s]}return n}function WS(t){return!!(t.metaKey||t.altKey||t.ctrlKey||t.shiftKey)}function HS(t,e){return t.button===0&&(!e||e==="_self")&&!WS(t)}function Rd(t){return t===void 0&&(t=""),new URLSearchParams(typeof t=="string"||Array.isArray(t)||t instanceof URLSearchParams?t:Object.keys(t).reduce((e,n)=>{let s=t[n];return e.concat(Array.isArray(s)?s.map(r=>[n,r]):[[n,s]])},[]))}function VS(t,e){let n=Rd(t);return e&&e.forEach((s,r)=>{n.has(r)||e.getAll(r).forEach(i=>{n.append(r,i)})}),n}const QS=["onClick","relative","reloadDocument","replace","state","target","to","preventScrollReset","viewTransition"],KS=["aria-current","caseSensitive","className","end","style","to","viewTransition","children"],YS="6";try{window.__reactRouterVersion=YS}catch{}const XS=w.createContext({isTransitioning:!1}),qS="startTransition",Qp=ib[qS];function GS(t){let{basename:e,children:n,future:s,window:r}=t,i=w.useRef();i.current==null&&(i.current=tS({window:r,v5Compat:!0}));let o=i.current,[a,l]=w.useState({action:o.action,location:o.location}),{v7_startTransition:c}=s||{},d=w.useCallback(f=>{c&&Qp?Qp(()=>l(f)):l(f)},[l,c]);return w.useLayoutEffect(()=>o.listen(d),[o,d]),w.useEffect(()=>BS(s),[s]),w.createElement($S,{basename:e,children:n,location:a.location,navigationType:a.action,navigator:o,future:s})}const JS=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u",ZS=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,e_=w.forwardRef(function(e,n){let{onClick:s,relative:r,reloadDocument:i,replace:o,state:a,target:l,to:c,preventScrollReset:d,viewTransition:f}=e,h=py(e,QS),{basename:p}=w.useContext(Os),x,g=!1;if(typeof c=="string"&&ZS.test(c)&&(x=c,JS))try{let v=new URL(window.location.href),S=c.startsWith("//")?new URL(v.protocol+c):new URL(c),_=pi(S.pathname,p);S.origin===v.origin&&_!=null?c=_+S.search+S.hash:g=!0}catch{}let b=CS(c,{relative:r}),y=n_(c,{replace:o,state:a,target:l,preventScrollReset:d,relative:r,viewTransition:f});function m(v){s&&s(v),v.defaultPrevented||y(v)}return w.createElement("a",Al({},h,{href:x||b,onClick:g||i?s:m,ref:n,target:l}))}),gy=w.forwardRef(function(e,n){let{"aria-current":s="page",caseSensitive:r=!1,className:i="",end:o=!1,style:a,to:l,viewTransition:c,children:d}=e,f=py(e,KS),h=vc(l,{relative:f.relative}),p=Ts(),x=w.useContext(ly),{navigator:g,basename:b}=w.useContext(Os),y=x!=null&&r_(h)&&c===!0,m=g.encodeLocation?g.encodeLocation(h).pathname:h.pathname,v=p.pathname,S=x&&x.navigation&&x.navigation.location?x.navigation.location.pathname:null;r||(v=v.toLowerCase(),S=S?S.toLowerCase():null,m=m.toLowerCase()),S&&b&&(S=pi(S,b)||S);const _=m!=="/"&&m.endsWith("/")?m.length-1:m.length;let k=v===m||!o&&v.startsWith(m)&&v.charAt(_)==="/",C=S!=null&&(S===m||!o&&S.startsWith(m)&&S.charAt(m.length)==="/"),j={isActive:k,isPending:C,isTransitioning:y},P=k?s:void 0,E;typeof i=="function"?E=i(j):E=[i,k?"active":null,C?"pending":null,y?"transitioning":null].filter(Boolean).join(" ");let R=typeof a=="function"?a(j):a;return w.createElement(e_,Al({},f,{"aria-current":P,className:E,ref:n,style:R,to:l,viewTransition:c}),typeof d=="function"?d(j):d)});var Od;(function(t){t.UseScrollRestoration="useScrollRestoration",t.UseSubmit="useSubmit",t.UseSubmitFetcher="useSubmitFetcher",t.UseFetcher="useFetcher",t.useViewTransitionState="useViewTransitionState"})(Od||(Od={}));var Kp;(function(t){t.UseFetcher="useFetcher",t.UseFetchers="useFetchers",t.UseScrollRestoration="useScrollRestoration"})(Kp||(Kp={}));function t_(t){let e=w.useContext(xc);return e||je(!1),e}function n_(t,e){let{target:n,replace:s,state:r,preventScrollReset:i,relative:o,viewTransition:a}=e===void 0?{}:e,l=bi(),c=Ts(),d=vc(t,{relative:o});return w.useCallback(f=>{if(HS(f,n)){f.preventDefault();let h=s!==void 0?s:Ll(c)===Ll(d);l(t,{replace:h,state:r,preventScrollReset:i,relative:o,viewTransition:a})}},[c,l,d,s,r,n,t,i,o,a])}function s_(t){let e=w.useRef(Rd(t)),n=w.useRef(!1),s=Ts(),r=w.useMemo(()=>VS(s.search,n.current?null:e.current),[s.search]),i=bi(),o=w.useCallback((a,l)=>{const c=Rd(typeof a=="function"?a(r):a);n.current=!0,i("?"+c,l)},[i,r]);return[r,o]}function r_(t,e){e===void 0&&(e={});let n=w.useContext(XS);n==null&&je(!1);let{basename:s}=t_(Od.useViewTransitionState),r=vc(t,{relative:e.relative});if(!n.isTransitioning)return!1;let i=pi(n.currentLocation.pathname,s)||n.currentLocation.pathname,o=pi(n.nextLocation.pathname,s)||n.nextLocation.pathname;return Md(r.pathname,o)!=null||Md(r.pathname,i)!=null}const i_="/api";class o_ extends Error{constructor(n,s){super(s);$(this,"status");this.status=n,this.name="ApiError"}}async function Y(t,e={}){const n=await fetch(i_+t,{...e,headers:e.body?{"Content-Type":"application/json",...e.headers||{}}:e.headers});if(!n.ok){let s=`Erro HTTP ${n.status}`;try{const r=await n.json();s=String(r.detail||r.mensagem||s)}catch{}throw new o_(n.status,s)}if(n.status!==204)return n.json()}const Me=t=>({method:"POST",body:JSON.stringify(t)}),W={health:()=>Y("/health"),doctor:()=>Y("/doctor"),shutdown:()=>Y("/shutdown",Me({})),metricas:()=>Y("/metrics"),leads:{listar:()=>Y("/leads"),criar:t=>Y("/leads",Me(t)),atualizar:(t,e)=>Y(`/leads/${t}`,{method:"PATCH",body:JSON.stringify(e)}),remover:t=>Y(`/leads/${t}`,{method:"DELETE"}),importar:t=>Y("/leads/import",Me({leads:t})),enriquecer:t=>Y(`/leads/${t}/enrich`,Me({}))},crm:{leadsListar:()=>Y("/crm/leads"),criarLead:t=>Y("/crm/leads",Me(t)),moverLead:(t,e)=>Y(`/crm/leads/${t}/etapa`,{method:"PUT",body:JSON.stringify({etapa:e})}),sugestoes:()=>Y("/crm/sugestoes"),aplicar:(t,e)=>Y("/crm/sugestoes/aplicar",Me({lead_id:t,para:e}))},campanhas:{listar:()=>Y("/campaigns"),criar:t=>Y("/campaigns",Me(t)),iniciar:t=>Y(`/campaigns/${t}/start`,Me({})),pausar:t=>Y(`/campaigns/${t}/pause`,Me({})),parar:t=>Y(`/campaigns/${t}/stop`,Me({})),remover:t=>Y(`/campaigns/${t}`,{method:"DELETE"})},extrair:{maps:t=>Y("/extractors/maps",Me(t)),instagram:(t,e)=>Y("/extractors/instagram",Me({profile_url:t,limit:e})),cnpj:t=>Y("/extract/cnpj",Me({cnpj:t})),cnpjBusca:t=>Y("/extract/cnpj/search",Me(t)),cnpjExtract:t=>Y("/extractors/cnpj",Me(t))},config:{obter:()=>Y("/config"),salvar:t=>Y("/config",{method:"PUT",body:JSON.stringify(t)}),testarLlm:()=>Y("/config/test/llm",Me({})),testarIntegracao:t=>Y("/config/test/integracao",Me({url:t}))},integracoes:()=>Y("/integrations/status"),agentes:{templates:()=>Y("/agents/templates"),salvarTemplates:t=>Y("/agents/templates",{method:"PUT",body:JSON.stringify(t)}),testarTemplate:(t,e)=>Y("/agents/templates/test",Me({template:t,lead_id:e})),chat:t=>Y("/agents/attendance/chat",Me({mensagens:t}))},webhook:{obter:()=>Y("/webhook"),salvar:t=>Y("/webhook",{method:"PUT",body:JSON.stringify(t)}),testar:t=>Y("/webhook/test",Me({url:t}))}};function Q(...t){return t.filter(Boolean).join(" ")}function a_(t){return(t||"?").trim().charAt(0).toUpperCase()||"?"}function Il(t){if(!t)return"—";try{return new Date(t).toLocaleString("pt-BR",{day:"2-digit",month:"2-digit",year:"2-digit",hour:"2-digit",minute:"2-digit"})}catch{return t}}function na(t){const e=(t||"").replace(/\D/g,"");return e?e.length===11?`(${e.slice(0,2)}) ${e.slice(2,7)}-${e.slice(7)}`:e.length===10?`(${e.slice(0,2)}) ${e.slice(2,6)}-${e.slice(6)}`:t||"—":"—"}function l_(t){const e=["nome","telefone","email","empresa","cidade","observacoes"],n=e.filter(s=>String(t[s]||"").trim().length>0);return Math.round(n.length/e.length*100)}function c_(t){const e={nome:"nome",telefone:"telefone",email:"e-mail",empresa:"empresa",cidade:"cidade",observacoes:"observações"};return["nome","telefone","email","empresa","cidade","observacoes"].filter(s=>String(t[s]||"").trim().length===0).map(s=>e[s])}function u_(t,e){if(t>=100)return"Lead 100% completo — excelente! 🎉";const n=e.slice(0,2).join(" e ");return`Faltam ${100-t}% (${n}${e.length>2?"...":""}) para o lead ficar completo`}function d_(t,e){const n=new Blob([JSON.stringify(t,null,2)],{type:"application/json"}),s=URL.createObjectURL(n),r=document.createElement("a");r.href=s,r.download=e,r.click(),URL.revokeObjectURL(s)}const f_={md:"max-w-lg",lg:"max-w-2xl",xl:"max-w-4xl"};function Xt({aberto:t,onFechar:e,titulo:n,children:s,icone:r="fas fa-circle-info",largura:i="md"}){return w.useEffect(()=>{const o=a=>{a.key==="Escape"&&e()};return t&&window.addEventListener("keydown",o),()=>window.removeEventListener("keydown",o)},[t,e]),t?u.jsx("div",{className:"fixed inset-0 z-50 bg-black/40 flex items-center justify-center p-4",onMouseDown:o=>{o.target===o.currentTarget&&e()},children:u.jsxs("div",{className:Q("bg-white rounded-2xl shadow-xl w-full",f_[i]),children:[u.jsxs("div",{className:"flex items-center justify-between px-6 py-4 border-b border-gray-100",children:[u.jsxs("h3",{className:"text-base font-semibold text-gray-800 flex items-center gap-2",children:[u.jsx("i",{className:Q(r,"text-indigo-500")})," ",n]}),u.jsx("button",{onClick:e,className:"text-gray-400 hover:text-gray-600","aria-label":"Fechar",children:u.jsx("i",{className:"fas fa-times"})})]}),u.jsx("div",{className:"px-6 py-5 max-h-[70vh] overflow-y-auto",children:s})]})}):null}const h_=[{icone:"fas fa-users",texto:"Cadastre leads manualmente ou importe pelos Extratores (Maps, Instagram, CNPJ)."},{icone:"fas fa-columns",texto:"Arraste os cards no CRM conforme avança no funil (Leads → Em contato → Proposta → Fechado)."},{icone:"fas fa-bullhorn",texto:"Monte campanhas de disparo com seus templates e acompanhe as taxas no Painel."},{icone:"fas fa-robot",texto:"Use os Agentes (disparo e atendimento) e configure o LLM em Ferramentas para turbiná-los."}];function p_({titulo:t,nomeUsuario:e}){const[n,s]=w.useState(!1);return u.jsxs("header",{className:"bg-white shadow-sm px-8 py-4 flex items-center justify-between shrink-0",children:[u.jsx("div",{children:u.jsx("h2",{className:"text-lg font-semibold text-gray-800",children:t})}),u.jsxs("div",{className:"flex items-center space-x-4",children:[u.jsxs("button",{onClick:()=>s(!0),className:"text-sm text-indigo-600 hover:text-indigo-800 font-medium",children:[u.jsx("i",{className:"fas fa-play-circle mr-1"})," Ver tutorial"]}),u.jsx("div",{className:"w-8 h-8 rounded-full bg-indigo-100 flex items-center justify-center text-indigo-600 font-bold text-sm",children:a_(e)}),u.jsx("span",{className:"text-sm font-medium text-gray-700",children:e})]}),u.jsxs(Xt,{aberto:n,onFechar:()=>s(!1),titulo:"Tutorial rápido",icone:"fas fa-graduation-cap",children:[u.jsx("ol",{className:"space-y-4",children:h_.map((r,i)=>u.jsxs("li",{className:"flex gap-3 items-start",children:[u.jsx("span",{className:"w-8 h-8 rounded-full bg-indigo-100 text-indigo-600 flex items-center justify-center shrink-0",children:u.jsx("i",{className:r.icone})}),u.jsx("p",{className:"text-sm text-gray-600 pt-1.5",children:r.texto})]},i))}),u.jsx("p",{className:"text-xs text-gray-400 mt-5",children:"Dica: o LLM (para enriquecimento e agentes) é configurado em Ferramentas."})]})]})}const g_=[{itens:[{para:"/",icone:"fas fa-chart-pie text-indigo-300",rotulo:"Painel",fim:!0}]},{secao:"Conexões",itens:[{para:"/leads",icone:"fas fa-users",rotulo:"Leads"},{para:"/agentes",icone:"fas fa-robot",rotulo:"Agentes"},{para:"/ferramentas",icone:"fas fa-tools",rotulo:"Ferramentas"}]},{itens:[{para:"/extrator-maps",icone:"fas fa-map-marked-alt",rotulo:"Extrator Maps"},{para:"/extrator-instagram",icone:"fab fa-instagram",rotulo:"Extrator Instagram"},{para:"/extrator-cnpj",icone:"fas fa-building",rotulo:"Extrator CNPJ"}]},{secao:"Gestão",itens:[{para:"/disparos",icone:"fas fa-bullhorn",rotulo:"Disparos"},{para:"/crm",icone:"fas fa-columns",rotulo:"CRM"}]},{secao:"Integrações",itens:[{para:"/webhook",icone:"fas fa-link",rotulo:"Webhook"},{para:"/suporte",icone:"fas fa-headset",rotulo:"Suporte"}]}];function m_({onSair:t}){const e=Ts(),[n,s]=w.useState(!1);w.useEffect(()=>{e.pathname==="/agentes"&&s(!0)},[e.pathname]);const r=i=>{if(i.rotulo==="Agentes")return e.pathname==="/agentes";const[o,a]=i.para.split("?");return a?e.pathname===o&&e.search.includes(a):i.fim?e.pathname===o:e.pathname.startsWith(o)};return u.jsxs("aside",{className:"w-64 bg-[#1e1b4b] text-white flex flex-col shrink-0 h-screen sticky top-0",children:[u.jsx("div",{className:"p-6 border-b border-gray-700",children:u.jsxs("h1",{className:"text-2xl font-bold tracking-tight flex items-center gap-2",children:[u.jsx("span",{className:"text-2xl",children:"⚡"}),"LeadMaster",u.jsxs("span",{className:"text-[10px] font-medium text-indigo-300 border border-indigo-400/40 rounded-md px-1.5 py-0.5",children:["v","0.1.14"]})]})}),u.jsx("nav",{className:"flex-1 overflow-y-auto py-4",children:u.jsx("ul",{className:"space-y-1",children:g_.map((i,o)=>u.jsx("li",{children:u.jsxs("ul",{children:[i.secao&&u.jsx("li",{className:"px-6 py-2 text-xs font-semibold text-gray-400 uppercase tracking-wider list-none",children:i.secao}),i.itens.map(a=>a.rotulo==="Agentes"?u.jsxs("li",{children:[u.jsxs("button",{type:"button",onClick:()=>s(l=>!l),className:Q("sidebar-link w-full flex items-center px-6 py-2.5 text-sm",r(a)&&"active"),children:[u.jsx("i",{className:Q(a.icone,"w-5 text-gray-400")}),u.jsx("span",{className:"ml-3",children:a.rotulo}),u.jsx("i",{className:Q("fas fa-chevron-down ml-auto text-xs text-gray-400 transition-transform",n&&"rotate-180")})]}),n&&u.jsxs("ul",{className:"pl-12 space-y-1 mt-1",children:[u.jsx("li",{children:u.jsx(Yp,{para:"/agentes?tab=disparo",rotulo:"Agente de Disparo",ativo:e.pathname==="/agentes"&&e.search.includes("disparo")})}),u.jsx("li",{children:u.jsx(Yp,{para:"/agentes?tab=atendimento",rotulo:"Agente de Atendimento",ativo:e.pathname==="/agentes"&&e.search.includes("atendimento")})})]})]},"agentes"):u.jsx("li",{children:u.jsxs(gy,{to:a.para,className:Q("sidebar-link flex items-center px-6 py-2.5 text-sm",r(a)&&"active"),children:[u.jsx("i",{className:Q(a.icone,"w-5 text-gray-400")}),u.jsx("span",{className:"ml-3",children:a.rotulo})]})},a.para))]})},o))})}),u.jsx("div",{className:"p-4 border-t border-gray-700",children:u.jsxs("button",{onClick:t,className:"w-full flex items-center justify-center px-4 py-2.5 bg-gradient-to-r from-indigo-500 to-purple-500 rounded-lg text-sm font-medium hover:opacity-90 transition",children:[u.jsx("i",{className:"fas fa-sign-out-alt mr-2"})," Sair"]})})]})}function Yp({para:t,rotulo:e,ativo:n}){return u.jsx(gy,{to:t,className:Q("sidebar-link block px-4 py-2 text-xs text-gray-300 rounded",n&&"active"),children:e})}const my=w.createContext({toast:()=>{}});function en(){return w.useContext(my)}const x_={sucesso:"bg-green-600",erro:"bg-red-600",info:"bg-indigo-600"},y_={sucesso:"fas fa-check-circle",erro:"fas fa-exclamation-circle",info:"fas fa-info-circle"};function v_({children:t}){const[e,n]=w.useState([]),s=w.useRef(0),r=w.useCallback((i,o)=>{const a=++s.current;n(l=>[...l,{id:a,tipo:i,mensagem:o}]),setTimeout(()=>n(l=>l.filter(c=>c.id!==a)),4500)},[]);return u.jsxs(my.Provider,{value:{toast:r},children:[t,u.jsx("div",{className:"fixed top-4 right-4 z-[100] space-y-2 w-80",children:e.map(i=>u.jsxs("div",{className:Q(x_[i.tipo],"text-white text-sm rounded-lg shadow-lg px-4 py-3 flex items-start gap-2 animate-pulse-once"),role:"status",children:[u.jsx("i",{className:Q(y_[i.tipo],"mt-0.5")}),u.jsx("span",{className:"flex-1",children:i.mensagem}),u.jsx("button",{className:"opacity-70 hover:opacity-100",onClick:()=>n(o=>o.filter(a=>a.id!==i.id)),"aria-label":"Fechar",children:u.jsx("i",{className:"fas fa-times"})})]},i.id))})]})}function Pa({rotulo:t,valor:e,subtitulo:n,children:s,icone:r}){return u.jsxs("div",{className:"bg-white rounded-xl shadow-sm p-5 border border-gray-100",children:[u.jsxs("div",{className:"flex items-center justify-between",children:[u.jsx("p",{className:"text-sm text-gray-500 font-medium",children:t}),r&&u.jsx("i",{className:`${r} text-indigo-300`})]}),s??u.jsxs(u.Fragment,{children:[u.jsx("p",{className:"text-2xl font-bold text-gray-800 mt-1",children:e}),n&&u.jsx("p",{className:"text-xs text-gray-400 mt-0.5",children:n})]})]})}/*!
|
|
67
|
+
*/function Al(){return Al=Object.assign?Object.assign.bind():function(t){for(var e=1;e<arguments.length;e++){var n=arguments[e];for(var s in n)({}).hasOwnProperty.call(n,s)&&(t[s]=n[s])}return t},Al.apply(null,arguments)}function py(t,e){if(t==null)return{};var n={};for(var s in t)if({}.hasOwnProperty.call(t,s)){if(e.indexOf(s)!==-1)continue;n[s]=t[s]}return n}function WS(t){return!!(t.metaKey||t.altKey||t.ctrlKey||t.shiftKey)}function HS(t,e){return t.button===0&&(!e||e==="_self")&&!WS(t)}function Rd(t){return t===void 0&&(t=""),new URLSearchParams(typeof t=="string"||Array.isArray(t)||t instanceof URLSearchParams?t:Object.keys(t).reduce((e,n)=>{let s=t[n];return e.concat(Array.isArray(s)?s.map(r=>[n,r]):[[n,s]])},[]))}function VS(t,e){let n=Rd(t);return e&&e.forEach((s,r)=>{n.has(r)||e.getAll(r).forEach(i=>{n.append(r,i)})}),n}const QS=["onClick","relative","reloadDocument","replace","state","target","to","preventScrollReset","viewTransition"],KS=["aria-current","caseSensitive","className","end","style","to","viewTransition","children"],YS="6";try{window.__reactRouterVersion=YS}catch{}const XS=w.createContext({isTransitioning:!1}),qS="startTransition",Qp=ib[qS];function GS(t){let{basename:e,children:n,future:s,window:r}=t,i=w.useRef();i.current==null&&(i.current=tS({window:r,v5Compat:!0}));let o=i.current,[a,l]=w.useState({action:o.action,location:o.location}),{v7_startTransition:c}=s||{},d=w.useCallback(f=>{c&&Qp?Qp(()=>l(f)):l(f)},[l,c]);return w.useLayoutEffect(()=>o.listen(d),[o,d]),w.useEffect(()=>BS(s),[s]),w.createElement($S,{basename:e,children:n,location:a.location,navigationType:a.action,navigator:o,future:s})}const JS=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u",ZS=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,e_=w.forwardRef(function(e,n){let{onClick:s,relative:r,reloadDocument:i,replace:o,state:a,target:l,to:c,preventScrollReset:d,viewTransition:f}=e,h=py(e,QS),{basename:p}=w.useContext(Os),x,g=!1;if(typeof c=="string"&&ZS.test(c)&&(x=c,JS))try{let v=new URL(window.location.href),S=c.startsWith("//")?new URL(v.protocol+c):new URL(c),_=pi(S.pathname,p);S.origin===v.origin&&_!=null?c=_+S.search+S.hash:g=!0}catch{}let b=CS(c,{relative:r}),y=n_(c,{replace:o,state:a,target:l,preventScrollReset:d,relative:r,viewTransition:f});function m(v){s&&s(v),v.defaultPrevented||y(v)}return w.createElement("a",Al({},h,{href:x||b,onClick:g||i?s:m,ref:n,target:l}))}),gy=w.forwardRef(function(e,n){let{"aria-current":s="page",caseSensitive:r=!1,className:i="",end:o=!1,style:a,to:l,viewTransition:c,children:d}=e,f=py(e,KS),h=vc(l,{relative:f.relative}),p=Ts(),x=w.useContext(ly),{navigator:g,basename:b}=w.useContext(Os),y=x!=null&&r_(h)&&c===!0,m=g.encodeLocation?g.encodeLocation(h).pathname:h.pathname,v=p.pathname,S=x&&x.navigation&&x.navigation.location?x.navigation.location.pathname:null;r||(v=v.toLowerCase(),S=S?S.toLowerCase():null,m=m.toLowerCase()),S&&b&&(S=pi(S,b)||S);const _=m!=="/"&&m.endsWith("/")?m.length-1:m.length;let k=v===m||!o&&v.startsWith(m)&&v.charAt(_)==="/",C=S!=null&&(S===m||!o&&S.startsWith(m)&&S.charAt(m.length)==="/"),j={isActive:k,isPending:C,isTransitioning:y},P=k?s:void 0,E;typeof i=="function"?E=i(j):E=[i,k?"active":null,C?"pending":null,y?"transitioning":null].filter(Boolean).join(" ");let R=typeof a=="function"?a(j):a;return w.createElement(e_,Al({},f,{"aria-current":P,className:E,ref:n,style:R,to:l,viewTransition:c}),typeof d=="function"?d(j):d)});var Od;(function(t){t.UseScrollRestoration="useScrollRestoration",t.UseSubmit="useSubmit",t.UseSubmitFetcher="useSubmitFetcher",t.UseFetcher="useFetcher",t.useViewTransitionState="useViewTransitionState"})(Od||(Od={}));var Kp;(function(t){t.UseFetcher="useFetcher",t.UseFetchers="useFetchers",t.UseScrollRestoration="useScrollRestoration"})(Kp||(Kp={}));function t_(t){let e=w.useContext(xc);return e||je(!1),e}function n_(t,e){let{target:n,replace:s,state:r,preventScrollReset:i,relative:o,viewTransition:a}=e===void 0?{}:e,l=bi(),c=Ts(),d=vc(t,{relative:o});return w.useCallback(f=>{if(HS(f,n)){f.preventDefault();let h=s!==void 0?s:Ll(c)===Ll(d);l(t,{replace:h,state:r,preventScrollReset:i,relative:o,viewTransition:a})}},[c,l,d,s,r,n,t,i,o,a])}function s_(t){let e=w.useRef(Rd(t)),n=w.useRef(!1),s=Ts(),r=w.useMemo(()=>VS(s.search,n.current?null:e.current),[s.search]),i=bi(),o=w.useCallback((a,l)=>{const c=Rd(typeof a=="function"?a(r):a);n.current=!0,i("?"+c,l)},[i,r]);return[r,o]}function r_(t,e){e===void 0&&(e={});let n=w.useContext(XS);n==null&&je(!1);let{basename:s}=t_(Od.useViewTransitionState),r=vc(t,{relative:e.relative});if(!n.isTransitioning)return!1;let i=pi(n.currentLocation.pathname,s)||n.currentLocation.pathname,o=pi(n.nextLocation.pathname,s)||n.nextLocation.pathname;return Md(r.pathname,o)!=null||Md(r.pathname,i)!=null}const i_="/api";class o_ extends Error{constructor(n,s){super(s);$(this,"status");this.status=n,this.name="ApiError"}}async function Y(t,e={}){const n=await fetch(i_+t,{...e,headers:e.body?{"Content-Type":"application/json",...e.headers||{}}:e.headers});if(!n.ok){let s=`Erro HTTP ${n.status}`;try{const r=await n.json();s=String(r.detail||r.mensagem||s)}catch{}throw new o_(n.status,s)}if(n.status!==204)return n.json()}const Me=t=>({method:"POST",body:JSON.stringify(t)}),W={health:()=>Y("/health"),doctor:()=>Y("/doctor"),shutdown:()=>Y("/shutdown",Me({})),metricas:()=>Y("/metrics"),leads:{listar:()=>Y("/leads"),criar:t=>Y("/leads",Me(t)),atualizar:(t,e)=>Y(`/leads/${t}`,{method:"PATCH",body:JSON.stringify(e)}),remover:t=>Y(`/leads/${t}`,{method:"DELETE"}),importar:t=>Y("/leads/import",Me({leads:t})),enriquecer:t=>Y(`/leads/${t}/enrich`,Me({}))},crm:{leadsListar:()=>Y("/crm/leads"),criarLead:t=>Y("/crm/leads",Me(t)),moverLead:(t,e)=>Y(`/crm/leads/${t}/etapa`,{method:"PUT",body:JSON.stringify({etapa:e})}),sugestoes:()=>Y("/crm/sugestoes"),aplicar:(t,e)=>Y("/crm/sugestoes/aplicar",Me({lead_id:t,para:e}))},campanhas:{listar:()=>Y("/campaigns"),criar:t=>Y("/campaigns",Me(t)),iniciar:t=>Y(`/campaigns/${t}/start`,Me({})),pausar:t=>Y(`/campaigns/${t}/pause`,Me({})),parar:t=>Y(`/campaigns/${t}/stop`,Me({})),remover:t=>Y(`/campaigns/${t}`,{method:"DELETE"})},extrair:{maps:t=>Y("/extractors/maps",Me(t)),instagram:(t,e)=>Y("/extractors/instagram",Me({profile_url:t,limit:e})),cnpj:t=>Y("/extract/cnpj",Me({cnpj:t})),cnpjBusca:t=>Y("/extract/cnpj/search",Me(t)),cnpjExtract:t=>Y("/extractors/cnpj",Me(t))},config:{obter:()=>Y("/config"),salvar:t=>Y("/config",{method:"PUT",body:JSON.stringify(t)}),testarLlm:()=>Y("/config/test/llm",Me({})),testarIntegracao:t=>Y("/config/test/integracao",Me({url:t}))},integracoes:()=>Y("/integrations/status"),agentes:{templates:()=>Y("/agents/templates"),salvarTemplates:t=>Y("/agents/templates",{method:"PUT",body:JSON.stringify(t)}),testarTemplate:(t,e)=>Y("/agents/templates/test",Me({template:t,lead_id:e})),chat:t=>Y("/agents/attendance/chat",Me({mensagens:t}))},webhook:{obter:()=>Y("/webhook"),salvar:t=>Y("/webhook",{method:"PUT",body:JSON.stringify(t)}),testar:t=>Y("/webhook/test",Me({url:t}))}};function Q(...t){return t.filter(Boolean).join(" ")}function a_(t){return(t||"?").trim().charAt(0).toUpperCase()||"?"}function Il(t){if(!t)return"—";try{return new Date(t).toLocaleString("pt-BR",{day:"2-digit",month:"2-digit",year:"2-digit",hour:"2-digit",minute:"2-digit"})}catch{return t}}function na(t){const e=(t||"").replace(/\D/g,"");return e?e.length===11?`(${e.slice(0,2)}) ${e.slice(2,7)}-${e.slice(7)}`:e.length===10?`(${e.slice(0,2)}) ${e.slice(2,6)}-${e.slice(6)}`:t||"—":"—"}function l_(t){const e=["nome","telefone","email","empresa","cidade","observacoes"],n=e.filter(s=>String(t[s]||"").trim().length>0);return Math.round(n.length/e.length*100)}function c_(t){const e={nome:"nome",telefone:"telefone",email:"e-mail",empresa:"empresa",cidade:"cidade",observacoes:"observações"};return["nome","telefone","email","empresa","cidade","observacoes"].filter(s=>String(t[s]||"").trim().length===0).map(s=>e[s])}function u_(t,e){if(t>=100)return"Lead 100% completo — excelente! 🎉";const n=e.slice(0,2).join(" e ");return`Faltam ${100-t}% (${n}${e.length>2?"...":""}) para o lead ficar completo`}function d_(t,e){const n=new Blob([JSON.stringify(t,null,2)],{type:"application/json"}),s=URL.createObjectURL(n),r=document.createElement("a");r.href=s,r.download=e,r.click(),URL.revokeObjectURL(s)}const f_={md:"max-w-lg",lg:"max-w-2xl",xl:"max-w-4xl"};function Xt({aberto:t,onFechar:e,titulo:n,children:s,icone:r="fas fa-circle-info",largura:i="md"}){return w.useEffect(()=>{const o=a=>{a.key==="Escape"&&e()};return t&&window.addEventListener("keydown",o),()=>window.removeEventListener("keydown",o)},[t,e]),t?u.jsx("div",{className:"fixed inset-0 z-50 bg-black/40 flex items-center justify-center p-4",onMouseDown:o=>{o.target===o.currentTarget&&e()},children:u.jsxs("div",{className:Q("bg-white rounded-2xl shadow-xl w-full",f_[i]),children:[u.jsxs("div",{className:"flex items-center justify-between px-6 py-4 border-b border-gray-100",children:[u.jsxs("h3",{className:"text-base font-semibold text-gray-800 flex items-center gap-2",children:[u.jsx("i",{className:Q(r,"text-indigo-500")})," ",n]}),u.jsx("button",{onClick:e,className:"text-gray-400 hover:text-gray-600","aria-label":"Fechar",children:u.jsx("i",{className:"fas fa-times"})})]}),u.jsx("div",{className:"px-6 py-5 max-h-[70vh] overflow-y-auto",children:s})]})}):null}const h_=[{icone:"fas fa-users",texto:"Cadastre leads manualmente ou importe pelos Extratores (Maps, Instagram, CNPJ)."},{icone:"fas fa-columns",texto:"Arraste os cards no CRM conforme avança no funil (Leads → Em contato → Proposta → Fechado)."},{icone:"fas fa-bullhorn",texto:"Monte campanhas de disparo com seus templates e acompanhe as taxas no Painel."},{icone:"fas fa-robot",texto:"Use os Agentes (disparo e atendimento) e configure o LLM em Ferramentas para turbiná-los."}];function p_({titulo:t,nomeUsuario:e}){const[n,s]=w.useState(!1);return u.jsxs("header",{className:"bg-white shadow-sm px-8 py-4 flex items-center justify-between shrink-0",children:[u.jsx("div",{children:u.jsx("h2",{className:"text-lg font-semibold text-gray-800",children:t})}),u.jsxs("div",{className:"flex items-center space-x-4",children:[u.jsxs("button",{onClick:()=>s(!0),className:"text-sm text-indigo-600 hover:text-indigo-800 font-medium",children:[u.jsx("i",{className:"fas fa-play-circle mr-1"})," Ver tutorial"]}),u.jsx("div",{className:"w-8 h-8 rounded-full bg-indigo-100 flex items-center justify-center text-indigo-600 font-bold text-sm",children:a_(e)}),u.jsx("span",{className:"text-sm font-medium text-gray-700",children:e})]}),u.jsxs(Xt,{aberto:n,onFechar:()=>s(!1),titulo:"Tutorial rápido",icone:"fas fa-graduation-cap",children:[u.jsx("ol",{className:"space-y-4",children:h_.map((r,i)=>u.jsxs("li",{className:"flex gap-3 items-start",children:[u.jsx("span",{className:"w-8 h-8 rounded-full bg-indigo-100 text-indigo-600 flex items-center justify-center shrink-0",children:u.jsx("i",{className:r.icone})}),u.jsx("p",{className:"text-sm text-gray-600 pt-1.5",children:r.texto})]},i))}),u.jsx("p",{className:"text-xs text-gray-400 mt-5",children:"Dica: o LLM (para enriquecimento e agentes) é configurado em Ferramentas."})]})]})}const g_=[{itens:[{para:"/",icone:"fas fa-chart-pie text-indigo-300",rotulo:"Painel",fim:!0}]},{secao:"Conexões",itens:[{para:"/leads",icone:"fas fa-users",rotulo:"Leads"},{para:"/agentes",icone:"fas fa-robot",rotulo:"Agentes"},{para:"/ferramentas",icone:"fas fa-tools",rotulo:"Ferramentas"}]},{itens:[{para:"/extrator-maps",icone:"fas fa-map-marked-alt",rotulo:"Extrator Maps"},{para:"/extrator-instagram",icone:"fab fa-instagram",rotulo:"Extrator Instagram"},{para:"/extrator-cnpj",icone:"fas fa-building",rotulo:"Extrator CNPJ"}]},{secao:"Gestão",itens:[{para:"/disparos",icone:"fas fa-bullhorn",rotulo:"Disparos"},{para:"/crm",icone:"fas fa-columns",rotulo:"CRM"}]},{secao:"Integrações",itens:[{para:"/webhook",icone:"fas fa-link",rotulo:"Webhook"},{para:"/suporte",icone:"fas fa-headset",rotulo:"Suporte"}]}];function m_({onSair:t}){const e=Ts(),[n,s]=w.useState(!1);w.useEffect(()=>{e.pathname==="/agentes"&&s(!0)},[e.pathname]);const r=i=>{if(i.rotulo==="Agentes")return e.pathname==="/agentes";const[o,a]=i.para.split("?");return a?e.pathname===o&&e.search.includes(a):i.fim?e.pathname===o:e.pathname.startsWith(o)};return u.jsxs("aside",{className:"w-64 bg-[#1e1b4b] text-white flex flex-col shrink-0 h-screen sticky top-0",children:[u.jsx("div",{className:"p-6 border-b border-gray-700",children:u.jsxs("h1",{className:"text-2xl font-bold tracking-tight flex items-center gap-2",children:[u.jsx("span",{className:"text-2xl",children:"⚡"}),"LeadMaster",u.jsxs("span",{className:"text-[10px] font-medium text-indigo-300 border border-indigo-400/40 rounded-md px-1.5 py-0.5",children:["v","0.1.15"]})]})}),u.jsx("nav",{className:"flex-1 overflow-y-auto py-4",children:u.jsx("ul",{className:"space-y-1",children:g_.map((i,o)=>u.jsx("li",{children:u.jsxs("ul",{children:[i.secao&&u.jsx("li",{className:"px-6 py-2 text-xs font-semibold text-gray-400 uppercase tracking-wider list-none",children:i.secao}),i.itens.map(a=>a.rotulo==="Agentes"?u.jsxs("li",{children:[u.jsxs("button",{type:"button",onClick:()=>s(l=>!l),className:Q("sidebar-link w-full flex items-center px-6 py-2.5 text-sm",r(a)&&"active"),children:[u.jsx("i",{className:Q(a.icone,"w-5 text-gray-400")}),u.jsx("span",{className:"ml-3",children:a.rotulo}),u.jsx("i",{className:Q("fas fa-chevron-down ml-auto text-xs text-gray-400 transition-transform",n&&"rotate-180")})]}),n&&u.jsxs("ul",{className:"pl-12 space-y-1 mt-1",children:[u.jsx("li",{children:u.jsx(Yp,{para:"/agentes?tab=disparo",rotulo:"Agente de Disparo",ativo:e.pathname==="/agentes"&&e.search.includes("disparo")})}),u.jsx("li",{children:u.jsx(Yp,{para:"/agentes?tab=atendimento",rotulo:"Agente de Atendimento",ativo:e.pathname==="/agentes"&&e.search.includes("atendimento")})})]})]},"agentes"):u.jsx("li",{children:u.jsxs(gy,{to:a.para,className:Q("sidebar-link flex items-center px-6 py-2.5 text-sm",r(a)&&"active"),children:[u.jsx("i",{className:Q(a.icone,"w-5 text-gray-400")}),u.jsx("span",{className:"ml-3",children:a.rotulo})]})},a.para))]})},o))})}),u.jsx("div",{className:"p-4 border-t border-gray-700",children:u.jsxs("button",{onClick:t,className:"w-full flex items-center justify-center px-4 py-2.5 bg-gradient-to-r from-indigo-500 to-purple-500 rounded-lg text-sm font-medium hover:opacity-90 transition",children:[u.jsx("i",{className:"fas fa-sign-out-alt mr-2"})," Sair"]})})]})}function Yp({para:t,rotulo:e,ativo:n}){return u.jsx(gy,{to:t,className:Q("sidebar-link block px-4 py-2 text-xs text-gray-300 rounded",n&&"active"),children:e})}const my=w.createContext({toast:()=>{}});function en(){return w.useContext(my)}const x_={sucesso:"bg-green-600",erro:"bg-red-600",info:"bg-indigo-600"},y_={sucesso:"fas fa-check-circle",erro:"fas fa-exclamation-circle",info:"fas fa-info-circle"};function v_({children:t}){const[e,n]=w.useState([]),s=w.useRef(0),r=w.useCallback((i,o)=>{const a=++s.current;n(l=>[...l,{id:a,tipo:i,mensagem:o}]),setTimeout(()=>n(l=>l.filter(c=>c.id!==a)),4500)},[]);return u.jsxs(my.Provider,{value:{toast:r},children:[t,u.jsx("div",{className:"fixed top-4 right-4 z-[100] space-y-2 w-80",children:e.map(i=>u.jsxs("div",{className:Q(x_[i.tipo],"text-white text-sm rounded-lg shadow-lg px-4 py-3 flex items-start gap-2 animate-pulse-once"),role:"status",children:[u.jsx("i",{className:Q(y_[i.tipo],"mt-0.5")}),u.jsx("span",{className:"flex-1",children:i.mensagem}),u.jsx("button",{className:"opacity-70 hover:opacity-100",onClick:()=>n(o=>o.filter(a=>a.id!==i.id)),"aria-label":"Fechar",children:u.jsx("i",{className:"fas fa-times"})})]},i.id))})]})}function Pa({rotulo:t,valor:e,subtitulo:n,children:s,icone:r}){return u.jsxs("div",{className:"bg-white rounded-xl shadow-sm p-5 border border-gray-100",children:[u.jsxs("div",{className:"flex items-center justify-between",children:[u.jsx("p",{className:"text-sm text-gray-500 font-medium",children:t}),r&&u.jsx("i",{className:`${r} text-indigo-300`})]}),s??u.jsxs(u.Fragment,{children:[u.jsx("p",{className:"text-2xl font-bold text-gray-800 mt-1",children:e}),n&&u.jsx("p",{className:"text-xs text-gray-400 mt-0.5",children:n})]})]})}/*!
|
|
68
68
|
* @kurkle/color v0.3.4
|
|
69
69
|
* https://github.com/kurkle/color#readme
|
|
70
70
|
* (c) 2024 Jukka Kurkela
|
package/frontend/dist/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
7
7
|
<title>LeadMaster — Painel</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-CAvEEMWg.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/assets/index-Bf28IekR.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body class="bg-gray-100 min-h-screen">
|
package/package.json
CHANGED
package/scripts/cli.mjs
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* LEADMASTER_PORT Porta inicial da UI/API (mesmo efeito de --port).
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
import { spawn, spawnSync } from 'node:child_process';
|
|
25
|
+
import { spawn, spawnSync, execSync } from 'node:child_process';
|
|
26
26
|
import { existsSync, mkdirSync, readFileSync, appendFileSync } from 'node:fs';
|
|
27
27
|
import { homedir, platform } from 'node:os';
|
|
28
28
|
import net from 'node:net';
|
|
@@ -267,23 +267,20 @@ function portaLivre(port) {
|
|
|
267
267
|
|
|
268
268
|
async function resolverPortaUI(portaInicial) {
|
|
269
269
|
/**
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
270
|
+
* Devolve a primeira porta livre a partir de portaInicial (padrão 5055).
|
|
271
|
+
* Instâncias anteriores do LeadMaster já foram encerradas pelo kill switch
|
|
272
|
+
* (`encerrarInstanciaAnterior`) antes desta chamada; se uma porta seguir
|
|
273
|
+
* ocupada, é de outro projeto — o launcher avança (5056, 5057, ...).
|
|
274
|
+
* Se nenhuma porta livre no intervalo, devolve {porta: null}.
|
|
274
275
|
*/
|
|
275
276
|
for (let i = 0; i < UI_PORT_MAX_TENTATIVAS; i++) {
|
|
276
277
|
const porta = portaInicial + i;
|
|
277
|
-
const saude = await httpGetJson(`http://127.0.0.1:${porta}/api/health`, 800);
|
|
278
|
-
if (saude.ok && saude.body && (saude.body.versao || saude.body.storage)) {
|
|
279
|
-
return { jaRodando: true, porta };
|
|
280
|
-
}
|
|
281
278
|
if (await portaLivre(porta)) {
|
|
282
|
-
return {
|
|
279
|
+
return { porta };
|
|
283
280
|
}
|
|
284
281
|
console.log(`ℹ Porta ${porta} ocupada — tentando ${porta + 1}...`);
|
|
285
282
|
}
|
|
286
|
-
return {
|
|
283
|
+
return { porta: null };
|
|
287
284
|
}
|
|
288
285
|
|
|
289
286
|
async function procurarInstanciaAtiva(portaInicial, maxTentativas = 10) {
|
|
@@ -297,6 +294,114 @@ async function procurarInstanciaAtiva(portaInicial, maxTentativas = 10) {
|
|
|
297
294
|
return null;
|
|
298
295
|
}
|
|
299
296
|
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
// Kill switch (regra inviolável do AGENTS.md)
|
|
299
|
+
//
|
|
300
|
+
// O launcher NUNCA deve ignorar uma porta ocupada por outra instância do
|
|
301
|
+
// LeadMaster: avisar que já existe instância no ar é PROIBIDO — isso impede
|
|
302
|
+
// atualizações (a versão antiga continua rodando, travando arquivos e
|
|
303
|
+
// causando EBUSY no npx). Antes de subir a nova versão, encerramos qualquer
|
|
304
|
+
// instância anterior confirmada.
|
|
305
|
+
//
|
|
306
|
+
// Segurança (higiene de processos do AGENTS.md): só mata processos
|
|
307
|
+
// CONFIRMADOS como LeadMaster — /api/health respondendo com o payload próprio
|
|
308
|
+
// (versao/home/storage) e o PID dono da porta. Processos de outros projetos
|
|
309
|
+
// NUNCA são tocados: nesse caso o launcher apenas avança para a próxima porta
|
|
310
|
+
// livre, como sempre.
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
function saudeEhLeadmaster(body) {
|
|
314
|
+
return Boolean(body && (body.versao || body.storage));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function pidsOuvindoPortaWindows(porta) {
|
|
318
|
+
try {
|
|
319
|
+
const out = execSync('netstat -ano', { windowsHide: true, encoding: 'utf8', timeout: 8000 });
|
|
320
|
+
const pids = new Set();
|
|
321
|
+
for (const linha of out.split(/\r?\n/)) {
|
|
322
|
+
if (!linha.includes('LISTENING')) continue;
|
|
323
|
+
const colunas = linha.trim().split(/\s+/);
|
|
324
|
+
if (colunas.length < 5) continue;
|
|
325
|
+
if (!colunas[1].endsWith(`:${porta}`)) continue;
|
|
326
|
+
const pid = Number(colunas[colunas.length - 1]);
|
|
327
|
+
if (Number.isInteger(pid) && pid > 0) pids.add(pid);
|
|
328
|
+
}
|
|
329
|
+
return [...pids];
|
|
330
|
+
} catch {
|
|
331
|
+
return [];
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function pidsOuvindoPortaUnix(porta) {
|
|
336
|
+
try {
|
|
337
|
+
const out = execSync(`lsof -ti:${porta} -sTCP:LISTEN`, { encoding: 'utf8', timeout: 8000, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
338
|
+
const pids = out.split(/\s+/).map((s) => Number(s.trim())).filter((n) => Number.isInteger(n) && n > 0);
|
|
339
|
+
return [...new Set(pids)];
|
|
340
|
+
} catch {
|
|
341
|
+
// Sem lsof: fallback amplo — processos do LeadMaster na lista de processos.
|
|
342
|
+
try {
|
|
343
|
+
const out = execSync('ps -eo pid=,args=', { encoding: 'utf8', timeout: 8000 });
|
|
344
|
+
const pids = new Set();
|
|
345
|
+
for (const linha of out.split('\n')) {
|
|
346
|
+
const m = linha.trim().match(/^(\d+)\s+(.*)$/);
|
|
347
|
+
if (!m) continue;
|
|
348
|
+
const pid = Number(m[1]);
|
|
349
|
+
const args = m[2];
|
|
350
|
+
const ehLeadmaster = args.includes('leadmaster') && (args.includes('cli.mjs') || args.includes('uvicorn main:app'));
|
|
351
|
+
if (pid > 0 && pid !== process.pid && ehLeadmaster) pids.add(pid);
|
|
352
|
+
}
|
|
353
|
+
return [...pids];
|
|
354
|
+
} catch {
|
|
355
|
+
return [];
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function matarArvoreProcesso(pid) {
|
|
361
|
+
try {
|
|
362
|
+
if (platform() === 'win32') {
|
|
363
|
+
const r = spawnSync('taskkill', ['/pid', String(pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' });
|
|
364
|
+
return r.status === 0;
|
|
365
|
+
}
|
|
366
|
+
process.kill(pid, 'SIGKILL');
|
|
367
|
+
return true;
|
|
368
|
+
} catch {
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Kill switch: encerra toda instância anterior do LeadMaster (varre as portas
|
|
375
|
+
* candidatas) antes de subir a nova versão. Portas ocupadas por outros
|
|
376
|
+
* projetos são deixadas em paz (o resolvedor avançará para a próxima porta).
|
|
377
|
+
*/
|
|
378
|
+
async function encerrarInstanciaAnterior(portaInicial) {
|
|
379
|
+
let encerrouAlguma = false;
|
|
380
|
+
for (let i = 0; i < UI_PORT_MAX_TENTATIVAS; i++) {
|
|
381
|
+
const porta = portaInicial + i;
|
|
382
|
+
const saude = await httpGetJson(`http://127.0.0.1:${porta}/api/health`, 600);
|
|
383
|
+
if (!saude.ok || !saudeEhLeadmaster(saude.body)) continue; // não é LeadMaster — respeitar
|
|
384
|
+
const pids = platform() === 'win32' ? pidsOuvindoPortaWindows(porta) : pidsOuvindoPortaUnix(porta);
|
|
385
|
+
if (pids.length === 0) continue;
|
|
386
|
+
console.log(`ℹ Instância anterior do LeadMaster detectada na porta ${porta} — encerrando (PID ${pids.join(', ')})...`);
|
|
387
|
+
for (const pid of pids) {
|
|
388
|
+
if (!matarArvoreProcesso(pid)) {
|
|
389
|
+
console.warn(`⚠ Não foi possível encerrar o PID ${pid} (permissão?). A nova instância usará outra porta.`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
encerrouAlguma = true;
|
|
393
|
+
// Aguarda o sistema operacional liberar a porta (até ~5s).
|
|
394
|
+
for (let t = 0; t < 10; t++) {
|
|
395
|
+
await sleep(500);
|
|
396
|
+
const ainda = await httpGetJson(`http://127.0.0.1:${porta}/api/health`, 600);
|
|
397
|
+
if (!ainda.ok) break;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (encerrouAlguma) {
|
|
401
|
+
console.log('✓ Instância anterior encerrada — a versão nova assume o controle.');
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
300
405
|
/**
|
|
301
406
|
* Guarda da consola (anti-EPIPE): se o terminal for fechado (MobaXterm, VS
|
|
302
407
|
* Code), escrever em stdout/stderr gera EPIPE — que o Node trataria como
|
|
@@ -331,10 +436,17 @@ function guardConsoleStream(stream, logFile) {
|
|
|
331
436
|
// Comandos
|
|
332
437
|
// ---------------------------------------------------------------------------
|
|
333
438
|
|
|
334
|
-
function cmdInstall(args) {
|
|
439
|
+
async function cmdInstall(args) {
|
|
335
440
|
const home = resolveHome(args);
|
|
441
|
+
const portaInicial = Number.isFinite(args.port) && args.port > 0 ? args.port : UI_PORT;
|
|
336
442
|
console.log(`LeadMaster v${VERSION} — instalação local`);
|
|
337
443
|
console.log('');
|
|
444
|
+
|
|
445
|
+
// Kill switch (regra inviolável do AGENTS.md): encerra instâncias anteriores
|
|
446
|
+
// ANTES de instalar — libera os arquivos travados no Windows (erro EBUSY do
|
|
447
|
+
// npx ao atualizar por cima de uma instância rodando).
|
|
448
|
+
await encerrarInstanciaAnterior(portaInicial);
|
|
449
|
+
|
|
338
450
|
ensureHome(home);
|
|
339
451
|
console.log(`✓ Pasta de dados pronta: ${home}`);
|
|
340
452
|
console.log(` (leads, campanhas e configuração ficam em "${path.join(home, 'storage')}")`);
|
|
@@ -377,23 +489,19 @@ function cmdStart(args) {
|
|
|
377
489
|
console.log(`✓ Pasta de dados: ${home}`);
|
|
378
490
|
|
|
379
491
|
(async () => {
|
|
380
|
-
//
|
|
381
|
-
|
|
492
|
+
// Kill switch (regra inviolável do AGENTS.md): encerra qualquer instância
|
|
493
|
+
// anterior do LeadMaster (Node + Python) antes de subir a versão nova —
|
|
494
|
+
// nunca deixar a versão antiga no ar nem exibir aviso de reuso.
|
|
495
|
+
await encerrarInstanciaAnterior(portaInicial);
|
|
496
|
+
|
|
497
|
+
// Porta: primeira livre a partir de 5055 (→ 5056 → 5057 → ...).
|
|
498
|
+
const { porta } = await resolverPortaUI(portaInicial);
|
|
382
499
|
if (porta === null) {
|
|
383
500
|
console.error(`✗ Nenhuma porta livre entre ${portaInicial} e ${portaInicial + UI_PORT_MAX_TENTATIVAS - 1}.`);
|
|
384
501
|
console.error(' Libere uma porta ou inicie com --port=<numero>.');
|
|
385
502
|
process.exit(1);
|
|
386
503
|
}
|
|
387
504
|
const url = `http://127.0.0.1:${porta}`;
|
|
388
|
-
if (jaRodando) {
|
|
389
|
-
console.log(`ℹ LeadMaster já está rodando em ${url} — abra o endereço no navegador.`);
|
|
390
|
-
// Saída INFORMATIVA (antes de qualquer inicialização própria): não há
|
|
391
|
-
// servidor filho para supervisionar nem terminal para prender — o
|
|
392
|
-
// launcher não deve chamar process.exit(0) no caminho de SUCESSO da
|
|
393
|
-
// inicialização (regra inviolável do AGENTS.md).
|
|
394
|
-
process.exit(0);
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
505
|
if (porta !== portaInicial) {
|
|
398
506
|
console.log(`ℹ Porta ${portaInicial} ocupada — usando a porta ${porta}.`);
|
|
399
507
|
}
|
|
@@ -614,7 +722,14 @@ const CONSOLE_LOG = path.join(LOG_DIR, 'launcher_console.log');
|
|
|
614
722
|
[process.stdout, process.stderr].forEach((stream) => guardConsoleStream(stream, CONSOLE_LOG));
|
|
615
723
|
|
|
616
724
|
switch (command) {
|
|
617
|
-
case 'install':
|
|
725
|
+
case 'install':
|
|
726
|
+
// cmdInstall é async (kill switch): a promise mantém o evento vivo até o
|
|
727
|
+
// fim; rejeições são logadas pelos handlers globais e encerram com código 1.
|
|
728
|
+
cmdInstall(args).catch((err) => {
|
|
729
|
+
console.error('✗ Erro inesperado na instalação:', err && err.message ? err.message : err);
|
|
730
|
+
process.exit(1);
|
|
731
|
+
});
|
|
732
|
+
break;
|
|
618
733
|
case 'start': cmdStart(args); break;
|
|
619
734
|
case 'doctor': cmdDoctor(args); break;
|
|
620
735
|
case 'version': console.log(VERSION); break;
|