@danhachuel/leadmaster 0.1.23 → 0.1.25
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 +54 -0
- package/backend/__pycache__/main.cpython-311.pyc +0 -0
- package/backend/main.py +18 -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_agents_routes.cpython-311-pytest-8.4.2.pyc +0 -0
- package/backend/tests/__pycache__/test_campaigns_routes.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_config.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_extractors_real.cpython-311-pytest-8.4.2.pyc +0 -0
- package/backend/tests/__pycache__/test_funnel_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_lead_edit_routes.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_metrics.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/__pycache__/test_webhook_routes.cpython-311-pytest-8.4.2.pyc +0 -0
- package/backend/tests/test_launcher_protections.py +31 -0
- package/backend/tests/test_lead_edit_routes.py +63 -0
- package/frontend/dist/assets/{index-ViDiQGJH.css → index-BLuj5jS4.css} +1 -1
- package/frontend/dist/assets/index-C1mEC85V.js +90 -0
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
- package/scripts/cli.mjs +10 -0
- package/frontend/dist/assets/index-Bc1EFZVI.js +0 -90
package/CHANGELOG.md
CHANGED
|
@@ -37,6 +37,60 @@ 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.25] — 2026-11-09
|
|
41
|
+
|
|
42
|
+
### Adicionado
|
|
43
|
+
|
|
44
|
+
- **Modal de Detalhes do Lead no CRM (clique no card):**
|
|
45
|
+
- Rota `PUT /api/crm/leads/{lead_id}` — edição de `nome`, `telefone` e `email`
|
|
46
|
+
(todos opcionais no payload); nome vazio → **400** (prevenção de erros),
|
|
47
|
+
lead inexistente → **404**, payload sem campos editáveis → **400**.
|
|
48
|
+
- Card do Kanban passa a abrir os detalhes no **clique simples** (antes era
|
|
49
|
+
duplo clique); flag global suprime o clique disparado no pointerup após um
|
|
50
|
+
drag, então abrir detalhes e arrastar convivem sem conflito.
|
|
51
|
+
- Modal "Detalhes do Lead" exibe **todos os dados** — inclusive `extras`
|
|
52
|
+
(Site, Endereço, Avaliação, Seguidores, Username, Bio), tags, fonte e data
|
|
53
|
+
de criação — com **inputs de edição** para Nome/Telefone/E-mail pré-preenchidos
|
|
54
|
+
(vermelho + mensagem se o nome for apagado), textarea de observações e botão
|
|
55
|
+
**"Salvar Alterações"** grande em gradiente (Fitts + von Restorff), com
|
|
56
|
+
**spinner** durante o salvamento (Zeigarnik), toast de sucesso e
|
|
57
|
+
**atualização automática** do Kanban. O "Fechar" (X) permanece sutil.
|
|
58
|
+
- **5 novos testes pytest** (`backend/tests/test_lead_edit_routes.py`): edição
|
|
59
|
+
completa, edição parcial (campos omitidos preservados), 404, nome vazio (400)
|
|
60
|
+
e payload sem campos editáveis (400).
|
|
61
|
+
|
|
62
|
+
## [0.1.24] — 2026-11-09
|
|
63
|
+
|
|
64
|
+
### Corrigido
|
|
65
|
+
|
|
66
|
+
- **Launcher — regressão do terminal preso investigada e blindagem reforçada** —
|
|
67
|
+
usuário reportou o prompt do MobaXterm voltando logo após "LeadMaster no ar"
|
|
68
|
+
na 0.1.21 (runtime permanente). A investigação mostrou que o fluxo publicado
|
|
69
|
+
**mantém** as proteções (`process.stdin.resume();`, `setInterval`,
|
|
70
|
+
`child.on('exit')`/`close`, `process.on('exit')`, sem `.unref()`) e que uma
|
|
71
|
+
morte silenciosa assim — sem nenhum log do handler de saída — só ocorre por
|
|
72
|
+
`SIGKILL`/`taskkill /F` (ex.: outra execução `install`/`start` matando a
|
|
73
|
+
instância anterior pelo kill switch, ou fechamento forçado da aba). Mesmo
|
|
74
|
+
assim, o launcher foi reforçado e os gates agora validam o **fluxo**:
|
|
75
|
+
- **Rede de segurança no `cmdStart`:** `process.on('beforeExit')` re-arma o
|
|
76
|
+
`setInterval` se o event loop esvaziar (stdin em EOF, timer perdido,
|
|
77
|
+
surpresa de PTY) — o Node jamais encerra sozinho; erros do stream de
|
|
78
|
+
stdin (`process.stdin.on('error')`) são ignorados;
|
|
79
|
+
- **Gates de fluxo (`test_launcher_protections.py`):** novo helper
|
|
80
|
+
`_corpo_cmd_start()` extrai o corpo da função para garantir que
|
|
81
|
+
`spawn`, `process.stdin.resume();`, `setInterval`, `child.on('exit')`,
|
|
82
|
+
`child.on('close')`, `process.on('exit')` e `beforeExit` existam **dentro
|
|
83
|
+
do `start`** (não no `install` nem em função morta) e que **nenhum
|
|
84
|
+
`process.exit(`** apareça após o terminal ser preso — a saída só por ciclo
|
|
85
|
+
de vida do filho ou por `SIGTERM`/`SIGBREAK`;
|
|
86
|
+
- **AGENTS.md:** regra 2 do launcher documenta a rede de segurança e a
|
|
87
|
+
exigência de posição (após o spawn, no fluxo do start).
|
|
88
|
+
- **Diagnóstico rápido para o usuário:** com a regra acima, se o prompt voltar
|
|
89
|
+
sem as linhas "Encerrando LeadMaster..." (SIGTERM/SIGBREAK) ou "Servidor
|
|
90
|
+
local encerrou com código N" (Python caiu), a causa é externa ao launcher
|
|
91
|
+
(outra execução ou fechamento forçado) — o espelho
|
|
92
|
+
`storage/logs/launcher_console.log` mostra exatamente qual caminho ocorreu.
|
|
93
|
+
|
|
40
94
|
## [0.1.23] — 2026-11-09
|
|
41
95
|
|
|
42
96
|
### Adicionado
|
|
Binary file
|
package/backend/main.py
CHANGED
|
@@ -344,6 +344,24 @@ def crm_mover_lead(lead_id: str, dados: dict):
|
|
|
344
344
|
return atualizado
|
|
345
345
|
|
|
346
346
|
|
|
347
|
+
@app.put("/api/crm/leads/{lead_id}")
|
|
348
|
+
def crm_editar_lead(lead_id: str, dados: dict):
|
|
349
|
+
"""Edita os dados principais do lead (modal de detalhes do CRM).
|
|
350
|
+
|
|
351
|
+
Payload aceita qualquer subconjunto de {nome, telefone, email, ...}; o nome,
|
|
352
|
+
quando presente, não pode ficar vazio (prevenção de erros).
|
|
353
|
+
"""
|
|
354
|
+
if "nome" in dados and not str(dados.get("nome") or "").strip():
|
|
355
|
+
raise HTTPException(400, "O nome do lead não pode ficar vazio.")
|
|
356
|
+
patch = {k: v for k, v in dados.items() if k in ("nome", "telefone", "email")}
|
|
357
|
+
if not patch:
|
|
358
|
+
raise HTTPException(400, "Nenhum campo editável informado (nome, telefone, email).")
|
|
359
|
+
atualizado = storage.update_lead(lead_id, patch)
|
|
360
|
+
if not atualizado:
|
|
361
|
+
raise HTTPException(404, "Lead não encontrado.")
|
|
362
|
+
return atualizado
|
|
363
|
+
|
|
364
|
+
|
|
347
365
|
# ---------------------------------------------------------------------------
|
|
348
366
|
# Campanhas / Disparos
|
|
349
367
|
# ---------------------------------------------------------------------------
|
|
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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -23,6 +23,15 @@ def _conteudo() -> str:
|
|
|
23
23
|
return CAMINHO.read_text(encoding="utf-8")
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
def _corpo_cmd_start() -> str:
|
|
27
|
+
"""Corpo da função cmdStart — valida o FLUXO do start (não só strings)."""
|
|
28
|
+
c = _conteudo()
|
|
29
|
+
assert "function cmdStart(" in c, "função cmdStart não encontrada"
|
|
30
|
+
inicio = c.index("function cmdStart(")
|
|
31
|
+
fim = c.index("async function cmdDoctor")
|
|
32
|
+
return c[inicio:fim]
|
|
33
|
+
|
|
34
|
+
|
|
26
35
|
class TestLauncherProtections:
|
|
27
36
|
def test_keepalivo(self):
|
|
28
37
|
assert "setInterval(() => {}, 1000)" in _conteudo()
|
|
@@ -44,6 +53,28 @@ class TestLauncherProtections:
|
|
|
44
53
|
# Avisar que já existe instância no ar é proibido (impede atualizações).
|
|
45
54
|
assert "já está rodando" not in c
|
|
46
55
|
|
|
56
|
+
def test_keepalive_no_fluxo_do_start(self):
|
|
57
|
+
# Gate de FLUXO (regressão da 0.1.21): as proteções precisam existir
|
|
58
|
+
# dentro do cmdStart — no install, em função morta ou em bloco errado
|
|
59
|
+
# não valem. A morte prematura do Node devolve o prompt do MobaXterm.
|
|
60
|
+
corpo = _corpo_cmd_start()
|
|
61
|
+
assert "spawn(" in corpo # worker Python é spawnado no start
|
|
62
|
+
assert "process.stdin.resume();" in corpo # terminal preso
|
|
63
|
+
assert "setInterval(() => {}, 1000)" in corpo # keep-alive
|
|
64
|
+
assert "beforeExit" in corpo # rede de segurança do event loop
|
|
65
|
+
assert "child.on('exit'" in corpo or 'child.on("exit"' in corpo
|
|
66
|
+
assert "child.on('close'" in corpo or 'child.on("close"' in corpo
|
|
67
|
+
assert "process.on('exit'" in corpo or 'process.on("exit"' in corpo
|
|
68
|
+
|
|
69
|
+
def test_start_nunca_chama_exit_apos_terminal_preso(self):
|
|
70
|
+
# Após o terminal ser preso, nenhum caminho de sucesso chama
|
|
71
|
+
# process.exit — a saída só ocorre pelo ciclo de vida do filho
|
|
72
|
+
# (child exit/close) ou por sinais de parada legítima.
|
|
73
|
+
corpo = _corpo_cmd_start()
|
|
74
|
+
pos = corpo.index("process.stdin.resume();")
|
|
75
|
+
fim_iife = corpo.index("})().catch(") # catch de erro fatal fica de fora
|
|
76
|
+
assert "process.exit(" not in corpo[pos:fim_iife]
|
|
77
|
+
|
|
47
78
|
def test_ciclo_de_vida_vinculado(self):
|
|
48
79
|
c = _conteudo()
|
|
49
80
|
assert "child.on('exit'" in c or "child.on(\"exit\"" in c
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Testes da edição do lead — PUT /api/crm/leads/{lead_id}.
|
|
2
|
+
|
|
3
|
+
Edição de nome/telefone/email (todos opcionais), persistência, 404 para ID
|
|
4
|
+
inexistente e bloqueio de nome vazio (prevenção de erros).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from fastapi.testclient import TestClient
|
|
9
|
+
|
|
10
|
+
import main
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.fixture()
|
|
14
|
+
def client(tmp_path, monkeypatch):
|
|
15
|
+
monkeypatch.setenv("LEADMASTER_HOME", str(tmp_path))
|
|
16
|
+
with TestClient(main.app) as c:
|
|
17
|
+
yield c
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _criar_lead(client):
|
|
21
|
+
return client.post("/api/crm/leads", json={"nome": "Lead Original", "telefone": "(11) 90000-0000", "email": "a@b.com"}).json()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_editar_campos_principais(client):
|
|
25
|
+
lead = _criar_lead(client)
|
|
26
|
+
r = client.put(
|
|
27
|
+
f"/api/crm/leads/{lead['id']}",
|
|
28
|
+
json={"nome": "Lead Editado", "telefone": "(21) 98888-7777", "email": "novo@empresa.com"},
|
|
29
|
+
)
|
|
30
|
+
assert r.status_code == 200
|
|
31
|
+
corpo = r.json()
|
|
32
|
+
assert corpo["nome"] == "Lead Editado"
|
|
33
|
+
assert corpo["telefone"] == "(21) 98888-7777"
|
|
34
|
+
assert corpo["email"] == "novo@empresa.com"
|
|
35
|
+
# Persistido no leads.json (a listagem reflete a edição).
|
|
36
|
+
salvo = [l for l in client.get("/api/crm/leads").json() if l["id"] == lead["id"]][0]
|
|
37
|
+
assert salvo["nome"] == "Lead Editado"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_editar_parcial(client):
|
|
41
|
+
lead = _criar_lead(client)
|
|
42
|
+
r = client.put(f"/api/crm/leads/{lead['id']}", json={"email": "parcial@x.com"})
|
|
43
|
+
assert r.status_code == 200
|
|
44
|
+
assert r.json()["email"] == "parcial@x.com"
|
|
45
|
+
assert r.json()["nome"] == "Lead Original" # campos omitidos permanecem
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_editar_inexistente_404(client):
|
|
49
|
+
r = client.put("/api/crm/leads/nao-existe", json={"nome": "X"})
|
|
50
|
+
assert r.status_code == 404
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_nome_vazio_bloqueado(client):
|
|
54
|
+
lead = _criar_lead(client)
|
|
55
|
+
r = client.put(f"/api/crm/leads/{lead['id']}", json={"nome": " "})
|
|
56
|
+
assert r.status_code == 400
|
|
57
|
+
assert "vazio" in r.json()["detail"]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_payload_sem_campos_editaveis(client):
|
|
61
|
+
lead = _criar_lead(client)
|
|
62
|
+
r = client.put(f"/api/crm/leads/{lead['id']}", json={"etapa": "fechado"})
|
|
63
|
+
assert r.status_code == 400
|