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:
85
app/db.py
85
app/db.py
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user