From 2fcee9f06531f5c346be99a15599f7e8f55c2f9c Mon Sep 17 00:00:00 2001 From: Maksim Date: Thu, 4 Sep 2025 21:33:13 +0300 Subject: [PATCH 1/5] =?UTF-8?q?streamlit=20=D1=80=D0=B5=D1=84=D0=B0=D0=BA?= =?UTF-8?q?=D1=82=D0=BE=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- streamlit_app/api_client.py | 80 ++ streamlit_app/config.py | 53 ++ streamlit_app/parsers_ui/__init__.py | 3 + .../parsers_ui/monitoring_fuel_ui.py | 91 ++ streamlit_app/parsers_ui/monitoring_tar_ui.py | 108 +++ .../parsers_ui/oper_spravka_tech_pos_ui.py | 84 ++ .../parsers_ui/statuses_repair_ca_ui.py | 146 ++++ streamlit_app/parsers_ui/svodka_ca_ui.py | 80 ++ streamlit_app/parsers_ui/svodka_pm_ui.py | 118 +++ .../parsers_ui/svodka_repair_ca_ui.py | 110 +++ streamlit_app/sidebar.py | 53 ++ streamlit_app/streamlit_app.py | 825 +----------------- 12 files changed, 948 insertions(+), 803 deletions(-) create mode 100644 streamlit_app/api_client.py create mode 100644 streamlit_app/config.py create mode 100644 streamlit_app/parsers_ui/__init__.py create mode 100644 streamlit_app/parsers_ui/monitoring_fuel_ui.py create mode 100644 streamlit_app/parsers_ui/monitoring_tar_ui.py create mode 100644 streamlit_app/parsers_ui/oper_spravka_tech_pos_ui.py create mode 100644 streamlit_app/parsers_ui/statuses_repair_ca_ui.py create mode 100644 streamlit_app/parsers_ui/svodka_ca_ui.py create mode 100644 streamlit_app/parsers_ui/svodka_pm_ui.py create mode 100644 streamlit_app/parsers_ui/svodka_repair_ca_ui.py create mode 100644 streamlit_app/sidebar.py diff --git a/streamlit_app/api_client.py b/streamlit_app/api_client.py new file mode 100644 index 0000000..22b92c2 --- /dev/null +++ b/streamlit_app/api_client.py @@ -0,0 +1,80 @@ +""" +Модуль для работы с API +""" +import requests +import os +from typing import Dict, Any, List, Tuple + +# Конфигурация API +API_BASE_URL = os.getenv("API_BASE_URL", "http://fastapi:8000") # Внутренний адрес для Docker +API_PUBLIC_URL = os.getenv("API_PUBLIC_URL", "http://localhost:8000") # Внешний адрес для пользователя + + +def check_api_health() -> bool: + """Проверка доступности API""" + try: + response = requests.get(f"{API_BASE_URL}/", timeout=5) + return response.status_code == 200 + except: + return False + + +def get_available_parsers() -> List[str]: + """Получение списка доступных парсеров""" + try: + response = requests.get(f"{API_BASE_URL}/parsers") + if response.status_code == 200: + return response.json()["parsers"] + return [] + except: + return [] + + +def get_server_info() -> Dict[str, Any]: + """Получение информации о сервере""" + try: + response = requests.get(f"{API_BASE_URL}/server-info") + if response.status_code == 200: + return response.json() + return {} + except: + return {} + + +def upload_file_to_api(endpoint: str, file_data: bytes, filename: str) -> Tuple[Dict[str, Any], int]: + """Загрузка файла на API""" + try: + # Определяем правильное имя поля в зависимости от эндпоинта + if "zip" in endpoint: + files = {"zip_file": (filename, file_data, "application/zip")} + else: + files = {"file": (filename, file_data, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")} + + response = requests.post(f"{API_BASE_URL}{endpoint}", files=files) + return response.json(), response.status_code + except Exception as e: + return {"error": str(e)}, 500 + + +def make_api_request(endpoint: str, data: Dict[str, Any]) -> Tuple[Dict[str, Any], int]: + """Выполнение API запроса""" + try: + response = requests.post(f"{API_BASE_URL}{endpoint}", json=data) + return response.json(), response.status_code + except Exception as e: + return {"error": str(e)}, 500 + + +def get_available_ogs(parser_name: str) -> List[str]: + """Получение доступных ОГ для парсера""" + try: + response = requests.get(f"{API_BASE_URL}/parsers/{parser_name}/available_ogs") + if response.status_code == 200: + data = response.json() + return data.get("available_ogs", []) + else: + print(f"⚠️ Ошибка получения ОГ: {response.status_code}") + return [] + except Exception as e: + print(f"⚠️ Ошибка при запросе ОГ: {e}") + return [] \ No newline at end of file diff --git a/streamlit_app/config.py b/streamlit_app/config.py new file mode 100644 index 0000000..bb49d58 --- /dev/null +++ b/streamlit_app/config.py @@ -0,0 +1,53 @@ +""" +Конфигурация приложения +""" +import streamlit as st + +# Конфигурация страницы +def setup_page_config(): + """Настройка конфигурации страницы Streamlit""" + st.set_page_config( + page_title="NIN Excel Parsers API Demo", + page_icon="📊", + layout="wide", + initial_sidebar_state="expanded" + ) + +# Константы для парсеров +PARSER_TABS = [ + "📊 Сводки ПМ", + "🏭 Сводки СА", + "⛽ Мониторинг топлива", + "🔧 Ремонт СА", + "📋 Статусы ремонта СА", + "⚡ Мониторинг ТЭР", + "🏭 Операционные справки" +] + +# Константы для ОГ +DEFAULT_OGS = [ + "SNPZ", "KNPZ", "ANHK", "AchNPZ", "UNPZ", "UNH", "NOV", + "NovKuybNPZ", "KuybNPZ", "CyzNPZ", "TuapsNPZ", "RNPK", + "NVNPO", "KLNPZ", "PurNP", "YANOS" +] + +# Константы для кодов строк ПМ +PM_CODES = [78, 79, 394, 395, 396, 397, 81, 82, 83, 84] + +# Константы для столбцов ПМ +PM_COLUMNS = ["БП", "ПП", "СЭБ", "Факт", "План"] + +# Константы для режимов СА +CA_MODES = ["plan", "fact", "normativ"] + +# Константы для таблиц СА +CA_TABLES = ["ТиП", "Топливо", "Потери"] + +# Константы для столбцов мониторинга топлива +FUEL_COLUMNS = ["normativ", "total", "total_1"] + +# Константы для типов ремонта +REPAIR_TYPES = ["КР", "КП", "ТР"] + +# Константы для режимов мониторинга ТЭР +TAR_MODES = ["all", "total", "last_day"] \ No newline at end of file diff --git a/streamlit_app/parsers_ui/__init__.py b/streamlit_app/parsers_ui/__init__.py new file mode 100644 index 0000000..515e575 --- /dev/null +++ b/streamlit_app/parsers_ui/__init__.py @@ -0,0 +1,3 @@ +""" +UI модули для парсеров +""" \ No newline at end of file diff --git a/streamlit_app/parsers_ui/monitoring_fuel_ui.py b/streamlit_app/parsers_ui/monitoring_fuel_ui.py new file mode 100644 index 0000000..8ee4fc1 --- /dev/null +++ b/streamlit_app/parsers_ui/monitoring_fuel_ui.py @@ -0,0 +1,91 @@ +""" +UI модуль для мониторинга топлива +""" +import streamlit as st +from api_client import upload_file_to_api, make_api_request +from config import FUEL_COLUMNS + + +def render_monitoring_fuel_tab(): + """Рендер вкладки мониторинга топлива""" + st.header("⛽ Мониторинг топлива - Полный функционал") + + # Секция загрузки файлов + st.subheader("📤 Загрузка файлов") + uploaded_fuel = st.file_uploader( + "Выберите ZIP архив с мониторингом топлива", + type=['zip'], + key="fuel_upload" + ) + + if uploaded_fuel is not None: + if st.button("📤 Загрузить мониторинг топлива", key="upload_fuel_btn"): + with st.spinner("Загружаю файл..."): + result, status = upload_file_to_api( + "/monitoring_fuel/upload-zip", + uploaded_fuel.read(), + uploaded_fuel.name + ) + + if status == 200: + st.success(f"✅ {result.get('message', 'Файл загружен')}") + st.info(f"ID объекта: {result.get('object_id', 'N/A')}") + else: + st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") + + st.markdown("---") + + # Секция получения данных + st.subheader("🔍 Получение данных") + + col1, col2 = st.columns(2) + + with col1: + st.subheader("Агрегация по колонкам") + + columns_fuel = st.multiselect( + "Выберите столбцы", + FUEL_COLUMNS, + default=["normativ", "total"], + key="fuel_columns" + ) + + if st.button("🔍 Получить агрегированные данные", key="fuel_total_btn"): + if columns_fuel: + with st.spinner("Получаю данные..."): + data = { + "columns": columns_fuel + } + + result, status = make_api_request("/monitoring_fuel/get_total_by_columns", data) + + if status == 200: + st.success("✅ Данные получены") + st.json(result) + else: + st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") + else: + st.warning("⚠️ Выберите столбцы") + + with col2: + st.subheader("Данные за месяц") + + month = st.selectbox( + "Выберите месяц", + [f"{i:02d}" for i in range(1, 13)], + key="fuel_month" + ) + + if st.button("🔍 Получить данные за месяц", key="fuel_month_btn"): + with st.spinner("Получаю данные..."): + data = { + "month": month + } + + result, status = make_api_request("/monitoring_fuel/get_month_by_code", data) + + if status == 200: + st.success("✅ Данные получены") + st.json(result) + else: + st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") \ No newline at end of file diff --git a/streamlit_app/parsers_ui/monitoring_tar_ui.py b/streamlit_app/parsers_ui/monitoring_tar_ui.py new file mode 100644 index 0000000..e9ca666 --- /dev/null +++ b/streamlit_app/parsers_ui/monitoring_tar_ui.py @@ -0,0 +1,108 @@ +""" +UI модуль для мониторинга ТЭР +""" +import streamlit as st +import pandas as pd +import json +from api_client import upload_file_to_api, make_api_request +from config import TAR_MODES + + +def render_monitoring_tar_tab(): + """Рендер вкладки мониторинга ТЭР""" + st.header("⚡ Мониторинг ТЭР (Топливно-энергетических ресурсов)") + + # Секция загрузки файлов + st.subheader("📤 Загрузка файлов") + uploaded_file = st.file_uploader( + "Выберите ZIP архив с файлами мониторинга ТЭР", + type=['zip'], + key="monitoring_tar_upload" + ) + + if uploaded_file is not None: + if st.button("📤 Загрузить файл", key="monitoring_tar_upload_btn"): + with st.spinner("Загружаем файл..."): + file_data = uploaded_file.read() + result, status_code = upload_file_to_api("/monitoring_tar/upload", file_data, uploaded_file.name) + + if status_code == 200: + st.success("✅ Файл успешно загружен!") + st.json(result) + else: + st.error(f"❌ Ошибка загрузки: {result}") + + # Секция получения данных + st.subheader("📊 Получение данных") + + # Выбор формата отображения + display_format = st.radio( + "Формат отображения:", + ["JSON", "Таблица"], + key="monitoring_tar_display_format", + horizontal=True + ) + + # Выбор режима данных + mode = st.selectbox( + "Выберите режим данных:", + TAR_MODES, + help="total - строки 'Всего' (агрегированные данные), last_day - последние строки данных, all - все данные", + key="monitoring_tar_mode" + ) + + if st.button("📊 Получить данные", key="monitoring_tar_get_data_btn"): + with st.spinner("Получаем данные..."): + # Выбираем эндпоинт в зависимости от режима + if mode == "all": + # Используем полный эндпоинт + result, status_code = make_api_request("/monitoring_tar/get_full_data", {}) + else: + # Используем фильтрованный эндпоинт + request_data = {"mode": mode} + result, status_code = make_api_request("/monitoring_tar/get_data", request_data) + + if status_code == 200 and result.get("success"): + st.success("✅ Данные успешно получены!") + + # Показываем данные + data = result.get("data", {}).get("value", {}) + if data: + st.subheader("📋 Результат:") + + # Парсим данные, если они пришли как строка + if isinstance(data, str): + try: + data = json.loads(data) + st.write("✅ JSON успешно распарсен") + except json.JSONDecodeError as e: + st.error(f"❌ Ошибка при парсинге JSON данных: {e}") + st.write("Сырые данные:", data) + return + + if display_format == "JSON": + # Отображаем как JSON + st.json(data) + else: + # Отображаем как таблицы + if isinstance(data, dict): + # Показываем данные по установкам + for installation_id, installation_data in data.items(): + with st.expander(f"🏭 {installation_id}"): + if isinstance(installation_data, dict): + # Показываем структуру данных + for data_type, type_data in installation_data.items(): + st.write(f"**{data_type}:**") + if isinstance(type_data, list) and type_data: + df = pd.DataFrame(type_data) + st.dataframe(df) + else: + st.write("Нет данных") + else: + st.write("Нет данных") + else: + st.json(data) + else: + st.info("📋 Нет данных для отображения") + else: + st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") \ No newline at end of file diff --git a/streamlit_app/parsers_ui/oper_spravka_tech_pos_ui.py b/streamlit_app/parsers_ui/oper_spravka_tech_pos_ui.py new file mode 100644 index 0000000..f007bfa --- /dev/null +++ b/streamlit_app/parsers_ui/oper_spravka_tech_pos_ui.py @@ -0,0 +1,84 @@ +""" +UI модуль для операционных справок технологических позиций +""" +import streamlit as st +import pandas as pd +from api_client import upload_file_to_api, make_api_request, get_available_ogs + + +def render_oper_spravka_tech_pos_tab(): + """Рендер вкладки операционных справок технологических позиций""" + st.header("🏭 Операционные справки технологических позиций") + + # Секция загрузки файлов + st.subheader("📤 Загрузка файлов") + + uploaded_file = st.file_uploader( + "Выберите ZIP архив с файлами операционных справок", + type=['zip'], + key="oper_spravka_tech_pos_upload" + ) + + if uploaded_file is not None: + if st.button("📤 Загрузить файл", key="oper_spravka_tech_pos_upload_btn"): + with st.spinner("Загружаем файл..."): + file_data = uploaded_file.read() + result, status_code = upload_file_to_api("/oper_spravka_tech_pos/upload", file_data, uploaded_file.name) + + if status_code == 200: + st.success("✅ Файл успешно загружен!") + st.json(result) + else: + st.error(f"❌ Ошибка загрузки: {result}") + + st.markdown("---") + + # Секция получения данных + st.subheader("📊 Получение данных") + + # Выбор формата отображения + display_format = st.radio( + "Формат отображения:", + ["JSON", "Таблица"], + key="oper_spravka_tech_pos_display_format", + horizontal=True + ) + + # Получаем доступные ОГ динамически + available_ogs = get_available_ogs("oper_spravka_tech_pos") + + # Выбор ОГ + og_id = st.selectbox( + "Выберите ОГ:", + available_ogs if available_ogs else ["SNPZ", "KNPZ", "ANHK", "BASH", "UNH", "NOV"], + key="oper_spravka_tech_pos_og_id" + ) + + if st.button("📊 Получить данные", key="oper_spravka_tech_pos_get_data_btn"): + with st.spinner("Получаем данные..."): + request_data = {"id": og_id} + result, status_code = make_api_request("/oper_spravka_tech_pos/get_data", request_data) + + if status_code == 200 and result.get("success"): + st.success("✅ Данные успешно получены!") + + # Показываем данные + data = result.get("data", []) + + if data and len(data) > 0: + st.subheader("📋 Результат:") + + if display_format == "JSON": + # Отображаем как JSON + st.json(data) + else: + # Отображаем как таблицу + if isinstance(data, list) and data: + df = pd.DataFrame(data) + st.dataframe(df, use_container_width=True) + else: + st.write("Нет данных") + else: + st.info("📋 Нет данных для отображения") + else: + st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") \ No newline at end of file diff --git a/streamlit_app/parsers_ui/statuses_repair_ca_ui.py b/streamlit_app/parsers_ui/statuses_repair_ca_ui.py new file mode 100644 index 0000000..ac2f680 --- /dev/null +++ b/streamlit_app/parsers_ui/statuses_repair_ca_ui.py @@ -0,0 +1,146 @@ +""" +UI модуль для статусов ремонта СА +""" +import streamlit as st +import pandas as pd +from api_client import upload_file_to_api, make_api_request, get_available_ogs + + +def render_statuses_repair_ca_tab(): + """Рендер вкладки статусов ремонта СА""" + st.header("📋 Статусы ремонта СА") + + # Секция загрузки файлов + st.subheader("📤 Загрузка файлов") + uploaded_file = st.file_uploader( + "Выберите файл статусов ремонта СА", + type=['xlsx', 'xlsm', 'xls', 'zip'], + key="statuses_repair_ca_upload" + ) + + if uploaded_file is not None: + if st.button("📤 Загрузить файл", key="statuses_repair_ca_upload_btn"): + with st.spinner("Загружаем файл..."): + file_data = uploaded_file.read() + result, status_code = upload_file_to_api("/statuses_repair_ca/upload", file_data, uploaded_file.name) + + if status_code == 200: + st.success("✅ Файл успешно загружен!") + st.json(result) + else: + st.error(f"❌ Ошибка загрузки: {result}") + + # Секция получения данных + st.subheader("📊 Получение данных") + + # Получаем доступные ОГ динамически + available_ogs = get_available_ogs("statuses_repair_ca") + + # Фильтр по ОГ + og_ids = st.multiselect( + "Выберите ОГ (оставьте пустым для всех)", + available_ogs if available_ogs else ["KNPZ", "ANHK", "SNPZ", "BASH", "UNH", "NOV"], # fallback + key="statuses_repair_ca_og_ids" + ) + + # Предустановленные ключи для извлечения + st.subheader("🔑 Ключи для извлечения данных") + + # Основные ключи + include_basic_keys = st.checkbox("Основные данные", value=True, key="statuses_basic_keys") + include_readiness_keys = st.checkbox("Готовность к КР", value=True, key="statuses_readiness_keys") + include_contract_keys = st.checkbox("Заключение договоров", value=True, key="statuses_contract_keys") + include_supply_keys = st.checkbox("Поставка МТР", value=True, key="statuses_supply_keys") + + # Формируем ключи на основе выбора + keys = [] + if include_basic_keys: + keys.append(["Дата начала ремонта"]) + keys.append(["Отставание / опережение подготовки к КР", "Отставание / опережение"]) + keys.append(["Отставание / опережение подготовки к КР", "Динамика за прошедшую неделю"]) + + if include_readiness_keys: + keys.append(["Готовность к КР", "Факт"]) + + if include_contract_keys: + keys.append(["Заключение договоров на СМР", "Договор", "%"]) + + if include_supply_keys: + keys.append(["Поставка МТР", "На складе, позиций", "%"]) + + # Кнопка получения данных + if st.button("📊 Получить данные", key="statuses_repair_ca_get_data_btn"): + if not keys: + st.warning("⚠️ Выберите хотя бы одну группу ключей для извлечения") + else: + with st.spinner("Получаем данные..."): + request_data = { + "ids": og_ids if og_ids else None, + "keys": keys + } + + result, status_code = make_api_request("/statuses_repair_ca/get_data", request_data) + + if status_code == 200 and result.get("success"): + st.success("✅ Данные успешно получены!") + + data = result.get("data", {}).get("value", []) + if data: + # Отображаем данные в виде таблицы + if isinstance(data, list) and len(data) > 0: + # Преобразуем в DataFrame для лучшего отображения + df_data = [] + for item in data: + row = { + "ID": item.get("id", ""), + "Название": item.get("name", ""), + } + + # Добавляем основные поля + if "Дата начала ремонта" in item: + row["Дата начала ремонта"] = item["Дата начала ремонта"] + + # Добавляем готовность к КР + if "Готовность к КР" in item: + readiness = item["Готовность к КР"] + if isinstance(readiness, dict) and "Факт" in readiness: + row["Готовность к КР (Факт)"] = readiness["Факт"] + + # Добавляем отставание/опережение + if "Отставание / опережение подготовки к КР" in item: + delay = item["Отставание / опережение подготовки к КР"] + if isinstance(delay, dict): + if "Отставание / опережение" in delay: + row["Отставание/опережение"] = delay["Отставание / опережение"] + if "Динамика за прошедшую неделю" in delay: + row["Динамика за неделю"] = delay["Динамика за прошедшую неделю"] + + # Добавляем договоры + if "Заключение договоров на СМР" in item: + contracts = item["Заключение договоров на СМР"] + if isinstance(contracts, dict) and "Договор" in contracts: + contract = contracts["Договор"] + if isinstance(contract, dict) and "%" in contract: + row["Договоры (%)"] = contract["%"] + + # Добавляем поставки МТР + if "Поставка МТР" in item: + supply = item["Поставка МТР"] + if isinstance(supply, dict) and "На складе, позиций" in supply: + warehouse = supply["На складе, позиций"] + if isinstance(warehouse, dict) and "%" in warehouse: + row["МТР на складе (%)"] = warehouse["%"] + + df_data.append(row) + + if df_data: + df = pd.DataFrame(df_data) + st.dataframe(df, use_container_width=True) + else: + st.info("📋 Нет данных для отображения") + else: + st.json(result) + else: + st.info("📋 Нет данных для отображения") + else: + st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") \ No newline at end of file diff --git a/streamlit_app/parsers_ui/svodka_ca_ui.py b/streamlit_app/parsers_ui/svodka_ca_ui.py new file mode 100644 index 0000000..c451362 --- /dev/null +++ b/streamlit_app/parsers_ui/svodka_ca_ui.py @@ -0,0 +1,80 @@ +""" +UI модуль для парсера сводок СА +""" +import streamlit as st +import requests +from api_client import make_api_request, API_BASE_URL +from config import CA_MODES, CA_TABLES + + +def render_svodka_ca_tab(): + """Рендер вкладки сводок СА""" + st.header("🏭 Сводки СА - Полный функционал") + + # Секция загрузки файлов + st.subheader("📤 Загрузка файлов") + uploaded_ca = st.file_uploader( + "Выберите Excel файл сводки СА", + type=['xlsx', 'xlsm', 'xls'], + key="ca_upload" + ) + + if uploaded_ca is not None: + if st.button("📤 Загрузить сводку СА", key="upload_ca_btn"): + with st.spinner("Загружаю файл..."): + try: + files = {"file": (uploaded_ca.name, uploaded_ca.read(), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")} + response = requests.post(f"{API_BASE_URL}/svodka_ca/upload", files=files) + result = response.json() + + if response.status_code == 200: + st.success(f"✅ {result.get('message', 'Файл загружен')}") + st.info(f"ID объекта: {result.get('object_id', 'N/A')}") + else: + st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") + except Exception as e: + st.error(f"❌ Ошибка: {str(e)}") + + st.markdown("---") + + # Секция получения данных + st.subheader("🔍 Получение данных") + + col1, col2 = st.columns(2) + + with col1: + st.subheader("Параметры запроса") + + modes = st.multiselect( + "Выберите режимы", + CA_MODES, + default=["plan", "fact"], + key="ca_modes" + ) + + tables = st.multiselect( + "Выберите таблицы", + CA_TABLES, + default=["ТиП", "Топливо"], + key="ca_tables" + ) + + with col2: + st.subheader("Результат") + if st.button("🔍 Получить данные СА", key="ca_btn"): + if modes and tables: + with st.spinner("Получаю данные..."): + data = { + "modes": modes, + "tables": tables + } + + result, status = make_api_request("/svodka_ca/get_data", data) + + if status == 200: + st.success("✅ Данные получены") + st.json(result) + else: + st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") + else: + st.warning("⚠️ Выберите режимы и таблицы") \ No newline at end of file diff --git a/streamlit_app/parsers_ui/svodka_pm_ui.py b/streamlit_app/parsers_ui/svodka_pm_ui.py new file mode 100644 index 0000000..df0007c --- /dev/null +++ b/streamlit_app/parsers_ui/svodka_pm_ui.py @@ -0,0 +1,118 @@ +""" +UI модуль для парсера сводок ПМ +""" +import streamlit as st +from api_client import upload_file_to_api, make_api_request +from config import PM_CODES, PM_COLUMNS, DEFAULT_OGS + + +def render_svodka_pm_tab(): + """Рендер вкладки сводок ПМ""" + st.header("📊 Сводки ПМ - Полный функционал") + + # Секция загрузки файлов + st.subheader("📤 Загрузка файлов") + uploaded_pm = st.file_uploader( + "Выберите ZIP архив со сводками ПМ", + type=['zip'], + key="pm_upload" + ) + + if uploaded_pm is not None: + if st.button("📤 Загрузить сводки ПМ", key="upload_pm_btn"): + with st.spinner("Загружаю файл..."): + result, status = upload_file_to_api( + "/svodka_pm/upload-zip", + uploaded_pm.read(), + uploaded_pm.name + ) + + if status == 200: + st.success(f"✅ {result.get('message', 'Файл загружен')}") + st.info(f"ID объекта: {result.get('object_id', 'N/A')}") + else: + st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") + + st.markdown("---") + + # Секция получения данных + st.subheader("🔍 Получение данных") + + col1, col2 = st.columns(2) + + with col1: + st.subheader("Данные по одному ОГ") + + og_id = st.selectbox( + "Выберите ОГ", + DEFAULT_OGS, + key="pm_single_og" + ) + + codes = st.multiselect( + "Выберите коды строк", + PM_CODES, + default=[78, 79], + key="pm_single_codes" + ) + + columns = st.multiselect( + "Выберите столбцы", + PM_COLUMNS, + default=["БП", "ПП"], + key="pm_single_columns" + ) + + if st.button("🔍 Получить данные по ОГ", key="pm_single_btn"): + if codes and columns: + with st.spinner("Получаю данные..."): + data = { + "id": og_id, + "codes": codes, + "columns": columns + } + + result, status = make_api_request("/svodka_pm/get_single_og", data) + + if status == 200: + st.success("✅ Данные получены") + st.json(result) + else: + st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") + else: + st.warning("⚠️ Выберите коды и столбцы") + + with col2: + st.subheader("Данные по всем ОГ") + + codes_total = st.multiselect( + "Выберите коды строк", + PM_CODES, + default=[78, 79, 394, 395], + key="pm_total_codes" + ) + + columns_total = st.multiselect( + "Выберите столбцы", + PM_COLUMNS, + default=["БП", "ПП", "СЭБ"], + key="pm_total_columns" + ) + + if st.button("🔍 Получить данные по всем ОГ", key="pm_total_btn"): + if codes_total and columns_total: + with st.spinner("Получаю данные..."): + data = { + "codes": codes_total, + "columns": columns_total + } + + result, status = make_api_request("/svodka_pm/get_total_ogs", data) + + if status == 200: + st.success("✅ Данные получены") + st.json(result) + else: + st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") + else: + st.warning("⚠️ Выберите коды и столбцы") \ No newline at end of file diff --git a/streamlit_app/parsers_ui/svodka_repair_ca_ui.py b/streamlit_app/parsers_ui/svodka_repair_ca_ui.py new file mode 100644 index 0000000..944acde --- /dev/null +++ b/streamlit_app/parsers_ui/svodka_repair_ca_ui.py @@ -0,0 +1,110 @@ +""" +UI модуль для ремонта СА +""" +import streamlit as st +import pandas as pd +from api_client import upload_file_to_api, make_api_request, get_available_ogs +from config import REPAIR_TYPES + + +def render_svodka_repair_ca_tab(): + """Рендер вкладки ремонта СА""" + st.header("🔧 Ремонт СА - Управление ремонтными работами") + + # Секция загрузки файлов + st.subheader("📤 Загрузка файлов") + + uploaded_file = st.file_uploader( + "Выберите Excel файл или ZIP архив с данными о ремонте СА", + type=['xlsx', 'xlsm', 'xls', 'zip'], + key="repair_ca_upload" + ) + + if uploaded_file is not None: + if st.button("📤 Загрузить файл", key="repair_ca_upload_btn"): + with st.spinner("Загружаю файл..."): + file_data = uploaded_file.read() + result, status = upload_file_to_api("/svodka_repair_ca/upload", file_data, uploaded_file.name) + + if status == 200: + st.success("✅ Файл успешно загружен") + st.json(result) + else: + st.error(f"❌ Ошибка загрузки: {result.get('message', 'Неизвестная ошибка')}") + + st.markdown("---") + + # Секция получения данных + st.subheader("🔍 Получение данных") + + col1, col2 = st.columns(2) + + with col1: + st.subheader("Фильтры") + + # Получаем доступные ОГ динамически + available_ogs = get_available_ogs("svodka_repair_ca") + + # Фильтр по ОГ + og_ids = st.multiselect( + "Выберите ОГ (оставьте пустым для всех)", + available_ogs if available_ogs else ["KNPZ", "ANHK", "SNPZ", "BASH", "UNH", "NOV"], # fallback + key="repair_ca_og_ids" + ) + + # Фильтр по типам ремонта + repair_types = st.multiselect( + "Выберите типы ремонта (оставьте пустым для всех)", + REPAIR_TYPES, + key="repair_ca_types" + ) + + # Включение плановых/фактических данных + include_planned = st.checkbox("Включать плановые данные", value=True, key="repair_ca_planned") + include_factual = st.checkbox("Включать фактические данные", value=True, key="repair_ca_factual") + + with col2: + st.subheader("Действия") + + if st.button("🔍 Получить данные о ремонте", key="repair_ca_get_btn"): + with st.spinner("Получаю данные..."): + data = { + "include_planned": include_planned, + "include_factual": include_factual + } + + # Добавляем фильтры только если они выбраны + if og_ids: + data["og_ids"] = og_ids + if repair_types: + data["repair_types"] = repair_types + + result, status = make_api_request("/svodka_repair_ca/get_data", data) + + if status == 200: + st.success("✅ Данные получены") + + # Отображаем данные в виде таблицы, если возможно + if result.get("data") and isinstance(result["data"], list): + df_data = [] + for item in result["data"]: + df_data.append({ + "ID ОГ": item.get("id", ""), + "Наименование": item.get("name", ""), + "Тип ремонта": item.get("type", ""), + "Дата начала": item.get("start_date", ""), + "Дата окончания": item.get("end_date", ""), + "План": item.get("plan", ""), + "Факт": item.get("fact", ""), + "Простой": item.get("downtime", "") + }) + + if df_data: + df = pd.DataFrame(df_data) + st.dataframe(df, use_container_width=True) + else: + st.info("📋 Нет данных для отображения") + else: + st.json(result) + else: + st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") \ No newline at end of file diff --git a/streamlit_app/sidebar.py b/streamlit_app/sidebar.py new file mode 100644 index 0000000..557e7e9 --- /dev/null +++ b/streamlit_app/sidebar.py @@ -0,0 +1,53 @@ +""" +Модуль для сайдбара +""" +import streamlit as st +from api_client import get_server_info, get_available_parsers, API_PUBLIC_URL + + +def render_sidebar(): + """Рендер боковой панели""" + with st.sidebar: + st.header("ℹ️ Информация") + + # Информация о сервере + server_info = get_server_info() + if server_info: + st.subheader("Сервер") + st.write(f"PID: {server_info.get('process_id', 'N/A')}") + st.write(f"CPU ядер: {server_info.get('cpu_cores', 'N/A')}") + st.write(f"Память: {server_info.get('memory_mb', 'N/A'):.1f} MB") + + # Доступные парсеры + parsers = get_available_parsers() + if parsers: + st.subheader("Доступные парсеры") + for parser in parsers: + st.write(f"• {parser}") + + +def render_footer(): + """Рендер футера""" + st.markdown("---") + st.markdown("### 📚 Документация API") + st.markdown(f"Полная документация доступна по адресу: {API_PUBLIC_URL}/docs") + + # Информация о проекте + with st.expander("ℹ️ О проекте"): + st.markdown(""" + **NIN Excel Parsers API** - это веб-сервис для парсинга и обработки Excel-файлов нефтеперерабатывающих заводов. + + **Возможности:** + - 📊 Парсинг сводок ПМ (план и факт) + - 🏭 Парсинг сводок СА + - ⛽ Мониторинг топлива + - ⚡ Мониторинг ТЭР (Топливно-энергетические ресурсы) + - 🔧 Управление ремонтными работами СА + - 📋 Мониторинг статусов ремонта СА + + **Технологии:** + - FastAPI + - Pandas + - MinIO (S3-совместимое хранилище) + - Streamlit (веб-интерфейс) + """) \ No newline at end of file diff --git a/streamlit_app/streamlit_app.py b/streamlit_app/streamlit_app.py index 22513ab..103e878 100644 --- a/streamlit_app/streamlit_app.py +++ b/streamlit_app/streamlit_app.py @@ -1,87 +1,17 @@ import streamlit as st -import requests -import json -import pandas as pd -import io -import zipfile -from typing import Dict, Any, List -import os +from config import setup_page_config, PARSER_TABS, API_PUBLIC_URL +from api_client import check_api_health +from sidebar import render_sidebar, render_footer +from parsers_ui.svodka_pm_ui import render_svodka_pm_tab +from parsers_ui.svodka_ca_ui import render_svodka_ca_tab +from parsers_ui.monitoring_fuel_ui import render_monitoring_fuel_tab +from parsers_ui.svodka_repair_ca_ui import render_svodka_repair_ca_tab +from parsers_ui.statuses_repair_ca_ui import render_statuses_repair_ca_tab +from parsers_ui.monitoring_tar_ui import render_monitoring_tar_tab +from parsers_ui.oper_spravka_tech_pos_ui import render_oper_spravka_tech_pos_tab # Конфигурация страницы -st.set_page_config( - page_title="NIN Excel Parsers API Demo", - page_icon="📊", - layout="wide", - initial_sidebar_state="expanded" -) - -# Конфигурация API -API_BASE_URL = os.getenv("API_BASE_URL", "http://fastapi:8000") # Внутренний адрес для Docker -API_PUBLIC_URL = os.getenv("API_PUBLIC_URL", "http://localhost:8000") # Внешний адрес для пользователя - -def check_api_health(): - """Проверка доступности API""" - try: - response = requests.get(f"{API_BASE_URL}/", timeout=5) - return response.status_code == 200 - except: - return False - -def get_available_parsers(): - """Получение списка доступных парсеров""" - try: - response = requests.get(f"{API_BASE_URL}/parsers") - if response.status_code == 200: - return response.json()["parsers"] - return [] - except: - return [] - -def get_server_info(): - """Получение информации о сервере""" - try: - response = requests.get(f"{API_BASE_URL}/server-info") - if response.status_code == 200: - return response.json() - return {} - except: - return {} - -def upload_file_to_api(endpoint: str, file_data: bytes, filename: str): - """Загрузка файла на API""" - try: - # Определяем правильное имя поля в зависимости от эндпоинта - if "zip" in endpoint: - files = {"zip_file": (filename, file_data, "application/zip")} - else: - files = {"file": (filename, file_data, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")} - - response = requests.post(f"{API_BASE_URL}{endpoint}", files=files) - return response.json(), response.status_code - except Exception as e: - return {"error": str(e)}, 500 - -def make_api_request(endpoint: str, data: Dict[str, Any]): - """Выполнение API запроса""" - try: - response = requests.post(f"{API_BASE_URL}{endpoint}", json=data) - return response.json(), response.status_code - except Exception as e: - return {"error": str(e)}, 500 - -def get_available_ogs(parser_name: str) -> List[str]: - """Получение доступных ОГ для парсера""" - try: - response = requests.get(f"{API_BASE_URL}/parsers/{parser_name}/available_ogs") - if response.status_code == 200: - data = response.json() - return data.get("available_ogs", []) - else: - print(f"⚠️ Ошибка получения ОГ: {response.status_code}") - return [] - except Exception as e: - print(f"⚠️ Ошибка при запросе ОГ: {e}") - return [] +setup_page_config() def main(): st.title("🚀 NIN Excel Parsers API - Демонстрация") @@ -89,759 +19,48 @@ def main(): # Проверка доступности API if not check_api_health(): - st.error(f"❌ API недоступен по адресу {API_BASE_URL}") + st.error(f"❌ API недоступен по адресу {API_PUBLIC_URL}") st.info("Убедитесь, что FastAPI сервер запущен") return st.success(f"✅ API доступен по адресу {API_PUBLIC_URL}") # Боковая панель с информацией - with st.sidebar: - st.header("ℹ️ Информация") - - # Информация о сервере - server_info = get_server_info() - if server_info: - st.subheader("Сервер") - st.write(f"PID: {server_info.get('process_id', 'N/A')}") - st.write(f"CPU ядер: {server_info.get('cpu_cores', 'N/A')}") - st.write(f"Память: {server_info.get('memory_mb', 'N/A'):.1f} MB") - - # Доступные парсеры - parsers = get_available_parsers() - if parsers: - st.subheader("Доступные парсеры") - for parser in parsers: - st.write(f"• {parser}") + render_sidebar() # Основные вкладки - по одной на каждый парсер - tab1, tab2, tab3, tab4, tab5, tab6, tab7 = st.tabs([ - "📊 Сводки ПМ", - "🏭 Сводки СА", - "⛽ Мониторинг топлива", - "🔧 Ремонт СА", - "📋 Статусы ремонта СА", - "⚡ Мониторинг ТЭР", - "🏭 Операционные справки" - ]) + tab1, tab2, tab3, tab4, tab5, tab6, tab7 = st.tabs(PARSER_TABS) # Вкладка 1: Сводки ПМ - полный функционал with tab1: - st.header("📊 Сводки ПМ - Полный функционал") - - # Секция загрузки файлов - st.subheader("📤 Загрузка файлов") - uploaded_pm = st.file_uploader( - "Выберите ZIP архив со сводками ПМ", - type=['zip'], - key="pm_upload" - ) - - if uploaded_pm is not None: - if st.button("📤 Загрузить сводки ПМ", key="upload_pm_btn"): - with st.spinner("Загружаю файл..."): - result, status = upload_file_to_api( - "/svodka_pm/upload-zip", - uploaded_pm.read(), - uploaded_pm.name - ) - - if status == 200: - st.success(f"✅ {result.get('message', 'Файл загружен')}") - st.info(f"ID объекта: {result.get('object_id', 'N/A')}") - else: - st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") - - st.markdown("---") - - # Секция получения данных - st.subheader("🔍 Получение данных") - - col1, col2 = st.columns(2) - - with col1: - st.subheader("Данные по одному ОГ") - - og_id = st.selectbox( - "Выберите ОГ", - ["SNPZ", "KNPZ", "ANHK", "AchNPZ", "UNPZ", "UNH", "NOV", - "NovKuybNPZ", "KuybNPZ", "CyzNPZ", "TuapsNPZ", "RNPK", - "NVNPO", "KLNPZ", "PurNP", "YANOS"], - key="pm_single_og" - ) - - codes = st.multiselect( - "Выберите коды строк", - [78, 79, 394, 395, 396, 397, 81, 82, 83, 84], - default=[78, 79], - key="pm_single_codes" - ) - - columns = st.multiselect( - "Выберите столбцы", - ["БП", "ПП", "СЭБ", "Факт", "План"], - default=["БП", "ПП"], - key="pm_single_columns" - ) - - if st.button("🔍 Получить данные по ОГ", key="pm_single_btn"): - if codes and columns: - with st.spinner("Получаю данные..."): - data = { - "id": og_id, - "codes": codes, - "columns": columns - } - - result, status = make_api_request("/svodka_pm/get_single_og", data) - - if status == 200: - st.success("✅ Данные получены") - st.json(result) - else: - st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") - else: - st.warning("⚠️ Выберите коды и столбцы") - - with col2: - st.subheader("Данные по всем ОГ") - - codes_total = st.multiselect( - "Выберите коды строк", - [78, 79, 394, 395, 396, 397, 81, 82, 83, 84], - default=[78, 79, 394, 395], - key="pm_total_codes" - ) - - columns_total = st.multiselect( - "Выберите столбцы", - ["БП", "ПП", "СЭБ", "Факт", "План"], - default=["БП", "ПП", "СЭБ"], - key="pm_total_columns" - ) - - if st.button("🔍 Получить данные по всем ОГ", key="pm_total_btn"): - if codes_total and columns_total: - with st.spinner("Получаю данные..."): - data = { - "codes": codes_total, - "columns": columns_total - } - - result, status = make_api_request("/svodka_pm/get_total_ogs", data) - - if status == 200: - st.success("✅ Данные получены") - st.json(result) - else: - st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") - else: - st.warning("⚠️ Выберите коды и столбцы") + render_svodka_pm_tab() # Вкладка 2: Сводки СА - полный функционал with tab2: - st.header("🏭 Сводки СА - Полный функционал") - - # Секция загрузки файлов - st.subheader("📤 Загрузка файлов") - uploaded_ca = st.file_uploader( - "Выберите Excel файл сводки СА", - type=['xlsx', 'xlsm', 'xls'], - key="ca_upload" - ) - - if uploaded_ca is not None: - if st.button("📤 Загрузить сводку СА", key="upload_ca_btn"): - with st.spinner("Загружаю файл..."): - try: - files = {"file": (uploaded_ca.name, uploaded_ca.read(), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")} - response = requests.post(f"{API_BASE_URL}/svodka_ca/upload", files=files) - result = response.json() - - if response.status_code == 200: - st.success(f"✅ {result.get('message', 'Файл загружен')}") - st.info(f"ID объекта: {result.get('object_id', 'N/A')}") - else: - st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") - except Exception as e: - st.error(f"❌ Ошибка: {str(e)}") - - st.markdown("---") - - # Секция получения данных - st.subheader("🔍 Получение данных") - - col1, col2 = st.columns(2) - - with col1: - st.subheader("Параметры запроса") - - modes = st.multiselect( - "Выберите режимы", - ["plan", "fact", "normativ"], - default=["plan", "fact"], - key="ca_modes" - ) - - tables = st.multiselect( - "Выберите таблицы", - ["ТиП", "Топливо", "Потери"], - default=["ТиП", "Топливо"], - key="ca_tables" - ) - - with col2: - st.subheader("Результат") - if st.button("🔍 Получить данные СА", key="ca_btn"): - if modes and tables: - with st.spinner("Получаю данные..."): - data = { - "modes": modes, - "tables": tables - } - - result, status = make_api_request("/svodka_ca/get_data", data) - - if status == 200: - st.success("✅ Данные получены") - st.json(result) - else: - st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") - else: - st.warning("⚠️ Выберите режимы и таблицы") + render_svodka_ca_tab() # Вкладка 3: Мониторинг топлива - полный функционал with tab3: - st.header("⛽ Мониторинг топлива - Полный функционал") - - # Секция загрузки файлов - st.subheader("📤 Загрузка файлов") - uploaded_fuel = st.file_uploader( - "Выберите ZIP архив с мониторингом топлива", - type=['zip'], - key="fuel_upload" - ) - - if uploaded_fuel is not None: - if st.button("📤 Загрузить мониторинг топлива", key="upload_fuel_btn"): - with st.spinner("Загружаю файл..."): - result, status = upload_file_to_api( - "/monitoring_fuel/upload-zip", - uploaded_fuel.read(), - uploaded_fuel.name - ) - - if status == 200: - st.success(f"✅ {result.get('message', 'Файл загружен')}") - st.info(f"ID объекта: {result.get('object_id', 'N/A')}") - else: - st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") - - st.markdown("---") - - # Секция получения данных - st.subheader("🔍 Получение данных") - - col1, col2 = st.columns(2) - - with col1: - st.subheader("Агрегация по колонкам") - - columns_fuel = st.multiselect( - "Выберите столбцы", - ["normativ", "total", "total_1"], - default=["normativ", "total"], - key="fuel_columns" - ) - - if st.button("🔍 Получить агрегированные данные", key="fuel_total_btn"): - if columns_fuel: - with st.spinner("Получаю данные..."): - data = { - "columns": columns_fuel - } - - result, status = make_api_request("/monitoring_fuel/get_total_by_columns", data) - - if status == 200: - st.success("✅ Данные получены") - st.json(result) - else: - st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") - else: - st.warning("⚠️ Выберите столбцы") - - with col2: - st.subheader("Данные за месяц") - - month = st.selectbox( - "Выберите месяц", - [f"{i:02d}" for i in range(1, 13)], - key="fuel_month" - ) - - if st.button("🔍 Получить данные за месяц", key="fuel_month_btn"): - with st.spinner("Получаю данные..."): - data = { - "month": month - } - - result, status = make_api_request("/monitoring_fuel/get_month_by_code", data) - - if status == 200: - st.success("✅ Данные получены") - st.json(result) - else: - st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") + render_monitoring_fuel_tab() # Вкладка 4: Ремонт СА with tab4: - st.header("🔧 Ремонт СА - Управление ремонтными работами") - - # Секция загрузки файлов - st.subheader("📤 Загрузка файлов") - - uploaded_file = st.file_uploader( - "Выберите Excel файл или ZIP архив с данными о ремонте СА", - type=['xlsx', 'xlsm', 'xls', 'zip'], - key="repair_ca_upload" - ) - - if uploaded_file is not None: - if st.button("📤 Загрузить файл", key="repair_ca_upload_btn"): - with st.spinner("Загружаю файл..."): - file_data = uploaded_file.read() - result, status = upload_file_to_api("/svodka_repair_ca/upload", file_data, uploaded_file.name) - - if status == 200: - st.success("✅ Файл успешно загружен") - st.json(result) - else: - st.error(f"❌ Ошибка загрузки: {result.get('message', 'Неизвестная ошибка')}") - - st.markdown("---") - - # Секция получения данных - st.subheader("🔍 Получение данных") - - col1, col2 = st.columns(2) - - with col1: - st.subheader("Фильтры") - - # Получаем доступные ОГ динамически - available_ogs = get_available_ogs("svodka_repair_ca") - - # Фильтр по ОГ - og_ids = st.multiselect( - "Выберите ОГ (оставьте пустым для всех)", - available_ogs if available_ogs else ["KNPZ", "ANHK", "SNPZ", "BASH", "UNH", "NOV"], # fallback - key="repair_ca_og_ids" - ) - - # Фильтр по типам ремонта - repair_types = st.multiselect( - "Выберите типы ремонта (оставьте пустым для всех)", - ["КР", "КП", "ТР"], - key="repair_ca_types" - ) - - # Включение плановых/фактических данных - include_planned = st.checkbox("Включать плановые данные", value=True, key="repair_ca_planned") - include_factual = st.checkbox("Включать фактические данные", value=True, key="repair_ca_factual") - - with col2: - st.subheader("Действия") - - if st.button("🔍 Получить данные о ремонте", key="repair_ca_get_btn"): - with st.spinner("Получаю данные..."): - data = { - "include_planned": include_planned, - "include_factual": include_factual - } - - # Добавляем фильтры только если они выбраны - if og_ids: - data["og_ids"] = og_ids - if repair_types: - data["repair_types"] = repair_types - - result, status = make_api_request("/svodka_repair_ca/get_data", data) - - if status == 200: - st.success("✅ Данные получены") - - # Отображаем данные в виде таблицы, если возможно - if result.get("data") and isinstance(result["data"], list): - df_data = [] - for item in result["data"]: - df_data.append({ - "ID ОГ": item.get("id", ""), - "Наименование": item.get("name", ""), - "Тип ремонта": item.get("type", ""), - "Дата начала": item.get("start_date", ""), - "Дата окончания": item.get("end_date", ""), - "План": item.get("plan", ""), - "Факт": item.get("fact", ""), - "Простой": item.get("downtime", "") - }) - - if df_data: - df = pd.DataFrame(df_data) - st.dataframe(df, use_container_width=True) - else: - st.info("📋 Нет данных для отображения") - else: - st.json(result) - else: - st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") + render_svodka_repair_ca_tab() # Вкладка 5: Статусы ремонта СА with tab5: - st.header("📋 Статусы ремонта СА") - - # Секция загрузки файлов - st.subheader("📤 Загрузка файлов") - uploaded_file = st.file_uploader( - "Выберите файл статусов ремонта СА", - type=['xlsx', 'xlsm', 'xls', 'zip'], - key="statuses_repair_ca_upload" - ) - - if uploaded_file is not None: - if st.button("📤 Загрузить файл", key="statuses_repair_ca_upload_btn"): - with st.spinner("Загружаем файл..."): - file_data = uploaded_file.read() - result, status_code = upload_file_to_api("/statuses_repair_ca/upload", file_data, uploaded_file.name) - - if status_code == 200: - st.success("✅ Файл успешно загружен!") - st.json(result) - else: - st.error(f"❌ Ошибка загрузки: {result}") - - # Секция получения данных - st.subheader("📊 Получение данных") - - # Получаем доступные ОГ динамически - available_ogs = get_available_ogs("statuses_repair_ca") - - # Фильтр по ОГ - og_ids = st.multiselect( - "Выберите ОГ (оставьте пустым для всех)", - available_ogs if available_ogs else ["KNPZ", "ANHK", "SNPZ", "BASH", "UNH", "NOV"], # fallback - key="statuses_repair_ca_og_ids" - ) - - # Предустановленные ключи для извлечения - st.subheader("🔑 Ключи для извлечения данных") - - # Основные ключи - include_basic_keys = st.checkbox("Основные данные", value=True, key="statuses_basic_keys") - include_readiness_keys = st.checkbox("Готовность к КР", value=True, key="statuses_readiness_keys") - include_contract_keys = st.checkbox("Заключение договоров", value=True, key="statuses_contract_keys") - include_supply_keys = st.checkbox("Поставка МТР", value=True, key="statuses_supply_keys") - - # Формируем ключи на основе выбора - keys = [] - if include_basic_keys: - keys.append(["Дата начала ремонта"]) - keys.append(["Отставание / опережение подготовки к КР", "Отставание / опережение"]) - keys.append(["Отставание / опережение подготовки к КР", "Динамика за прошедшую неделю"]) - - if include_readiness_keys: - keys.append(["Готовность к КР", "Факт"]) - - if include_contract_keys: - keys.append(["Заключение договоров на СМР", "Договор", "%"]) - - if include_supply_keys: - keys.append(["Поставка МТР", "На складе, позиций", "%"]) - - # Кнопка получения данных - if st.button("📊 Получить данные", key="statuses_repair_ca_get_data_btn"): - if not keys: - st.warning("⚠️ Выберите хотя бы одну группу ключей для извлечения") - else: - with st.spinner("Получаем данные..."): - request_data = { - "ids": og_ids if og_ids else None, - "keys": keys - } - - result, status_code = make_api_request("/statuses_repair_ca/get_data", request_data) - - if status_code == 200 and result.get("success"): - st.success("✅ Данные успешно получены!") - - data = result.get("data", {}).get("value", []) - if data: - # Отображаем данные в виде таблицы - if isinstance(data, list) and len(data) > 0: - # Преобразуем в DataFrame для лучшего отображения - df_data = [] - for item in data: - row = { - "ID": item.get("id", ""), - "Название": item.get("name", ""), - } - - # Добавляем основные поля - if "Дата начала ремонта" in item: - row["Дата начала ремонта"] = item["Дата начала ремонта"] - - # Добавляем готовность к КР - if "Готовность к КР" in item: - readiness = item["Готовность к КР"] - if isinstance(readiness, dict) and "Факт" in readiness: - row["Готовность к КР (Факт)"] = readiness["Факт"] - - # Добавляем отставание/опережение - if "Отставание / опережение подготовки к КР" in item: - delay = item["Отставание / опережение подготовки к КР"] - if isinstance(delay, dict): - if "Отставание / опережение" in delay: - row["Отставание/опережение"] = delay["Отставание / опережение"] - if "Динамика за прошедшую неделю" in delay: - row["Динамика за неделю"] = delay["Динамика за прошедшую неделю"] - - # Добавляем договоры - if "Заключение договоров на СМР" in item: - contracts = item["Заключение договоров на СМР"] - if isinstance(contracts, dict) and "Договор" in contracts: - contract = contracts["Договор"] - if isinstance(contract, dict) and "%" in contract: - row["Договоры (%)"] = contract["%"] - - # Добавляем поставки МТР - if "Поставка МТР" in item: - supply = item["Поставка МТР"] - if isinstance(supply, dict) and "На складе, позиций" in supply: - warehouse = supply["На складе, позиций"] - if isinstance(warehouse, dict) and "%" in warehouse: - row["МТР на складе (%)"] = warehouse["%"] - - df_data.append(row) - - if df_data: - df = pd.DataFrame(df_data) - st.dataframe(df, use_container_width=True) - else: - st.info("📋 Нет данных для отображения") - else: - st.json(result) - else: - st.info("📋 Нет данных для отображения") - else: - st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") + render_statuses_repair_ca_tab() # Вкладка 6: Мониторинг ТЭР with tab6: - st.header("⚡ Мониторинг ТЭР (Топливно-энергетических ресурсов)") - - # Секция загрузки файлов - st.subheader("📤 Загрузка файлов") - uploaded_file = st.file_uploader( - "Выберите ZIP архив с файлами мониторинга ТЭР", - type=['zip'], - key="monitoring_tar_upload" - ) - - if uploaded_file is not None: - if st.button("📤 Загрузить файл", key="monitoring_tar_upload_btn"): - with st.spinner("Загружаем файл..."): - file_data = uploaded_file.read() - result, status_code = upload_file_to_api("/monitoring_tar/upload", file_data, uploaded_file.name) - - if status_code == 200: - st.success("✅ Файл успешно загружен!") - st.json(result) - else: - st.error(f"❌ Ошибка загрузки: {result}") - - # Секция получения данных - st.subheader("📊 Получение данных") - - # Выбор формата отображения - display_format = st.radio( - "Формат отображения:", - ["JSON", "Таблица"], - key="monitoring_tar_display_format", - horizontal=True - ) - - # Выбор режима данных - mode = st.selectbox( - "Выберите режим данных:", - ["all", "total", "last_day"], - help="total - строки 'Всего' (агрегированные данные), last_day - последние строки данных, all - все данные", - key="monitoring_tar_mode" - ) - - if st.button("📊 Получить данные", key="monitoring_tar_get_data_btn"): - with st.spinner("Получаем данные..."): - # Выбираем эндпоинт в зависимости от режима - if mode == "all": - # Используем полный эндпоинт - result, status_code = make_api_request("/monitoring_tar/get_full_data", {}) - else: - # Используем фильтрованный эндпоинт - request_data = {"mode": mode} - result, status_code = make_api_request("/monitoring_tar/get_data", request_data) - - if status_code == 200 and result.get("success"): - st.success("✅ Данные успешно получены!") - - # Показываем данные - data = result.get("data", {}).get("value", {}) - if data: - st.subheader("📋 Результат:") - - # # Отладочная информация - # st.write(f"🔍 Тип данных: {type(data)}") - # if isinstance(data, str): - # st.write(f"🔍 Длина строки: {len(data)}") - # st.write(f"🔍 Первые 200 символов: {data[:200]}...") - - # Парсим данные, если они пришли как строка - if isinstance(data, str): - try: - import json - data = json.loads(data) - st.write("✅ JSON успешно распарсен") - except json.JSONDecodeError as e: - st.error(f"❌ Ошибка при парсинге JSON данных: {e}") - st.write("Сырые данные:", data) - return - - if display_format == "JSON": - # Отображаем как JSON - st.json(data) - else: - # Отображаем как таблицы - if isinstance(data, dict): - # Показываем данные по установкам - for installation_id, installation_data in data.items(): - with st.expander(f"🏭 {installation_id}"): - if isinstance(installation_data, dict): - # Показываем структуру данных - for data_type, type_data in installation_data.items(): - st.write(f"**{data_type}:**") - if isinstance(type_data, list) and type_data: - df = pd.DataFrame(type_data) - st.dataframe(df) - else: - st.write("Нет данных") - else: - st.write("Нет данных") - else: - st.json(data) - else: - st.info("📋 Нет данных для отображения") - else: - st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") + render_monitoring_tar_tab() # Вкладка 7: Операционные справки технологических позиций with tab7: - st.header("🏭 Операционные справки технологических позиций") - - # Секция загрузки файлов - st.subheader("📤 Загрузка файлов") - - uploaded_file = st.file_uploader( - "Выберите ZIP архив с файлами операционных справок", - type=['zip'], - key="oper_spravka_tech_pos_upload" - ) - - if uploaded_file is not None: - if st.button("📤 Загрузить файл", key="oper_spravka_tech_pos_upload_btn"): - with st.spinner("Загружаем файл..."): - file_data = uploaded_file.read() - result, status_code = upload_file_to_api("/oper_spravka_tech_pos/upload", file_data, uploaded_file.name) - - if status_code == 200: - st.success("✅ Файл успешно загружен!") - st.json(result) - else: - st.error(f"❌ Ошибка загрузки: {result}") - - st.markdown("---") - - # Секция получения данных - st.subheader("📊 Получение данных") - - # Выбор формата отображения - display_format = st.radio( - "Формат отображения:", - ["JSON", "Таблица"], - key="oper_spravka_tech_pos_display_format", - horizontal=True - ) - - # Получаем доступные ОГ динамически - available_ogs = get_available_ogs("oper_spravka_tech_pos") - - # Выбор ОГ - og_id = st.selectbox( - "Выберите ОГ:", - available_ogs if available_ogs else ["SNPZ", "KNPZ", "ANHK", "BASH", "UNH", "NOV"], - key="oper_spravka_tech_pos_og_id" - ) - - if st.button("📊 Получить данные", key="oper_spravka_tech_pos_get_data_btn"): - with st.spinner("Получаем данные..."): - request_data = {"id": og_id} - result, status_code = make_api_request("/oper_spravka_tech_pos/get_data", request_data) - - if status_code == 200 and result.get("success"): - st.success("✅ Данные успешно получены!") - - # Показываем данные - data = result.get("data", []) - - if data and len(data) > 0: - st.subheader("📋 Результат:") - - if display_format == "JSON": - # Отображаем как JSON - st.json(data) - else: - # Отображаем как таблицу - if isinstance(data, list) and data: - df = pd.DataFrame(data) - st.dataframe(df, use_container_width=True) - else: - st.write("Нет данных") - else: - st.info("📋 Нет данных для отображения") - else: - st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") + render_oper_spravka_tech_pos_tab() # Футер - st.markdown("---") - st.markdown("### 📚 Документация API") - st.markdown(f"Полная документация доступна по адресу: {API_PUBLIC_URL}/docs") - - # Информация о проекте - with st.expander("ℹ️ О проекте"): - st.markdown(""" - **NIN Excel Parsers API** - это веб-сервис для парсинга и обработки Excel-файлов нефтеперерабатывающих заводов. - - **Возможности:** - - 📊 Парсинг сводок ПМ (план и факт) - - 🏭 Парсинг сводок СА - - ⛽ Мониторинг топлива - - ⚡ Мониторинг ТЭР (Топливно-энергетические ресурсы) - - 🔧 Управление ремонтными работами СА - - 📋 Мониторинг статусов ремонта СА - - **Технологии:** - - FastAPI - - Pandas - - MinIO (S3-совместимое хранилище) - - Streamlit (веб-интерфейс) - """) + render_footer() if __name__ == "__main__": main() \ No newline at end of file From 6a1f685ee30500ce2c299d9de72c508d46c50829 Mon Sep 17 00:00:00 2001 From: Maksim Date: Thu, 4 Sep 2025 21:44:48 +0300 Subject: [PATCH 2/5] fix errors --- streamlit_app/api_client.py | 6 +----- streamlit_app/config.py | 5 +++++ streamlit_app/sidebar.py | 3 ++- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/streamlit_app/api_client.py b/streamlit_app/api_client.py index 22b92c2..4d433c5 100644 --- a/streamlit_app/api_client.py +++ b/streamlit_app/api_client.py @@ -2,12 +2,8 @@ Модуль для работы с API """ import requests -import os from typing import Dict, Any, List, Tuple - -# Конфигурация API -API_BASE_URL = os.getenv("API_BASE_URL", "http://fastapi:8000") # Внутренний адрес для Docker -API_PUBLIC_URL = os.getenv("API_PUBLIC_URL", "http://localhost:8000") # Внешний адрес для пользователя +from config import API_BASE_URL, API_PUBLIC_URL def check_api_health() -> bool: diff --git a/streamlit_app/config.py b/streamlit_app/config.py index bb49d58..dbf2339 100644 --- a/streamlit_app/config.py +++ b/streamlit_app/config.py @@ -2,6 +2,11 @@ Конфигурация приложения """ import streamlit as st +import os + +# Конфигурация API +API_BASE_URL = os.getenv("API_BASE_URL", "http://fastapi:8000") # Внутренний адрес для Docker +API_PUBLIC_URL = os.getenv("API_PUBLIC_URL", "http://localhost:8000") # Внешний адрес для пользователя # Конфигурация страницы def setup_page_config(): diff --git a/streamlit_app/sidebar.py b/streamlit_app/sidebar.py index 557e7e9..479a6c9 100644 --- a/streamlit_app/sidebar.py +++ b/streamlit_app/sidebar.py @@ -2,7 +2,8 @@ Модуль для сайдбара """ import streamlit as st -from api_client import get_server_info, get_available_parsers, API_PUBLIC_URL +from api_client import get_server_info, get_available_parsers +from config import API_PUBLIC_URL def render_sidebar(): From 36f37ffacb19cf44cf16f8fd6c4691da3231dafa Mon Sep 17 00:00:00 2001 From: Maksim Date: Thu, 4 Sep 2025 22:42:31 +0300 Subject: [PATCH 3/5] =?UTF-8?q?=D0=A0=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=D0=B5?= =?UTF-8?q?=D1=82=20=D0=B2=D1=81=D0=B5,=20=D0=BA=D1=80=D0=BE=D0=BC=D0=B5?= =?UTF-8?q?=20=D0=BE=D1=82=D0=BE=D0=B1=D1=80=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- python_parser/app/main.py | 194 +++++++++++++++++++++++++++ python_parser/core/async_services.py | 72 ++++++++++ streamlit_app/async_upload_page.py | 146 ++++++++++++++++++++ streamlit_app/sidebar.py | 24 +++- streamlit_app/streamlit_app.py | 71 +++++----- streamlit_app/sync_parsers_page.py | 54 ++++++++ streamlit_app/tasks_page.py | 159 ++++++++++++++++++++++ 7 files changed, 681 insertions(+), 39 deletions(-) create mode 100644 python_parser/core/async_services.py create mode 100644 streamlit_app/async_upload_page.py create mode 100644 streamlit_app/sync_parsers_page.py create mode 100644 streamlit_app/tasks_page.py diff --git a/python_parser/app/main.py b/python_parser/app/main.py index 820431b..a8ace37 100644 --- a/python_parser/app/main.py +++ b/python_parser/app/main.py @@ -21,6 +21,7 @@ from adapters.parsers import SvodkaPMParser, SvodkaCAParser, MonitoringFuelParse from core.models import UploadRequest, DataRequest from core.services import ReportService, PARSERS +from core.async_services import AsyncReportService from app.schemas import ( ServerInfoResponse, @@ -55,6 +56,10 @@ def get_report_service() -> ReportService: return ReportService(storage_adapter) +def get_async_report_service() -> AsyncReportService: + return AsyncReportService(ReportService(storage_adapter)) + + tags_metadata = [ { "name": "Общее", @@ -1443,5 +1448,194 @@ async def get_oper_spravka_tech_pos_data(request: OperSpravkaTechPosRequest): raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}") +# ============================================================================ +# АСИНХРОННЫЕ ЭНДПОИНТЫ +# ============================================================================ + +@app.post("/async/svodka_pm/upload-zip", tags=[SvodkaPMParser.name], + summary="Асинхронная загрузка файлов сводок ПМ одним ZIP-архивом", + response_model=UploadResponse, + responses={ + 400: {"model": UploadErrorResponse, "description": "Неверный формат архива или файлов"}, + 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} + },) +async def async_upload_svodka_pm_zip( + zip_file: UploadFile = File(..., description="ZIP архив с Excel файлами (.zip)") +): + """Асинхронная загрузка файлов сводок ПМ (факта и плана) по всем ОГ в **одном ZIP-архиве**""" + async_service = get_async_report_service() + try: + if not zip_file.filename.lower().endswith('.zip'): + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=UploadErrorResponse( + message="Файл должен быть ZIP архивом", + error_code="INVALID_FILE_TYPE", + details={ + "expected_formats": [".zip"], + "received_format": zip_file.filename.split('.')[-1] if '.' in zip_file.filename else "unknown" + } + ).model_dump() + ) + file_content = await zip_file.read() + # Создаем запрос + request = UploadRequest( + report_type='svodka_pm', + file_content=file_content, + file_name=zip_file.filename + ) + # Загружаем отчет асинхронно + result = await async_service.upload_report_async(request) + + if result.success: + return UploadResponse( + success=True, + message=result.message, + object_id=result.object_id + ) + else: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=UploadErrorResponse( + message=result.message, + error_code="UPLOAD_FAILED" + ).model_dump() + ) + except Exception as e: + logger.error(f"Ошибка при асинхронной загрузке сводки ПМ: {str(e)}") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=UploadErrorResponse( + message=f"Внутренняя ошибка сервера: {str(e)}", + error_code="INTERNAL_ERROR" + ).model_dump() + ) + + +@app.post("/async/svodka_ca/upload", tags=[SvodkaCAParser.name], + summary="Асинхронная загрузка файла отчета сводки СА", + response_model=UploadResponse, + responses={ + 400: {"model": UploadErrorResponse, "description": "Неверный формат файла"}, + 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} + },) +async def async_upload_svodka_ca( + file: UploadFile = File(..., description="Excel файл сводки СА (.xlsx, .xlsm, .xls)") +): + """Асинхронная загрузка и обработка Excel файла отчета сводки СА""" + async_service = get_async_report_service() + try: + # Проверяем тип файла + if not file.filename.endswith(('.xlsx', '.xlsm', '.xls')): + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=UploadErrorResponse( + message="Поддерживаются только Excel файлы (.xlsx, .xlsm, .xls)", + error_code="INVALID_FILE_TYPE", + details={ + "expected_formats": [".xlsx", ".xlsm", ".xls"], + "received_format": file.filename.split('.')[-1] if '.' in file.filename else "unknown" + } + ).model_dump() + ) + + # Читаем содержимое файла + file_content = await file.read() + + # Создаем запрос + request = UploadRequest( + report_type='svodka_ca', + file_content=file_content, + file_name=file.filename + ) + + # Загружаем отчет асинхронно + result = await async_service.upload_report_async(request) + + if result.success: + return UploadResponse( + success=True, + message=result.message, + object_id=result.object_id + ) + else: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=UploadErrorResponse( + message=result.message, + error_code="UPLOAD_FAILED" + ).model_dump() + ) + except Exception as e: + logger.error(f"Ошибка при асинхронной загрузке сводки СА: {str(e)}") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=UploadErrorResponse( + message=f"Внутренняя ошибка сервера: {str(e)}", + error_code="INTERNAL_ERROR" + ).model_dump() + ) + + +@app.post("/async/monitoring_fuel/upload-zip", tags=[MonitoringFuelParser.name], + summary="Асинхронная загрузка ZIP архива с мониторингом топлива", + response_model=UploadResponse, + responses={ + 400: {"model": UploadErrorResponse, "description": "Неверный формат архива или файлов"}, + 500: {"model": UploadErrorResponse, "description": "Внутренняя ошибка сервера"} + },) +async def async_upload_monitoring_fuel_zip( + zip_file: UploadFile = File(..., description="ZIP архив с Excel файлами мониторинга топлива (.zip)") +): + """Асинхронная загрузка ZIP архива с файлами мониторинга топлива""" + async_service = get_async_report_service() + try: + if not zip_file.filename.lower().endswith('.zip'): + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=UploadErrorResponse( + message="Файл должен быть ZIP архивом", + error_code="INVALID_FILE_TYPE", + details={ + "expected_formats": [".zip"], + "received_format": zip_file.filename.split('.')[-1] if '.' in zip_file.filename else "unknown" + } + ).model_dump() + ) + file_content = await zip_file.read() + # Создаем запрос + request = UploadRequest( + report_type='monitoring_fuel', + file_content=file_content, + file_name=zip_file.filename + ) + # Загружаем отчет асинхронно + result = await async_service.upload_report_async(request) + + if result.success: + return UploadResponse( + success=True, + message=result.message, + object_id=result.object_id + ) + else: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=UploadErrorResponse( + message=result.message, + error_code="UPLOAD_FAILED" + ).model_dump() + ) + except Exception as e: + logger.error(f"Ошибка при асинхронной загрузке мониторинга топлива: {str(e)}") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=UploadErrorResponse( + message=f"Внутренняя ошибка сервера: {str(e)}", + error_code="INTERNAL_ERROR" + ).model_dump() + ) + + if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8080) diff --git a/python_parser/core/async_services.py b/python_parser/core/async_services.py new file mode 100644 index 0000000..627a48f --- /dev/null +++ b/python_parser/core/async_services.py @@ -0,0 +1,72 @@ +""" +Асинхронные сервисы для работы с отчетами +""" +import asyncio +import tempfile +import os +import logging +from concurrent.futures import ThreadPoolExecutor +from typing import Optional + +from .services import ReportService +from .models import UploadRequest, UploadResult, DataRequest, DataResult +from .ports import StoragePort + +logger = logging.getLogger(__name__) + + +class AsyncReportService: + """Асинхронный сервис для работы с отчетами""" + + def __init__(self, report_service: ReportService): + self.report_service = report_service + self.executor = ThreadPoolExecutor(max_workers=4) + + async def upload_report_async(self, request: UploadRequest) -> UploadResult: + """Асинхронная загрузка отчета""" + try: + # Запускаем синхронную обработку в отдельном потоке + loop = asyncio.get_event_loop() + result = await loop.run_in_executor( + self.executor, + self._process_upload_sync, + request + ) + return result + except Exception as e: + logger.error(f"Ошибка при асинхронной загрузке отчета: {str(e)}") + return UploadResult( + success=False, + message=f"Ошибка при асинхронной загрузке отчета: {str(e)}" + ) + + def _process_upload_sync(self, request: UploadRequest) -> UploadResult: + """Синхронная обработка загрузки (выполняется в отдельном потоке)""" + return self.report_service.upload_report(request) + + async def get_data_async(self, request: DataRequest) -> DataResult: + """Асинхронное получение данных""" + try: + # Запускаем синхронную обработку в отдельном потоке + loop = asyncio.get_event_loop() + result = await loop.run_in_executor( + self.executor, + self._process_get_data_sync, + request + ) + return result + except Exception as e: + logger.error(f"Ошибка при асинхронном получении данных: {str(e)}") + return DataResult( + success=False, + message=f"Ошибка при асинхронном получении данных: {str(e)}" + ) + + def _process_get_data_sync(self, request: DataRequest) -> DataResult: + """Синхронное получение данных (выполняется в отдельном потоке)""" + return self.report_service.get_data(request) + + def __del__(self): + """Очистка ресурсов""" + if hasattr(self, 'executor'): + self.executor.shutdown(wait=False) \ No newline at end of file diff --git a/streamlit_app/async_upload_page.py b/streamlit_app/async_upload_page.py new file mode 100644 index 0000000..dfa967c --- /dev/null +++ b/streamlit_app/async_upload_page.py @@ -0,0 +1,146 @@ +""" +Страница асинхронной загрузки файлов +""" +import streamlit as st +import asyncio +import threading +import time +from api_client import upload_file_to_api +from config import PARSER_TABS + + +def upload_file_async_background(endpoint, file_data, filename, task_id): + """Асинхронная загрузка файла в фоновом режиме""" + try: + # Имитируем асинхронную работу + time.sleep(1) # Небольшая задержка для демонстрации + + # Выполняем загрузку + result, status = upload_file_to_api(endpoint, file_data, filename) + + # Сохраняем результат в session_state + if 'upload_tasks' not in st.session_state: + st.session_state.upload_tasks = {} + + st.session_state.upload_tasks[task_id] = { + 'status': 'completed' if status == 200 else 'failed', + 'result': result, + 'status_code': status, + 'filename': filename, + 'endpoint': endpoint, + 'completed_at': time.time() + } + + except Exception as e: + # Сохраняем ошибку + if 'upload_tasks' not in st.session_state: + st.session_state.upload_tasks = {} + + st.session_state.upload_tasks[task_id] = { + 'status': 'failed', + 'error': str(e), + 'filename': filename, + 'endpoint': endpoint, + 'completed_at': time.time() + } + + +def render_async_upload_page(): + """Рендер страницы асинхронной загрузки""" + st.title("🚀 Асинхронная загрузка файлов") + st.markdown("---") + + st.info(""" + **Асинхронная загрузка** позволяет загружать файлы без блокировки интерфейса. + После загрузки файл будет обработан в фоновом режиме, а вы сможете отслеживать прогресс на странице "Управление задачами". + """) + + # Выбор парсера + st.subheader("📋 Выбор парсера") + + # Создаем словарь парсеров с их асинхронными эндпоинтами + parser_endpoints = { + "Сводки ПМ": "/async/svodka_pm/upload-zip", + "Сводки СА": "/async/svodka_ca/upload", + "Мониторинг топлива": "/async/monitoring_fuel/upload-zip", + "Ремонт СА": "/svodka_repair_ca/upload", # Пока синхронный + "Статусы ремонта СА": "/statuses_repair_ca/upload", # Пока синхронный + "Мониторинг ТЭР": "/monitoring_tar/upload", # Пока синхронный + "Операционные справки": "/oper_spravka_tech_pos/upload" # Пока синхронный + } + + selected_parser = st.selectbox( + "Выберите тип парсера для загрузки:", + list(parser_endpoints.keys()), + key="async_parser_select" + ) + + st.markdown("---") + + # Загрузка файла + st.subheader("📤 Загрузка файла") + + uploaded_file = st.file_uploader( + f"Выберите ZIP архив для парсера '{selected_parser}'", + type=['zip'], + key="async_file_upload" + ) + + if uploaded_file is not None: + st.success(f"✅ Файл выбран: {uploaded_file.name}") + st.info(f"📊 Размер файла: {uploaded_file.size / 1024 / 1024:.2f} MB") + + if st.button("🚀 Загрузить асинхронно", key="async_upload_btn", use_container_width=True): + # Создаем уникальный ID задачи + task_id = f"task_{int(time.time())}_{uploaded_file.name}" + + # Показываем сообщение о создании задачи + st.success("✅ Задача загрузки создана!") + st.info(f"ID задачи: `{task_id}`") + st.info("📋 Перейдите на страницу 'Управление задачами' для отслеживания прогресса") + + # Запускаем загрузку в фоновом потоке + endpoint = parser_endpoints[selected_parser] + file_data = uploaded_file.read() + + # Создаем поток для асинхронной загрузки + thread = threading.Thread( + target=upload_file_async_background, + args=(endpoint, file_data, uploaded_file.name, task_id) + ) + thread.daemon = True + thread.start() + + # Автоматически переключаемся на страницу задач + st.session_state.sidebar_tasks_clicked = True + st.rerun() + + st.markdown("---") + + # Информация о поддерживаемых форматах + with st.expander("ℹ️ Поддерживаемые форматы файлов"): + st.markdown(""" + **Поддерживаемые форматы:** + - 📦 ZIP архивы с Excel файлами + - 📊 Excel файлы (.xlsx, .xls) + - 📋 CSV файлы (для некоторых парсеров) + + **Ограничения:** + - Максимальный размер файла: 100 MB + - Количество файлов в архиве: до 50 + - Поддерживаемые кодировки: UTF-8, Windows-1251 + """) + + # Статистика загрузок + st.subheader("📈 Статистика загрузок") + + col1, col2, col3 = st.columns(3) + + with col1: + st.metric("Всего загружено", "0", "0") + + with col2: + st.metric("В обработке", "0", "0") + + with col3: + st.metric("Завершено", "0", "0") \ No newline at end of file diff --git a/streamlit_app/sidebar.py b/streamlit_app/sidebar.py index 479a6c9..31adbec 100644 --- a/streamlit_app/sidebar.py +++ b/streamlit_app/sidebar.py @@ -9,7 +9,7 @@ from config import API_PUBLIC_URL def render_sidebar(): """Рендер боковой панели""" with st.sidebar: - st.header("ℹ️ Информация") + st.header("ℹ️ Информация1") # Информация о сервере server_info = get_server_info() @@ -25,6 +25,28 @@ def render_sidebar(): st.subheader("Доступные парсеры") for parser in parsers: st.write(f"• {parser}") + + # Навигация по страницам + st.markdown("---") + st.subheader("🧭 Навигация") + + # Определяем активную страницу + active_page = st.session_state.get("active_page", 0) + + # Кнопка для страницы синхронных парсеров + if st.button("📊 Синхронные парсеры", key="sidebar_sync_btn", use_container_width=True, type="primary" if active_page == 0 else "secondary"): + st.session_state.sidebar_sync_clicked = True + st.rerun() + + # Кнопка для страницы асинхронной загрузки + if st.button("🚀 Асинхронная загрузка", key="sidebar_async_btn", use_container_width=True, type="primary" if active_page == 1 else "secondary"): + st.session_state.sidebar_async_clicked = True + st.rerun() + + # Кнопка для страницы управления задачами + if st.button("📋 Управление задачами", key="sidebar_tasks_btn", use_container_width=True, type="primary" if active_page == 2 else "secondary"): + st.session_state.sidebar_tasks_clicked = True + st.rerun() def render_footer(): diff --git a/streamlit_app/streamlit_app.py b/streamlit_app/streamlit_app.py index 103e878..573df0d 100644 --- a/streamlit_app/streamlit_app.py +++ b/streamlit_app/streamlit_app.py @@ -1,20 +1,24 @@ import streamlit as st -from config import setup_page_config, PARSER_TABS, API_PUBLIC_URL +from config import setup_page_config, API_PUBLIC_URL from api_client import check_api_health from sidebar import render_sidebar, render_footer -from parsers_ui.svodka_pm_ui import render_svodka_pm_tab -from parsers_ui.svodka_ca_ui import render_svodka_ca_tab -from parsers_ui.monitoring_fuel_ui import render_monitoring_fuel_tab -from parsers_ui.svodka_repair_ca_ui import render_svodka_repair_ca_tab -from parsers_ui.statuses_repair_ca_ui import render_statuses_repair_ca_tab -from parsers_ui.monitoring_tar_ui import render_monitoring_tar_tab -from parsers_ui.oper_spravka_tech_pos_ui import render_oper_spravka_tech_pos_tab +from sync_parsers_page import render_sync_parsers_page +from async_upload_page import render_async_upload_page +from tasks_page import render_tasks_page # Конфигурация страницы setup_page_config() def main(): - st.title("🚀 NIN Excel Parsers API - Демонстрация") + # Определяем активную страницу для заголовка + active_page = st.session_state.get("active_page", 0) + page_titles = { + 0: "Синхронные парсеры", + 1: "Асинхронная загрузка", + 2: "Управление задачами" + } + + st.title(f"🚀 NIN Excel Parsers API - {page_titles.get(active_page, 'Демонстрация')}") st.markdown("---") # Проверка доступности API @@ -25,39 +29,30 @@ def main(): st.success(f"✅ API доступен по адресу {API_PUBLIC_URL}") - # Боковая панель с информацией + # Боковая панель с информацией и навигацией render_sidebar() - # Основные вкладки - по одной на каждый парсер - tab1, tab2, tab3, tab4, tab5, tab6, tab7 = st.tabs(PARSER_TABS) + # Обрабатываем клики по кнопкам в сайдбаре + if st.session_state.get("sidebar_sync_clicked", False): + st.session_state.sidebar_sync_clicked = False + st.session_state.active_page = 0 + elif st.session_state.get("sidebar_async_clicked", False): + st.session_state.sidebar_async_clicked = False + st.session_state.active_page = 1 + elif st.session_state.get("sidebar_tasks_clicked", False): + st.session_state.sidebar_tasks_clicked = False + st.session_state.active_page = 2 - # Вкладка 1: Сводки ПМ - полный функционал - with tab1: - render_svodka_pm_tab() + # Определяем активную страницу + active_page = st.session_state.get("active_page", 0) - # Вкладка 2: Сводки СА - полный функционал - with tab2: - render_svodka_ca_tab() - - # Вкладка 3: Мониторинг топлива - полный функционал - with tab3: - render_monitoring_fuel_tab() - - # Вкладка 4: Ремонт СА - with tab4: - render_svodka_repair_ca_tab() - - # Вкладка 5: Статусы ремонта СА - with tab5: - render_statuses_repair_ca_tab() - - # Вкладка 6: Мониторинг ТЭР - with tab6: - render_monitoring_tar_tab() - - # Вкладка 7: Операционные справки технологических позиций - with tab7: - render_oper_spravka_tech_pos_tab() + # Рендерим соответствующую страницу + if active_page == 0: + render_sync_parsers_page() + elif active_page == 1: + render_async_upload_page() + else: + render_tasks_page() # Футер render_footer() diff --git a/streamlit_app/sync_parsers_page.py b/streamlit_app/sync_parsers_page.py new file mode 100644 index 0000000..f776931 --- /dev/null +++ b/streamlit_app/sync_parsers_page.py @@ -0,0 +1,54 @@ +""" +Страница синхронных парсеров +""" +import streamlit as st +from parsers_ui.svodka_pm_ui import render_svodka_pm_tab +from parsers_ui.svodka_ca_ui import render_svodka_ca_tab +from parsers_ui.monitoring_fuel_ui import render_monitoring_fuel_tab +from parsers_ui.svodka_repair_ca_ui import render_svodka_repair_ca_tab +from parsers_ui.statuses_repair_ca_ui import render_statuses_repair_ca_tab +from parsers_ui.monitoring_tar_ui import render_monitoring_tar_tab +from parsers_ui.oper_spravka_tech_pos_ui import render_oper_spravka_tech_pos_tab +from config import PARSER_TABS + + +def render_sync_parsers_page(): + """Рендер страницы синхронных парсеров""" + st.title("📊 Синхронные парсеры") + st.markdown("---") + + st.info(""" + **Синхронные парсеры** обрабатывают файлы сразу после загрузки. + Интерфейс будет заблокирован до завершения обработки. + """) + + # Основные вкладки - по одной на каждый парсер + tab1, tab2, tab3, tab4, tab5, tab6, tab7 = st.tabs(PARSER_TABS) + + # Вкладка 1: Сводки ПМ - полный функционал + with tab1: + render_svodka_pm_tab() + + # Вкладка 2: Сводки СА - полный функционал + with tab2: + render_svodka_ca_tab() + + # Вкладка 3: Мониторинг топлива - полный функционал + with tab3: + render_monitoring_fuel_tab() + + # Вкладка 4: Ремонт СА + with tab4: + render_svodka_repair_ca_tab() + + # Вкладка 5: Статусы ремонта СА + with tab5: + render_statuses_repair_ca_tab() + + # Вкладка 6: Мониторинг ТЭР + with tab6: + render_monitoring_tar_tab() + + # Вкладка 7: Операционные справки технологических позиций + with tab7: + render_oper_spravka_tech_pos_tab() \ No newline at end of file diff --git a/streamlit_app/tasks_page.py b/streamlit_app/tasks_page.py new file mode 100644 index 0000000..512655a --- /dev/null +++ b/streamlit_app/tasks_page.py @@ -0,0 +1,159 @@ +""" +Страница управления задачами загрузки +""" +import streamlit as st +from datetime import datetime +import time + + +def render_tasks_page(): + """Рендер страницы управления задачами""" + st.title("📋 Управление задачами загрузки") + st.markdown("---") + + # Кнопки управления + col1, col2, col3, col4 = st.columns([1, 1, 1, 2]) + + with col1: + if st.button("🔄 Обновить", key="refresh_tasks_btn", use_container_width=True): + st.rerun() + + with col2: + if st.button("🗑️ Очистить завершенные", key="clear_completed_btn", use_container_width=True): + st.info("Функция очистки будет добавлена в следующих версиях") + + with col3: + auto_refresh = st.checkbox("🔄 Автообновление", key="auto_refresh_checkbox") + if auto_refresh: + time.sleep(2) + st.rerun() + + with col4: + st.caption("Последнее обновление: " + datetime.now().strftime("%H:%M:%S")) + + st.markdown("---") + + # Статистика задач + st.subheader("📊 Статистика задач") + + # Получаем задачи из session_state + tasks = st.session_state.get('upload_tasks', {}) + + # Подсчитываем статистику + total_tasks = len(tasks) + pending_tasks = len([t for t in tasks.values() if t.get('status') == 'pending']) + running_tasks = len([t for t in tasks.values() if t.get('status') == 'running']) + completed_tasks = len([t for t in tasks.values() if t.get('status') == 'completed']) + failed_tasks = len([t for t in tasks.values() if t.get('status') == 'failed']) + + col1, col2, col3, col4, col5 = st.columns(5) + + with col1: + st.metric("Всего", total_tasks, f"+{total_tasks}") + + with col2: + st.metric("Ожидают", pending_tasks, f"+{pending_tasks}") + + with col3: + st.metric("Выполняются", running_tasks, f"+{running_tasks}") + + with col4: + st.metric("Завершены", completed_tasks, f"+{completed_tasks}") + + with col5: + st.metric("Ошибки", failed_tasks, f"+{failed_tasks}") + + st.markdown("---") + + # Список задач + st.subheader("📋 Список задач") + + # Получаем задачи из session_state + tasks = st.session_state.get('upload_tasks', {}) + + if tasks: + # Показываем задачи + for task_id, task in tasks.items(): + status_emoji = { + 'pending': '🟡', + 'running': '🔵', + 'completed': '🟢', + 'failed': '🔴' + }.get(task.get('status', 'pending'), '⚪') + + with st.expander(f"{status_emoji} {task.get('filename', 'Unknown')} - {task.get('status', 'unknown').upper()}", expanded=True): + col1, col2 = st.columns([3, 1]) + + with col1: + st.write(f"**ID:** `{task_id}`") + st.write(f"**Статус:** {status_emoji} {task.get('status', 'unknown').upper()}") + st.write(f"**Файл:** {task.get('filename', 'Unknown')}") + st.write(f"**Эндпоинт:** {task.get('endpoint', 'Unknown')}") + + if task.get('completed_at'): + completed_time = datetime.fromtimestamp(task['completed_at']).strftime("%Y-%m-%d %H:%M:%S") + st.write(f"**Завершена:** {completed_time}") + + if task.get('result'): + result = task['result'] + if task.get('status') == 'completed': + st.success(f"✅ {result.get('message', 'Задача выполнена')}") + if result.get('object_id'): + st.info(f"ID объекта: {result['object_id']}") + else: + st.error(f"❌ {result.get('message', 'Ошибка выполнения')}") + + if task.get('error'): + st.error(f"❌ Ошибка: {task['error']}") + + with col2: + if task.get('status') in ['pending', 'running']: + if st.button("❌ Отменить", key=f"cancel_{task_id}_btn", use_container_width=True): + st.info("Функция отмены будет реализована в следующих версиях") + else: + if st.button("🗑️ Удалить", key=f"delete_{task_id}_btn", use_container_width=True): + # Удаляем задачу из session_state + if 'upload_tasks' in st.session_state: + del st.session_state.upload_tasks[task_id] + st.rerun() + else: + # Пустое состояние + st.info(""" + **Нет активных задач** + + Загрузите файл на странице "Асинхронная загрузка", чтобы создать новую задачу. + Здесь вы сможете отслеживать прогресс обработки и управлять задачами. + """) + + # Кнопка для создания тестовой задачи + if st.button("🧪 Создать тестовую задачу", key="create_test_task_btn"): + test_task_id = f"test_task_{int(time.time())}" + if 'upload_tasks' not in st.session_state: + st.session_state.upload_tasks = {} + + st.session_state.upload_tasks[test_task_id] = { + 'status': 'completed', + 'filename': 'test_file.zip', + 'endpoint': '/test/upload', + 'result': {'message': 'Тестовая задача выполнена', 'object_id': 'test-123'}, + 'completed_at': time.time() + } + st.rerun() + + st.markdown("---") + + # Информация о статусах задач + with st.expander("ℹ️ Статусы задач"): + st.markdown(""" + **Статусы задач:** + - 🟡 **Ожидает** - задача создана и ожидает выполнения + - 🔵 **Выполняется** - задача обрабатывается + - 🟢 **Завершена** - задача успешно выполнена + - 🔴 **Ошибка** - произошла ошибка при выполнении + - ⚫ **Отменена** - задача была отменена пользователем + + **Действия:** + - ❌ **Отменить** - отменить выполнение задачи + - 🔄 **Обновить** - обновить статус задачи + - 📊 **Детали** - просмотреть подробную информацию + """) \ No newline at end of file From 1bfe3c0cd8c05507315012540e3df55991f7596d Mon Sep 17 00:00:00 2001 From: Maksim Date: Thu, 4 Sep 2025 22:53:01 +0300 Subject: [PATCH 4/5] =?UTF-8?q?=D0=A0=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=D0=B5?= =?UTF-8?q?=D1=82!!!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- streamlit_app/async_upload_page.py | 36 ++++++++++++++------- streamlit_app/tasks_page.py | 51 +++++++++++++++++++++++------- 2 files changed, 64 insertions(+), 23 deletions(-) diff --git a/streamlit_app/async_upload_page.py b/streamlit_app/async_upload_page.py index dfa967c..d5adadb 100644 --- a/streamlit_app/async_upload_page.py +++ b/streamlit_app/async_upload_page.py @@ -5,43 +5,57 @@ import streamlit as st import asyncio import threading import time +import json +import os from api_client import upload_file_to_api from config import PARSER_TABS +# Глобальное хранилище задач (в реальном приложении лучше использовать Redis или БД) +TASKS_STORAGE = {} + def upload_file_async_background(endpoint, file_data, filename, task_id): """Асинхронная загрузка файла в фоновом режиме""" + global TASKS_STORAGE + try: + # Обновляем статус на "running" + TASKS_STORAGE[task_id] = { + 'status': 'running', + 'filename': filename, + 'endpoint': endpoint, + 'started_at': time.time(), + 'progress': 0 + } + # Имитируем асинхронную работу time.sleep(1) # Небольшая задержка для демонстрации # Выполняем загрузку result, status = upload_file_to_api(endpoint, file_data, filename) - # Сохраняем результат в session_state - if 'upload_tasks' not in st.session_state: - st.session_state.upload_tasks = {} - - st.session_state.upload_tasks[task_id] = { + # Сохраняем результат в глобальном хранилище + TASKS_STORAGE[task_id] = { 'status': 'completed' if status == 200 else 'failed', 'result': result, 'status_code': status, 'filename': filename, 'endpoint': endpoint, - 'completed_at': time.time() + 'started_at': TASKS_STORAGE.get(task_id, {}).get('started_at', time.time()), + 'completed_at': time.time(), + 'progress': 100 } except Exception as e: # Сохраняем ошибку - if 'upload_tasks' not in st.session_state: - st.session_state.upload_tasks = {} - - st.session_state.upload_tasks[task_id] = { + TASKS_STORAGE[task_id] = { 'status': 'failed', 'error': str(e), 'filename': filename, 'endpoint': endpoint, - 'completed_at': time.time() + 'started_at': TASKS_STORAGE.get(task_id, {}).get('started_at', time.time()), + 'completed_at': time.time(), + 'progress': 0 } diff --git a/streamlit_app/tasks_page.py b/streamlit_app/tasks_page.py index 512655a..440ce9c 100644 --- a/streamlit_app/tasks_page.py +++ b/streamlit_app/tasks_page.py @@ -4,6 +4,7 @@ import streamlit as st from datetime import datetime import time +from async_upload_page import TASKS_STORAGE def render_tasks_page(): @@ -20,7 +21,17 @@ def render_tasks_page(): with col2: if st.button("🗑️ Очистить завершенные", key="clear_completed_btn", use_container_width=True): - st.info("Функция очистки будет добавлена в следующих версиях") + # Удаляем завершенные и неудачные задачи + tasks_to_remove = [] + for task_id, task in TASKS_STORAGE.items(): + if task.get('status') in ['completed', 'failed']: + tasks_to_remove.append(task_id) + + for task_id in tasks_to_remove: + del TASKS_STORAGE[task_id] + + st.success(f"✅ Удалено {len(tasks_to_remove)} завершенных задач") + st.rerun() with col3: auto_refresh = st.checkbox("🔄 Автообновление", key="auto_refresh_checkbox") @@ -36,8 +47,8 @@ def render_tasks_page(): # Статистика задач st.subheader("📊 Статистика задач") - # Получаем задачи из session_state - tasks = st.session_state.get('upload_tasks', {}) + # Получаем задачи из глобального хранилища + tasks = TASKS_STORAGE # Подсчитываем статистику total_tasks = len(tasks) @@ -68,8 +79,8 @@ def render_tasks_page(): # Список задач st.subheader("📋 Список задач") - # Получаем задачи из session_state - tasks = st.session_state.get('upload_tasks', {}) + # Получаем задачи из глобального хранилища + tasks = TASKS_STORAGE if tasks: # Показываем задачи @@ -90,9 +101,25 @@ def render_tasks_page(): st.write(f"**Файл:** {task.get('filename', 'Unknown')}") st.write(f"**Эндпоинт:** {task.get('endpoint', 'Unknown')}") + # Показываем прогресс для выполняющихся задач + if task.get('status') == 'running': + progress = task.get('progress', 0) + st.write(f"**Прогресс:** {progress}%") + st.progress(progress / 100) + + # Показываем время выполнения + if task.get('started_at'): + started_time = datetime.fromtimestamp(task['started_at']).strftime("%Y-%m-%d %H:%M:%S") + st.write(f"**Начата:** {started_time}") + if task.get('completed_at'): completed_time = datetime.fromtimestamp(task['completed_at']).strftime("%Y-%m-%d %H:%M:%S") st.write(f"**Завершена:** {completed_time}") + + # Показываем длительность + if task.get('started_at'): + duration = task['completed_at'] - task['started_at'] + st.write(f"**Длительность:** {duration:.1f} сек") if task.get('result'): result = task['result'] @@ -112,9 +139,9 @@ def render_tasks_page(): st.info("Функция отмены будет реализована в следующих версиях") else: if st.button("🗑️ Удалить", key=f"delete_{task_id}_btn", use_container_width=True): - # Удаляем задачу из session_state - if 'upload_tasks' in st.session_state: - del st.session_state.upload_tasks[task_id] + # Удаляем задачу из глобального хранилища + if task_id in TASKS_STORAGE: + del TASKS_STORAGE[task_id] st.rerun() else: # Пустое состояние @@ -128,15 +155,15 @@ def render_tasks_page(): # Кнопка для создания тестовой задачи if st.button("🧪 Создать тестовую задачу", key="create_test_task_btn"): test_task_id = f"test_task_{int(time.time())}" - if 'upload_tasks' not in st.session_state: - st.session_state.upload_tasks = {} - st.session_state.upload_tasks[test_task_id] = { + TASKS_STORAGE[test_task_id] = { 'status': 'completed', 'filename': 'test_file.zip', 'endpoint': '/test/upload', 'result': {'message': 'Тестовая задача выполнена', 'object_id': 'test-123'}, - 'completed_at': time.time() + 'started_at': time.time() - 5, # 5 секунд назад + 'completed_at': time.time(), + 'progress': 100 } st.rerun() From 55626490dd5a657f085b08f3666a1e042732b13a Mon Sep 17 00:00:00 2001 From: Maksim Date: Thu, 4 Sep 2025 22:59:47 +0300 Subject: [PATCH 5/5] =?UTF-8?q?fix:=20=D1=86=D0=B2=D0=B5=D1=82=D0=B0=20?= =?UTF-8?q?=D0=B2=D0=BA=D0=BB=D0=B0=D0=B4=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- streamlit_app/streamlit_app.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/streamlit_app/streamlit_app.py b/streamlit_app/streamlit_app.py index 573df0d..10c3392 100644 --- a/streamlit_app/streamlit_app.py +++ b/streamlit_app/streamlit_app.py @@ -29,10 +29,7 @@ def main(): st.success(f"✅ API доступен по адресу {API_PUBLIC_URL}") - # Боковая панель с информацией и навигацией - render_sidebar() - - # Обрабатываем клики по кнопкам в сайдбаре + # Обрабатываем клики по кнопкам в сайдбаре ПЕРЕД рендером if st.session_state.get("sidebar_sync_clicked", False): st.session_state.sidebar_sync_clicked = False st.session_state.active_page = 0 @@ -46,6 +43,9 @@ def main(): # Определяем активную страницу active_page = st.session_state.get("active_page", 0) + # Боковая панель с информацией и навигацией + render_sidebar() + # Рендерим соответствующую страницу if active_page == 0: render_sync_parsers_page()