Stage 04.1 - storage foundation (PostgreSQL) and system dashboard UI

This commit is contained in:
2026-04-14 21:38:52 +03:00
parent 1deb676585
commit aad680e2f9
15 changed files with 649 additions and 90 deletions

View File

@@ -1,137 +1,150 @@
from __future__ import annotations
import platform
import re
from dataclasses import dataclass
from src.core.config import load_settings
from src.core.constants import APP_NAME, APP_VERSION
from src.integrations.exchange.service import ExchangeService
from src.storage.session import check_database_health
@dataclass(slots=True)
class ComponentStatus:
name: str
state: str
details: str
details: str = ""
@dataclass(slots=True)
class SystemSnapshot:
app_name: str
app_version: str
app_env: str
python_version: str
os_name: str
db_label: str
timezone_name: str
exchange_enabled: bool
exchange_name: str
mode_label: str
default_symbol: str
symbol_validation_message: str
private_auth_message: str
components: list[ComponentStatus]
def _extract_postgres_version(raw: str) -> str:
if not raw:
return "PostgreSQL"
match = re.search(r"PostgreSQL\s+(\d+(?:\.\d+)?)", raw)
if match:
return f"PostgreSQL {match.group(1)}"
return "PostgreSQL"
def _build_exchange_status(exchange_service: ExchangeService, default_symbol: str) -> ComponentStatus:
try:
symbol_validation = exchange_service.validate_symbol(default_symbol)
except Exception as exc:
return ComponentStatus(
name="Биржа",
state="🔴",
details=f"Не удалось проверить инструмент: {exc}",
)
exchange_health = exchange_service.get_health()
if exchange_health.ok and symbol_validation.is_valid:
return ComponentStatus(name="Биржа", state="🟢")
if not exchange_health.ok:
return ComponentStatus(
name="Биржа",
state="🔴",
details=exchange_health.message or "Ошибка подключения к API биржи.",
)
return ComponentStatus(
name="Биржа",
state="🔴",
details=symbol_validation.message or "Инструмент не прошел проверку.",
)
def _build_account_status(exchange_service: ExchangeService) -> ComponentStatus:
private_auth_health = exchange_service.get_private_auth_health()
if private_auth_health.ok:
return ComponentStatus(name="Аккаунт", state="🟢")
return ComponentStatus(
name="Аккаунт",
state="🔴",
details=private_auth_health.message or "Ошибка private API.",
)
def _build_database_status() -> tuple[ComponentStatus, str]:
db_ok, db_message = check_database_health()
db_label = _extract_postgres_version(db_message)
if db_ok:
return ComponentStatus(name="База данных", state="🟢"), db_label
return (
ComponentStatus(
name="База данных",
state="🔴",
details=db_message or "Ошибка подключения к БД.",
),
db_label,
)
def _resolve_mode_label(exchange_testnet: bool) -> str:
return "ДЕМО аккаунт" if exchange_testnet else "РЕАЛЬНЫЙ аккаунт"
def get_system_snapshot() -> SystemSnapshot:
settings = load_settings()
exchange_service = ExchangeService()
try:
symbol_validation = exchange_service.validate_symbol(settings.default_symbol)
except Exception as exc:
symbol_validation = None
symbol_validation_message = f"Не удалось проверить символ: {exc}"
else:
symbol_validation_message = symbol_validation.message
exchange_health = exchange_service.get_health()
private_auth_health = exchange_service.get_private_auth_health()
if exchange_health.ok and exchange_health.mode == "mock":
exchange_state = "🟡 mock mode"
elif exchange_health.ok:
exchange_state = "🟢 API OK"
else:
exchange_state = "🔴 ошибка"
symbol_state = "🟢 OK" if symbol_validation and symbol_validation.is_valid else "🔴 ошибка"
private_auth_state = "🟢 OK" if private_auth_health.ok else "🔴 ошибка"
database_status, db_label = _build_database_status()
exchange_status = _build_exchange_status(exchange_service, settings.default_symbol)
account_status = _build_account_status(exchange_service)
components = [
ComponentStatus(
name="Бот",
state="🟢 работает",
details="Процесс бота запущен и обрабатывает команды.",
),
ComponentStatus(
name="Telegram",
state="🟢 OK",
details="Polling активен, базовая маршрутизация подключена.",
),
ComponentStatus(
name="Биржа",
state=exchange_state,
details=exchange_health.message,
),
ComponentStatus(
name="Символ",
state=symbol_state,
details=symbol_validation_message,
),
ComponentStatus(
name="Авторизация",
state=private_auth_state,
details=private_auth_health.message,
),
ComponentStatus(
name="База данных",
state="🟡 не подключена",
details="Слой хранения пока только подготовлен структурно.",
),
ComponentStatus(name="Приложение", state="🟢"),
database_status,
ComponentStatus(name="Telegram", state="🟢"),
exchange_status,
account_status,
]
return SystemSnapshot(
app_name=APP_NAME,
app_version=APP_VERSION,
app_env=settings.app_env,
python_version=platform.python_version(),
os_name=f"{platform.system()} {platform.release()}",
db_label=db_label,
timezone_name=settings.tz,
exchange_enabled=settings.exchange_enabled,
exchange_name=settings.exchange_name,
mode_label=_resolve_mode_label(settings.exchange_testnet),
default_symbol=settings.default_symbol,
symbol_validation_message=symbol_validation_message,
private_auth_message=private_auth_health.message,
components=components,
)
def _render_component(component: ComponentStatus) -> str:
if component.state == "🟢":
return f"{component.state} <b>{component.name}</b>"
return f"{component.state} <b>{component.name}</b>\n{component.details}"
def build_system_text() -> str:
snapshot = get_system_snapshot()
component_lines = []
for component in snapshot.components:
component_lines.append(
f"{component.state} <b>{component.name}</b>\n"
f"{component.details}"
)
components_block = "\n\n".join(component_lines)
components_block = "\n".join(_render_component(component) for component in snapshot.components)
return (
"<b>⚙️ Система</b>\n\n"
"<b>Статус компонентов</b>\n"
f"{components_block}\n\n"
"<b>Окружение</b>\n"
f"- приложение: {snapshot.app_name} {snapshot.app_version}\n"
f"- env: {snapshot.app_env}\n"
f"- python: {snapshot.python_version}\n"
f"- os: {snapshot.os_name}\n"
f"- timezone: {snapshot.timezone_name}\n"
f"- exchange_enabled: {snapshot.exchange_enabled}\n"
f"- exchange_name: {snapshot.exchange_name}\n"
f"- default_symbol: {snapshot.default_symbol}\n\n"
"<b>Справка</b>\n"
"/start — стартовый экран\n"
"/menu — показать меню\n"
"/help — открыть системную справку"
)
"<b>🌐 Окружение</b>\n"
f" приложение: {snapshot.app_name} {snapshot.app_version}\n"
f"• база данных: {snapshot.db_label}\n"
f"• часовой пояс: {snapshot.timezone_name}\n"
f"• режим: {snapshot.mode_label}\n"
f"• инструмент: {snapshot.default_symbol}"
)