From fc9ef0852527cc0615c0ea0294c428235a167c62 Mon Sep 17 00:00:00 2001 From: benya Date: Sat, 7 Feb 2026 23:24:58 +0300 Subject: [PATCH] Add Arcane project status --- config.example.yaml | 5 +++++ handlers/arcane.py | 52 +++++++++++++++++++++++++++++++++++++++++++++ keyboards.py | 9 ++++++++ main.py | 1 + services/arcane.py | 27 +++++++++++++++++++++++ 5 files changed, 94 insertions(+) create mode 100644 handlers/arcane.py create mode 100644 services/arcane.py diff --git a/config.example.yaml b/config.example.yaml index 243adc2..373bb66 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -22,6 +22,11 @@ alerts: smart_cooldown_sec: 21600 smart_temp_warn: 50 +arcane: + base_url: "http://localhost:3552" + api_key: "arc_..." + env_id: 0 + docker: # If true, discover containers by name/label autodiscovery: true diff --git a/handlers/arcane.py b/handlers/arcane.py new file mode 100644 index 0000000..eb0b273 --- /dev/null +++ b/handlers/arcane.py @@ -0,0 +1,52 @@ +import asyncio +from aiogram import F +from aiogram.types import Message +from app import dp, cfg +from auth import is_admin_msg +from keyboards import docker_kb, arcane_kb +from services.arcane import list_projects + + +def _arcane_cfg(): + arc = cfg.get("arcane", {}) + return arc.get("base_url"), arc.get("api_key"), int(arc.get("env_id", 0)) + + +async def cmd_arcane_projects(msg: Message): + base_url, api_key, env_id = _arcane_cfg() + if not base_url or not api_key: + await msg.answer("⚠️ Arcane config missing", reply_markup=docker_kb) + return + + await msg.answer("⏳ Arcane projects…", reply_markup=arcane_kb) + + async def worker(): + ok, info, items = await asyncio.to_thread(list_projects, base_url, api_key, env_id) + if not ok: + await msg.answer(f"❌ Arcane error: {info}", reply_markup=arcane_kb) + return + + lines = ["🧰 Arcane projects\n"] + for p in items: + status = p.get("status", "unknown") + name = p.get("name", "?") + running = p.get("runningCount", 0) + total = p.get("serviceCount", 0) + icon = "🟢" if status == "running" else "🟡" + lines.append(f"{icon} {name}: {status} ({running}/{total})") + + await msg.answer("\n".join(lines), reply_markup=arcane_kb) + + asyncio.create_task(worker()) + + +@dp.message(F.text == "🧰 Arcane") +async def arcane_menu(msg: Message): + if is_admin_msg(msg): + await cmd_arcane_projects(msg) + + +@dp.message(F.text == "🔄 Refresh") +async def arcane_refresh(msg: Message): + if is_admin_msg(msg): + await cmd_arcane_projects(msg) diff --git a/keyboards.py b/keyboards.py index 2afd969..ccde5a3 100644 --- a/keyboards.py +++ b/keyboards.py @@ -19,12 +19,21 @@ menu_kb = ReplyKeyboardMarkup( docker_kb = ReplyKeyboardMarkup( keyboard=[ [KeyboardButton(text="🐳 Status")], + [KeyboardButton(text="🧰 Arcane")], [KeyboardButton(text="🔄 Restart"), KeyboardButton(text="📜 Logs")], [KeyboardButton(text="⬅️ Назад")], ], resize_keyboard=True, ) +arcane_kb = ReplyKeyboardMarkup( + keyboard=[ + [KeyboardButton(text="🔄 Refresh")], + [KeyboardButton(text="⬅️ Назад")], + ], + resize_keyboard=True, +) + backup_kb = ReplyKeyboardMarkup( keyboard=[ [KeyboardButton(text="📦 Status"), KeyboardButton(text="📦 Last snapshot")], diff --git a/main.py b/main.py index 7ca8e5f..5748f7e 100644 --- a/main.py +++ b/main.py @@ -16,6 +16,7 @@ import handlers.artifacts import handlers.system import handlers.help import handlers.callbacks +import handlers.arcane async def notify_start(): diff --git a/services/arcane.py b/services/arcane.py new file mode 100644 index 0000000..f155f1d --- /dev/null +++ b/services/arcane.py @@ -0,0 +1,27 @@ +import json +from urllib.request import Request, urlopen +from urllib.error import URLError, HTTPError + + +def list_projects(base_url: str, api_key: str, env_id: int, timeout: int = 10) -> tuple[bool, str, list[dict]]: + url = f"{base_url.rstrip('/')}/api/environments/{env_id}/projects" + req = Request(url, headers={"X-Api-Key": api_key}) + try: + with urlopen(req, timeout=timeout) as resp: + raw = resp.read().decode("utf-8", errors="ignore") + except HTTPError as e: + return False, f"HTTP {e.code}", [] + except URLError as e: + return False, f"URL error: {e.reason}", [] + except Exception as e: + return False, f"Error: {e}", [] + + try: + payload = json.loads(raw) + except json.JSONDecodeError: + return False, "Invalid JSON", [] + + if not payload.get("success"): + return False, "API returned success=false", [] + + return True, "OK", payload.get("data", [])