feat: визуальный запуск парсера в Admin-панели

render_parser_launcher() в Admin: форма (source/city/categories/limit/
enrich-флаги), кнопки Старт/Стоп, живой лог с авто-обновлением каждые 2с,
статус завершения. Константы _PYTHON/_MAIN/_LOG_FILE с абс. путями.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Aks
2026-06-09 13:40:57 +03:00
parent e116e508f9
commit 5466a0c943
+149
View File
@@ -9,7 +9,10 @@
"""
import importlib
import json
import os
import subprocess
import sys
import time
from pathlib import Path
import pandas as pd
@@ -26,6 +29,10 @@ import db_layer # noqa: E402 (рядом с app.py — Streamlit добавл
DB_PATH = PARENT / config.DB_PATH
_LOG_FILE = "/tmp/parser_run.log"
_PYTHON = str(Path(__file__).resolve().parent.parent / ".venv-linux" / "bin" / "python3")
_MAIN = str(Path(__file__).resolve().parent.parent / "main.py")
# Автомиграция: если БД старая (без 4 CRM-колонок / outreach_events / lead_in_run) —
# init_db догонит схему через ALTER TABLE и CREATE TABLE IF NOT EXISTS.
# Идемпотентно — на свежей БД ничего не делает.
@@ -543,12 +550,154 @@ def render_crm():
render_lead_detail(selected_lead_id)
def render_parser_launcher() -> None:
st.subheader("Запуск парсера")
proc: subprocess.Popen | None = st.session_state.get("parser_proc")
running = proc is not None and proc.poll() is None
# ── Форма настроек (показывать только когда не запущен) ──────────
if not running:
import config as _cfg
with st.form("parser_form"):
col1, col2 = st.columns(2)
source = col1.selectbox(
"Источник",
options=["yandex", "hh", "all"],
format_func=lambda s: {"yandex": "Яндекс.Карты", "hh": "HH.ru", "all": "Все"}[s],
)
cities_ui = [
{"label": "Москва", "city": "Москва", "district": None},
{"label": "Москва и МО", "city": "Москва и МО", "district": None},
{"label": "Санкт-Петербург", "city": "Санкт-Петербург", "district": None},
{"label": "Мытищи", "city": "Москва и МО", "district": "Мытищи"},
{"label": "Химки", "city": "Москва и МО", "district": "Химки"},
{"label": "Балашиха", "city": "Москва и МО", "district": "Балашиха"},
{"label": "Подольск", "city": "Москва и МО", "district": "Подольск"},
{"label": "Красногорск", "city": "Москва и МО", "district": "Красногорск"},
{"label": "Зеленоград", "city": "Москва", "district": "Зеленоград"},
{"label": "Другой...", "city": "__custom__", "district": None},
]
city_labels = [c["label"] for c in cities_ui]
city_idx = col2.selectbox(
"Город / район",
range(len(city_labels)),
format_func=lambda i: city_labels[i],
)
city_choice = cities_ui[city_idx]
custom_city = None
if city_choice["city"] == "__custom__":
custom_city = st.text_input("Введи город вручную")
categories = st.multiselect(
"Категории (пусто = все из config)",
options=_cfg.CATEGORIES,
default=[],
)
col3, col4 = st.columns(2)
limit = col3.number_input("Лимит на категорию", min_value=1, max_value=500, value=20)
col_e1, col_e2, col_e3 = st.columns(3)
do_enrich = col_e1.checkbox("--enrich (сайты)")
do_enrich_egrul = col_e2.checkbox("--enrich-egrul (ЕГРЮЛ)")
do_export = col_e3.checkbox("--export (CSV)")
submitted = st.form_submit_button("Запустить", type="primary")
if submitted:
city_val = custom_city if city_choice["city"] == "__custom__" else city_choice["city"]
district_val = city_choice["district"]
cmd = [
_PYTHON, _MAIN,
"--source", source,
"--city", city_val or "Москва",
"--limit", str(int(limit)),
]
if categories:
cmd += ["--category", ",".join(categories)]
if district_val:
cmd += ["--district", district_val]
if do_enrich:
cmd.append("--enrich")
if do_enrich_egrul:
cmd.append("--enrich-egrul")
if do_export:
cmd.append("--export")
log_f = open(_LOG_FILE, "w", encoding="utf-8")
proc = subprocess.Popen(
cmd,
stdout=log_f,
stderr=subprocess.STDOUT,
cwd=str(Path(_MAIN).parent),
)
st.session_state["parser_proc"] = proc
st.session_state["parser_log_fh"] = log_f
st.session_state["parser_cmd"] = " ".join(cmd)
st.rerun()
# ── Статус и управление ──────────────────────────────────────────
if running:
st.info(f"Парсер запущен (PID {proc.pid})")
if st.button("Остановить", type="primary", key="btn_stop_parser"):
proc.terminate()
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
proc.kill()
fh = st.session_state.pop("parser_log_fh", None)
if fh:
fh.close()
st.session_state.pop("parser_proc", None)
st.rerun()
else:
# Процесс завершился
if proc is not None:
rc = proc.poll()
fh = st.session_state.pop("parser_log_fh", None)
if fh:
fh.close()
st.session_state.pop("parser_proc", None)
if rc == 0:
st.success("Парсер завершился успешно")
else:
st.error(f"Парсер завершился с кодом {rc}")
# ── Лог ─────────────────────────────────────────────────────────
if os.path.exists(_LOG_FILE):
with open(_LOG_FILE, "r", encoding="utf-8", errors="replace") as f:
log_text = f.read()
if log_text:
st.text_area(
"Лог",
value=log_text,
height=300,
key="parser_log_area",
label_visibility="collapsed",
)
# Пока запущен — автообновление каждые 2 сек
if running:
time.sleep(2)
st.rerun()
st.divider()
def render_admin() -> None:
"""Страница управления: статистика, repass, удаление БД."""
import database as _db
st.header("Управление / Admin")
render_parser_launcher()
# ── Статистика ───────────────────────────────────────────────────
st.subheader("Статистика БД")
try: