feat: Admin-панель в CRM + CLI --repass / --delete-db

- app/app.py: навигация CRM / Управление (st.sidebar.radio), render_admin()
  со статистикой, кнопками rescore / repass / сброс флагов / удаление БД с чекбоксом
- main.py: run_repass() (ренормализация телефонов+доменов + rescore, без HTTP),
  флаги --delete-db и --repass, Path import добавлен

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Aks
2026-06-09 13:36:30 +03:00
parent 98309dcc96
commit e116e508f9
2 changed files with 220 additions and 12 deletions
+139 -12
View File
@@ -7,6 +7,7 @@
Работает над той же leads.db что и парсер — изменения видны
с обеих сторон сразу.
"""
import importlib
import json
import sys
from pathlib import Path
@@ -16,21 +17,13 @@ import streamlit as st
# Чтобы импортировать config из родительской папки parser_v1/
PARENT = Path(__file__).parent.parent
sys.path.insert(0, str(PARENT))
if str(PARENT) not in sys.path:
sys.path.insert(0, str(PARENT))
import config # noqa: E402
import database # noqa: E402 (для автомиграции БД при первом запуске CRM)
import db_layer # noqa: E402 (рядом с app.py — Streamlit добавляет эту папку в sys.path)
# ─── Настройка страницы ──────────────────────────────────────────────
st.set_page_config(
page_title="CRM — 44AS Парсер лидов",
page_icon="🎯",
layout="wide",
initial_sidebar_state="expanded",
)
DB_PATH = PARENT / config.DB_PATH
# Автомиграция: если БД старая (без 4 CRM-колонок / outreach_events / lead_in_run) —
@@ -342,8 +335,8 @@ def render_lead_detail(lead_id: int) -> None:
st.rerun()
# ─── Главная страница ───────────────────────────────────────────────
def main():
# ─── Главная страница (CRM) ─────────────────────────────────────────
def render_crm():
st.title("🎯 CRM — Парсер лидов 44AS")
# ── Сайдбар: фильтры ────────────────────────────────────────────
@@ -550,5 +543,139 @@ def main():
render_lead_detail(selected_lead_id)
def render_admin() -> None:
"""Страница управления: статистика, repass, удаление БД."""
import database as _db
st.header("Управление / Admin")
# ── Статистика ───────────────────────────────────────────────────
st.subheader("Статистика БД")
try:
_conn = _db.get_connection(str(DB_PATH))
stats = _db.get_stats(_conn)
# band distribution (отдельный запрос — get_stats не содержит band)
band_rows = _conn.execute(
"SELECT band, COUNT(*) as cnt FROM leads GROUP BY band ORDER BY cnt DESC"
).fetchall()
_conn.close()
has_db = True
except Exception as e:
st.warning(f"БД недоступна: {e}")
has_db = False
stats = {}
band_rows = []
if has_db:
col1, col2, col3 = st.columns(3)
col1.metric("Всего лидов", stats.get("total", 0))
col2.metric("С телефоном", stats.get("with_phone", 0))
col3.metric("С email", stats.get("with_email", 0))
if band_rows:
st.caption("Распределение по band")
band_dict = {r[0] or "": r[1] for r in band_rows}
st.bar_chart(band_dict)
by_source = stats.get("by_source", {})
if by_source:
with st.expander("По источнику"):
st.write(by_source)
st.divider()
# ── Пересчёт score ───────────────────────────────────────────────
st.subheader("Пересчитать score")
st.caption("Применяет текущую формулу скоринга ко всем лидам. Без HTTP (~секунды).")
if st.button("Пересчитать score", type="primary", key="btn_rescore"):
with st.spinner("Пересчёт..."):
try:
_main = importlib.import_module("main")
_conn = _db.get_connection(str(DB_PATH))
changed = _main.run_rescore(_conn)
_conn.close()
st.success(f"Score изменился у {changed} лидов")
except Exception as e:
st.error(f"Ошибка: {e}")
st.divider()
# ── Repass ───────────────────────────────────────────────────────
st.subheader("Перепросчитать всё (repass)")
st.caption("Нормализует телефоны/домены + пересчитывает score. Без HTTP. ~1030 сек.")
if st.button("Запустить repass", type="primary", key="btn_repass"):
with st.spinner("Нормализация + rescore..."):
try:
_main = importlib.import_module("main")
_conn = _db.get_connection(str(DB_PATH))
result = _main.run_repass(_conn)
_conn.close()
st.success(
f"Готово: телефонов={result['phones_fixed']}, "
f"доменов={result['domains_fixed']}, "
f"score изменился у {result['scores_changed']}/{result['total']}"
)
except Exception as e:
st.error(f"Ошибка: {e}")
st.divider()
# ── Сбросить флаги обогащения ────────────────────────────────────
st.subheader("Сбросить флаги обогащения")
st.caption("site_checked_at → NULL у всех лидов. Следующий --enrich пройдёт заново.")
if st.button("Сбросить флаги сайтов", key="btn_rescan"):
try:
_conn = _db.get_connection(str(DB_PATH))
cur = _conn.execute(
"UPDATE leads SET site_checked_at = NULL WHERE site_checked_at IS NOT NULL"
)
_conn.commit()
count = cur.rowcount
_conn.close()
st.success(f"Сброшено у {count} лидов")
except Exception as e:
st.error(f"Ошибка: {e}")
st.divider()
# ── Удалить БД ───────────────────────────────────────────────────
st.subheader("Удалить БД")
st.warning("Удаляет leads.db + WAL/SHM и пересоздаёт пустую схему. Все лиды будут стёрты.")
confirm = st.checkbox("Да, удалить всю БД безвозвратно", key="chk_confirm_delete")
if st.button("Удалить БД", type="primary", key="btn_delete_db", disabled=not confirm):
try:
db_path = Path(str(DB_PATH))
for suffix in ("", "-wal", "-shm"):
p = Path(str(db_path) + suffix) if suffix else db_path
if p.exists():
p.unlink()
_db.init_db(str(DB_PATH))
st.success("БД удалена и пересоздана (пустая схема)")
st.rerun()
except Exception as e:
st.error(f"Ошибка: {e}")
def main() -> None:
st.set_page_config(
page_title="CRM — 44AS Парсер лидов",
page_icon="🎯",
layout="wide",
initial_sidebar_state="expanded",
)
mode = st.sidebar.radio(
"Раздел",
options=["crm", "admin"],
format_func=lambda m: {"crm": "CRM", "admin": "Управление / Admin"}[m],
)
st.sidebar.divider()
if mode == "admin":
render_admin()
else:
render_crm()
# Streamlit запускает скрипт целиком на каждое действие — без if __name__
main()