@danhachuel/leadmaster 0.1.16 → 0.1.18
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 +56 -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_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_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_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/test_launcher_protections.py +13 -0
- package/backend/tests/test_metrics.py +112 -0
- package/frontend/dist/assets/{index-BTNs0Fhp.js → index-DFVTLg-7.js} +4 -4
- package/frontend/dist/index.html +1 -1
- package/package.json +1 -1
- package/scripts/cli.mjs +66 -5
package/CHANGELOG.md
CHANGED
|
@@ -37,6 +37,62 @@ 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.18] — 2026-11-09
|
|
41
|
+
|
|
42
|
+
### Adicionado
|
|
43
|
+
|
|
44
|
+
- **Zero states no Painel (Efeito Zeigarnik)** — com base vazia a tela não
|
|
45
|
+
mostra mais gráficos em branco:
|
|
46
|
+
- `LeadsStageChart`: **"Nenhum dado ainda. Comece extraindo leads!"** com
|
|
47
|
+
orientação aos extratores (Maps/Instagram/CNPJ);
|
|
48
|
+
- `CampaignsDoughnut`: **"Nenhuma campanha disparada ainda."** com orientação
|
|
49
|
+
a Disparos;
|
|
50
|
+
- ambos em bloco tracejado discreto, sem quebrar o layout (altura preservada).
|
|
51
|
+
- **Testes das métricas (`backend/tests/test_metrics.py`)** — 4 novos testes da
|
|
52
|
+
rota `GET /api/metrics` com dados conhecidos em `LEADMASTER_HOME` temporário
|
|
53
|
+
(seeds de demo neutralizados):
|
|
54
|
+
- base vazia: totais zerados e taxas padrão (100% entrega / 0% erro);
|
|
55
|
+
- leads conhecidos: `leads_total` e `leads_por_etapa` exatos por coluna;
|
|
56
|
+
- consolidação de campanhas: enviadas/erros somados e taxas proporcionais
|
|
57
|
+
(150/162 → 92,6% e 7,4%);
|
|
58
|
+
- reflexo do Kanban: mover lead de coluna via `PUT /api/crm/leads/{id}/etapa`
|
|
59
|
+
recalcula `leads_por_etapa` na hora.
|
|
60
|
+
- Suíte completa: **44 passed**.
|
|
61
|
+
|
|
62
|
+
### Auditoria
|
|
63
|
+
|
|
64
|
+
- **`GET /api/metrics` (dinâmica):** auditada contra a especificação — já lê
|
|
65
|
+
`leads.json`/`campaigns.json` em tempo real e devolve `leads_total`,
|
|
66
|
+
`leads_por_etapa` (leads/em_contato/proposta/fechado), totais consolidados
|
|
67
|
+
das campanhas, `taxa_entrega`/`taxa_erro` (fallback 100%/0% sem campanhas) e
|
|
68
|
+
`conexao` (geral/llm/openoutreach). Nenhuma alteração de cálculo necessária —
|
|
69
|
+
agora com cobertura de testes e zero states na UI.
|
|
70
|
+
|
|
71
|
+
## [0.1.17] — 2026-11-09
|
|
72
|
+
|
|
73
|
+
### Corrigido
|
|
74
|
+
|
|
75
|
+
- **Erro `EBUSY` do npx — migração para Runtime Permanente.** A 0.1.15 tentou
|
|
76
|
+
resolver com kill switch dentro do launcher, o que é impossível: o `EBUSY`
|
|
77
|
+
acontece no gerenciador de arquivos do `npx` (`_npx\...`) **antes** de o
|
|
78
|
+
`cli.mjs` ser carregado, então o kill switch nunca rodava. Causa raiz: o
|
|
79
|
+
`uvicorn` era spawnado com `cwd` dentro do cache do npx, segurando o
|
|
80
|
+
diretório. Nova arquitetura:
|
|
81
|
+
- Pasta permanente **`<home>/.runtime`** guarda `backend/` + `frontend/dist/`.
|
|
82
|
+
- `install`: kill switch (libera arquivos) → apaga o `.runtime` anterior →
|
|
83
|
+
copia esta versão do pacote para ele. O cache do npx nunca mais é tocado
|
|
84
|
+
por processos nossos.
|
|
85
|
+
- `start`: spawna o Python a partir de **`<home>/.runtime/backend`** (nunca
|
|
86
|
+
de `__dirname`); sem `.runtime` pronto, auto-aprovisiona; com pronto,
|
|
87
|
+
reutiliza sem recopiar.
|
|
88
|
+
- Launcher faz `process.chdir(home)` na entrada — cwd fora do cache.
|
|
89
|
+
- Regra inviolável documentada no AGENTS.md ("terminantemente proibido
|
|
90
|
+
executar o servidor a partir da pasta temporária do npx") + gate de
|
|
91
|
+
regressão (`.runtime`, `copiarParaRuntime`, `cpSync`, `rmSync`,
|
|
92
|
+
`path.join(runtime, 'backend')`, `process.chdir(homeLauncher)`).
|
|
93
|
+
- `.runtime/` adicionado ao `.gitignore`; `doctor` reporta o status do
|
|
94
|
+
runtime.
|
|
95
|
+
|
|
40
96
|
## [0.1.16] — 2026-11-09
|
|
41
97
|
|
|
42
98
|
### 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
|
|
Binary file
|
|
@@ -78,6 +78,19 @@ class TestLauncherProtections:
|
|
|
78
78
|
assert "já está rodando" not in _conteudo().lower()
|
|
79
79
|
assert "ja esta rodando" not in _conteudo().lower()
|
|
80
80
|
|
|
81
|
+
def test_runtime_permanente(self):
|
|
82
|
+
# Arquitetura anti-EBUSY: executar de <home>/.runtime, nunca do cache npx.
|
|
83
|
+
c = _conteudo()
|
|
84
|
+
assert ".runtime" in c
|
|
85
|
+
assert "copiarParaRuntime" in c
|
|
86
|
+
assert "runtimeDir" in c
|
|
87
|
+
assert "cpSync" in c # cópia recursiva para o runtime
|
|
88
|
+
assert "rmSync" in c # limpeza do runtime anterior
|
|
89
|
+
# O spawn do servidor usa o runtime, não o PKG_ROOT.
|
|
90
|
+
assert "path.join(runtime, 'backend')" in c
|
|
91
|
+
# O launcher sai do cache do npx logo na entrada.
|
|
92
|
+
assert "process.chdir(homeLauncher)" in c
|
|
93
|
+
|
|
81
94
|
def test_sigterm_nao_absorvido(self):
|
|
82
95
|
# SIGTERM/SIGBREAK são paradas legítimas: sempre encerram.
|
|
83
96
|
c = _conteudo()
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Testes das métricas do Painel — GET /api/metrics.
|
|
2
|
+
|
|
3
|
+
Popula os JSONs de teste (LEADMASTER_HOME em diretório temporário) com dados
|
|
4
|
+
conhecidos e verifica o cálculo em tempo real: totais de leads, distribuição
|
|
5
|
+
por etapa, consolidação das campanhas (enviadas/erros/taxas) e status da
|
|
6
|
+
conexão.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from fastapi.testclient import TestClient
|
|
10
|
+
|
|
11
|
+
import main
|
|
12
|
+
from services import storage
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _client(tmp_path, monkeypatch):
|
|
16
|
+
monkeypatch.setenv("LEADMASTER_HOME", str(tmp_path))
|
|
17
|
+
# Cria os arquivos vazios ANTES do lifespan: evita os seeds de demo que o
|
|
18
|
+
# backend semeia na primeira execução — assim o teste parte de base zerada.
|
|
19
|
+
storage.save_leads([])
|
|
20
|
+
storage.save_campaigns([])
|
|
21
|
+
return TestClient(main.app)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _lead(nome: str, etapa: str, telefone: str) -> dict:
|
|
25
|
+
return {
|
|
26
|
+
"nome": nome,
|
|
27
|
+
"telefone": telefone,
|
|
28
|
+
"email": "",
|
|
29
|
+
"fonte": "teste",
|
|
30
|
+
"etapa": etapa,
|
|
31
|
+
"empresa": "",
|
|
32
|
+
"cidade": "",
|
|
33
|
+
"endereco": "",
|
|
34
|
+
"score": 50,
|
|
35
|
+
"tags": [],
|
|
36
|
+
"observacoes": "",
|
|
37
|
+
"extras": {},
|
|
38
|
+
"criado_em": storage.now_iso(),
|
|
39
|
+
"atualizado_em": storage.now_iso(),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_metrics_sem_dados(tmp_path, monkeypatch):
|
|
44
|
+
# Base vazia: nada de quebrar — taxas padrão (100% entrega, 0% erro).
|
|
45
|
+
with _client(tmp_path, monkeypatch) as client:
|
|
46
|
+
r = client.get("/api/metrics")
|
|
47
|
+
assert r.status_code == 200
|
|
48
|
+
m = r.json()
|
|
49
|
+
assert m["leads_total"] == 0
|
|
50
|
+
assert m["leads_por_etapa"] == {"leads": 0, "em_contato": 0, "proposta": 0, "fechado": 0}
|
|
51
|
+
assert m["total_enviadas"] == 0
|
|
52
|
+
assert m["total_erros"] == 0
|
|
53
|
+
assert m["taxa_entrega"] == 100.0
|
|
54
|
+
assert m["taxa_erro"] == 0.0
|
|
55
|
+
assert m["campanhas"] == {"enviadas": 0, "erros": 0, "total": 0}
|
|
56
|
+
assert m["conexao"]["geral"] is True
|
|
57
|
+
assert isinstance(m["conexao"]["llm"], bool)
|
|
58
|
+
assert isinstance(m["conexao"]["openoutreach"], bool)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_metrics_com_leads_conhecidos(tmp_path, monkeypatch):
|
|
62
|
+
monkeypatch.setenv("LEADMASTER_HOME", str(tmp_path))
|
|
63
|
+
storage.save_leads([])
|
|
64
|
+
storage.save_campaigns([])
|
|
65
|
+
storage.import_leads(
|
|
66
|
+
[
|
|
67
|
+
_lead("Lead A", "leads", "(11) 90000-0001"),
|
|
68
|
+
_lead("Lead B", "leads", "(11) 90000-0002"),
|
|
69
|
+
_lead("Lead C", "em_contato", "(11) 90000-0003"),
|
|
70
|
+
_lead("Lead D", "proposta", "(11) 90000-0004"),
|
|
71
|
+
_lead("Lead E", "fechado", "(11) 90000-0005"),
|
|
72
|
+
],
|
|
73
|
+
dedup=False,
|
|
74
|
+
)
|
|
75
|
+
with TestClient(main.app) as client:
|
|
76
|
+
m = client.get("/api/metrics").json()
|
|
77
|
+
assert m["leads_total"] == 5
|
|
78
|
+
assert m["leads_por_etapa"] == {"leads": 2, "em_contato": 1, "proposta": 1, "fechado": 1}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_metrics_consolida_campanhas(tmp_path, monkeypatch):
|
|
82
|
+
monkeypatch.setenv("LEADMASTER_HOME", str(tmp_path))
|
|
83
|
+
storage.save_leads([])
|
|
84
|
+
storage.save_campaigns(
|
|
85
|
+
[
|
|
86
|
+
{"id": "c1", "nome": "Campanha 1", "status": "concluida", "enviadas": 90, "erros": 10},
|
|
87
|
+
{"id": "c2", "nome": "Campanha 2", "status": "concluida", "enviadas": 60, "erros": 2},
|
|
88
|
+
]
|
|
89
|
+
)
|
|
90
|
+
with TestClient(main.app) as client:
|
|
91
|
+
m = client.get("/api/metrics").json()
|
|
92
|
+
assert m["total_enviadas"] == 150
|
|
93
|
+
assert m["total_erros"] == 12
|
|
94
|
+
assert m["taxa_entrega"] == 92.6 # 150 / 162 * 100
|
|
95
|
+
assert m["taxa_erro"] == 7.4 # 12 / 162 * 100
|
|
96
|
+
assert m["campanhas"] == {"enviadas": 150, "erros": 12, "total": 2}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_metrics_reflete_alteracoes_no_kanban(tmp_path, monkeypatch):
|
|
100
|
+
# Mover um lead de etapa (rota do CRM) muda a contagem no /api/metrics.
|
|
101
|
+
with _client(tmp_path, monkeypatch) as client:
|
|
102
|
+
criado = client.post(
|
|
103
|
+
"/api/crm/leads",
|
|
104
|
+
json={"nome": "Lead Kanban", "telefone": "(11) 90000-0009", "fonte": "teste"},
|
|
105
|
+
).json()
|
|
106
|
+
m1 = client.get("/api/metrics").json()
|
|
107
|
+
assert m1["leads_por_etapa"]["leads"] == 1
|
|
108
|
+
|
|
109
|
+
client.put(f"/api/crm/leads/{criado['id']}/etapa", json={"etapa": "fechado"})
|
|
110
|
+
m2 = client.get("/api/metrics").json()
|
|
111
|
+
assert m2["leads_por_etapa"] == {"leads": 0, "em_contato": 0, "proposta": 0, "fechado": 1}
|
|
112
|
+
assert m2["leads_total"] == 1
|