diff --git a/bot.py b/bot.py index f83a857..5974b66 100644 --- a/bot.py +++ b/bot.py @@ -11,6 +11,7 @@ import asyncio import json import os import logging +import sys from datetime import datetime, timezone from pathlib import Path @@ -57,6 +58,7 @@ class StatusBot(commands.Bot): self.targets: dict[str, dict] = cfg.get("guilds", {}) self._api_runner: web.AppRunner | None = None + self._agent_proc: asyncio.subprocess.Process | None = None # ── Lifecycle ───────────────────────────────────────────────────────────── @@ -66,6 +68,16 @@ class StatusBot(commands.Bot): self.update_loop.start() log.info("Services de monitoring démarrés.") + # Automation : Lancement de l'agent en tâche de fond + try: + self._agent_proc = await asyncio.create_subprocess_exec( + sys.executable, "agent.py", + stdout=None, stderr=None # L'agent loggera dans son propre flux si besoin + ) + log.info(f"✅ Agent lancé automatiquement (PID {self._agent_proc.pid})") + except Exception as e: + log.error(f"❌ Impossible de lancer l'agent automatiquement : {e}") + await self.tree.sync() log.info("Slash commands synced.") @@ -75,6 +87,23 @@ class StatusBot(commands.Bot): activity=discord.Activity(type=discord.ActivityType.watching, name="l'état du serveur") ) + async def close(self): + """Action à la fermeture du bot (propre).""" + if self._agent_proc: + log.info("Arrêt de l'agent...") + try: + self._agent_proc.terminate() + await self._agent_proc.wait() + except Exception: + pass + + if self._api_runner: + log.info("Arrêt du serveur API HTTP...") + if hasattr(self, "_api_site") and self._api_site: + await self._api_site.stop() + await self._api_runner.cleanup() + await super().close() + # ── API HTTP (réception métriques) ──────────────────────────────────────── async def _start_api_server(self): @@ -82,10 +111,22 @@ class StatusBot(commands.Bot): app.router.add_post("/metrics", self._handle_metrics) runner = web.AppRunner(app) await runner.setup() - site = web.TCPSite(runner, "0.0.0.0", self.cfg.get("api_port", 8765)) - await site.start() - self._api_runner = runner - log.info(f"API HTTP en écoute sur le port {self.cfg.get('api_port', 8765)}") + port = self.cfg.get("api_port", 8765) + site = web.TCPSite(runner, "0.0.0.0", port) + + try: + await site.start() + self._api_runner = runner + self._api_site = site + log.info(f"✅ API HTTP en écoute sur le port {port}") + except OSError as e: + if e.errno == 98: + log.critical(f"❌ IMPOSSIBLE DE DÉMARRER L'API : Le port {port} est déjà utilisé.") + log.critical("Vérifiez qu'une autre instance du bot n'est pas déjà lancée.") + else: + log.critical(f"❌ Erreur lors du démarrage de l'API : {e}") + # On peut choisir de fermer le bot ici si l'API est critique + await self.close() async def _handle_metrics(self, request: web.Request) -> web.Response: try: @@ -161,79 +202,69 @@ class StatusBot(commands.Bot): def _build_embed(self, r: AnalysisResult) -> discord.Embed: COLOR = { - STATUS_OK: 0x2ECC71, # vert - STATUS_WARNING: 0xF39C12, # orange - STATUS_CRITICAL: 0xE74C3C, # rouge - } - - TITLE = { - STATUS_OK: "🟢 Serveur — Tout est normal", - STATUS_WARNING: "🟡 Serveur — Charge élevée", - STATUS_CRITICAL: "🔴 Serveur — Surchargé", + STATUS_OK: 0x00FF9D, # Vert Matrix / Cyberpunk + STATUS_WARNING: 0xFFB300, # Or + STATUS_CRITICAL: 0xFF3D00, # Rouge Vif } + # Embed Principal embed = discord.Embed( - title=TITLE[r.status], color=COLOR[r.status], timestamp=datetime.now(timezone.utc), ) - embed.set_footer(text="Dernière mise à jour") - # ── Ressources globales ── - cpu_bar = self._bar(r.cpu, 100) - ram_bar = self._bar(r.ram_pct, 100) - swap_bar = self._bar(r.swap_pct, 100) - - temp_str = f" 🌡️ {r.cpu_temp}°C" if r.cpu_temp else "" - - resources = ( - f"`CPU ` {cpu_bar} **{r.cpu:.0f}%**{temp_str}\n" - f"`RAM ` {ram_bar} **{r.ram_pct:.0f}%** " - f"({r.ram_used_gb:.1f}/{r.ram_total_gb:.1f} GB)\n" + # En-tête premium + status_text = { + STATUS_OK: "OPÉRATIONNEL", + STATUS_WARNING: "CHARGE ÉLEVÉE", + STATUS_CRITICAL: "SURCHARGE SYSTÈME", + } + embed.set_author( + name=f"STATUT DU SERVEUR — {status_text[r.status]}", + icon_url="https://cdn-icons-png.flaticon.com/512/689/689307.png" # Icone de serveur ) - if r.swap_pct > 5: - resources += f"`SWAP` {swap_bar} **{r.swap_pct:.0f}%**\n" + embed.set_footer(text="Système de monitoring Bêta • Temps réel") - resources += ( - f"\n📈 Charge (1m / 5m / 15m) : " - f"`{r.load_avg[0]:.2f}` / `{r.load_avg[1]:.2f}` / `{r.load_avg[2]:.2f}`" + # ── Ressources Système (Colonne 1) ── + cpu_bar = self._bar(r.cpu, 100, length=8) + ram_bar = self._bar(r.ram_pct, 100, length=8) + + temp_str = f" • 🌡️ `{r.cpu_temp}°C`" if r.cpu_temp else "" + + load_str = f"`{r.load_avg[0]:.2f}` | `{r.load_avg[1]:.2f}` | `{r.load_avg[2]:.2f}`" + + res_val = ( + f"**CPU** {cpu_bar} **{r.cpu:.0f}%**{temp_str}\n" + f"**RAM** {ram_bar} **{r.ram_pct:.0f}%**\n" + f"┗ `{r.ram_used_gb:.1f} / {r.ram_total_gb:.1f} GB`" ) + embed.add_field(name="🚀 Ressources", value=res_val, inline=True) + embed.add_field(name="📈 Charge Load", value=f"\n{load_str}", inline=True) - embed.add_field(name="📊 Ressources", value=resources, inline=False) - - # ── Services ── - running = [s for s in r.services if s.running] - stopped = [s for s in r.services if not s.running] - + # ── Services Actifs (Section Large) ── + running = [s for s in r.services if s.running] if running: - lines = [] + svc_lines = [] for svc in running: - cpu_indicator = "" - if svc.cpu >= 40: - cpu_indicator = " 🔥" - elif svc.cpu >= 20: - cpu_indicator = " ⚡" - lines.append( - f"▸ {svc.name}{cpu_indicator} — " - f"CPU **{svc.cpu:.0f}%** | RAM **{svc.ram_gb:.1f} GB**" + icon = "🔥" if svc.cpu >= 40 else ("⚡" if svc.cpu >= 20 else "🔹") + svc_lines.append( + f"{icon} **{svc.name}**\n" + f"╚ `CPU {svc.cpu:2.0f}%` `RAM {svc.ram_gb:4.1f} GB`" ) embed.add_field( - name="🎮 Services actifs", - value="\n".join(lines), + name="🎮 Services Actifs", + value="\n".join(svc_lines), inline=False ) - if stopped: - stopped_names = " • ".join(s.name for s in stopped) - embed.add_field(name="💤 Services arrêtés", value=stopped_names, inline=False) - - # ── Cause ── + # ── Diagnostics & Analyse ── if r.status != STATUS_OK: - cause_text = "\n".join(r.cause_lines) - embed.add_field(name="🔍 Pourquoi ça rame ?", value=cause_text, inline=False) - - # ── ETA ── - embed.add_field(name="⏱️ Retour à la normale", value=r.eta_text, inline=False) + diag_val = "\n".join([f"• {line}" for line in r.cause_lines]) + embed.add_field(name="🔍 Analyse Diagnostic", value=f"```\n{diag_val}\n```", inline=False) + + # ── ETA & Footer ── + eta_icon = "🟢" if r.status == STATUS_OK else "⏳" + embed.add_field(name="⏱️ Prévision", value=f"{eta_icon} {r.eta_text}", inline=False) return embed