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:
+139
-12
@@ -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. ~10–30 сек.")
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user