@danhachuel/leadmaster 0.1.20 → 0.1.21
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 +23 -0
- package/backend/__pycache__/main.cpython-311.pyc +0 -0
- package/backend/main.py +29 -3
- 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/services/agents.py +1 -0
- package/backend/services/storage.py +11 -6
- 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_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/__pycache__/test_webhook_routes.cpython-311-pytest-8.4.2.pyc +0 -0
- package/backend/tests/test_webhook_routes.py +83 -0
- package/frontend/dist/assets/{index-BBxljeDe.css → index-Bn20U7qZ.css} +1 -1
- package/frontend/dist/assets/{index-ErHj9z-h.js → index-CC4fz5Tw.js} +18 -18
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -37,6 +37,29 @@ 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.21] — 2026-11-09
|
|
41
|
+
|
|
42
|
+
### Adicionado
|
|
43
|
+
|
|
44
|
+
- **Módulo de Webhooks revisado conforme especificação:**
|
|
45
|
+
- `GET /api/webhook` agora retorna também **`ultimo_teste`** (registrado a cada
|
|
46
|
+
teste bem-sucedido no `config.json`).
|
|
47
|
+
- `PUT /api/webhook` e `POST /api/webhook/test` **validam a URL**: só
|
|
48
|
+
`http://`/`https://` são aceitos (400 com mensagem acionável).
|
|
49
|
+
- Novo evento **`campanha.iniciada`** — disparado quando uma campanha começa a
|
|
50
|
+
rodar; migração aditiva garante que ele apareça nas configurações existentes.
|
|
51
|
+
- Frontend `frontend/src/pages/Webhook.tsx` reformulado: eventos exibidos
|
|
52
|
+
diretamente e limpos (**Lead Criado**, **Campanha Iniciada**, **Campanha
|
|
53
|
+
Finalizada** — Lei de Hick), **prevenção de erros** (Nielsen): URL fora do
|
|
54
|
+
padrão http(s) bloqueia o salvamento e marca o input com **borda vermelha**,
|
|
55
|
+
botão **"Salvar Configurações"** em gradiente (Fitts) e **"Testar Webhook"**
|
|
56
|
+
em outline contrastante (von Restorff), toast de sucesso/erro no teste e card
|
|
57
|
+
**"Último teste realizado em: [data/hora]"**.
|
|
58
|
+
- **7 novos testes pytest** (`backend/tests/test_webhook_routes.py`): leitura do
|
|
59
|
+
padrão, salvamento de URL/eventos com persistência, URL inválida (400), teste
|
|
60
|
+
com sucesso (mock do `httpx.post`, registra `ultimo_teste`), teste com falha de
|
|
61
|
+
conexão sem quebrar (mock de `httpx.ConnectError`) e URL de teste inválida.
|
|
62
|
+
|
|
40
63
|
## [0.1.20] — 2026-11-09
|
|
41
64
|
|
|
42
65
|
### Adicionado
|
|
Binary file
|
package/backend/main.py
CHANGED
|
@@ -712,21 +712,47 @@ def atendimento_chat(dados: ChatIn):
|
|
|
712
712
|
|
|
713
713
|
@app.get("/api/webhook")
|
|
714
714
|
def obter_webhook():
|
|
715
|
-
|
|
715
|
+
webhook = storage.get_config().get("webhook", {})
|
|
716
|
+
return {
|
|
717
|
+
"url": webhook.get("url", ""),
|
|
718
|
+
"eventos": webhook.get("eventos", []),
|
|
719
|
+
"ultimo_teste": webhook.get("ultimo_teste"),
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def _url_valida(url: str) -> bool:
|
|
724
|
+
return isinstance(url, str) and (url.startswith("http://") or url.startswith("https://"))
|
|
716
725
|
|
|
717
726
|
|
|
718
727
|
@app.put("/api/webhook")
|
|
719
728
|
def salvar_webhook(patch: dict):
|
|
720
729
|
config = storage.get_config()
|
|
721
730
|
webhook = config.get("webhook", {})
|
|
731
|
+
url = patch.get("url", webhook.get("url", ""))
|
|
732
|
+
if url and not _url_valida(url):
|
|
733
|
+
raise HTTPException(400, "URL inválida: deve começar com http:// ou https://.")
|
|
722
734
|
webhook.update({k: v for k, v in patch.items() if k in ("url", "eventos")})
|
|
723
735
|
storage.update_config({"webhook": webhook})
|
|
724
|
-
return
|
|
736
|
+
return {
|
|
737
|
+
"url": webhook.get("url", ""),
|
|
738
|
+
"eventos": webhook.get("eventos", []),
|
|
739
|
+
"ultimo_teste": webhook.get("ultimo_teste"),
|
|
740
|
+
}
|
|
725
741
|
|
|
726
742
|
|
|
727
743
|
@app.post("/api/webhook/test")
|
|
728
744
|
def testar_webhook(dados: WebhookTestIn):
|
|
729
|
-
|
|
745
|
+
if dados.url and not _url_valida(dados.url):
|
|
746
|
+
raise HTTPException(400, "URL inválida: deve começar com http:// ou https://.")
|
|
747
|
+
resultado = agents.testar_webhook(dados.url)
|
|
748
|
+
if resultado.get("ok"):
|
|
749
|
+
# Registra o último teste bem-sucedido no config.json.
|
|
750
|
+
config = storage.get_config()
|
|
751
|
+
webhook = config.get("webhook", {})
|
|
752
|
+
webhook["ultimo_teste"] = storage.now_iso()
|
|
753
|
+
storage.update_config({"webhook": webhook})
|
|
754
|
+
resultado["ultimo_teste"] = webhook["ultimo_teste"]
|
|
755
|
+
return resultado
|
|
730
756
|
|
|
731
757
|
|
|
732
758
|
# ---------------------------------------------------------------------------
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -100,6 +100,7 @@ def iniciar_campanha(campaign_id: str) -> dict:
|
|
|
100
100
|
campaign.setdefault("erros", 0)
|
|
101
101
|
campaign.setdefault("eventos", [])
|
|
102
102
|
storage.upsert_campaign(campaign)
|
|
103
|
+
disparar_webhook("campanha.iniciada", {"campanha_id": campaign_id, "nome": campaign["nome"]})
|
|
103
104
|
|
|
104
105
|
threading.Thread(target=_executar_campanha, args=(campaign_id,), daemon=True).start()
|
|
105
106
|
return {"ok": True, "mensagem": "Campanha iniciada."}
|
|
@@ -135,7 +135,7 @@ DEFAULT_CONFIG = {
|
|
|
135
135
|
},
|
|
136
136
|
"webhook": {
|
|
137
137
|
"url": "",
|
|
138
|
-
"eventos": ["lead.criado", "
|
|
138
|
+
"eventos": ["lead.criado", "campanha.iniciada", "campanha.concluida"],
|
|
139
139
|
},
|
|
140
140
|
}
|
|
141
141
|
|
|
@@ -189,11 +189,16 @@ def _limpar_deprecated(config: dict) -> dict:
|
|
|
189
189
|
mudou = True
|
|
190
190
|
# Evento de webhook do recurso removido.
|
|
191
191
|
webhook = config.get("webhook")
|
|
192
|
-
if isinstance(webhook, dict)
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
192
|
+
if isinstance(webhook, dict):
|
|
193
|
+
if isinstance(webhook.get("eventos"), list):
|
|
194
|
+
eventos = [e for e in webhook["eventos"] if e != "video.publicado"]
|
|
195
|
+
# Migração aditiva: o evento "campanha.iniciada" passou a existir
|
|
196
|
+
# (0.1.21) — garantimos que configs antigas também o ofereçam.
|
|
197
|
+
if "campanha.iniciada" not in eventos:
|
|
198
|
+
eventos.append("campanha.iniciada")
|
|
199
|
+
if eventos != webhook["eventos"]:
|
|
200
|
+
webhook["eventos"] = eventos
|
|
201
|
+
mudou = True
|
|
197
202
|
# Dados órfãos do pipeline de vídeos: o arquivo só existia para esse recurso.
|
|
198
203
|
pipeline_path = storage_dir() / PIPELINE_FILE
|
|
199
204
|
if pipeline_path.exists():
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Testes das rotas de Webhook (backend/main.py).
|
|
2
|
+
|
|
3
|
+
GET/PUT /api/webhook (com validação de URL) e POST /api/webhook/test com o
|
|
4
|
+
HTTP externo mockado — o endpoint de teste nunca quebra com URL inválida ou
|
|
5
|
+
indisponível. HOME isolado em diretório temporário.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from unittest.mock import patch
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
import pytest
|
|
12
|
+
from fastapi.testclient import TestClient
|
|
13
|
+
|
|
14
|
+
import main
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture()
|
|
18
|
+
def client(tmp_path, monkeypatch):
|
|
19
|
+
monkeypatch.setenv("LEADMASTER_HOME", str(tmp_path))
|
|
20
|
+
with TestClient(main.app) as c:
|
|
21
|
+
yield c
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_obter_padrao(client):
|
|
25
|
+
r = client.get("/api/webhook")
|
|
26
|
+
assert r.status_code == 200
|
|
27
|
+
corpo = r.json()
|
|
28
|
+
assert corpo["url"] == ""
|
|
29
|
+
assert "lead.criado" in corpo["eventos"]
|
|
30
|
+
assert corpo["ultimo_teste"] is None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_salvar_url_e_eventos(client):
|
|
34
|
+
r = client.put(
|
|
35
|
+
"/api/webhook",
|
|
36
|
+
json={"url": "https://n8n.exemplo.com/webhook/abc", "eventos": ["lead.criado"]},
|
|
37
|
+
)
|
|
38
|
+
assert r.status_code == 200
|
|
39
|
+
corpo = r.json()
|
|
40
|
+
assert corpo["url"] == "https://n8n.exemplo.com/webhook/abc"
|
|
41
|
+
assert corpo["eventos"] == ["lead.criado"]
|
|
42
|
+
# Persistido no config.json
|
|
43
|
+
salvo = client.get("/api/webhook").json()
|
|
44
|
+
assert salvo["url"] == "https://n8n.exemplo.com/webhook/abc"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_salvar_url_invalida_400(client):
|
|
48
|
+
r = client.put("/api/webhook", json={"url": "nao-e-uma-url"})
|
|
49
|
+
assert r.status_code == 400
|
|
50
|
+
assert "http" in r.json()["detail"]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _resposta_ok(*_args, **_kwargs):
|
|
54
|
+
return httpx.Response(200, json={"ok": True})
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_testar_webhook_sucesso(client):
|
|
58
|
+
client.put("/api/webhook", json={"url": "https://exemplo.com/hook"})
|
|
59
|
+
with patch.object(main.agents.httpx, "post", side_effect=_resposta_ok):
|
|
60
|
+
r = client.post("/api/webhook/test", json={"url": "https://exemplo.com/hook"})
|
|
61
|
+
assert r.status_code == 200
|
|
62
|
+
corpo = r.json()
|
|
63
|
+
assert corpo["ok"] is True
|
|
64
|
+
assert corpo["status_http"] == 200
|
|
65
|
+
assert corpo["ultimo_teste"] # registrado
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_testar_webhook_conexao_falha_nao_quebra(client):
|
|
69
|
+
# URL indisponível: retorna erro amigável (não 500).
|
|
70
|
+
with patch.object(main.agents.httpx, "post", side_effect=httpx.ConnectError("sem rede")):
|
|
71
|
+
r = client.post("/api/webhook/test", json={"url": "https://inexistente.invalid/hook"})
|
|
72
|
+
assert r.status_code == 200
|
|
73
|
+
assert r.json()["ok"] is False
|
|
74
|
+
assert "mensagem" in r.json()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_testar_webhook_url_invalida_400(client):
|
|
78
|
+
# Passa o min_length do modelo, mas falha no esquema http(s) → 400.
|
|
79
|
+
r = client.post("/api/webhook/test", json={"url": "nao-http-url.com/webhook"})
|
|
80
|
+
assert r.status_code == 400
|
|
81
|
+
# URL curta demais falha na validação do modelo.
|
|
82
|
+
r2 = client.post("/api/webhook/test", json={"url": "xyz"})
|
|
83
|
+
assert r2.status_code == 422
|