""" Service de gestion de l'historique des commandes ad-hoc. """ import uuid from datetime import datetime, timezone from typing import List, Optional from sqlalchemy import select, update, delete from sqlalchemy.ext.asyncio import AsyncSession from app.models.database import async_session_maker from app.schemas.ansible import AdHocHistoryEntry, AdHocHistoryCategory from app.core.constants import DEFAULT_ADHOC_CATEGORIES class AdHocHistoryService: """Service pour gérer l'historique des commandes ad-hoc en base de données.""" def __init__(self): self._default_categories_initialized = False async def _ensure_default_categories(self, session: AsyncSession): """S'assure que les catégories par défaut existent.""" if self._default_categories_initialized: return from app.crud.log import LogRepository repo = LogRepository(session) # Vérifier si des catégories existent déjà existing = await self.get_categories() if not existing: # Créer les catégories par défaut for cat in DEFAULT_ADHOC_CATEGORIES: await self._create_category_internal( session, name=cat["name"], description=cat.get("description"), color=cat.get("color", "#7c3aed"), icon=cat.get("icon", "fa-folder") ) await session.commit() self._default_categories_initialized = True async def _create_category_internal( self, session: AsyncSession, name: str, description: str = None, color: str = "#7c3aed", icon: str = "fa-folder" ): """Crée une catégorie en base (interne).""" from app.models.log import Log # Utiliser le modèle Log avec un type spécial pour stocker les catégories log = Log( level="ADHOC_CATEGORY", message=name, source=description or "", host_id=None, task_id=f"{color}|{icon}", # Stocker color et icon dans task_id ) session.add(log) async def add_command( self, command: str, target: str, module: str = "shell", become: bool = False, category: str = "default", description: str = None ) -> AdHocHistoryEntry: """Ajoute une commande à l'historique.""" async with async_session_maker() as session: await self._ensure_default_categories(session) from app.models.log import Log cmd_id = f"adhoc_{uuid.uuid4().hex[:12]}" log = Log( level="ADHOC_COMMAND", message=command, source=category, host_id=target, task_id=cmd_id, ) session.add(log) await session.commit() return AdHocHistoryEntry( id=cmd_id, command=command, target=target, module=module, become=become, category=category, description=description, created_at=datetime.now(timezone.utc), last_used=datetime.now(timezone.utc), use_count=1 ) async def get_commands( self, category: str = None, search: str = None, limit: int = 50 ) -> List[AdHocHistoryEntry]: """Récupère les commandes de l'historique.""" async with async_session_maker() as session: from app.models.log import Log stmt = select(Log).where(Log.level == "ADHOC_COMMAND") if category and category != "all": stmt = stmt.where(Log.source == category) stmt = stmt.order_by(Log.created_at.desc()).limit(limit) result = await session.execute(stmt) logs = result.scalars().all() commands = [] for log in logs: if search and search.lower() not in log.message.lower(): continue commands.append(AdHocHistoryEntry( id=log.task_id or str(log.id), command=log.message, target=log.host_id or "all", module="shell", become=False, category=log.source or "default", created_at=log.created_at, last_used=log.created_at, use_count=1 )) return commands async def update_command_category( self, command_id: str, category: str, description: str = None ) -> bool: """Met à jour la catégorie d'une commande.""" async with async_session_maker() as session: from app.models.log import Log stmt = ( update(Log) .where(Log.task_id == command_id) .where(Log.level == "ADHOC_COMMAND") .values(source=category) ) result = await session.execute(stmt) await session.commit() return result.rowcount > 0 async def delete_command(self, command_id: str) -> bool: """Supprime une commande de l'historique.""" async with async_session_maker() as session: from app.models.log import Log stmt = ( delete(Log) .where(Log.task_id == command_id) .where(Log.level == "ADHOC_COMMAND") ) result = await session.execute(stmt) await session.commit() return result.rowcount > 0 async def get_categories(self) -> List[AdHocHistoryCategory]: """Récupère la liste des catégories.""" async with async_session_maker() as session: from app.models.log import Log stmt = select(Log).where(Log.level == "ADHOC_CATEGORY") result = await session.execute(stmt) logs = result.scalars().all() if not logs: # Retourner les catégories par défaut return [ AdHocHistoryCategory( name=cat["name"], description=cat.get("description"), color=cat.get("color", "#7c3aed"), icon=cat.get("icon", "fa-folder") ) for cat in DEFAULT_ADHOC_CATEGORIES ] categories = [] for log in logs: # Extraire color et icon depuis task_id color, icon = "#7c3aed", "fa-folder" if log.task_id and "|" in log.task_id: parts = log.task_id.split("|", 1) color = parts[0] icon = parts[1] if len(parts) > 1 else "fa-folder" categories.append(AdHocHistoryCategory( name=log.message, description=log.source or None, color=color, icon=icon )) return categories async def add_category( self, name: str, description: str = None, color: str = "#7c3aed", icon: str = "fa-folder" ) -> AdHocHistoryCategory: """Ajoute une nouvelle catégorie.""" async with async_session_maker() as session: await self._create_category_internal(session, name, description, color, icon) await session.commit() return AdHocHistoryCategory( name=name, description=description, color=color, icon=icon ) async def update_category( self, old_name: str, new_name: str, description: str = None, color: str = "#7c3aed", icon: str = "fa-folder" ) -> bool: """Met à jour une catégorie existante.""" async with async_session_maker() as session: from app.models.log import Log # Mettre à jour la catégorie stmt = ( update(Log) .where(Log.message == old_name) .where(Log.level == "ADHOC_CATEGORY") .values( message=new_name, source=description or "", task_id=f"{color}|{icon}" ) ) result = await session.execute(stmt) # Mettre à jour les commandes associées si le nom a changé if old_name != new_name: stmt2 = ( update(Log) .where(Log.source == old_name) .where(Log.level == "ADHOC_COMMAND") .values(source=new_name) ) await session.execute(stmt2) await session.commit() return result.rowcount > 0 async def delete_category(self, name: str) -> bool: """Supprime une catégorie et déplace ses commandes vers 'default'.""" if name == "default": return False async with async_session_maker() as session: from app.models.log import Log # Déplacer les commandes vers default stmt1 = ( update(Log) .where(Log.source == name) .where(Log.level == "ADHOC_COMMAND") .values(source="default") ) await session.execute(stmt1) # Supprimer la catégorie stmt2 = ( delete(Log) .where(Log.message == name) .where(Log.level == "ADHOC_CATEGORY") ) result = await session.execute(stmt2) await session.commit() return result.rowcount > 0 # Instance singleton du service adhoc_history_service = AdHocHistoryService()