Работает все, кроме отображения задач

This commit is contained in:
2025-09-04 22:42:31 +03:00
parent 6a1f685ee3
commit 36f37ffacb
7 changed files with 681 additions and 39 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -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")

View File

@@ -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():

View File

@@ -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()

View File

@@ -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()

159
streamlit_app/tasks_page.py Normal file
View File

@@ -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("""
**Статусы задач:**
- 🟡 **Ожидает** - задача создана и ожидает выполнения
- 🔵 **Выполняется** - задача обрабатывается
- 🟢 **Завершена** - задача успешно выполнена
- 🔴 **Ошибка** - произошла ошибка при выполнении
- ⚫ **Отменена** - задача была отменена пользователем
**Действия:**
- ❌ **Отменить** - отменить выполнение задачи
- 🔄 **Обновить** - обновить статус задачи
- 📊 **Детали** - просмотреть подробную информацию
""")