feat: BeautyLeads B2B cosmetics frontend on port 7788

New service (app/beauty_main.py) sharing the same /data volume:
- Separate FastAPI app running on port 7788
- beauty_ai.py: brand universe scan (~650 brands), portfolio match
  detection against OUR_BRANDS, Gemini B2B assessment prompt in Spanish
  returning quality/categories/dist_matches/outreach_email
- beauty_queue table + beauty_lead_quality/beauty_assessment columns
  in enriched_domains (with migrations)
- Endpoints: /api/beauty/assess/batch, /api/beauty/leads,
  /api/beauty/status, /api/beauty/export, /api/beauty/reset
- Static frontend: Browse (beauty/ecommerce pre-filtered, no CMS/SSL/KD
  columns), Validator, B2B Pipeline (brand chips, expandable outreach),
  Pre-screen, Export CSV
- docker-compose: second 'beauty' service with shared data volume
- Dockerfile: expose 7788 alongside 6677

Also: add 'error' prescreen_status handling + UI (orange stat box,
filter option) for 4xx/5xx HTTP responses

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-04 19:31:10 +02:00
parent db95876db2
commit a7dd7927b9
6 changed files with 1459 additions and 9 deletions

View File

@@ -88,6 +88,16 @@ _MIGRATIONS = [
"ALTER TABLE enriched_domains ADD COLUMN prescreen_at TEXT",
"ALTER TABLE enriched_domains ADD COLUMN ip TEXT",
"ALTER TABLE enriched_domains ADD COLUMN load_time_ms INTEGER",
"ALTER TABLE enriched_domains ADD COLUMN beauty_lead_quality TEXT",
"ALTER TABLE enriched_domains ADD COLUMN beauty_assessment TEXT",
"ALTER TABLE enriched_domains ADD COLUMN beauty_assessed_at TEXT",
"""CREATE TABLE IF NOT EXISTS beauty_queue (
domain TEXT PRIMARY KEY,
status TEXT DEFAULT 'pending',
created_at TEXT DEFAULT (datetime('now')),
completed_at TEXT,
error TEXT
)""",
]
# Index build state
@@ -488,6 +498,81 @@ async def queue_domains(domains: list[str]):
await db.commit()
async def queue_beauty(domains: list[str]):
async with aiosqlite.connect(SQLITE_PATH, timeout=30) as db:
await db.executemany(
"INSERT OR IGNORE INTO beauty_queue (domain) VALUES (?)",
[(d,) for d in domains],
)
await db.commit()
async def get_beauty_queue_status():
async with aiosqlite.connect(SQLITE_PATH, timeout=30) as db:
async with db.execute("SELECT status, COUNT(*) FROM beauty_queue GROUP BY status") as cur:
rows = {r[0]: r[1] async for r in cur}
return {
"pending": rows.get("pending", 0),
"running": rows.get("running", 0),
"done": rows.get("done", 0),
"failed": rows.get("failed", 0),
"total": sum(rows.values()),
}
async def save_beauty_assessment(domain: str, assessment: dict):
import json as _json
async with aiosqlite.connect(SQLITE_PATH, timeout=30) as db:
await db.execute(
"INSERT INTO enriched_domains (domain) VALUES (?) ON CONFLICT(domain) DO NOTHING",
(domain,),
)
await db.execute(
"""UPDATE enriched_domains SET
beauty_lead_quality=?, beauty_assessment=?, beauty_assessed_at=datetime('now')
WHERE domain=?""",
(assessment.get("lead_quality"), _json.dumps(assessment), domain),
)
await db.execute(
"UPDATE beauty_queue SET status='done', completed_at=datetime('now') WHERE domain=?",
(domain,),
)
await db.commit()
async def get_beauty_leads(quality: str = None, country: str = None,
page: int = 1, limit: int = 100):
import json as _json
offset = (page - 1) * limit
conditions = ["beauty_lead_quality IS NOT NULL"]
params: list = []
if quality:
conditions.append("beauty_lead_quality = ?")
params.append(quality.upper())
if country:
conditions.append("ip_country = ?")
params.append(country.upper())
where = "WHERE " + " AND ".join(conditions)
async with aiosqlite.connect(SQLITE_PATH, timeout=30) as db:
db.row_factory = aiosqlite.Row
async with db.execute(
f"SELECT * FROM enriched_domains {where} "
f"ORDER BY CASE beauty_lead_quality WHEN 'HOT' THEN 1 WHEN 'WARM' THEN 2 ELSE 3 END "
f"LIMIT ? OFFSET ?",
params + [limit, offset],
) as cur:
rows = [dict(r) async for r in cur]
async with db.execute(f"SELECT COUNT(*) FROM enriched_domains {where}", params) as cur:
total = (await cur.fetchone())[0]
# Parse beauty_assessment JSON inline
for r in rows:
try:
r["_beauty"] = _json.loads(r.get("beauty_assessment") or "{}")
except Exception:
r["_beauty"] = {}
return total, rows
async def get_queue_status():
async with aiosqlite.connect(SQLITE_PATH, timeout=30) as db:
async with db.execute("SELECT status, COUNT(*) FROM job_queue GROUP BY status") as cur: