@danhachuel/leadmaster 0.1.7 → 0.1.9
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 +48 -0
- package/backend/__pycache__/main.cpython-311.pyc +0 -0
- package/backend/main.py +36 -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_crm_routes.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_process_flags.cpython-311-pytest-8.4.2.pyc +0 -0
- package/backend/tests/test_crm_routes.py +64 -0
- package/backend/tests/test_launcher_protections.py +46 -0
- package/frontend/dist/assets/{index-pSbjfRli.css → index-DVFFIa5O.css} +1 -1
- package/frontend/dist/assets/index-ZsRPRMCF.js +90 -0
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
- package/scripts/cli.mjs +70 -6
- package/frontend/dist/assets/index-0xtgAs3t.js +0 -90
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,54 @@ O formato é baseado no [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.
|
|
|
6
6
|
A etiqueta visual de versão usa patch de dois dígitos (ex.: `0.1.00`); o registry NPM
|
|
7
7
|
normaliza para a forma canônica (ex.: `0.1.0`).
|
|
8
8
|
|
|
9
|
+
## [0.1.09] — 2026-11-08
|
|
10
|
+
|
|
11
|
+
### Corrigido
|
|
12
|
+
|
|
13
|
+
- **Encerramento silencioso do launcher no MobaXterm (Windows)** — o app morria
|
|
14
|
+
sozinho por sinais espúrios (perda de foco/minimização) e por EPIPE ao fechar o
|
|
15
|
+
terminal. Correções no `scripts/cli.mjs`:
|
|
16
|
+
- **Keep-alive:** `setInterval(() => {}, 1000);` mantém o event loop ativo mesmo
|
|
17
|
+
sem I/O de console.
|
|
18
|
+
- **Absorção de sinais no Windows:** `SIGINT`/`SIGHUP` agora são apenas logados
|
|
19
|
+
(`[LAUNCHER] Sinal absorvido no Windows`) e **não** encerram o launcher;
|
|
20
|
+
`SIGTERM`/`SIGBREAK` (Ctrl+Break) seguem como paradas legítimas. No
|
|
21
|
+
Linux/macOS o comportamento gracioso é mantido.
|
|
22
|
+
- **Guarda da consola (anti-EPIPE):** `guardConsoleStream()` ignora erros `EPIPE`
|
|
23
|
+
no `'error'` dos streams e espelha toda a saída em
|
|
24
|
+
`storage/logs/launcher_console.log`; escrita original com exceção retorna
|
|
25
|
+
silenciosamente. Aplicada a `process.stdout`/`process.stderr` antes de
|
|
26
|
+
qualquer escrita do launcher.
|
|
27
|
+
- Isolamento mantido: worker Python com `detached: true` + `windowsHide: true`
|
|
28
|
+
(`CREATE_NEW_PROCESS_GROUP`).
|
|
29
|
+
- **AGENTS.md:** nova seção *🚨 REGRA INVIOLÁVEL: ARQUITETURA DO LAUNCHER* com as
|
|
30
|
+
4 proteções obrigatórias.
|
|
31
|
+
- **Gate de regressão:** `backend/tests/test_launcher_protections.py` falha se
|
|
32
|
+
keep-alive, absorção de sinais, `guardConsoleStream` ou isolamento forem
|
|
33
|
+
removidos do launcher.
|
|
34
|
+
|
|
35
|
+
## [0.1.08] — 2026-11-08
|
|
36
|
+
|
|
37
|
+
### Adicionado
|
|
38
|
+
|
|
39
|
+
- **Módulo CRM Kanban implementado conforme especificação:**
|
|
40
|
+
- Rotas: `GET /api/crm/leads`, `POST /api/crm/leads` (criação manual) e
|
|
41
|
+
`PUT /api/crm/leads/{lead_id}/etapa` (movimentação ao soltar o card) —
|
|
42
|
+
persistência no `leads.json` local (seeds de demonstração mantidos).
|
|
43
|
+
- Kanban com **@dnd-kit/core + @dnd-kit/sortable** (multi-contêiner):
|
|
44
|
+
arrastar entre colunas persiste via API; reordenação visual dentro da
|
|
45
|
+
coluna; soltar fora das colunas reverte o movimento.
|
|
46
|
+
- Colunas: **Novos Leads → Em Contato → Proposta → Fechado**.
|
|
47
|
+
- Card mostra Nome, Telefone, Fonte e **Data de Criação**.
|
|
48
|
+
- Botão **"Adicionar Lead"** (gradiente) com modal de criação (campos
|
|
49
|
+
avançados em accordion).
|
|
50
|
+
- UX: card inteiro arrastável (Fitts); **opacity 0.5** no card arrastado e
|
|
51
|
+
**borda tracejada indigo** na coluna de destino (Zeigarnik); colunas
|
|
52
|
+
brancas com sombra suave (padrão do Dashboard).
|
|
53
|
+
- **7 novos testes pytest** para as rotas do CRM (`backend/tests/test_crm_routes.py`):
|
|
54
|
+
listagem, criação, persistência, validação, movimentação, 404 e etapas
|
|
55
|
+
inválidas — isolados com `LEADMASTER_HOME` temporário.
|
|
56
|
+
|
|
9
57
|
## [0.1.07] — 2026-11-08
|
|
10
58
|
|
|
11
59
|
### Adicionado
|
|
Binary file
|
package/backend/main.py
CHANGED
|
@@ -295,6 +295,42 @@ def aplicar_sugestao(dados: dict):
|
|
|
295
295
|
return {"ok": True, "lead": atualizado}
|
|
296
296
|
|
|
297
297
|
|
|
298
|
+
# ---------------------------------------------------------------------------
|
|
299
|
+
# CRM — Kanban (leads por etapa)
|
|
300
|
+
# ---------------------------------------------------------------------------
|
|
301
|
+
|
|
302
|
+
@app.get("/api/crm/leads")
|
|
303
|
+
def crm_listar_leads():
|
|
304
|
+
"""Todos os leads (persistidos em leads.json) — usado pelo quadro Kanban."""
|
|
305
|
+
return storage.list_leads()
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@app.post("/api/crm/leads", status_code=201)
|
|
309
|
+
def crm_criar_lead(lead_in: LeadIn):
|
|
310
|
+
"""Cria um lead manualmente pelo modal 'Adicionar Lead' do CRM."""
|
|
311
|
+
lead = lead_in.model_dump()
|
|
312
|
+
lead["id"] = storage._novo_id()
|
|
313
|
+
lead["etapa"] = _validar_etapa(lead.get("etapa", "leads"))
|
|
314
|
+
lead["score"] = lead.get("score") if lead.get("score") is not None else crm_agent.calcular_completude(lead)
|
|
315
|
+
storage.add_lead(lead)
|
|
316
|
+
agents.disparar_webhook("lead.criado", {"lead_id": lead["id"], "nome": lead["nome"]})
|
|
317
|
+
return lead
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
@app.put("/api/crm/leads/{lead_id}/etapa")
|
|
321
|
+
def crm_mover_lead(lead_id: str, dados: dict):
|
|
322
|
+
"""Move um lead de coluna no Kanban (drag-and-drop)."""
|
|
323
|
+
etapa = dados.get("etapa")
|
|
324
|
+
if not etapa:
|
|
325
|
+
raise HTTPException(400, "Informe a etapa de destino.")
|
|
326
|
+
_validar_etapa(etapa)
|
|
327
|
+
atualizado = storage.update_lead(lead_id, {"etapa": etapa})
|
|
328
|
+
if not atualizado:
|
|
329
|
+
raise HTTPException(404, "Lead não encontrado.")
|
|
330
|
+
agents.disparar_webhook("lead.etapa", {"lead_id": lead_id, "etapa": etapa, "via": "kanban"})
|
|
331
|
+
return atualizado
|
|
332
|
+
|
|
333
|
+
|
|
298
334
|
# ---------------------------------------------------------------------------
|
|
299
335
|
# Campanhas / Disparos
|
|
300
336
|
# ---------------------------------------------------------------------------
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Testes das rotas do CRM Kanban (backend/main.py).
|
|
2
|
+
|
|
3
|
+
Usam TestClient do FastAPI com LEADMASTER_HOME em diretório temporário, para
|
|
4
|
+
nunca tocar nos dados reais do usuário. Rodam iguais no CI (Linux) e local.
|
|
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 test_listar_leads(client):
|
|
21
|
+
r = client.get("/api/crm/leads")
|
|
22
|
+
assert r.status_code == 200
|
|
23
|
+
corpo = r.json()
|
|
24
|
+
assert isinstance(corpo, list) and len(corpo) >= 1 # seeds de demonstração
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_criar_lead(client):
|
|
28
|
+
r = client.post("/api/crm/leads", json={"nome": "Padaria Teste", "telefone": "(11) 90000-0000", "fonte": "Manual"})
|
|
29
|
+
assert r.status_code == 201
|
|
30
|
+
lead = r.json()
|
|
31
|
+
assert lead["id"]
|
|
32
|
+
assert lead["etapa"] == "leads"
|
|
33
|
+
# Confirma persistência no JSON local
|
|
34
|
+
lista = client.get("/api/crm/leads").json()
|
|
35
|
+
assert any(l["id"] == lead["id"] for l in lista)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_criar_lead_sem_nome_invalido(client):
|
|
39
|
+
r = client.post("/api/crm/leads", json={"nome": ""})
|
|
40
|
+
assert r.status_code == 422 # validação do modelo Pydantic
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_mover_lead_entre_colunas(client):
|
|
44
|
+
criado = client.post("/api/crm/leads", json={"nome": "Lead Móvel"}).json()
|
|
45
|
+
r = client.put(f"/api/crm/leads/{criado['id']}/etapa", json={"etapa": "proposta"})
|
|
46
|
+
assert r.status_code == 200
|
|
47
|
+
assert r.json()["etapa"] == "proposta"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_mover_lead_inexistente(client):
|
|
51
|
+
r = client.put("/api/crm/leads/nao-existe/etapa", json={"etapa": "fechado"})
|
|
52
|
+
assert r.status_code == 404
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_mover_lead_etapa_invalida(client):
|
|
56
|
+
criado = client.post("/api/crm/leads", json={"nome": "Lead X"}).json()
|
|
57
|
+
r = client.put(f"/api/crm/leads/{criado['id']}/etapa", json={"etapa": "nao_existe"})
|
|
58
|
+
assert r.status_code == 400
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_mover_lead_sem_etapa(client):
|
|
62
|
+
criado = client.post("/api/crm/leads", json={"nome": "Lead Y"}).json()
|
|
63
|
+
r = client.put(f"/api/crm/leads/{criado['id']}/etapa", json={})
|
|
64
|
+
assert r.status_code == 400
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Gate de regressão da arquitetura do launcher (scripts/cli.mjs).
|
|
2
|
+
|
|
3
|
+
Lê o launcher e falha se qualquer proteção contra encerramento silencioso no
|
|
4
|
+
Windows (MobaXterm) for removida:
|
|
5
|
+
|
|
6
|
+
1. Keep-alive com setInterval (event loop ativo sem I/O de console);
|
|
7
|
+
2. Absorção de SIGINT/SIGHUP no Windows (apenas log, sem encerrar);
|
|
8
|
+
3. Função guardConsoleStream (anti-EPIPE com espelho em arquivo de log);
|
|
9
|
+
4. Isolamento do worker Python (detached: true / CREATE_NEW_PROCESS_GROUP).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
CAMINHO = Path(__file__).resolve().parents[2] / "scripts" / "cli.mjs"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _conteudo() -> str:
|
|
18
|
+
assert CAMINHO.exists(), f"launcher não encontrado em {CAMINHO}"
|
|
19
|
+
return CAMINHO.read_text(encoding="utf-8")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TestLauncherProtections:
|
|
23
|
+
def test_keepalivo(self):
|
|
24
|
+
assert "setInterval(() => {}, 1000)" in _conteudo()
|
|
25
|
+
|
|
26
|
+
def test_absorcao_de_sinais_no_windows(self):
|
|
27
|
+
c = _conteudo()
|
|
28
|
+
assert "SIGINT" in c and "SIGTERM" in c and "SIGHUP" in c
|
|
29
|
+
assert "absorvido" in c
|
|
30
|
+
|
|
31
|
+
def test_guarda_anti_epipe(self):
|
|
32
|
+
c = _conteudo()
|
|
33
|
+
assert "guardConsoleStream" in c
|
|
34
|
+
assert "EPIPE" in c
|
|
35
|
+
assert "launcher_console.log" in c
|
|
36
|
+
|
|
37
|
+
def test_isolamento_do_worker(self):
|
|
38
|
+
c = _conteudo()
|
|
39
|
+
assert "detached: true" in c
|
|
40
|
+
assert "windowsHide: true" in c
|
|
41
|
+
|
|
42
|
+
def test_sigterm_nao_absorvido(self):
|
|
43
|
+
# SIGTERM/SIGBREAK são paradas legítimas: sempre encerram.
|
|
44
|
+
c = _conteudo()
|
|
45
|
+
assert "SIGTERM" in c and "SIGBREAK" in c
|
|
46
|
+
assert "naoAbsorver" in c
|