diff --git a/app/app.py b/app/app.py index f6366fb..9abb2c2 100644 --- a/app/app.py +++ b/app/app.py @@ -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: