Merge branch 'to-faster'

This commit is contained in:
2025-09-04 23:01:09 +03:00
17 changed files with 1655 additions and 825 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,76 @@
"""
Модуль для работы с API
"""
import requests
from typing import Dict, Any, List, Tuple
from config import API_BASE_URL, API_PUBLIC_URL
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 []

View File

@@ -0,0 +1,160 @@
"""
Страница асинхронной загрузки файлов
"""
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)
# Сохраняем результат в глобальном хранилище
TASKS_STORAGE[task_id] = {
'status': 'completed' if status == 200 else 'failed',
'result': result,
'status_code': status,
'filename': filename,
'endpoint': endpoint,
'started_at': TASKS_STORAGE.get(task_id, {}).get('started_at', time.time()),
'completed_at': time.time(),
'progress': 100
}
except Exception as e:
# Сохраняем ошибку
TASKS_STORAGE[task_id] = {
'status': 'failed',
'error': str(e),
'filename': filename,
'endpoint': endpoint,
'started_at': TASKS_STORAGE.get(task_id, {}).get('started_at', time.time()),
'completed_at': time.time(),
'progress': 0
}
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")

58
streamlit_app/config.py Normal file
View File

@@ -0,0 +1,58 @@
"""
Конфигурация приложения
"""
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():
"""Настройка конфигурации страницы 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"]

View File

@@ -0,0 +1,3 @@
"""
UI модули для парсеров
"""

View File

@@ -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', 'Неизвестная ошибка')}")

View File

@@ -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', 'Неизвестная ошибка')}")

View File

@@ -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', 'Неизвестная ошибка')}")

View File

@@ -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', 'Неизвестная ошибка')}")

View File

@@ -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("⚠️ Выберите режимы и таблицы")

View File

@@ -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("⚠️ Выберите коды и столбцы")

View File

@@ -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', 'Неизвестная ошибка')}")

76
streamlit_app/sidebar.py Normal file
View File

@@ -0,0 +1,76 @@
"""
Модуль для сайдбара
"""
import streamlit as st
from api_client import get_server_info, get_available_parsers
from config import API_PUBLIC_URL
def render_sidebar():
"""Рендер боковой панели"""
with st.sidebar:
st.header(" Информация1")
# Информация о сервере
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}")
# Навигация по страницам
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():
"""Рендер футера"""
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 (веб-интерфейс)
""")

View File

@@ -1,847 +1,61 @@
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, API_PUBLIC_URL
from api_client import check_api_health
from sidebar import render_sidebar, render_footer
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
# Конфигурация страницы
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 - Демонстрация")
# Определяем активную страницу для заголовка
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
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}")
# Основные вкладки - по одной на каждый парсер
tab1, tab2, tab3, tab4, tab5, tab6, tab7 = st.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("⚠️ Выберите коды и столбцы")
# Вкладка 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("⚠️ Выберите режимы и таблицы")
# Вкладка 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', 'Неизвестная ошибка')}")
# Вкладка 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', 'Неизвестная ошибка')}")
# Вкладка 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', 'Неизвестная ошибка')}")
# Вкладка 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', 'Неизвестная ошибка')}")
# Вкладка 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', 'Неизвестная ошибка')}")
# Обрабатываем клики по кнопкам в сайдбаре ПЕРЕД рендером
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
# Определяем активную страницу
active_page = st.session_state.get("active_page", 0)
# Боковая панель с информацией и навигацией
render_sidebar()
# Рендерим соответствующую страницу
if active_page == 0:
render_sync_parsers_page()
elif active_page == 1:
render_async_upload_page()
else:
render_tasks_page()
# Футер
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()

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

186
streamlit_app/tasks_page.py Normal file
View File

@@ -0,0 +1,186 @@
"""
Страница управления задачами загрузки
"""
import streamlit as st
from datetime import datetime
import time
from async_upload_page import TASKS_STORAGE
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):
# Удаляем завершенные и неудачные задачи
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")
if auto_refresh:
time.sleep(2)
st.rerun()
with col4:
st.caption("Последнее обновление: " + datetime.now().strftime("%H:%M:%S"))
st.markdown("---")
# Статистика задач
st.subheader("📊 Статистика задач")
# Получаем задачи из глобального хранилища
tasks = TASKS_STORAGE
# Подсчитываем статистику
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("📋 Список задач")
# Получаем задачи из глобального хранилища
tasks = TASKS_STORAGE
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('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']
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):
# Удаляем задачу из глобального хранилища
if task_id in TASKS_STORAGE:
del TASKS_STORAGE[task_id]
st.rerun()
else:
# Пустое состояние
st.info("""
**Нет активных задач**
Загрузите файл на странице "Асинхронная загрузка", чтобы создать новую задачу.
Здесь вы сможете отслеживать прогресс обработки и управлять задачами.
""")
# Кнопка для создания тестовой задачи
if st.button("🧪 Создать тестовую задачу", key="create_test_task_btn"):
test_task_id = f"test_task_{int(time.time())}"
TASKS_STORAGE[test_task_id] = {
'status': 'completed',
'filename': 'test_file.zip',
'endpoint': '/test/upload',
'result': {'message': 'Тестовая задача выполнена', 'object_id': 'test-123'},
'started_at': time.time() - 5, # 5 секунд назад
'completed_at': time.time(),
'progress': 100
}
st.rerun()
st.markdown("---")
# Информация о статусах задач
with st.expander(" Статусы задач"):
st.markdown("""
**Статусы задач:**
- 🟡 **Ожидает** - задача создана и ожидает выполнения
- 🔵 **Выполняется** - задача обрабатывается
- 🟢 **Завершена** - задача успешно выполнена
- 🔴 **Ошибка** - произошла ошибка при выполнении
- ⚫ **Отменена** - задача была отменена пользователем
**Действия:**
- ❌ **Отменить** - отменить выполнение задачи
- 🔄 **Обновить** - обновить статус задачи
- 📊 **Детали** - просмотреть подробную информацию
""")