@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.
Files changed (29) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/backend/__pycache__/main.cpython-311.pyc +0 -0
  3. package/backend/main.py +29 -3
  4. package/backend/services/__pycache__/__init__.cpython-311.pyc +0 -0
  5. package/backend/services/__pycache__/agents.cpython-311.pyc +0 -0
  6. package/backend/services/__pycache__/crm_agent.cpython-311.pyc +0 -0
  7. package/backend/services/__pycache__/extractors.cpython-311.pyc +0 -0
  8. package/backend/services/__pycache__/flags.cpython-311.pyc +0 -0
  9. package/backend/services/__pycache__/llm.cpython-311.pyc +0 -0
  10. package/backend/services/__pycache__/storage.cpython-311.pyc +0 -0
  11. package/backend/services/agents.py +1 -0
  12. package/backend/services/storage.py +11 -6
  13. package/backend/tests/__pycache__/conftest.cpython-311-pytest-8.4.2.pyc +0 -0
  14. package/backend/tests/__pycache__/test_agents_routes.cpython-311-pytest-8.4.2.pyc +0 -0
  15. package/backend/tests/__pycache__/test_campaigns_routes.cpython-311-pytest-8.4.2.pyc +0 -0
  16. package/backend/tests/__pycache__/test_cnpj_extractor.cpython-311-pytest-8.4.2.pyc +0 -0
  17. package/backend/tests/__pycache__/test_config.cpython-311-pytest-8.4.2.pyc +0 -0
  18. package/backend/tests/__pycache__/test_crm_routes.cpython-311-pytest-8.4.2.pyc +0 -0
  19. package/backend/tests/__pycache__/test_instagram_extractor.cpython-311-pytest-8.4.2.pyc +0 -0
  20. package/backend/tests/__pycache__/test_launcher_protections.cpython-311-pytest-8.4.2.pyc +0 -0
  21. package/backend/tests/__pycache__/test_maps_extractor.cpython-311-pytest-8.4.2.pyc +0 -0
  22. package/backend/tests/__pycache__/test_metrics.cpython-311-pytest-8.4.2.pyc +0 -0
  23. package/backend/tests/__pycache__/test_process_flags.cpython-311-pytest-8.4.2.pyc +0 -0
  24. package/backend/tests/__pycache__/test_webhook_routes.cpython-311-pytest-8.4.2.pyc +0 -0
  25. package/backend/tests/test_webhook_routes.py +83 -0
  26. package/frontend/dist/assets/{index-BBxljeDe.css → index-Bn20U7qZ.css} +1 -1
  27. package/frontend/dist/assets/{index-ErHj9z-h.js → index-CC4fz5Tw.js} +18 -18
  28. package/frontend/dist/index.html +2 -2
  29. 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
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
- return storage.get_config().get("webhook", {})
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 webhook
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
- return agents.testar_webhook(dados.url)
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
  # ---------------------------------------------------------------------------
@@ -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", "lead.etapa", "campanha.concluida"],
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) and isinstance(webhook.get("eventos"), list):
193
- eventos = [e for e in webhook["eventos"] if e != "video.publicado"]
194
- if eventos != webhook["eventos"]:
195
- webhook["eventos"] = eventos
196
- mudou = True
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():
@@ -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