From 36f37ffacb19cf44cf16f8fd6c4691da3231dafa Mon Sep 17 00:00:00 2001 From: Maksim Date: Thu, 4 Sep 2025 22:42:31 +0300 Subject: [PATCH] =?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