feat: add Leads tab and Hide Assessed filter in Browse

- db.py: get_enriched() accepts ai_only + lead_quality params
- main.py: /api/enriched exposes ai_only + lead_quality query params;
  new /api/export/leads endpoint produces CSV with contacts + pitch
- index.html:
  - New "Leads 🤖" tab shows all AI-assessed domains with contacts
    (quality/country/limit filters, per-row 📋 copy email, 🔍 modal,
    CSV export, pagination, auto-refreshes every 3s)
  - Browse: "Hide assessed" checkbox filters out already-processed
    domains so you can focus on fresh targets
  - Poll cycle refreshes Leads tab when active

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-14 18:57:15 +02:00
parent 22eae3f9b7
commit 63f961dc80
3 changed files with 188 additions and 9 deletions

View File

@@ -156,12 +156,15 @@ async def enriched(
cms: str = Query(None),
country: str = Query(None),
kit_digital: Optional[bool] = Query(None),
ai_only: bool = Query(False),
lead_quality: str = Query(None),
page: int = Query(1, ge=1),
limit: int = Query(100, ge=1, le=1000),
):
total, rows = await get_enriched(
min_score=min_score, cms=cms, country=country,
kit_digital=kit_digital, page=page, limit=limit,
kit_digital=kit_digital, ai_only=ai_only, lead_quality=lead_quality,
page=page, limit=limit,
)
return {"page": page, "limit": limit, "total": total, "results": rows}
@@ -286,6 +289,52 @@ async def export_csv(
)
@app.get("/api/export/leads")
async def export_leads_csv(lead_quality: str = Query(None), country: str = Query(None)):
import json as _json
async def generate():
yield "domain,lead_quality,score,best_contact_channel,best_contact_value,emails,phones,whatsapp,social,cms,ip_country,page_title,ai_pitch\n"
p = 1
while True:
_, rows = await get_enriched(ai_only=True, lead_quality=lead_quality,
country=country, page=p, limit=500)
if not rows:
break
for r in rows:
contacts = {}
try:
contacts = _json.loads(r.get("contact_info") or "{}")
except Exception:
pass
def esc(v):
return f'"{str(v or "").replace(chr(34), chr(39))}"'
emails = esc(";".join(contacts.get("emails", [])[:3]))
phones = esc(";".join(contacts.get("phones", [])[:3]))
whatsapp = esc(";".join(contacts.get("whatsapp",[])[:2]))
social = esc(";".join(contacts.get("social", [])[:4]))
line = ",".join([
esc(r.get("domain")),
esc(r.get("ai_lead_quality")),
esc(r.get("score")),
esc(r.get("ai_contact_channel")),
esc(r.get("ai_contact_value")),
emails, phones, whatsapp, social,
esc(r.get("cms")),
esc(r.get("ip_country")),
esc(r.get("page_title")),
esc(r.get("ai_pitch")),
])
yield line + "\n"
p += 1
qual = f"_{lead_quality.lower()}" if lead_quality else ""
return StreamingResponse(
generate(), media_type="text/csv",
headers={"Content-Disposition": f'attachment; filename="domgod_leads{qual}.csv"'},
)
@app.post("/api/score/run")
async def score_run():
return await run_scoring()