diff --git a/handlers/arcane.py b/handlers/arcane.py index c28c675..a4d7f9b 100644 --- a/handlers/arcane.py +++ b/handlers/arcane.py @@ -5,7 +5,7 @@ from aiogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton, C from app import dp, cfg from auth import is_admin_msg from keyboards import docker_kb, arcane_kb -from services.arcane import list_projects, restart_project, set_project_state +from services.arcane import list_projects, restart_project, set_project_state, get_project_details from state import ARCANE_CACHE @@ -26,6 +26,7 @@ def _arcane_kb(page: int, total_pages: int, items: list[dict]) -> InlineKeyboard action_text = "⏹" if action == "down" else "▶️" rows.append([ InlineKeyboardButton(text=f"🔄 {name}", callback_data=f"arcane:restart:{pid}"), + InlineKeyboardButton(text="ℹ️", callback_data=f"arcane:details:{pid}"), InlineKeyboardButton(text=action_text, callback_data=f"arcane:{action}:{pid}"), ]) @@ -156,6 +157,54 @@ async def arcane_restart(cb: CallbackQuery): await cb.message.answer(f"❌ Arcane restart failed: {info}", reply_markup=arcane_kb) +@dp.callback_query(F.data.startswith("arcane:details:")) +async def arcane_details(cb: CallbackQuery): + if cb.from_user.id != cfg["telegram"]["admin_id"]: + return + + _, _, pid = cb.data.split(":", 2) + base_url, api_key, env_id = _arcane_cfg() + if not base_url or not api_key: + await cb.answer("Arcane config missing") + return + + await cb.answer("Loading…") + ok, info, data = await asyncio.to_thread(get_project_details, base_url, api_key, env_id, pid) + if not ok: + await cb.message.answer(f"❌ Arcane details failed: {info}", reply_markup=arcane_kb) + return + + name = data.get("name", "?") + status = data.get("status", "unknown") + running = data.get("runningCount", 0) + total = data.get("serviceCount", 0) + status_reason = data.get("statusReason") + icon = "🟢" if status == "running" else "🟡" + + lines = [ + f"🧰 **{name}**", + f"{icon} Status: {status} ({running}/{total})", + ] + if status_reason: + lines.append(f"⚠️ {status_reason}") + + services = data.get("runtimeServices", []) + if services: + lines.append("") + lines.append("🧩 Services:") + for s in services: + s_name = s.get("name", "?") + s_status = s.get("status", "unknown") + s_health = s.get("health") + s_icon = "🟢" if s_status == "running" else "🟡" + line = f"{s_icon} {s_name}: {s_status}" + if s_health: + line += f" ({s_health})" + lines.append(line) + + await cb.message.answer("\n".join(lines), parse_mode="Markdown", reply_markup=arcane_kb) + + @dp.callback_query(F.data.startswith("arcane:up:")) async def arcane_up(cb: CallbackQuery): if cb.from_user.id != cfg["telegram"]["admin_id"]: diff --git a/services/arcane.py b/services/arcane.py index e54b3dd..31a69c6 100644 --- a/services/arcane.py +++ b/services/arcane.py @@ -78,3 +78,27 @@ def set_project_state( if payload and not payload.get("success", True): return False, "API returned success=false" return True, "OK" + + +def get_project_details(base_url: str, api_key: str, env_id: int, project_id: str, timeout: int = 10) -> tuple[bool, str, dict]: + url = f"{base_url.rstrip('/')}/api/environments/{env_id}/projects/{project_id}" + 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", {})