Stage 04.1 - storage foundation (PostgreSQL) and system dashboard UI
This commit is contained in:
@@ -21,6 +21,11 @@ class Settings:
|
||||
exchange_timeout_sec: int
|
||||
exchange_testnet: bool
|
||||
default_symbol: str
|
||||
db_host: str
|
||||
db_port: int
|
||||
db_name: str
|
||||
db_user: str
|
||||
db_password: str
|
||||
def _parse_bool(raw_value: str, default: bool = False) -> bool:
|
||||
value = (raw_value or "").strip().lower()
|
||||
if not value:
|
||||
@@ -49,4 +54,9 @@ def load_settings() -> Settings:
|
||||
exchange_timeout_sec=_parse_int(os.getenv("EXCHANGE_TIMEOUT_SEC", "10"), 10),
|
||||
exchange_testnet=_parse_bool(os.getenv("EXCHANGE_TESTNET", "false")),
|
||||
default_symbol=os.getenv("DEFAULT_SYMBOL", "BTC/USD_LEVERAGE").strip() or "BTC/USD_LEVERAGE",
|
||||
db_host=os.getenv("DB_HOST", "localhost").strip() or "localhost",
|
||||
db_port=_parse_int(os.getenv("DB_PORT", "5432"), 5432),
|
||||
db_name=os.getenv("DB_NAME", "dzentra_bot").strip() or "dzentra_bot",
|
||||
db_user=os.getenv("DB_USER", "dzentra_bot").strip() or "dzentra_bot",
|
||||
db_password=os.getenv("DB_PASSWORD", "").strip(),
|
||||
)
|
||||
@@ -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}"
|
||||
)
|
||||
Reference in New Issue
Block a user